midenup 1.1.0

The Miden toolchain manager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//! What `midenup` says while it works: which stream it lands on, and how the level is chosen.
//!
//! Every assertion here is on a real process's streams, because the split between them is the
//! contract: stdout is what the command was asked for, stderr is how it went.

use std::process::Output;
#[cfg(unix)]
use std::{
    fs::File,
    io::Read,
    os::fd::{FromRawFd, OwnedFd},
    process::{Command, Stdio},
    sync::mpsc,
    time::{Duration, Instant},
};

mod common;

use common::*;

/// Runs the real binary against `env`.
fn run(env: &TestEnvironment, manifest_uri: &str, args: &[&str]) -> Output {
    midenup_command(env!("CARGO_BIN_EXE_midenup"), env, manifest_uri)
        .args(args)
        .output()
        .expect("failed to run midenup")
}

fn streams(output: &Output) -> (String, String) {
    (
        String::from_utf8_lossy(&output.stdout).into_owned(),
        String::from_utf8_lossy(&output.stderr).into_owned(),
    )
}

/// The default level announces each component, and does it on stderr.
#[test]
fn integration_reporting_default_announces_components_on_stderr() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_default");
    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");

    let output = run(&test_env, &fixture.manifest_uri, &["install", "stable"]);
    let (stdout, stderr) = streams(&output);
    assert!(output.status.success(), "install must succeed: {stderr}");

    assert!(stderr.contains("component 'vm'"), "each component must be announced: {stderr}");
    assert!(
        stderr.contains("installed channel '0.15.0'"),
        "and the channel at the end: {stderr}"
    );
    assert!(
        !stdout.contains("component '"),
        "announcements are progress, and must not reach stdout: {stdout}"
    );
}

/// An install says what it is installing, and how fresh the data behind it is, before it starts.
#[test]
fn integration_reporting_an_install_names_its_channel_up_front() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_header");
    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");

    let output = run(&test_env, &fixture.manifest_uri, &["install", "stable"]);
    let (stdout, stderr) = streams(&output);
    assert!(output.status.success(), "install must succeed: {stderr}");

    assert!(
        stderr.contains("syncing channel updates from upstream"),
        "the sync must be announced: {stderr}"
    );
    assert!(
        stderr.contains("upstream last updated on"),
        "and how fresh the manifest behind it is: {stderr}"
    );
    assert!(
        stderr.contains("installing mainnet (0.15.0)"),
        "the install line must name the network and what it resolved to: {stderr}"
    );
    assert!(!stdout.contains("syncing"), "the header is progress, not a result: {stdout}");

    // Ordering is the point of a header: sync, then what resolved, then the work.
    let sync = stderr.find("syncing channel updates").expect("the sync must be announced");
    let installing = stderr.find("installing mainnet").expect("the install line must be present");
    let first_component = stderr.find("component '").expect("components must be announced");
    assert!(sync < installing, "the sync precedes the resolution it enables: {stderr}");
    assert!(installing < first_component, "and the header precedes the work: {stderr}");
}

/// A component built from source says so, and inline at its own position -- unlike a package
/// extraction, which is batched to the end of the install.
#[test]
fn integration_reporting_a_source_build_is_announced_in_place() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_build");
    let fixture = common::harness::OfflineFixture::new(test_env.tmp_dir.path())
        .with_channel("0.15.0")
        .with_cargo_component("prover")
        .build();

    let output = run(&test_env, &fixture.manifest_uri, &["install", "stable"]);
    let (stdout, stderr) = streams(&output);
    assert!(output.status.success(), "install must succeed: {stderr}");

    assert!(
        stderr.contains("building component 'prover' from source"),
        "a build must be announced as a build: {stderr}"
    );
    assert!(
        !stdout.contains("building component"),
        "which is progress, and does not belong on stdout: {stdout}"
    );

    // The fixture's artifacts are `file://`, so its prebuilt components are copied rather than
    // downloaded. Either way they are the cheap steps the build sits among.
    let build = stderr.find("building component").expect("the build must be announced");
    let last_transfer = stderr
        .rfind("copying component")
        .expect("the prebuilt components must be announced");
    assert!(
        build < last_transfer,
        "an inline build is not the last thing to happen: {stderr}"
    );
}

