mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-22 19:49:28 +00:00
initial Abscissa bin (#18)
* abscissa init * remove package in cargo.toml * cargo fmt + remove more stuff fro toml * bump rust edition
This commit is contained in:
parent
a187d9a357
commit
5f20342736
14 changed files with 490 additions and 103 deletions
33
Cargo.toml
33
Cargo.toml
|
@ -1,19 +1,3 @@
|
|||
[package]
|
||||
name = "cuprate"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.68.0"
|
||||
description = "An upcoming experimental, modern & secure monero node"
|
||||
readme = "readme.md"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/SyntheticBird45/cuprate"
|
||||
|
||||
|
||||
# All Contributors on github
|
||||
authors=[
|
||||
"SyntheticBird45",
|
||||
"Boog900"
|
||||
]
|
||||
|
||||
[workspace]
|
||||
|
||||
|
@ -36,20 +20,3 @@ tracing-subscriber = "*"
|
|||
|
||||
# As suggested by /u/danda :
|
||||
thiserror = "*"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = 0
|
||||
strip = "symbols"
|
||||
lto = "thin"
|
||||
panic = "abort"
|
||||
|
||||
[build]
|
||||
linker="clang"
|
||||
rustflags=[
|
||||
"-Clink-arg=-fuse-ld=mold",
|
||||
"-Zcf-protection=full",
|
||||
"-Zsanitizer=cfi",
|
||||
"-Crelocation-model=pie",
|
||||
"-Cstack-protector=all",
|
||||
]
|
|
@ -1,12 +1,21 @@
|
|||
[package]
|
||||
name = "cuprate-bin"
|
||||
name = "cuprate"
|
||||
authors = []
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "*", features = [] }
|
||||
clap_complete = "*"
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
clap = "4"
|
||||
serde = { version = "1", features = ["serde_derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
[dependencies.abscissa_core]
|
||||
version = "0.7.0"
|
||||
# optional: use `gimli` to capture backtraces
|
||||
# see https://github.com/rust-lang/backtrace-rs/issues/189
|
||||
# features = ["gimli-backtrace"]
|
||||
|
||||
[dev-dependencies]
|
||||
abscissa_core = { version = "0.7.0", features = ["testing"] }
|
||||
once_cell = "1.2"
|
||||
|
||||
|
|
9
cuprate/README.md
Normal file
9
cuprate/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
This application is authored using [Abscissa], a Rust application framework.
|
||||
|
||||
For more information, see:
|
||||
|
||||
[Documentation]
|
||||
|
||||
[Abscissa]: https://github.com/iqlusioninc/abscissa
|
||||
[Documentation]: https://docs.rs/abscissa_core/
|
88
cuprate/src/application.rs
Normal file
88
cuprate/src/application.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
//! Cuprate Abscissa Application
|
||||
|
||||
use crate::{commands::EntryPoint, config::CuprateConfig};
|
||||
use abscissa_core::{
|
||||
application::{self, AppCell},
|
||||
config::{self, CfgCell},
|
||||
trace, Application, FrameworkError, StandardPaths,
|
||||
};
|
||||
|
||||
/// Application state
|
||||
pub static APP: AppCell<CuprateApp> = AppCell::new();
|
||||
|
||||
/// Cuprate Application
|
||||
#[derive(Debug)]
|
||||
pub struct CuprateApp {
|
||||
/// Application configuration.
|
||||
config: CfgCell<CuprateConfig>,
|
||||
|
||||
/// Application state.
|
||||
state: application::State<Self>,
|
||||
}
|
||||
|
||||
/// Initialize a new application instance.
|
||||
///
|
||||
/// By default no configuration is loaded, and the framework state is
|
||||
/// initialized to a default, empty state (no components, threads, etc).
|
||||
impl Default for CuprateApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: CfgCell::default(),
|
||||
state: application::State::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Application for CuprateApp {
|
||||
/// Entrypoint command for this application.
|
||||
type Cmd = EntryPoint;
|
||||
|
||||
/// Application configuration.
|
||||
type Cfg = CuprateConfig;
|
||||
|
||||
/// Paths to resources within the application.
|
||||
type Paths = StandardPaths;
|
||||
|
||||
/// Accessor for application configuration.
|
||||
fn config(&self) -> config::Reader<CuprateConfig> {
|
||||
self.config.read()
|
||||
}
|
||||
|
||||
/// Borrow the application state immutably.
|
||||
fn state(&self) -> &application::State<Self> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Register all components used by this application.
|
||||
///
|
||||
/// If you would like to add additional components to your application
|
||||
/// beyond the default ones provided by the framework, this is the place
|
||||
/// to do so.
|
||||
fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> {
|
||||
let framework_components = self.framework_components(command)?;
|
||||
let mut app_components = self.state.components_mut();
|
||||
app_components.register(framework_components)
|
||||
}
|
||||
|
||||
/// Post-configuration lifecycle callback.
|
||||
///
|
||||
/// Called regardless of whether config is loaded to indicate this is the
|
||||
/// time in app lifecycle when configuration would be loaded if
|
||||
/// possible.
|
||||
fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> {
|
||||
// Configure components
|
||||
let mut components = self.state.components_mut();
|
||||
components.after_config(&config)?;
|
||||
self.config.set_once(config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get tracing configuration from command-line options
|
||||
fn tracing_config(&self, command: &EntryPoint) -> trace::Config {
|
||||
if command.verbose {
|
||||
trace::Config::verbose()
|
||||
} else {
|
||||
trace::Config::default()
|
||||
}
|
||||
}
|
||||
}
|
11
cuprate/src/bin/cuprate/main.rs
Normal file
11
cuprate/src/bin/cuprate/main.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
//! Main entry point for Cuprate
|
||||
|
||||
#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use cuprate::application::APP;
|
||||
|
||||
/// Boot Cuprate
|
||||
fn main() {
|
||||
abscissa_core::boot(&APP);
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
use crate::CUPRATE_VERSION;
|
||||
use clap::{value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command};
|
||||
use tracing::{event, span, Level, Span};
|
||||
|
||||
/// This function simply contains clap arguments
|
||||
pub fn args() -> ArgMatches {
|
||||
Command::new("Cuprate")
|
||||
.version(CUPRATE_VERSION)
|
||||
.author("Cuprate's contributors")
|
||||
.about("An upcoming experimental, modern, and secure monero node")
|
||||
// Generic Arguments
|
||||
.arg(
|
||||
Arg::new("log")
|
||||
.long("log-level")
|
||||
.value_name("Level")
|
||||
.help("Set the log level")
|
||||
.value_parser(value_parser!(u8))
|
||||
.default_value("1")
|
||||
.long_help("Set the log level. There is 3 log level: <1~INFO, 2~DEBUG >3~TRACE.")
|
||||
.required(false)
|
||||
.action(ArgAction::Set),
|
||||
)
|
||||
.get_matches()
|
||||
}
|
||||
|
||||
/// This function initialize the FmtSubscriber used by tracing to display event in the console. It send back a span used during runtime.
|
||||
pub fn init(matches: &ArgMatches) -> Span {
|
||||
// Getting the log level from args
|
||||
let log_level = matches.get_one::<u8>("log").unwrap();
|
||||
let level_filter = match log_level {
|
||||
2 => Level::DEBUG,
|
||||
x if x > &2 => Level::TRACE,
|
||||
_ => Level::INFO,
|
||||
};
|
||||
|
||||
// Initializing tracing subscriber and runtime span
|
||||
let subscriber = tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(level_filter)
|
||||
.with_target(false)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).expect("Failed to set global subscriber for tracing. We prefer to abort the node since without it you have no output in the console");
|
||||
let runtime_span = span!(Level::INFO, "Runtime");
|
||||
let _guard = runtime_span.enter();
|
||||
|
||||
// Notifying log level
|
||||
event!(Level::INFO, "Log level set to {}", level_filter);
|
||||
|
||||
drop(_guard);
|
||||
runtime_span
|
||||
}
|
87
cuprate/src/commands.rs
Normal file
87
cuprate/src/commands.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
//! Cuprate Subcommands
|
||||
//!
|
||||
//! This is where you specify the subcommands of your application.
|
||||
//!
|
||||
//! The default application comes with two subcommands:
|
||||
//!
|
||||
//! - `start`: launches the application
|
||||
//! - `--version`: print application version
|
||||
//!
|
||||
//! See the `impl Configurable` below for how to specify the path to the
|
||||
//! application's configuration file.
|
||||
|
||||
mod start;
|
||||
|
||||
use self::start::StartCmd;
|
||||
use crate::config::CuprateConfig;
|
||||
use abscissa_core::{config::Override, Command, Configurable, FrameworkError, Runnable};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Cuprate Configuration Filename
|
||||
pub const CONFIG_FILE: &str = "cuprate.toml";
|
||||
|
||||
/// Cuprate Subcommands
|
||||
/// Subcommands need to be listed in an enum.
|
||||
#[derive(clap::Parser, Command, Debug, Runnable)]
|
||||
pub enum CuprateCmd {
|
||||
/// The `start` subcommand
|
||||
Start(StartCmd),
|
||||
}
|
||||
|
||||
/// Entry point for the application. It needs to be a struct to allow using subcommands!
|
||||
#[derive(clap::Parser, Command, Debug)]
|
||||
#[command(author, about, version)]
|
||||
pub struct EntryPoint {
|
||||
#[command(subcommand)]
|
||||
cmd: CuprateCmd,
|
||||
|
||||
/// Enable verbose logging
|
||||
#[arg(short, long)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Use the specified config file
|
||||
#[arg(short, long)]
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
impl Runnable for EntryPoint {
|
||||
fn run(&self) {
|
||||
self.cmd.run()
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows you to define how application configuration is loaded.
|
||||
impl Configurable<CuprateConfig> for EntryPoint {
|
||||
/// Location of the configuration file
|
||||
fn config_path(&self) -> Option<PathBuf> {
|
||||
// Check if the config file exists, and if it does not, ignore it.
|
||||
// If you'd like for a missing configuration file to be a hard error
|
||||
// instead, always return `Some(CONFIG_FILE)` here.
|
||||
let filename = self
|
||||
.config
|
||||
.as_ref()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| CONFIG_FILE.into());
|
||||
|
||||
if filename.exists() {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply changes to the config after it's been loaded, e.g. overriding
|
||||
/// values in a config file using command-line options.
|
||||
///
|
||||
/// This can be safely deleted if you don't want to override config
|
||||
/// settings from command-line options.
|
||||
fn process_config(&self, config: CuprateConfig) -> Result<CuprateConfig, FrameworkError> {
|
||||
match &self.cmd {
|
||||
CuprateCmd::Start(cmd) => cmd.override_config(config),
|
||||
//
|
||||
// If you don't need special overrides for some
|
||||
// subcommands, you can just use a catch all
|
||||
// _ => Ok(config),
|
||||
}
|
||||
}
|
||||
}
|
42
cuprate/src/commands/start.rs
Normal file
42
cuprate/src/commands/start.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
//! `start` subcommand - example of how to write a subcommand
|
||||
|
||||
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
|
||||
/// accessors along with logging macros. Customize as you see fit.
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::config::CuprateConfig;
|
||||
use abscissa_core::{config, Command, FrameworkError, Runnable};
|
||||
|
||||
/// `start` subcommand
|
||||
///
|
||||
/// The `Parser` proc macro generates an option parser based on the struct
|
||||
/// definition, and is defined in the `clap` crate. See their documentation
|
||||
/// for a more comprehensive example:
|
||||
///
|
||||
/// <https://docs.rs/clap/>
|
||||
#[derive(clap::Parser, Command, Debug)]
|
||||
pub struct StartCmd {
|
||||
/// To whom are we saying hello?
|
||||
recipient: Vec<String>,
|
||||
}
|
||||
|
||||
impl Runnable for StartCmd {
|
||||
/// Start the application.
|
||||
fn run(&self) {
|
||||
let config = APP.config();
|
||||
println!("Hello, {}!", &config.hello.recipient);
|
||||
}
|
||||
}
|
||||
|
||||
impl config::Override<CuprateConfig> for StartCmd {
|
||||
// Process the given command line options, overriding settings from
|
||||
// a configuration file using explicit flags taken from command-line
|
||||
// arguments.
|
||||
fn override_config(&self, mut config: CuprateConfig) -> Result<CuprateConfig, FrameworkError> {
|
||||
if !self.recipient.is_empty() {
|
||||
config.hello.recipient = self.recipient.join(" ");
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
45
cuprate/src/config.rs
Normal file
45
cuprate/src/config.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
//! Cuprate Config
|
||||
//!
|
||||
//! See instructions in `commands.rs` to specify the path to your
|
||||
//! application's configuration file and/or command-line options
|
||||
//! for specifying it.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Cuprate Configuration
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CuprateConfig {
|
||||
/// An example configuration section
|
||||
pub hello: ExampleSection,
|
||||
}
|
||||
|
||||
/// Default configuration settings.
|
||||
///
|
||||
/// Note: if your needs are as simple as below, you can
|
||||
/// use `#[derive(Default)]` on CuprateConfig instead.
|
||||
impl Default for CuprateConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hello: ExampleSection::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example configuration section.
|
||||
///
|
||||
/// Delete this and replace it with your actual configuration structs.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ExampleSection {
|
||||
/// Example configuration value
|
||||
pub recipient: String,
|
||||
}
|
||||
|
||||
impl Default for ExampleSection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
recipient: "world".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
70
cuprate/src/error.rs
Normal file
70
cuprate/src/error.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
//! Error types
|
||||
|
||||
use abscissa_core::error::{BoxError, Context};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
ops::Deref,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Kinds of errors
|
||||
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
|
||||
pub enum ErrorKind {
|
||||
/// Error in configuration file
|
||||
#[error("config error")]
|
||||
Config,
|
||||
|
||||
/// Input/output error
|
||||
#[error("I/O error")]
|
||||
Io,
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
/// Create an error context from this error
|
||||
pub fn context(self, source: impl Into<BoxError>) -> Context<ErrorKind> {
|
||||
Context::new(self, Some(source.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type
|
||||
#[derive(Debug)]
|
||||
pub struct Error(Box<Context<ErrorKind>>);
|
||||
|
||||
impl Deref for Error {
|
||||
type Target = Context<ErrorKind>;
|
||||
|
||||
fn deref(&self) -> &Context<ErrorKind> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.0.source()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Self {
|
||||
Context::new(kind, None).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Context<ErrorKind>> for Error {
|
||||
fn from(context: Context<ErrorKind>) -> Self {
|
||||
Error(Box::new(context))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Self {
|
||||
ErrorKind::Io.context(err).into()
|
||||
}
|
||||
}
|
22
cuprate/src/lib.rs
Normal file
22
cuprate/src/lib.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
//! Cuprate
|
||||
//!
|
||||
//! Application based on the [Abscissa] framework.
|
||||
//!
|
||||
//! [Abscissa]: https://github.com/iqlusioninc/abscissa
|
||||
|
||||
// Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
trivial_casts,
|
||||
unused_lifetimes,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
pub mod application;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod prelude;
|
|
@ -1,13 +0,0 @@
|
|||
use tracing::{event, info, span, Level};
|
||||
|
||||
pub mod cli;
|
||||
|
||||
const CUPRATE_VERSION: &str = "0.1.0";
|
||||
|
||||
fn main() {
|
||||
// Collecting options
|
||||
let matches = cli::args();
|
||||
|
||||
// Initializing tracing subscriber and runtime span
|
||||
let _runtime_span = cli::init(&matches);
|
||||
}
|
9
cuprate/src/prelude.rs
Normal file
9
cuprate/src/prelude.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
//! Application-local prelude: conveniently import types/functions/macros
|
||||
//! which are generally useful and should be available in every module with
|
||||
//! `use crate::prelude::*;
|
||||
|
||||
/// Abscissa core prelude
|
||||
pub use abscissa_core::prelude::*;
|
||||
|
||||
/// Application state
|
||||
pub use crate::application::APP;
|
91
cuprate/tests/acceptance.rs
Normal file
91
cuprate/tests/acceptance.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
//! Acceptance test: runs the application as a subprocess and asserts its
|
||||
//! output for given argument combinations matches what is expected.
|
||||
//!
|
||||
//! Modify and/or delete these as you see fit to test the specific needs of
|
||||
//! your application.
|
||||
//!
|
||||
//! For more information, see:
|
||||
//! <https://docs.rs/abscissa_core/latest/abscissa_core/testing/index.html>
|
||||
|
||||
// Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
trivial_casts,
|
||||
unused_lifetimes,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
use abscissa_core::testing::prelude::*;
|
||||
use cuprate::config::CuprateConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// Executes your application binary via `cargo run`.
|
||||
///
|
||||
/// Storing this value as a [`Lazy`] static ensures that all instances of
|
||||
/// the runner acquire a mutex when executing commands and inspecting
|
||||
/// exit statuses, serializing what would otherwise be multithreaded
|
||||
/// invocations as `cargo test` executes tests in parallel by default.
|
||||
pub static RUNNER: Lazy<CmdRunner> = Lazy::new(|| CmdRunner::default());
|
||||
|
||||
/// Use `CuprateConfig::default()` value if no config or args
|
||||
#[test]
|
||||
fn start_no_args() {
|
||||
let mut runner = RUNNER.clone();
|
||||
let mut cmd = runner.arg("start").capture_stdout().run();
|
||||
cmd.stdout().expect_line("Hello, world!");
|
||||
cmd.wait().unwrap().expect_success();
|
||||
}
|
||||
|
||||
/// Use command-line argument value
|
||||
#[test]
|
||||
fn start_with_args() {
|
||||
let mut runner = RUNNER.clone();
|
||||
let mut cmd = runner
|
||||
.args(&["start", "acceptance", "test"])
|
||||
.capture_stdout()
|
||||
.run();
|
||||
|
||||
cmd.stdout().expect_line("Hello, acceptance test!");
|
||||
cmd.wait().unwrap().expect_success();
|
||||
}
|
||||
|
||||
/// Use configured value
|
||||
#[test]
|
||||
fn start_with_config_no_args() {
|
||||
let mut config = CuprateConfig::default();
|
||||
config.hello.recipient = "configured recipient".to_owned();
|
||||
let expected_line = format!("Hello, {}!", &config.hello.recipient);
|
||||
|
||||
let mut runner = RUNNER.clone();
|
||||
let mut cmd = runner.config(&config).arg("start").capture_stdout().run();
|
||||
cmd.stdout().expect_line(&expected_line);
|
||||
cmd.wait().unwrap().expect_success();
|
||||
}
|
||||
|
||||
/// Override configured value with command-line argument
|
||||
#[test]
|
||||
fn start_with_config_and_args() {
|
||||
let mut config = CuprateConfig::default();
|
||||
config.hello.recipient = "configured recipient".to_owned();
|
||||
|
||||
let mut runner = RUNNER.clone();
|
||||
let mut cmd = runner
|
||||
.config(&config)
|
||||
.args(&["start", "acceptance", "test"])
|
||||
.capture_stdout()
|
||||
.run();
|
||||
|
||||
cmd.stdout().expect_line("Hello, acceptance test!");
|
||||
cmd.wait().unwrap().expect_success();
|
||||
}
|
||||
|
||||
/// Example of a test which matches a regular expression
|
||||
#[test]
|
||||
fn version_no_args() {
|
||||
let mut runner = RUNNER.clone();
|
||||
let mut cmd = runner.arg("--version").capture_stdout().run();
|
||||
cmd.stdout().expect_regex(r"\A\w+ [\d\.\-]+\z");
|
||||
}
|
Loading…
Reference in a new issue