From 2d79eecd66605bad30680cc39f40410f4742bbef Mon Sep 17 00:00:00 2001 From: cloudhead Date: Thu, 3 Aug 2023 11:35:32 +0200 Subject: [PATCH] cli-test: Update framework with new features * Ability to switch between environments within the same test file * Ability to expand env vars in `cd` command --- radicle-cli-test/src/lib.rs | 173 ++++++++++++++++++++----- radicle-cli/examples/framework/home.md | 33 +++++ radicle-cli/tests/commands.rs | 46 +++++-- 3 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 radicle-cli/examples/framework/home.md diff --git a/radicle-cli-test/src/lib.rs b/radicle-cli-test/src/lib.rs index f2e395cc..0bf701f4 100644 --- a/radicle-cli-test/src/lib.rs +++ b/radicle-cli-test/src/lib.rs @@ -19,6 +19,8 @@ pub enum Error { Parse, #[error("invalid file path: {0:?}")] InvalidFilePath(String), + #[error("unknown home {0:?}")] + UnknownHome(String), #[error("test file not found: {0:?}")] TestNotFound(PathBuf), #[error("i/o: {0}")] @@ -44,6 +46,8 @@ pub struct Test { stderr: bool, /// Whether to expect an error status code. fail: bool, + /// Home directory under which to run this test. + home: Option, /// Local env vars to use just for this test. env: HashMap, } @@ -63,10 +67,92 @@ pub struct Assertion { exit: ExitStatus, } +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct Home { + name: Option, + path: PathBuf, + envs: HashMap, +} + +#[derive(Debug)] +pub struct TestRun { + home: Home, +} + +impl TestRun { + fn cd(&mut self, path: PathBuf) { + self.home.path = path; + } + + fn envs(&self) -> impl Iterator { + self.home.envs.iter() + } + + fn path(&self) -> PathBuf { + self.home.path.clone() + } +} + +#[derive(Debug)] +pub struct TestRunner<'a> { + cwd: Option, + homes: HashMap, + formula: &'a TestFormula, +} + +impl<'a> TestRunner<'a> { + fn new(formula: &'a TestFormula) -> Self { + Self { + cwd: None, + homes: formula.homes.clone(), + formula, + } + } + + fn run(&mut self, test: &'a Test) -> TestRun { + let mut envs = self.formula.env.clone(); + + if let Some(ref h) = test.home { + if let Some(home) = self.homes.get(h) { + envs.extend(home.envs.clone()); + envs.extend(test.env.clone()); + + let home = Home { + name: home.name.clone(), + path: home.path.clone(), + envs, + }; + return TestRun { home }; + } else { + panic!("TestRunner::test: home `~{h}` does not exist"); + } + } + envs.extend(test.env.clone()); + + TestRun { + home: Home { + name: None, + path: self.cwd.clone().unwrap_or_else(|| self.formula.cwd.clone()), + envs, + }, + } + } + + fn finish(&mut self, run: TestRun) { + if let Some(name) = &run.home.name { + self.homes.insert(name.clone(), run.home); + } else { + self.cwd = Some(run.home.path); + } + } +} + #[derive(Debug, Default, PartialEq, Eq)] pub struct TestFormula { /// Current working directory to run the test in. cwd: PathBuf, + /// User homes. + homes: HashMap, /// Environment to pass to the test. env: HashMap, /// Tests to run. @@ -78,10 +164,11 @@ pub struct TestFormula { } impl TestFormula { - pub fn new() -> Self { + pub fn new(cwd: PathBuf) -> Self { Self { - cwd: PathBuf::new(), + cwd, env: HashMap::new(), + homes: HashMap::new(), tests: Vec::new(), subs: Substitutions::new(), bins: env::var("PATH") @@ -140,13 +227,28 @@ impl TestFormula { self } - pub fn cwd(&mut self, path: impl AsRef) -> &mut Self { - self.cwd = path.as_ref().into(); + pub fn env(&mut self, key: impl ToString, val: impl ToString) -> &mut Self { + self.env.insert(key.to_string(), val.to_string()); self } - pub fn env(&mut self, key: impl Into, val: impl Into) -> &mut Self { - self.env.insert(key.into(), val.into()); + pub fn home( + &mut self, + user: impl ToString, + path: impl AsRef, + envs: impl IntoIterator, + ) -> &mut Self { + self.homes.insert( + user.to_string(), + Home { + name: Some(user.to_string()), + path: path.as_ref().to_path_buf(), + envs: envs + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + }, + ); self } @@ -198,22 +300,21 @@ impl TestFormula { } } else { for token in line.split_whitespace() { - if token.contains("stderr") { + if let Some(home) = token.strip_prefix('~') { + test.home = Some(home.to_owned()); + } else if let Some((key, val)) = token.split_once('=') { + test.env.insert(key.to_owned(), val.to_owned()); + } else if token.contains("stderr") { test.stderr = true; - } - if token.contains("fail") { + } else if token.contains("fail") { test.fail = true; - } - if let Some(path) = token.strip_prefix("./") { + } else if let Some(path) = token.strip_prefix("./") { file = Some(( PathBuf::from_str(path) .map_err(|_| Error::InvalidFilePath(token.to_owned()))?, String::new(), )); } - if let Some((key, val)) = token.split_once('=') { - test.env.insert(key.to_owned(), val.to_owned()); - } } } fenced = !fenced; @@ -291,11 +392,16 @@ impl TestFormula { pub fn run(&mut self) -> Result { let assert = Assert::new().substitutions(self.subs.clone()); + 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 { + let mut run = runner.run(test); + + // For each command. for assertion in &test.assertions { let path = assertion .path @@ -305,28 +411,34 @@ impl TestFormula { 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); + let mut arg = assertion.args.first().unwrap().clone(); + for (k, v) in run.envs() { + arg = arg.replace(&format!("${k}"), v); + } + let dir: PathBuf = arg.into(); + let dir = run.path().join(dir); // TODO: Add support for `..` and `/` // TODO: Error if more than one args are given. - if !path.exists() { + log::debug!(target: "test", "{path}: Running `cd {}`..", dir.display()); + + if !dir.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, - format!("cd: '{}' does not exist", path.display()), + format!("cd: '{}' does not exist", dir.display()), )); } - self.cwd = path; + run.cd(dir); continue; } else { PathBuf::from(&assertion.command) }; - log::debug!(target: "test", "{path}: Running `{}` with {:?} in `{}`..", cmd.display(), assertion.args, self.cwd.display()); + log::debug!(target: "test", "{path}: Running `{}` with {:?} in `{}`..", cmd.display(), assertion.args, run.path().display()); - if !self.cwd.exists() { - log::error!(target: "test", "{path}: Directory {} does not exist..", self.cwd.display()); + if !run.path().exists() { + log::error!(target: "test", "{path}: Directory {} does not exist..", run.path().display()); } let bins = self @@ -339,9 +451,8 @@ impl TestFormula { .env_clear() .env("PATH", &bins) .env("RUST_BACKTRACE", "1") - .envs(self.env.clone()) - .envs(test.env.clone()) - .current_dir(&self.cwd) + .envs(run.envs()) + .current_dir(run.path()) .args(&assertion.args) .with_assert(assert.clone()) .output(); @@ -376,6 +487,7 @@ impl TestFormula { } } } + runner.finish(run); } Ok(true) } @@ -409,13 +521,14 @@ $ rad sync .as_bytes() .to_owned(); - let mut actual = TestFormula::new(); + let mut actual = TestFormula::new(PathBuf::new()); let path = Path::new("test.md").to_path_buf(); actual .read(path.as_path(), io::BufReader::new(io::Cursor::new(input))) .unwrap(); let expected = TestFormula { + homes: HashMap::new(), cwd: PathBuf::new(), env: HashMap::new(), subs: Substitutions::new(), @@ -427,6 +540,7 @@ $ rad sync tests: vec![ Test { context: vec![String::from("Let's try to track @dave and @sean:")], + home: None, assertions: vec![ Assertion { path: path.clone(), @@ -453,6 +567,7 @@ $ rad sync }, Test { context: vec![String::from("Super, now let's move on to the next step.")], + home: None, assertions: vec![Assertion { path: path.clone(), command: String::from("rad"), @@ -484,9 +599,8 @@ name = "radicle-cli-test" .as_bytes() .to_owned(); - let mut formula = TestFormula::new(); + let mut formula = TestFormula::new(PathBuf::from_str(env!("CARGO_MANIFEST_DIR")).unwrap()); formula - .cwd(env!("CARGO_MANIFEST_DIR")) .read( Path::new("test.md"), io::BufReader::new(io::Cursor::new(input)), @@ -518,9 +632,8 @@ $ echo "[bug, good-first-issue]" .as_bytes() .to_owned(); - let mut formula = TestFormula::new(); + let mut formula = TestFormula::new(PathBuf::from_str(env!("CARGO_MANIFEST_DIR")).unwrap()); formula - .cwd(env!("CARGO_MANIFEST_DIR")) .read( Path::new("test.md"), io::BufReader::new(io::Cursor::new(input)), diff --git a/radicle-cli/examples/framework/home.md b/radicle-cli/examples/framework/home.md new file mode 100644 index 00000000..66795003 --- /dev/null +++ b/radicle-cli/examples/framework/home.md @@ -0,0 +1,33 @@ +``` +$ mkdir bin +$ cd bin +$ touch file.bin +``` + +``` ~bob +$ rad self --did +did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk +$ pwd +[..]/home/bob/.radicle +$ mkdir src +$ cd src +$ pwd +[..]/home/bob/.radicle/src +``` + +``` ~alice +$ rad self --did +did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi +$ pwd +[..]/home/alice/.radicle +``` + +``` ~bob +$ pwd +[..]/home/bob/.radicle/src +``` + +``` +$ ls +file.bin +``` diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index a8a2692f..8882ba0e 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -28,7 +28,6 @@ fn test<'a>( home: Option<&Home>, envs: impl IntoIterator, ) -> Result<(), Box> { - let base = Path::new(env!("CARGO_MANIFEST_DIR")); let tmp = tempfile::tempdir().unwrap(); let home = if let Some(home) = home { home.path().to_path_buf() @@ -36,14 +35,25 @@ fn test<'a>( tmp.path().to_path_buf() }; - TestFormula::new() + formula(cwd.as_ref(), test)? + .env("RAD_HOME", home.to_string_lossy()) + .envs(envs) + .run()?; + + Ok(()) +} + +fn formula(root: &Path, test: impl AsRef) -> Result> { + let mut formula = TestFormula::new(root.to_path_buf()); + let base = Path::new(env!("CARGO_MANIFEST_DIR")); + + formula .env("GIT_AUTHOR_DATE", "1671125284") .env("GIT_AUTHOR_EMAIL", "radicle@localhost") .env("GIT_AUTHOR_NAME", "radicle") .env("GIT_COMMITTER_DATE", "1671125284") .env("GIT_COMMITTER_EMAIL", "radicle@localhost") .env("GIT_COMMITTER_NAME", "radicle") - .env("RAD_HOME", home.to_string_lossy()) .env("RAD_PASSPHRASE", "radicle") .env("RAD_SEED", RAD_SEED) .env("EDITOR", "true") @@ -52,16 +62,13 @@ fn test<'a>( .env("USER", "alice") .env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284") .envs(git::env::GIT_DEFAULT_CONFIG) - .envs(envs) - .cwd(cwd) .build(&[ ("radicle-remote-helper", "git-remote-rad"), ("radicle-cli", "rad"), ]) - .file(base.join(test))? - .run()?; + .file(base.join(test))?; - Ok(()) + Ok(formula) } #[test] @@ -993,6 +1000,29 @@ fn rad_merge_after_update() { .unwrap(); } +fn framework_home() { + logger::init(log::Level::Debug); + + let mut environment = Environment::new(); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); + + formula(&environment.tmp(), "examples/framework/home.md") + .unwrap() + .home( + "alice", + alice.home.path(), + [("RAD_HOME", alice.home.path().display())], + ) + .home( + "bob", + bob.home.path(), + [("RAD_HOME", bob.home.path().display())], + ) + .run() + .unwrap(); +} + #[test] fn git_push_and_pull() { logger::init(log::Level::Debug);