@hasna/terminal 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/dist/cli.js +64 -16
  2. package/package.json +1 -1
  3. package/src/ai.ts +8 -0
  4. package/src/cli.tsx +57 -18
  5. package/src/output-processor.ts +6 -1
  6. package/src/output-store.ts +58 -12
  7. package/src/tool-profiles.ts +139 -0
  8. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  9. package/temp/rtk/.claude/agents/debugger.md +0 -519
  10. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  11. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  12. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  13. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  14. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  15. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  16. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  17. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  18. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  19. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  20. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  21. package/temp/rtk/.claude/skills/performance.md +0 -435
  22. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  23. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  24. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  25. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  26. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  27. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  28. package/temp/rtk/.claude/skills/ship.md +0 -404
  29. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  30. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  31. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  32. package/temp/rtk/.github/workflows/release.yml +0 -343
  33. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  34. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  35. package/temp/rtk/.release-please-manifest.json +0 -3
  36. package/temp/rtk/ARCHITECTURE.md +0 -1491
  37. package/temp/rtk/CHANGELOG.md +0 -640
  38. package/temp/rtk/CLAUDE.md +0 -605
  39. package/temp/rtk/CONTRIBUTING.md +0 -199
  40. package/temp/rtk/Cargo.lock +0 -1668
  41. package/temp/rtk/Cargo.toml +0 -64
  42. package/temp/rtk/Formula/rtk.rb +0 -43
  43. package/temp/rtk/INSTALL.md +0 -390
  44. package/temp/rtk/LICENSE +0 -21
  45. package/temp/rtk/README.md +0 -386
  46. package/temp/rtk/README_es.md +0 -159
  47. package/temp/rtk/README_fr.md +0 -197
  48. package/temp/rtk/README_ja.md +0 -159
  49. package/temp/rtk/README_ko.md +0 -159
  50. package/temp/rtk/README_zh.md +0 -167
  51. package/temp/rtk/ROADMAP.md +0 -15
  52. package/temp/rtk/SECURITY.md +0 -217
  53. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  54. package/temp/rtk/build.rs +0 -57
  55. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  56. package/temp/rtk/docs/FEATURES.md +0 -1410
  57. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  58. package/temp/rtk/docs/filter-workflow.md +0 -102
  59. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  60. package/temp/rtk/docs/tracking.md +0 -583
  61. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  62. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  63. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  64. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  65. package/temp/rtk/install.sh +0 -124
  66. package/temp/rtk/release-please-config.json +0 -10
  67. package/temp/rtk/scripts/benchmark.sh +0 -592
  68. package/temp/rtk/scripts/check-installation.sh +0 -162
  69. package/temp/rtk/scripts/install-local.sh +0 -37
  70. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  71. package/temp/rtk/scripts/test-all.sh +0 -561
  72. package/temp/rtk/scripts/test-aristote.sh +0 -227
  73. package/temp/rtk/scripts/test-tracking.sh +0 -79
  74. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  75. package/temp/rtk/scripts/validate-docs.sh +0 -73
  76. package/temp/rtk/src/aws_cmd.rs +0 -880
  77. package/temp/rtk/src/binlog.rs +0 -1645
  78. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  79. package/temp/rtk/src/cc_economics.rs +0 -1157
  80. package/temp/rtk/src/ccusage.rs +0 -340
  81. package/temp/rtk/src/config.rs +0 -187
  82. package/temp/rtk/src/container.rs +0 -855
  83. package/temp/rtk/src/curl_cmd.rs +0 -134
  84. package/temp/rtk/src/deps.rs +0 -268
  85. package/temp/rtk/src/diff_cmd.rs +0 -367
  86. package/temp/rtk/src/discover/mod.rs +0 -274
  87. package/temp/rtk/src/discover/provider.rs +0 -388
  88. package/temp/rtk/src/discover/registry.rs +0 -2022
  89. package/temp/rtk/src/discover/report.rs +0 -202
  90. package/temp/rtk/src/discover/rules.rs +0 -667
  91. package/temp/rtk/src/display_helpers.rs +0 -402
  92. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  93. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  94. package/temp/rtk/src/dotnet_trx.rs +0 -593
  95. package/temp/rtk/src/env_cmd.rs +0 -204
  96. package/temp/rtk/src/filter.rs +0 -462
  97. package/temp/rtk/src/filters/README.md +0 -52
  98. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  99. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  100. package/temp/rtk/src/filters/biome.toml +0 -45
  101. package/temp/rtk/src/filters/brew-install.toml +0 -37
  102. package/temp/rtk/src/filters/composer-install.toml +0 -40
  103. package/temp/rtk/src/filters/df.toml +0 -16
  104. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  105. package/temp/rtk/src/filters/du.toml +0 -16
  106. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  107. package/temp/rtk/src/filters/gcc.toml +0 -49
  108. package/temp/rtk/src/filters/gcloud.toml +0 -22
  109. package/temp/rtk/src/filters/hadolint.toml +0 -24
  110. package/temp/rtk/src/filters/helm.toml +0 -29
  111. package/temp/rtk/src/filters/iptables.toml +0 -27
  112. package/temp/rtk/src/filters/jj.toml +0 -28
  113. package/temp/rtk/src/filters/jq.toml +0 -24
  114. package/temp/rtk/src/filters/make.toml +0 -41
  115. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  116. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  117. package/temp/rtk/src/filters/mix-format.toml +0 -15
  118. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  119. package/temp/rtk/src/filters/oxlint.toml +0 -43
  120. package/temp/rtk/src/filters/ping.toml +0 -63
  121. package/temp/rtk/src/filters/pio-run.toml +0 -40
  122. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  123. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  124. package/temp/rtk/src/filters/ps.toml +0 -16
  125. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  126. package/temp/rtk/src/filters/rsync.toml +0 -48
  127. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  128. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  129. package/temp/rtk/src/filters/skopeo.toml +0 -45
  130. package/temp/rtk/src/filters/sops.toml +0 -16
  131. package/temp/rtk/src/filters/ssh.toml +0 -44
  132. package/temp/rtk/src/filters/stat.toml +0 -34
  133. package/temp/rtk/src/filters/swift-build.toml +0 -41
  134. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  135. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  136. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  137. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  138. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  139. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  140. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  141. package/temp/rtk/src/filters/ty.toml +0 -50
  142. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  143. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  144. package/temp/rtk/src/filters/yamllint.toml +0 -25
  145. package/temp/rtk/src/find_cmd.rs +0 -598
  146. package/temp/rtk/src/format_cmd.rs +0 -386
  147. package/temp/rtk/src/gain.rs +0 -723
  148. package/temp/rtk/src/gh_cmd.rs +0 -1651
  149. package/temp/rtk/src/git.rs +0 -2012
  150. package/temp/rtk/src/go_cmd.rs +0 -592
  151. package/temp/rtk/src/golangci_cmd.rs +0 -254
  152. package/temp/rtk/src/grep_cmd.rs +0 -288
  153. package/temp/rtk/src/gt_cmd.rs +0 -810
  154. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  155. package/temp/rtk/src/hook_check.rs +0 -171
  156. package/temp/rtk/src/init.rs +0 -1859
  157. package/temp/rtk/src/integrity.rs +0 -537
  158. package/temp/rtk/src/json_cmd.rs +0 -231
  159. package/temp/rtk/src/learn/detector.rs +0 -628
  160. package/temp/rtk/src/learn/mod.rs +0 -119
  161. package/temp/rtk/src/learn/report.rs +0 -184
  162. package/temp/rtk/src/lint_cmd.rs +0 -694
  163. package/temp/rtk/src/local_llm.rs +0 -316
  164. package/temp/rtk/src/log_cmd.rs +0 -248
  165. package/temp/rtk/src/ls.rs +0 -324
  166. package/temp/rtk/src/main.rs +0 -2482
  167. package/temp/rtk/src/mypy_cmd.rs +0 -389
  168. package/temp/rtk/src/next_cmd.rs +0 -241
  169. package/temp/rtk/src/npm_cmd.rs +0 -236
  170. package/temp/rtk/src/parser/README.md +0 -267
  171. package/temp/rtk/src/parser/error.rs +0 -46
  172. package/temp/rtk/src/parser/formatter.rs +0 -336
  173. package/temp/rtk/src/parser/mod.rs +0 -311
  174. package/temp/rtk/src/parser/types.rs +0 -119
  175. package/temp/rtk/src/pip_cmd.rs +0 -302
  176. package/temp/rtk/src/playwright_cmd.rs +0 -479
  177. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  178. package/temp/rtk/src/prettier_cmd.rs +0 -221
  179. package/temp/rtk/src/prisma_cmd.rs +0 -482
  180. package/temp/rtk/src/psql_cmd.rs +0 -382
  181. package/temp/rtk/src/pytest_cmd.rs +0 -384
  182. package/temp/rtk/src/read.rs +0 -217
  183. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  184. package/temp/rtk/src/ruff_cmd.rs +0 -402
  185. package/temp/rtk/src/runner.rs +0 -271
  186. package/temp/rtk/src/summary.rs +0 -297
  187. package/temp/rtk/src/tee.rs +0 -405
  188. package/temp/rtk/src/telemetry.rs +0 -248
  189. package/temp/rtk/src/toml_filter.rs +0 -1655
  190. package/temp/rtk/src/tracking.rs +0 -1416
  191. package/temp/rtk/src/tree.rs +0 -209
  192. package/temp/rtk/src/tsc_cmd.rs +0 -259
  193. package/temp/rtk/src/utils.rs +0 -432
  194. package/temp/rtk/src/verify_cmd.rs +0 -47
  195. package/temp/rtk/src/vitest_cmd.rs +0 -385
  196. package/temp/rtk/src/wc_cmd.rs +0 -401
  197. package/temp/rtk/src/wget_cmd.rs +0 -260
  198. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  199. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  200. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  201. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  202. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
