msri 0.2.3__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.
- {msri-0.2.3 → msri-0.2.4}/Cargo.lock +42 -31
- {msri-0.2.3 → msri-0.2.4}/Cargo.toml +1 -1
- {msri-0.2.3 → msri-0.2.4}/PKG-INFO +35 -27
- {msri-0.2.3 → msri-0.2.4}/README.md +34 -26
- {msri-0.2.3 → msri-0.2.4}/SECURITY.md +2 -2
- {msri-0.2.3 → msri-0.2.4}/docs/cli-reference.md +6 -4
- {msri-0.2.3 → msri-0.2.4}/docs/what-is-missouri.md +3 -2
- {msri-0.2.3 → msri-0.2.4}/src/executor.rs +285 -18
- msri-0.2.4/tests/nix_once.rs +302 -0
- {msri-0.2.3 → msri-0.2.4}/.gitignore +0 -0
- {msri-0.2.3 → msri-0.2.4}/LICENSE +0 -0
- {msri-0.2.3 → msri-0.2.4}/action.yml +0 -0
- {msri-0.2.3 → msri-0.2.4}/almanac.yml +0 -0
- {msri-0.2.3 → msri-0.2.4}/build.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/diataxis.yml +0 -0
- {msri-0.2.3 → msri-0.2.4}/docker/mitm/Dockerfile +0 -0
- {msri-0.2.3 → msri-0.2.4}/docs/getting-started.md +0 -0
- {msri-0.2.3 → msri-0.2.4}/docs/writing-tests.md +0 -0
- {msri-0.2.3 → msri-0.2.4}/flake.lock +0 -0
- {msri-0.2.3 → msri-0.2.4}/flake.nix +0 -0
- {msri-0.2.3 → msri-0.2.4}/homebrew/missouri.rb +0 -0
- {msri-0.2.3 → msri-0.2.4}/pyproject.toml +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/agent_cli.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/agent_eval.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/bin/msri.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/cli.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/compare.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/config.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/docgen.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/error.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/graph.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/lib.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/main.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/mangen.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/paths.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/recorder.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/report.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/scaffold.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/src/signal.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/tests/docker_refusals.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/tests/docs_pages.rs +0 -0
- {msri-0.2.3 → msri-0.2.4}/tests/short_name_shim.rs +0 -0
- {msri-0.2.3 → 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.
|
|
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.
|
|
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.
|
|
162
|
+
version = "1.5.1"
|
|
163
163
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
164
|
-
checksum = "
|
|
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.
|
|
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.
|
|
783
|
+
"windows-sys 0.59.0",
|
|
775
784
|
]
|
|
776
785
|
|
|
777
786
|
[[package]]
|
|
778
787
|
name = "faster-hex"
|
|
779
|
-
version = "0.10.
|
|
788
|
+
version = "0.10.1"
|
|
780
789
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
781
|
-
checksum = "
|
|
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.
|
|
2520
|
+
version = "1.2.4"
|
|
2510
2521
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2511
|
-
checksum = "
|
|
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.
|
|
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.
|
|
2793
|
+
version = "0.13.5"
|
|
2783
2794
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2784
|
-
checksum = "
|
|
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.
|
|
2824
|
+
version = "0.11.19"
|
|
2814
2825
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2815
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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.
|
|
3084
|
+
version = "0.23.46"
|
|
3074
3085
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3075
|
-
checksum = "
|
|
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.
|
|
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.
|
|
3147
|
+
version = "0.103.16"
|
|
3137
3148
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3138
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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.
|
|
3632
|
+
version = "1.54.1"
|
|
3622
3633
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3623
|
-
checksum = "
|
|
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.
|
|
4021
|
+
version = "1.0.11"
|
|
4011
4022
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4012
|
-
checksum = "
|
|
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.
|
|
4050
|
+
"windows-sys 0.59.0",
|
|
4040
4051
|
]
|
|
4041
4052
|
|
|
4042
4053
|
[[package]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msri
|
|
3
|
-
Version: 0.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
|
-
|
|
63
|
-
|
|
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,42 +72,50 @@ running. An agent assertion hands a judgment call to an LLM.
|
|
|
72
72
|
|
|
73
73
|
## Install
|
|
74
74
|
|
|
75
|
-
The
|
|
76
|
-
npm it carries the scope, `@cjohnhanson/msri`, because the registry
|
|
77
|
-
refuses `msri` as too close to names it already holds. The command is
|
|
78
|
-
`missouri` everywhere, and both names install together.
|
|
75
|
+
The command is `missouri`, whichever package you install.
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
npm install -g @cjohnhanson/msri
|
|
85
|
-
```
|
|
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.
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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` |
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
On Debian or Ubuntu, download the `.deb` from the [releases
|
|
90
|
+
page](https://github.com/cjohnhanson/missouri/releases) and install it:
|
|
92
91
|
|
|
93
|
-
```
|
|
94
|
-
|
|
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
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
To run it once without installing anything:
|
|
98
98
|
|
|
99
|
-
```
|
|
99
|
+
```
|
|
100
100
|
uvx msri run
|
|
101
101
|
npx @cjohnhanson/msri run
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
109
110
|
|
|
110
|
-
|
|
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
|
+
```
|
|
111
119
|
|
|
112
120
|
## Usage
|
|
113
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
|
-
|
|
53
|
-
|
|
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,42 +62,50 @@ running. An agent assertion hands a judgment call to an LLM.
|
|
|
62
62
|
|
|
63
63
|
## Install
|
|
64
64
|
|
|
65
|
-
The
|
|
66
|
-
npm it carries the scope, `@cjohnhanson/msri`, because the registry
|
|
67
|
-
refuses `msri` as too close to names it already holds. The command is
|
|
68
|
-
`missouri` everywhere, and both names install together.
|
|
65
|
+
The command is `missouri`, whichever package you install.
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
npm install -g @cjohnhanson/msri
|
|
75
|
-
```
|
|
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.
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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` |
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
On Debian or Ubuntu, download the `.deb` from the [releases
|
|
80
|
+
page](https://github.com/cjohnhanson/missouri/releases) and install it:
|
|
82
81
|
|
|
83
|
-
```
|
|
84
|
-
|
|
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
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
To run it once without installing anything:
|
|
88
88
|
|
|
89
|
-
```
|
|
89
|
+
```
|
|
90
90
|
uvx msri run
|
|
91
91
|
npx @cjohnhanson/msri run
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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.
|
|
99
100
|
|
|
100
|
-
|
|
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
|
+
```
|
|
101
109
|
|
|
102
110
|
## Usage
|
|
103
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
|
|
53
|
-
|
|
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
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
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`,
|
|
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.
|
|
187
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|