max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_0153yKAYUmoC9bZz2eV6cA88
1 file changed,
+53 insertions,
-18 deletions
| @@ -241,19 +241,23 @@ | |||
| 241 | 241 | /// The builder's default language set and the Containerfile's `ARG LANGS` | |
| 242 | 242 | /// default are one decision written in two files, so they are asserted equal. | |
| 243 | 243 | /// | |
| 244 | - | /// The rule behind the value (Max, 2026-08-17) is that the default is whatever | |
| 245 | - | /// compiles the stack Alloy ships: a user rebuilding a program that came with | |
| 246 | - | /// the image should not have to install a toolchain first. That makes the | |
| 247 | - | /// value a measurement of the shipped binaries rather than a preference, and | |
| 248 | - | /// it makes drift between the two files a real defect rather than an | |
| 249 | - | /// inconsistency — a build that bypasses the TUI has to get the stack the TUI | |
| 250 | - | /// would have asked for, and `alloy image` printing a command whose `LANGS` | |
| 251 | - | /// matches the ARG it is overriding is how anyone would ever notice. | |
| 244 | + | /// The rule behind the value (Max, 2026-08-17) was that the default is whatever | |
| 245 | + | /// compiles the stack Alloy ships. That ruling has since been narrowed on the | |
| 246 | + | /// `langs` field's own doc comment: compiling the stack is an argument about a | |
| 247 | + | /// machine being a build host, a build host is a thing a mint asks for, and | |
| 248 | + | /// fw13 and astra both pass `LANGS=rust` explicitly. So the default is now no | |
| 249 | + | /// toolchain at all, in both files. What the test asserts is unchanged: they are | |
| 250 | + | /// one decision, and drift between them is a real defect, because a build that | |
| 251 | + | /// bypasses the TUI has to get the stack the TUI would have asked for. | |
| 252 | 252 | /// | |
| 253 | - | /// Asserted against the literal in `image.rs` rather than by importing it: the | |
| 254 | - | /// crate is a binary, the test is a text check over the Containerfile in the | |
| 255 | - | /// spirit of the rest of this file, and the point is that the two spellings | |
| 256 | - | /// agree. | |
| 253 | + | /// Asserted against `image.rs` as text rather than by importing it: the crate is | |
| 254 | + | /// a binary, the test is a text check over the Containerfile in the spirit of | |
| 255 | + | /// the rest of this file, and the point is that the two spellings agree. | |
| 256 | + | /// | |
| 257 | + | /// `Choices` derives `Default`, so the empty set is the derive rather than a | |
| 258 | + | /// literal. That is read here rather than demanded either way: a hand-written | |
| 259 | + | /// `impl Default` naming languages is a legitimate future state, and this has to | |
| 260 | + | /// compare against whichever one is in force, not fail because the other is. | |
| 257 | 261 | #[test] | |
| 258 | 262 | fn the_langs_default_matches_the_builder() { | |
| 259 | 263 | let text = containerfile(); | |
| @@ -265,16 +269,27 @@ | |||
| 265 | 269 | let source = | |
| 266 | 270 | std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/image.rs")) | |
| 267 | 271 | .expect("cannot read image.rs"); | |
| 268 | - | // Either spelling of the default, because an empty default is a real | |
| 269 | - | // answer here and `BTreeSet::from([])` is not how it is written. | |
| 272 | + | // The derive is the default while there is no hand-written one, and a | |
| 273 | + | // derived `BTreeSet` is empty. Scoped to `impl Default for Choices` rather | |
| 274 | + | // than any `langs: BTreeSet::` line, because the tests below `Choices` | |
| 275 | + | // build fixtures with that exact shape and the first match in the file used | |
| 276 | + | // to be one of them. | |
| 270 | 277 | let builder = source | |
| 271 | - | .lines() | |
| 272 | - | .find_map(|line| line.trim().strip_prefix("langs: BTreeSet::")) | |
| 273 | - | .expect("no langs default in image.rs"); | |
| 278 | + | .split_once("impl Default for Choices {") | |
| 279 | + | .and_then(|(_, rest)| { | |
| 280 | + | rest.lines() | |
| 281 | + | .take_while(|line| !line.starts_with('}')) | |
| 282 | + | .find_map(|line| line.trim().strip_prefix("langs: BTreeSet::")) | |
| 283 | + | }) | |
| 284 | + | .unwrap_or(""); | |
| 274 | 285 | let builder = builder | |
| 275 | 286 | .strip_prefix("from([") | |
| 276 | 287 | .and_then(|rest| rest.split(']').next()) | |
| 277 | 288 | .unwrap_or(""); | |
| 289 | + | assert!( | |
| 290 | + | derives_default(&source), | |
| 291 | + | "Choices neither derives Default nor writes one, so it has no default to agree with", | |
| 292 | + | ); | |
| 278 | 293 | ||
| 279 | 294 | let arg = arg.trim(); | |
| 280 | 295 | let from_arg: Vec<&str> = if arg.is_empty() { | |
| @@ -328,7 +343,7 @@ | |||
| 328 | 343 | std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/image.rs")) | |
| 329 | 344 | .expect("cannot read image.rs"); | |
| 330 | 345 | assert!( | |
| 331 | - | source.contains("db: Db::default()"), | |
| 346 | + | source.contains("db: Db::default()") || derives_default(&source), | |
| 332 | 347 | "the builder does not default its db field" | |
| 333 | 348 | ); | |
| 334 | 349 | ||
| @@ -356,3 +371,23 @@ | |||
| 356 | 371 | "ARG DB={arg} and the default variant of Db disagree; they are one decision", | |
| 357 | 372 | ); | |
| 358 | 373 | } | |
| 374 | + | ||
| 375 | + | /// Whether `Choices` takes its default from the derive. | |
| 376 | + | /// | |
| 377 | + | /// The two tests above compare a Containerfile `ARG` against the builder's | |
| 378 | + | /// default, and the builder has had that default written both ways. Reading | |
| 379 | + | /// which one is in force keeps them checking the agreement rather than the | |
| 380 | + | /// spelling: `#[derive(Default)]` on the struct means every field's default is | |
| 381 | + | /// declared on its own type, which is where the `#[default]` variant checks | |
| 382 | + | /// point anyway. | |
| 383 | + | fn derives_default(source: &str) -> bool { | |
| 384 | + | source | |
| 385 | + | .split_once("pub(crate) struct Choices {") | |
| 386 | + | .and_then(|(before, _)| { | |
| 387 | + | before | |
| 388 | + | .lines() | |
| 389 | + | .rev() | |
| 390 | + | .find(|line| line.starts_with("#[derive(")) | |
| 391 | + | }) | |
| 392 | + | .is_some_and(|derive| derive.contains("Default")) | |
| 393 | + | } |