/// The work ahead is described before it starts, and each announcement says where in it we are.
#[test]
fn integration_reporting_states_the_work_ahead_and_counts_through_it() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_progress");
    let fixture = common::harness::OfflineFixture::new(test_env.tmp_dir.path())
        .with_channel("0.15.0")
        .with_cargo_component("prover")
        .build();

    let output = run(&test_env, &fixture.manifest_uri, &["install", "stable"]);
    let (_, stderr) = streams(&output);
    assert!(output.status.success(), "install must succeed: {stderr}");

    assert!(
        stderr.contains("3 steps: 2 copies, 1 source build"),
        "the summary must break the work down by kind: {stderr}"
    );

    // The counter must reach its total.
    for position in ["[1/3]", "[2/3]", "[3/3]"] {
        assert!(
            stderr.contains(position),
            "every position must appear, missing {position}: {stderr}"
        );
    }

    let summary = stderr.find("3 steps:").expect("the summary must be present");
    let first = stderr.find("[1/3]").expect("the first step must be numbered");
    assert!(summary < first, "the summary must come before the work: {stderr}");
}

/// `--quiet` suppresses the announcements without suppressing the command's result.
#[test]
fn integration_reporting_quiet_suppresses_announcements_but_not_results() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_quiet");
    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");

    let installed = run(&test_env, &fixture.manifest_uri, &["install", "stable", "-q"]);
    let (_, stderr) = streams(&installed);
    assert!(installed.status.success(), "install must succeed: {stderr}");
    assert!(
        !stderr.contains("component '"),
        "--quiet must not announce components: {stderr}"
    );
    assert!(
        !stderr.contains("installed channel"),
        "nor the channel it finished installing: {stderr}"
    );
    assert!(!stderr.contains("syncing channel updates"), "nor the header: {stderr}");

    // The result of a command is not progress, so quiet has no bearing on it.
    let shown = run(&test_env, &fixture.manifest_uri, &["show", "active-toolchain", "-q"]);
    let (stdout, stderr) = streams(&shown);
    assert!(shown.status.success(), "show must succeed: {stderr}");
    // The active channel is the network name here, which is what `show` is being asked for.
    assert!(stdout.contains("mainnet"), "the result belongs on stdout regardless: {stdout}");
}

/// The trace tier emits low-level diagnostic details, which the levels below it never mention.
#[test]
fn integration_reporting_trace_emits_diagnostic_details() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_trace");
    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");

    let output = run(&test_env, &fixture.manifest_uri, &["install", "stable", "--verbose=trace"]);
    let (stdout, stderr) = streams(&output);
    assert!(output.status.success(), "install must succeed: {stderr}");
    assert!(stderr.contains("trace:"), "the trace tier must trace: {stderr}");
    assert!(!stdout.contains("trace:"), "tracing is not a result: {stdout}");
}

/// A failing build's compiler errors reach the user.
#[test]
fn integration_reporting_a_failing_build_shows_its_compiler_errors() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_build_failure");
    let fixture = common::harness::OfflineFixture::new(test_env.tmp_dir.path())
        .with_channel("0.15.0")
        .with_cargo_component("prover")
        .build();

    std::fs::write(
        fixture.dir.join("prover-source").join("src").join("main.rs"),
        "compile_error!(\"fixture build failure\");\nfn main() {}\n",
    )
    .expect("failed to break the fixture crate");

    let output = run(&test_env, &fixture.manifest_uri, &["install", "stable"]);
    let (_, stderr) = streams(&output);
    assert!(!output.status.success(), "the install must fail with the build");
    assert!(
        stderr.contains("fixture build failure"),
        "the compiler's error must reach stderr: {stderr}"
    );
}

