@hasna/terminal 2.3.0 → 2.3.2

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