Fix 5560: add support for a new staking-miner info command (#5577)

* Refactoring opts out

* Implement info command

fix #5560

* remove useless change

* Remove unnecessary brackets

* Fix and add tests

* Promote the uri flag to global

* Ignore lint identity ops

* Reverse adding #[allow(identity_op)]

* Add cli test for the info command

* Add licende headers and fix some grumbles

* Add retrieval of the linked version and make the json output optional

* Fix tests

* Keep it generic and renamed builtin into linked

* Rebase master

* Add runtimes compatibility information

* Silence erroneous warning about unsafe

* Fix spellcheck

* Update utils/staking-miner/src/runtime_versions.rs

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>
This commit is contained in:
Chevdor
2022-06-25 16:46:18 +02:00
committed by GitHub
parent bda05cecaf
commit 700f19e2ed
7 changed files with 459 additions and 265 deletions
+1 -1
View File
@@ -43,4 +43,4 @@ westend-runtime = { path = "../../runtime/westend" }
sub-tokens = { git = "https://github.com/paritytech/substrate-debug-kit", branch = "master" }
[dev-dependencies]
assert_cmd = "2.0.2"
assert_cmd = "2.0.4"
+1 -1
View File
@@ -28,7 +28,7 @@ There are 2 options to build a staking-miner Docker image:
### Building the injected image
First build the binary as documented [above](#building).
You may then inject the binary into a Docker base image usingfrom the root of the Polkadot repository:
You may then inject the binary into a Docker base image from the root of the Polkadot repository:
```
docker build -t staking-miner -f scripts/ci/dockerfiles/staking-miner/staking-miner_injected.Dockerfile target/release
```
+1 -1
View File
@@ -16,7 +16,7 @@
//! The dry-run command.
use crate::{prelude::*, rpc::*, signer::Signer, DryRunConfig, Error, SharedRpcClient};
use crate::{opts::DryRunConfig, prelude::*, rpc::*, signer::Signer, Error, SharedRpcClient};
use codec::Encode;
use frame_support::traits::Currency;
use sp_core::Bytes;
+47 -262
View File
@@ -28,30 +28,34 @@
//! development. It is intended to run this bot with a `restart = true` way, so that it reports it
//! crash, but resumes work thereafter.
// Silence erroneous warning about unsafe not being required whereas it is
// see https://github.com/rust-lang/rust/issues/49112
#![allow(unused_unsafe)]
mod dry_run;
mod emergency_solution;
mod monitor;
mod opts;
mod prelude;
mod rpc;
mod runtime_versions;
mod signer;
use std::str::FromStr;
pub(crate) use prelude::*;
pub(crate) use signer::get_account_info;
use crate::opts::*;
use clap::Parser;
use frame_election_provider_support::NposSolver;
use frame_support::traits::Get;
use jsonrpsee::ws_client::{WsClient, WsClientBuilder};
use remote_externalities::{Builder, Mode, OnlineConfig};
use rpc::{RpcApiClient, SharedRpcClient};
use runtime_versions::RuntimeVersions;
use sp_npos_elections::BalancingConfig;
use sp_runtime::{traits::Block as BlockT, DeserializeOwned, Perbill};
use tracing_subscriber::{fmt, EnvFilter};
use sp_runtime::{traits::Block as BlockT, DeserializeOwned};
use std::{ops::Deref, sync::Arc};
use tracing_subscriber::{fmt, EnvFilter};
pub(crate) enum AnyRuntime {
Polkadot,
Kusama,
@@ -62,7 +66,6 @@ pub(crate) static mut RUNTIME: AnyRuntime = AnyRuntime::Polkadot;
macro_rules! construct_runtime_prelude {
($runtime:ident) => { paste::paste! {
#[allow(unused_import)]
pub(crate) mod [<$runtime _runtime_exports>] {
pub(crate) use crate::prelude::EPM;
pub(crate) use [<$runtime _runtime>]::*;
@@ -278,70 +281,6 @@ impl<T: EPM::Config> std::fmt::Display for Error<T> {
}
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
enum Command {
/// Monitor for the phase being signed, then compute.
Monitor(MonitorConfig),
/// Just compute a solution now, and don't submit it.
DryRun(DryRunConfig),
/// Provide a solution that can be submitted to the chain as an emergency response.
EmergencySolution(EmergencySolutionConfig),
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
enum Solver {
SeqPhragmen {
#[clap(long, default_value = "10")]
iterations: usize,
},
PhragMMS {
#[clap(long, default_value = "10")]
iterations: usize,
},
}
/// Submission strategy to use.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq))]
enum SubmissionStrategy {
// Only submit if at the time, we are the best.
IfLeading,
// Always submit.
Always,
// Submit if we are leading, or if the solution that's leading is more that the given `Perbill`
// better than us. This helps detect obviously fake solutions and still combat them.
ClaimBetterThan(Perbill),
}
/// Custom `impl` to parse `SubmissionStrategy` from CLI.
///
/// Possible options:
/// * --submission-strategy if-leading: only submit if leading
/// * --submission-strategy always: always submit
/// * --submission-strategy "percent-better <percent>": submit if submission is `n` percent better.
///
impl FromStr for SubmissionStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let res = if s == "if-leading" {
Self::IfLeading
} else if s == "always" {
Self::Always
} else if s.starts_with("percent-better ") {
let percent: u32 = s[15..].parse().map_err(|e| format!("{:?}", e))?;
Self::ClaimBetterThan(Perbill::from_percent(percent))
} else {
return Err(s.into())
};
Ok(res)
}
}
frame_support::parameter_types! {
/// Number of balancing iterations for a solution algorithm. Set based on the [`Solvers`] CLI
/// config.
@@ -349,87 +288,6 @@ frame_support::parameter_types! {
pub static Balancing: Option<BalancingConfig> = Some( BalancingConfig { iterations: BalanceIterations::get(), tolerance: 0 } );
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
struct MonitorConfig {
/// They type of event to listen to.
///
/// Typically, finalized is safer and there is no chance of anything going wrong, but it can be
/// slower. It is recommended to use finalized, if the duration of the signed phase is longer
/// than the the finality delay.
#[clap(long, default_value = "head", possible_values = &["head", "finalized"])]
listen: String,
/// The solver algorithm to use.
#[clap(subcommand)]
solver: Solver,
/// Submission strategy to use.
///
/// Possible options:
///
/// `--submission-strategy if-leading`: only submit if leading.
///
/// `--submission-strategy always`: always submit.
///
/// `--submission-strategy "percent-better <percent>"`: submit if the submission is `n` percent better.
#[clap(long, parse(try_from_str), default_value = "if-leading")]
submission_strategy: SubmissionStrategy,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
struct EmergencySolutionConfig {
/// The block hash at which scraping happens. If none is provided, the latest head is used.
#[clap(long)]
at: Option<Hash>,
/// The solver algorithm to use.
#[clap(subcommand)]
solver: Solver,
/// The number of top backed winners to take. All are taken, if not provided.
take: Option<usize>,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
struct DryRunConfig {
/// The block hash at which scraping happens. If none is provided, the latest head is used.
#[clap(long)]
at: Option<Hash>,
/// The solver algorithm to use.
#[clap(subcommand)]
solver: Solver,
/// Force create a new snapshot, else expect one to exist onchain.
#[clap(long)]
force_snapshot: bool,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
#[clap(author, version, about)]
struct Opt {
/// The `ws` node to connect to.
#[clap(long, short, default_value = DEFAULT_URI, env = "URI")]
uri: String,
/// The path to a file containing the seed of the account. If the file is not found, the seed is
/// used as-is.
///
/// Can also be provided via the `SEED` environment variable.
///
/// WARNING: Don't use an account with a large stash for this. Based on how the bot is
/// configured, it might re-try and lose funds through transaction fees/deposits.
#[clap(long, short, env = "SEED")]
seed_or_path: String,
#[clap(subcommand)]
command: Command,
}
/// Build the Ext at hash with all the data of `ElectionProviderMultiPhase` and any additional
/// pallets.
async fn create_election_ext<T: EPM::Config, B: BlockT + DeserializeOwned>(
@@ -582,7 +440,7 @@ pub(crate) async fn check_versions<T: frame_system::Config + EPM::Config>(
async fn main() {
fmt().with_env_filter(EnvFilter::from_default_env()).init();
let Opt { uri, seed_or_path, command } = Opt::parse();
let Opt { uri, command } = Opt::parse();
log::debug!(target: LOG_TARGET, "attempting to connect to {:?}", uri);
let rpc = loop {
@@ -648,26 +506,51 @@ async fn main() {
check_versions::<Runtime>(&rpc).await
};
let signer_account = any_runtime! {
signer::signer_uri_from_string::<Runtime>(&seed_or_path, &rpc)
.await
.expect("Provided account is invalid, terminating.")
};
let outcome = any_runtime! {
match command {
Command::Monitor(cmd) => monitor_cmd(rpc, cmd, signer_account).await
Command::Monitor(monitor_config) =>
{
let signer_account = any_runtime! {
signer::signer_uri_from_string::<Runtime>(&monitor_config.seed_or_path , &rpc)
.await
.expect("Provided account is invalid, terminating.")
};
monitor_cmd(rpc, monitor_config, signer_account).await
.map_err(|e| {
log::error!(target: LOG_TARGET, "Monitor error: {:?}", e);
}),
Command::DryRun(cmd) => dry_run_cmd(rpc, cmd, signer_account).await
})},
Command::DryRun(dryrun_config) => {
let signer_account = any_runtime! {
signer::signer_uri_from_string::<Runtime>(&dryrun_config.seed_or_path , &rpc)
.await
.expect("Provided account is invalid, terminating.")
};
dry_run_cmd(rpc, dryrun_config, signer_account).await
.map_err(|e| {
log::error!(target: LOG_TARGET, "DryRun error: {:?}", e);
}),
Command::EmergencySolution(cmd) => emergency_solution_cmd(rpc, cmd).await
})},
Command::EmergencySolution(emergency_solution_config) =>
emergency_solution_cmd(rpc, emergency_solution_config).await
.map_err(|e| {
log::error!(target: LOG_TARGET, "EmergencySolution error: {:?}", e);
}),
Command::Info(info_opts) => {
let remote_runtime_version = rpc.runtime_version(None).await.expect("runtime_version infallible; qed.");
let builtin_version = any_runtime! {
Version::get()
};
let versions = RuntimeVersions::new(&remote_runtime_version, &builtin_version);
if !info_opts.json {
println!("{}", versions);
} else {
let versions = serde_json::to_string_pretty(&versions).expect("Failed serializing version info");
println!("{}", versions);
}
Ok(())
}
}
};
log::info!(target: LOG_TARGET, "round of execution finished. outcome = {:?}", outcome);
@@ -696,102 +579,4 @@ mod tests {
assert_eq!(polkadot_version.spec_name, "polkadot".into());
assert_eq!(kusama_version.spec_name, "kusama".into());
}
#[test]
fn cli_monitor_works() {
let opt = Opt::try_parse_from([
env!("CARGO_PKG_NAME"),
"--uri",
"hi",
"--seed-or-path",
"//Alice",
"monitor",
"--listen",
"head",
"seq-phragmen",
])
.unwrap();
assert_eq!(
opt,
Opt {
uri: "hi".to_string(),
seed_or_path: "//Alice".to_string(),
command: Command::Monitor(MonitorConfig {
listen: "head".to_string(),
solver: Solver::SeqPhragmen { iterations: 10 },
submission_strategy: SubmissionStrategy::IfLeading,
}),
}
);
}
#[test]
fn cli_dry_run_works() {
let opt = Opt::try_parse_from([
env!("CARGO_PKG_NAME"),
"--uri",
"hi",
"--seed-or-path",
"//Alice",
"dry-run",
"phrag-mms",
])
.unwrap();
assert_eq!(
opt,
Opt {
uri: "hi".to_string(),
seed_or_path: "//Alice".to_string(),
command: Command::DryRun(DryRunConfig {
at: None,
solver: Solver::PhragMMS { iterations: 10 },
force_snapshot: false,
}),
}
);
}
#[test]
fn cli_emergency_works() {
let opt = Opt::try_parse_from([
env!("CARGO_PKG_NAME"),
"--uri",
"hi",
"--seed-or-path",
"//Alice",
"emergency-solution",
"99",
"phrag-mms",
"--iterations",
"1337",
])
.unwrap();
assert_eq!(
opt,
Opt {
uri: "hi".to_string(),
seed_or_path: "//Alice".to_string(),
command: Command::EmergencySolution(EmergencySolutionConfig {
take: Some(99),
at: None,
solver: Solver::PhragMMS { iterations: 1337 }
}),
}
);
}
#[test]
fn submission_strategy_from_str_works() {
use std::str::FromStr;
assert_eq!(SubmissionStrategy::from_str("if-leading"), Ok(SubmissionStrategy::IfLeading));
assert_eq!(SubmissionStrategy::from_str("always"), Ok(SubmissionStrategy::Always));
assert_eq!(
SubmissionStrategy::from_str(" percent-better 99 "),
Ok(SubmissionStrategy::ClaimBetterThan(Perbill::from_percent(99)))
);
}
}
+298
View File
@@ -0,0 +1,298 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::prelude::*;
use clap::Parser;
use sp_runtime::Perbill;
use std::str::FromStr;
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
#[clap(author, version, about)]
pub(crate) struct Opt {
/// The `ws` node to connect to.
#[clap(long, short, default_value = DEFAULT_URI, env = "URI", global = true)]
pub uri: String,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) enum Command {
/// Monitor for the phase being signed, then compute.
Monitor(MonitorConfig),
/// Just compute a solution now, and don't submit it.
DryRun(DryRunConfig),
/// Provide a solution that can be submitted to the chain as an emergency response.
EmergencySolution(EmergencySolutionConfig),
/// Return information about the current version
Info(InfoOpts),
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct MonitorConfig {
/// The path to a file containing the seed of the account. If the file is not found, the seed is
/// used as-is.
///
/// Can also be provided via the `SEED` environment variable.
///
/// WARNING: Don't use an account with a large stash for this. Based on how the bot is
/// configured, it might re-try and lose funds through transaction fees/deposits.
#[clap(long, short, env = "SEED")]
pub seed_or_path: String,
/// They type of event to listen to.
///
/// Typically, finalized is safer and there is no chance of anything going wrong, but it can be
/// slower. It is recommended to use finalized, if the duration of the signed phase is longer
/// than the the finality delay.
#[clap(long, default_value = "head", possible_values = &["head", "finalized"])]
pub listen: String,
/// The solver algorithm to use.
#[clap(subcommand)]
pub solver: Solver,
/// Submission strategy to use.
///
/// Possible options:
///
/// `--submission-strategy if-leading`: only submit if leading.
///
/// `--submission-strategy always`: always submit.
///
/// `--submission-strategy "percent-better <percent>"`: submit if the submission is `n` percent better.
#[clap(long, parse(try_from_str), default_value = "if-leading")]
pub submission_strategy: SubmissionStrategy,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct DryRunConfig {
/// The path to a file containing the seed of the account. If the file is not found, the seed is
/// used as-is.
///
/// Can also be provided via the `SEED` environment variable.
///
/// WARNING: Don't use an account with a large stash for this. Based on how the bot is
/// configured, it might re-try and lose funds through transaction fees/deposits.
#[clap(long, short, env = "SEED")]
pub seed_or_path: String,
/// The block hash at which scraping happens. If none is provided, the latest head is used.
#[clap(long)]
pub at: Option<Hash>,
/// The solver algorithm to use.
#[clap(subcommand)]
pub solver: Solver,
/// Force create a new snapshot, else expect one to exist onchain.
#[clap(long)]
pub force_snapshot: bool,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct EmergencySolutionConfig {
/// The block hash at which scraping happens. If none is provided, the latest head is used.
#[clap(long)]
pub at: Option<Hash>,
/// The solver algorithm to use.
#[clap(subcommand)]
pub solver: Solver,
/// The number of top backed winners to take. All are taken, if not provided.
pub take: Option<usize>,
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct InfoOpts {
/// Serialize the output as json
#[clap(long, short)]
pub json: bool,
}
/// Submission strategy to use.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum SubmissionStrategy {
// Only submit if at the time, we are the best.
IfLeading,
// Always submit.
Always,
// Submit if we are leading, or if the solution that's leading is more that the given `Perbill`
// better than us. This helps detect obviously fake solutions and still combat them.
ClaimBetterThan(Perbill),
}
#[derive(Debug, Clone, Parser)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) enum Solver {
SeqPhragmen {
#[clap(long, default_value = "10")]
iterations: usize,
},
PhragMMS {
#[clap(long, default_value = "10")]
iterations: usize,
},
}
/// Custom `impl` to parse `SubmissionStrategy` from CLI.
///
/// Possible options:
/// * --submission-strategy if-leading: only submit if leading
/// * --submission-strategy always: always submit
/// * --submission-strategy "percent-better <percent>": submit if submission is `n` percent better.
///
impl FromStr for SubmissionStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let res = if s == "if-leading" {
Self::IfLeading
} else if s == "always" {
Self::Always
} else if s.starts_with("percent-better ") {
let percent: u32 = s[15..].parse().map_err(|e| format!("{:?}", e))?;
Self::ClaimBetterThan(Perbill::from_percent(percent))
} else {
return Err(s.into())
};
Ok(res)
}
}
#[cfg(test)]
mod test_super {
use super::*;
#[test]
fn cli_monitor_works() {
let opt = Opt::try_parse_from([
env!("CARGO_PKG_NAME"),
"--uri",
"hi",
"monitor",
"--seed-or-path",
"//Alice",
"--listen",
"head",
"seq-phragmen",
])
.unwrap();
assert_eq!(
opt,
Opt {
uri: "hi".to_string(),
command: Command::Monitor(MonitorConfig {
seed_or_path: "//Alice".to_string(),
listen: "head".to_string(),
solver: Solver::SeqPhragmen { iterations: 10 },
submission_strategy: SubmissionStrategy::IfLeading,
}),
}
);
}
#[test]
fn cli_dry_run_works() {
let opt = Opt::try_parse_from([
env!("CARGO_PKG_NAME"),
"--uri",
"hi",
"dry-run",
"--seed-or-path",
"//Alice",
"phrag-mms",
])
.unwrap();
assert_eq!(
opt,
Opt {
uri: "hi".to_string(),
command: Command::DryRun(DryRunConfig {
seed_or_path: "//Alice".to_string(),
at: None,
solver: Solver::PhragMMS { iterations: 10 },
force_snapshot: false,
}),
}
);
}
#[test]
fn cli_emergency_works() {
let opt = Opt::try_parse_from([
env!("CARGO_PKG_NAME"),
"--uri",
"hi",
"emergency-solution",
"99",
"phrag-mms",
"--iterations",
"1337",
])
.unwrap();
assert_eq!(
opt,
Opt {
uri: "hi".to_string(),
command: Command::EmergencySolution(EmergencySolutionConfig {
take: Some(99),
at: None,
solver: Solver::PhragMMS { iterations: 1337 }
}),
}
);
}
#[test]
fn cli_info_works() {
let opt = Opt::try_parse_from([env!("CARGO_PKG_NAME"), "--uri", "hi", "info"]).unwrap();
assert_eq!(
opt,
Opt { uri: "hi".to_string(), command: Command::Info(InfoOpts { json: false }) }
);
}
#[test]
fn submission_strategy_from_str_works() {
use std::str::FromStr;
assert_eq!(SubmissionStrategy::from_str("if-leading"), Ok(SubmissionStrategy::IfLeading));
assert_eq!(SubmissionStrategy::from_str("always"), Ok(SubmissionStrategy::Always));
assert_eq!(
SubmissionStrategy::from_str(" percent-better 99 "),
Ok(SubmissionStrategy::ClaimBetterThan(Perbill::from_percent(99)))
);
}
}
@@ -0,0 +1,90 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
use sp_version::RuntimeVersion;
use std::fmt;
#[derive(Debug, serde::Serialize)]
pub(crate) struct RuntimeWrapper<'a>(pub &'a RuntimeVersion);
impl<'a> fmt::Display for RuntimeWrapper<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let width = 16;
writeln!(
f,
r#" impl_name : {impl_name:>width$}
spec_name : {spec_name:>width$}
spec_version : {spec_version:>width$}
transaction_version : {transaction_version:>width$}
impl_version : {impl_version:>width$}
authoringVersion : {authoring_version:>width$}
state_version : {state_version:>width$}"#,
spec_name = self.0.spec_name.to_string(),
impl_name = self.0.impl_name.to_string(),
spec_version = self.0.spec_version,
impl_version = self.0.impl_version,
authoring_version = self.0.authoring_version,
transaction_version = self.0.transaction_version,
state_version = self.0.state_version,
)
}
}
impl<'a> From<&'a RuntimeVersion> for RuntimeWrapper<'a> {
fn from(r: &'a RuntimeVersion) -> Self {
RuntimeWrapper(r)
}
}
#[derive(Debug, serde::Serialize)]
pub(crate) struct RuntimeVersions<'a> {
/// The `RuntimeVersion` linked in the staking-miner
pub linked: RuntimeWrapper<'a>,
/// The `RuntimeVersion` reported by the node we connect to via RPC
pub remote: RuntimeWrapper<'a>,
/// This `bool` reports whether both remote and linked `RuntimeVersion` are compatible
/// and if the staking-miner is expected to work properly against the remote runtime
compatible: bool,
}
impl<'a> RuntimeVersions<'a> {
pub fn new(
remote_runtime_version: &'a RuntimeVersion,
linked_runtime_version: &'a RuntimeVersion,
) -> Self {
Self {
remote: remote_runtime_version.into(),
linked: linked_runtime_version.into(),
compatible: are_runtimes_compatible(remote_runtime_version, linked_runtime_version),
}
}
}
/// Check whether runtimes are compatible. Currently we only support equality.
fn are_runtimes_compatible(r1: &RuntimeVersion, r2: &RuntimeVersion) -> bool {
r1 == r2
}
impl<'a> fmt::Display for RuntimeVersions<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let _ = write!(f, "- linked:\n{}", self.linked);
let _ = write!(f, "- remote :\n{}", self.remote);
write!(f, "Compatible: {}", if self.compatible { "YES" } else { "NO" })
}
}
+21
View File
@@ -1,4 +1,5 @@
use assert_cmd::{cargo::cargo_bin, Command};
use serde_json::{Result, Value};
#[test]
fn cli_version_works() {
@@ -10,3 +11,23 @@ fn cli_version_works() {
assert_eq!(version, format!("{} {}", crate_name, env!("CARGO_PKG_VERSION")));
}
#[test]
fn cli_info_works() {
let crate_name = env!("CARGO_PKG_NAME");
let output = Command::new(cargo_bin(crate_name))
.arg("info")
.arg("--json")
.env("RUST_LOG", "none")
.output()
.unwrap();
assert!(output.status.success(), "command returned with non-success exit code");
let info = String::from_utf8_lossy(&output.stdout).trim().to_owned();
let v: Result<Value> = serde_json::from_str(&info);
let v = v.unwrap();
assert!(!v["builtin"].to_string().is_empty());
assert!(!v["builtin"]["spec_name"].to_string().is_empty());
assert!(!v["builtin"]["spec_version"].to_string().is_empty());
assert!(!v["remote"].to_string().is_empty());
}