/// Automatic color follows the destination stream rather than stdout globally.
#[cfg(unix)]
#[test]
fn integration_reporting_auto_color_is_stream_aware() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_reporting_stream_color");
    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (master, slave) = open_pty().expect("failed to open a pseudo-terminal");

    let child = midenup_command(env!("CARGO_BIN_EXE_midenup"), &test_env, &fixture.manifest_uri)
        .args(["list", "--color=auto"])
        .env_remove("CLICOLOR")
        .env_remove("CLICOLOR_FORCE")
        .env_remove("NO_COLOR")
        .stdout(Stdio::piped())
        .stderr(Stdio::from(File::from(slave)))
        .spawn()
        .expect("failed to launch stream-aware color check");
    let (reader, stderr) = read_pty(master);
    let output = child.wait_with_output().expect("failed to wait for color check");
    reader.join().expect("PTY reader panicked");
    let stderr = stderr.try_iter().flatten().collect::<Vec<_>>();

    assert!(output.status.success(), "list failed: {}", String::from_utf8_lossy(&stderr));
    assert!(
        !output.stdout.windows(2).any(|window| window == b"\x1b["),
        "piped stdout contained ANSI escapes: {:?}",
        output.stdout
    );
    assert!(
        stderr.windows(2).any(|window| window == b"\x1b["),
        "terminal stderr did not contain ANSI escapes: {:?}",
        stderr
    );
}

/// Runs only when one of the PTY regression tests launches this test binary recursively.
#[cfg(unix)]
#[test]
fn live_child_reporting_helper() {
    let Ok(mode) = std::env::var("MIDENUP_TEST_LIVE_CHILD") else {
        return;
    };

    midenup::report::set(
        midenup::report::Verbosity::Info,
        midenup::report::ProgressStyle::Pretty,
        midenup::report::ColorChoice::False,
    );
    assert!(midenup::report::activity_is_live(), "the helper's stderr must be a terminal");

    let script = match mode.as_str() {
        // The invalid byte proves the forwarding path is byte-oriented. Waiting for the outer
        // test's release marker makes it observable whether PROMPT reached the terminal before a
        // newline or process exit without relying on scheduler timing.
        "prompt" => {
            "printf 'PROMPT>' >&2; printf '\\377' >&2; while [ ! -e \"$MIDENUP_TEST_RELEASE\" ]; \
             do sleep 0.01; done; printf 'DONE\\n' >&2"
        },
        // This is larger than the bounded reader channel, so the post-exit path must continue
        // draining rather than retaining only the first channelful of a direct child's tail.
        "tail" => {
            "i=0; while [ \"$i\" -lt 16384 ]; do printf '0123456789abcdef' >&2; i=$((i + 1)); \
             done; printf 'END\\n' >&2"
        },
        // The shell exits immediately, but its background child retains the stderr pipe.
        "descendant" => "sleep 5 & printf 'TAIL' >&2",
        other => panic!("unknown live-child helper mode '{other}'"),
    };
    let mut command = Command::new("/bin/sh");
    command.args(["-c", script]);
    let status = midenup::install::run_reporting_progress(&mut command, "child")
        .expect("failed to run reporting helper child");
    assert!(status.success(), "reporting helper child failed with {status}");
}

#[cfg(unix)]
#[test]
fn integration_reporting_live_child_output_is_raw_and_prompt() {
    let signals = tempdir::TempDir::new("midenup-reporting-prompt").unwrap();
    let release = signals.path().join("release");
    let (mut child, master) = spawn_live_child_helper("prompt", Some(&release));
    let (reader, output) = read_pty(master);

    let prompt = b"PROMPT>\xff";
    let mut transcript = Vec::new();
    let prompt_deadline = Instant::now() + Duration::from_secs(5);
    while !transcript.windows(prompt.len()).any(|window| window == prompt) {
        let remaining = prompt_deadline.saturating_duration_since(Instant::now());
        assert!(!remaining.is_zero(), "unterminated prompt was not forwarded: {transcript:?}");
        transcript.extend(
            output
                .recv_timeout(remaining)
                .expect("PTY closed before the unterminated prompt was forwarded"),
        );
    }

    // Once a partial child line owns the terminal, the live activity must not erase it.
    let prompt_at = transcript.windows(prompt.len()).position(|window| window == prompt).unwrap();
    let quiet_deadline = Instant::now() + Duration::from_millis(300);
    while let Ok(bytes) =
        output.recv_timeout(quiet_deadline.saturating_duration_since(Instant::now()))
    {
        transcript.extend(bytes);
        if Instant::now() >= quiet_deadline {
            break;
        }
    }
    assert!(
        !transcript[prompt_at + prompt.len()..]
            .windows(4)
            .any(|window| window == b"\x1b[2K"),
        "the activity display redrew over a partial child line: {transcript:?}"
    );

    std::fs::write(&release, b"go").expect("failed to release the prompt helper");
    wait_for_test_child(&mut child, Duration::from_secs(5));
    reader.join().expect("PTY reader panicked");
    transcript.extend(output.try_iter().flatten());
    assert!(
        transcript.windows(4).any(|window| window == b"DONE"),
        "missing tail: {transcript:?}"
    );
}

