Create workspace skeleton based on design.md
This commit is contained in:
commit
ec363d2d41
|
@ -0,0 +1,3 @@
|
|||
# Cargo files
|
||||
/target/
|
||||
Cargo.lock
|
|
@ -0,0 +1,13 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"zebra-chain",
|
||||
"zebra-network",
|
||||
"zebra-storage",
|
||||
"zebra-script",
|
||||
"zebra-consensus",
|
||||
"zebra-rpc",
|
||||
"zebra-client",
|
||||
"zebra-reactor",
|
||||
"zebrad",
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-chain"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-consensus"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-network"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-reactor"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-rpc"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-script"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "zebra-storage"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry de Valence <hdevalence@hdevalence.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "zebrad"
|
||||
authors = []
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
abscissa_core = "0.3.0"
|
||||
failure = "0.1"
|
||||
gumdrop = "0.6"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["serde_derive"] }
|
||||
|
||||
[dev-dependencies.abscissa_core]
|
||||
version = "0.3.0"
|
||||
features = ["testing"]
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Zebrad
|
||||
|
||||
Zebrad is an application.
|
||||
|
||||
## Getting Started
|
||||
|
||||
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/
|
|
@ -0,0 +1,111 @@
|
|||
//! Zebrad Abscissa Application
|
||||
|
||||
use crate::{commands::ZebradCmd, config::ZebradConfig};
|
||||
use abscissa_core::{
|
||||
application, config, logging, Application, EntryPoint, FrameworkError, StandardPaths,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
/// Application state
|
||||
pub static ref APPLICATION: application::Lock<ZebradApp> = application::Lock::default();
|
||||
}
|
||||
|
||||
/// Obtain a read-only (multi-reader) lock on the application state.
|
||||
///
|
||||
/// Panics if the application state has not been initialized.
|
||||
pub fn app_reader() -> application::lock::Reader<ZebradApp> {
|
||||
APPLICATION.read()
|
||||
}
|
||||
|
||||
/// Obtain an exclusive mutable lock on the application state.
|
||||
pub fn app_writer() -> application::lock::Writer<ZebradApp> {
|
||||
APPLICATION.write()
|
||||
}
|
||||
|
||||
/// Obtain a read-only (multi-reader) lock on the application configuration.
|
||||
///
|
||||
/// Panics if the application configuration has not been loaded.
|
||||
pub fn app_config() -> config::Reader<ZebradApp> {
|
||||
config::Reader::new(&APPLICATION)
|
||||
}
|
||||
|
||||
/// Zebrad Application
|
||||
#[derive(Debug)]
|
||||
pub struct ZebradApp {
|
||||
/// Application configuration.
|
||||
config: Option<ZebradConfig>,
|
||||
|
||||
/// 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 ZebradApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: None,
|
||||
state: application::State::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Application for ZebradApp {
|
||||
/// Entrypoint command for this application.
|
||||
type Cmd = EntryPoint<ZebradCmd>;
|
||||
|
||||
/// Application configuration.
|
||||
type Cfg = ZebradConfig;
|
||||
|
||||
/// Paths to resources within the application.
|
||||
type Paths = StandardPaths;
|
||||
|
||||
/// Accessor for application configuration.
|
||||
fn config(&self) -> &ZebradConfig {
|
||||
self.config.as_ref().expect("config not loaded")
|
||||
}
|
||||
|
||||
/// Borrow the application state immutably.
|
||||
fn state(&self) -> &application::State<Self> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Borrow the application state mutably.
|
||||
fn state_mut(&mut self) -> &mut application::State<Self> {
|
||||
&mut 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 components = self.framework_components(command)?;
|
||||
self.state.components.register(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
|
||||
self.state.components.after_config(&config)?;
|
||||
self.config = Some(config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get logging configuration from command-line options
|
||||
fn logging_config(&self, command: &EntryPoint<ZebradCmd>) -> logging::Config {
|
||||
if command.verbose {
|
||||
logging::Config::verbose()
|
||||
} else {
|
||||
logging::Config::default()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
//! Main entry point for Zebrad
|
||||
|
||||
#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use zebrad::application::APPLICATION;
|
||||
|
||||
/// Boot Zebrad
|
||||
fn main() {
|
||||
abscissa_core::boot(&APPLICATION);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
//! Zebrad 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;
|
||||
mod version;
|
||||
|
||||
use self::{start::StartCmd, version::VersionCmd};
|
||||
use crate::config::ZebradConfig;
|
||||
use abscissa_core::{
|
||||
config::Override, Command, Configurable, FrameworkError, Help, Options, Runnable,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Zebrad Configuration Filename
|
||||
pub const CONFIG_FILE: &str = "zebrad.toml";
|
||||
|
||||
/// Zebrad Subcommands
|
||||
#[derive(Command, Debug, Options, Runnable)]
|
||||
pub enum ZebradCmd {
|
||||
/// The `help` subcommand
|
||||
#[options(help = "get usage information")]
|
||||
Help(Help<Self>),
|
||||
|
||||
/// The `start` subcommand
|
||||
#[options(help = "start the application")]
|
||||
Start(StartCmd),
|
||||
|
||||
/// The `version` subcommand
|
||||
#[options(help = "display version information")]
|
||||
Version(VersionCmd),
|
||||
}
|
||||
|
||||
/// This trait allows you to define how application configuration is loaded.
|
||||
impl Configurable<ZebradConfig> for ZebradCmd {
|
||||
/// 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 = PathBuf::from(CONFIG_FILE);
|
||||
|
||||
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: ZebradConfig,
|
||||
) -> Result<ZebradConfig, FrameworkError> {
|
||||
match self {
|
||||
ZebradCmd::Start(cmd) => cmd.override_config(config),
|
||||
_ => Ok(config),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//! `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::ZebradConfig;
|
||||
use abscissa_core::{config, Command, FrameworkError, Options, Runnable};
|
||||
|
||||
/// `start` subcommand
|
||||
///
|
||||
/// The `Options` proc macro generates an option parser based on the struct
|
||||
/// definition, and is defined in the `gumdrop` crate. See their documentation
|
||||
/// for a more comprehensive example:
|
||||
///
|
||||
/// <https://docs.rs/gumdrop/>
|
||||
#[derive(Command, Debug, Options)]
|
||||
pub struct StartCmd {
|
||||
/// To whom are we saying hello?
|
||||
#[options(free)]
|
||||
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<ZebradConfig> 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: ZebradConfig,
|
||||
) -> Result<ZebradConfig, FrameworkError> {
|
||||
if !self.recipient.is_empty() {
|
||||
config.hello.recipient = self.recipient.join(" ");
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//! `version` subcommand
|
||||
|
||||
#![allow(clippy::never_loop)]
|
||||
|
||||
use super::ZebradCmd;
|
||||
use abscissa_core::{Command, Options, Runnable};
|
||||
|
||||
/// `version` subcommand
|
||||
#[derive(Command, Debug, Default, Options)]
|
||||
pub struct VersionCmd {}
|
||||
|
||||
impl Runnable for VersionCmd {
|
||||
/// Print version message
|
||||
fn run(&self) {
|
||||
println!(
|
||||
"{} {}",
|
||||
ZebradCmd::name(),
|
||||
ZebradCmd::version()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//! Zebrad 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 abscissa_core::Config;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Zebrad Configuration
|
||||
#[derive(Clone, Config, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ZebradConfig {
|
||||
/// 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 ZebradConfig instead.
|
||||
impl Default for ZebradConfig {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//! Error types
|
||||
|
||||
use abscissa_core::err;
|
||||
use failure::Fail;
|
||||
use std::{fmt, io};
|
||||
|
||||
/// Error type
|
||||
#[derive(Debug)]
|
||||
pub struct Error(abscissa_core::Error<ErrorKind>);
|
||||
|
||||
/// Kinds of errors
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
|
||||
pub enum ErrorKind {
|
||||
/// Error in configuration file
|
||||
#[fail(display = "config error")]
|
||||
Config,
|
||||
|
||||
/// Input/output error
|
||||
#[fail(display = "I/O error")]
|
||||
Io,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<abscissa_core::Error<ErrorKind>> for Error {
|
||||
fn from(other: abscissa_core::Error<ErrorKind>) -> Self {
|
||||
Error(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(other: io::Error) -> Self {
|
||||
err!(ErrorKind::Io, other).into()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
//! Zebrad
|
||||
//!
|
||||
//! Application based on the [Abscissa] framework.
|
||||
//!
|
||||
//! [Abscissa]: https://github.com/iqlusioninc/abscissa
|
||||
|
||||
#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod application;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod prelude;
|
|
@ -0,0 +1,11 @@
|
|||
//! Application-local prelude: conveniently import types/functions/macros
|
||||
//! which are generally useful and should be available everywhere.
|
||||
|
||||
/// Application state accessors
|
||||
pub use crate::application::{app_config, app_reader, app_writer};
|
||||
|
||||
/// Commonly used Abscissa traits
|
||||
pub use abscissa_core::{Application, Command, Runnable};
|
||||
|
||||
/// Logging macros
|
||||
pub use abscissa_core::log::{debug, error, info, log, log_enabled, trace, warn};
|
|
@ -0,0 +1,85 @@
|
|||
//! 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>
|
||||
|
||||
#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use abscissa_core::testing::prelude::*;
|
||||
use zebrad::config::ZebradConfig;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
/// Executes your application binary via `cargo run`.
|
||||
///
|
||||
/// Storing this value in 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 ref RUNNER: CmdRunner = CmdRunner::default();
|
||||
}
|
||||
|
||||
/// Use `ZebradConfig::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 = ZebradConfig::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 = ZebradConfig::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 New Issue