headwater-cli 0.3.0

The headwater binary, and what CI runs. headwater --help is the verb list
Documentation
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
// SPDX-License-Identifier: Apache-2.0
//! What the help says, held against the table it is written out of.
//!
//! [`super`] holds the *shape* of the command tree — every command line the
//! parser answers to, against `headwater_verbs::VERBS`, in both directions.
//! Nothing held the *words*. A verb could ship with nothing against its name
//! and a flag could ship undescribed, and every case in this crate stayed
//! green: [#321](https://github.com/headwater-ai/headwater/issues/321) clause 6
//! names five flags that did exactly that — `--facet`, `--tier`, `--arm`,
//! `--category` and `--seed` — and the verification of the parser migration
//! recorded that the walk of the command tree can never catch a flag, because
//! it compares command lines and a flag is not one.
//!
//! # The three routes are one command, so they cannot drift apart
//!
//! `headwater help check`, `headwater check --help` and `headwater check -h`
//! all render the same node of the tree `headwater_cli::command` builds. No
//! `long_about` and no `long_help` exists anywhere in that tree, which is what
//! makes the two spellings of the flag render the same text, and
//! `headwater help` prints through `Command::print_help` for the same reason.
//! The case below asserts the three are byte-identical rather than merely all
//! non-empty, because "each carries something" is satisfied by three different
//! answers to one question.
//!
//! # Why the summaries are asserted against `VERBS` rather than against a string
//!
//! A case that pinned the text would be a second copy of it, and a second copy
//! of the verb list is what #257 was filed about. Every assertion here reads
//! the expected text out of `headwater_verbs::VERBS` at run time, so changing a
//! summary there moves the help and this file follows without an edit — and
//! moving a summary *into the parser* breaks it, because the table would then
//! carry a string the rendered help does not.

use headwater_cli::{command, GLOBALS};
use std::path::{Path, PathBuf};
use std::process::Command as Process;

/// One invocation from a directory that is not a corpus, with the streams apart.
struct Ran {
    code: Option<i32>,
    out: String,
    err: String,
}

/// A directory under the temporary directory that is removed when this value
/// is dropped, so a case that fails an assertion leaves nothing behind (#1158).
struct Scratch(PathBuf);

