@hasna/terminal 2.2.0 → 2.3.0

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 (205) hide show
  1. package/dist/cli.js +29 -12
  2. package/package.json +1 -1
  3. package/src/ai.ts +50 -36
  4. package/src/cli.tsx +29 -12
  5. package/src/context-hints.ts +89 -0
  6. package/src/discover.ts +238 -0
  7. package/src/economy.ts +53 -0
  8. package/src/output-store.ts +65 -0
  9. package/src/providers/index.ts +4 -4
  10. package/src/sessions-db.ts +81 -0
  11. package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
  12. package/temp/rtk/.claude/agents/debugger.md +519 -0
  13. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
  14. package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
  15. package/temp/rtk/.claude/agents/technical-writer.md +355 -0
  16. package/temp/rtk/.claude/commands/diagnose.md +352 -0
  17. package/temp/rtk/.claude/commands/test-routing.md +362 -0
  18. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
  19. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
  20. package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
  21. package/temp/rtk/.claude/rules/cli-testing.md +526 -0
  22. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
  23. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
  24. package/temp/rtk/.claude/skills/performance.md +435 -0
  25. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
  26. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
  27. package/temp/rtk/.claude/skills/repo-recap.md +206 -0
  28. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
  29. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
  30. package/temp/rtk/.claude/skills/security-guardian.md +503 -0
  31. package/temp/rtk/.claude/skills/ship.md +404 -0
  32. package/temp/rtk/.github/workflows/benchmark.yml +34 -0
  33. package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
  34. package/temp/rtk/.github/workflows/release-please.yml +51 -0
  35. package/temp/rtk/.github/workflows/release.yml +343 -0
  36. package/temp/rtk/.github/workflows/security-check.yml +135 -0
  37. package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
  38. package/temp/rtk/.release-please-manifest.json +3 -0
  39. package/temp/rtk/ARCHITECTURE.md +1491 -0
  40. package/temp/rtk/CHANGELOG.md +640 -0
  41. package/temp/rtk/CLAUDE.md +605 -0
  42. package/temp/rtk/CONTRIBUTING.md +199 -0
  43. package/temp/rtk/Cargo.lock +1668 -0
  44. package/temp/rtk/Cargo.toml +64 -0
  45. package/temp/rtk/Formula/rtk.rb +43 -0
  46. package/temp/rtk/INSTALL.md +390 -0
  47. package/temp/rtk/LICENSE +21 -0
  48. package/temp/rtk/README.md +386 -0
  49. package/temp/rtk/README_es.md +159 -0
  50. package/temp/rtk/README_fr.md +197 -0
  51. package/temp/rtk/README_ja.md +159 -0
  52. package/temp/rtk/README_ko.md +159 -0
  53. package/temp/rtk/README_zh.md +167 -0
  54. package/temp/rtk/ROADMAP.md +15 -0
  55. package/temp/rtk/SECURITY.md +217 -0
  56. package/temp/rtk/TEST_EXEC_TIME.md +102 -0
  57. package/temp/rtk/build.rs +57 -0
  58. package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
  59. package/temp/rtk/docs/FEATURES.md +1410 -0
  60. package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
  61. package/temp/rtk/docs/filter-workflow.md +102 -0
  62. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  63. package/temp/rtk/docs/tracking.md +583 -0
  64. package/temp/rtk/hooks/opencode-rtk.ts +39 -0
  65. package/temp/rtk/hooks/rtk-awareness.md +29 -0
  66. package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
  67. package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
  68. package/temp/rtk/install.sh +124 -0
  69. package/temp/rtk/release-please-config.json +10 -0
  70. package/temp/rtk/scripts/benchmark.sh +592 -0
  71. package/temp/rtk/scripts/check-installation.sh +162 -0
  72. package/temp/rtk/scripts/install-local.sh +37 -0
  73. package/temp/rtk/scripts/rtk-economics.sh +137 -0
  74. package/temp/rtk/scripts/test-all.sh +561 -0
  75. package/temp/rtk/scripts/test-aristote.sh +227 -0
  76. package/temp/rtk/scripts/test-tracking.sh +79 -0
  77. package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
  78. package/temp/rtk/scripts/validate-docs.sh +73 -0
  79. package/temp/rtk/src/aws_cmd.rs +880 -0
  80. package/temp/rtk/src/binlog.rs +1645 -0
  81. package/temp/rtk/src/cargo_cmd.rs +1727 -0
  82. package/temp/rtk/src/cc_economics.rs +1157 -0
  83. package/temp/rtk/src/ccusage.rs +340 -0
  84. package/temp/rtk/src/config.rs +187 -0
  85. package/temp/rtk/src/container.rs +855 -0
  86. package/temp/rtk/src/curl_cmd.rs +134 -0
  87. package/temp/rtk/src/deps.rs +268 -0
  88. package/temp/rtk/src/diff_cmd.rs +367 -0
  89. package/temp/rtk/src/discover/mod.rs +274 -0
  90. package/temp/rtk/src/discover/provider.rs +388 -0
  91. package/temp/rtk/src/discover/registry.rs +2022 -0
  92. package/temp/rtk/src/discover/report.rs +202 -0
  93. package/temp/rtk/src/discover/rules.rs +667 -0
  94. package/temp/rtk/src/display_helpers.rs +402 -0
  95. package/temp/rtk/src/dotnet_cmd.rs +1771 -0
  96. package/temp/rtk/src/dotnet_format_report.rs +133 -0
  97. package/temp/rtk/src/dotnet_trx.rs +593 -0
  98. package/temp/rtk/src/env_cmd.rs +204 -0
  99. package/temp/rtk/src/filter.rs +462 -0
  100. package/temp/rtk/src/filters/README.md +52 -0
  101. package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
  102. package/temp/rtk/src/filters/basedpyright.toml +47 -0
  103. package/temp/rtk/src/filters/biome.toml +45 -0
  104. package/temp/rtk/src/filters/brew-install.toml +37 -0
  105. package/temp/rtk/src/filters/composer-install.toml +40 -0
  106. package/temp/rtk/src/filters/df.toml +16 -0
  107. package/temp/rtk/src/filters/dotnet-build.toml +64 -0
  108. package/temp/rtk/src/filters/du.toml +16 -0
  109. package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
  110. package/temp/rtk/src/filters/gcc.toml +49 -0
  111. package/temp/rtk/src/filters/gcloud.toml +22 -0
  112. package/temp/rtk/src/filters/hadolint.toml +24 -0
  113. package/temp/rtk/src/filters/helm.toml +29 -0
  114. package/temp/rtk/src/filters/iptables.toml +27 -0
  115. package/temp/rtk/src/filters/jj.toml +28 -0
  116. package/temp/rtk/src/filters/jq.toml +24 -0
  117. package/temp/rtk/src/filters/make.toml +41 -0
  118. package/temp/rtk/src/filters/markdownlint.toml +24 -0
  119. package/temp/rtk/src/filters/mix-compile.toml +27 -0
  120. package/temp/rtk/src/filters/mix-format.toml +15 -0
  121. package/temp/rtk/src/filters/mvn-build.toml +44 -0
  122. package/temp/rtk/src/filters/oxlint.toml +43 -0
  123. package/temp/rtk/src/filters/ping.toml +63 -0
  124. package/temp/rtk/src/filters/pio-run.toml +40 -0
  125. package/temp/rtk/src/filters/poetry-install.toml +50 -0
  126. package/temp/rtk/src/filters/pre-commit.toml +35 -0
  127. package/temp/rtk/src/filters/ps.toml +16 -0
  128. package/temp/rtk/src/filters/quarto-render.toml +41 -0
  129. package/temp/rtk/src/filters/rsync.toml +48 -0
  130. package/temp/rtk/src/filters/shellcheck.toml +27 -0
  131. package/temp/rtk/src/filters/shopify-theme.toml +29 -0
  132. package/temp/rtk/src/filters/skopeo.toml +45 -0
  133. package/temp/rtk/src/filters/sops.toml +16 -0
  134. package/temp/rtk/src/filters/ssh.toml +44 -0
  135. package/temp/rtk/src/filters/stat.toml +34 -0
  136. package/temp/rtk/src/filters/swift-build.toml +41 -0
  137. package/temp/rtk/src/filters/systemctl-status.toml +33 -0
  138. package/temp/rtk/src/filters/terraform-plan.toml +35 -0
  139. package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
  140. package/temp/rtk/src/filters/tofu-init.toml +38 -0
  141. package/temp/rtk/src/filters/tofu-plan.toml +35 -0
  142. package/temp/rtk/src/filters/tofu-validate.toml +17 -0
  143. package/temp/rtk/src/filters/trunk-build.toml +39 -0
  144. package/temp/rtk/src/filters/ty.toml +50 -0
  145. package/temp/rtk/src/filters/uv-sync.toml +37 -0
  146. package/temp/rtk/src/filters/xcodebuild.toml +99 -0
  147. package/temp/rtk/src/filters/yamllint.toml +25 -0
  148. package/temp/rtk/src/find_cmd.rs +598 -0
  149. package/temp/rtk/src/format_cmd.rs +386 -0
  150. package/temp/rtk/src/gain.rs +723 -0
  151. package/temp/rtk/src/gh_cmd.rs +1651 -0
  152. package/temp/rtk/src/git.rs +2012 -0
  153. package/temp/rtk/src/go_cmd.rs +592 -0
  154. package/temp/rtk/src/golangci_cmd.rs +254 -0
  155. package/temp/rtk/src/grep_cmd.rs +288 -0
  156. package/temp/rtk/src/gt_cmd.rs +810 -0
  157. package/temp/rtk/src/hook_audit_cmd.rs +283 -0
  158. package/temp/rtk/src/hook_check.rs +171 -0
  159. package/temp/rtk/src/init.rs +1859 -0
  160. package/temp/rtk/src/integrity.rs +537 -0
  161. package/temp/rtk/src/json_cmd.rs +231 -0
  162. package/temp/rtk/src/learn/detector.rs +628 -0
  163. package/temp/rtk/src/learn/mod.rs +119 -0
  164. package/temp/rtk/src/learn/report.rs +184 -0
  165. package/temp/rtk/src/lint_cmd.rs +694 -0
  166. package/temp/rtk/src/local_llm.rs +316 -0
  167. package/temp/rtk/src/log_cmd.rs +248 -0
  168. package/temp/rtk/src/ls.rs +324 -0
  169. package/temp/rtk/src/main.rs +2482 -0
  170. package/temp/rtk/src/mypy_cmd.rs +389 -0
  171. package/temp/rtk/src/next_cmd.rs +241 -0
  172. package/temp/rtk/src/npm_cmd.rs +236 -0
  173. package/temp/rtk/src/parser/README.md +267 -0
  174. package/temp/rtk/src/parser/error.rs +46 -0
  175. package/temp/rtk/src/parser/formatter.rs +336 -0
  176. package/temp/rtk/src/parser/mod.rs +311 -0
  177. package/temp/rtk/src/parser/types.rs +119 -0
  178. package/temp/rtk/src/pip_cmd.rs +302 -0
  179. package/temp/rtk/src/playwright_cmd.rs +479 -0
  180. package/temp/rtk/src/pnpm_cmd.rs +573 -0
  181. package/temp/rtk/src/prettier_cmd.rs +221 -0
  182. package/temp/rtk/src/prisma_cmd.rs +482 -0
  183. package/temp/rtk/src/psql_cmd.rs +382 -0
  184. package/temp/rtk/src/pytest_cmd.rs +384 -0
  185. package/temp/rtk/src/read.rs +217 -0
  186. package/temp/rtk/src/rewrite_cmd.rs +50 -0
  187. package/temp/rtk/src/ruff_cmd.rs +402 -0
  188. package/temp/rtk/src/runner.rs +271 -0
  189. package/temp/rtk/src/summary.rs +297 -0
  190. package/temp/rtk/src/tee.rs +405 -0
  191. package/temp/rtk/src/telemetry.rs +248 -0
  192. package/temp/rtk/src/toml_filter.rs +1655 -0
  193. package/temp/rtk/src/tracking.rs +1416 -0
  194. package/temp/rtk/src/tree.rs +209 -0
  195. package/temp/rtk/src/tsc_cmd.rs +259 -0
  196. package/temp/rtk/src/utils.rs +432 -0
  197. package/temp/rtk/src/verify_cmd.rs +47 -0
  198. package/temp/rtk/src/vitest_cmd.rs +385 -0
  199. package/temp/rtk/src/wc_cmd.rs +401 -0
  200. package/temp/rtk/src/wget_cmd.rs +260 -0
  201. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
  202. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
  203. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
  204. package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
  205. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
@@ -0,0 +1,1771 @@
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
+ }