#[cfg(unix)]
#[test]
fn integration_reporting_drains_a_large_direct_child_tail() {
    let (mut child, master) = spawn_live_child_helper("tail", None);
    let (reader, output) = read_pty(master);

    wait_for_test_child(&mut child, Duration::from_secs(10));
    reader.join().expect("PTY reader panicked");
    let transcript = output.try_iter().flatten().collect::<Vec<_>>();
    assert!(
        transcript.windows(3).any(|window| window == b"END"),
        "large child tail was truncated ({} bytes captured)",
        transcript.len()
    );
}

#[cfg(unix)]
fn read_pty(master: OwnedFd) -> (std::thread::JoinHandle<()>, mpsc::Receiver<Vec<u8>>) {
    let (sender, output) = mpsc::channel();
    let reader = std::thread::spawn(move || {
        let mut master = File::from(master);
        let mut buffer = [0u8; 4096];
        loop {
            match master.read(&mut buffer) {
                Ok(0) | Err(_) => break,
                Ok(read) => {
                    if sender.send(buffer[..read].to_vec()).is_err() {
                        break;
                    }
                },
            }
        }
    });
    (reader, output)
}

#[cfg(unix)]
#[test]
fn integration_reporting_does_not_wait_for_a_descendants_stderr_pipe() {
    let started = Instant::now();
    let (mut child, _master) = spawn_live_child_helper("descendant", None);
    wait_for_test_child(&mut child, Duration::from_secs(2));
    assert!(
        started.elapsed() < Duration::from_secs(2),
        "reporting waited for a background descendant to close stderr"
    );
}

#[cfg(unix)]
fn spawn_live_child_helper(
    mode: &str,
    release: Option<&std::path::Path>,
) -> (std::process::Child, OwnedFd) {
    let (master, slave) = open_pty().expect("failed to open a pseudo-terminal");
    let mut command = Command::new(std::env::current_exe().expect("test executable has no path"));
    command
        .args(["--exact", "live_child_reporting_helper", "--nocapture"])
        .env("MIDENUP_TEST_LIVE_CHILD", mode)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::from(File::from(slave)));
    if let Some(release) = release {
        command.env("MIDENUP_TEST_RELEASE", release);
    }
    let child = command.spawn().expect("failed to launch live reporting helper");
    (child, master)
}

#[cfg(unix)]
fn open_pty() -> std::io::Result<(OwnedFd, OwnedFd)> {
    let mut master = -1;
    let mut slave = -1;
    // SAFETY: openpty initializes both descriptors on success. The optional terminal metadata and
    // window-size pointers may be null, and each initialized descriptor is immediately owned.
    if unsafe {
        libc::openpty(
            &mut master,
            &mut slave,
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
        )
    } == -1
    {
        return Err(std::io::Error::last_os_error());
    }
    // SAFETY: the successful openpty call returned fresh, valid descriptors owned by this process.
    Ok(unsafe { (OwnedFd::from_raw_fd(master), OwnedFd::from_raw_fd(slave)) })
}

#[cfg(unix)]
fn wait_for_test_child(child: &mut std::process::Child, timeout: Duration) {
    let deadline = Instant::now() + timeout;
    loop {
        if let Some(status) = child.try_wait().expect("failed to inspect helper status") {
            assert!(status.success(), "live reporting helper failed with {status}");
            return;
        }
        if Instant::now() >= deadline {
            let _ = child.kill();
            let _ = child.wait();
            panic!("live reporting helper exceeded {timeout:?}");
        }
        std::thread::sleep(Duration::from_millis(10));
    }
}