@@ -1,1771 +0,0 @@
1
- use crate::binlog;
2
- use crate::dotnet_format_report;
3
- use crate::dotnet_trx;
4
- use crate::tracking;
5
- use crate::utils::truncate;
6
- use anyhow::{Context, Result};
7
- use std::ffi::OsString;
8
- use std::path::{Path, PathBuf};
9
- use std::process::Command;
10
- use std::sync::atomic::{AtomicU64, Ordering};
11
- use std::time::{SystemTime, UNIX_EPOCH};
12
-
13
- const DOTNET_CLI_UI_LANGUAGE: &str = "DOTNET_CLI_UI_LANGUAGE";
14
- const DOTNET_CLI_UI_LANGUAGE_VALUE: &str = "en-US";
15
- static TEMP_PATH_COUNTER: AtomicU64 = AtomicU64::new(0);
16
-
17
- pub fn run_build(args: &[String], verbose: u8) -> Result<()> {
18
- run_dotnet_with_binlog("build", args, verbose)
19
- }
20
-
21
- pub fn run_test(args: &[String], verbose: u8) -> Result<()> {
22
- run_dotnet_with_binlog("test", args, verbose)
23
- }
24
-
25
- pub fn run_restore(args: &[String], verbose: u8) -> Result<()> {
26
- run_dotnet_with_binlog("restore", args, verbose)
27
- }
28
-
29
- pub fn run_format(args: &[String], verbose: u8) -> Result<()> {
30
- let timer = tracking::TimedExecution::start();
31
- let (report_path, cleanup_report_path) = resolve_format_report_path(args);
32
- let mut cmd = Command::new("dotnet");
33
- cmd.env(DOTNET_CLI_UI_LANGUAGE, DOTNET_CLI_UI_LANGUAGE_VALUE);
34
- cmd.arg("format");
35
-
36
- for arg in build_effective_dotnet_format_args(args, report_path.as_deref()) {
37
- cmd.arg(arg);
38
- }
39
-
40
- if verbose > 0 {
41
- eprintln!("Running: dotnet format {}", args.join(" "));
42
- }
43
-
44
- let command_started_at = SystemTime::now();
45
- let output = cmd.output().context("Failed to run dotnet format")?;
46
- let stdout = String::from_utf8_lossy(&output.stdout);
47
- let stderr = String::from_utf8_lossy(&output.stderr);
48
- let raw = format!("{}\n{}", stdout, stderr);
49
-
50
- let check_mode = !has_write_mode_override(args);
51
- let filtered =
52
- format_report_summary_or_raw(report_path.as_deref(), check_mode, &raw, command_started_at);
53
- println!("{}", filtered);
54
-
55
- timer.track(
56
- &format!("dotnet format {}", args.join(" ")),
57
- &format!("rtk dotnet format {}", args.join(" ")),
58
- &raw,
59
- &filtered,
60
- );
61
-
62
- if cleanup_report_path {
63
- if let Some(path) = report_path.as_deref() {
64
- cleanup_temp_file(path);
65
- }
66
- }
67
-
68
- if !output.status.success() {
69
- std::process::exit(output.status.code().unwrap_or(1));
70
- }
71
-
72
- Ok(())
73
- }
74
-
75
- pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
76
- if args.is_empty() {
77
- anyhow::bail!("dotnet: no subcommand specified");
78
- }
79
-
80
- let timer = tracking::TimedExecution::start();
81
- let subcommand = args[0].to_string_lossy().to_string();
82
-
83
- let mut cmd = Command::new("dotnet");
84
- cmd.env(DOTNET_CLI_UI_LANGUAGE, DOTNET_CLI_UI_LANGUAGE_VALUE);
85
- cmd.arg(&subcommand);
86
- for arg in &args[1..] {
87
- cmd.arg(arg);
88
- }
89
-
90
- if verbose > 0 {
91
- eprintln!("Running: dotnet {} ...", subcommand);
92
- }
93
-
94
- let output = cmd
95
- .output()
96
- .with_context(|| format!("Failed to run dotnet {}", subcommand))?;
97
-
98
- let stdout = String::from_utf8_lossy(&output.stdout);
99
- let stderr = String::from_utf8_lossy(&output.stderr);
100
- let raw = format!("{}\n{}", stdout, stderr);
101
-
102
- print!("{}", stdout);
103
- eprint!("{}", stderr);
104
-
105
- timer.track(
106
- &format!("dotnet {}", subcommand),
107
- &format!("rtk dotnet {}", subcommand),
108
- &raw,
109
- &raw,
110
- );
111
-
112
- if !output.status.success() {
113
- std::process::exit(output.status.code().unwrap_or(1));
114
- }
115
-
116
- Ok(())
117
- }
118
-
119
- fn run_dotnet_with_binlog(subcommand: &str, args: &[String], verbose: u8) -> Result<()> {
120
- let timer = tracking::TimedExecution::start();
121
- let binlog_path = build_binlog_path(subcommand);
122
- let should_expect_binlog = subcommand != "test" || has_binlog_arg(args);
123
-
124
- // For test commands, prefer user-provided results directory; otherwise create isolated one.
125
- let (trx_results_dir, cleanup_trx_results_dir) = resolve_trx_results_dir(subcommand, args);
126
-
127
- let mut cmd = Command::new("dotnet");
128
- cmd.env(DOTNET_CLI_UI_LANGUAGE, DOTNET_CLI_UI_LANGUAGE_VALUE);
129
- cmd.arg(subcommand);
130
-
131
- for arg in
132
- build_effective_dotnet_args(subcommand, args, &binlog_path, trx_results_dir.as_deref())
133
- {
134
- cmd.arg(arg);
135
- }
136
-
137
- if verbose > 0 {
138
- eprintln!("Running: dotnet {} {}", subcommand, args.join(" "));
139
- }
140
-
141
- let command_started_at = SystemTime::now();
142
- let output = cmd
143
- .output()
144
- .with_context(|| format!("Failed to run dotnet {}", subcommand))?;
145
-
146
- let stdout = String::from_utf8_lossy(&output.stdout);
147
- let stderr = String::from_utf8_lossy(&output.stderr);
148
- let raw = format!("{}\n{}", stdout, stderr);
149
-
150
- let filtered = match subcommand {
151
- "build" => {
152
- let binlog_summary = if should_expect_binlog && binlog_path.exists() {
153
- normalize_build_summary(
154
- binlog::parse_build(&binlog_path).unwrap_or_default(),
155
- output.status.success(),
156
- )
157
- } else {
158
- binlog::BuildSummary::default()
159
- };
160
- let raw_summary = normalize_build_summary(
161
- binlog::parse_build_from_text(&raw),
162
- output.status.success(),
163
- );
164
- let summary = merge_build_summaries(binlog_summary, raw_summary);
165
- format_build_output(&summary, &binlog_path)
166
- }
167
- "test" => {
168
- // First try to parse from binlog/console output
169
- let parsed_summary = if should_expect_binlog && binlog_path.exists() {
170
- binlog::parse_test(&binlog_path).unwrap_or_default()
171
- } else {
172
- binlog::TestSummary::default()
173
- };
174
- let raw_summary = binlog::parse_test_from_text(&raw);
175
- let merged_summary = merge_test_summaries(parsed_summary, raw_summary);
176
- let summary = merge_test_summary_from_trx(
177
- merged_summary,
178
- trx_results_dir.as_deref(),
179
- dotnet_trx::find_recent_trx_in_testresults(),
180
- command_started_at,
181
- );
182
-
183
- let summary = normalize_test_summary(summary, output.status.success());
184
- let binlog_diagnostics = if should_expect_binlog && binlog_path.exists() {
185
- normalize_build_summary(
186
- binlog::parse_build(&binlog_path).unwrap_or_default(),
187
- output.status.success(),
188
- )
189
- } else {
190
- binlog::BuildSummary::default()
191
- };
192
- let raw_diagnostics = normalize_build_summary(
193
- binlog::parse_build_from_text(&raw),
194
- output.status.success(),
195
- );
196
- let test_build_summary = merge_build_summaries(binlog_diagnostics, raw_diagnostics);
197
- format_test_output(
198
- &summary,
199
- &test_build_summary.errors,
200
- &test_build_summary.warnings,
201
- &binlog_path,
202
- )
203
- }
204
- "restore" => {
205
- let binlog_summary = if should_expect_binlog && binlog_path.exists() {
206
- normalize_restore_summary(
207
- binlog::parse_restore(&binlog_path).unwrap_or_default(),
208
- output.status.success(),
209
- )
210
- } else {
211
- binlog::RestoreSummary::default()
212
- };
213
- let raw_summary = normalize_restore_summary(
214
- binlog::parse_restore_from_text(&raw),
215
- output.status.success(),
216
- );
217
- let summary = merge_restore_summaries(binlog_summary, raw_summary);
218
-
219
- let (raw_errors, raw_warnings) = binlog::parse_restore_issues_from_text(&raw);
220
-
221
- format_restore_output(&summary, &raw_errors, &raw_warnings, &binlog_path)
222
- }
223
- _ => raw.clone(),
224
- };
225
-
226
- let output_to_print = if !output.status.success() {
227
- let stdout_trimmed = stdout.trim();
228
- let stderr_trimmed = stderr.trim();
229
- if !stdout_trimmed.is_empty() {
230
- format!("{}\n\n{}", stdout_trimmed, filtered)
231
- } else if !stderr_trimmed.is_empty() {
232
- format!("{}\n\n{}", stderr_trimmed, filtered)
233
- } else {
234
- filtered
235
- }
236
- } else {
237
- filtered
238
- };
239
-
240
- println!("{}", output_to_print);
241
-
242
- timer.track(
243
- &format!("dotnet {} {}", subcommand, args.join(" ")),
244
- &format!("rtk dotnet {} {}", subcommand, args.join(" ")),
245
- &raw,
246
- &output_to_print,
247
- );
248
-
249
- cleanup_temp_file(&binlog_path);
250
- if cleanup_trx_results_dir {
251
- if let Some(dir) = trx_results_dir.as_deref() {
252
- cleanup_temp_dir(dir);
253
- }
254
- }
255
-
256
- if verbose > 0 {
257
- eprintln!("Binlog cleaned up: {}", binlog_path.display());
258
- }
259
-
260
- if !output.status.success() {
261
- std::process::exit(output.status.code().unwrap_or(1));
262
- }
263
-
264
- Ok(())
265
- }
266
-
267
- fn build_binlog_path(subcommand: &str) -> PathBuf {
268
- std::env::temp_dir().join(format!(
269
- "rtk_dotnet_{}_{}.binlog",
270
- subcommand,
271
- unique_temp_suffix()
272
- ))
273
- }
274
-
275
- fn build_trx_results_dir() -> PathBuf {
276
- std::env::temp_dir().join(format!("rtk_dotnet_testresults_{}", unique_temp_suffix()))
277
- }
278
-
279
- fn unique_temp_suffix() -> String {
280
- let ts = SystemTime::now()
281
- .duration_since(UNIX_EPOCH)
282
- .map(|d| d.as_millis())
283
- .unwrap_or(0);
284
- let pid = std::process::id();
285
- let seq = TEMP_PATH_COUNTER.fetch_add(1, Ordering::Relaxed);
286
-
287
- // Keep suffix compact to avoid long temp paths while preserving practical uniqueness.
288
- format!("{:x}{:x}{:x}", ts, pid, seq)
289
- }
290
-
291
- fn resolve_trx_results_dir(subcommand: &str, args: &[String]) -> (Option<PathBuf>, bool) {
292
- if subcommand != "test" {
293
- return (None, false);
294
- }
295
-
296
- if let Some(user_dir) = extract_results_directory_arg(args) {
297
- return (Some(user_dir), false);
298
- }
299
-
300
- (Some(build_trx_results_dir()), true)
301
- }
302
-
303
- fn build_format_report_path() -> PathBuf {
304
- std::env::temp_dir().join(format!("rtk_dotnet_format_{}.json", unique_temp_suffix()))
305
- }
306
-
307
- fn resolve_format_report_path(args: &[String]) -> (Option<PathBuf>, bool) {
308
- if let Some(user_report_path) = extract_report_arg(args) {
309
- return (Some(user_report_path), false);
310
- }
311
-
312
- (Some(build_format_report_path()), true)
313
- }
314
-
315
- fn build_effective_dotnet_format_args(args: &[String], report_path: Option<&Path>) -> Vec<String> {
316
- let mut effective: Vec<String> = args
317
- .iter()
318
- .filter(|arg| !arg.eq_ignore_ascii_case("--write"))
319
- .cloned()
320
- .collect();
321
- let force_write_mode = has_write_mode_override(args);
322
-
323
- if !force_write_mode && !has_verify_no_changes_arg(args) {
324
- effective.push("--verify-no-changes".to_string());
325
- }
326
-
327
- if !has_report_arg(args) {
328
- if let Some(path) = report_path {
329
- effective.push("--report".to_string());
330
- effective.push(path.display().to_string());
331
- }
332
- }
333
-
334
- effective
335
- }
336
-
337
- fn format_report_summary_or_raw(
338
- report_path: Option<&Path>,
339
- check_mode: bool,
340
- raw: &str,
341
- command_started_at: SystemTime,
342
- ) -> String {
343
- let Some(report_path) = report_path else {
344
- return raw.to_string();
345
- };
346
-
347
- if !is_fresh_report(report_path, command_started_at) {
348
- return raw.to_string();
349
- }
350
-
351
- match dotnet_format_report::parse_format_report(report_path) {
352
- Ok(summary) => format_dotnet_format_output(&summary, check_mode),
353
- Err(_) => raw.to_string(),
354
- }
355
- }
356
-
357
- fn is_fresh_report(path: &Path, command_started_at: SystemTime) -> bool {
358
- let Ok(metadata) = std::fs::metadata(path) else {
359
- return false;
360
- };
361
-
362
- let Ok(modified_at) = metadata.modified() else {
363
- return false;
364
- };
365
-
366
- modified_at.duration_since(command_started_at).is_ok()
367
- }
368
-
369
- fn format_dotnet_format_output(
370
- summary: &dotnet_format_report::FormatSummary,
371
- check_mode: bool,
372
- ) -> String {
373
- let changed_count = summary.files_with_changes.len();
374
-
375
- if changed_count == 0 {
376
- return format!(
377
- "ok dotnet format: {} files formatted correctly",
378
- summary.total_files
379
- );
380
- }
381
-
382
- if !check_mode {
383
- return format!(
384
- "ok dotnet format: formatted {} files ({} already formatted)",
385
- changed_count, summary.files_unchanged
386
- );
387
- }
388
-
389
- let mut output = format!("Format: {} files need formatting", changed_count);
390
- output.push_str("\n---------------------------------------");
391
-
392
- for (index, file) in summary.files_with_changes.iter().take(20).enumerate() {
393
- let first_change = &file.changes[0];
394
- let rule = if first_change.diagnostic_id.is_empty() {
395
- first_change.format_description.as_str()
396
- } else {
397
- first_change.diagnostic_id.as_str()
398
- };
399
- output.push_str(&format!(
400
- "\n{}. {} (line {}, col {}, {})",
401
- index + 1,
402
- file.path,
403
- first_change.line_number,
404
- first_change.char_number,
405
- rule
406
- ));
407
- }
408
-
409
- if changed_count > 20 {
410
- output.push_str(&format!("\n... +{} more files", changed_count - 20));
411
- }
412
-
413
- output.push_str(&format!(
414
- "\n\nok {} files already formatted\nRun `dotnet format` to apply fixes",
415
- summary.files_unchanged
416
- ));
417
- output
418
- }
419
-
420
- fn cleanup_temp_file(path: &Path) {
421
- if path.exists() {
422
- std::fs::remove_file(path).ok();
423
- }
424
- }
425
-
426
- fn cleanup_temp_dir(path: &Path) {
427
- if path.exists() {
428
- std::fs::remove_dir_all(path).ok();
429
- }
430
- }
431
-
432
- fn merge_test_summary_from_trx(
433
- mut summary: binlog::TestSummary,
434
- trx_results_dir: Option<&Path>,
435
- fallback_trx_path: Option<PathBuf>,
436
- command_started_at: SystemTime,
437
- ) -> binlog::TestSummary {
438
- let mut trx_summary = None;
439
-
440
- if let Some(dir) = trx_results_dir.filter(|path| path.exists()) {
441
- trx_summary = dotnet_trx::parse_trx_files_in_dir_since(dir, Some(command_started_at));
442
-
443
- if trx_summary.is_none() {
444
- trx_summary = dotnet_trx::parse_trx_files_in_dir(dir);
445
- }
446
- }
447
-
448
- if trx_summary.is_none() {
449
- if let Some(trx) = fallback_trx_path {
450
- trx_summary = dotnet_trx::parse_trx_file_since(&trx, command_started_at);
451
- }
452
- }
453
-
454
- let Some(trx_summary) = trx_summary else {
455
- return summary;
456
- };
457
-
458
- if trx_summary.total > 0 && (summary.total == 0 || trx_summary.total >= summary.total) {
459
- summary.passed = trx_summary.passed;
460
- summary.failed = trx_summary.failed;
461
- summary.skipped = trx_summary.skipped;
462
- summary.total = trx_summary.total;
463
- }
464
-
465
- if summary.failed_tests.is_empty() && !trx_summary.failed_tests.is_empty() {
466
- summary.failed_tests = trx_summary.failed_tests;
467
- }
468
-
469
- if let Some(duration) = trx_summary.duration_text {
470
- summary.duration_text = Some(duration);
471
- }
472
-
473
- if trx_summary.project_count > summary.project_count {
474
- summary.project_count = trx_summary.project_count;
475
- }
476
-
477
- summary
478
- }
479
-
480
- fn build_effective_dotnet_args(
481
- subcommand: &str,
482
- args: &[String],
483
- binlog_path: &Path,
484
- trx_results_dir: Option<&Path>,
485
- ) -> Vec<String> {
486
- let mut effective = Vec::new();
487
-
488
- if subcommand != "test" && !has_binlog_arg(args) {
489
- effective.push(format!("-bl:{}", binlog_path.display()));
490
- }
491
-
492
- if subcommand != "test" && !has_verbosity_arg(args) {
493
- effective.push("-v:minimal".to_string());
494
- }
495
-
496
- if !has_nologo_arg(args) {
497
- effective.push("-nologo".to_string());
498
- }
499
-
500
- if subcommand == "test" {
501
- if !has_trx_logger_arg(args) {
502
- effective.push("--logger".to_string());
503
- effective.push("trx".to_string());
504
- }
505
-
506
- if !has_results_directory_arg(args) {
507
- if let Some(results_dir) = trx_results_dir {
508
- effective.push("--results-directory".to_string());
509
- effective.push(results_dir.display().to_string());
510
- }
511
- }
512
- }
513
-
514
- effective.extend(args.iter().cloned());
515
- effective
516
- }
517
-
518
- fn has_binlog_arg(args: &[String]) -> bool {
519
- args.iter().any(|arg| {
520
- let lower = arg.to_ascii_lowercase();
521
- lower.starts_with("-bl") || lower.starts_with("/bl")
522
- })
523
- }
524
-
525
- fn has_verbosity_arg(args: &[String]) -> bool {
526
- args.iter().any(|arg| {
527
- let lower = arg.to_ascii_lowercase();
528
- lower.starts_with("-v:")
529
- || lower.starts_with("/v:")
530
- || lower == "-v"
531
- || lower == "/v"
532
- || lower == "--verbosity"
533
- || lower.starts_with("--verbosity=")
534
- })
535
- }
536
-
537
- fn has_nologo_arg(args: &[String]) -> bool {
538
- args.iter()
539
- .any(|arg| matches!(arg.to_ascii_lowercase().as_str(), "-nologo" | "/nologo"))
540
- }
541
-
542
- fn has_trx_logger_arg(args: &[String]) -> bool {
543
- let mut iter = args.iter().peekable();
544
- while let Some(arg) = iter.next() {
545
- let lower = arg.to_ascii_lowercase();
546
- if lower == "--logger" {
547
- if let Some(next) = iter.peek() {
548
- let next_lower = next.to_ascii_lowercase();
549
- if next_lower == "trx" || next_lower.starts_with("trx;") {
550
- return true;
551
- }
552
- }
553
- continue;
554
- }
555
-
556
- for prefix in ["--logger:", "--logger="] {
557
- if let Some(value) = lower.strip_prefix(prefix) {
558
- if value == "trx" || value.starts_with("trx;") {
559
- return true;
560
- }
561
- }
562
- }
563
- }
564
-
565
- false
566
- }
567
-
568
- fn has_results_directory_arg(args: &[String]) -> bool {
569
- args.iter().any(|arg| {
570
- let lower = arg.to_ascii_lowercase();
571
- lower == "--results-directory" || lower.starts_with("--results-directory=")
572
- })
573
- }
574
-
575
- fn has_report_arg(args: &[String]) -> bool {
576
- args.iter().any(|arg| {
577
- let lower = arg.to_ascii_lowercase();
578
- lower == "--report" || lower.starts_with("--report=")
579
- })
580
- }
581
-
582
- fn extract_report_arg(args: &[String]) -> Option<PathBuf> {
583
- let mut iter = args.iter().peekable();
584
- while let Some(arg) = iter.next() {
585
- if arg.eq_ignore_ascii_case("--report") {
586
- if let Some(next) = iter.peek() {
587
- return Some(PathBuf::from(next.as_str()));
588
- }
589
- continue;
590
- }
591
-
592
- if let Some((_, value)) = arg.split_once('=') {
593
- if arg
594
- .split('=')
595
- .next()
596
- .is_some_and(|key| key.eq_ignore_ascii_case("--report"))
597
- {
598
- return Some(PathBuf::from(value));
599
- }
600
- }
601
- }
602
-
603
- None
604
- }
605
-
606
- fn has_verify_no_changes_arg(args: &[String]) -> bool {
607
- args.iter().any(|arg| {
608
- let lower = arg.to_ascii_lowercase();
609
- lower == "--verify-no-changes" || lower.starts_with("--verify-no-changes=")
610
- })
611
- }
612
-
613
- fn has_write_mode_override(args: &[String]) -> bool {
614
- args.iter().any(|arg| arg.eq_ignore_ascii_case("--write"))
615
- }
616
-
617
- fn extract_results_directory_arg(args: &[String]) -> Option<PathBuf> {
618
- let mut iter = args.iter().peekable();
619
- while let Some(arg) = iter.next() {
620
- if arg.eq_ignore_ascii_case("--results-directory") {
621
- if let Some(next) = iter.peek() {
622
- return Some(PathBuf::from(next.as_str()));
623
- }
624
- continue;
625
- }
626
-
627
- if let Some((_, value)) = arg.split_once('=') {
628
- if arg
629
- .split('=')
630
- .next()
631
- .is_some_and(|key| key.eq_ignore_ascii_case("--results-directory"))
632
- {
633
- return Some(PathBuf::from(value));
634
- }
635
- }
636
- }
637
-
638
- None
639
- }
640
-
641
- fn normalize_build_summary(
642
- mut summary: binlog::BuildSummary,
643
- command_success: bool,
644
- ) -> binlog::BuildSummary {
645
- if command_success {
646
- summary.succeeded = true;
647
- if summary.project_count == 0 {
648
- summary.project_count = 1;
649
- }
650
- }
651
-
652
- summary
653
- }
654
-
655
- fn merge_build_summaries(
656
- mut binlog_summary: binlog::BuildSummary,
657
- raw_summary: binlog::BuildSummary,
658
- ) -> binlog::BuildSummary {
659
- if binlog_summary.errors.is_empty() {
660
- binlog_summary.errors = raw_summary.errors;
661
- }
662
- if binlog_summary.warnings.is_empty() {
663
- binlog_summary.warnings = raw_summary.warnings;
664
- }
665
-
666
- if binlog_summary.project_count == 0 {
667
- binlog_summary.project_count = raw_summary.project_count;
668
- }
669
- if binlog_summary.duration_text.is_none() {
670
- binlog_summary.duration_text = raw_summary.duration_text;
671
- }
672
-
673
- binlog_summary
674
- }
675
-
676
- fn normalize_test_summary(
677
- mut summary: binlog::TestSummary,
678
- command_success: bool,
679
- ) -> binlog::TestSummary {
680
- if !command_success && summary.failed == 0 && summary.failed_tests.is_empty() {
681
- summary.failed = 1;
682
- if summary.total == 0 {
683
- summary.total = 1;
684
- }
685
- }
686
-
687
- if command_success && summary.total == 0 && summary.passed == 0 {
688
- summary.project_count = summary.project_count.max(1);
689
- }
690
-
691
- summary
692
- }
693
-
694
- fn merge_test_summaries(
695
- mut binlog_summary: binlog::TestSummary,
696
- raw_summary: binlog::TestSummary,
697
- ) -> binlog::TestSummary {
698
- if binlog_summary.total == 0 && raw_summary.total > 0 {
699
- binlog_summary.passed = raw_summary.passed;
700
- binlog_summary.failed = raw_summary.failed;
701
- binlog_summary.skipped = raw_summary.skipped;
702
- binlog_summary.total = raw_summary.total;
703
- }
704
-
705
- if !raw_summary.failed_tests.is_empty() {
706
- binlog_summary.failed_tests = raw_summary.failed_tests;
707
- }
708
-
709
- if binlog_summary.project_count == 0 {
710
- binlog_summary.project_count = raw_summary.project_count;
711
- }
712
-
713
- if binlog_summary.duration_text.is_none() {
714
- binlog_summary.duration_text = raw_summary.duration_text;
715
- }
716
-
717
- binlog_summary
718
- }
719
-
720
- fn normalize_restore_summary(
721
- mut summary: binlog::RestoreSummary,
722
- command_success: bool,
723
- ) -> binlog::RestoreSummary {
724
- if !command_success && summary.errors == 0 {
725
- summary.errors = 1;
726
- }
727
-
728
- summary
729
- }
730
-
731
- fn merge_restore_summaries(
732
- mut binlog_summary: binlog::RestoreSummary,
733
- raw_summary: binlog::RestoreSummary,
734
- ) -> binlog::RestoreSummary {
735
- if binlog_summary.restored_projects == 0 {
736
- binlog_summary.restored_projects = raw_summary.restored_projects;
737
- }
738
- if binlog_summary.errors == 0 {
739
- binlog_summary.errors = raw_summary.errors;
740
- }
741
- if binlog_summary.warnings == 0 {
742
- binlog_summary.warnings = raw_summary.warnings;
743
- }
744
- if binlog_summary.duration_text.is_none() {
745
- binlog_summary.duration_text = raw_summary.duration_text;
746
- }
747
-
748
- binlog_summary
749
- }
750
-
751
- fn format_issue(issue: &binlog::BinlogIssue, kind: &str) -> String {
752
- if issue.file.is_empty() {
753
- return format!(" {} {}", kind, truncate(&issue.message, 180));
754
- }
755
- if issue.code.is_empty() {
756
- return format!(
757
- " {}({},{}) {}: {}",
758
- issue.file,
759
- issue.line,
760
- issue.column,
761
- kind,
762
- truncate(&issue.message, 180)
763
- );
764
- }
765
- format!(
766
- " {}({},{}) {} {}: {}",
767
- issue.file,
768
- issue.line,
769
- issue.column,
770
- kind,
771
- issue.code,
772
- truncate(&issue.message, 180)
773
- )
774
- }
775
-
776
- fn format_build_output(summary: &binlog::BuildSummary, _binlog_path: &Path) -> String {
777
- let status_icon = if summary.succeeded { "ok" } else { "fail" };
778
- let duration = summary.duration_text.as_deref().unwrap_or("unknown");
779
-
780
- let mut out = format!(
781
- "{} dotnet build: {} projects, {} errors, {} warnings ({})",
782
- status_icon,
783
- summary.project_count,
784
- summary.errors.len(),
785
- summary.warnings.len(),
786
- duration
787
- );
788
-
789
- if !summary.errors.is_empty() {
790
- out.push_str("\n---------------------------------------\n\nErrors:\n");
791
- for issue in summary.errors.iter().take(20) {
792
- out.push_str(&format!("{}\n", format_issue(issue, "error")));
793
- }
794
- if summary.errors.len() > 20 {
795
- out.push_str(&format!(
796
- " ... +{} more errors\n",
797
- summary.errors.len() - 20
798
- ));
799
- }
800
- }
801
-
802
- if !summary.warnings.is_empty() {
803
- out.push_str("\nWarnings:\n");
804
- for issue in summary.warnings.iter().take(10) {
805
- out.push_str(&format!("{}\n", format_issue(issue, "warning")));
806
- }
807
- if summary.warnings.len() > 10 {
808
- out.push_str(&format!(
809
- " ... +{} more warnings\n",
810
- summary.warnings.len() - 10
811
- ));
812
- }
813
- }
814
-
815
- // Binlog path omitted from output (temp file, already cleaned up)
816
- out
817
- }
818
-
819
- fn format_test_output(
820
- summary: &binlog::TestSummary,
821
- errors: &[binlog::BinlogIssue],
822
- warnings: &[binlog::BinlogIssue],
823
- _binlog_path: &Path,
824
- ) -> String {
825
- let has_failures = summary.failed > 0 || !summary.failed_tests.is_empty();
826
- let status_icon = if has_failures { "fail" } else { "ok" };
827
- let duration = summary.duration_text.as_deref().unwrap_or("unknown");
828
- let warning_count = warnings.len();
829
- let counts_unavailable = summary.passed == 0
830
- && summary.failed == 0
831
- && summary.skipped == 0
832
- && summary.total == 0
833
- && summary.failed_tests.is_empty();
834
-
835
- let mut out = if counts_unavailable {
836
- format!(
837
- "{} dotnet test: completed (binlog-only mode, counts unavailable, {} warnings) ({})",
838
- status_icon, warning_count, duration
839
- )
840
- } else if has_failures {
841
- format!(
842
- "{} dotnet test: {} passed, {} failed, {} skipped, {} warnings in {} projects ({})",
843
- status_icon,
844
- summary.passed,
845
- summary.failed,
846
- summary.skipped,
847
- warning_count,
848
- summary.project_count,
849
- duration
850
- )
851
- } else {
852
- format!(
853
- "{} dotnet test: {} tests passed, {} warnings in {} projects ({})",
854
- status_icon, summary.passed, warning_count, summary.project_count, duration
855
- )
856
- };
857
-
858
- if has_failures && !summary.failed_tests.is_empty() {
859
- out.push_str("\n---------------------------------------\n\nFailed Tests:\n");
860
- for failed in summary.failed_tests.iter().take(15) {
861
- out.push_str(&format!(" {}\n", failed.name));
862
- for detail in &failed.details {
863
- out.push_str(&format!(" {}\n", truncate(detail, 320)));
864
- }
865
- out.push('\n');
866
- }
867
- if summary.failed_tests.len() > 15 {
868
- out.push_str(&format!(
869
- "... +{} more failed tests\n",
870
- summary.failed_tests.len() - 15
871
- ));
872
- }
873
- }
874
-
875
- if !errors.is_empty() {
876
- out.push_str("\nErrors:\n");
877
- for issue in errors.iter().take(10) {
878
- out.push_str(&format!("{}\n", format_issue(issue, "error")));
879
- }
880
- if errors.len() > 10 {
881
- out.push_str(&format!(" ... +{} more errors\n", errors.len() - 10));
882
- }
883
- }
884
-
885
- if !warnings.is_empty() {
886
- out.push_str("\nWarnings:\n");
887
- for issue in warnings.iter().take(10) {
888
- out.push_str(&format!("{}\n", format_issue(issue, "warning")));
889
- }
890
- if warnings.len() > 10 {
891
- out.push_str(&format!(" ... +{} more warnings\n", warnings.len() - 10));
892
- }
893
- }
894
-
895
- // Binlog path omitted from output (temp file, already cleaned up)
896
- out
897
- }
898
-
899
- fn format_restore_output(
900
- summary: &binlog::RestoreSummary,
901
- errors: &[binlog::BinlogIssue],
902
- warnings: &[binlog::BinlogIssue],
903
- _binlog_path: &Path,
904
- ) -> String {
905
- let has_errors = summary.errors > 0;
906
- let status_icon = if has_errors { "fail" } else { "ok" };
907
- let duration = summary.duration_text.as_deref().unwrap_or("unknown");
908
-
909
- let mut out = format!(
910
- "{} dotnet restore: {} projects, {} errors, {} warnings ({})",
911
- status_icon, summary.restored_projects, summary.errors, summary.warnings, duration
912
- );
913
-
914
- if !errors.is_empty() {
915
- out.push_str("\n---------------------------------------\n\nErrors:\n");
916
- for issue in errors.iter().take(20) {
917
- out.push_str(&format!("{}\n", format_issue(issue, "error")));
918
- }
919
- if errors.len() > 20 {
920
- out.push_str(&format!(" ... +{} more errors\n", errors.len() - 20));
921
- }
922
- }
923
-
924
- if !warnings.is_empty() {
925
- out.push_str("\nWarnings:\n");
926
- for issue in warnings.iter().take(10) {
927
- out.push_str(&format!("{}\n", format_issue(issue, "warning")));
928
- }
929
- if warnings.len() > 10 {
930
- out.push_str(&format!(" ... +{} more warnings\n", warnings.len() - 10));
931
- }
932
- }
933
-
934
- // Binlog path omitted from output (temp file, already cleaned up)
935
- out
936
- }
937
-
938
- #[cfg(test)]
939
- mod tests {
940
- use super::*;
941
- use crate::dotnet_format_report;
942
- use std::fs;
943
- use std::time::Duration;
944
-
945
- fn build_dotnet_args_for_test(
946
- subcommand: &str,
947
- args: &[String],
948
- with_trx: bool,
949
- ) -> Vec<String> {
950
- let binlog_path = Path::new("/tmp/test.binlog");
951
- let trx_results_dir = if with_trx {
952
- Some(Path::new("/tmp/test results"))
953
- } else {
954
- None
955
- };
956
-
957
- build_effective_dotnet_args(subcommand, args, binlog_path, trx_results_dir)
958
- }
959
-
960
- fn trx_with_counts(total: usize, passed: usize, failed: usize) -> String {
961
- format!(
962
- r#"<?xml version="1.0" encoding="utf-8"?>
963
- <TestRun xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
964
- <ResultSummary outcome="Completed">
965
- <Counters total="{}" executed="{}" passed="{}" failed="{}" error="0" />
966
- </ResultSummary>
967
- </TestRun>"#,
968
- total, total, passed, failed
969
- )
970
- }
971
-
972
- fn format_fixture(name: &str) -> PathBuf {
973
- PathBuf::from(env!("CARGO_MANIFEST_DIR"))
974
- .join("tests")
975
- .join("fixtures")
976
- .join("dotnet")
977
- .join(name)
978
- }
979
-
980
- #[test]
981
- fn test_has_binlog_arg_detects_variants() {
982
- let args = vec!["-bl:my.binlog".to_string()];
983
- assert!(has_binlog_arg(&args));
984
-
985
- let args = vec!["/bl".to_string()];
986
- assert!(has_binlog_arg(&args));
987
-
988
- let args = vec!["--configuration".to_string(), "Release".to_string()];
989
- assert!(!has_binlog_arg(&args));
990
- }
991
-
992
- #[test]
993
- fn test_format_build_output_includes_errors_and_warnings() {
994
- let summary = binlog::BuildSummary {
995
- succeeded: false,
996
- project_count: 2,
997
- errors: vec![binlog::BinlogIssue {
998
- code: "CS0103".to_string(),
999
- file: "src/Program.cs".to_string(),
1000
- line: 42,
1001
- column: 15,
1002
- message: "The name 'foo' does not exist".to_string(),
1003
- }],
1004
- warnings: vec![binlog::BinlogIssue {
1005
- code: "CS0219".to_string(),
1006
- file: "src/Program.cs".to_string(),
1007
- line: 25,
1008
- column: 10,
1009
- message: "Variable 'x' is assigned but never used".to_string(),
1010
- }],
1011
- duration_text: Some("00:00:04.20".to_string()),
1012
- };
1013
-
1014
- let output = format_build_output(&summary, Path::new("/tmp/build.binlog"));
1015
- assert!(output.contains("dotnet build: 2 projects, 1 errors, 1 warnings"));
1016
- assert!(output.contains("error CS0103"));
1017
- assert!(output.contains("warning CS0219"));
1018
- }
1019
-
1020
- #[test]
1021
- fn test_format_test_output_shows_failures() {
1022
- let summary = binlog::TestSummary {
1023
- passed: 10,
1024
- failed: 1,
1025
- skipped: 0,
1026
- total: 11,
1027
- project_count: 1,
1028
- failed_tests: vec![binlog::FailedTest {
1029
- name: "MyTests.ShouldFail".to_string(),
1030
- details: vec!["Assert.Equal failure".to_string()],
1031
- }],
1032
- duration_text: Some("1 s".to_string()),
1033
- };
1034
-
1035
- let output = format_test_output(&summary, &[], &[], Path::new("/tmp/test.binlog"));
1036
- assert!(output.contains("10 passed, 1 failed"));
1037
- assert!(output.contains("MyTests.ShouldFail"));
1038
- }
1039
-
1040
- #[test]
1041
- fn test_format_test_output_surfaces_warnings() {
1042
- let summary = binlog::TestSummary {
1043
- passed: 940,
1044
- failed: 0,
1045
- skipped: 7,
1046
- total: 947,
1047
- project_count: 1,
1048
- failed_tests: Vec::new(),
1049
- duration_text: Some("1 s".to_string()),
1050
- };
1051
-
1052
- let warnings = vec![binlog::BinlogIssue {
1053
- code: String::new(),
1054
- file: "/sdk/Microsoft.TestPlatform.targets".to_string(),
1055
- line: 48,
1056
- column: 5,
1057
- message: "Violators:".to_string(),
1058
- }];
1059
-
1060
- let output = format_test_output(&summary, &[], &warnings, Path::new("/tmp/test.binlog"));
1061
- assert!(output.contains("940 tests passed, 1 warnings"));
1062
- assert!(output.contains("Warnings:"));
1063
- assert!(output.contains("Microsoft.TestPlatform.targets"));
1064
- }
1065
-
1066
- #[test]
1067
- fn test_format_test_output_surfaces_errors() {
1068
- let summary = binlog::TestSummary {
1069
- passed: 939,
1070
- failed: 1,
1071
- skipped: 7,
1072
- total: 947,
1073
- project_count: 1,
1074
- failed_tests: Vec::new(),
1075
- duration_text: Some("1 s".to_string()),
1076
- };
1077
-
1078
- let errors = vec![binlog::BinlogIssue {
1079
- code: "TESTERROR".to_string(),
1080
- file: "/repo/MessageMapperTests.cs".to_string(),
1081
- line: 135,
1082
- column: 0,
1083
- message: "CreateInstance_should_initialize_interface_message_type_on_demand"
1084
- .to_string(),
1085
- }];
1086
-
1087
- let output = format_test_output(&summary, &errors, &[], Path::new("/tmp/test.binlog"));
1088
- assert!(output.contains("Errors:"));
1089
- assert!(output.contains("error TESTERROR"));
1090
- assert!(
1091
- output.contains("CreateInstance_should_initialize_interface_message_type_on_demand")
1092
- );
1093
- }
1094
-
1095
- #[test]
1096
- fn test_format_restore_output_success() {
1097
- let summary = binlog::RestoreSummary {
1098
- restored_projects: 3,
1099
- warnings: 1,
1100
- errors: 0,
1101
- duration_text: Some("00:00:01.10".to_string()),
1102
- };
1103
-
1104
- let output = format_restore_output(&summary, &[], &[], Path::new("/tmp/restore.binlog"));
1105
- assert!(output.starts_with("ok dotnet restore"));
1106
- assert!(output.contains("3 projects"));
1107
- assert!(output.contains("1 warnings"));
1108
- }
1109
-
1110
- #[test]
1111
- fn test_format_restore_output_failure() {
1112
- let summary = binlog::RestoreSummary {
1113
- restored_projects: 2,
1114
- warnings: 0,
1115
- errors: 1,
1116
- duration_text: Some("00:00:01.00".to_string()),
1117
- };
1118
-
1119
- let output = format_restore_output(&summary, &[], &[], Path::new("/tmp/restore.binlog"));
1120
- assert!(output.starts_with("fail dotnet restore"));
1121
- assert!(output.contains("1 errors"));
1122
- }
1123
-
1124
- #[test]
1125
- fn test_format_restore_output_includes_error_details() {
1126
- let summary = binlog::RestoreSummary {
1127
- restored_projects: 2,
1128
- warnings: 0,
1129
- errors: 1,
1130
- duration_text: Some("00:00:01.00".to_string()),
1131
- };
1132
-
1133
- let issues = vec![binlog::BinlogIssue {
1134
- code: "NU1101".to_string(),
1135
- file: "/repo/src/App/App.csproj".to_string(),
1136
- line: 0,
1137
- column: 0,
1138
- message: "Unable to find package Foo.Bar".to_string(),
1139
- }];
1140
-
1141
- let output =
1142
- format_restore_output(&summary, &issues, &[], Path::new("/tmp/restore.binlog"));
1143
- assert!(output.contains("Errors:"));
1144
- assert!(output.contains("error NU1101"));
1145
- assert!(output.contains("Unable to find package Foo.Bar"));
1146
- }
1147
-
1148
- #[test]
1149
- fn test_format_test_output_handles_binlog_only_without_counts() {
1150
- let summary = binlog::TestSummary {
1151
- passed: 0,
1152
- failed: 0,
1153
- skipped: 0,
1154
- total: 0,
1155
- project_count: 0,
1156
- failed_tests: Vec::new(),
1157
- duration_text: Some("unknown".to_string()),
1158
- };
1159
-
1160
- let output = format_test_output(&summary, &[], &[], Path::new("/tmp/test.binlog"));
1161
- assert!(output.contains("counts unavailable"));
1162
- }
1163
-
1164
- #[test]
1165
- fn test_normalize_build_summary_sets_success_floor() {
1166
- let summary = binlog::BuildSummary {
1167
- succeeded: false,
1168
- project_count: 0,
1169
- errors: Vec::new(),
1170
- warnings: Vec::new(),
1171
- duration_text: None,
1172
- };
1173
-
1174
- let normalized = normalize_build_summary(summary, true);
1175
- assert!(normalized.succeeded);
1176
- assert_eq!(normalized.project_count, 1);
1177
- }
1178
-
1179
- #[test]
1180
- fn test_merge_build_summaries_keeps_structured_issues_when_present() {
1181
- let binlog_summary = binlog::BuildSummary {
1182
- succeeded: false,
1183
- project_count: 11,
1184
- errors: vec![binlog::BinlogIssue {
1185
- code: String::new(),
1186
- file: "IDE0055".to_string(),
1187
- line: 0,
1188
- column: 0,
1189
- message: "Fix formatting".to_string(),
1190
- }],
1191
- warnings: Vec::new(),
1192
- duration_text: Some("00:00:03.54".to_string()),
1193
- };
1194
-
1195
- let raw_summary = binlog::BuildSummary {
1196
- succeeded: false,
1197
- project_count: 2,
1198
- errors: vec![
1199
- binlog::BinlogIssue {
1200
- code: "IDE0055".to_string(),
1201
- file: "/repo/src/Behavior.cs".to_string(),
1202
- line: 13,
1203
- column: 32,
1204
- message: "Fix formatting".to_string(),
1205
- },
1206
- binlog::BinlogIssue {
1207
- code: "IDE0055".to_string(),
1208
- file: "/repo/src/Behavior.cs".to_string(),
1209
- line: 13,
1210
- column: 41,
1211
- message: "Fix formatting".to_string(),
1212
- },
1213
- ],
1214
- warnings: Vec::new(),
1215
- duration_text: Some("00:00:03.54".to_string()),
1216
- };
1217
-
1218
- let merged = merge_build_summaries(binlog_summary, raw_summary);
1219
- assert_eq!(merged.project_count, 11);
1220
- assert_eq!(merged.errors.len(), 1);
1221
- assert_eq!(merged.errors[0].file, "IDE0055");
1222
- assert_eq!(merged.errors[0].line, 0);
1223
- assert_eq!(merged.errors[0].column, 0);
1224
- }
1225
-
1226
- #[test]
1227
- fn test_merge_build_summaries_keeps_binlog_when_context_is_good() {
1228
- let binlog_summary = binlog::BuildSummary {
1229
- succeeded: false,
1230
- project_count: 2,
1231
- errors: vec![binlog::BinlogIssue {
1232
- code: "CS0103".to_string(),
1233
- file: "src/Program.cs".to_string(),
1234
- line: 42,
1235
- column: 15,
1236
- message: "The name 'foo' does not exist".to_string(),
1237
- }],
1238
- warnings: Vec::new(),
1239
- duration_text: Some("00:00:01.00".to_string()),
1240
- };
1241
-
1242
- let raw_summary = binlog::BuildSummary {
1243
- succeeded: false,
1244
- project_count: 2,
1245
- errors: vec![binlog::BinlogIssue {
1246
- code: "CS0103".to_string(),
1247
- file: String::new(),
1248
- line: 0,
1249
- column: 0,
1250
- message: "Build error #1 (details omitted)".to_string(),
1251
- }],
1252
- warnings: Vec::new(),
1253
- duration_text: None,
1254
- };
1255
-
1256
- let merged = merge_build_summaries(binlog_summary.clone(), raw_summary);
1257
- assert_eq!(merged.errors, binlog_summary.errors);
1258
- }
1259
-
1260
- #[test]
1261
- fn test_normalize_test_summary_sets_failure_floor() {
1262
- let summary = binlog::TestSummary {
1263
- passed: 0,
1264
- failed: 0,
1265
- skipped: 0,
1266
- total: 0,
1267
- project_count: 0,
1268
- failed_tests: Vec::new(),
1269
- duration_text: None,
1270
- };
1271
-
1272
- let normalized = normalize_test_summary(summary, false);
1273
- assert_eq!(normalized.failed, 1);
1274
- assert_eq!(normalized.total, 1);
1275
- }
1276
-
1277
- #[test]
1278
- fn test_merge_test_summaries_keeps_structured_counts_and_fills_failed_tests() {
1279
- let binlog_summary = binlog::TestSummary {
1280
- passed: 939,
1281
- failed: 1,
1282
- skipped: 8,
1283
- total: 948,
1284
- project_count: 1,
1285
- failed_tests: Vec::new(),
1286
- duration_text: Some("unknown".to_string()),
1287
- };
1288
-
1289
- let raw_summary = binlog::TestSummary {
1290
- passed: 939,
1291
- failed: 1,
1292
- skipped: 7,
1293
- total: 947,
1294
- project_count: 0,
1295
- failed_tests: vec![binlog::FailedTest {
1296
- name: "MessageMapperTests.CreateInstance_should_initialize_interface_message_type_on_demand"
1297
- .to_string(),
1298
- details: vec!["Assert.That(messageInstance, Is.Null)".to_string()],
1299
- }],
1300
- duration_text: Some("1 s".to_string()),
1301
- };
1302
-
1303
- let merged = merge_test_summaries(binlog_summary, raw_summary);
1304
- assert_eq!(merged.skipped, 8);
1305
- assert_eq!(merged.total, 948);
1306
- assert_eq!(merged.failed_tests.len(), 1);
1307
- assert!(merged.failed_tests[0]
1308
- .name
1309
- .contains("CreateInstance_should_initialize"));
1310
- }
1311
-
1312
- #[test]
1313
- fn test_normalize_restore_summary_sets_error_floor_on_failed_command() {
1314
- let summary = binlog::RestoreSummary {
1315
- restored_projects: 2,
1316
- warnings: 0,
1317
- errors: 0,
1318
- duration_text: None,
1319
- };
1320
-
1321
- let normalized = normalize_restore_summary(summary, false);
1322
- assert_eq!(normalized.errors, 1);
1323
- }
1324
-
1325
- #[test]
1326
- fn test_merge_restore_summaries_prefers_raw_error_count() {
1327
- let binlog_summary = binlog::RestoreSummary {
1328
- restored_projects: 2,
1329
- warnings: 0,
1330
- errors: 0,
1331
- duration_text: Some("unknown".to_string()),
1332
- };
1333
-
1334
- let raw_summary = binlog::RestoreSummary {
1335
- restored_projects: 0,
1336
- warnings: 0,
1337
- errors: 1,
1338
- duration_text: Some("unknown".to_string()),
1339
- };
1340
-
1341
- let merged = merge_restore_summaries(binlog_summary, raw_summary);
1342
- assert_eq!(merged.errors, 1);
1343
- assert_eq!(merged.restored_projects, 2);
1344
- }
1345
-
1346
- #[test]
1347
- fn test_forwarding_args_with_spaces() {
1348
- let args = vec![
1349
- "--filter".to_string(),
1350
- "FullyQualifiedName~MyTests.Calculator*".to_string(),
1351
- "-c".to_string(),
1352
- "Release".to_string(),
1353
- ];
1354
-
1355
- let injected = build_dotnet_args_for_test("test", &args, true);
1356
- assert!(injected.contains(&"--filter".to_string()));
1357
- assert!(injected.contains(&"FullyQualifiedName~MyTests.Calculator*".to_string()));
1358
- assert!(injected.contains(&"-c".to_string()));
1359
- assert!(injected.contains(&"Release".to_string()));
1360
- }
1361
-
1362
- #[test]
1363
- fn test_forwarding_config_and_framework() {
1364
- let args = vec![
1365
- "--configuration".to_string(),
1366
- "Release".to_string(),
1367
- "--framework".to_string(),
1368
- "net8.0".to_string(),
1369
- ];
1370
-
1371
- let injected = build_dotnet_args_for_test("test", &args, true);
1372
- assert!(injected.contains(&"--configuration".to_string()));
1373
- assert!(injected.contains(&"Release".to_string()));
1374
- assert!(injected.contains(&"--framework".to_string()));
1375
- assert!(injected.contains(&"net8.0".to_string()));
1376
- }
1377
-
1378
- #[test]
1379
- fn test_forwarding_project_file() {
1380
- let args = vec![
1381
- "--project".to_string(),
1382
- "src/My App.Tests/My App.Tests.csproj".to_string(),
1383
- ];
1384
-
1385
- let injected = build_dotnet_args_for_test("test", &args, true);
1386
- assert!(injected.contains(&"--project".to_string()));
1387
- assert!(injected.contains(&"src/My App.Tests/My App.Tests.csproj".to_string()));
1388
- }
1389
-
1390
- #[test]
1391
- fn test_forwarding_no_build_and_no_restore() {
1392
- let args = vec!["--no-build".to_string(), "--no-restore".to_string()];
1393
-
1394
- let injected = build_dotnet_args_for_test("test", &args, true);
1395
- assert!(injected.contains(&"--no-build".to_string()));
1396
- assert!(injected.contains(&"--no-restore".to_string()));
1397
- }
1398
-
1399
- #[test]
1400
- fn test_user_verbose_override() {
1401
- let args = vec!["-v:detailed".to_string()];
1402
-
1403
- let injected = build_dotnet_args_for_test("test", &args, true);
1404
- let verbose_count = injected.iter().filter(|a| a.starts_with("-v:")).count();
1405
- assert_eq!(verbose_count, 1);
1406
- assert!(injected.contains(&"-v:detailed".to_string()));
1407
- assert!(!injected.contains(&"-v:minimal".to_string()));
1408
- }
1409
-
1410
- #[test]
1411
- fn test_user_long_verbosity_override() {
1412
- let args = vec!["--verbosity".to_string(), "detailed".to_string()];
1413
-
1414
- let injected = build_dotnet_args_for_test("build", &args, false);
1415
- assert!(injected.contains(&"--verbosity".to_string()));
1416
- assert!(injected.contains(&"detailed".to_string()));
1417
- assert!(!injected.contains(&"-v:minimal".to_string()));
1418
- }
1419
-
1420
- #[test]
1421
- fn test_test_subcommand_does_not_inject_minimal_verbosity_by_default() {
1422
- let args = Vec::<String>::new();
1423
-
1424
- let injected = build_dotnet_args_for_test("test", &args, true);
1425
- assert!(!injected.contains(&"-v:minimal".to_string()));
1426
- }
1427
-
1428
- #[test]
1429
- fn test_user_logger_override() {
1430
- let args = vec![
1431
- "--logger".to_string(),
1432
- "console;verbosity=detailed".to_string(),
1433
- ];
1434
-
1435
- let injected = build_dotnet_args_for_test("test", &args, true);
1436
- assert!(injected.contains(&"--logger".to_string()));
1437
- assert!(injected.contains(&"console;verbosity=detailed".to_string()));
1438
- assert!(injected.iter().any(|a| a == "trx"));
1439
- assert!(injected.iter().any(|a| a == "--results-directory"));
1440
- }
1441
-
1442
- #[test]
1443
- fn test_trx_logger_and_results_directory_injected() {
1444
- let args = Vec::<String>::new();
1445
-
1446
- let injected = build_dotnet_args_for_test("test", &args, true);
1447
- assert!(injected.contains(&"--logger".to_string()));
1448
- assert!(injected.contains(&"trx".to_string()));
1449
- assert!(injected.contains(&"--results-directory".to_string()));
1450
- assert!(injected.contains(&"/tmp/test results".to_string()));
1451
- }
1452
-
1453
- #[test]
1454
- fn test_user_trx_logger_does_not_duplicate() {
1455
- let args = vec!["--logger".to_string(), "trx".to_string()];
1456
-
1457
- let injected = build_dotnet_args_for_test("test", &args, true);
1458
- let trx_logger_count = injected.iter().filter(|a| *a == "trx").count();
1459
- assert_eq!(trx_logger_count, 1);
1460
- }
1461
-
1462
- #[test]
1463
- fn test_user_results_directory_prevents_extra_injection() {
1464
- let args = vec![
1465
- "--results-directory".to_string(),
1466
- "/custom/results".to_string(),
1467
- ];
1468
-
1469
- let injected = build_dotnet_args_for_test("test", &args, true);
1470
- assert!(!injected
1471
- .windows(2)
1472
- .any(|w| w[0] == "--results-directory" && w[1] == "/tmp/test results"));
1473
- assert!(injected
1474
- .windows(2)
1475
- .any(|w| w[0] == "--results-directory" && w[1] == "/custom/results"));
1476
- }
1477
-
1478
- #[test]
1479
- fn test_merge_test_summary_from_trx_uses_primary_and_cleans_file() {
1480
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1481
- let primary = temp_dir.path().join("primary.trx");
1482
- fs::write(&primary, trx_with_counts(3, 3, 0)).expect("write primary trx");
1483
-
1484
- let filled = merge_test_summary_from_trx(
1485
- binlog::TestSummary::default(),
1486
- Some(temp_dir.path()),
1487
- None,
1488
- SystemTime::now(),
1489
- );
1490
-
1491
- assert_eq!(filled.total, 3);
1492
- assert_eq!(filled.passed, 3);
1493
- assert!(primary.exists());
1494
- }
1495
-
1496
- #[test]
1497
- fn test_merge_test_summary_from_trx_falls_back_to_testresults() {
1498
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1499
- let fallback = temp_dir.path().join("fallback.trx");
1500
- fs::write(&fallback, trx_with_counts(2, 1, 1)).expect("write fallback trx");
1501
- let missing_primary = temp_dir.path().join("missing.trx");
1502
-
1503
- let filled = merge_test_summary_from_trx(
1504
- binlog::TestSummary::default(),
1505
- Some(&missing_primary),
1506
- Some(fallback.clone()),
1507
- UNIX_EPOCH,
1508
- );
1509
-
1510
- assert_eq!(filled.total, 2);
1511
- assert_eq!(filled.failed, 1);
1512
- assert!(fallback.exists());
1513
- }
1514
-
1515
- #[test]
1516
- fn test_merge_test_summary_from_trx_returns_default_when_no_trx() {
1517
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1518
- let missing = temp_dir.path().join("missing.trx");
1519
-
1520
- let filled = merge_test_summary_from_trx(
1521
- binlog::TestSummary::default(),
1522
- Some(&missing),
1523
- None,
1524
- SystemTime::now(),
1525
- );
1526
- assert_eq!(filled.total, 0);
1527
- }
1528
-
1529
- #[test]
1530
- fn test_merge_test_summary_from_trx_ignores_stale_fallback_file() {
1531
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1532
- let fallback = temp_dir.path().join("fallback.trx");
1533
- fs::write(&fallback, trx_with_counts(2, 1, 1)).expect("write fallback trx");
1534
- std::thread::sleep(std::time::Duration::from_millis(5));
1535
- let command_started_at = SystemTime::now();
1536
- let missing_primary = temp_dir.path().join("missing.trx");
1537
-
1538
- let filled = merge_test_summary_from_trx(
1539
- binlog::TestSummary::default(),
1540
- Some(&missing_primary),
1541
- Some(fallback.clone()),
1542
- command_started_at,
1543
- );
1544
-
1545
- assert_eq!(filled.total, 0);
1546
- assert!(fallback.exists());
1547
- }
1548
-
1549
- #[test]
1550
- fn test_merge_test_summary_from_trx_keeps_larger_existing_counts() {
1551
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1552
- let primary = temp_dir.path().join("primary.trx");
1553
- fs::write(&primary, trx_with_counts(5, 4, 1)).expect("write primary trx");
1554
-
1555
- let existing = binlog::TestSummary {
1556
- passed: 10,
1557
- failed: 2,
1558
- skipped: 0,
1559
- total: 12,
1560
- project_count: 1,
1561
- failed_tests: Vec::new(),
1562
- duration_text: Some("1 s".to_string()),
1563
- };
1564
-
1565
- let merged =
1566
- merge_test_summary_from_trx(existing, Some(temp_dir.path()), None, SystemTime::now());
1567
- assert_eq!(merged.total, 12);
1568
- assert_eq!(merged.passed, 10);
1569
- assert_eq!(merged.failed, 2);
1570
- }
1571
-
1572
- #[test]
1573
- fn test_merge_test_summary_from_trx_overrides_smaller_existing_counts() {
1574
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1575
- let primary = temp_dir.path().join("primary.trx");
1576
- fs::write(&primary, trx_with_counts(12, 10, 2)).expect("write primary trx");
1577
-
1578
- let existing = binlog::TestSummary {
1579
- passed: 4,
1580
- failed: 1,
1581
- skipped: 0,
1582
- total: 5,
1583
- project_count: 1,
1584
- failed_tests: Vec::new(),
1585
- duration_text: Some("1 s".to_string()),
1586
- };
1587
-
1588
- let merged =
1589
- merge_test_summary_from_trx(existing, Some(temp_dir.path()), None, SystemTime::now());
1590
- assert_eq!(merged.total, 12);
1591
- assert_eq!(merged.passed, 10);
1592
- assert_eq!(merged.failed, 2);
1593
- }
1594
-
1595
- #[test]
1596
- fn test_merge_test_summary_from_trx_uses_larger_project_count() {
1597
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1598
- let trx_a = temp_dir.path().join("a.trx");
1599
- let trx_b = temp_dir.path().join("b.trx");
1600
- fs::write(&trx_a, trx_with_counts(2, 2, 0)).expect("write first trx");
1601
- fs::write(&trx_b, trx_with_counts(3, 3, 0)).expect("write second trx");
1602
-
1603
- let existing = binlog::TestSummary {
1604
- passed: 5,
1605
- failed: 0,
1606
- skipped: 0,
1607
- total: 5,
1608
- project_count: 1,
1609
- failed_tests: Vec::new(),
1610
- duration_text: Some("1 s".to_string()),
1611
- };
1612
-
1613
- let merged =
1614
- merge_test_summary_from_trx(existing, Some(temp_dir.path()), None, SystemTime::now());
1615
- assert_eq!(merged.project_count, 2);
1616
- }
1617
-
1618
- #[test]
1619
- fn test_has_results_directory_arg_detects_variants() {
1620
- let args = vec!["--results-directory".to_string(), "/tmp/trx".to_string()];
1621
- assert!(has_results_directory_arg(&args));
1622
-
1623
- let args = vec!["--results-directory=/tmp/trx".to_string()];
1624
- assert!(has_results_directory_arg(&args));
1625
-
1626
- let args = vec!["--logger".to_string(), "trx".to_string()];
1627
- assert!(!has_results_directory_arg(&args));
1628
- }
1629
-
1630
- #[test]
1631
- fn test_extract_results_directory_arg_detects_variants() {
1632
- let args = vec!["--results-directory".to_string(), "/tmp/r1".to_string()];
1633
- assert_eq!(
1634
- extract_results_directory_arg(&args),
1635
- Some(PathBuf::from("/tmp/r1"))
1636
- );
1637
-
1638
- let args = vec!["--results-directory=/tmp/r2".to_string()];
1639
- assert_eq!(
1640
- extract_results_directory_arg(&args),
1641
- Some(PathBuf::from("/tmp/r2"))
1642
- );
1643
- }
1644
-
1645
- #[test]
1646
- fn test_resolve_trx_results_dir_user_directory_is_not_marked_for_cleanup() {
1647
- let args = vec![
1648
- "--results-directory".to_string(),
1649
- "/custom/results".to_string(),
1650
- ];
1651
-
1652
- let (dir, cleanup) = resolve_trx_results_dir("test", &args);
1653
- assert_eq!(dir, Some(PathBuf::from("/custom/results")));
1654
- assert!(!cleanup);
1655
- }
1656
-
1657
- #[test]
1658
- fn test_resolve_trx_results_dir_generated_directory_is_marked_for_cleanup() {
1659
- let args = Vec::<String>::new();
1660
-
1661
- let (dir, cleanup) = resolve_trx_results_dir("test", &args);
1662
- assert!(dir.is_some());
1663
- assert!(cleanup);
1664
- }
1665
-
1666
- #[test]
1667
- fn test_format_all_formatted() {
1668
- let summary =
1669
- dotnet_format_report::parse_format_report(&format_fixture("format_success.json"))
1670
- .expect("parse format report");
1671
-
1672
- let output = format_dotnet_format_output(&summary, true);
1673
- assert!(output.contains("ok dotnet format: 2 files formatted correctly"));
1674
- }
1675
-
1676
- #[test]
1677
- fn test_format_needs_formatting() {
1678
- let summary =
1679
- dotnet_format_report::parse_format_report(&format_fixture("format_changes.json"))
1680
- .expect("parse format report");
1681
-
1682
- let output = format_dotnet_format_output(&summary, true);
1683
- assert!(output.contains("Format: 2 files need formatting"));
1684
- assert!(output.contains("src/Program.cs (line 42, col 17, WHITESPACE)"));
1685
- assert!(output.contains("Run `dotnet format` to apply fixes"));
1686
- }
1687
-
1688
- #[test]
1689
- fn test_format_temp_file_cleanup() {
1690
- let args = Vec::<String>::new();
1691
- let (report_path, cleanup) = resolve_format_report_path(&args);
1692
- let report_path = report_path.expect("report path");
1693
-
1694
- assert!(cleanup);
1695
- fs::write(&report_path, "[]").expect("write temp report");
1696
- cleanup_temp_file(&report_path);
1697
- assert!(!report_path.exists());
1698
- }
1699
-
1700
- #[test]
1701
- fn test_format_user_report_arg_no_cleanup() {
1702
- let args = vec![
1703
- "--report".to_string(),
1704
- "/tmp/user-format-report.json".to_string(),
1705
- ];
1706
-
1707
- let (report_path, cleanup) = resolve_format_report_path(&args);
1708
- assert_eq!(
1709
- report_path,
1710
- Some(PathBuf::from("/tmp/user-format-report.json"))
1711
- );
1712
- assert!(!cleanup);
1713
- }
1714
-
1715
- #[test]
1716
- fn test_format_preserves_positional_project_argument_order() {
1717
- let args = vec!["src/App/App.csproj".to_string()];
1718
-
1719
- let effective =
1720
- build_effective_dotnet_format_args(&args, Some(Path::new("/tmp/report.json")));
1721
- assert_eq!(
1722
- effective.first().map(String::as_str),
1723
- Some("src/App/App.csproj")
1724
- );
1725
- }
1726
-
1727
- #[test]
1728
- fn test_format_report_summary_ignores_stale_report_file() {
1729
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1730
- let report = temp_dir.path().join("report.json");
1731
- fs::write(&report, "[]").expect("write report");
1732
-
1733
- let command_started_at = SystemTime::now()
1734
- .checked_add(Duration::from_secs(2))
1735
- .expect("future timestamp");
1736
- let raw = "RAW OUTPUT";
1737
-
1738
- let output = format_report_summary_or_raw(Some(&report), true, raw, command_started_at);
1739
- assert_eq!(output, raw);
1740
- }
1741
-
1742
- #[test]
1743
- fn test_format_report_summary_uses_fresh_report_file() {
1744
- let report = format_fixture("format_success.json");
1745
- let raw = "RAW OUTPUT";
1746
-
1747
- let output = format_report_summary_or_raw(Some(&report), true, raw, UNIX_EPOCH);
1748
- assert!(output.contains("ok dotnet format: 2 files formatted correctly"));
1749
- }
1750
-
1751
- #[test]
1752
- fn test_cleanup_temp_file_removes_existing_file() {
1753
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1754
- let temp_file = temp_dir.path().join("temp.binlog");
1755
- fs::write(&temp_file, "content").expect("write temp file");
1756
-
1757
- cleanup_temp_file(&temp_file);
1758
-
1759
- assert!(!temp_file.exists());
1760
- }
1761
-
1762
- #[test]
1763
- fn test_cleanup_temp_file_ignores_missing_file() {
1764
- let temp_dir = tempfile::tempdir().expect("create temp dir");
1765
- let missing_file = temp_dir.path().join("missing.binlog");
1766
-
1767
- cleanup_temp_file(&missing_file);
1768
-
1769
- assert!(!missing_file.exists());
1770
- }
1771
- }