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
/// Test discovery subcommands for systematic test fixing
#[derive(Debug, Clone, Subcommand)]
pub enum TestDiscoveryCommands {
/// Discover all test failures in workspace
#[command(visible_aliases = &["d"])]
Run {
/// Project path (defaults to current directory)
#[arg(short = 'p', long = "path", default_value = ".")]
path: PathBuf,
/// Output file for failures JSON
#[arg(short = 'o', long = "output", default_value = "test-failures.json")]
output: PathBuf,
/// Use cargo nextest (faster, parallel); `--use-nextest false` runs `cargo test`
///
/// `#[arg(long, default_value = "true")]` derived `ArgAction::SetTrue`,
/// which ignores the default and can only ever set the flag to true:
/// the value was true whether or not the flag was passed, `--use-nextest
/// false` was rejected as an unexpected argument, and the `cargo test`
/// branch in `handle_discovery_run` was unreachable. `ArgAction::Set`
/// with an optional value keeps nextest the default while making the
/// flag actually settable.
#[arg(
long,
action = clap::ArgAction::Set,
num_args = 0..=1,
default_value_t = true,
default_missing_value = "true"
)]
use_nextest: bool,
/// Maximum test timeout in seconds
#[arg(long, default_value = "600")]
timeout: u64,
},
/// Categorize test failures by root cause
#[command(visible_aliases = &["cat"])]
Categorize {
/// Input failures JSON from discovery
#[arg(short = 'i', long = "input")]
input: PathBuf,
/// Output categories JSON
#[arg(short = 'o', long = "output", default_value = "test-categories.json")]
output: PathBuf,
},
/// Mark tests as #[ignore] with reasons
#[command(visible_aliases = &["m"])]
Mark {
/// Input categories JSON
#[arg(short = 'i', long = "input")]
input: PathBuf,
/// Actually apply changes (default: dry-run)
#[arg(long)]
apply: bool,
},
/// Verify all tests pass after marking
#[command(visible_aliases = &["v"])]
Verify {
/// Project path
#[arg(short = 'p', long = "path", default_value = ".")]
path: PathBuf,
},
/// Create GitHub issues from categorized test failures (Phase 5)
#[command(visible_aliases = &["tickets", "t"])]
CreateTickets {
/// Input categories JSON
#[arg(short = 'i', long = "input")]
input: PathBuf,
/// Actually create GitHub issues (default: dry-run)
#[arg(long)]
create: bool,
/// Output tickets summary
#[arg(short = 'o', long = "output")]
output: Option<PathBuf>,
/// GitHub repository (owner/repo format)
#[arg(long)]
repo: Option<String>,
/// Labels to add to created issues
#[arg(long, value_delimiter = ',')]
labels: Option<Vec<String>>,
},
/// Resolve test file paths from test names
#[command(visible_aliases = &["resolve", "r"])]
ResolvePaths {
/// Input failures JSON
#[arg(short = 'i', long = "input")]
input: PathBuf,
/// Output with resolved paths
#[arg(short = 'o', long = "output")]
output: PathBuf,
/// Project path to search for test files
#[arg(short = 'p', long = "path", default_value = ".")]
path: PathBuf,
},
}
/// Test discovery output formats
#[derive(Debug, Clone, clap::ValueEnum, PartialEq)]
pub enum TestDiscoveryFormat {
Json,
Markdown,
Text,
}
#[cfg(test)]
mod use_nextest_flag_tests {
//! `--use-nextest` must be a real switch: it used to derive
//! `ArgAction::SetTrue`, so the value was true no matter what the user
//! typed and `cargo test` could never run.
use super::TestDiscoveryCommands;
use clap::Parser;
#[derive(Parser, Debug)]
struct Harness {
#[command(subcommand)]
cmd: TestDiscoveryCommands,
}
fn use_nextest_of(args: &[&str]) -> bool {
// 8MB stack on its own thread — clap's generated parser overflows the
// default 2MB test stack, the same reason the other clap parsing tests
// in this crate spawn a thread. Inline it aborts the whole test binary
// with SIGABRT under CI's coverage job, which sets no RUST_MIN_STACK.
let argv: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(move || {
match Harness::try_parse_from(&argv)
.expect("parse test-discovery run")
.cmd
{
TestDiscoveryCommands::Run { use_nextest, .. } => use_nextest,
other => panic!("expected `run`, got {other:?}"),
}
})
.expect("spawn clap parse thread")
.join()
.expect("clap parse thread panicked")
}
#[test]
fn use_nextest_defaults_on_but_can_be_switched_off() {
assert!(use_nextest_of(&["td", "run"]), "nextest stays the default");
assert!(use_nextest_of(&["td", "run", "--use-nextest"]));
assert!(
!use_nextest_of(&["td", "run", "--use-nextest", "false"]),
"--use-nextest false must select `cargo test`"
);
}
}