Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Move CLI command tests to integration tests
Alexis Sellier committed 3 years ago
commit c33aa8758989e7d96fd35146697224cf72b803a7
parent e63f33489055962845c70976bd9a40d0d84c6b1c
7 files changed +304 -307
modified Cargo.toml
@@ -13,6 +13,7 @@ members = [
]
default-members = [
  "radicle",
+
  "radicle-cli",
  "radicle-cob",
  "radicle-crdt",
  "radicle-crypto",
modified radicle-cli/src/lib.rs
@@ -5,7 +5,3 @@ pub mod commands;
pub mod git;
pub mod project;
pub mod terminal;
-
#[cfg(test)]
-
mod testing;
-
#[cfg(test)]
-
mod tests;
deleted radicle-cli/src/testing.rs
@@ -1,245 +0,0 @@
-
#![allow(clippy::collapsible_else_if)]
-
use std::borrow::Cow;
-
use std::collections::HashMap;
-
use std::path::{Path, PathBuf};
-
use std::{fs, io, mem};
-

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

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

-
/// 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 program to run, eg. `git`.
-
    program: String,
-
    /// Program arguments, eg. `["push"]`.
-
    args: Vec<String>,
-
    /// Expected output (stdout or stderr).
-
    expected: String,
-
}
-

-
#[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 file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
-
        let contents = fs::read(path)?;
-
        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 (program, args) = parts.split_first().ok_or(Error::Parse)?;
-

-
                    test.assertions.push(Assertion {
-
                        program: program.to_owned(),
-
                        args: args.to_owned(),
-
                        expected: String::new(),
-
                    });
-
                } else if let Some(test) = test.assertions.last_mut() {
-
                    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(&self) -> Result<bool, io::Error> {
-
        let assert = Assert::new().substitutions(self.subs.clone());
-

-
        for test in &self.tests {
-
            for assertion in &test.assertions {
-
                let program = if assertion.program == "rad" {
-
                    snapbox::cmd::cargo_bin("rad")
-
                } else {
-
                    PathBuf::from(&assertion.program)
-
                };
-

-
                Command::new(program)
-
                    .envs(self.env.clone())
-
                    .args(&assertion.args)
-
                    .with_assert(assert.clone())
-
                    .assert()
-
                    .stdout_matches(&assertion.expected)
-
                    .success();
-
            }
-
        }
-
        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 {
-
                            program: String::from("rad"),
-
                            args: vec![String::from("track"), String::from("@dave")],
-
                            expected: String::from(
-
                                "Tracking relationship established for @dave.\nNothing to do.\n\n",
-
                            ),
-
                        },
-
                        Assertion {
-
                            program: String::from("rad"),
-
                            args: vec![String::from("track"), String::from("@sean")],
-
                            expected: String::from(
-
                                "Tracking relationship established for @sean.\nNothing to do.\n",
-
                            ),
-
                        },
-
                    ],
-
                },
-
                Test {
-
                    context: vec![String::from("Super, now let's move on to the next step.")],
-
                    assertions: vec![Assertion {
-
                        program: String::from("rad"),
-
                        args: vec![String::from("sync")],
-
                        expected: String::new(),
-
                    }],
-
                },
-
            ],
-
        };
-

-
        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();
-
    }
-
}
deleted radicle-cli/src/tests.rs
@@ -1 +0,0 @@
-
mod cli;
deleted radicle-cli/src/tests/cli.rs
@@ -1,57 +0,0 @@
-
use std::env;
-
use std::path::Path;
-

-
use radicle::profile::Profile;
-
use radicle::test::fixtures;
-

-
use crate::testing::TestFormula;
-

-
/// Run a CLI test file.
-
fn test(
-
    path: impl AsRef<Path>,
-
    profile: Option<Profile>,
-
) -> Result<(), Box<dyn std::error::Error>> {
-
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
-
    let tmp = tempfile::tempdir().unwrap();
-
    let home = if let Some(profile) = profile {
-
        profile.home.as_path().to_path_buf()
-
    } else {
-
        tmp.path().to_path_buf()
-
    };
-

-
    TestFormula::new()
-
        .env("RAD_PASSPHRASE", "radicle")
-
        .env("RAD_HOME", home.to_string_lossy())
-
        .env("RAD_DEBUG", "1")
-
        .file(base.join(path))?
-
        .run()?;
-

-
    Ok(())
-
}
-

-
/// Create a new user profile.
-
fn profile(home: &Path) -> Profile {
-
    // Set debug mode, to make test output more predictable.
-
    env::set_var("RAD_DEBUG", "1");
-
    // Setup a new user.
-
    Profile::init(home, "radicle").unwrap()
-
}
-

-
#[test]
-
fn rad_auth() {
-
    test("examples/rad-auth.md", None).unwrap();
-
}
-

