From faabbc3c61e958863e616da0b55783f8db82cab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 23 Oct 2026 00:23:17 +0200 Subject: [PATCH 1/2] tests: build the NAR fixture tree in a TempDir rstest fixture fixture_tree() built its tree under the shared temp dir, named by pid and a tag every caller had to keep unique, and never removed it. As an rstest fixture each test gets its own TempDir, dropped at the end. Change-Id: Ia860f1d3fea5c648833551e0e99ed6e6118ad9e8 --- src/devenv/mod.rs | 8 ++--- src/flake/mod.rs | 17 +++++---- src/remote/plan.rs | 8 ++--- src/source/local.rs | 8 ++--- src/source/scm/git.rs | 8 ++--- src/store/nar.rs | 84 +++++++++++++++++++++++-------------------- src/store/path.rs | 24 ++++++------- src/trace/mod.rs | 13 ++++--- 8 files changed, 86 insertions(+), 84 deletions(-) diff --git a/src/devenv/mod.rs b/src/devenv/mod.rs index 2a85f994..4e5ca82e 100644 --- a/src/devenv/mod.rs +++ b/src/devenv/mod.rs @@ -508,7 +508,8 @@ mod tests { use super::*; use crate::store::drv::load_derivation_from_store; #[cfg(unix)] - use crate::store::nar::tests::fixture_tree; + use crate::store::nar::tests::{Tree, tree}; + use rstest::rstest; fn fixture(name: &str) -> String { fs::read_to_string( @@ -617,9 +618,8 @@ mod tests { // TODO: the script is for a POSIX shell and the test names its paths that way. #[cfg(unix)] - #[test] - fn rc_script_rewrites_outputs_and_writes_attrs() { - let dir = fixture_tree("devenv-rc"); + #[rstest] + fn rc_script_rewrites_outputs_and_writes_attrs(#[from(tree)] dir: Tree) { let json: serde_json::Value = serde_json::from_str( r#"{ "bashFunctions": {}, diff --git a/src/flake/mod.rs b/src/flake/mod.rs index 0b39585a..b1f21848 100644 --- a/src/flake/mod.rs +++ b/src/flake/mod.rs @@ -632,7 +632,8 @@ mod tests { use super::*; use crate::source::scm; - use crate::store::nar::tests::fixture_tree; + use crate::store::nar::tests::{Tree, tree}; + use rstest::rstest; /// A path as the evaluator and a flake URL name it: `/C:/x` on Windows. fn internal(p: &Path) -> String { @@ -653,15 +654,14 @@ mod tests { assert!(parse_attr_path("a.\"b").is_err()); } - #[test] - fn refs_resolve_against_cwd_and_home() { - let d = fixture_tree("flakeref"); + #[rstest] + fn refs_resolve_against_cwd_and_home(#[from(tree)] d: Tree) { fs::write(d.join("flake.nix"), b"{ outputs = _: {}; }").unwrap(); fs::create_dir_all(d.join("sub")).unwrap(); fs::write(d.join("sub/flake.nix"), b"{ outputs = _: {}; }").unwrap(); let cwd = &sys::path_to_internal(&d); let r = parse_flake_ref(".#a.b", cwd, None).unwrap(); - assert_eq!(r.dir, d); + assert_eq!(r.dir, *d); assert_eq!(r.source, SourceRef::Path(d.clone())); assert_eq!(r.attr, ["a", "b"]); let r = parse_flake_ref("./sub", cwd, None).unwrap(); @@ -704,9 +704,8 @@ mod tests { assert!(!is_flake_ref("x.nix")); } - #[test] - fn a_pinned_checkout_is_its_revision() { - let d = fixture_tree("flakeref-git"); + #[rstest] + fn a_pinned_checkout_is_its_revision(#[from(tree)] d: Tree) { fs::write(d.join("flake.nix"), b"{ outputs = _: {}; }").unwrap(); fs::create_dir_all(d.join("sub")).unwrap(); fs::write(d.join("sub/flake.nix"), b"{ outputs = _: {}; }").unwrap(); @@ -716,7 +715,7 @@ mod tests { let url = format!("git+file://{}", internal(&d)); let r = parse_flake_ref(&format!("{url}?ref=HEAD&rev={rev}#a"), b"/", None).unwrap(); - assert_eq!(r.dir, d); + assert_eq!(r.dir, *d); assert_eq!(r.attr, ["a"]); assert_eq!( r.source, diff --git a/src/remote/plan.rs b/src/remote/plan.rs index e5fa8bae..f2eb292a 100644 --- a/src/remote/plan.rs +++ b/src/remote/plan.rs @@ -507,12 +507,12 @@ fn hash_sources(sources: &[&StorePath], infos: &[SourceOrigin]) -> Result Hash { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::sys; use crate::sys::fs::set_executable; - use rstest::rstest; + use rstest::{fixture, rstest}; use std::env; + use std::ops::Deref; use std::path::PathBuf; - use tempfile::tempdir; + use tempfile::{TempDir, tempdir}; - /// Builds the fixture tree used by the shell commands in the test comments: + /// The tree the shell commands in the test comments build: /// mkdir -p tree/sub; printf 'hello world\n' > tree/a.txt; /// printf '#!/bin/sh\necho hi\n' > tree/run.sh; chmod +x tree/run.sh; /// ln -s a.txt tree/link; : > tree/sub/empty; printf zzz > tree/sub/z - pub(crate) fn fixture_tree(tag: &str) -> PathBuf { - fixture_tree_with(tag, true) + pub(crate) struct Tree { + path: PathBuf, + _dir: TempDir, + } + + impl Deref for Tree { + type Target = PathBuf; + fn deref(&self) -> &PathBuf { + &self.path + } + } + + impl AsRef for Tree { + fn as_ref(&self) -> &Path { + &self.path + } } /// `exec` is whether `run.sh` has the executable bit, which Windows cannot set. - fn fixture_tree_with(tag: &str, exec: bool) -> PathBuf { - let dir = sys::temp_dir().join(format!("iets-nar-{}-{tag}", std::process::id())); - let _ = fs::remove_dir_all(&dir); - let tree = dir.join("tree"); + #[fixture] + pub(crate) fn tree(#[default(true)] exec: bool) -> Tree { + let dir = tempdir().unwrap(); + let tree = dir.path().join("tree"); fs::create_dir_all(tree.join("sub")).unwrap(); fs::write(tree.join("a.txt"), b"hello world\n").unwrap(); fs::write(tree.join("run.sh"), b"#!/bin/sh\necho hi\n").unwrap(); @@ -905,13 +919,15 @@ pub(crate) mod tests { symlink("a.txt", tree.join("link")).unwrap(); fs::write(tree.join("sub/empty"), b"").unwrap(); fs::write(tree.join("sub/z"), b"zzz").unwrap(); - tree + Tree { + path: tree, + _dir: dir, + } } // nix-store --dump tree | sha256sum ; nix-store --dump tree | wc -c - #[test] - fn directory_nar() { - let tree = fixture_tree_with("dir", false); + #[rstest] + fn directory_nar(#[with(false)] tree: Tree) { assert_eq!( nar_hash(&tree).unwrap().to_base16(), "5bd341428326a5608117fb128d3114fc3fb6c76de99cd19c10397bdad728beae" @@ -921,9 +937,8 @@ pub(crate) mod tests { // the same with `chmod +x tree/run.sh` #[cfg(unix)] - #[test] - fn directory_nar_with_an_executable() { - let tree = fixture_tree("dir-x"); + #[rstest] + fn directory_nar_with_an_executable(tree: Tree) { assert_eq!( nar_hash(&tree).unwrap().to_base16(), "9db4d4d873838f1fbb8f363841597d61244beebb19bd10b606ae2a6d4b940ede" @@ -932,9 +947,8 @@ pub(crate) mod tests { } // nix hash path tree ; nix hash path --base32 tree - #[test] - fn directory_nar_hash_formats() { - let tree = fixture_tree_with("fmt", false); + #[rstest] + fn directory_nar_hash_formats(#[with(false)] tree: Tree) { let h = nar_hash(&tree).unwrap(); assert_eq!( h.to_sri(), @@ -947,18 +961,16 @@ pub(crate) mod tests { } // nix-store --dump tree/a.txt | sha256sum - #[test] - fn regular_file_nar() { - let tree = fixture_tree("file"); + #[rstest] + fn regular_file_nar(tree: Tree) { let expected = "34ca3ac63094d1d5751f741101692a78f95eedf10744b088129fc324dfd0f603"; assert_eq!(nar_hash(&tree.join("a.txt")).unwrap().to_base16(), expected); assert_eq!(nar_hash_of_string(b"hello world\n").to_base16(), expected); } // nix-store --dump tree/run.sh | sha256sum - #[test] - fn script_nar() { - let tree = fixture_tree_with("script", false); + #[rstest] + fn script_nar(#[with(false)] tree: Tree) { assert_eq!( nar_hash(&tree.join("run.sh")).unwrap().to_base16(), "e519505edb9f77f7f02efefd3c9b29766fdd0f449e9bf932313d39ce249982e9" @@ -967,9 +979,8 @@ pub(crate) mod tests { // the same after `chmod +x tree/run.sh` #[cfg(unix)] - #[test] - fn executable_file_nar() { - let tree = fixture_tree("exec"); + #[rstest] + fn executable_file_nar(tree: Tree) { assert_eq!( nar_hash(&tree.join("run.sh")).unwrap().to_base16(), "5e0accf02cedede5e4119ffa15e79e79a5fb1fb9bc43c3d434f33227a14477a0" @@ -977,9 +988,8 @@ pub(crate) mod tests { } // nix-store --dump tree/link | sha256sum - #[test] - fn symlink_nar() { - let tree = fixture_tree("link"); + #[rstest] + fn symlink_nar(tree: Tree) { assert_eq!( nar_hash(&tree.join("link")).unwrap().to_base16(), "8d3c00cfa866e4d1b809772afeac240786246221eb2c574d69c4bba168834e81" @@ -1028,9 +1038,8 @@ pub(crate) mod tests { } // nix-store --dump tree | nix-store --restore out ; nix-store --dump out | sha256sum - #[test] - fn restore_roundtrip() { - let tree = fixture_tree("restore"); + #[rstest] + fn restore_roundtrip(tree: Tree) { let mut nar = Vec::new(); dump_path(&tree, &mut nar, &mut |_, _| true).unwrap(); let out = tree.with_file_name("restored"); @@ -1054,9 +1063,8 @@ pub(crate) mod tests { assert!(restore_path(&mut &b"nix-archive-2"[..], &out).is_err()); } - #[test] - fn filter_sees_types_and_paths() { - let tree = fixture_tree("filter"); + #[rstest] + fn filter_sees_types_and_paths(tree: Tree) { let mut seen = Vec::new(); let (_, _) = nar_hash_filtered(&tree, HashAlgo::Sha256, &mut |p, t| { seen.push((p.strip_prefix(&tree).unwrap().to_path_buf(), t)); diff --git a/src/store/path.rs b/src/store/path.rs index 57e6226d..670d69e4 100644 --- a/src/store/path.rs +++ b/src/store/path.rs @@ -441,7 +441,7 @@ mod tests { use super::*; use crate::store::nar::nar_hash; use crate::store::nar::nar_hash_filtered; - use crate::store::nar::tests::fixture_tree; + use crate::store::nar::tests::{Tree, tree}; fn set(paths: &[&str]) -> BTreeSet { paths.iter().map(|p| StorePath::parse(p).unwrap()).collect() @@ -482,10 +482,9 @@ mod tests { ); } - // nix-instantiate --eval -E '"${./tree}"' (tree from nar.rs fixture_tree) - #[test] - fn path_copy_directory() { - let tree = fixture_tree("pathcopy"); + // nix-instantiate --eval -E '"${./tree}"' (tree from the nar.rs fixture) + #[rstest] + fn path_copy_directory(tree: Tree) { let h = nar_hash(&tree).unwrap(); let p = make_fixed_output_path(IngestionMethod::Recursive, &h, "tree", &BTreeSet::new()) .unwrap(); @@ -498,9 +497,8 @@ mod tests { } // nix-instantiate --eval -E '"${./tree/a.txt}"' - #[test] - fn path_copy_file() { - let tree = fixture_tree("pathfile"); + #[rstest] + fn path_copy_file(tree: Tree) { let h = nar_hash(&tree.join("a.txt")).unwrap(); let p = make_fixed_output_path(IngestionMethod::Recursive, &h, "a.txt", &BTreeSet::new()) .unwrap(); @@ -511,9 +509,8 @@ mod tests { } // nix-instantiate --eval -E '"${builtins.filterSource (p: t: baseNameOf p != "sub") ./tree}"' - #[test] - fn filter_source() { - let tree = fixture_tree("filtersrc"); + #[rstest] + fn filter_source(tree: Tree) { let (h, _) = nar_hash_filtered(&tree, HashAlgo::Sha256, &mut |p, _| { p.file_name().unwrap() != "sub" }) @@ -528,9 +525,8 @@ mod tests { } // nix-instantiate --eval -E '"${builtins.path { path = ./tree; name = "mytree"; }}"' - #[test] - fn path_with_name() { - let tree = fixture_tree("pathname"); + #[rstest] + fn path_with_name(tree: Tree) { let h = nar_hash(&tree).unwrap(); let p = make_fixed_output_path(IngestionMethod::Recursive, &h, "mytree", &BTreeSet::new()) .unwrap(); diff --git a/src/trace/mod.rs b/src/trace/mod.rs index 6853925b..30deb9d2 100644 --- a/src/trace/mod.rs +++ b/src/trace/mod.rs @@ -847,7 +847,8 @@ mod tests { use std::time::Instant; - use crate::store::nar::tests::fixture_tree; + use crate::store::nar::tests::{Tree, tree}; + use rstest::rstest; #[test] fn dedupe_keeps_conflicting_values() { @@ -912,9 +913,8 @@ mod tests { /// The fused fingerprint must be byte-identical to the separate walk, or /// every cache entry a dump writes would miss on validation. - #[test] - fn dump_fingerprint_matches_the_separate_walk() { - let dir = fixture_tree("trace-fused"); + #[rstest] + fn dump_fingerprint_matches_the_separate_walk(#[from(tree)] dir: Tree) { for accepted in [ None, Some( @@ -937,9 +937,8 @@ mod tests { } } - #[test] - fn fingerprint_changes_with_content_size_and_seen_entries() { - let dir = fixture_tree("trace-fp"); + #[rstest] + fn fingerprint_changes_with_content_size_and_seen_entries(#[from(tree)] dir: Tree) { let (a, sa) = tree_fingerprint(&dir, None).unwrap(); let (b, sb) = tree_fingerprint(&dir, None).unwrap(); assert_eq!((a, sa), (b, sb)); From 1661a8f801057de53030833333e4770f180e9be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 23 Oct 2026 00:28:31 +0200 Subject: [PATCH 2/2] tests: give each test its iets home through an rstest fixture hermetic::iets() picked the home from the thread name, so a run from a thread the test spawned got a shared pid-named home, and homes were left in the target dir. The bin fixture owns a TempDir home for the test. The e2e Store and the cache_semantics Work own one too. Change-Id: I7e45e9a1341f0db401f365a8bca8885286e25bb9 --- tests/cache_hygiene.rs | 10 +- tests/cache_semantics.rs | 261 +++++++++++++++++++-------------------- tests/cli.rs | 125 ++++++++++--------- tests/diff.rs | 47 ++++--- tests/e2e/mod.rs | 6 +- tests/e2e_evaljobs.rs | 31 ++--- tests/events.rs | 53 ++++---- tests/fetchurl.rs | 41 +++--- tests/help_topics.rs | 47 +++---- tests/hermetic/mod.rs | 76 ++++++------ tests/langtests.rs | 12 +- tests/nixpkgs_eval.rs | 9 +- tests/profile.rs | 54 ++++---- tests/progress.rs | 10 +- tests/pure.rs | 52 ++++---- tests/store_dir.rs | 17 ++- 16 files changed, 455 insertions(+), 396 deletions(-) diff --git a/tests/cache_hygiene.rs b/tests/cache_hygiene.rs index 5040513a..6c41b851 100644 --- a/tests/cache_hygiene.rs +++ b/tests/cache_hygiene.rs @@ -5,6 +5,9 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::collections::HashSet; use std::fs::{self, File}; use std::process::Command; @@ -297,8 +300,8 @@ fn an_input_observation_marks_its_entry_as_used() { ); } -#[test] -fn cache_gc_removes_entries_past_the_ttl() { +#[rstest] +fn cache_gc_removes_entries_past_the_ttl(bin: Iets) { let dir = tempdir().unwrap(); let path = dir.path().to_str().expect("utf-8 path"); let store = Store::locate(Some(path)).expect("store"); @@ -317,7 +320,8 @@ fn cache_gc_removes_entries_past_the_ttl() { assert!(ir.join(&ir_key[2..]).exists(), "an IR entry to sweep"); let gc = |args: &[&str]| { - let out = hermetic::iets() + let out = bin + .cmd() .args(["cache", "gc", "--cache-dir"]) .arg(dir.path()) .args(args) diff --git a/tests/cache_semantics.rs b/tests/cache_semantics.rs index 672509b6..f0d34816 100644 --- a/tests/cache_semantics.rs +++ b/tests/cache_semantics.rs @@ -5,6 +5,9 @@ mod hermetic; +use hermetic::Iets; +use rstest::{fixture, rstest}; + use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -23,15 +26,18 @@ struct Run { struct Work { dir: TempDir, + bin: Iets, } -impl Work { - fn new() -> Work { - Work { - dir: tempfile::tempdir().unwrap(), - } +#[fixture] +fn work() -> Work { + Work { + dir: tempfile::tempdir().unwrap(), + bin: hermetic::bin(), } +} +impl Work { fn path(&self, rel: &str) -> PathBuf { self.dir.path().join(rel) } @@ -48,7 +54,7 @@ impl Work { } fn iets(&self) -> Command { - let mut c = hermetic::iets(); + let mut c = self.bin.cmd(); c.env("IETS_CACHE_DIR", self.path("cache")) .env("CACHETEST_VAR", "one") .current_dir(env!("CARGO_MANIFEST_DIR")); @@ -142,14 +148,13 @@ fn strs(v: &[String]) -> Vec<&str> { v.iter().map(String::as_str).collect() } -#[test] -fn hits_and_misses() { - let w = Work::new(); - let main = main_nix(&w); +#[rstest] +fn hits_and_misses(work: Work) { + let main = main_nix(&work); let main = strs(&main); - let lib = w.s("src/lib.nix"); + let lib = work.s("src/lib.nix"); - let r = w.eval(&main); + let r = work.eval(&main); assert!( r.rc == 0 && r.status.contains("miss (no entry)") && r.status.contains("wrote"), "{}", @@ -157,7 +162,7 @@ fn hits_and_misses() { ); assert_eq!(r.why, "", "a cold run reports no invalidation"); let first = r.out; - let r = w.eval(&main); + let r = work.eval(&main); assert!( r.status.contains("cache: hit") && r.out == first, "{}", @@ -165,8 +170,8 @@ fn hits_and_misses() { ); assert_eq!(r.why, ""); - w.write("src/lib.nix", "40 + 3\n"); - let r = w.eval(&main); + work.write("src/lib.nix", "40 + 3\n"); + let r = work.eval(&main); assert!( r.status.contains("miss (") && r.status.contains("lib.nix changed"), "{}", @@ -177,41 +182,41 @@ fn hits_and_misses() { assert_eq!(r.why, changed); // the same message whichever thread saw the change first for _ in 0..5 { - w.write("src/lib.nix", "40 + 4\n"); - assert_eq!(w.eval(&main).why, changed); - w.write("src/lib.nix", "40 + 3\n"); - assert_eq!(w.eval(&main).why, changed); + work.write("src/lib.nix", "40 + 4\n"); + assert_eq!(work.eval(&main).why, changed); + work.write("src/lib.nix", "40 + 3\n"); + assert_eq!(work.eval(&main).why, changed); } touch(946_684_800, &[&lib]); - let r = w.eval(&main); + let r = work.eval(&main); assert!( r.status.contains("cache: hit") && r.status.contains(" 1 rehashed"), "{}", r.status ); - let r = w.eval(&main); + let r = work.eval(&main); assert!( r.status.contains("cache: hit") && r.status.contains(" 0 rehashed"), "{}", r.status ); - w.write("src/dir/two", "y\n"); - let r = w.eval(&main); + work.write("src/dir/two", "y\n"); + let r = work.eval(&main); assert!( r.status.contains("miss (") && r.status.contains("directory"), "{}", r.status ); assert!(r.out.contains("\"two\"")); - let dir = w.s("src/dir"); + let dir = work.s("src/dir"); assert_eq!( r.why, format!("re-evaluating: directory {dir} has different entries") ); - let r = w.eval_with(&[("CACHETEST_VAR", "two")], &main); + let r = work.eval_with(&[("CACHETEST_VAR", "two")], &main); assert!( r.status .contains("miss (environment variable $CACHETEST_VAR changed)"), @@ -223,15 +228,15 @@ fn hits_and_misses() { r.why, "re-evaluating: environment variable $CACHETEST_VAR changed" ); - let r = w.eval(&main); + let r = work.eval(&main); assert!( r.status.contains("CACHETEST_VAR"), "one entry per key: {}", r.status ); - let inc2 = format!("inc={}", w.s("inc2")); - let r = w.eval(&["-I", &inc2, main[2]]); + let inc2 = format!("inc={}", work.s("inc2")); + let r = work.eval(&["-I", &inc2, main[2]]); assert!( r.status.contains("miss (no entry)") && r.out.contains("d = \"inc2\""), "{}", @@ -239,25 +244,24 @@ fn hits_and_misses() { ); // a failed evaluation writes nothing, and the fixed file is no stale hit - w.write("src/lib.nix", "throw \"boom\"\n"); - let r = w.eval(&main); + work.write("src/lib.nix", "throw \"boom\"\n"); + let r = work.eval(&main); assert!(r.rc != 0 && !r.status.contains("wrote"), "{}", r.status); - w.write("src/lib.nix", "1\n"); - let r = w.eval(&main); + work.write("src/lib.nix", "1\n"); + let r = work.eval(&main); assert!(r.rc == 0 && r.status.contains("miss ("), "{}", r.status); } /// `cp -p` in place keeps the size, the mtime and the inode. Only ctime says /// the bytes are new. -#[test] -fn a_copy_in_place_misses() { - let w = Work::new(); - let main = w.write("ctime/main.nix", "import ./lib.nix\n"); - let lib = w.write("ctime/lib.nix", "\"AAAA\"\n"); - let other = w.write("ctime/other.nix", "\"BBBB\"\n"); +#[rstest] +fn a_copy_in_place_misses(work: Work) { + let main = work.write("ctime/main.nix", "import ./lib.nix\n"); + let lib = work.write("ctime/lib.nix", "\"AAAA\"\n"); + let other = work.write("ctime/other.nix", "\"BBBB\"\n"); touch(1_577_836_800, &[&lib, &other]); - let cc = w.s("ctimecache"); - let c1 = w.eval(&["--cache-dir", &cc, &main]); + let cc = work.s("ctimecache"); + let c1 = work.eval(&["--cache-dir", &cc, &main]); assert!( Command::new("cp") .args(["-p", &other, &lib]) @@ -265,7 +269,7 @@ fn a_copy_in_place_misses() { .unwrap() .success() ); - let c2 = w.eval(&["--cache-dir", &cc, &main]); + let c2 = work.eval(&["--cache-dir", &cc, &main]); assert_eq!(c1.out, "\"AAAA\""); assert_eq!(c2.out, "\"BBBB\""); assert!(c2.status.contains("miss ("), "{}", c2.status); @@ -273,25 +277,24 @@ fn a_copy_in_place_misses() { /// The compiled-file cache folds `builtins.currentSystem` into an entry, and /// `--pure-eval` removes that name, so an impure run must not answer a pure one. -#[test] -fn a_pure_run_never_reads_an_impure_entry() { - let w = Work::new(); +#[rstest] +fn a_pure_run_never_reads_an_impure_entry(work: Work) { let pad: String = (1..=30) .map(|i| format!("# comment line {i} padding padding padding padding\n")) .collect(); - w.write( + work.write( "pure/lib.nix", &format!("{pad}{{ dflt = builtins.currentSystem or \"fallback\"; }}\n"), ); - w.write( + work.write( "pure/flake.nix", "{ outputs = _: { dflt = (import ./lib.nix).dflt; }; }\n", ); - w.write("pure/flake.lock", LOCK); - let pc = w.s("purecache"); - let flake = format!("path:{}#dflt", w.s("pure")); + work.write("pure/flake.lock", LOCK); + let pc = work.s("purecache"); + let flake = format!("path:{}#dflt", work.s("pure")); let ev = |mode: &str| { - let mut c = w.iets(); + let mut c = work.iets(); c.args(["eval", "--cache-dir", &pc, mode, &flake]); run(c).out }; @@ -304,49 +307,47 @@ fn a_pure_run_never_reads_an_impure_entry() { assert_eq!(p3, "\"fallback\""); } -#[test] -fn uncacheable_runs_write_nothing() { - let w = Work::new(); - let r = w.eval(&["-E", "builtins.currentTime > 0"]); +#[rstest] +fn uncacheable_runs_write_nothing(work: Work) { + let r = work.eval(&["-E", "builtins.currentTime > 0"]); assert!(r.status.contains("not written"), "{}", r.status); - let r = w.eval(&["-E", "builtins.currentTime > 0"]); + let r = work.eval(&["-E", "builtins.currentTime > 0"]); assert!(r.status.contains("miss (no entry)"), "{}", r.status); let fetch = r#"(builtins.tryEval (builtins.fetchTarball "https://x/y.tar.gz")).success"#; - let r = w.eval(&["-E", fetch]); + let r = work.eval(&["-E", fetch]); assert!(r.rc != 0 && !r.status.contains("wrote"), "{}", r.status); // `nix eval` prints «error: …» for a read that failed, with exit 0, and // that read left no observation - let e = w.write( + let e = work.write( "errs/e.nix", "{ b = import ./missing.nix; c = builtins.readDir ./missingdir; }\n", ); let nix_eval = || { - let mut c = w.iets(); + let mut c = work.iets(); c.args(["nix", "eval", "--file", &e]); c.output().unwrap() }; let e1 = nix_eval(); - w.write("errs/missing.nix", "\"B\"\n"); - fs::create_dir(w.path("errs/missingdir")).unwrap(); + work.write("errs/missing.nix", "\"B\"\n"); + fs::create_dir(work.path("errs/missingdir")).unwrap(); let e2 = nix_eval(); assert!(e1.status.success() && String::from_utf8_lossy(&e1.stdout).contains("«error:")); assert!(e2.status.success() && !String::from_utf8_lossy(&e2.stdout).contains("«error:")); } -#[test] -fn flags_that_turn_the_cache_off() { - let w = Work::new(); - let main = main_nix(&w); +#[rstest] +fn flags_that_turn_the_cache_off(work: Work) { + let main = main_nix(&work); let main = strs(&main); - w.eval(&main); - let cache = w.path("cache"); + work.eval(&main); + let cache = work.path("cache"); let before = fingerprint(&cache); let mut args = vec!["--no-tracing-cache"]; args.extend(&main); - assert_eq!(w.eval(&args).status, ""); - w.eval(&[ + assert_eq!(work.eval(&args).status, ""); + work.eval(&[ "--no-tracing-cache", "-E", "import /nonexistent/nocache-probe.nix", @@ -356,7 +357,7 @@ fn flags_that_turn_the_cache_off() { before, "--no-tracing-cache touched the store" ); - assert_eq!(w.eval_with(&[("IETS_CACHE", "0")], &main).status, ""); + assert_eq!(work.eval_with(&[("IETS_CACHE", "0")], &main).status, ""); // each flag turns off the store it names, and no other let cases: [(&[&str], bool, bool); 4] = [ @@ -366,29 +367,28 @@ fn flags_that_turn_the_cache_off() { (&["--no-cache"], false, false), ]; for (i, (flags, trace, ir)) in cases.into_iter().enumerate() { - let dir = w.s(&format!("stores{i}")); + let dir = work.s(&format!("stores{i}")); let mut args = vec!["--cache-dir", &dir]; args.extend(flags); args.extend(&main); - assert_eq!(w.eval(&args).rc, 0); + assert_eq!(work.eval(&args).rc, 0); let got = ( - w.path(&format!("stores{i}/trace")).exists(), - w.path(&format!("stores{i}/ir")).exists(), + work.path(&format!("stores{i}/trace")).exists(), + work.path(&format!("stores{i}/ir")).exists(), ); assert_eq!(got, (trace, ir), "{flags:?}"); } } /// A lint warning is an effect: the entry records it and a hit prints it again. -#[test] -fn a_lint_warning_is_replayed_on_a_hit() { - let w = Work::new(); - let url = w.write("src/url.nix", "http://z.example\n"); - let w1 = w.eval(&["--lint", "url-literal=warn", &url]); - let w2 = w.eval(&["--lint", "url-literal=warn", &url]); +#[rstest] +fn a_lint_warning_is_replayed_on_a_hit(work: Work) { + let url = work.write("src/url.nix", "http://z.example\n"); + let w1 = work.eval(&["--lint", "url-literal=warn", &url]); + let w2 = work.eval(&["--lint", "url-literal=warn", &url]); assert!(w1.status.contains("miss"), "{}", w1.status); assert!(w2.status.contains("cache: hit"), "{}", w2.status); - let o = w + let o = work .iets() .args([ "eval", @@ -412,13 +412,12 @@ fn a_lint_warning_is_replayed_on_a_hit() { } /// The file on the command line is in the read set, not in the key. -#[test] -fn the_file_argument_keeps_one_entry() { - let w = Work::new(); - let kd = w.s("keycache"); - let top = w.write("key/top.nix", "import ./dep.nix\n"); - w.write("key/dep.nix", "41 + 1\n"); - let ev = || w.eval(&["--cache-dir", &kd, &top]); +#[rstest] +fn the_file_argument_keeps_one_entry(work: Work) { + let kd = work.s("keycache"); + let top = work.write("key/top.nix", "import ./dep.nix\n"); + work.write("key/dep.nix", "41 + 1\n"); + let ev = || work.eval(&["--cache-dir", &kd, &top]); let k = ev(); assert!( k.status.contains("miss (no entry)") && k.why.is_empty(), @@ -426,10 +425,10 @@ fn the_file_argument_keeps_one_entry() { k.status ); assert!(ev().status.contains("cache: hit")); - w.write("key/top.nix", "import ./dep.nix # edited\n"); + work.write("key/top.nix", "import ./dep.nix # edited\n"); assert_eq!(ev().why, format!("re-evaluating: {top} changed")); assert!(ev().status.contains("cache: hit")); - assert_eq!(entries(&w.path("keycache/trace")), 1); + assert_eq!(entries(&work.path("keycache/trace")), 1); fs::remove_file(&top).unwrap(); let k = ev(); assert!(k.rc != 0); @@ -438,15 +437,14 @@ fn the_file_argument_keeps_one_entry() { /// A flake's directory is the key, and flake.nix and flake.lock are read like /// any other file. -#[test] -fn a_flake_keeps_one_entry() { - let w = Work::new(); - let fd = w.s("flakecache"); - let fl = w.s("fl"); - w.write("fl/flake.nix", "{ outputs = _: { x = 41 + 1; }; }\n"); - w.write("fl/flake.lock", LOCK); +#[rstest] +fn a_flake_keeps_one_entry(work: Work) { + let fd = work.s("flakecache"); + let fl = work.s("fl"); + work.write("fl/flake.nix", "{ outputs = _: { x = 41 + 1; }; }\n"); + work.write("fl/flake.lock", LOCK); let flake = format!("path:{fl}#x"); - let ev = || w.eval(&["--cache-dir", &fd, &flake]); + let ev = || work.eval(&["--cache-dir", &fd, &flake]); let f = ev(); assert!( f.rc == 0 && f.status.contains("miss (no entry)") && f.why.is_empty(), @@ -454,26 +452,26 @@ fn a_flake_keeps_one_entry() { f.status ); assert!(ev().status.contains("cache: hit")); - w.write("fl/flake.nix", "{ outputs = _: { x = 40 + 2; }; }\n"); + work.write("fl/flake.nix", "{ outputs = _: { x = 40 + 2; }; }\n"); let f = ev(); assert_eq!(f.why, format!("re-evaluating: source tree {fl} changed")); assert_eq!(f.out, "42"); - w.write("fl/flake.lock", &LOCK.replace('\n', " \n")); + work.write("fl/flake.lock", &LOCK.replace('\n', " \n")); assert_eq!(ev().why, format!("re-evaluating: {fl}/flake.lock changed")); - assert_eq!(entries(&w.path("flakecache/trace")), 1); + assert_eq!(entries(&work.path("flakecache/trace")), 1); // a `path:` flake's lastModified is the newest mtime of its tree - let lm = w.s("lm"); - let nix = w.write( + let lm = work.s("lm"); + let nix = work.write( "lm/flake.nix", "{ outputs = { self }: { x = self.lastModified; }; }\n", ); - let lock = w.write("lm/flake.lock", LOCK); + let lock = work.write("lm/flake.lock", LOCK); touch(1_577_836_800, &[&nix, &lock, &lm]); let flake = format!("path:{lm}#x"); - let f = w.eval(&["--cache-dir", &fd, &flake]); + let f = work.eval(&["--cache-dir", &fd, &flake]); touch(1_609_459_200, &[&nix]); - let g = w.eval(&["--cache-dir", &fd, &flake]); + let g = work.eval(&["--cache-dir", &fd, &flake]); assert_eq!(f.out, "1577836800"); assert_eq!(g.out, "1609459200"); assert_eq!( @@ -483,19 +481,18 @@ fn a_flake_keeps_one_entry() { } /// iets owns only the store subdirectories it makes in `--cache-dir`. -#[test] -fn the_cache_dir_keeps_what_it_did_not_make() { - let w = Work::new(); - let unrelated = w.write("mine/unrelated.txt", "PRECIOUS\n"); - let mine = w.s("mine"); - assert_eq!(w.eval(&["--cache-dir", &mine, "-E", "2"]).rc, 0); +#[rstest] +fn the_cache_dir_keeps_what_it_did_not_make(work: Work) { + let unrelated = work.write("mine/unrelated.txt", "PRECIOUS\n"); + let mine = work.s("mine"); + assert_eq!(work.eval(&["--cache-dir", &mine, "-E", "2"]).rc, 0); assert_eq!(fs::read_to_string(&unrelated).unwrap(), "PRECIOUS\n"); - assert!(w.path("mine/trace").exists() && w.path("mine/ir").exists()); + assert!(work.path("mine/trace").exists() && work.path("mine/ir").exists()); // a directory that is not iets's is refused, not wiped - let notes = w.write("theirs/trace/notes.txt", "PRECIOUS\n"); - let theirs = w.s("theirs"); - let o = w + let notes = work.write("theirs/trace/notes.txt", "PRECIOUS\n"); + let theirs = work.s("theirs"); + let o = work .iets() .args([ "eval", @@ -513,8 +510,8 @@ fn the_cache_dir_keeps_what_it_did_not_make() { assert_eq!(fs::read_to_string(¬es).unwrap(), "PRECIOUS\n"); // --ttl in days must fit in seconds - let ttl = w.s("ttl"); - let g = w + let ttl = work.s("ttl"); + let g = work .iets() .args([ "cache", @@ -531,12 +528,11 @@ fn the_cache_dir_keeps_what_it_did_not_make() { } /// A hit prints the same output for a lang test with paths and positions. -#[test] -fn a_hit_prints_paths_and_positions_the_same() { - let w = Work::new(); +#[rstest] +fn a_hit_prints_paths_and_positions_the_same(work: Work) { let t = "tests/lang/eval-okay-getattrpos.nix"; - let o1 = w.iets().args(["eval", "--strict", t]).output().unwrap(); - let r2 = w + let o1 = work.iets().args(["eval", "--strict", t]).output().unwrap(); + let r2 = work .iets() .args(["eval", "--strict", "--timings", t]) .output() @@ -547,21 +543,20 @@ fn a_hit_prints_paths_and_positions_the_same() { } /// search takes the same cache and worker flags as eval. -#[test] -fn search_uses_the_cache() { - let w = Work::new(); - let pkgs = w.write( +#[rstest] +fn search_uses_the_cache(work: Work) { + let pkgs = work.write( "src/pkgs.nix", "{ }: { hello = derivation { name = \"hello-1\"; system = builtins.currentSystem; builder = \"/bin/sh\"; args = [ ]; } // { meta.description = \"greets\"; }; }\n", ); let search = |dir: &str, extra: &[&str]| { - let mut c = w.iets(); + let mut c = work.iets(); c.args(["search", "-f", &pkgs, "--cache-dir", dir, "--timings"]) .args(extra) .arg("hello"); run(c) }; - let sdir = w.s("searchcache"); + let sdir = work.s("searchcache"); let s1 = search(&sdir, &[]); let s2 = search(&sdir, &[]); assert!(s1.rc == 0 && s1.status.contains("miss"), "{}", s1.status); @@ -570,16 +565,16 @@ fn search_uses_the_cache() { "{}", s2.status ); - let before = fingerprint(&w.path("searchcache")); + let before = fingerprint(&work.path("searchcache")); let s3 = search(&sdir, &["--no-tracing-cache"]); assert!(s3.rc == 0 && s3.status.is_empty() && s3.out == s1.out); - assert_eq!(fingerprint(&w.path("searchcache")), before); - let idir = w.s("searchcache-noir"); + assert_eq!(fingerprint(&work.path("searchcache")), before); + let idir = work.s("searchcache-noir"); assert_eq!(search(&idir, &["--no-ir-cache"]).rc, 0); - assert!(!w.path("searchcache-noir/ir").exists()); + assert!(!work.path("searchcache-noir/ir").exists()); let s5 = search(&sdir, &["--workers", "2"]); assert!(s5.rc == 0 && s5.out == s1.out); - let o = w + let o = work .iets() .args(["search", "-f", &pkgs, "--workers", "0", "hello"]) .output() diff --git a/tests/cli.rs b/tests/cli.rs index 8e91edc7..b2d71e59 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -3,14 +3,16 @@ mod hermetic; +use hermetic::{Iets, bin}; + use rstest::rstest; use std::path::Path; use std::process::{Command, Output}; use std::{env, fs}; use tempfile::tempdir; -fn iets(args: &[&str]) -> Output { - hermetic::iets().args(args).output().expect("run iets") +fn run(bin: &Iets, args: &[&str]) -> Output { + bin.cmd().args(args).output().expect("run iets") } fn text(b: &[u8]) -> String { @@ -24,8 +26,8 @@ fn run_complete(mut c: Command, words: &[&str]) -> String { text(&o.stdout) } -#[test] -fn help_and_version_go_to_stdout() { +#[rstest] +fn help_and_version_go_to_stdout(bin: Iets) { for args in [ &["--help"][..], &["-h"], @@ -38,17 +40,17 @@ fn help_and_version_go_to_stdout() { &["version"], &["--version"], ] { - let o = iets(args); + let o = run(&bin, args); assert_eq!(o.status.code(), Some(0), "{args:?}"); assert!(o.stderr.is_empty(), "{args:?}: {}", text(&o.stderr)); assert!(!o.stdout.is_empty(), "{args:?}"); } - let o = iets(&["--help"]); + let o = run(&bin, &["--help"]); assert!(text(&o.stdout).contains(" eval-jobs ")); - let o = iets(&["eval", "--help"]); + let o = run(&bin, &["eval", "--help"]); assert!(text(&o.stdout).starts_with("usage: iets eval ")); assert!(text(&o.stdout).contains("--strict")); - let v = text(&iets(&["--version"]).stdout); + let v = text(&run(&bin, &["--version"]).stdout); assert_eq!(v.trim_end(), format!("iets {}", env!("CARGO_PKG_VERSION"))); } @@ -67,8 +69,8 @@ fn help_and_version_go_to_stdout() { #[case::build_nothing(&["build"], "nothing to evaluate")] #[case::build_flag(&["build", "--what"], "unknown flag '--what'")] #[case::subcommand_typo(&["cache", "dunp"], "did you mean 'dump'?")] -fn a_bad_command_line_names_the_problem(#[case] args: &[&str], #[case] want: &str) { - let o = iets(args); +fn a_bad_command_line_names_the_problem(bin: Iets, #[case] args: &[&str], #[case] want: &str) { + let o = run(&bin, args); let err = text(&o.stderr); assert_eq!(o.status.code(), Some(2), "{err}"); assert!(o.stdout.is_empty()); @@ -88,8 +90,8 @@ const NO_SUCH_FILE: &str = if cfg!(windows) { /// `-f FILE` is the positional FILE in the native namespace; under a classic /// name the argv stays Nix's, where `nix-eval-jobs` has no `-f`. -#[test] -fn dash_f_names_the_file_in_the_native_namespace() { +#[rstest] +fn dash_f_names_the_file_in_the_native_namespace(bin: Iets) { for args in [ &["eval", "-f", "/nonexistent/missing.nix"][..], &["eval", "--file", "/nonexistent/missing.nix"], @@ -97,30 +99,30 @@ fn dash_f_names_the_file_in_the_native_namespace() { &["prefetch", "-f", "/nonexistent/missing.nix"], &["eval-jobs", "-f", "/nonexistent/missing.nix"], ] { - let err = text(&iets(args).stderr); + let err = text(&run(&bin, args).stderr); assert!(err.contains(NO_SUCH_FILE), "{args:?}: {err}"); } - let o = iets(&["nix-eval-jobs", "-f", "/nonexistent/missing.nix"]); + let o = run(&bin, &["nix-eval-jobs", "-f", "/nonexistent/missing.nix"]); let err = text(&o.stderr); assert!(err.contains("unknown flag '-f'"), "{err}"); assert_eq!(o.status.code(), Some(1), "{err}"); } -#[test] -fn io_errors_read_like_strerror() { - let o = iets(&["eval", "/nonexistent/missing.nix"]); +#[rstest] +fn io_errors_read_like_strerror(bin: Iets) { + let o = run(&bin, &["eval", "/nonexistent/missing.nix"]); let err = text(&o.stderr); assert!(err.contains(NO_SUCH_FILE), "{err}"); assert!(!err.contains("os error"), "{err}"); } -#[test] -fn completion_asks_the_binary_and_ignores_settings() { +#[rstest] +fn completion_asks_the_binary_and_ignores_settings(bin: Iets) { let conf = tempdir().unwrap(); fs::create_dir_all(conf.path().join("iets")).unwrap(); fs::write(conf.path().join("iets/iets.toml"), "this is = not [valid\n").unwrap(); let complete = |words: &[&str]| { - let mut c = hermetic::iets(); + let mut c = bin.cmd(); c.env("XDG_CONFIG_HOME", conf.path()); run_complete(c, words) }; @@ -132,17 +134,17 @@ fn completion_asks_the_binary_and_ignores_settings() { assert!(complete(&["cache", ""]).starts_with("gc\t")); for shell in ["bash", "zsh", "fish", "nushell", "elvish", "powershell"] { - let o = iets(&["completion", shell]); + let o = run(&bin, &["completion", shell]); assert_eq!(o.status.code(), Some(0), "{shell}"); assert!(text(&o.stdout).contains("__complete"), "{shell}"); } - let o = iets(&["completion", "ksh"]); + let o = run(&bin, &["completion", "ksh"]); assert_eq!(o.status.code(), Some(2)); assert!(text(&o.stderr).contains("unknown subcommand \'ksh\'")); } -#[test] -fn a_flake_fragment_completes_to_the_outputs_attribute_names() { +#[rstest] +fn a_flake_fragment_completes_to_the_outputs_attribute_names(bin: Iets) { let d = tempdir().unwrap(); let sys = [ "x86_64-linux", @@ -162,7 +164,7 @@ fn a_flake_fragment_completes_to_the_outputs_attribute_names() { ) .unwrap(); let complete = |word: &str| { - let mut c = hermetic::iets(); + let mut c = bin.cmd(); c.current_dir(d.path()); run_complete(c, &["eval", word]) }; @@ -183,8 +185,8 @@ fn a_flake_fragment_completes_to_the_outputs_attribute_names() { assert_eq!(complete("/nonexistent#a"), ":6\n"); } -#[test] -fn dash_a_completes_the_attribute_names_under_its_root() { +#[rstest] +fn dash_a_completes_the_attribute_names_under_its_root(bin: Iets) { let d = tempdir().unwrap(); fs::write( d.path().join("x.nix"), @@ -192,7 +194,7 @@ fn dash_a_completes_the_attribute_names_under_its_root() { ) .unwrap(); let complete = |words: &[&str]| { - let mut c = hermetic::iets(); + let mut c = bin.cmd(); c.current_dir(d.path()); run_complete(c, words) }; @@ -237,8 +239,13 @@ in { checks.x86_64-linux = { a = mk "a"; b = mk "b"; }; }; }"#, /// `iets build` with a dead builder. The evaluation comes first, so /// `--timings` still says what it found. -fn build_unreachable(dir: &Path, cache: Option<&Path>, installables: &[&str]) -> String { - let mut c = hermetic::iets(); +fn build_unreachable( + bin: &Iets, + dir: &Path, + cache: Option<&Path>, + installables: &[&str], +) -> String { + let mut c = bin.cmd(); c.current_dir(dir) .args(["build", "--builder", "unix:///nonexistent", "--timings"]); match cache { @@ -252,42 +259,45 @@ const A: &str = ".#checks.x86_64-linux.a"; const B: &str = ".#checks.x86_64-linux.b"; /// The last installable used to replace the earlier ones. -#[test] -fn build_evaluates_every_installable() { +#[rstest] +fn build_evaluates_every_installable(bin: Iets) { let d = tempdir().unwrap(); two_checks(d.path()); - let log = build_unreachable(d.path(), None, &[A, B]); + let log = build_unreachable(&bin, d.path(), None, &[A, B]); assert!(log.contains("eval: 2 derivations"), "{log}"); } /// Installables in another order are one job and one cache entry. -#[test] -fn build_cache_ignores_installable_order() { +#[rstest] +fn build_cache_ignores_installable_order(bin: Iets) { let d = tempdir().unwrap(); two_checks(&d.path().join("flake")); let cache = d.path().join("cache"); - let first = build_unreachable(&d.path().join("flake"), Some(&cache), &[A, B]); - let second = build_unreachable(&d.path().join("flake"), Some(&cache), &[B, A]); + let first = build_unreachable(&bin, &d.path().join("flake"), Some(&cache), &[A, B]); + let second = build_unreachable(&bin, &d.path().join("flake"), Some(&cache), &[B, A]); assert!(first.contains("cache: wrote"), "{first}"); assert!(second.contains("eval: skipped"), "{second}"); } -#[test] -fn eval_json_forces_without_strict() { - let o = iets(&["eval", "--json", "--expr", "{ a = [ (1 + 1) ]; }"]); +#[rstest] +fn eval_json_forces_without_strict(bin: Iets) { + let o = run(&bin, &["eval", "--json", "--expr", "{ a = [ (1 + 1) ]; }"]); assert!(o.status.success(), "{}", text(&o.stderr)); assert_eq!(text(&o.stdout).trim(), r#"{"a":[2]}"#); } /// Nix's currentSystem is the host's unless `system` says otherwise. -#[test] -fn current_system_is_the_hosts() { +#[rstest] +fn current_system_is_the_hosts(bin: Iets) { let os = match env::consts::OS { "macos" => "darwin", other => other, }; let want = format!("\"{}-{os}\"\n", env::consts::ARCH); - let o = iets(&["eval", "--impure", "--expr", "builtins.currentSystem"]); + let o = run( + &bin, + &["eval", "--impure", "--expr", "builtins.currentSystem"], + ); assert_eq!(text(&o.stdout), want, "{}", text(&o.stderr)); } @@ -297,14 +307,15 @@ fn current_system_is_the_hosts() { #[case::parse(&["parse", "multi.nix"])] #[case::help(&["--help"])] #[case::lint_list(&["lint", "list"])] -fn a_full_stdout_is_an_error(#[case] args: &[&str]) { +fn a_full_stdout_is_an_error(bin: Iets, #[case] args: &[&str]) { let d = tempdir().unwrap(); fs::write(d.path().join("multi.nix"), "{ }").unwrap(); let full = fs::OpenOptions::new() .write(true) .open("/dev/full") .unwrap(); - let o = hermetic::iets() + let o = bin + .cmd() .current_dir(d.path()) .args(args) .stdout(full) @@ -315,13 +326,13 @@ fn a_full_stdout_is_an_error(#[case] args: &[&str]) { assert!(err.contains("No space left on device"), "{err}"); } -#[test] -fn a_bad_config_stops_only_what_reads_a_setting() { +#[rstest] +fn a_bad_config_stops_only_what_reads_a_setting(bin: Iets) { let conf = tempdir().unwrap(); fs::create_dir_all(conf.path().join("iets")).unwrap(); fs::write(conf.path().join("iets/iets.toml"), "bogus = 1\n").unwrap(); let run = |a: &[&str]| { - hermetic::iets() + bin.cmd() .env("XDG_CONFIG_HOME", conf.path()) .args(a) .output() @@ -340,9 +351,9 @@ fn a_bad_config_stops_only_what_reads_a_setting() { assert!(text(&o.stderr).starts_with("@nix "), "{}", text(&o.stderr)); } -#[test] -fn eval_says_what_it_does_not_support() { - let o = iets(&["eval", "--expr", "builtins.fetchMercurial ./."]); +#[rstest] +fn eval_says_what_it_does_not_support(bin: Iets) { + let o = run(&bin, &["eval", "--expr", "builtins.fetchMercurial ./."]); assert_eq!(o.status.code(), Some(1)); assert!( text(&o.stderr).contains(r#"builtins.fetchTree { type = "hg";"#), @@ -357,23 +368,23 @@ fn eval_says_what_it_does_not_support() { "flake registry references (nixpkgs#…) are not supported" )] #[case::github("github:NixOS/nixpkgs#lib.version", "only local paths are supported")] -fn eval_says_which_flake_refs_it_does_not_support(#[case] r: &str, #[case] want: &str) { - let o = iets(&["eval", r]); +fn eval_says_which_flake_refs_it_does_not_support(bin: Iets, #[case] r: &str, #[case] want: &str) { + let o = run(&bin, &["eval", r]); assert_eq!(o.status.code(), Some(1)); assert!(text(&o.stderr).contains(want), "{}", text(&o.stderr)); } /// Every `-A` prints, in order, from one evaluation of the input /// (nix-instantiate --eval). -#[test] -fn eval_prints_every_attr() { +#[rstest] +fn eval_prints_every_attr(bin: Iets) { let e = r#"builtins.trace "root" { a = 1; b = 2; c = throw "boom"; }"#; - let o = iets(&["eval", "-E", e, "-A", "b", "-A", "a"]); + let o = run(&bin, &["eval", "-E", e, "-A", "b", "-A", "a"]); assert!(o.status.success(), "{}", text(&o.stderr)); assert_eq!(text(&o.stdout), "2\n1\n"); assert_eq!(text(&o.stderr).matches("trace: root").count(), 1); // what came before the failing one is out already - let o = iets(&["eval", "-E", e, "-A", "a", "-A", "c", "-A", "b"]); + let o = run(&bin, &["eval", "-E", e, "-A", "a", "-A", "c", "-A", "b"]); assert!(!o.status.success()); assert_eq!(text(&o.stdout), "1\n"); } diff --git a/tests/diff.rs b/tests/diff.rs index e1b49efb..b1ee6e0a 100644 --- a/tests/diff.rs +++ b/tests/diff.rs @@ -4,6 +4,8 @@ mod hermetic; +use hermetic::{Iets, bin}; + use rstest::rstest; use serde_json::Value; use std::fs; @@ -48,8 +50,9 @@ in { work } -fn diff(work: &Path, args: &[&str]) -> Run { - let o = hermetic::iets() +fn diff(bin: &Iets, work: &Path, args: &[&str]) -> Run { + let o = bin + .cmd() .current_dir(work) .arg("diff") .args(args) @@ -62,10 +65,10 @@ fn diff(work: &Path, args: &[&str]) -> Run { } } -#[test] -fn the_text_names_the_env_the_source_and_the_input_drv() { +#[rstest] +fn the_text_names_the_env_the_source_and_the_input_drv(bin: Iets) { let w = demo(); - let r = diff(w.path(), &["-A", "a", "-A", "b", "demo.nix"]); + let r = diff(&bin, w.path(), &["-A", "a", "-A", "b", "demo.nix"]); assert_eq!(r.rc, 1, "{}", r.out); for want in [ "env GREETING:", @@ -80,10 +83,14 @@ fn the_text_names_the_env_the_source_and_the_input_drv() { } } -#[test] -fn the_json_object_names_the_same_differences() { +#[rstest] +fn the_json_object_names_the_same_differences(bin: Iets) { let w = demo(); - let r = diff(w.path(), &["-A", "a", "-A", "b", "--json", "demo.nix"]); + let r = diff( + &bin, + w.path(), + &["-A", "a", "-A", "b", "--json", "demo.nix"], + ); assert_eq!(r.rc, 1, "{}", r.out); let j: Value = serde_json::from_str(&r.out).expect("JSON on stdout"); assert_eq!(j["equal"], false); @@ -128,26 +135,26 @@ fn the_json_object_names_the_same_differences() { #[case::an_unknown_flag_is_a_usage_error(&["-A", "a", "-A", "b", "--bogus"], 2, "unknown flag")] #[case::equal_derivations_exit_0(&["-A", "a", "-A", "c"], 0, "are the same derivation")] #[case::depth_zero_stops_at_the_input_derivation(&["-A", "a", "-A", "b", "--depth", "0"], 1, "stopped: depth")] -fn exit_code(#[case] args: &[&str], #[case] rc: i32, #[case] want: &str) { +fn exit_code(bin: Iets, #[case] args: &[&str], #[case] rc: i32, #[case] want: &str) { let w = demo(); - let r = diff(w.path(), &[args, &["demo.nix"]].concat()); + let r = diff(&bin, w.path(), &[args, &["demo.nix"]].concat()); let text = if rc == 2 { &r.err } else { &r.out }; assert_eq!(r.rc, rc, "{}{}", r.out, r.err); assert!(text.contains(want), "{text}"); } -#[test] -fn two_installables_are_one_evaluation_each() { +#[rstest] +fn two_installables_are_one_evaluation_each(bin: Iets) { let w = demo(); fs::write(w.path().join("a.nix"), "(import ./demo.nix).a\n").expect("write a.nix"); fs::write(w.path().join("b.nix"), "(import ./demo.nix).b\n").expect("write b.nix"); - let r = diff(w.path(), &["a.nix", "b.nix"]); + let r = diff(&bin, w.path(), &["a.nix", "b.nix"]); assert_eq!(r.rc, 1, "{}", r.out); assert!(r.out.contains("input drv changed: diffdep"), "{}", r.out); } -#[test] -fn an_env_name_that_repeats_the_derivation_name_is_left_out() { +#[rstest] +fn an_env_name_that_repeats_the_derivation_name_is_left_out(bin: Iets) { let w = demo(); fs::write( w.path().join("named.nix"), @@ -161,18 +168,18 @@ in { one = mk "ux-one"; two = mk "ux-two"; three = mk "ux-one" // { }; } "#, ) .expect("write named.nix"); - let r = diff(w.path(), &["-A", "one", "-A", "two", "named.nix"]); + let r = diff(&bin, w.path(), &["-A", "one", "-A", "two", "named.nix"]); assert_eq!(r.rc, 1, "{}", r.out); assert!(r.out.contains("name: ux-one -> ux-two"), "{}", r.out); assert!(!r.out.contains("env name"), "{}", r.out); } -#[test] -fn dash_f_is_the_positional_file() { +#[rstest] +fn dash_f_is_the_positional_file(bin: Iets) { let w = demo(); - let want = diff(w.path(), &["-A", "a", "-A", "b", "demo.nix"]); + let want = diff(&bin, w.path(), &["-A", "a", "-A", "b", "demo.nix"]); for flag in ["-f", "--file"] { - let r = diff(w.path(), &["-A", "a", "-A", "b", flag, "demo.nix"]); + let r = diff(&bin, w.path(), &["-A", "a", "-A", "b", flag, "demo.nix"]); assert_eq!((r.rc, &r.out), (want.rc, &want.out), "{flag}"); } } diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index 67ec64ca..882dd05f 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -15,7 +15,7 @@ use std::{env, fs, thread}; use tempfile::TempDir; -use crate::hermetic; +use crate::hermetic::{self, Iets}; pub mod classic; @@ -32,6 +32,7 @@ pub struct Store { daemon: Child, shell: PathBuf, nonce: String, + bin: Iets, } pub fn which(prog: &str) -> Option { @@ -99,6 +100,7 @@ impl Store { daemon, shell: resolved("bash"), nonce, + bin: hermetic::bin(), }; store.write_pkgs(); let deadline = Instant::now() + Duration::from_secs(30); @@ -202,7 +204,7 @@ impl Store { /// iets started as `prog`, a link to it under another name. pub fn iets_as(&self, prog: impl AsRef) -> Command { - let mut c = hermetic::iets_as(prog); + let mut c = self.bin.cmd_as(prog); Store::env(self.dir(), &mut c); c.current_dir(self.dir()); c diff --git a/tests/e2e_evaljobs.rs b/tests/e2e_evaljobs.rs index 818db138..480fe284 100644 --- a/tests/e2e_evaljobs.rs +++ b/tests/e2e_evaljobs.rs @@ -9,6 +9,9 @@ mod e2e; mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::env; use std::fs::{self, File}; use std::path::{Path, PathBuf}; @@ -438,11 +441,11 @@ fn flags_match_nix_eval_jobs() { /// Every internal-json record of one run carries one `sid`, and /// IETS_SESSION_ID sets it (docs/reference/errors.md). -#[test] -fn one_session_id_per_run() { +#[rstest] +fn one_session_id_per_run(bin: Iets) { let e = r#"{ a = builtins.trace "a" (derivation { name = "a"; system = "x"; builder = ":"; }); b = builtins.trace "b" (derivation { name = "b"; system = "x"; builder = ":"; }); }"#; let sids = |id: Option<&str>| -> Vec { - let mut c = hermetic::iets(); + let mut c = bin.cmd(); if let Some(id) = id { c.env("IETS_SESSION_ID", id); } @@ -465,17 +468,17 @@ fn one_session_id_per_run() { assert!(sids(Some("run-42")).iter().all(|s| s == "run-42")); } -fn ci() -> Command { - let mut c = hermetic::iets(); +fn ci(bin: &Iets) -> Command { + let mut c = bin.cmd(); c.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/evaljobs")) .args(["eval-jobs", "--workers", "2", "--no-instantiate"]); c } /// Every line from a worker carries the `stats` nixbot reads. -#[test] -fn every_line_has_stats() { - let o = ok(ci().arg("ci.nix")); +#[rstest] +fn every_line_has_stats(bin: Iets) { + let o = ok(ci(&bin).arg("ci.nix")); let lines: Vec = String::from_utf8_lossy(&o.stdout) .lines() .map(|l| serde_json::from_str(l).unwrap()) @@ -496,9 +499,9 @@ fn every_line_has_stats() { /// `--verbose` and `--debug` are one warning each, from the main process; /// `--max-jobs` and `--quiet` hold (docs/reference/divergences.md). -#[test] -fn ignored_flags_warn_once() { - let o = ci() +#[rstest] +fn ignored_flags_warn_once(bin: Iets) { + let o = ci(&bin) .args([ "--verbose", "--debug", @@ -528,11 +531,11 @@ fn ignored_flags_warn_once() { /// `--iets-option` leaves the argv before any command sees it, so the /// collector puts it back for every worker. Only a worker that read /// `cache-dir` makes the directory. -#[test] -fn iets_options_reach_the_workers() { +#[rstest] +fn iets_options_reach_the_workers(bin: Iets) { let dir = tempfile::tempdir().unwrap(); let cache = dir.path().join("opt-cache"); - let mut c = hermetic::iets(); + let mut c = bin.cmd(); c.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/evaljobs")) .args(["eval-jobs", "--iets-option", "cache-dir"]) .arg(&cache) diff --git a/tests/events.rs b/tests/events.rs index f1076df6..928fb1ba 100644 --- a/tests/events.rs +++ b/tests/events.rs @@ -3,14 +3,16 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; use std::path::Path; use std::process::Output; use std::{env, fs}; use tempfile::tempdir; /// `groups` is `IETS_EVENTS`; the sink stays stderr, which is its default. -fn eval(expr: &str, groups: Option<&str>) -> Output { - let mut c = hermetic::iets(); +fn eval(bin: &Iets, expr: &str, groups: Option<&str>) -> Output { + let mut c = bin.cmd(); c.args(["eval", "--no-cache", "--raw", "-E", expr]) .env_remove("IETS_EVENTS_TO"); match groups { @@ -20,8 +22,8 @@ fn eval(expr: &str, groups: Option<&str>) -> Output { c.output().expect("run iets") } -fn stderr(expr: &str, groups: Option<&str>) -> String { - let o = eval(expr, groups); +fn stderr(bin: &Iets, expr: &str, groups: Option<&str>) -> String { + let o = eval(bin, expr, groups); assert_eq!( o.status.code(), Some(0), @@ -33,9 +35,9 @@ fn stderr(expr: &str, groups: Option<&str>) -> String { const TO_FILE: &str = r#""${builtins.toFile "t" "hi"}""#; -#[test] -fn a_text_add_names_its_size_and_its_position() { - let err = stderr(TO_FILE, Some("store")); +#[rstest] +fn a_text_add_names_its_size_and_its_position(bin: Iets) { + let err = stderr(&bin, TO_FILE, Some("store")); let line = err.lines().next().unwrap_or_default(); assert!(line.contains(r#""event":"store-add""#), "{err}"); assert!(line.contains(r#""kind":"text""#), "{err}"); @@ -45,8 +47,8 @@ fn a_text_add_names_its_size_and_its_position() { assert!(line.contains(r#""at":"«string»:1:4""#), "{err}"); } -#[test] -fn a_tree_add_names_the_local_path_it_copied() { +#[rstest] +fn a_tree_add_names_the_local_path_it_copied(bin: Iets) { // A Nix path has forward slashes, and a drive is its first component let native = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml").replace('\\', "/"); let src = if native.starts_with('/') { @@ -55,7 +57,7 @@ fn a_tree_add_names_the_local_path_it_copied() { format!("/{native}") }; // A path literal cannot hold the colon of a drive - let err = stderr(&format!(r#""${{/. + "{src}"}}""#), Some("store")); + let err = stderr(&bin, &format!(r#""${{/. + "{src}"}}""#), Some("store")); assert!(err.contains(r#""kind":"tree""#), "{err}"); // `from` is where the file is on disk, written as a JSON string let on_disk = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); @@ -63,22 +65,22 @@ fn a_tree_add_names_the_local_path_it_copied() { assert!(err.contains(&format!(r#""from":"{on_disk}""#)), "{err}"); } -#[test] -fn all_takes_the_cold_groups() { - assert!(stderr(TO_FILE, Some("all")).contains("store-add")); - assert!(stderr(TO_FILE, Some("store,fetch")).contains("store-add")); +#[rstest] +fn all_takes_the_cold_groups(bin: Iets) { + assert!(stderr(&bin, TO_FILE, Some("all")).contains("store-add")); + assert!(stderr(&bin, TO_FILE, Some("store,fetch")).contains("store-add")); } -#[test] -fn a_group_nobody_named_says_nothing() { - assert_eq!(stderr(TO_FILE, None), ""); - assert_eq!(stderr(TO_FILE, Some("fetch")), ""); - assert_eq!(stderr(TO_FILE, Some("")), ""); +#[rstest] +fn a_group_nobody_named_says_nothing(bin: Iets) { + assert_eq!(stderr(&bin, TO_FILE, None), ""); + assert_eq!(stderr(&bin, TO_FILE, Some("fetch")), ""); + assert_eq!(stderr(&bin, TO_FILE, Some("")), ""); } -#[test] -fn a_group_that_does_not_exist_is_a_settings_error() { - let o = eval(TO_FILE, Some("store,stor")); +#[rstest] +fn a_group_that_does_not_exist_is_a_settings_error(bin: Iets) { + let o = eval(&bin, TO_FILE, Some("store,stor")); let err = String::from_utf8_lossy(&o.stderr); assert!( err.contains("IETS_EVENTS: 'stor' is not an event group"), @@ -88,11 +90,12 @@ fn a_group_that_does_not_exist_is_a_settings_error() { assert_ne!(o.status.code(), Some(0), "{err}"); } -#[test] -fn the_sink_is_its_own_knob() { +#[rstest] +fn the_sink_is_its_own_knob(bin: Iets) { let dir = tempdir().unwrap(); let file = dir.path().join("e.jsonl"); - let o = hermetic::iets() + let o = bin + .cmd() .args(["eval", "--no-cache", "--raw", "-E", TO_FILE]) .env("IETS_EVENTS", "store") .env("IETS_EVENTS_TO", &file) diff --git a/tests/fetchurl.rs b/tests/fetchurl.rs index a860035c..57963bfa 100644 --- a/tests/fetchurl.rs +++ b/tests/fetchurl.rs @@ -3,6 +3,9 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::fs; use std::path::{Path, PathBuf}; use std::process::Output; @@ -11,8 +14,8 @@ use tempfile::{TempDir, tempdir}; const BYTES: &[u8] = b"hello fetchurl\n"; const SHA256: &str = "113a52l57f2jv9fh9igj2xh2mi0is40flxbkqi69wbh7nlybkcbm"; -fn iets(cache: &Path, offline: bool, args: &[&str]) -> Output { - let mut c = hermetic::iets(); +fn run(bin: &Iets, cache: &Path, offline: bool, args: &[&str]) -> Output { + let mut c = bin.cmd(); c.args(args) .env("IETS_CACHE_DIR", cache) .env("IETS_CACHE", "0"); @@ -45,20 +48,20 @@ fn scratch() -> (TempDir, PathBuf) { (d, cache) } -fn eval(cache: &Path, offline: bool, flags: &[&str], expr: &str) -> Output { +fn eval(bin: &Iets, cache: &Path, offline: bool, flags: &[&str], expr: &str) -> Output { let mut args = vec!["eval", "--raw"]; args.extend_from_slice(flags); args.extend_from_slice(&["--expr", expr]); - iets(cache, offline, &args) + run(bin, cache, offline, &args) } -#[test] -fn a_hashed_fetch_has_the_store_path_nix_gives() { +#[rstest] +fn a_hashed_fetch_has_the_store_path_nix_gives(bin: Iets) { let (d, cache) = scratch(); let url = format!("{}/hello.txt", file_url(d.path())); let call = format!(r#"builtins.fetchurl {{ url = "{url}"; sha256 = "{SHA256}"; }}"#); - let o = eval(&cache, false, &[], &call); + let o = eval(&bin, &cache, false, &[], &call); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!( text(&o.stdout), @@ -67,14 +70,20 @@ fn a_hashed_fetch_has_the_store_path_nix_gives() { // the input store holds it now: no download, and the bytes read back fs::remove_file(d.path().join("hello.txt")).unwrap(); - let o = eval(&cache, true, &[], &format!("builtins.readFile ({call})")); + let o = eval( + &bin, + &cache, + true, + &[], + &format!("builtins.readFile ({call})"), + ); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!(o.stdout, BYTES); let named = format!(r#"builtins.fetchurl {{ url = "{url}"; sha256 = "{SHA256}"; name = "x"; }}"#); fs::write(d.path().join("hello.txt"), BYTES).unwrap(); - let o = eval(&cache, false, &[], &named); + let o = eval(&bin, &cache, false, &[], &named); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!( text(&o.stdout), @@ -82,12 +91,13 @@ fn a_hashed_fetch_has_the_store_path_nix_gives() { ); } -#[test] -fn a_wrong_hash_is_refused_with_the_hash_the_file_has() { +#[rstest] +fn a_wrong_hash_is_refused_with_the_hash_the_file_has(bin: Iets) { let (d, cache) = scratch(); let url = format!("{}/hello.txt", file_url(d.path())); let zero = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; let o = eval( + &bin, &cache, false, &[], @@ -102,8 +112,8 @@ fn a_wrong_hash_is_refused_with_the_hash_the_file_has() { assert!(text(&o.stderr).starts_with(&want), "{}", text(&o.stderr)); } -#[test] -fn without_a_hash_no_evaluation_fetches_and_prefetch_gives_the_hash() { +#[rstest] +fn without_a_hash_no_evaluation_fetches_and_prefetch_gives_the_hash(bin: Iets) { let (d, cache) = scratch(); let url = format!("{}/hello.txt", file_url(d.path())); let call = format!(r#"builtins.fetchurl "{url}""#); @@ -111,15 +121,16 @@ fn without_a_hash_no_evaluation_fetches_and_prefetch_gives_the_hash() { "error: 'fetchurl' requires a 'sha256' argument: run 'iets prefetch {url}' and pass the hash as 'sha256'" ); for flags in [&[][..], &["--impure"][..]] { - let o = eval(&cache, false, flags, &call); + let o = eval(&bin, &cache, false, flags, &call); assert_eq!(o.status.code(), Some(1), "{flags:?}: {}", text(&o.stdout)); assert!(text(&o.stderr).starts_with(&want), "{}", text(&o.stderr)); } - let o = iets(&cache, false, &["prefetch", &url]); + let o = run(&bin, &cache, false, &["prefetch", &url]); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); let sri = text(&o.stdout).trim().to_string(); let o = eval( + &bin, &cache, false, &[], diff --git a/tests/help_topics.rs b/tests/help_topics.rs index 8d024fe1..ef22b938 100644 --- a/tests/help_topics.rs +++ b/tests/help_topics.rs @@ -3,26 +3,29 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::process::Output; /// The binary, run from `/`, where there is no `docs/`. -fn run(args: &[&str]) -> Output { - hermetic::iets() +fn run(bin: &Iets, args: &[&str]) -> Output { + bin.cmd() .args(args) .current_dir("/") .output() .expect("iets runs") } -fn stdout(args: &[&str]) -> String { - let o = run(args); +fn stdout(bin: &Iets, args: &[&str]) -> String { + let o = run(bin, args); assert!(o.status.success(), "{args:?}: {:?}", o.status); String::from_utf8(o.stdout).expect("utf-8") } -#[test] -fn help_lists_the_topics() { - let s = stdout(&["help"]); +#[rstest] +fn help_lists_the_topics(bin: Iets) { + let s = stdout(&bin, &["help"]); assert!(s.contains("usage: iets COMMAND")); assert!(s.contains("topics:")); for t in ["settings", "errors", "lints", "divergences", "caching"] { @@ -30,38 +33,38 @@ fn help_lists_the_topics() { } } -#[test] -fn a_topic_prints_its_page() { - let s = stdout(&["help", "settings"]); +#[rstest] +fn a_topic_prints_its_page(bin: Iets) { + let s = stdout(&bin, &["help", "settings"]); assert!(s.starts_with("# Settings"), "{}", &s[..40.min(s.len())]); assert!(s.contains("IETS_WORKERS")); - let s = stdout(&["help", "errors"]); + let s = stdout(&bin, &["help", "errors"]); assert!(s.contains("| `build-failed` |")); - let s = stdout(&["help", "divergences"]); + let s = stdout(&bin, &["help", "divergences"]); assert!(s.contains("builtins.fromTOML")); } -#[test] -fn a_command_wins_over_a_topic_of_the_same_name() { - let s = stdout(&["help", "plumbing"]); +#[rstest] +fn a_command_wins_over_a_topic_of_the_same_name(bin: Iets) { + let s = stdout(&bin, &["help", "plumbing"]); assert!(s.contains("usage: iets plumbing OP")); assert!(s.contains("'iets help reference/plumbing'")); - let s = stdout(&["help", "reference/plumbing"]); + let s = stdout(&bin, &["help", "reference/plumbing"]); assert!(s.starts_with("# Plumbing operations")); } -#[test] -fn a_page_the_binary_does_not_carry_names_its_path() { - let s = stdout(&["help", "caching"]); +#[rstest] +fn a_page_the_binary_does_not_carry_names_its_path(bin: Iets) { + let s = stdout(&bin, &["help", "caching"]); assert!(s.contains("docs/explanation/caching.md")); } -#[test] -fn an_unknown_topic_is_a_usage_error_with_a_suggestion() { - let o = run(&["help", "settigns"]); +#[rstest] +fn an_unknown_topic_is_a_usage_error_with_a_suggestion(bin: Iets) { + let o = run(&bin, &["help", "settigns"]); assert_eq!(o.status.code(), Some(2)); let e = String::from_utf8(o.stderr).expect("utf-8"); assert!(e.contains("unknown command or topic 'settigns'"), "{e}"); diff --git a/tests/hermetic/mod.rs b/tests/hermetic/mod.rs index 6240b8c6..6b6db3b8 100644 --- a/tests/hermetic/mod.rs +++ b/tests/hermetic/mod.rs @@ -1,18 +1,16 @@ //! The binary under test, cut off from the machine: it gets only the -//! environment below and a home of its own per test, so a `nix.conf` or a -//! cache from an earlier run cannot change what it prints. +//! environment below and, from the [`bin`] fixture, a home of its own +//! per test, so a `nix.conf` or a cache from an earlier run cannot change +//! what it prints. // each test crate uses what it needs #![allow(dead_code)] -use std::collections::BTreeSet; +use rstest::fixture; use std::env; use std::ffi::OsStr; -use std::fs; -use std::path::PathBuf; use std::process::Command; -use std::sync::Mutex; -use std::thread; +use tempfile::TempDir; /// The IETS_ settings every run gets. tests/e2e gives them to Nix too, so a /// shell either one starts has the same environment. @@ -30,23 +28,38 @@ const INHERIT: &[&str] = &[ "ComSpec", ]; -pub fn iets() -> Command { - iets_as(env!("CARGO_BIN_EXE_iets")) +/// iets with a home of its own, kept for the whole test so runs within it +/// share a cache. +pub struct Iets { + home: TempDir, } -/// `iets` started as `prog`, a link to it under another name. -pub fn iets_as(prog: impl AsRef) -> Command { - let mut c = Command::new(prog); - isolate(&mut c); - let home = home(); - // its own so a test can fake HOME - c.env("XDG_CACHE_HOME", home.join(".cache")) - .env("HOME", home) - .env("XDG_CONFIG_DIRS", "/nonexistent") - .env("NIX_CONF_DIR", "/nonexistent") - // the expected paths are Nix's under /nix/store, on every platform - .env("NIX_STORE_DIR", "/nix/store"); - c +#[fixture] +pub fn bin() -> Iets { + let tmp = env!("CARGO_TARGET_TMPDIR"); + let home = TempDir::with_prefix_in("home-", tmp).expect("create the test home"); + Iets { home } +} + +impl Iets { + pub fn cmd(&self) -> Command { + self.cmd_as(env!("CARGO_BIN_EXE_iets")) + } + + /// `iets` started as `prog`, a link to it under another name. + pub fn cmd_as(&self, prog: impl AsRef) -> Command { + let mut c = Command::new(prog); + isolate(&mut c); + let home = self.home.path(); + // its own so a test can fake HOME + c.env("XDG_CACHE_HOME", home.join(".cache")) + .env("HOME", home) + .env("XDG_CONFIG_DIRS", "/nonexistent") + .env("NIX_CONF_DIR", "/nonexistent") + // the expected paths are Nix's under /nix/store, on every platform + .env("NIX_STORE_DIR", "/nix/store"); + c + } } /// Only [`INHERIT`] and [`ENV`]. tests/e2e starts Nix with this too. @@ -59,22 +72,3 @@ pub fn isolate(c: &mut Command) { } c.envs(ENV.iter().copied()); } - -/// A directory per test, emptied on its first use in this process. The -/// test thread is named after the test, under cargo test and nextest. -fn home() -> PathBuf { - static FRESH: Mutex> = Mutex::new(BTreeSet::new()); - let name = match thread::current().name() { - Some(n) if n != "main" => n.replace("::", "-"), - _ => format!("pid-{}", std::process::id()), - }; - let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) - .join("hermetic") - .join(env!("CARGO_CRATE_NAME")) - .join(&name); - if FRESH.lock().unwrap().insert(name) { - let _ = fs::remove_dir_all(&dir); - fs::create_dir_all(&dir).expect("create the test home"); - } - dir -} diff --git a/tests/langtests.rs b/tests/langtests.rs index 562b3e6a..5a596c9c 100644 --- a/tests/langtests.rs +++ b/tests/langtests.rs @@ -4,6 +4,7 @@ mod hermetic; +use hermetic::{Iets, bin}; use iets::sys::path_to_internal; use iets::sys::process::signal; use rstest::rstest; @@ -24,8 +25,8 @@ fn flags_of(stem: &str) -> Vec { .unwrap_or_default() } -fn eval(stem: &str) -> Output { - hermetic::iets() +fn eval(bin: &Iets, stem: &str) -> Output { + bin.cmd() .current_dir(tests_dir()) .env("HOME", "/fake-home") .env("TEST_VAR", "foo") @@ -41,8 +42,8 @@ fn chomp(s: &str) -> &str { s.trim_end_matches('\n') } -fn check(stem: &str) -> Option { - let out = eval(stem); +fn check(bin: &Iets, stem: &str) -> Option { + let out = eval(bin, stem); if stem.starts_with("eval-fail-") { return match (out.status.code(), signal(&out.status)) { (Some(0), _) => Some("should fail".into()), @@ -71,12 +72,13 @@ fn check(stem: &str) -> Option { // a new case shows up after `touch tests/langtests.rs` #[rstest] fn lang( + bin: Iets, #[files("tests/lang/eval-okay-*.nix")] #[files("tests/lang/eval-fail-*.nix")] path: PathBuf, ) { let stem = path.file_stem().unwrap().to_str().unwrap(); - if let Some(why) = check(stem) { + if let Some(why) = check(&bin, stem) { panic!("{stem}: {why}"); } } diff --git a/tests/nixpkgs_eval.rs b/tests/nixpkgs_eval.rs index dbc5848f..7de43817 100644 --- a/tests/nixpkgs_eval.rs +++ b/tests/nixpkgs_eval.rs @@ -6,6 +6,9 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::collections::BTreeMap; use std::env; use std::path::Path; @@ -43,9 +46,9 @@ fn jobs(mut c: Command, nixpkgs: &str, limit: &str) -> Jobs { jobs } -#[test] +#[rstest] #[ignore = "evaluates nixpkgs twice; set IETS_TEST_NIXPKGS"] -fn nixpkgs_drvs_match_nix() { +fn nixpkgs_drvs_match_nix(bin: Iets) { let (Ok(nixpkgs), Ok(nej)) = ( env::var("IETS_TEST_NIXPKGS"), env::var("IETS_TEST_NIX_EVAL_JOBS"), @@ -67,7 +70,7 @@ fn nixpkgs_drvs_match_nix() { .env_remove("NIX_CONFIG"); let theirs = jobs(theirs, &nixpkgs, &limit); - let mut ours = hermetic::iets(); + let mut ours = bin.cmd(); // no daemon, as in the check's sandbox ours.arg("nix-eval-jobs") .env("NIX_DAEMON_SOCKET_PATH", "/nonexistent/socket") diff --git a/tests/profile.rs b/tests/profile.rs index 052a2d69..4cdcd130 100644 --- a/tests/profile.rs +++ b/tests/profile.rs @@ -3,12 +3,15 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::fs; use std::process::Output; use tempfile::tempdir; -fn eval(expr: &str, env: &[(&str, &str)]) -> Output { - let mut c = hermetic::iets(); +fn eval(bin: &Iets, expr: &str, env: &[(&str, &str)]) -> Output { + let mut c = bin.cmd(); c.args(["eval", "--no-cache", "--workers", "1", "-E", expr]) .env_remove("IETS_PROFILE") .env_remove("IETS_PROFILE_TO") @@ -20,10 +23,11 @@ fn eval(expr: &str, env: &[(&str, &str)]) -> Output { } /// The folded lines of a `kind` profile of `expr`, and the run. -fn profile(kind: &str, expr: &str) -> (String, Output) { +fn profile(bin: &Iets, kind: &str, expr: &str) -> (String, Output) { let dir = tempdir().unwrap(); let out = dir.path().join("p.folded"); let o = eval( + bin, expr, &[ ("IETS_PROFILE", kind), @@ -42,37 +46,37 @@ fn profile(kind: &str, expr: &str) -> (String, Output) { (folded, o) } -#[test] -fn a_profile_that_is_not_eval_is_a_settings_error() { - let o = eval("1", &[("IETS_PROFILE", "cpu")]); +#[rstest] +fn a_profile_that_is_not_eval_is_a_settings_error(bin: Iets) { + let o = eval(&bin, "1", &[("IETS_PROFILE", "cpu")]); assert_ne!(o.status.code(), Some(0)); let err = String::from_utf8_lossy(&o.stderr); assert!(err.contains("IETS_PROFILE: 'cpu' is not 'eval'"), "{err}"); } -#[test] -fn an_unprofiled_run_says_nothing_about_a_profile() { - let o = eval("1", &[]); +#[rstest] +fn an_unprofiled_run_says_nothing_about_a_profile(bin: Iets) { + let o = eval(&bin, "1", &[]); let err = String::from_utf8_lossy(&o.stderr); assert!(!err.contains("profile"), "{err}"); } /// A run with no call in it takes no sample, and an empty profile is said /// out loud rather than written as an empty file. -#[test] -fn a_run_shorter_than_a_tick_says_it_sampled_nothing() { - let o = eval("1 + 1", &[("IETS_PROFILE", "eval")]); +#[rstest] +fn a_run_shorter_than_a_tick_says_it_sampled_nothing(bin: Iets) { + let o = eval(&bin, "1 + 1", &[("IETS_PROFILE", "eval")]); assert_eq!(o.status.code(), Some(0)); let err = String::from_utf8_lossy(&o.stderr); assert!(err.contains("profile: no samples"), "{err}"); } -#[test] -fn a_folded_line_is_a_stack_and_a_count() { +#[rstest] +fn a_folded_line_is_a_stack_and_a_count(bin: Iets) { // mutual recursion, so a sample sees a stack and not one frame let expr = "let f = n: if n == 0 then 0 else g (n - 1); g = n: f n; \ in builtins.foldl' (a: _: a + f 400) 0 (builtins.genList (x: x) 4000)"; - let (folded, _) = profile("eval", expr); + let (folded, _) = profile(&bin, "eval", expr); let mut deepest = 0; for line in folded.lines() { let (stack, count) = line.rsplit_once(' ').expect("a stack and a count"); @@ -87,11 +91,11 @@ fn a_folded_line_is_a_stack_and_a_count() { /// A builtin is a frame too, so time inside one is not blamed on the lambda /// that called it. -#[test] -fn a_builtin_names_itself_in_a_frame() { +#[rstest] +fn a_builtin_names_itself_in_a_frame(bin: Iets) { let expr = "builtins.length (builtins.foldl' (a: _: a ++ builtins.genList (x: x) 50) [] \ (builtins.genList (x: x) 2000))"; - let (folded, _) = profile("eval", expr); + let (folded, _) = profile(&bin, "eval", expr); assert!(folded.contains(":«"), "no builtin frame:\n{folded}"); } @@ -99,11 +103,11 @@ fn a_builtin_names_itself_in_a_frame() { /// the folded counts add up to what the arena allocated. Every byte is /// taken by the next sample, so only what came after the last one is /// missing. -#[test] -fn an_allocation_profile_accounts_for_the_arena() { +#[rstest] +fn an_allocation_profile_accounts_for_the_arena(bin: Iets) { let expr = "builtins.length (builtins.foldl' (a: _: a ++ builtins.genList toString 200) [] \ (builtins.genList (x: x) 2000))"; - let (folded, o) = profile("alloc", expr); + let (folded, o) = profile(&bin, "alloc", expr); let weighed: u64 = folded .lines() .map(|l| { @@ -127,9 +131,9 @@ fn an_allocation_profile_accounts_for_the_arena() { /// A profile is read per fiber, so more than one worker gives stacks with no /// shape. Asking for both is refused rather than answered misleadingly. -#[test] -fn a_profile_refuses_more_than_one_worker() { - let mut c = hermetic::iets(); +#[rstest] +fn a_profile_refuses_more_than_one_worker(bin: Iets) { + let mut c = bin.cmd(); let o = c .args(["eval", "--no-cache", "--workers", "4", "-E", "1"]) .env("IETS_PROFILE", "eval") @@ -140,7 +144,7 @@ fn a_profile_refuses_more_than_one_worker() { assert!(err.contains("--workers must be 1, not 4"), "{err}"); // the same through the setting rather than the flag - let mut c = hermetic::iets(); + let mut c = bin.cmd(); let o = c .args(["eval", "--no-cache", "-E", "1"]) .env("IETS_PROFILE", "eval") diff --git a/tests/progress.rs b/tests/progress.rs index a45b834b..f836bf6e 100644 --- a/tests/progress.rs +++ b/tests/progress.rs @@ -3,6 +3,9 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpListener; use std::process::Stdio; @@ -10,8 +13,8 @@ use std::sync::mpsc; use std::thread; use std::time::Duration; -#[test] -fn a_pipe_hears_of_a_run_that_takes_five_seconds() { +#[rstest] +fn a_pipe_hears_of_a_run_that_takes_five_seconds(bin: Iets) { let server = TcpListener::bind("127.0.0.1:0").unwrap(); let url = format!("http://{}/x", server.local_addr().unwrap()); let (heard, wait) = mpsc::channel::<()>(); @@ -27,7 +30,8 @@ fn a_pipe_hears_of_a_run_that_takes_five_seconds() { }); let cache = tempfile::tempdir().unwrap(); let zero = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - let mut child = hermetic::iets() + let mut child = bin + .cmd() .env("IETS_PROGRESS", "true") .env("IETS_CACHE_DIR", cache.path()) .env("IETS_CACHE", "0") diff --git a/tests/pure.rs b/tests/pure.rs index 9be8ce0e..8fea118d 100644 --- a/tests/pure.rs +++ b/tests/pure.rs @@ -3,13 +3,16 @@ mod hermetic; +use hermetic::{Iets, bin}; +use rstest::rstest; + use std::fs; use std::path::Path; use std::process::Output; use tempfile::{TempDir, tempdir}; -fn iets(dir: &Path, args: &[&str]) -> Output { - hermetic::iets() +fn run(bin: &Iets, dir: &Path, args: &[&str]) -> Output { + bin.cmd() .args(args) .current_dir(dir) .env("IETS_CACHE", "0") @@ -38,16 +41,16 @@ fn checkout() -> TempDir { d } -#[test] -fn a_read_in_the_project_is_pure_and_one_above_it_is_not() { +#[rstest] +fn a_read_in_the_project_is_pure_and_one_above_it_is_not(bin: Iets) { let d = checkout(); let repo = d.path().join("repo"); - let o = iets(&repo, &["eval", "--raw", "-f", "read.nix"]); + let o = run(&bin, &repo, &["eval", "--raw", "-f", "read.nix"]); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!(text(&o.stdout), "in"); - let o = iets(&repo, &["eval", "--raw", "-f", "up.nix"]); + let o = run(&bin, &repo, &["eval", "--raw", "-f", "up.nix"]); assert_eq!(o.status.code(), Some(1), "{}", text(&o.stdout)); let e = text(&o.stderr); assert!( @@ -55,46 +58,47 @@ fn a_read_in_the_project_is_pure_and_one_above_it_is_not() { "{e}" ); - let o = iets(&repo, &["eval", "--raw", "--impure", "-f", "up.nix"]); + let o = run(&bin, &repo, &["eval", "--raw", "--impure", "-f", "up.nix"]); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!(text(&o.stdout), "out"); } /// No marker above the entry file: its own directory is the project, so a /// one-off expression in a scratch directory still evaluates. -#[test] -fn a_directory_with_no_marker_is_its_own_project() { +#[rstest] +fn a_directory_with_no_marker_is_its_own_project(bin: Iets) { let d = checkout(); let repo = d.path().join("repo"); fs::remove_file(repo.join("iets.toml")).unwrap(); - let o = iets(&repo, &["eval", "--raw", "-f", "read.nix"]); + let o = run(&bin, &repo, &["eval", "--raw", "-f", "read.nix"]); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!(text(&o.stdout), "in"); } -#[test] -fn the_other_purity_rules_hold_without_a_flag() { +#[rstest] +fn the_other_purity_rules_hold_without_a_flag(bin: Iets) { let d = checkout(); let repo = d.path().join("repo"); - let o = iets(&repo, &["eval", "--raw", "-f", "env.nix"]); + let o = run(&bin, &repo, &["eval", "--raw", "-f", "env.nix"]); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); assert_eq!(text(&o.stdout), ""); - let o = iets(&repo, &["eval", "-E", "builtins ? currentSystem"]); + let o = run(&bin, &repo, &["eval", "-E", "builtins ? currentSystem"]); assert_eq!(text(&o.stdout).trim(), "false"); } /// The user's file turns it off for every run; a project's file may not. -#[test] -fn the_pure_setting_is_the_users_alone() { +#[rstest] +fn the_pure_setting_is_the_users_alone(bin: Iets) { let d = checkout(); let repo = d.path().join("repo"); let conf = d.path().join("config/iets"); fs::create_dir_all(&conf).unwrap(); fs::write(conf.join("iets.toml"), "pure = false\n").unwrap(); - let o = hermetic::iets() + let o = bin + .cmd() .args(["eval", "--raw", "-f", "up.nix"]) .current_dir(&repo) .env("XDG_CONFIG_HOME", d.path().join("config")) @@ -105,7 +109,7 @@ fn the_pure_setting_is_the_users_alone() { assert_eq!(text(&o.stdout), "out"); fs::write(repo.join("iets.toml"), "pure = false\n").unwrap(); - let o = iets(&repo, &["eval", "--raw", "-f", "read.nix"]); + let o = run(&bin, &repo, &["eval", "--raw", "-f", "read.nix"]); assert_eq!(o.status.code(), Some(1), "{}", text(&o.stdout)); let e = text(&o.stderr); assert!(e.contains("'pure' may not be set in a project file"), "{e}"); @@ -113,8 +117,8 @@ fn the_pure_setting_is_the_users_alone() { /// A `narHash` locks a `path:` reference, so a pure evaluation takes it when /// the tree matches, refuses a tree that does not, and refuses no hash at all. -#[test] -fn get_flake_takes_a_path_its_nar_hash_locks() { +#[rstest] +fn get_flake_takes_a_path_its_nar_hash_locks(bin: Iets) { let d = tempdir().unwrap(); fs::write(d.path().join("flake.nix"), "{ outputs = _: { x = 42; }; }").unwrap(); fs::write( @@ -126,13 +130,17 @@ fn get_flake_takes_a_path_its_nar_hash_locks() { let at = String::from_utf8(iets::sys::path_to_internal(d.path())).unwrap(); let tree = format!(r#"(builtins.fetchTree {{ type = "path"; path = "{at}"; }}).narHash"#); - let o = iets(d.path(), &["eval", "--impure", "--raw", "--expr", &tree]); + let o = run( + &bin, + d.path(), + &["eval", "--impure", "--raw", "--expr", &tree], + ); assert_eq!(o.status.code(), Some(0), "{}", text(&o.stderr)); let hash = text(&o.stdout); let get = |query: &str| { let e = format!(r#"(builtins.getFlake "path:{at}{query}").x"#); - iets(d.path(), &["eval", "--expr", &e]) + run(&bin, d.path(), &["eval", "--expr", &e]) }; let right = get(&format!("?narHash={hash}")); let wrong = get("?narHash=sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); diff --git a/tests/store_dir.rs b/tests/store_dir.rs index ad19298c..0abec7e9 100644 --- a/tests/store_dir.rs +++ b/tests/store_dir.rs @@ -3,10 +3,14 @@ mod hermetic; -#[test] -fn store_dir_follows_nix_store_dir() { +use hermetic::{Iets, bin}; +use rstest::rstest; + +#[rstest] +fn store_dir_follows_nix_store_dir(bin: Iets) { let expr = r#"[ builtins.storeDir (derivation { name = "x"; system = "x"; builder = "/b"; }).drvPath ]"#; - let o = hermetic::iets() + let o = bin + .cmd() .env("NIX_STORE_DIR", "/tmp/other/store") .args(["eval", "--json", "--strict", "--expr", expr]) .output() @@ -20,14 +24,15 @@ fn store_dir_follows_nix_store_dir() { // Windows has no daemon socket #[cfg(unix)] -#[test] -fn daemon_socket_follows_nix_state_dir() { +#[rstest] +fn daemon_socket_follows_nix_state_dir(bin: Iets) { let drv = r#"derivation { name = "x"; system = "x"; builder = "/b"; }"#; for args in [ &["build", "--expr", drv][..], &["nix-instantiate", "--expr", drv], ] { - let o = hermetic::iets() + let o = bin + .cmd() .env("NIX_STATE_DIR", "/nonexistent/state") .args(args) .output()