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
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! The host that talks to the real terminal and the real process.
use std::io::{IsTerminal, Write, stderr, stdout};
use cargo_gamma_lib::Host;
/// Host that talks to the real terminal and the real process.
#[derive(Debug, Clone, Copy, Default)]
pub struct RealHost;
#[cfg_attr(coverage_nightly, coverage(off))]
impl Host for RealHost {
fn output(&mut self) -> impl Write {
stdout()
}
fn error(&mut self) -> impl Write {
stderr()
}
// #[gamma::skip(fn_value.bool_false, fn_value.bool_true, reason = "the answer belongs to the invoking terminal and tests validly run both attached and detached")]
fn is_terminal(&self) -> bool {
stderr().is_terminal()
}
// #[gamma::skip(fn_value.none, reason = "terminal dimensions are optional and None is already the valid fallback when the host cannot report them")]
fn terminal_width(&self) -> Option<u16> {
terminal_size::terminal_size().map(|(width, _)| width.0)
}
/// This host is the real process, so `current_exe` is cargo-gamma and replacing it is safe.
fn may_replace_process(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use std::env;
use std::process::Command;
use super::*;
/// A variable the parent gives the child, whose exact value `RealHost` has to return.
const PRESENT_VAR: &str = "GAMMA_REAL_HOST_PRESENT";
/// What the parent sets it to, chosen so that no ambient environment could supply it.
const PRESENT_VALUE: &str = "a value only this test chose";
/// A variable the parent removes from the child, whatever the launching environment held.
const ABSENT_VAR: &str = "GAMMA_REAL_HOST_ABSENT";
/// The real host is driven through the trait, because every other test in the workspace is not.
///
/// The fakes in `cargo_gamma_lib::testing` are only worth as much as their agreement with this
/// implementation, and that agreement is otherwise assumed: `RealHost` is excluded from coverage
/// and is constructed in exactly one place, `main`, which no test calls. Nothing here asserts a
/// particular terminal — the suite runs both on a tty and in CI — but it does assert the parts
/// of the contract that hold either way, and it makes the type reachable so that a signature
/// that stopped satisfying `Host` fails a test rather than only the release build.
#[test]
fn the_real_host_answers_the_same_contract_the_fakes_stand_in_for() {
let mut host = RealHost;
// Both streams accept a write. An empty one is used because a test that printed would be
// indistinguishable from a test that failed noisily, and the write path is what is under
// test, not the bytes.
host.output().write_all(b"").expect("stdout accepts a write");
host.error().write_all(b"").expect("stderr accepts a write");
// A width is a terminal's property, so claiming one without being a terminal would have the
// console wrap its progress to a width nothing has. The converse is allowed: a terminal
// whose size cannot be interrogated is an ordinary state, and the console falls back.
assert!(
host.is_terminal() || host.terminal_width().is_none(),
"a host that is not a terminal must not report a terminal width"
);
// This is the one answer that differs from every fake, and it is the answer that decides
// whether the tool may replace its own process. It is true here precisely because this host
// is the real process; a fake saying so would relaunch the test harness.
assert!(host.may_replace_process());
}
/// The real host reads the real environment rather than overriding `env`.
///
/// Driven through a child process this test launches itself, because the two states the
/// host has to tell apart — set, and not set — are properties of a process environment, and
/// this process's own is whatever the developer or the CI runner happened to export. Asserting
/// against it reads correct code as broken in a sanitized environment, and reads broken code as
/// correct whenever the ambient process happens to agree. Writing this process's environment to
/// arrange the two states is not an option either: `setenv` races every other thread's read,
/// which is why nothing in this workspace does it.
///
/// So the states are arranged on the child's `Command` — one variable given a value only this
/// test chose, one removed outright — and the child reports what the host returned for each.
#[test]
fn the_real_host_reads_the_process_environment() {
let executable = env::current_exe().expect("the test binary knows its own path");
// `--exact` confines the child to the reporting helper, and `--nocapture` exposes its
// printed protocol to `Command::output()`.
let output = Command::new(&executable)
.args(["--exact", "real_host::tests::the_child_reports_what_the_host_read", "--nocapture"])
.env(PRESENT_VAR, PRESENT_VALUE)
.env_remove(ABSENT_VAR)
.output()
.expect("the child runs");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let text = String::from_utf8_lossy(&output.stdout);
// The value is compared, not merely its presence: a host that answered `Some` with the
// name, the empty string, or another variable's value would satisfy a presence check.
assert!(text.contains(&format!("present={:?}", Some(PRESENT_VALUE))), "{text}");
assert!(text.contains("absent=None"), "{text}");
}
/// Reports what [`RealHost`] read for each variable the test above arranged.
///
/// Only meaningful when launched by that test; harmless on its own, where it prints two
/// absences and asserts nothing.
#[test]
fn the_child_reports_what_the_host_read() {
let host = RealHost;
println!("present={:?} absent={:?}", host.env(PRESENT_VAR), host.env(ABSENT_VAR));
}
}