-
#[test]
-
fn rad_init() {
-
    let home = tempfile::tempdir().unwrap();
-
    let working = tempfile::tempdir().unwrap();
-
    let profile = profile(home.path());
-

-
    // Setup a test repository.
-
    fixtures::repository(working.path());
-
    // Navigate to repository.
-
    env::set_current_dir(working.path()).unwrap();
-

-
    test("examples/rad-init.md", Some(profile)).unwrap();
-
}
added radicle-cli/tests/commands.rs
@@ -0,0 +1,58 @@
+
use std::env;
+
use std::path::Path;
+

+
use radicle::profile::Profile;
+
use radicle::test::fixtures;
+

+
mod framework;
+
use framework::TestFormula;
+

+
/// Run a CLI test file.
+
fn test(
+
    path: impl AsRef<Path>,
+
    profile: Option<Profile>,
+
) -> Result<(), Box<dyn std::error::Error>> {
+
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
+
    let tmp = tempfile::tempdir().unwrap();
+
    let home = if let Some(profile) = profile {
+
        profile.home.as_path().to_path_buf()
+
    } else {
+
        tmp.path().to_path_buf()
+
    };
+

+
    TestFormula::new()
+
        .env("RAD_PASSPHRASE", "radicle")
+
        .env("RAD_HOME", home.to_string_lossy())
+
        .env("RAD_DEBUG", "1")
+
        .file(base.join(path))?
+
        .run()?;
+

+
    Ok(())
+
}
+

+
/// Create a new user profile.
+
fn profile(home: &Path) -> Profile {
+
    // Set debug mode, to make test output more predictable.
+
    env::set_var("RAD_DEBUG", "1");
+
    // Setup a new user.
+
    Profile::init(home, "radicle").unwrap()
+
}
+

+
#[test]
+
fn rad_auth() {
+
    test("examples/rad-auth.md", None).unwrap();
+
}
+

+
#[test]
+
fn rad_init() {
+
    let home = tempfile::tempdir().unwrap();
+
    let working = tempfile::tempdir().unwrap();
+
    let profile = profile(home.path());
+

+
    // Setup a test repository.
+
    fixtures::repository(working.path());
+
    // Navigate to repository.
+
    env::set_current_dir(working.path()).unwrap();
+

+
    test("examples/rad-init.md", Some(profile)).unwrap();
+
}
added radicle-cli/tests/framework/mod.rs
@@ -0,0 +1,245 @@
+
#![allow(clippy::collapsible_else_if)]
+
use std::borrow::Cow;
+
use std::collections::HashMap;
+
use std::path::{Path, PathBuf};
+
use std::{fs, io, mem};
+

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

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

+
/// 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 program to run, eg. `git`.
+
    program: String,
+
    /// Program arguments, eg. `["push"]`.
+
    args: Vec<String>,
+
    /// Expected output (stdout or stderr).
+
    expected: String,
+
}
+

+
#[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 file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
+
        let contents = fs::read(path)?;
+
        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 (program, args) = parts.split_first().ok_or(Error::Parse)?;
+

+
                    test.assertions.push(Assertion {
+
                        program: program.to_owned(),
+
                        args: args.to_owned(),
+
                        expected: String::new(),
+
                    });
+
                } else if let Some(test) = test.assertions.last_mut() {
+
                    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(&self) -> Result<bool, io::Error> {
+
        let assert = Assert::new().substitutions(self.subs.clone());
+

+
        for test in &self.tests {
+
            for assertion in &test.assertions {
+
                let program = if assertion.program == "rad" {
+
                    snapbox::cmd::cargo_bin("rad")
+
                } else {
+
                    PathBuf::from(&assertion.program)
+
                };
+

+
                Command::new(program)
+
                    .envs(self.env.clone())
+
                    .args(&assertion.args)
+
                    .with_assert(assert.clone())
+
                    .assert()
+
                    .stdout_matches(&assertion.expected)
+
                    .success();
+
            }
+
        }
+
        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 {
+
                            program: String::from("rad"),
+
                            args: vec![String::from("track"), String::from("@dave")],
+
                            expected: String::from(
+
                                "Tracking relationship established for @dave.\nNothing to do.\n\n",
+
                            ),
+
                        },
+
                        Assertion {
+
                            program: String::from("rad"),
+
                            args: vec![String::from("track"), String::from("@sean")],
+
                            expected: String::from(
+
                                "Tracking relationship established for @sean.\nNothing to do.\n",
+
                            ),
+
                        },
+
                    ],
+
                },
+
                Test {
+
                    context: vec![String::from("Super, now let's move on to the next step.")],
+
                    assertions: vec![Assertion {
+
                        program: String::from("rad"),
+
                        args: vec![String::from("sync")],
+
                        expected: String::new(),
+
                    }],
+
                },
+
            ],
+
        };
+

+
        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();
+
    }
+
}