impl Drop for Scratch {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

impl std::ops::Deref for Scratch {
    type Target = Path;
    fn deref(&self) -> &Path {
        &self.0
    }
}

impl AsRef<Path> for Scratch {
    fn as_ref(&self) -> &Path {
        &self.0
    }
}

impl AsRef<std::ffi::OsStr> for Scratch {
    fn as_ref(&self) -> &std::ffi::OsStr {
        self.0.as_os_str()
    }
}

/// A case that panics while it holds a scratch directory still removes it.
///
/// This is the half of #1158 that a passing suite cannot show: the CI step
/// that counts what the suite left reads only runs that passed. A trailing
/// `remove_dir_all` is skipped by a failed assertion, and the guard is not.
#[test]
fn a_scratch_directory_is_removed_when_its_case_panics() {
    let at = std::env::temp_dir().join(format!("headwater-cli-help-{}-panics", std::process::id()));
    let held = at.clone();
    let unwound = std::panic::catch_unwind(move || {
        let scratch = Scratch(held);
        std::fs::create_dir_all(&scratch).expect("the directory is there");
        assert!(scratch.is_dir(), "the directory was made");
        panic!("a failed assertion in the case");
    });
    assert!(unwound.is_err(), "the closure panicked");
    assert!(!at.exists(), "{} survived the panic", at.display());
}

fn ran(label: &str, arguments: &[&str]) -> Ran {
    let at = Scratch(
        std::env::temp_dir().join(format!("headwater-cli-help-{}-{label}", std::process::id())),
    );
    let _ = std::fs::remove_dir_all(&at);
    std::fs::create_dir_all(&at).expect("the directory is there");
    let output = Process::new(env!("CARGO_BIN_EXE_headwater"))
        .args(arguments)
        .current_dir(&at)
        .output()
        .expect("the binary runs");
    Ran {
        code: output.status.code(),
        out: String::from_utf8_lossy(&output.stdout).into_owned(),
        err: String::from_utf8_lossy(&output.stderr).into_owned(),
    }
}

/// Every run of whitespace as one space, so an assertion survives a rewrap.
///
/// `clap` does no wrapping in this build — `StyledStr::wrap` is compiled out
/// without the `wrap_help` feature, which this workspace does not take — so
/// help text is one line however long it is today. #321 clause 12 changes that,
/// and a case that compared raw bytes would go red on the piece that does it
/// for no reason a reader would recognize.
fn flat(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

/// The help of one node of the command tree, rendered in process.
fn help_of(words: &[&str]) -> String {
    let mut root = command();
    root.build();
    let mut cursor = &mut root;
    for word in words {
        cursor = cursor
            .find_subcommand_mut(word)
            .unwrap_or_else(|| panic!("the parser answers to `{}`", words.join(" ")));
    }
    cursor.render_help().to_string()
}

/// Every argument the parser admits says what it is.
///
/// # This is the guard clause 7 cannot be
///
/// `tests/verbs.rs` walks the same tree and compares **command lines**. A flag
/// is not a command line, so a flag added to any verb — global or not,
/// described or not — is invisible there. That is how `--facet`, `--tier`,
/// `--arm`, `--category` and `--seed` reached a shipped binary with no
/// description anywhere: the synopsis named them and the flag block did not,
/// and nothing read the two against each other.
///
/// It is asserted over **every** argument rather than every flag, because a
/// positional a caller has to guess at is the same defect one level along:
/// `headwater taxonomy vendor <dir>` says nothing about what the directory is
/// unless somebody writes it down.
///
/// The failure names every offender at once. A case that returned at the first
/// one would make a reader run it as many times as there are undescribed flags.
#[test]
fn every_argument_the_parser_admits_says_what_it_is() {
    fn walk(prefix: &str, command: &clap::Command, silent: &mut Vec<String>) {
        for argument in command.get_arguments() {
            let says = argument
                .get_help()
                .map(ToString::to_string)
                .unwrap_or_default();
            if says.trim().is_empty() {
                silent.push(format!("{prefix}: {}", argument.get_id()));
            }
        }
        for word in command.get_subcommands() {
            walk(&format!("{prefix} {}", word.get_name()), word, silent);
        }
    }

    let mut root = command();
    root.build();
    let mut silent = Vec::new();
    walk(headwater_verbs::BINARY, &root, &mut silent);
    assert!(
        silent.is_empty(),
        "{} arguments of this parser carry no help text, and a caller who meets one has nothing \
         to read: {silent:#?}",
        silent.len()
    );
}

/// The three routes clause 4 names reach one text, and it is the table's.
///
/// Byte-identity is the assertion rather than three separate containments,
/// because three routes that each carry *something* is the state a reader
/// cannot rely on: the point of the clause is that a caller who reaches for the
/// habit they already have arrives at the same page.
#[test]
fn the_long_description_of_a_verb_is_reachable_three_ways() {
    let verb = headwater_verbs::parse("check").expect("`check` is a verb of this binary");
    let routes = [
        ("flag-long", vec!["check", "--help"]),
        ("flag-short", vec!["check", "-h"]),
        ("verb", vec!["help", "check"]),
    ];
    let mut rendered: Vec<(String, String)> = Vec::new();
    for (label, arguments) in routes {
        let ran = ran(label, &arguments);
        assert_eq!(
            ran.code,
            Some(0),
            "`headwater {}` is a question rather than a mistake:\n{}{}",
            arguments.join(" "),
            ran.out,
            ran.err
        );
        assert_eq!(
            ran.err,
            "",
            "`headwater {}` writes nothing to standard error",
            arguments.join(" ")
        );
        assert!(
            flat(&ran.out).contains(&flat(verb.description)),
            "`headwater {}` carries the description the dispatch table holds for `check`:\n{}",
            arguments.join(" "),
            ran.out
        );
        rendered.push((arguments.join(" "), ran.out));
    }
    let (first, text) = &rendered[0];
    for (other, also) in &rendered[1..] {
        assert_eq!(
            text, also,
            "`headwater {first}` and `headwater {other}` print the same bytes"
        );
    }
}

/// The first screen is grouped, and every group and every summary comes off the
/// dispatch table.
///
/// A group declared in the parser would be the fifth hand-kept copy of the verb
/// list that [#257](https://github.com/headwater-ai/headwater/issues/257) was
/// filed about, so what this asserts is the direction of the read: change
/// `headwater_verbs::VERBS` and the screen moves with no edit to
/// `engine/crates/cli/src/lib.rs`. Move a summary into the parser and this case
/// fails, because the table then carries a line the screen does not.
#[test]
fn the_first_screen_prints_the_group_and_the_summary_the_table_carries() {
    let screen = flat(&help_of(&[]));
    for group in headwater_verbs::groups() {
        assert!(
            screen.contains(&format!("{group}:")),
            "the first screen carries the heading `{group}:`"
        );
    }
    for verb in headwater_verbs::VERBS {
        assert!(
            screen.contains(&flat(verb.summary)),
            "the first screen carries `{}`'s summary from the dispatch table",
            verb.name
        );
        assert!(
            screen.contains(&format!(" {} ", verb.name)),
            "the first screen names `{}`",
            verb.name
        );
    }
}

/// Every verb and every second word prints the description the table carries.
///
/// The case above holds the first screen and this one holds what is behind it,
/// so a verb whose summary is on the screen and whose long form is empty is
/// caught here rather than by a reader.
#[test]
fn the_long_description_of_every_command_line_is_what_the_table_carries() {
    for verb in headwater_verbs::VERBS {
        let rendered = flat(&help_of(&[verb.name]));
        assert!(
            rendered.contains(&flat(verb.description)),
            "`headwater {} --help` carries the description the table holds:\n{rendered}",
            verb.name
        );
        for word in verb.words {
            let rendered = flat(&help_of(&[verb.name, word.name]));
            assert!(
                rendered.contains(&flat(word.description)),
                "`headwater {} {} --help` carries the description the table holds:\n{rendered}",
                verb.name,
                word.name
            );
        }
    }
}

/// Every global flag gets one line on the first screen, and the table covers
/// every one of them.
///
/// `HW-DR-0042` holds the first screen to one line per entry. The ceiling in
/// [`the_first_screen_holds_the_height_the_ruling_names`] is the consequence of
/// that bar; this is the bar itself, and it is the half that catches a flag
/// added later. A global flag with no entry in `headwater_cli::GLOBALS` fails
/// the first assertion, and an entry whose summary does not fit the column folds
/// onto a second line and fails the second.
#[test]
fn every_global_flag_is_one_line_on_the_first_screen() {
    let mut root = command();
    root.build();

    // The table covers every argument the root command carries. The root's own
    // arguments are the four globals and `clap`'s help flag; every argument of
    // a verb is declared on that verb's subcommand and is not reached here.
    for argument in root.get_arguments() {
        let id = argument.get_id().as_str();
        assert!(
            GLOBALS.iter().any(|one| one.covers(id)),
            "`{id}` is printed on the first screen and `GLOBALS` does not name it, so the screen \
             would carry it with no summary or with its whole description"
        );
    }

    // Each one occupies exactly one line of the rendered screen.
    let screen = help_of(&[]);
    let block: Vec<&str> = screen
        .lines()
        .skip_while(|line| *line != "Global flags:")
        .skip(1)
        .take_while(|line| !line.trim().is_empty())
        .collect();
    assert_eq!(
        block.len(),
        GLOBALS.len(),
        "the first screen gives {} lines to {} global flags, and HW-DR-0042 holds it to one line \
         each:\n{}",
        block.len(),
        GLOBALS.len(),
        block.join("\n")
    );
    for one in GLOBALS {
        assert!(
            block.iter().any(|line| line.contains(one.summary)),
            "the first screen carries `{}`'s summary on one line of its own",
            one.name
        );
    }
}

/// Every global flag's whole description is on every verb page, which is where
/// the first screen stopped printing it.
///
/// The summary is short because the description is somewhere else, so this is
/// the case that says where. It reads `check` because clause 4 of
/// [#342](https://github.com/headwater-ai/headwater/issues/342) names that page,
/// and it asserts the description the table declares rather than a string of its
/// own, so a description edited at the flag moves the page and this case follows.
#[test]
fn the_whole_of_every_global_flag_is_on_a_verb_page() {
    let page = flat(&help_of(&["check"]));
    for one in GLOBALS {
        assert!(
            page.contains(&flat(one.description)),
            "`headwater check --help` carries the whole of `{}`, which the first screen no longer \
             prints",
            one.name
        );
    }
}

/// The first screen holds the height `HW-DR-0042` names.
///
/// The ruling is
/// `docs/decisions/0042-q42-what-one-screen-means-for-the-first-help-screen.md`
/// and this case is the consequence of it, not the argument for it. It retires
/// "one screen" as a claim about a terminal, states the bar as one line per
/// entry, and names 64 as the ceiling that follows, revised from 60 when
/// [#929](https://github.com/headwater-ai/headwater/issues/929) spent the
/// headroom the ruling had priced. Read it there rather than here: an argument
/// restated at the case is an argument that goes stale where nobody is
/// looking.
///
/// [`every_global_flag_is_one_line_on_the_first_screen`] is the bar itself, and
/// it is the half that catches a flag added later. This is the height, and the
/// number is a ceiling rather than a pin: a pin goes red on every verb added and
/// teaches a reader to re-bless it, and a ceiling goes red only when the screen
/// stops being a list.
#[test]
fn the_first_screen_holds_the_height_the_ruling_names() {
    let screen = help_of(&[]);
    let lines = screen.lines().count();
    assert!(
        lines <= 64,
        "`headwater --help` is {lines} lines, and HW-DR-0042 holds the first screen to 64:\n{screen}"
    );
    assert!(
        lines > 20,
        "`headwater --help` is {lines} lines, which is too few to carry {} verbs with a line each",
        headwater_verbs::VERBS.len()
    );
}

/// Neither spelling of either global answer writes to standard error, at either
/// level of the tree.
///
/// The verification of the parser migration found the existing case loops over
/// the root alone, so a regression that moved `headwater check --help` to a
/// non-zero status would pass the suite. This closes it at both levels and for
/// both spellings.
#[test]
fn every_global_answer_exits_zero_on_standard_output_alone() {
    for (label, arguments) in [
        ("root-long", vec!["--help"]),
        ("root-short", vec!["-h"]),
        ("verb-long", vec!["check", "--help"]),
        ("verb-short", vec!["check", "-h"]),
        ("word-long", vec!["sweep", "plan", "--help"]),
        ("version-long", vec!["--version"]),
        ("version-short", vec!["-V"]),
        ("version-after-verb", vec!["check", "--version"]),
        ("help-verb", vec!["help"]),
        ("help-verb-word", vec!["help", "taxonomy", "diff"]),
    ] {
        let ran = ran(label, &arguments);
        let named = arguments.join(" ");
        assert_eq!(
            ran.code,
            Some(0),
            "`headwater {named}`:\n{}{}",
            ran.out,
            ran.err
        );
        assert_eq!(
            ran.err, "",
            "`headwater {named}` writes nothing to standard error"
        );
        assert!(
            !ran.out.is_empty(),
            "`headwater {named}` writes an answer to standard output"
        );
    }
}