use core::panic; use std::fs; use std::path::Path; use std::str::FromStr; use radicle::node::config::seeds::RADICLE_NODE_BOOTSTRAP_IRIS; use radicle::node::policy::Scope; use radicle::node::{Alias, Config, Handle as _, DEFAULT_TIMEOUT}; use radicle::prelude::RepoId; use radicle::profile; use radicle::profile::Home; use radicle::test::fixtures; #[allow(unused_imports)] use radicle_node::test::logger; mod util; use util::environment::Environment; use util::formula::formula; mod commands { mod checkout; mod clone; mod cob; mod git; mod id; mod inbox; mod init; mod issue; mod jj; mod node; mod patch; mod policy; mod remote; mod sync; } /// Run a CLI test file. pub(crate) fn test<'a>( test: impl AsRef, cwd: impl AsRef, home: Option<&Home>, envs: impl IntoIterator, ) -> Result<(), Box> { let tmp = tempfile::tempdir().unwrap(); let (unix_home, rad_home) = if let Some(home) = home { let unix_home = home.path().to_path_buf(); let unix_home = unix_home.parent().unwrap().to_path_buf(); (unix_home, home.path().to_path_buf()) } else { let mut rad_home = tmp.path().to_path_buf(); rad_home.push(".radicle"); (tmp.path().to_path_buf(), rad_home) }; formula(cwd.as_ref(), test)? .env("RAD_HOME", rad_home.to_string_lossy()) .env( "JJ_CONFIG", unix_home.join(".jjconfig.toml").to_string_lossy(), ) .envs(envs) .run()?; Ok(()) } /// A utility to check that some program can be executed with a `--version` /// argument and exits successfully. /// /// # Panics /// /// If there is an error executing the program other than the program not being /// found, or the program does not exit successfully. fn program_reports_version(program: &str) -> bool { use std::io::ErrorKind; use std::process::{Command, Stdio}; match Command::new(program) .arg("--version") .stdout(Stdio::null()) .status() { Err(e) if e.kind() == ErrorKind::NotFound => { log::warn!(target: "test", "`{program}` not found."); false } Err(e) => panic!("failure to execute `{program}`: {e}"), Ok(status) if status.success() => true, Ok(status) => panic!("executing `{program}` resulted in status {status}"), } } #[test] fn rad_help() { Environment::alice(["rad-help"]); } #[test] fn rad_auth() { test("examples/rad-auth.md", Path::new("."), None, []).unwrap(); } #[test] fn rad_key_mismatch() { let mut environment = Environment::new(); let alice = environment.profile("alice"); environment.repository(&alice); environment.test("rad-init", &alice).unwrap(); // Replace the public key with one that does not match the secret key anymore. fs::write(alice.home.path().join("keys").join("radicle.pub"), "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE6Ul/D+P0I/Hl1JVOWGS8Z589us9FqKQXWv8OMOpKCh snakeoil\n").unwrap(); environment.test("rad-key-mismatch", &alice).unwrap(); } #[test] fn rad_auth_errors() { test("examples/rad-auth-errors.md", Path::new("."), None, []).unwrap(); } #[test] fn rad_inspect() { let mut environment = Environment::new(); let profile = environment.profile("alice"); environment.repository(&profile); environment .tests(["rad-init", "rad-inspect"], &profile) .unwrap(); // NOTE: The next test runs without $RAD_HOME set. test( "examples/rad-inspect-noauth.md", environment.work(&profile), None, [], ) .unwrap(); } #[test] fn rad_config() { let mut environment = Environment::new(); let alias = Alias::new("alice"); let profile = environment.profile_with(profile::Config { preferred_seeds: vec![RADICLE_NODE_BOOTSTRAP_IRIS.clone().first().unwrap().clone()], ..profile::Config::new(alias) }); let working = tempfile::tempdir().unwrap(); test( "examples/rad-config.md", working.path(), Some(&profile.home), [], ) .unwrap(); } #[test] fn rad_warn_old_nodes() { Environment::alice(["rad-warn-old-nodes"]); } #[test] fn rad_clean() { let mut environment = Environment::new(); let alice = environment.node("alice"); let bob = environment.node("bob"); let eve = environment.node("eve"); let working = environment.tempdir().join("working"); // Setup a test project. let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); fixtures::repository(working.join("acme")); test( "examples/rad-init.md", working.join("acme"), Some(&alice.home), [], ) .unwrap(); let mut alice = alice.spawn(); let mut bob = bob.spawn(); let mut eve = eve.spawn(); alice.handle.seed(acme, Scope::All).unwrap(); eve.handle.seed(acme, Scope::Followed).unwrap(); bob.connect(&alice).converge([&alice]); eve.connect(&alice).converge([&alice]); eve.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.fork(acme, bob.home.path()).unwrap(); bob.announce(acme, 1, bob.home.path()).unwrap(); bob.has_remote_of(&acme, &alice.id); alice.has_remote_of(&acme, &bob.id); eve.has_remote_of(&acme, &alice.id); formula(&environment.tempdir(), "examples/rad-clean.md") .unwrap() .home( "alice", working.join("acme"), [("RAD_HOME", alice.home.path().display())], ) .home( "bob", working.join("bob"), [("RAD_HOME", bob.home.path().display())], ) .home( "eve", working.join("eve"), [("RAD_HOME", eve.home.path().display())], ) .run() .unwrap(); } #[test] fn rad_self() { let mut environment = Environment::new(); let alice = environment.node_with(Config { external_addresses: vec!["seed.alice.acme:8776".parse().unwrap()], ..Config::test(Alias::new("alice")) }); let working = environment.tempdir().join("working"); test("examples/rad-self.md", working, Some(&alice.home), []).unwrap(); } #[cfg(unix)] #[test] fn rad_diff() { if std::env::consts::OS == "macos" { // macOS's `sed` requires an argument for `-i`, which we don't provide // in the example. Providing it makes the test fail on Linux. // Since this command is deprecated anyway, we just skip macOS. return; } let tmp = tempfile::tempdir().unwrap(); fixtures::repository(&tmp); test("examples/rad-diff.md", tmp, None, []).unwrap(); } #[test] fn framework_home() { let mut environment = Environment::new(); let alice = environment.node("alice"); let bob = environment.node("bob"); formula(&environment.tempdir(), "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 rad_watch() { let mut environment = Environment::new(); let mut alice = environment.node("alice"); let bob = environment.node("bob"); let (repo, _) = environment.repository(&alice); let rid = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo); let alice = alice.spawn(); let mut bob = bob.spawn(); bob.connect(&alice).converge([&alice]); bob.clone(rid, environment.work(&bob)).unwrap(); formula(&environment.tempdir(), "examples/rad-watch.md") .unwrap() .home( "alice", environment.work(&alice), [("RAD_HOME", alice.home.path().display())], ) .home( "bob", environment.work(&bob).join("heartwood"), [("RAD_HOME", bob.home.path().display())], ) .run() .unwrap(); } #[test] fn rad_workflow() { let mut environment = Environment::new(); let alice = environment.node("alice"); let bob = environment.node("bob"); environment.repository(&alice); environment.test("workflow/1-new-project", &alice).unwrap(); let alice = alice.spawn(); let mut bob = bob.spawn(); bob.connect(&alice).converge([&alice]); environment.test("workflow/2-cloning", &bob).unwrap(); test( "examples/workflow/3-issues.md", environment.work(&bob).join("heartwood"), Some(&bob.home), [], ) .unwrap(); test( "examples/workflow/4-patching-contributor.md", environment.work(&bob).join("heartwood"), Some(&bob.home), [], ) .unwrap(); test( "examples/workflow/5-patching-maintainer.md", environment.work(&alice), Some(&alice.home), [], ) .unwrap(); test( "examples/workflow/6-pulling-contributor.md", environment.work(&bob).join("heartwood"), Some(&bob.home), [], ) .unwrap(); }