msri 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {msri-0.2.2 → msri-0.2.4}/Cargo.lock +42 -31
  2. {msri-0.2.2 → msri-0.2.4}/Cargo.toml +1 -1
  3. {msri-0.2.2 → msri-0.2.4}/PKG-INFO +36 -26
  4. {msri-0.2.2 → msri-0.2.4}/README.md +35 -25
  5. {msri-0.2.2 → msri-0.2.4}/SECURITY.md +2 -2
  6. {msri-0.2.2 → msri-0.2.4}/docs/cli-reference.md +6 -4
  7. {msri-0.2.2 → msri-0.2.4}/docs/what-is-missouri.md +3 -2
  8. {msri-0.2.2 → msri-0.2.4}/src/bin/msri.rs +2 -1
  9. {msri-0.2.2 → msri-0.2.4}/src/executor.rs +285 -18
  10. msri-0.2.4/tests/nix_once.rs +302 -0
  11. {msri-0.2.2 → msri-0.2.4}/.gitignore +0 -0
  12. {msri-0.2.2 → msri-0.2.4}/LICENSE +0 -0
  13. {msri-0.2.2 → msri-0.2.4}/action.yml +0 -0
  14. {msri-0.2.2 → msri-0.2.4}/almanac.yml +0 -0
  15. {msri-0.2.2 → msri-0.2.4}/build.rs +0 -0
  16. {msri-0.2.2 → msri-0.2.4}/diataxis.yml +0 -0
  17. {msri-0.2.2 → msri-0.2.4}/docker/mitm/Dockerfile +0 -0
  18. {msri-0.2.2 → msri-0.2.4}/docs/getting-started.md +0 -0
  19. {msri-0.2.2 → msri-0.2.4}/docs/writing-tests.md +0 -0
  20. {msri-0.2.2 → msri-0.2.4}/flake.lock +0 -0
  21. {msri-0.2.2 → msri-0.2.4}/flake.nix +0 -0
  22. {msri-0.2.2 → msri-0.2.4}/homebrew/missouri.rb +0 -0
  23. {msri-0.2.2 → msri-0.2.4}/pyproject.toml +0 -0
  24. {msri-0.2.2 → msri-0.2.4}/src/agent_cli.rs +0 -0
  25. {msri-0.2.2 → msri-0.2.4}/src/agent_eval.rs +0 -0
  26. {msri-0.2.2 → msri-0.2.4}/src/cli.rs +0 -0
  27. {msri-0.2.2 → msri-0.2.4}/src/compare.rs +0 -0
  28. {msri-0.2.2 → msri-0.2.4}/src/config.rs +0 -0
  29. {msri-0.2.2 → msri-0.2.4}/src/docgen.rs +0 -0
  30. {msri-0.2.2 → msri-0.2.4}/src/error.rs +0 -0
  31. {msri-0.2.2 → msri-0.2.4}/src/graph.rs +0 -0
  32. {msri-0.2.2 → msri-0.2.4}/src/lib.rs +0 -0
  33. {msri-0.2.2 → msri-0.2.4}/src/main.rs +0 -0
  34. {msri-0.2.2 → msri-0.2.4}/src/mangen.rs +0 -0
  35. {msri-0.2.2 → msri-0.2.4}/src/paths.rs +0 -0
  36. {msri-0.2.2 → msri-0.2.4}/src/recorder.rs +0 -0
  37. {msri-0.2.2 → msri-0.2.4}/src/report.rs +0 -0
  38. {msri-0.2.2 → msri-0.2.4}/src/scaffold.rs +0 -0
  39. {msri-0.2.2 → msri-0.2.4}/src/signal.rs +0 -0
  40. {msri-0.2.2 → msri-0.2.4}/tests/docker_refusals.rs +0 -0
  41. {msri-0.2.2 → msri-0.2.4}/tests/docs_pages.rs +0 -0
  42. {msri-0.2.2 → msri-0.2.4}/tests/short_name_shim.rs +0 -0
  43. {msri-0.2.2 → msri-0.2.4}/uv.lock +0 -0
@@ -107,7 +107,7 @@ version = "1.1.5"
107
107
  source = "registry+https://github.com/rust-lang/crates.io-index"
108
108
  checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
109
109
  dependencies = [
110
- "windows-sys 0.61.2",
110
+ "windows-sys 0.60.2",
111
111
  ]
112
112
 
113
113
  [[package]]
@@ -118,7 +118,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
118
118
  dependencies = [
119
119
  "anstyle",
120
120
  "once_cell_polyfill",
121
- "windows-sys 0.61.2",
121
+ "windows-sys 0.60.2",
122
122
  ]
123
123
 
124
124
  [[package]]
@@ -159,9 +159,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
159
159
 
160
160
  [[package]]
161
161
  name = "autocfg"
162
- version = "1.5.0"
162
+ version = "1.5.1"
163
163
  source = "registry+https://github.com/rust-lang/crates.io-index"
164
- checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
164
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
165
165
 
166
166
  [[package]]
167
167
  name = "aws-lc-rs"
@@ -351,7 +351,7 @@ dependencies = [
351
351
  "maybe-owned",
352
352
  "rustix",
353
353
  "rustix-linux-procfs",
354
- "windows-sys 0.61.2",
354
+ "windows-sys 0.60.2",
355
355
  "winx",
356
356
  ]
357
357
 
@@ -624,6 +624,15 @@ dependencies = [
624
624
  "parking_lot_core",
625
625
  ]
626
626
 
627
+ [[package]]
628
+ name = "defmt"
629
+ version = "0.3.101"
630
+ source = "registry+https://github.com/rust-lang/crates.io-index"
631
+ checksum = "4deef977660e373c458df605a913d4ab121db0ccd18d1ad6976dff7d37cddb53"
632
+ dependencies = [
633
+ "defmt 1.1.1",
634
+ ]
635
+
627
636
  [[package]]
628
637
  name = "defmt"
629
638
  version = "1.1.1"
@@ -771,15 +780,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
771
780
  checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
772
781
  dependencies = [
773
782
  "libc",
774
- "windows-sys 0.61.2",
783
+ "windows-sys 0.59.0",
775
784
  ]
776
785
 
777
786
  [[package]]
778
787
  name = "faster-hex"
779
- version = "0.10.0"
788
+ version = "0.10.1"
780
789
  source = "registry+https://github.com/rust-lang/crates.io-index"
781
- checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
790
+ checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed"
782
791
  dependencies = [
792
+ "autocfg",
793
+ "defmt 0.3.101",
783
794
  "heapless",
784
795
  "serde",
785
796
  ]
@@ -2236,7 +2247,7 @@ version = "0.2.35"
2236
2247
  source = "registry+https://github.com/rust-lang/crates.io-index"
2237
2248
  checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
