Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Extract crate for md based tests
xla committed 3 years ago
commit 97f7064094d97ae694ce926d310cf7db6ec4a9e0
parent 3b0d83884646a4840e833eb727b469e84968a4a5
7 files changed +359 -335
modified Cargo.lock
@@ -1853,15 +1853,14 @@ dependencies = [
 "log",
 "pretty_assertions",
 "radicle",
+
 "radicle-cli-test",
 "radicle-cob",
 "radicle-crypto",
 "radicle-node",
 "serde",
 "serde_json",
 "serde_yaml",
-
 "shlex",
 "similar",
-
 "snapbox",
 "tempfile",
 "thiserror",
 "timeago",
@@ -1870,6 +1869,17 @@ dependencies = [
]

[[package]]
+
name = "radicle-cli-test"
+
version = "0.1.0"
+
dependencies = [
+
 "log",
+
 "pretty_assertions",
+
 "shlex",
+
 "snapbox",
+
 "thiserror",
+
]
+

+
[[package]]
name = "radicle-cob"
version = "0.1.0"
dependencies = [
modified Cargo.toml
@@ -3,6 +3,7 @@ members = [
  "radicle",
  "radicle-cob",
  "radicle-cli",
+
  "radicle-cli-test",
  "radicle-crdt",
  "radicle-crypto",
  "radicle-dag",
added radicle-cli-test/Cargo.toml
@@ -0,0 +1,13 @@
+
[package]
+
name = "radicle-cli-test"
+
license = "MIT OR Apache-2.0"
+
version = "0.1.0"
+
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
+
edition = "2021"
+

+
[dependencies]
+
log = { version = "0.4", features = ["std"] }
+
pretty_assertions = { version = "1.3.0" }
+
shlex = { version = "1.1.0" }
+
snapbox = { version = "0.4.3" }
+
thiserror = { version = "1" }
added radicle-cli-test/src/lib.rs
@@ -0,0 +1,328 @@
+
#![allow(clippy::collapsible_else_if)]
+
use std::borrow::Cow;
+
use std::collections::HashMap;
+
use std::path::{Path, PathBuf};
+
use std::{env, fs, io, mem};
+

+
use snapbox::cmd::{Command, OutputAssert};
+
use snapbox::{Assert, Substitutions};
+
use thiserror::Error;
+

+
/// Error lines in the CLI are prefixed with this string.
+
const ERROR_PREFIX: &str = "==";
+

+
#[derive(Error, Debug)]
+
pub enum Error {
+
    #[error("parsing failed")]
+
    Parse,
+
    #[error("test file not found: {0:?}")]
+
    TestNotFound(PathBuf),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("snapbox: {0}")]
+
    Snapbox(#[from] snapbox::Error),
+
}
+

+
#[derive(Debug, PartialEq, Eq)]
+
enum ExitStatus {
+
    Success,
+
    Failure,
+
}
+

+
/// A test which may contain multiple assertions.
+
#[derive(Debug, Default, PartialEq, Eq)]
+
pub struct Test {
+
    /// Human-readable context around the test. Functions as documentation.
+
    context: Vec<String>,
+
    /// Test assertions to run.
+
    assertions: Vec<Assertion>,
+
}
+

+
/// An assertion is a command to run with an expected output.
+
#[derive(Debug, PartialEq, Eq)]
+
pub struct Assertion {
+
    /// Name of command to run, eg. `git`.
+
    command: String,
+
    /// Command arguments, eg. `["push"]`.
+
    args: Vec<String>,
+
    /// Expected output (stdout or stderr).
+
    expected: String,
+
    /// Expected exit status.
+
    exit: ExitStatus,
+
}
+

+
#[derive(Debug, Default, PartialEq, Eq)]
+
pub struct TestFormula {
+
    /// Current working directory to run the test in.
+
    cwd: PathBuf,
+
    /// Environment to pass to the test.
+
    env: HashMap<String, String>,
+
    /// Tests to run.
+
    tests: Vec<Test>,
+
    /// Output substitutions.
+
    subs: Substitutions,
+
}
+

+
impl TestFormula {
+
    pub fn new() -> Self {
+
        Self {
+
            cwd: PathBuf::new(),
+
            env: HashMap::new(),
+
            tests: Vec::new(),
+
            subs: Substitutions::new(),
+
        }
+
    }
+

+
    pub fn cwd(&mut self, path: impl AsRef<Path>) -> &mut Self {
+
        self.cwd = path.as_ref().into();
+
        self
+
    }
+

+
    pub fn env(&mut self, key: impl Into<String>, val: impl Into<String>) -> &mut Self {
+
        self.env.insert(key.into(), val.into());
+
        self
+
    }
+

+
    pub fn envs<K: ToString, V: ToString>(
+
        &mut self,
+
        envs: impl IntoIterator<Item = (K, V)>,
+
    ) -> &mut Self {
+
        for (k, v) in envs {
+
            self.env.insert(k.to_string(), v.to_string());
+
        }
+
        self
+
    }
+

+
    pub fn file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
+
        let path = path.as_ref();
+
        let contents = match fs::read(path) {
+
            Ok(bytes) => bytes,
+
            Err(err) if err.kind() == io::ErrorKind::NotFound => {
+
                return Err(Error::TestNotFound(path.to_path_buf()));
+
            }
+
            Err(err) => return Err(err.into()),
+
        };
+
        self.read(io::Cursor::new(contents))
+
    }
+

+
    pub fn read(&mut self, r: impl io::BufRead) -> Result<&mut Self, Error> {
+
        let mut test = Test::default();
+
        let mut fenced = false; // Whether we're inside a fenced code block.
+

+
        for line in r.lines() {
+
            let line = line?;
+

+
            if line.starts_with("```") {
+
                if fenced {
+
                    // End existing code block.
+
                    self.tests.push(mem::take(&mut test));
+
                }
+
                fenced = !fenced;
+

+
                continue;
+
            }
+

+
            if fenced {
+
                if let Some(line) = line.strip_prefix('$') {
+
                    let line = line.trim();
+
                    let parts = shlex::split(line).ok_or(Error::Parse)?;
+
                    let (cmd, args) = parts.split_first().ok_or(Error::Parse)?;
+

+
                    test.assertions.push(Assertion {
+
                        command: cmd.to_owned(),
+
                        args: args.to_owned(),
+
                        expected: String::new(),
+
                        exit: ExitStatus::Success,
+
                    });
+
                } else if let Some(test) = test.assertions.last_mut() {
+
                    if line.starts_with(ERROR_PREFIX) {
+
                        test.exit = ExitStatus::Failure;
+
                    }
+
                    test.expected.push_str(line.as_str());
+
                    test.expected.push('\n');
+
                } else {
+
                    return Err(Error::Parse);
+
                }
+
            } else {
+
                test.context.push(line);
+
            }
+
        }
+
        Ok(self)
+
    }
+

+
    #[allow(dead_code)]
+
    pub fn substitute(
+
        &mut self,
+
        value: &'static str,
+
        other: impl Into<Cow<'static, str>>,
+
    ) -> Result<&mut Self, Error> {
+
        self.subs.insert(value, other)?;
+
        Ok(self)
+
    }
+

+
    pub fn run(&mut self) -> Result<bool, io::Error> {
+
        let assert = Assert::new().substitutions(self.subs.clone());
+

+
        fs::create_dir_all(&self.cwd)?;
+

+
        for test in &self.tests {
+
            for assertion in &test.assertions {
+
                let cmd = if assertion.command == "rad" {
+
                    snapbox::cmd::cargo_bin("rad")
+
                } else if assertion.command == "cd" {
+
                    let path: PathBuf = assertion.args.first().unwrap().into();
+
                    let path = self.cwd.join(path);
+

+
                    // TODO: Add support for `..` and `/`
+
                    // TODO: Error if more than one args are given.
+

+
                    if !path.exists() {
+
                        return Err(io::Error::new(
+
                            io::ErrorKind::NotFound,
+
                            format!("cd: '{}' does not exist", path.display()),
+
                        ));
+
                    }
+
                    self.cwd = path;
+

+
                    continue;
+
                } else {
+
                    PathBuf::from(&assertion.command)
+
                };
+
                log::debug!(target: "test", "Running `{}` in `{}`..", cmd.display(), self.cwd.display());
+

+
                if !self.cwd.exists() {
+
                    log::error!(target: "test", "Directory {} does not exist..", self.cwd.display());
+
                }
+
                let result = Command::new(cmd.clone())
+
                    .env_clear()
+
                    .envs(env::vars().filter(|(k, _)| k == "PATH"))
+
                    .envs(self.env.clone())
+
                    .current_dir(&self.cwd)
+
                    .args(&assertion.args)
+
                    .with_assert(assert.clone())
+
                    .output();
+

+
                match result {
+
                    Ok(output) => {
+
                        let assert = OutputAssert::new(output).with_assert(assert.clone());
+
                        match assertion.exit {
+
                            ExitStatus::Success => {
+
                                assert.stdout_matches(&assertion.expected).success();
+
                            }
+
                            ExitStatus::Failure => {
+
                                assert.stdout_matches(&assertion.expected).failure();
+
                            }
+
                        }
+
                    }
+
                    Err(err) => {
+
                        if err.kind() == io::ErrorKind::NotFound {
+
                            log::error!(target: "test", "Command `{}` does not exist..", cmd.display());
+
                        }
+
                        return Err(io::Error::new(
+
                            err.kind(),
+
                            format!("{err}: `{}`", cmd.display()),
+
                        ));
+
                    }
+
                }
+
            }
+
        }
+
        Ok(true)
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+

+
    use pretty_assertions::assert_eq;
+

+
    #[test]
+
    fn test_parse() {
+
        let input = r#"
+
Let's try to track @dave and @sean:
+
```
+
$ rad track @dave
+
Tracking relationship established for @dave.
+
Nothing to do.
+

+
$ rad track @sean
+
Tracking relationship established for @sean.
+
Nothing to do.
+
```
+
Super, now let's move on to the next step.
+
```
+
$ rad sync
+
```
+
"#
+
        .trim()
+
        .as_bytes()
+
        .to_owned();
+

+
        let mut actual = TestFormula::new();
+
        actual
+
            .read(io::BufReader::new(io::Cursor::new(input)))
+
            .unwrap();
+

+
        let expected = TestFormula {
+
            cwd: PathBuf::new(),
+
            env: HashMap::new(),
+
            subs: Substitutions::new(),
+
            tests: vec![
+
                Test {
+
                    context: vec![String::from("Let's try to track @dave and @sean:")],
+
                    assertions: vec![
+
                        Assertion {
+
                            command: String::from("rad"),
+
                            args: vec![String::from("track"), String::from("@dave")],
+
                            expected: String::from(
+
                                "Tracking relationship established for @dave.\nNothing to do.\n\n",
+
                            ),
+
                            exit: ExitStatus::Success,
+
                        },
+
                        Assertion {
+
                            command: String::from("rad"),
+
                            args: vec![String::from("track"), String::from("@sean")],
+
                            expected: String::from(
+
                                "Tracking relationship established for @sean.\nNothing to do.\n",
+
                            ),
+
                            exit: ExitStatus::Success,
+
                        },
+
                    ],
+
                },
+
                Test {
+
                    context: vec![String::from("Super, now let's move on to the next step.")],
+
                    assertions: vec![Assertion {
+
                        command: String::from("rad"),
+
                        args: vec![String::from("sync")],
+
                        expected: String::new(),
+
                        exit: ExitStatus::Success,
+
                    }],
+
                },
+
            ],
+
        };
+

+
        assert_eq!(actual, expected);
+
    }
+

+
    #[test]
+
    fn test_run() {
+
        let input = r#"
+
Running a simple command such as `head`:
+
```
+
$ head -n 2 Cargo.toml
+
[package]
+
name = "radicle-cli-test"
+
```
+
"#
+
        .trim()
+
        .as_bytes()
+
        .to_owned();
+

+
        let mut formula = TestFormula::new();
+
        formula
+
            .cwd(env!("CARGO_MANIFEST_DIR"))
+
            .read(io::BufReader::new(io::Cursor::new(input)))
+
            .unwrap();
+
        formula.run().unwrap();
+
    }
+
}
modified radicle-cli/Cargo.toml
@@ -32,6 +32,10 @@ zeroize = { version = "1.1" }
version = "0"
path = "../radicle"

+
[dependencies.radicle-cli-test]
+
version = "0"
+
path = "../radicle-cli-test"
+

[dependencies.radicle-cob]
version = "0"
path = "../radicle-cob"
@@ -45,5 +49,3 @@ pretty_assertions = { version = "1.3.0" }
tempfile = { version = "3.3.0" }
radicle = { path = "../radicle", features = ["test"] }
radicle-node = { path = "../radicle-node", features = ["test"] }
-
shlex = { version = "1.1.0" }
-
snapbox = { version = "0.4.3" }
modified radicle-cli/tests/commands.rs
@@ -9,15 +9,13 @@ use radicle::profile::Home;
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::test::fixtures;

+
use radicle_cli_test::TestFormula;
use radicle_node::service::tracking::Policy;
use radicle_node::test::{
    environment::{Config, Environment},
    logger,
};

-
mod framework;
-
use framework::TestFormula;
-

/// Run a CLI test file.
fn test<'a>(
    test: impl AsRef<Path>,
deleted radicle-cli/tests/framework/mod.rs
@@ -1,328 +0,0 @@
-
#![allow(clippy::collapsible_else_if)]
-
use std::borrow::Cow;
-
use std::collections::HashMap;
-
use std::path::{Path, PathBuf};
-
use std::{env, fs, io, mem};
-

-
use snapbox::cmd::{Command, OutputAssert};
-
use snapbox::{Assert, Substitutions};
-
use thiserror::Error;
-

-
/// Error lines in the CLI are prefixed with this string.
-
const ERROR_PREFIX: &str = "==";
-

-
#[derive(Error, Debug)]
-
pub enum Error {
-
    #[error("parsing failed")]
-
    Parse,
-
    #[error("test file not found: {0:?}")]
-
    TestNotFound(PathBuf),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("snapbox: {0}")]
-
    Snapbox(#[from] snapbox::Error),
-
}
-

-
#[derive(Debug, PartialEq, Eq)]
-
enum ExitStatus {
-
    Success,
-
    Failure,
-
}
-

-
/// A test which may contain multiple assertions.
-
#[derive(Debug, Default, PartialEq, Eq)]
-
pub struct Test {
-
    /// Human-readable context around the test. Functions as documentation.
-
    context: Vec<String>,
-
    /// Test assertions to run.
-
    assertions: Vec<Assertion>,
-
}
-

-
/// An assertion is a command to run with an expected output.
-
#[derive(Debug, PartialEq, Eq)]
-
pub struct Assertion {
-
    /// Name of command to run, eg. `git`.
-
    command: String,
-
    /// Command arguments, eg. `["push"]`.
-
    args: Vec<String>,
-
    /// Expected output (stdout or stderr).
-
    expected: String,
-
    /// Expected exit status.
-
    exit: ExitStatus,
-
}
-

-
#[derive(Debug, Default, PartialEq, Eq)]
-
pub struct TestFormula {
-
    /// Current working directory to run the test in.
-
    cwd: PathBuf,
-
    /// Environment to pass to the test.
-
    env: HashMap<String, String>,
-
    /// Tests to run.
-
    tests: Vec<Test>,
-
    /// Output substitutions.
-
    subs: Substitutions,
-
}
-

-
impl TestFormula {
-
    pub fn new() -> Self {
-
        Self {
-
            cwd: PathBuf::new(),
-
            env: HashMap::new(),
-
            tests: Vec::new(),
-
            subs: Substitutions::new(),
-
        }
-
    }
-

-
    pub fn cwd(&mut self, path: impl AsRef<Path>) -> &mut Self {
-
        self.cwd = path.as_ref().into();
-
        self
-
    }
-

-
    pub fn env(&mut self, key: impl Into<String>, val: impl Into<String>) -> &mut Self {
-
        self.env.insert(key.into(), val.into());
-
        self
-
    }
-

-
    pub fn envs<K: ToString, V: ToString>(
-
        &mut self,
-
        envs: impl IntoIterator<Item = (K, V)>,
-
    ) -> &mut Self {
-
        for (k, v) in envs {
-
            self.env.insert(k.to_string(), v.to_string());
-
        }
-
        self
-
    }
-

-
    pub fn file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
-
        let path = path.as_ref();
-
        let contents = match fs::read(path) {
-
            Ok(bytes) => bytes,
-
            Err(err) if err.kind() == io::ErrorKind::NotFound => {
-
                return Err(Error::TestNotFound(path.to_path_buf()));
-
            }
-
            Err(err) => return Err(err.into()),
-
        };
-
        self.read(io::Cursor::new(contents))
-
    }
-

-
    pub fn read(&mut self, r: impl io::BufRead) -> Result<&mut Self, Error> {
-
        let mut test = Test::default();
-
        let mut fenced = false; // Whether we're inside a fenced code block.
-

-
        for line in r.lines() {
-
            let line = line?;
-

-
            if line.starts_with("```") {
-
                if fenced {
-
                    // End existing code block.
-
                    self.tests.push(mem::take(&mut test));
-
                }
-
                fenced = !fenced;
-

-
                continue;
-
            }
-

-
            if fenced {
-
                if let Some(line) = line.strip_prefix('$') {
-
                    let line = line.trim();
-
                    let parts = shlex::split(line).ok_or(Error::Parse)?;
-
                    let (cmd, args) = parts.split_first().ok_or(Error::Parse)?;
-

-
                    test.assertions.push(Assertion {
-
                        command: cmd.to_owned(),
-
                        args: args.to_owned(),
-
                        expected: String::new(),
-
                        exit: ExitStatus::Success,
-
                    });
-
                } else if let Some(test) = test.assertions.last_mut() {
-
                    if line.starts_with(ERROR_PREFIX) {
-
                        test.exit = ExitStatus::Failure;
-
                    }
-
                    test.expected.push_str(line.as_str());
-
                    test.expected.push('\n');
-
                } else {
-
                    return Err(Error::Parse);
-
                }
-
            } else {
-
                test.context.push(line);
-
            }
-
        }
-
        Ok(self)
-
    }
-

-
    #[allow(dead_code)]
-
    pub fn substitute(
-
        &mut self,
-
        value: &'static str,
-
        other: impl Into<Cow<'static, str>>,
-
    ) -> Result<&mut Self, Error> {
-
        self.subs.insert(value, other)?;
-
        Ok(self)
-
    }
-

-
    pub fn run(&mut self) -> Result<bool, io::Error> {
-
        let assert = Assert::new().substitutions(self.subs.clone());
-

-
        fs::create_dir_all(&self.cwd)?;
-

-
        for test in &self.tests {
-
            for assertion in &test.assertions {
-
                let cmd = if assertion.command == "rad" {
-
                    snapbox::cmd::cargo_bin("rad")
-
                } else if assertion.command == "cd" {
-
                    let path: PathBuf = assertion.args.first().unwrap().into();
-
                    let path = self.cwd.join(path);
-

-
                    // TODO: Add support for `..` and `/`
-
                    // TODO: Error if more than one args are given.
-

-
                    if !path.exists() {
-
                        return Err(io::Error::new(
-
                            io::ErrorKind::NotFound,
-
                            format!("cd: '{}' does not exist", path.display()),
-
                        ));
-
                    }
-
                    self.cwd = path;
-

-
                    continue;
-
                } else {
-
                    PathBuf::from(&assertion.command)
-
                };
-
                log::debug!(target: "test", "Running `{}` in `{}`..", cmd.display(), self.cwd.display());
-

-
                if !self.cwd.exists() {
-
                    log::error!(target: "test", "Directory {} does not exist..", self.cwd.display());
-
                }
-
                let result = Command::new(cmd.clone())
-
                    .env_clear()
-
                    .envs(env::vars().filter(|(k, _)| k == "PATH"))
-
                    .envs(self.env.clone())
-
                    .current_dir(&self.cwd)
-
                    .args(&assertion.args)
-
                    .with_assert(assert.clone())
-
                    .output();
-

-
                match result {
-
                    Ok(output) => {
-
                        let assert = OutputAssert::new(output).with_assert(assert.clone());
-
                        match assertion.exit {
-
                            ExitStatus::Success => {
-
                                assert.stdout_matches(&assertion.expected).success();
-
                            }
-
                            ExitStatus::Failure => {
-
                                assert.stdout_matches(&assertion.expected).failure();
-
                            }
-
                        }
-
                    }
-
                    Err(err) => {
-
                        if err.kind() == io::ErrorKind::NotFound {
-
                            log::error!(target: "test", "Command `{}` does not exist..", cmd.display());
-
                        }
-
                        return Err(io::Error::new(
-
                            err.kind(),
-
                            format!("{err}: `{}`", cmd.display()),
-
                        ));
-
                    }
-
                }
-
            }
-
        }
-
        Ok(true)
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

-
    use pretty_assertions::assert_eq;
-

-
    #[test]
-
    fn test_parse() {
-
        let input = r#"
-
Let's try to track @dave and @sean:
-
```
-
$ rad track @dave
-
Tracking relationship established for @dave.
-
Nothing to do.
-

-
$ rad track @sean
-
Tracking relationship established for @sean.
-
Nothing to do.
-
```
-
Super, now let's move on to the next step.
-
```
-
$ rad sync
-
```
-
"#
-
        .trim()
-
        .as_bytes()
-
        .to_owned();
-

-
        let mut actual = TestFormula::new();
-
        actual
-
            .read(io::BufReader::new(io::Cursor::new(input)))
-
            .unwrap();
-

-
        let expected = TestFormula {
-
            cwd: PathBuf::new(),
-
            env: HashMap::new(),
-
            subs: Substitutions::new(),
-
            tests: vec![
-
                Test {
-
                    context: vec![String::from("Let's try to track @dave and @sean:")],
-
                    assertions: vec![
-
                        Assertion {
-
                            command: String::from("rad"),
-
                            args: vec![String::from("track"), String::from("@dave")],
-
                            expected: String::from(
-
                                "Tracking relationship established for @dave.\nNothing to do.\n\n",
-
                            ),
-
                            exit: ExitStatus::Success,
-
                        },
-
                        Assertion {
-
                            command: String::from("rad"),
-
                            args: vec![String::from("track"), String::from("@sean")],
-
                            expected: String::from(
-
                                "Tracking relationship established for @sean.\nNothing to do.\n",
-
                            ),
-
                            exit: ExitStatus::Success,
-
                        },
-
                    ],
-
                },
-
                Test {
-
                    context: vec![String::from("Super, now let's move on to the next step.")],
-
                    assertions: vec![Assertion {
-
                        command: String::from("rad"),
-
                        args: vec![String::from("sync")],
-
                        expected: String::new(),
-
                        exit: ExitStatus::Success,
-
                    }],
-
                },
-
            ],
-
        };
-

-
        assert_eq!(actual, expected);
-
    }
-

-
    #[test]
-
    fn test_run() {
-
        let input = r#"
-
Running a simple command such as `head`:
-
```
-
$ head -n 2 Cargo.toml
-
[package]
-
name = "radicle-cli"
-
```
-
"#
-
        .trim()
-
        .as_bytes()
-
        .to_owned();
-

-
        let mut formula = TestFormula::new();
-
        formula
-
            .cwd(env!("CARGO_MANIFEST_DIR"))
-
            .read(io::BufReader::new(io::Cursor::new(input)))
-
            .unwrap();
-
        formula.run().unwrap();
-
    }
-
}