2016-02-05 13:40:41 +01:00
|
|
|
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
|
|
|
// This file is part of Parity.
|
|
|
|
|
|
|
|
// Parity is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
|
|
|
// Parity is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2016-01-27 17:18:38 +01:00
|
|
|
//! Ethcore client application.
|
|
|
|
|
|
|
|
#![warn(missing_docs)]
|
2016-01-23 23:53:20 +01:00
|
|
|
#![feature(plugin)]
|
2016-02-12 22:01:23 +01:00
|
|
|
#![cfg_attr(feature="dev", plugin(clippy))]
|
2016-01-23 23:53:20 +01:00
|
|
|
extern crate docopt;
|
|
|
|
extern crate rustc_serialize;
|
2015-12-22 22:19:50 +01:00
|
|
|
extern crate ethcore_util as util;
|
2016-01-07 16:08:12 +01:00
|
|
|
extern crate ethcore;
|
2016-01-29 15:56:06 +01:00
|
|
|
extern crate ethsync;
|
2016-02-18 12:42:01 +01:00
|
|
|
#[macro_use]
|
2016-01-31 11:08:04 +01:00
|
|
|
extern crate log as rlog;
|
2016-01-09 23:21:57 +01:00
|
|
|
extern crate env_logger;
|
2016-01-22 00:11:19 +01:00
|
|
|
extern crate ctrlc;
|
2016-02-05 13:49:36 +01:00
|
|
|
extern crate fdlimit;
|
2016-02-09 15:51:48 +01:00
|
|
|
extern crate target_info;
|
2016-02-18 13:10:04 +01:00
|
|
|
extern crate daemonize;
|
2015-12-22 22:19:50 +01:00
|
|
|
|
2016-01-20 04:19:38 +01:00
|
|
|
#[cfg(feature = "rpc")]
|
2016-01-26 13:14:22 +01:00
|
|
|
extern crate ethcore_rpc as rpc;
|
2016-01-20 04:19:38 +01:00
|
|
|
|
2016-02-08 15:04:12 +01:00
|
|
|
use std::net::{SocketAddr};
|
2016-01-14 22:38:49 +01:00
|
|
|
use std::env;
|
2016-01-31 11:08:04 +01:00
|
|
|
use rlog::{LogLevelFilter};
|
2016-01-14 22:38:49 +01:00
|
|
|
use env_logger::LogBuilder;
|
2016-01-22 00:11:19 +01:00
|
|
|
use ctrlc::CtrlC;
|
2016-01-16 13:30:27 +01:00
|
|
|
use util::*;
|
2016-02-10 12:50:27 +01:00
|
|
|
use util::panics::MayPanic;
|
2016-02-09 15:51:48 +01:00
|
|
|
use ethcore::spec::*;
|
2016-01-16 13:30:27 +01:00
|
|
|
use ethcore::client::*;
|
2016-01-21 23:33:52 +01:00
|
|
|
use ethcore::service::{ClientService, NetSyncMessage};
|
2016-01-09 18:50:45 +01:00
|
|
|
use ethcore::ethereum;
|
2016-01-18 23:23:32 +01:00
|
|
|
use ethcore::blockchain::CacheSize;
|
2016-01-29 15:56:06 +01:00
|
|
|
use ethsync::EthSync;
|
2016-02-18 14:16:55 +01:00
|
|
|
use docopt::Docopt;
|
2016-02-09 15:51:48 +01:00
|
|
|
use target_info::Target;
|
2016-02-18 14:16:55 +01:00
|
|
|
use daemonize::Daemonize;
|
2016-01-23 23:53:20 +01:00
|
|
|
|
2016-02-18 14:16:55 +01:00
|
|
|
const USAGE: &'static str = "
|
2016-01-23 23:53:20 +01:00
|
|
|
Parity. Ethereum Client.
|
2016-02-09 15:51:48 +01:00
|
|
|
By Wood/Paronyan/Kotewicz/Drwięga/Volf.
|
|
|
|
Copyright 2015, 2016 Ethcore (UK) Limited
|
2016-01-23 23:53:20 +01:00
|
|
|
|
|
|
|
Usage:
|
2016-02-18 14:28:24 +01:00
|
|
|
parity daemon <pid-file> [options] [ --no-bootstrap | <enode>... ]
|
2016-02-11 10:46:55 +01:00
|
|
|
parity [options] [ --no-bootstrap | <enode>... ]
|
2016-01-23 23:53:20 +01:00
|
|
|
|
|
|
|
Options:
|
2016-02-09 15:51:48 +01:00
|
|
|
--chain CHAIN Specify the blockchain type. CHAIN may be either a JSON chain specification file
|
2016-02-18 13:56:15 +01:00
|
|
|
or frontier, mainnet, morden, or testnet [default: frontier].
|
2016-02-10 21:17:47 +01:00
|
|
|
-d --db-path PATH Specify the database & configuration directory path [default: $HOME/.parity]
|
2016-02-18 12:42:01 +01:00
|
|
|
--keys-path PATH Specify the path for JSON key files to be found [default: $HOME/.web3/keys]
|
2016-02-02 01:59:14 +01:00
|
|
|
|
2016-02-11 10:46:55 +01:00
|
|
|
--no-bootstrap Don't bother trying to connect to any nodes initially.
|
2016-02-08 15:04:12 +01:00
|
|
|
--listen-address URL Specify the IP/port on which to listen for peers [default: 0.0.0.0:30304].
|
|
|
|
--public-address URL Specify the IP/port on which peers may connect [default: 0.0.0.0:30304].
|
|
|
|
--address URL Equivalent to --listen-address URL --public-address URL.
|
2016-02-10 16:45:54 +01:00
|
|
|
--upnp Use UPnP to try to figure out the correct network settings.
|
2016-02-12 00:24:10 +01:00
|
|
|
--node-key KEY Specify node secret key as hex string.
|
2016-02-08 15:04:12 +01:00
|
|
|
|
2016-02-04 00:48:36 +01:00
|
|
|
--cache-pref-size BYTES Specify the prefered size of the blockchain cache in bytes [default: 16384].
|
|
|
|
--cache-max-size BYTES Specify the maximum size of the blockchain cache in bytes [default: 262144].
|
2016-02-02 01:59:14 +01:00
|
|
|
|
2016-02-09 15:51:48 +01:00
|
|
|
-j --jsonrpc Enable the JSON-RPC API sever.
|
|
|
|
--jsonrpc-url URL Specify URL for JSON-RPC API server [default: 127.0.0.1:8545].
|
|
|
|
|
|
|
|
-l --logging LOGGING Specify the logging level.
|
|
|
|
-v --version Show information about version.
|
2016-02-02 01:59:14 +01:00
|
|
|
-h --help Show this screen.
|
2016-02-18 14:16:55 +01:00
|
|
|
";
|
|
|
|
|
|
|
|
#[derive(Debug, RustcDecodable)]
|
|
|
|
struct Args {
|
|
|
|
cmd_daemon: bool,
|
|
|
|
arg_pid_file: String,
|
|
|
|
arg_enode: Vec<String>,
|
|
|
|
flag_chain: String,
|
|
|
|
flag_db_path: String,
|
|
|
|
flag_keys_path: String,
|
|
|
|
flag_no_bootstrap: bool,
|
|
|
|
flag_listen_address: String,
|
|
|
|
flag_public_address: String,
|
|
|
|
flag_address: Option<String>,
|
|
|
|
flag_upnp: bool,
|
|
|
|
flag_node_key: Option<String>,
|
|
|
|
flag_cache_pref_size: usize,
|
|
|
|
flag_cache_max_size: usize,
|
|
|
|
flag_jsonrpc: bool,
|
|
|
|
flag_jsonrpc_url: String,
|
|
|
|
flag_logging: Option<String>,
|
|
|
|
flag_version: bool,
|
|
|
|
}
|
2015-12-22 22:19:50 +01:00
|
|
|
|
2016-02-18 14:16:55 +01:00
|
|
|
fn setup_log(init: &Option<String>) {
|
2016-01-14 22:38:49 +01:00
|
|
|
let mut builder = LogBuilder::new();
|
2016-01-15 01:44:57 +01:00
|
|
|
builder.filter(None, LogLevelFilter::Info);
|
2016-01-14 22:38:49 +01:00
|
|
|
|
|
|
|
if env::var("RUST_LOG").is_ok() {
|
|
|
|
builder.parse(&env::var("RUST_LOG").unwrap());
|
|
|
|
}
|
|
|
|
|
2016-02-18 14:16:55 +01:00
|
|
|
if let Some(ref s) = *init {
|
|
|
|
builder.parse(s);
|
|
|
|
}
|
2016-01-23 23:53:20 +01:00
|
|
|
|
2016-01-14 22:38:49 +01:00
|
|
|
builder.init().unwrap();
|
|
|
|
}
|
|
|
|
|
2016-01-20 04:19:38 +01:00
|
|
|
#[cfg(feature = "rpc")]
|
2016-02-02 01:59:14 +01:00
|
|
|
fn setup_rpc_server(client: Arc<Client>, sync: Arc<EthSync>, url: &str) {
|
2016-01-27 17:14:41 +01:00
|
|
|
use rpc::v1::*;
|
2016-02-10 12:50:27 +01:00
|
|
|
|
2016-01-27 17:18:38 +01:00
|
|
|
let mut server = rpc::HttpServer::new(1);
|
2016-01-21 00:54:19 +01:00
|
|
|
server.add_delegate(Web3Client::new().to_delegate());
|
2016-02-10 16:28:59 +01:00
|
|
|
server.add_delegate(EthClient::new(client.clone(), sync.clone()).to_delegate());
|
2016-01-21 11:25:39 +01:00
|
|
|
server.add_delegate(EthFilterClient::new(client).to_delegate());
|
2016-01-28 10:54:49 +01:00
|
|
|
server.add_delegate(NetClient::new(sync).to_delegate());
|
2016-02-02 01:59:14 +01:00
|
|
|
server.start_async(url);
|
2016-01-20 04:19:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(feature = "rpc"))]
|
2016-02-02 01:59:14 +01:00
|
|
|
fn setup_rpc_server(_client: Arc<Client>, _sync: Arc<EthSync>, _url: &str) {
|
2016-01-20 04:19:38 +01:00
|
|
|
}
|
|
|
|
|
2016-02-10 18:11:10 +01:00
|
|
|
fn print_version() {
|
|
|
|
println!("\
|
2016-02-09 15:51:48 +01:00
|
|
|
Parity version {} ({}-{}-{})
|
|
|
|
Copyright 2015, 2016 Ethcore (UK) Limited
|
|
|
|
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
|
|
|
|
This is free software: you are free to change and redistribute it.
|
|
|
|
There is NO WARRANTY, to the extent permitted by law.
|
|
|
|
|
2016-02-09 17:23:25 +01:00
|
|
|
By Wood/Paronyan/Kotewicz/Drwięga/Volf.\
|
2016-02-09 15:51:48 +01:00
|
|
|
", env!("CARGO_PKG_VERSION"), Target::arch(), Target::env(), Target::os());
|
2016-02-10 18:11:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
struct Configuration {
|
|
|
|
args: Args
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Configuration {
|
|
|
|
fn parse() -> Self {
|
|
|
|
Configuration {
|
2016-02-18 14:16:55 +01:00
|
|
|
args: Docopt::new(USAGE).and_then(|d| d.decode()).unwrap_or_else(|e| e.exit()),
|
2016-02-10 18:11:10 +01:00
|
|
|
}
|
2016-02-10 13:13:04 +01:00
|
|
|
}
|
|
|
|
|
2016-02-10 21:17:47 +01:00
|
|
|
fn path(&self) -> String {
|
|
|
|
self.args.flag_db_path.replace("$HOME", env::home_dir().unwrap().to_str().unwrap())
|
|
|
|
}
|
|
|
|
|
2016-02-18 12:42:01 +01:00
|
|
|
fn keys_path(&self) -> String {
|
|
|
|
self.args.flag_keys_path.replace("$HOME", env::home_dir().unwrap().to_str().unwrap())
|
|
|
|
}
|
|
|
|
|
2016-02-10 21:17:47 +01:00
|
|
|
fn spec(&self) -> Spec {
|
2016-02-10 14:16:42 +01:00
|
|
|
match self.args.flag_chain.as_ref() {
|
2016-02-10 13:13:04 +01:00
|
|
|
"frontier" | "mainnet" => ethereum::new_frontier(),
|
|
|
|
"morden" | "testnet" => ethereum::new_morden(),
|
|
|
|
"olympic" => ethereum::new_olympic(),
|
|
|
|
f => Spec::from_json_utf8(contents(f).expect("Couldn't read chain specification file. Sure it exists?").as_ref()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-10 21:17:47 +01:00
|
|
|
fn init_nodes(&self, spec: &Spec) -> Vec<String> {
|
2016-02-11 10:46:55 +01:00
|
|
|
if self.args.flag_no_bootstrap { Vec::new() } else {
|
|
|
|
match self.args.arg_enode.len() {
|
|
|
|
0 => spec.nodes().clone(),
|
2016-02-18 14:28:24 +01:00
|
|
|
_ => self.args.arg_enode.clone(), // TODO check format first.
|
2016-02-11 10:46:55 +01:00
|
|
|
}
|
2016-02-10 12:50:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-10 21:17:47 +01:00
|
|
|
fn net_addresses(&self) -> (SocketAddr, SocketAddr) {
|
2016-02-10 12:50:27 +01:00
|
|
|
let listen_address;
|
|
|
|
let public_address;
|
|
|
|
|
|
|
|
match self.args.flag_address {
|
|
|
|
None => {
|
|
|
|
listen_address = SocketAddr::from_str(self.args.flag_listen_address.as_ref()).expect("Invalid listen address given with --listen-address");
|
|
|
|
public_address = SocketAddr::from_str(self.args.flag_public_address.as_ref()).expect("Invalid public address given with --public-address");
|
|
|
|
}
|
|
|
|
Some(ref a) => {
|
|
|
|
public_address = SocketAddr::from_str(a.as_ref()).expect("Invalid listen/public address given with --address");
|
2016-02-15 00:51:50 +01:00
|
|
|
listen_address = public_address;
|
2016-02-10 12:50:27 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
(listen_address, public_address)
|
|
|
|
}
|
2016-02-18 12:42:01 +01:00
|
|
|
|
|
|
|
fn execute(&self) {
|
|
|
|
if self.args.flag_version {
|
|
|
|
print_version();
|
|
|
|
return;
|
|
|
|
}
|
2016-02-18 13:10:04 +01:00
|
|
|
if self.args.cmd_daemon {
|
2016-02-18 13:54:18 +01:00
|
|
|
let daemonize = Daemonize::new().pid_file(self.args.arg_pid_file.clone()).chown_pid_file(true);
|
|
|
|
match daemonize.start() {
|
|
|
|
Ok(_) => info!("Daemonized"),
|
|
|
|
Err(e) => { error!("{}", e); return; },
|
|
|
|
}
|
2016-02-18 13:10:04 +01:00
|
|
|
}
|
2016-02-18 12:42:01 +01:00
|
|
|
self.execute_client();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn execute_client(&self) {
|
|
|
|
// Setup logging
|
|
|
|
setup_log(&self.args.flag_logging);
|
|
|
|
// Raise fdlimit
|
|
|
|
unsafe { ::fdlimit::raise_fd_limit(); }
|
|
|
|
|
|
|
|
let spec = self.spec();
|
|
|
|
|
|
|
|
// Configure network
|
|
|
|
let mut net_settings = NetworkConfiguration::new();
|
|
|
|
net_settings.nat_enabled = self.args.flag_upnp;
|
|
|
|
net_settings.boot_nodes = self.init_nodes(&spec);
|
|
|
|
let (listen, public) = self.net_addresses();
|
|
|
|
net_settings.listen_address = listen;
|
|
|
|
net_settings.public_address = public;
|
|
|
|
net_settings.use_secret = self.args.flag_node_key.as_ref().map(|s| Secret::from_str(&s).expect("Invalid key string"));
|
|
|
|
|
|
|
|
// Build client
|
|
|
|
let mut service = ClientService::start(spec, net_settings, &Path::new(&self.path())).unwrap();
|
|
|
|
let client = service.client().clone();
|
|
|
|
client.configure_cache(self.args.flag_cache_pref_size, self.args.flag_cache_max_size);
|
|
|
|
|
|
|
|
// Sync
|
|
|
|
let sync = EthSync::register(service.network(), client);
|
|
|
|
|
|
|
|
// Setup rpc
|
|
|
|
if self.args.flag_jsonrpc {
|
|
|
|
setup_rpc_server(service.client(), sync.clone(), &self.args.flag_jsonrpc_url);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register IO handler
|
|
|
|
let io_handler = Arc::new(ClientIoHandler {
|
|
|
|
client: service.client(),
|
|
|
|
info: Default::default(),
|
|
|
|
sync: sync
|
|
|
|
});
|
|
|
|
service.io().register_handler(io_handler).expect("Error registering IO handler");
|
|
|
|
|
|
|
|
// Handle exit
|
|
|
|
wait_for_exit(&service);
|
|
|
|
}
|
2016-02-10 12:50:27 +01:00
|
|
|
}
|
|
|
|
|
2016-02-10 16:35:52 +01:00
|
|
|
fn wait_for_exit(client_service: &ClientService) {
|
2016-02-10 12:50:27 +01:00
|
|
|
let exit = Arc::new(Condvar::new());
|
|
|
|
// Handle possible exits
|
|
|
|
let e = exit.clone();
|
|
|
|
CtrlC::set_handler(move || { e.notify_all(); });
|
|
|
|
let e = exit.clone();
|
2016-02-10 16:35:52 +01:00
|
|
|
client_service.on_panic(move |_reason| { e.notify_all(); });
|
2016-02-10 12:50:27 +01:00
|
|
|
// Wait for signal
|
|
|
|
let mutex = Mutex::new(());
|
|
|
|
let _ = exit.wait(mutex.lock().unwrap()).unwrap();
|
|
|
|
}
|
|
|
|
|
2015-12-22 22:19:50 +01:00
|
|
|
fn main() {
|
2016-02-18 12:42:01 +01:00
|
|
|
Configuration::parse().execute();
|
2015-12-22 22:19:50 +01:00
|
|
|
}
|
|
|
|
|
2016-01-18 23:23:32 +01:00
|
|
|
struct Informant {
|
2016-01-21 17:21:51 +01:00
|
|
|
chain_info: RwLock<Option<BlockChainInfo>>,
|
|
|
|
cache_info: RwLock<Option<CacheSize>>,
|
|
|
|
report: RwLock<Option<ClientReport>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Informant {
|
|
|
|
fn default() -> Self {
|
|
|
|
Informant {
|
|
|
|
chain_info: RwLock::new(None),
|
|
|
|
cache_info: RwLock::new(None),
|
|
|
|
report: RwLock::new(None),
|
|
|
|
}
|
|
|
|
}
|
2016-01-18 23:23:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Informant {
|
2016-01-22 04:54:38 +01:00
|
|
|
pub fn tick(&self, client: &Client, sync: &EthSync) {
|
2016-01-18 23:23:32 +01:00
|
|
|
// 5 seconds betwen calls. TODO: calculate this properly.
|
|
|
|
let dur = 5usize;
|
|
|
|
|
|
|
|
let chain_info = client.chain_info();
|
2016-01-22 04:54:38 +01:00
|
|
|
let queue_info = client.queue_info();
|
2016-01-18 23:23:32 +01:00
|
|
|
let cache_info = client.cache_info();
|
|
|
|
let report = client.report();
|
2016-01-22 04:54:38 +01:00
|
|
|
let sync_info = sync.status();
|
2016-01-18 23:23:32 +01:00
|
|
|
|
2016-01-21 17:21:51 +01:00
|
|
|
if let (_, &Some(ref last_cache_info), &Some(ref last_report)) = (self.chain_info.read().unwrap().deref(), self.cache_info.read().unwrap().deref(), self.report.read().unwrap().deref()) {
|
2016-02-11 22:14:06 +01:00
|
|
|
println!("[ #{} {} ]---[ {} blk/s | {} tx/s | {} gas/s //··· {}/{} peers, #{}, {}+{} queued ···// {} ({}) bl {} ({}) ex ]",
|
2016-01-18 23:23:32 +01:00
|
|
|
chain_info.best_block_number,
|
|
|
|
chain_info.best_block_hash,
|
|
|
|
(report.blocks_imported - last_report.blocks_imported) / dur,
|
|
|
|
(report.transactions_applied - last_report.transactions_applied) / dur,
|
|
|
|
(report.gas_processed - last_report.gas_processed) / From::from(dur),
|
2016-01-22 04:54:38 +01:00
|
|
|
|
|
|
|
sync_info.num_active_peers,
|
|
|
|
sync_info.num_peers,
|
2016-02-11 22:14:06 +01:00
|
|
|
sync_info.last_imported_block_number.unwrap_or(chain_info.best_block_number),
|
2016-01-22 15:58:52 +01:00
|
|
|
queue_info.unverified_queue_size,
|
|
|
|
queue_info.verified_queue_size,
|
2016-01-22 04:54:38 +01:00
|
|
|
|
2016-01-18 23:23:32 +01:00
|
|
|
cache_info.blocks,
|
|
|
|
cache_info.blocks as isize - last_cache_info.blocks as isize,
|
|
|
|
cache_info.block_details,
|
|
|
|
cache_info.block_details as isize - last_cache_info.block_details as isize
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-01-21 17:21:51 +01:00
|
|
|
*self.chain_info.write().unwrap().deref_mut() = Some(chain_info);
|
|
|
|
*self.cache_info.write().unwrap().deref_mut() = Some(cache_info);
|
|
|
|
*self.report.write().unwrap().deref_mut() = Some(report);
|
2016-01-18 23:23:32 +01:00
|
|
|
}
|
|
|
|
}
|
2016-01-16 13:30:27 +01:00
|
|
|
|
2016-01-21 17:21:51 +01:00
|
|
|
const INFO_TIMER: TimerToken = 0;
|
|
|
|
|
2016-01-16 13:30:27 +01:00
|
|
|
struct ClientIoHandler {
|
2016-01-21 23:33:52 +01:00
|
|
|
client: Arc<Client>,
|
2016-01-22 04:54:38 +01:00
|
|
|
sync: Arc<EthSync>,
|
2016-01-18 23:23:32 +01:00
|
|
|
info: Informant,
|
2016-01-16 13:30:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl IoHandler<NetSyncMessage> for ClientIoHandler {
|
2016-02-10 12:50:27 +01:00
|
|
|
fn initialize(&self, io: &IoContext<NetSyncMessage>) {
|
2016-01-21 17:21:51 +01:00
|
|
|
io.register_timer(INFO_TIMER, 5000).expect("Error registering timer");
|
2016-01-16 13:30:27 +01:00
|
|
|
}
|
|
|
|
|
2016-01-21 17:21:51 +01:00
|
|
|
fn timeout(&self, _io: &IoContext<NetSyncMessage>, timer: TimerToken) {
|
|
|
|
if INFO_TIMER == timer {
|
2016-01-22 04:54:38 +01:00
|
|
|
self.info.tick(&self.client, &self.sync);
|
2016-01-16 13:30:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-02 02:04:03 +01:00
|
|
|
/// Parity needs at least 1 test to generate coverage reports correctly.
|
|
|
|
#[test]
|
|
|
|
fn if_works() {
|
|
|
|
}
|