2238
2249
  dependencies = [
2239
- "defmt",
2250
+ "defmt 1.1.1",
2240
2251
  "jiff-core",
2241
2252
  "jiff-static",
2242
2253
  "jiff-tzdb-platform",
@@ -2253,7 +2264,7 @@ version = "0.1.0"
2253
2264
  source = "registry+https://github.com/rust-lang/crates.io-index"
2254
2265
  checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
2255
2266
  dependencies = [
2256
- "defmt",
2267
+ "defmt 1.1.1",
2257
2268
  ]
2258
2269
 
2259
2270
  [[package]]
@@ -2506,9 +2517,9 @@ dependencies = [
2506
2517
 
2507
2518
  [[package]]
2508
2519
  name = "mio"
2509
- version = "1.1.1"
2520
+ version = "1.2.4"
2510
2521
  source = "registry+https://github.com/rust-lang/crates.io-index"
2511
- checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
2522
+ checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
2512
2523
  dependencies = [
2513
2524
  "libc",
2514
2525
  "wasi",
@@ -2517,7 +2528,7 @@ dependencies = [
2517
2528
 
2518
2529
  [[package]]
2519
2530
  name = "missouri"
2520
- version = "0.2.2"
2531
+ version = "0.2.4"
2521
2532
  dependencies = [
2522
2533
  "assert_cmd",
2523
2534
  "bollard",
@@ -2779,9 +2790,9 @@ dependencies = [
2779
2790
 
2780
2791
  [[package]]
2781
2792
  name = "pulldown-cmark"
2782
- version = "0.13.4"
2793
+ version = "0.13.5"
2783
2794
  source = "registry+https://github.com/rust-lang/crates.io-index"
2784
- checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
2795
+ checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb"
2785
2796
  dependencies = [
2786
2797
  "bitflags 2.11.0",
2787
2798
  "memchr",
@@ -2810,9 +2821,9 @@ dependencies = [
2810
2821
 
2811
2822
  [[package]]
2812
2823
  name = "quinn-proto"
2813
- version = "0.11.17"
2824
+ version = "0.11.19"
2814
2825
  source = "registry+https://github.com/rust-lang/crates.io-index"
2815
- checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
2826
+ checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe"
2816
2827
  dependencies = [
2817
2828
  "aws-lc-rs",
2818
2829
  "bytes",
@@ -2842,7 +2853,7 @@ dependencies = [
2842
2853
  "once_cell",
2843
2854
  "socket2",
2844
2855
  "tracing",
2845
- "windows-sys 0.61.2",
2856
+ "windows-sys 0.59.0",
2846
2857
  ]
2847
2858
 
2848
2859
  [[package]]
@@ -3055,7 +3066,7 @@ dependencies = [
3055
3066
  "errno",
3056
3067
  "libc",
3057
3068
  "linux-raw-sys",
3058
- "windows-sys 0.61.2",
3069
+ "windows-sys 0.59.0",
3059
3070
  ]
3060
3071
 
3061
3072
  [[package]]
@@ -3070,9 +3081,9 @@ dependencies = [
3070
3081
 
3071
3082
  [[package]]
3072
3083
  name = "rustls"
3073
- version = "0.23.43"
3084
+ version = "0.23.46"
3074
3085
  source = "registry+https://github.com/rust-lang/crates.io-index"
3075
- checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
3086
+ checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
3076
3087
  dependencies = [
3077
3088
  "aws-lc-rs",
3078
3089
  "once_cell",
@@ -3122,7 +3133,7 @@ dependencies = [
3122
3133
  "security-framework",
3123
3134
  "security-framework-sys",
3124
3135
  "webpki-root-certs",
3125
- "windows-sys 0.61.2",
3136
+ "windows-sys 0.59.0",
3126
3137
  ]
3127
3138
 
3128
3139
  [[package]]
@@ -3133,9 +3144,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
3133
3144
 
3134
3145
  [[package]]
3135
3146
  name = "rustls-webpki"
3136
- version = "0.103.14"
3147
+ version = "0.103.16"
3137
3148
  source = "registry+https://github.com/rust-lang/crates.io-index"
3138
- checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
3149
+ checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
3139
3150
  dependencies = [
3140
3151
  "aws-lc-rs",
3141
3152
  "ring",
@@ -3398,7 +3409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
3398
3409
  checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
3399
3410
  dependencies = [
3400
3411
  "libc",
3401
- "windows-sys 0.61.2",
3412
+ "windows-sys 0.60.2",
3402
3413
  ]
3403
3414
 
3404
3415
  [[package]]
@@ -3503,7 +3514,7 @@ dependencies = [
3503
3514
  "getrandom 0.4.1",
3504
3515
  "once_cell",
3505
3516
  "rustix",
3506
- "windows-sys 0.61.2",
3517
+ "windows-sys 0.59.0",
3507
3518
  ]
3508
3519
 
3509
3520
  [[package]]
@@ -3618,9 +3629,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
3618
3629
 
3619
3630
  [[package]]
3620
3631
  name = "tokio"
3621
- version = "1.50.0"
3632
+ version = "1.54.1"
3622
3633
  source = "registry+https://github.com/rust-lang/crates.io-index"
3623
- checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
3634
+ checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
3624
3635
  dependencies = [
3625
3636
  "bytes",
3626
3637
  "libc",
@@ -4007,9 +4018,9 @@ dependencies = [
4007
4018
 
4008
4019
  [[package]]
4009
4020
  name = "webpki-root-certs"
4010
- version = "1.0.9"
4021
+ version = "1.0.11"
4011
4022
  source = "registry+https://github.com/rust-lang/crates.io-index"
4012
- checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
4023
+ checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
4013
4024
  dependencies = [
4014
4025
  "rustls-pki-types",
4015
4026
  ]
@@ -4036,7 +4047,7 @@ version = "0.1.11"
4036
4047
  source = "registry+https://github.com/rust-lang/crates.io-index"
4037
4048
  checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
4038
4049
  dependencies = [
4039
- "windows-sys 0.61.2",
4050
+ "windows-sys 0.59.0",
4040
4051
  ]
4041
4052
 
4042
4053
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "missouri"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  edition = "2024"
5
5
  # 1.88, read from the dependency graph rather than from the edition.
6
6
  # Edition 2024 needs 1.85, and a locked dependency here asks for 1.88.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msri
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  License-File: LICENSE
5
5
  Summary: Show-me-state: e2e testing as directed graphs of filesystem states
6
6
  Home-Page: https://github.com/cjohnhanson/missouri
@@ -59,8 +59,8 @@ environment variable from the host. Declare every variable a test needs
59
59
  in the test config. The test then behaves the same on your machine and in
60
60
  CI.
61
61
 
62
- For stronger isolation, missouri also runs a command inside a nix shell
63
- or a Docker container.
62
+ A `packages` list gives each command programs from nixpkgs. With
63
+ `docker: true`, missouri runs a command inside a Docker container.
64
64
 
65
65
  ## Beyond filesystem diffs
66
66
 
@@ -72,40 +72,50 @@ running. An agent assertion hands a judgment call to an LLM.
72
72
 
73
73
  ## Install
74
74
 
75
- The package is `msri` on PyPI and npm, because `missouri` was taken. The
76
- command is `missouri` everywhere, and both names install together.
75
+ The command is `missouri`, whichever package you install.
77
76
 
78
- ```sh
79
- cargo install --locked missouri
80
- brew install cjohnhanson/tap/missouri
81
- uv tool install msri
82
- npm install -g msri
83
- ```
77
+ **[Archives of precompiled binaries are available for macOS and
78
+ Linux.](https://github.com/cjohnhanson/missouri/releases)** The Linux
79
+ binaries are static executables. Each archive holds the binary, its man
80
+ pages, the README and the licence. There is no Windows build.
84
81
 
85
- `cargo install` builds from source. It needs Rust 1.88 and a C
86
- compiler. The other three carry a prebuilt binary for macOS and Linux,
87
- x86-64 and arm64, published by a tagged release.
82
+ | Package manager | Package | Command |
83
+ | --- | --- | --- |
84
+ | [Homebrew](https://brew.sh) | [cjohnhanson/tap/missouri](https://github.com/cjohnhanson/homebrew-tap) | `brew install cjohnhanson/tap/missouri` |
85
+ | [Cargo](https://doc.rust-lang.org/cargo/) | [missouri](https://crates.io/crates/missouri) | `cargo install --locked missouri` |
86
+ | [uv](https://docs.astral.sh/uv/) | [msri](https://pypi.org/project/msri/) | `uv tool install msri` |
87
+ | [npm](https://www.npmjs.com) | [@cjohnhanson/msri](https://www.npmjs.com/package/@cjohnhanson/msri) | `npm install -g @cjohnhanson/msri` |
88
88
 
89
- To build the unreleased `main` branch:
89
+ On Debian or Ubuntu, download the `.deb` from the [releases
90
+ page](https://github.com/cjohnhanson/missouri/releases) and install it:
90
91
 
91
- ```sh
92
- cargo install --locked --git https://github.com/cjohnhanson/missouri
92
+ ```
93
+ wget https://github.com/cjohnhanson/missouri/releases/download/v0.2.3/missouri_0.2.3-1_amd64.deb
94
+ sudo dpkg -i missouri_0.2.3-1_amd64.deb
93
95
  ```
94
96
 
95
- Or run it without installing:
97
+ To run it once without installing anything:
96
98
 
97
- ```sh
99
+ ```
98
100
  uvx msri run
99
- npx msri run
101
+ npx @cjohnhanson/msri run
100
102
  ```
101
103
 
102
- From that point the [releases
103
- page](https://github.com/cjohnhanson/missouri/releases) also carries
104
- prebuilt archives and a `.deb`. Each archive holds the binary and the man
105
- page. Install a `.deb` with `dpkg -i`: it
106
- is a file, not a repository, so `apt-get install` does not reach it.
104
+ ### Building
105
+
106
+ missouri is written in Rust, so you need a [Rust
107
+ installation](https://www.rust-lang.org/) to compile it. missouri
108
+ compiles with Rust 1.88 or newer. A C compiler is needed as well, which
109
+ `aws-lc-sys` uses for its cryptography.
107
110
 
108
- Check the install with `missouri --version`.
111
+ To build missouri:
112
+
113
+ ```
114
+ git clone https://github.com/cjohnhanson/missouri
115
+ cd missouri
116
+ cargo build --release
117
+ ./target/release/missouri --version
118
+ ```
109
119
 
110
120
  ## Usage
111
121
 
@@ -49,8 +49,8 @@ environment variable from the host. Declare every variable a test needs
49
49
  in the test config. The test then behaves the same on your machine and in
50
50
  CI.
51
51
 
52
- For stronger isolation, missouri also runs a command inside a nix shell
53
- or a Docker container.
52
+ A `packages` list gives each command programs from nixpkgs. With
53
+ `docker: true`, missouri runs a command inside a Docker container.
54
54
 
55
55
  ## Beyond filesystem diffs
56
56
 
@@ -62,40 +62,50 @@ running. An agent assertion hands a judgment call to an LLM.
62
62
 
63
63
  ## Install
64
64
 
65
- The package is `msri` on PyPI and npm, because `missouri` was taken. The
66
- command is `missouri` everywhere, and both names install together.
65
+ The command is `missouri`, whichever package you install.
67
66
 
68
- ```sh
69
- cargo install --locked missouri
70
- brew install cjohnhanson/tap/missouri
71
- uv tool install msri
72
- npm install -g msri
73
- ```
67
+ **[Archives of precompiled binaries are available for macOS and
68
+ Linux.](https://github.com/cjohnhanson/missouri/releases)** The Linux
69
+ binaries are static executables. Each archive holds the binary, its man
70
+ pages, the README and the licence. There is no Windows build.
74
71
 
75
- `cargo install` builds from source. It needs Rust 1.88 and a C
76
- compiler. The other three carry a prebuilt binary for macOS and Linux,
77
- x86-64 and arm64, published by a tagged release.
72
+ | Package manager | Package | Command |
73
+ | --- | --- | --- |
74
+ | [Homebrew](https://brew.sh) | [cjohnhanson/tap/missouri](https://github.com/cjohnhanson/homebrew-tap) | `brew install cjohnhanson/tap/missouri` |
75
+ | [Cargo](https://doc.rust-lang.org/cargo/) | [missouri](https://crates.io/crates/missouri) | `cargo install --locked missouri` |
76
+ | [uv](https://docs.astral.sh/uv/) | [msri](https://pypi.org/project/msri/) | `uv tool install msri` |
77
+ | [npm](https://www.npmjs.com) | [@cjohnhanson/msri](https://www.npmjs.com/package/@cjohnhanson/msri) | `npm install -g @cjohnhanson/msri` |
78
78
 
79
- To build the unreleased `main` branch:
79
+ On Debian or Ubuntu, download the `.deb` from the [releases
80
+ page](https://github.com/cjohnhanson/missouri/releases) and install it:
80
81
 
81
- ```sh
82
- cargo install --locked --git https://github.com/cjohnhanson/missouri
82
+ ```
83
+ wget https://github.com/cjohnhanson/missouri/releases/download/v0.2.3/missouri_0.2.3-1_amd64.deb
84
+ sudo dpkg -i missouri_0.2.3-1_amd64.deb
83
85
  ```
84
86
 
85
- Or run it without installing:
87
+ To run it once without installing anything:
86
88
 
87
- ```sh
89
+ ```
88
90
  uvx msri run
89
- npx msri run
91
+ npx @cjohnhanson/msri run
90
92
  ```
91
93
 
92
- From that point the [releases
93
- page](https://github.com/cjohnhanson/missouri/releases) also carries
94
- prebuilt archives and a `.deb`. Each archive holds the binary and the man
95
- page. Install a `.deb` with `dpkg -i`: it
96
- is a file, not a repository, so `apt-get install` does not reach it.
94
+ ### Building
95
+
96
+ missouri is written in Rust, so you need a [Rust
97
+ installation](https://www.rust-lang.org/) to compile it. missouri
98
+ compiles with Rust 1.88 or newer. A C compiler is needed as well, which
99
+ `aws-lc-sys` uses for its cryptography.
97
100
 
98
- Check the install with `missouri --version`.
101
+ To build missouri:
102
+
103
+ ```
104
+ git clone https://github.com/cjohnhanson/missouri
105
+ cd missouri
106
+ cargo build --release
107
+ ./target/release/missouri --version
108
+ ```
99
109
 
100
110
  ## Usage
101
111
 
@@ -49,5 +49,5 @@ Out of scope:
49
49
 
50
50
  Missouri runs a transition command on the host, as the user who started
51
51
  missouri, unless the project config sets `docker: true`. A `packages`
52
- list puts the command in a nix shell, which supplies programs and does
53
- not confine them. Neither is a vulnerability.
52
+ list puts programs from nixpkgs on the command's `PATH` without
53
+ confining the command. Neither is a vulnerability.
@@ -260,9 +260,9 @@ setup:
260
260
  - command: "db-seed"
261
261
  shell: false
262
262
 
263
- # Nix packages to provide through `nix shell`.
264
- # When this list is not empty, every command runs inside a nix shell
265
- # with these packages.
263
+ # Nixpkgs packages for transition, assertion, and comparator commands.
264
+ # Commands use the environment from one warm-up `nix shell`.
265
+ # If missouri cannot read that environment, each command runs inside its own `nix shell`.
266
266
  packages:
267
267
  - python3
268
268
  - uv
@@ -508,7 +508,9 @@ When you set `test_dir`, missouri looks for the project bin in the test director
508
508
 
509
509
  ### Sandbox and packages
510
510
 
511
- When the project config sets `packages`, every command runs inside `nix shell nixpkgs#pkg1 nixpkgs#pkg2 ... --command`. Missouri resolves the nixpkgs flake reference to a pinned commit hash during a warm-up phase. The warm-up runs before parallel execution starts. This keeps parallel paths from competing for the registry file.
511
+ When the project config sets `packages`, missouri gets the packages from nixpkgs through `nix shell`. For each suite, missouri runs warm-up once before the paths run in parallel. During warm-up, missouri resolves the `nixpkgs` flake reference to a pinned commit hash. Parallel commands never read the flake registry. During warm-up, missouri runs `/usr/bin/env -0` inside one `nix shell` with the packages. If `/usr/bin/env` is missing or does not accept `-0`, Missouri cannot read the environment during warm-up. The warm-up step records the environment that `nix shell` gives a command. This environment puts the package directories first in `PATH`. It also includes any other variables that `nix shell` sets. During the run, missouri roots those store paths to prevent garbage collection from deleting them.
512
+
513
+ With this environment, each transition, assertion, and comparator command runs without its own `nix shell`. Each command sees the same `PATH` as a command inside `nix shell`. The command's stderr contains no messages from nix itself. Nix writes nothing to the cache under the command's `HOME`. If missouri cannot read the environment from warm-up, it prints a warning. In this case, missouri runs each command inside its own `nix shell`. This fallback takes more time. The warning names `MISSOURI_SANDBOX=preinstalled` as the way to skip nix when the packages are already on `PATH`.
512
514
 
513
515
  The `MISSOURI_SANDBOX` environment variable overrides the sandbox behavior:
514
516
 
@@ -183,8 +183,9 @@ model.
183
183
  A Docker-based e2e test gives you isolation, but a container costs time,
184
184
  and a container does not model a state graph well. Missouri gets its
185
185
  isolation from temp directories and `env_clear`, which is cheaper and
186
- needs no container runtime. For stronger isolation, missouri also runs a
187
- command inside a nix shell, or inside a Docker container of its own.
186
+ needs no container runtime. Each command gets programs from nixpkgs
187
+ through a `packages` list. With `docker: true`, each command runs inside
188
+ a Docker container.
188
189
 
189
190
  ## Getting started
190
191
 
@@ -1,4 +1,5 @@
1
- //! The short name, so `uvx msri` and `npx msri` work.
1
+ //! The short name, so `uvx msri` works. On npm the wrapper carries the
2
+ //! scope, so the same run reads `npx @cjohnhanson/msri`.
2
3
  //!
3
4
  //! maturin ties an installed command name to the Cargo bin name, and
4
5
  //! refuses a `[project.scripts]` entry beside a binary. A wheel
@@ -155,6 +155,49 @@ pub struct NixBackend {
155
155
  /// Pinned nixpkgs flake URL (resolved during warm-up).
156
156
  /// When set, used instead of bare `nixpkgs` to avoid registry lookups.
157
157
  pinned_nixpkgs: Option<String>,
158
+ /// The environment one `nix shell` gives a child, captured during
159
+ /// warm-up. When set, each command runs without its own `nix shell`.
160
+ resolved: Option<NixEnv>,
161
+ /// Holds the out-links that root the captured store paths for the
162
+ /// run, so a garbage collection cannot delete them.
163
+ gc_roots: Option<TempDir>,
164
+ }
165
+
166
+ /// What `nix shell` adds to a child's environment: the directories it
167
+ /// puts in front of PATH, and any other variable.
168
+ #[derive(Debug, Clone, PartialEq, Eq)]
169
+ struct NixEnv {
170
+ path_prefix: String,
171
+ extra: Vec<(String, String)>,
172
+ }
173
+
174
+ /// The PATH the capture runs with. It names no directory, so the text in
175
+ /// front of it in the captured PATH is what `nix shell` added.
176
+ const CAPTURE_MARKER: &str = "/nonexistent/missouri-path-marker";
177
+
178
+ /// Read the output of `nix shell ... --command /usr/bin/env -0`, run with
179
+ /// a cleared environment and `marker` as PATH. `None` when the output does
180
+ /// not have that shape.
181
+ fn parse_capture(output: &[u8], marker: &str) -> Option<NixEnv> {
182
+ let text = std::str::from_utf8(output).ok()?;
183
+ let mut path_prefix = None;
184
+ let mut extra = Vec::new();
185
+ for entry in text.split('\0').filter(|entry| !entry.is_empty()) {
186
+ let (key, value) = entry.split_once('=')?;
187
+ if key == "PATH" {
188
+ let prefix = value.strip_suffix(marker)?.strip_suffix(':')?;
189
+ if prefix.is_empty() {
190
+ return None;
191
+ }
192
+ path_prefix = Some(prefix.to_string());
193
+ } else {
194
+ extra.push((key.to_string(), value.to_string()));
195
+ }
196
+ }
197
+ Some(NixEnv {
198
+ path_prefix: path_prefix?,
199
+ extra,
200
+ })
158
201
  }
159
202
 
160
203
  impl NixBackend {
@@ -212,8 +255,127 @@ impl NixBackend {
212
255
  let stderr = String::from_utf8_lossy(&output.stderr);
213
256
  return Err(format!("nix cache warm failed: {stderr}"));
214
257
  }
258
+
259
+ match self.capture_env() {
260
+ Ok((env, roots)) => {
261
+ self.resolved = Some(env);
262
+ self.gc_roots = Some(roots);
263
+ }
264
+ Err(reason) => eprintln!(
265
+ "warning: warm-up could not read the nix environment ({reason}), so each command \
266
+ runs in its own nix shell. If the packages are already on PATH, set \
267
+ MISSOURI_SANDBOX=preinstalled."
268
+ ),
269
+ }
215
270
  Ok(())
216
271
  }
272
+
273
+ /// Run one `nix shell` that prints its child's environment, then root
274
+ /// the store paths it put on PATH for the rest of the run.
275
+ fn capture_env(&self) -> Result<(NixEnv, TempDir), String> {
276
+ let mut args = self.nix_prefix_args();
277
+ args.extend(["/usr/bin/env".into(), "-0".into()]);
278
+ let output = std::process::Command::new(self.nix_bin.as_str())
279
+ .args(&args)
280
+ .env_clear()
281
+ .env("PATH", CAPTURE_MARKER)
282
+ .output()
283
+ .map_err(|e| format!("nix shell did not start: {e}"))?;
284
+ if !output.status.success() {
285
+ return Err("nix shell failed".to_string());
286
+ }
287
+ let env = parse_capture(&output.stdout, CAPTURE_MARKER)
288
+ .ok_or("nix shell printed no PATH of its own")?;
289
+
290
+ // The roots last as long as this directory. A run that is killed
291
+ // never removes it, so its roots stay in the temp directory until
292
+ // something cleans that up.
293
+ let roots = TempDir::new().map_err(|e| format!("no temp directory: {e}"))?;
294
+ let mut store_paths: Vec<&str> = env
295
+ .path_prefix
296
+ .split(':')
297
+ .map(|dir| Utf8Path::new(dir).parent().map_or(dir, |p| p.as_str()))
298
+ .collect();
299
+ store_paths.sort_unstable();
300
+ store_paths.dedup();
301
+ let link = roots.path().join("root");
302
+ let output = std::process::Command::new(self.nix_bin.as_str())
303
+ .args([
304
+ "build",
305
+ "--extra-experimental-features",
306
+ "nix-command flakes",
307
+ ])
308
+ .arg("--out-link")
309
+ .arg(&link)
310
+ .args(&store_paths)
311
+ .output()
312
+ .map_err(|e| format!("nix build did not start: {e}"))?;
313
+ if !output.status.success() {
314
+ return Err("nix build failed".to_string());
315
+ }
316
+ Ok((env, roots))
317
+ }
318
+
319
+ /// A command that runs `sh -c <wrapped>` with the packages on PATH:
320
+ /// directly when warm-up captured the environment, else inside its own
321
+ /// `nix shell`. `path_env` is the PATH missouri built for the command.
322
+ fn sh_command(
323
+ &self,
324
+ wrapped: String,
325
+ work_dir: &Utf8Path,
326
+ env: &BTreeMap<String, String>,
327
+ path_env: &str,
328
+ ) -> Command {
329
+ let Some(resolved) = &self.resolved else {
330
+ let mut args = self.nix_prefix_args();
331
+ args.extend(["sh".into(), "-c".into(), wrapped]);
332
+ let mut cmd = Command::new(self.nix_bin.as_str());
333
+ cmd.args(&args)
334
+ .current_dir(work_dir.as_std_path())
335
+ .env_clear()
336
+ .envs(env.iter())
337
+ .env("PATH", path_env);
338
+ return cmd;
339
+ };
340
+ // The PATH `nix shell` gives its child. Command looks `sh` up on the
341
+ // PATH set here, as `nix shell` does.
342
+ let path = if path_env.is_empty() {
343
+ resolved.path_prefix.clone()
344
+ } else {
345
+ format!("{}:{path_env}", resolved.path_prefix)
346
+ };
347
+ let mut cmd = Command::new("sh");
348
+ cmd.args(["-c", &wrapped])
349
+ .current_dir(work_dir.as_std_path())
350
+ .env_clear()
351
+ .envs(env.iter())
352
+ .env("PATH", path);
353
+ for (key, value) in &resolved.extra {
354
+ if !env.contains_key(key) {
355
+ cmd.env(key, value);
356
+ }
357
+ }
358
+ // nix shell raises the soft open-file limit of the command it runs
359
+ // to the hard limit. The child does the same here; missouri's own
360
+ // limit stays as it is.
361
+ // SAFETY: the closure runs in the child between fork and exec, and
362
+ // calls only getrlimit and setrlimit, which are async-signal-safe.
363
+ unsafe {
364
+ std::os::unix::process::CommandExt::pre_exec(&mut cmd, || {
365
+ let mut limit = libc::rlimit {
366
+ rlim_cur: 0,
367
+ rlim_max: 0,
368
+ };
369
+ if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) == 0 {
370
+ limit.rlim_cur = limit.rlim_max;
371
+ // A failure leaves the limit as it was, as under nix.
372
+ libc::setrlimit(libc::RLIMIT_NOFILE, &limit);
373
+ }
374
+ Ok(())
375
+ });
376
+ }
377
+ cmd
378
+ }
217
379
  }
218
380
 
219
381
  impl Backend for NixBackend {
@@ -245,15 +407,7 @@ impl Backend for NixBackend {
245
407
  } else {
246
408
  format!("export PATH=\"{project_dirs}:$PATH\"; {command}")
247
409
  };
248
- let mut args = self.nix_prefix_args();
249
- args.extend(["sh".into(), "-c".into(), wrapped]);
250
- let mut cmd = Command::new(self.nix_bin.as_str());
251
- cmd.args(&args)
252
- .current_dir(work_dir.as_std_path())
253
- .env_clear()
254
- .envs(env.iter())
255
- .env("PATH", path_env);
256
- cmd
410
+ self.sh_command(wrapped, work_dir, env, path_env)
257
411
  }
258
412
 
259
413
  fn build_direct_command(
@@ -270,15 +424,7 @@ impl Backend for NixBackend {
270
424
  .collect::<Vec<_>>()
271
425
  .join(" ");
272
426
  let wrapped = format!("export PATH=\"{path_env}:$PATH\"; {inner}");
273
- let mut args = self.nix_prefix_args();
274
- args.extend(["sh".into(), "-c".into(), wrapped]);
275
- let mut cmd = Command::new(self.nix_bin.as_str());
276
- cmd.args(&args)
277
- .current_dir(work_dir.as_std_path())
278
- .env_clear()
279
- .envs(env.iter())
280
- .env("PATH", path_env);
281
- cmd
427
+ self.sh_command(wrapped, work_dir, env, path_env)
282
428
  }
283
429
  }
284
430
 
@@ -731,6 +877,8 @@ pub fn detect_sandbox(graph: &StateGraph) -> error::Result<Box<dyn Backend>> {
731
877
  nix_bin,
732
878
  packages: packages.clone(),
733
879
  pinned_nixpkgs: None,
880
+ resolved: None,
881
+ gc_roots: None,
734
882
  };
735
883
  backend
736
884
  .warm_cache()
@@ -2726,6 +2874,125 @@ transitions:
2726
2874
  assert!(result.unwrap().as_str().ends_with("nix"));
2727
2875
  }
2728
2876
 
2877
+ #[test]
2878
+ fn a_capture_gives_the_path_prefix() {
2879
+ let out = b"PATH=/nix/store/a-jq/bin:/nix/store/b-jq-man/bin:/m\0";
2880
+ let env = parse_capture(out, "/m").unwrap();
2881
+ assert_eq!(
2882
+ env.path_prefix,
2883
+ "/nix/store/a-jq/bin:/nix/store/b-jq-man/bin"
2884
+ );
2885
+ assert!(env.extra.is_empty());
2886
+ }
2887
+
2888
+ #[test]
2889
+ fn a_capture_keeps_each_variable_nix_added() {
2890
+ let out = b"__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0\0PATH=/nix/store/a-jq/bin:/m\0";
2891
+ let env = parse_capture(out, "/m").unwrap();
2892
+ assert_eq!(
2893
+ env.extra,
2894
+ vec![(
2895
+ "__CF_USER_TEXT_ENCODING".to_string(),
2896
+ "0x1F5:0x0:0x0".to_string()
2897
+ )]
2898
+ );
2899
+ }
2900
+
2901
+ #[test]
2902
+ fn a_capture_without_the_marker_is_refused() {
2903
+ assert_eq!(
2904
+ parse_capture(b"PATH=/nix/store/a-jq/bin:/usr/bin\0", "/m"),
2905
+ None
2906
+ );
2907
+ assert_eq!(parse_capture(b"PATH=/m\0", "/m"), None);
2908
+ assert_eq!(parse_capture(b"PATH=:/m\0", "/m"), None);
2909
+ assert_eq!(parse_capture(b"HOME=/h\0", "/m"), None);
2910
+ assert_eq!(parse_capture(b"not an environment", "/m"), None);
2911
+ }
2912
+
2913
+ #[test]
2914
+ fn a_capture_that_is_not_utf8_is_refused() {
2915
+ assert_eq!(
2916
+ parse_capture(b"PATH=/nix/store/a-jq/bin:/m\0X=\xff\0", "/m"),
2917
+ None
2918
+ );
2919
+ }
2920
+
2921
+ #[test]
2922
+ fn a_resolved_backend_runs_sh_with_the_packages_in_front_of_path() {
2923
+ let backend = NixBackend {
2924
+ nix_bin: Utf8PathBuf::from("/nonexistent/nix"),
2925
+ packages: vec!["jq".into()],
2926
+ pinned_nixpkgs: None,
2927
+ resolved: Some(NixEnv {
2928
+ path_prefix: "/pkg/bin".into(),
2929
+ extra: vec![("ADDED".into(), "1".into()), ("KEPT".into(), "nix".into())],
2930
+ }),
2931
+ gc_roots: None,
2932
+ };
2933
+ let env = BTreeMap::from([("KEPT".to_string(), "state".to_string())]);
2934
+ let cmd = backend.build_shell_command("true", Utf8Path::new("/"), &env, "/usr/bin:/bin");
2935
+ let program = cmd.get_program().to_string_lossy().into_owned();
2936
+ assert_eq!(program, "sh");
2937
+ let args: Vec<String> = cmd
2938
+ .get_args()
2939
+ .map(|a| a.to_string_lossy().into_owned())
2940
+ .collect();
2941
+ assert_eq!(args, vec!["-c".to_string(), "true".to_string()]);
2942
+ let envs: BTreeMap<String, Option<String>> = cmd
2943
+ .get_envs()
2944
+ .map(|(k, v)| {
2945
+ (
2946
+ k.to_string_lossy().into_owned(),
2947
+ v.map(|v| v.to_string_lossy().into_owned()),
2948
+ )
2949
+ })
2950
+ .collect();
2951
+ assert_eq!(envs["PATH"].as_deref(), Some("/pkg/bin:/usr/bin:/bin"));
2952
+ assert_eq!(envs["ADDED"].as_deref(), Some("1"));
2953
+ assert_eq!(envs["KEPT"].as_deref(), Some("state"));
2954
+ }
2955
+
2956
+ fn resolved_backend(path_prefix: &str) -> NixBackend {
2957
+ NixBackend {
2958
+ nix_bin: Utf8PathBuf::from("/nonexistent/nix"),
2959
+ packages: vec!["jq".into()],
2960
+ pinned_nixpkgs: None,
2961
+ resolved: Some(NixEnv {
2962
+ path_prefix: path_prefix.into(),
2963
+ extra: Vec::new(),
2964
+ }),
2965
+ gc_roots: None,
2966
+ }
2967
+ }
2968
+
2969
+ #[test]
2970
+ fn a_resolved_backend_runs_the_sh_a_package_ships() {
2971
+ let pkg = tempfile::tempdir().unwrap();
2972
+ let pkg_bin = Utf8Path::from_path(pkg.path()).unwrap();
2973
+ let sh = pkg_bin.join("sh");
2974
+ fs::write(&sh, "#!/bin/sh\necho package-sh\n").unwrap();
2975
+ fs::set_permissions(&sh, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
2976
+ let backend = resolved_backend(pkg_bin.as_str());
2977
+ let output = backend
2978
+ .build_shell_command("true", pkg_bin, &BTreeMap::new(), "/usr/bin:/bin")
2979
+ .output()
2980
+ .unwrap();
2981
+ assert_eq!(String::from_utf8_lossy(&output.stdout), "package-sh\n");
2982
+ }
2983
+
2984
+ #[test]
2985
+ fn an_empty_command_path_leaves_no_trailing_colon() {
2986
+ let backend = resolved_backend("/pkg/bin");
2987
+ let cmd = backend.build_shell_command("true", Utf8Path::new("/"), &BTreeMap::new(), "");
2988
+ let path = cmd
2989
+ .get_envs()
2990
+ .find(|(k, _)| *k == "PATH")
2991
+ .and_then(|(_, v)| v)
2992
+ .map(|v| v.to_string_lossy().into_owned());
2993
+ assert_eq!(path.as_deref(), Some("/pkg/bin"));
2994
+ }
2995
+
2729
2996
  #[test]
2730
2997
  fn bare_backend_shell_command_runs() {
2731
2998
  let backend = BareBackend;
@@ -0,0 +1,302 @@
1
+ //! A suite that lists `packages` resolves them with one `nix shell` call,
2
+ //! not one call per command. A stand-in `nix` on PATH logs every call,
3
+ //! so the tests count calls without a nix daemon.
4
+
5
+ use std::fs;
6
+ use std::os::unix::fs::PermissionsExt;
7
+ use std::path::{Path, PathBuf};
8
+
9
+ /// How the stand-in `nix` answers warm-up.
10
+ #[derive(Clone, Copy, PartialEq)]
11
+ enum Nix {
12
+ /// Like nix: the capture prints the environment, the build succeeds.
13
+ Good,
14
+ /// The capture prints output that holds no PATH.
15
+ UnreadableCapture,
16
+ /// The capture prints a good environment, then exits 1.
17
+ CaptureExits1,
18
+ /// The build that roots the store paths exits 1.
19
+ BuildFails,
20
+ }
21
+
22
+ /// The stand-in answers `flake metadata` with a pinned URL, answers
23
+ /// `build` by its mode, and runs `shell ... --command <cmd>` the way nix
24
+ /// does: the package's `bin` in front of PATH, then exec.
25
+ fn write_fake_nix(dir: &Path, log: &Path, pkg_bin: &Path, mode: Nix) {
26
+ let mode = match mode {
27
+ Nix::Good => "good",
28
+ Nix::UnreadableCapture => "unreadable",
29
+ Nix::CaptureExits1 => "capture-exits-1",
30
+ Nix::BuildFails => "build-fails",
31
+ };
32
+ let script = format!(
33
+ r#"#!/bin/sh
34
+ printf '%s\n' "$*" >> '{log}'
35
+ case "$1" in
36
+ flake) printf '{{"url":"fake:pinned"}}\n'; exit 0 ;;
37
+ build) [ '{mode}' = build-fails ] && exit 1; exit 0 ;;
38
+ shell)
39
+ while [ $# -gt 0 ] && [ "$1" != "--command" ]; do shift; done
40
+ shift
41
+ PATH='{pkg_bin}':"$PATH"
42
+ export PATH
43
+ if [ "$1" = /usr/bin/env ]; then
44
+ [ '{mode}' = unreadable ] && {{ printf 'not an environment'; exit 0; }}
45
+ [ '{mode}' = capture-exits-1 ] && {{ "$@"; exit 1; }}
46
+ fi
47
+ ulimit -Sn "$(ulimit -Hn)"
48
+ exec "$@" ;;
49
+ esac
50
+ exit 2
51
+ "#,
52
+ log = log.display(),
53
+ pkg_bin = pkg_bin.display(),
54
+ );
55
+ let nix = dir.join("nix");
56
+ fs::write(&nix, script).unwrap();
57
+ fs::set_permissions(&nix, fs::Permissions::from_mode(0o755)).unwrap();
58
+ }
59
+
60
+ fn executable(path: &Path, body: &str) {
61
+ fs::write(path, body).unwrap();
62
+ fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
63
+ }
64
+
65
+ /// A suite with one package, `hello`. Its `hello-pkg` prints `hi`, and
66
+ /// its `cmp-pkg` compares two files and logs that it ran. A shell
67
+ /// transition, a shell assertion, a `shell: false` assertion, and a file
68
+ /// comparator each call a program of the package.
69
+ fn write_suite(root: &Path) {
70
+ fs::create_dir_all(root.join(".missouri")).unwrap();
71
+ fs::write(
72
+ root.join(".missouri/missouri.yml"),
73
+ "packages:\n - hello\n",
74
+ )
75
+ .unwrap();
76
+ fs::create_dir_all(root.join("a/.missouri")).unwrap();
77
+ fs::write(
78
+ root.join("a/.missouri/missouri.yml"),
79
+ r#"transitions:
80
+ - name: "greet"
81
+ command: "hello-pkg > out.txt"
82
+ target: "../b"
83
+ comparators:
84
+ files:
85
+ - path: "out.txt"
86
+ command: "cmp-pkg"
87
+ "#,
88
+ )
89
+ .unwrap();
90
+ fs::create_dir_all(root.join("b/.missouri")).unwrap();
91
+ fs::write(
92
+ root.join("b/.missouri/missouri.yml"),
93
+ r#"assertions:
94
+ - name: "the package is on PATH"
95
+ command: "hello-pkg"
96
+ stdout: "hi\n"
97
+ - name: "the package is on PATH without a shell"
98
+ command: "hello-pkg"
99
+ shell: false
100
+ stdout: "hi\n"
101
+ - name: "the soft open-file limit is the hard limit, as nix shell sets it"
102
+ command: "test \"$(ulimit -Sn)\" = \"$(ulimit -Hn)\""
103
+ "#,
104
+ )
105
+ .unwrap();
106
+ fs::write(root.join("b/out.txt"), "hi\n").unwrap();
107
+ }
108
+
109
+ struct Run {
110
+ passed: bool,
111
+ stdout: String,
112
+ stderr: String,
113
+ nix_calls: Vec<String>,
114
+ comparator_runs: usize,
115
+ pkg_dir: PathBuf,
116
+ }
117
+
118
+ fn run_suite(mode: Nix) -> Run {
119
+ let tmp = tempfile::tempdir().unwrap();
120
+ let fake = tmp.path().join("fake-bin");
121
+ let pkg_dir = tmp.path().join("pkg");
122
+ let pkg_bin = pkg_dir.join("bin");
123
+ let suite = tmp.path().join("suite");
124
+ let log = tmp.path().join("nix-calls.log");
125
+ let comparator_log = tmp.path().join("comparator.log");
126
+ fs::create_dir_all(&fake).unwrap();
127
+ fs::create_dir_all(&pkg_bin).unwrap();
128
+ executable(&pkg_bin.join("hello-pkg"), "#!/bin/sh\necho hi\n");
129
+ executable(
130
+ &pkg_bin.join("cmp-pkg"),
131
+ &format!(
132
+ "#!/bin/sh\necho ran >> '{}'\nexec cmp -s \"$1\" \"$2\"\n",
133
+ comparator_log.display()
134
+ ),
135
+ );
136
+ write_fake_nix(&fake, &log, &pkg_bin, mode);
137
+ write_suite(&suite);
138
+
139
+ let path = format!("{}:/usr/bin:/bin", fake.display());
140
+ // missouri starts with a low soft open-file limit, so a command that
141
+ // inherits it fails the limit assertion.
142
+ let output = std::process::Command::new("/bin/sh")
143
+ .args(["-c", "ulimit -Sn 256 && exec \"$0\" run -d \"$1\""])
144
+ .arg(env!("CARGO_BIN_EXE_missouri"))
145
+ .arg(&suite)
146
+ .env_clear()
147
+ .env("PATH", path)
148
+ .output()
149
+ .unwrap();
150
+ Run {
151
+ passed: output.status.success(),
152
+ stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
153
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
154
+ nix_calls: fs::read_to_string(&log)
155
+ .unwrap_or_default()
156
+ .lines()
157
+ .map(String::from)
158
+ .collect(),
159
+ comparator_runs: fs::read_to_string(&comparator_log)
160
+ .unwrap_or_default()
161
+ .lines()
162
+ .count(),
163
+ // The directory outlives the run only as a name to compare with.
164
+ pkg_dir,
165
+ }
166
+ }
167
+
168
+ /// The verdict lines of a run, with each duration removed: the PASS or
169
+ /// FAIL line of each path, and the summary of counts. Both runs write the
170
+ /// same colour codes, so the lines keep them.
171
+ fn verdict(stdout: &str) -> Vec<String> {
172
+ stdout
173
+ .lines()
174
+ .filter(|line| line.contains("PASS") || line.contains("FAIL") || line.contains(" passed, "))
175
+ .map(|line| match line.rfind(" in ") {
176
+ Some(at) if line.contains(" passed, ") => line[..at].to_string(),
177
+ _ => line
178
+ .rsplit_once(' ')
179
+ .map_or(line, |(head, _)| head)
180
+ .to_string(),
181
+ })
182
+ .collect()
183
+ }
184
+
185
+ fn calls_naming<'a>(run: &'a Run, program: &str) -> Vec<&'a String> {
186
+ run.nix_calls
187
+ .iter()
188
+ .filter(|call| call.starts_with("shell") && call.contains(program))
189
+ .collect()
190
+ }
191
+
192
+ fn assert_fallback(mode: Nix) {
193
+ let good = run_suite(Nix::Good);
194
+ let run = run_suite(mode);
195
+ assert!(
196
+ run.passed,
197
+ "the suite failed\nstdout:\n{}\nstderr:\n{}",
198
+ run.stdout, run.stderr
199
+ );
200
+ let warnings: Vec<&str> = run
201
+ .stderr
202
+ .lines()
203
+ .filter(|line| line.contains("warning"))
204
+ .collect();
205
+ assert_eq!(
206
+ warnings.len(),
207
+ 1,
208
+ "expected one warning line: {}",
209
+ run.stderr
210
+ );
211
+ assert!(
212
+ warnings[0].starts_with("warning: warm-up could not read the nix environment")
213
+ && warnings[0].contains("MISSOURI_SANDBOX=preinstalled"),
214
+ "unexpected warning: {}",
215
+ warnings[0]
216
+ );
217
+ assert_eq!(
218
+ calls_naming(&run, "hello-pkg").len(),
219
+ 3,
220
+ "expected the transition and both assertions through nix: {:?}",
221
+ run.nix_calls
222
+ );
223
+ assert_eq!(
224
+ calls_naming(&run, "cmp-pkg").len(),
225
+ 1,
226
+ "expected the comparator through nix: {:?}",
227
+ run.nix_calls
228
+ );
229
+ assert_eq!(verdict(&run.stdout), verdict(&good.stdout));
230
+ }
231
+
232
+ #[test]
233
+ fn a_suite_resolves_its_packages_once_and_runs_no_command_through_nix() {
234
+ let run = run_suite(Nix::Good);
235
+ assert!(
236
+ run.passed,
237
+ "the suite failed\nstdout:\n{}\nstderr:\n{}",
238
+ run.stdout, run.stderr
239
+ );
240
+ for program in ["hello-pkg", "cmp-pkg"] {
241
+ let per_command = calls_naming(&run, program);
242
+ assert!(
243
+ per_command.is_empty(),
244
+ "{program} ran through nix: {per_command:?}"
245
+ );
246
+ }
247
+ assert_eq!(run.comparator_runs, 1, "the comparator did not run");
248
+ let shells = run
249
+ .nix_calls
250
+ .iter()
251
+ .filter(|call| call.starts_with("shell"))
252
+ .count();
253
+ assert_eq!(
254
+ shells, 2,
255
+ "expected the warm-up and the capture only: {:?}",
256
+ run.nix_calls
257
+ );
258
+ assert!(
259
+ !run.stderr.contains("warning"),
260
+ "no warning on a good capture: {}",
261
+ run.stderr
262
+ );
263
+ }
264
+
265
+ #[test]
266
+ fn warm_up_roots_the_store_path_of_each_package_directory() {
267
+ let run = run_suite(Nix::Good);
268
+ let builds: Vec<Vec<&str>> = run
269
+ .nix_calls
270
+ .iter()
271
+ .filter(|call| call.starts_with("build"))
272
+ .map(|call| call.split_whitespace().collect())
273
+ .collect();
274
+ assert_eq!(builds.len(), 1, "expected one build: {:?}", run.nix_calls);
275
+ let words = &builds[0];
276
+ let store_dir = run.pkg_dir.to_str().unwrap();
277
+ let bin_dir = run.pkg_dir.join("bin");
278
+ assert!(words.contains(&"--out-link"), "no --out-link: {words:?}");
279
+ assert!(
280
+ words.contains(&store_dir),
281
+ "the store path {store_dir} is not rooted: {words:?}"
282
+ );
283
+ assert!(
284
+ !words.contains(&bin_dir.to_str().unwrap()),
285
+ "a bin directory is rooted instead of its store path: {words:?}"
286
+ );
287
+ }
288
+
289
+ #[test]
290
+ fn an_unreadable_capture_warns_and_wraps_each_command_with_the_same_verdict() {
291
+ assert_fallback(Nix::UnreadableCapture);
292
+ }
293
+
294
+ #[test]
295
+ fn a_capture_that_exits_1_warns_and_wraps_each_command_with_the_same_verdict() {
296
+ assert_fallback(Nix::CaptureExits1);
297
+ }
298
+
299
+ #[test]
300
+ fn a_failed_root_build_warns_and_wraps_each_command_with_the_same_verdict() {
301
+ assert_fallback(Nix::BuildFails);
302
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes