Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli-test: Miscellaneous Improvements
Merged lorenz opened 2 months ago
2 files changed +85 -63 dd122f10 ed2b36cf
modified crates/radicle-cli-test/src/lib.rs
@@ -10,6 +10,11 @@ use snapbox::cmd::{Command, OutputAssert};
use snapbox::{Assert, Substitutions};
use thiserror::Error;

+
#[cfg(windows)]
+
const PATH_SEPARATOR: char = ';';
+
#[cfg(not(windows))]
+
const PATH_SEPARATOR: char = ':';
+

/// Used to ensure the build task is only run once.
static BUILD: sync::Once = sync::Once::new();

@@ -65,6 +70,8 @@ pub struct Assertion {
    expected: String,
    /// Expected exit status.
    exit: ExitStatus,
+
    /// Line number in the test file where this assertion is defined.
+
    line: usize,
}

#[derive(Debug, Default, PartialEq, Eq, Clone)]
@@ -164,52 +171,20 @@ pub struct TestFormula {
    tests: Vec<Test>,
    /// Output substitutions.
    subs: Substitutions,
-
    /// Binaries path.
-
    bins: Vec<PathBuf>,
}

impl TestFormula {
    pub fn new(cwd: PathBuf) -> Self {
-
        #[cfg(windows)]
-
        const PATH_SEPARATOR: char = ';';
-
        #[cfg(not(windows))]
-
        const PATH_SEPARATOR: char = ':';
-

        Self {
            cwd: cwd.clone(),
            env: HashMap::new(),
            homes: HashMap::new(),
            tests: Vec::new(),
            subs: Substitutions::new(),
-
            bins: env::var("PATH")
-
                .map(|env_path| {
-
                    let mut bins: Vec<PathBuf> =
-
                        env_path.split(PATH_SEPARATOR).map(PathBuf::from).collect();
-
                    // Add current working directory to `$PATH`,
-
                    // this makes it more convenient to execute scripts during testing.
-
                    bins.push(cwd);
-
                    bins
-
                })
-
                .unwrap_or_default(),
        }
    }

    pub fn build(&mut self, binaries: &[(&str, &str)]) -> &mut Self {
-
        let manifest = env::var("CARGO_MANIFEST_DIR").expect(
-
            "TestFormula::build: cannot build binaries: variable `CARGO_MANIFEST_DIR` is not set",
-
        );
-
        let profile = if cfg!(debug_assertions) {
-
            "debug"
-
        } else {
-
            "release"
-
        };
-
        let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or("target".to_string());
-
        let manifest = Path::new(manifest.as_str());
-
        let bins = manifest.join(&target_dir).join(profile);
-

-
        // Add the target dir to the beginning of the list we will use as `PATH`.
-
        self.bins.insert(0, bins);
-

        // We don't need to re-build every time the `build` function is called. Once is enough.
        BUILD.call_once(|| {
            use escargot::format::Message;
@@ -228,8 +203,6 @@ impl TestFormula {
                let results = escargot::CargoBuild::new()
                    .package(package)
                    .bin(binary)
-
                    .manifest_path(manifest.join("Cargo.toml"))
-
                    .target_dir(&target_dir)
                    .exec()
                    .unwrap();

@@ -304,7 +277,7 @@ impl TestFormula {
        let mut fenced = false; // Whether we're inside a fenced code block.
        let mut file: Option<(PathBuf, String)> = None; // Path and content of file created by this test block.

-
        for line in r.lines() {
+
        for (row, line) in r.lines().enumerate() {
            let line = line?;

            if line.starts_with("```") {
@@ -372,6 +345,7 @@ impl TestFormula {
                        } else {
                            ExitStatus::Success
                        },
+
                        line: row + 1,
                    });
                } else if let Some(a) = test.assertions.last_mut() {
                    a.expected.push_str(line.as_str());
@@ -426,7 +400,6 @@ impl TestFormula {
        let mut runner = TestRunner::new(self);

        fs::create_dir_all(&self.cwd)?;
-
        log::debug!(target: "test", "Using PATH {:?}", self.bins);

        // For each code block.
        for test in &self.tests {
@@ -441,13 +414,16 @@ impl TestFormula {
                        *arg = arg.replace(format!("${k}").as_str(), &v);
                    }
                }
-
                let path = assertion
+
                let location = assertion
                    .path
                    .file_name()
                    .map(|f| f.to_string_lossy().to_string())
+
                    .map(|f| f.strip_suffix(".md").unwrap_or(&f).to_owned())
+
                    .map(|f| f + ":" + assertion.line.to_string().as_str())
                    .unwrap_or(String::from("<none>"));
-
                let cmd = if assertion.command == "rad" {
-
                    snapbox::cmd::cargo_bin("rad")
+

+
                let (cmd, cmd_display) = if assertion.command == "rad" {
+
                    (snapbox::cmd::cargo_bin("rad"), "rad".to_string())
                } else if assertion.command == "cd" {
                    let arg = assertion.args.first().unwrap();
                    let dir: PathBuf = arg.into();
@@ -456,7 +432,7 @@ impl TestFormula {
                    // TODO: Add support for `..` and `/`
                    // TODO: Error if more than one args are given.

-
                    log::debug!(target: "test", "{path}: Running `cd {}`..", dir.display());
+
                    log::debug!(target: "test", "{location}: `cd {}`..", dir.display());

                    if !dir.exists() {
                        return Err(io::Error::new(
@@ -468,12 +444,11 @@ impl TestFormula {

                    continue;
                } else {
-
                    PathBuf::from(&assertion.command)
+
                    (PathBuf::from(&assertion.command), assertion.command.clone())
                };
-
                log::debug!(target: "test", "{path}: Running `{}` with {:?} in `{}`..", cmd.display(), assertion.args, run.path().display());

                if !run.path().exists() {
-
                    log::warn!(target: "test", "{path}: Directory {} does not exist. Creating..", run.path().display());
+
                    log::warn!(target: "test", "{location}: Directory {} does not exist. Creating..", run.path().display());
                    fs::create_dir_all(run.path())?;
                }

@@ -487,28 +462,44 @@ impl TestFormula {
                    vec![]
                };

-
                let bins = self
-
                    .bins
+
                let bins = bins(self.cwd.clone())
                    .iter()
                    .map(|p| p.as_os_str())
                    .collect::<Vec<_>>()
-
                    .join(ffi::OsStr::new(":"));
-
                let result = Command::new(cmd.clone())
+
                    .join(ffi::OsStr::new(&PATH_SEPARATOR.to_string()));
+

+
                let command = Command::new(cmd.clone())
                    .env_clear()
                    .env("PATH", &bins)
                    .env("RUST_BACKTRACE", "1")
                    .envs(jj_envs)
                    .envs(run.envs())
                    .current_dir(run.path())
-
                    .args(args)
-
                    .with_assert(assert.clone())
-
                    .output();
+
                    .args(args.clone())
+
                    .with_assert(assert.clone());
+

+
                log::debug!(target: "test", "{location}: `{} {}` @ {}", cmd_display, args.join(" "), run.path().display());
+
                log::trace!(target: "test", "{location}: {}", run.envs().map(|(k, v)| format!("{}={}", k, v)).collect::<Vec<_>>().join(", "));
+
                log::logger().flush();

-
                match result {
+
                // Even though it would be possible to use `Command::assert` to directly obtain
+
                // `OutputAssert`, we use `Command::output` to be able to handle `io::ErrorKind::NotFound`
+
                // separately and provide a more helpful error message in that case.
+
                match command.output() {
                    Ok(output) => {
                        let assert = OutputAssert::new(output).with_assert(assert.clone());
                        let expected = Self::map_spaced_brackets(&assertion.expected);

+
                        let expected = {
+
                            #[cfg(windows)]
+
                            const EXE: &str = ".exe";
+

+
                            #[cfg(unix)]
+
                            const EXE: &str = "";
+

+
                            expected.replace("[EXE]", EXE)
+
                        };
+

                        let matches = if test.stderr {
                            assert.stderr_matches(&expected)
                        } else {
@@ -525,11 +516,11 @@ impl TestFormula {
                    }
                    Err(err) => {
                        if err.kind() == io::ErrorKind::NotFound {
-
                            log::error!(target: "test", "{path}: Command `{}` does not exist..", cmd.display());
+
                            log::error!(target: "test", "{location}: Command `{}` does not exist..", cmd.display());
                        }
                        return Err(io::Error::new(
                            err.kind(),
-
                            format!("{path}: {err}: `{}`", cmd.display()),
+
                            format!("{location}: {err}: `{}`", cmd.display()),
                        ));
                    }
                }
@@ -540,6 +531,43 @@ impl TestFormula {
    }
}

+
/// Get the list of binary paths to use as `PATH` for the tests,
+
/// starting with the current working directory.
+
fn bins(cwd: PathBuf) -> Vec<PathBuf> {
+
    let mut bins: Vec<PathBuf> = env::var("PATH")
+
        .map(|env_path| env_path.split(PATH_SEPARATOR).map(PathBuf::from).collect())
+
        .unwrap_or_default();
+

+
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
+
        let profile = cfg!(debug_assertions)
+
            .then_some("debug")
+
            .unwrap_or("release");
+
        let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or("target".to_string());
+

+
        bins.push(
+
            PathBuf::from(manifest_dir.as_str())
+
                .join(&target_dir)
+
                .join(profile),
+
        )
+
    }
+

+
    #[cfg(windows)]
+
    {
+
        // Radicle CLI tests rely on various Unix coreutils
+
        // (such as `ls` and `touch`) being available.
+
        // On Windows, it is very likely that we can find them in the
+
        // following location.
+
        // Note that adding this path to the end of `$PATH` causes
+
        // no harm, even if the directory does not exist.
+
        bins.push(PathBuf::from(r#"C:\Program Files\Git\usr\bin"#));
+
    }
+

+
    // Add current working directory to `$PATH`,
+
    // this makes it more convenient to execute scripts during testing.
+
    bins.push(cwd);
+
    bins
+
}
+

#[cfg(test)]
mod tests {
    use super::*;
@@ -581,21 +609,13 @@ $ rad sync
            cwd: cwd.clone(),
            env: HashMap::new(),
            subs: Substitutions::new(),
-
            bins: {
-
                let mut bins: Vec<_> = env::var("PATH")
-
                    .unwrap_or_default()
-
                    .split(':')
-
                    .map(PathBuf::from)
-
                    .collect();
-
                bins.push(cwd);
-
                bins
-
            },
            tests: vec![
                Test {
                    context: vec![String::from("Let's try to track @dave and @sean:")],
                    home: None,
                    assertions: vec![
                        Assertion {
+
                            line: 3,
                            path: path.clone(),
                            command: String::from("rad"),
                            args: vec![String::from("track"), String::from("@dave")],
@@ -605,6 +625,7 @@ $ rad sync
                            exit: ExitStatus::Success,
                        },
                        Assertion {
+
                            line: 7,
                            path: path.clone(),
                            command: String::from("rad"),
                            args: vec![String::from("track"), String::from("@sean")],
@@ -624,6 +645,7 @@ $ rad sync
                    context: vec![String::from("Super, now let's move on to the next step.")],
                    home: Some("alice".to_owned()),
                    assertions: vec![Assertion {
+
                        line: 13,
                        path: path.clone(),
                        command: String::from("rad"),
                        args: vec![String::from("sync")],
modified crates/radicle-cli/examples/rad-help.md
@@ -9,7 +9,7 @@ Do you have feedback?
 - Mail <feedback@radicle.xyz>
   (Messages are automatically posted to the public #feedback channel on Zulip.)

-
Usage: rad <COMMAND>
+
Usage: rad[EXE] <COMMAND>

Commands:
  auth      Manage identities and profiles