@hasna/terminal 2.0.5 → 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.
- package/dist/cli.js +52 -21
- package/package.json +1 -1
- package/src/ai.ts +77 -130
- package/src/cli.tsx +51 -21
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +291 -0
- package/src/discover.ts +238 -0
- package/src/economy.ts +53 -0
- package/src/output-processor.ts +7 -18
- package/src/output-store.ts +65 -0
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- package/src/sessions-db.ts +81 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
- package/temp/rtk/.claude/agents/debugger.md +519 -0
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
- package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
- package/temp/rtk/.claude/agents/technical-writer.md +355 -0
- package/temp/rtk/.claude/commands/diagnose.md +352 -0
- package/temp/rtk/.claude/commands/test-routing.md +362 -0
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
- package/temp/rtk/.claude/rules/cli-testing.md +526 -0
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
- package/temp/rtk/.claude/skills/performance.md +435 -0
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
- package/temp/rtk/.claude/skills/repo-recap.md +206 -0
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
- package/temp/rtk/.claude/skills/security-guardian.md +503 -0
- package/temp/rtk/.claude/skills/ship.md +404 -0
- package/temp/rtk/.github/workflows/benchmark.yml +34 -0
- package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
- package/temp/rtk/.github/workflows/release-please.yml +51 -0
- package/temp/rtk/.github/workflows/release.yml +343 -0
- package/temp/rtk/.github/workflows/security-check.yml +135 -0
- package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
- package/temp/rtk/.release-please-manifest.json +3 -0
- package/temp/rtk/ARCHITECTURE.md +1491 -0
- package/temp/rtk/CHANGELOG.md +640 -0
- package/temp/rtk/CLAUDE.md +605 -0
- package/temp/rtk/CONTRIBUTING.md +199 -0
- package/temp/rtk/Cargo.lock +1668 -0
- package/temp/rtk/Cargo.toml +64 -0
- package/temp/rtk/Formula/rtk.rb +43 -0
- package/temp/rtk/INSTALL.md +390 -0
- package/temp/rtk/LICENSE +21 -0
- package/temp/rtk/README.md +386 -0
- package/temp/rtk/README_es.md +159 -0
- package/temp/rtk/README_fr.md +197 -0
- package/temp/rtk/README_ja.md +159 -0
- package/temp/rtk/README_ko.md +159 -0
- package/temp/rtk/README_zh.md +167 -0
- package/temp/rtk/ROADMAP.md +15 -0
- package/temp/rtk/SECURITY.md +217 -0
- package/temp/rtk/TEST_EXEC_TIME.md +102 -0
- package/temp/rtk/build.rs +57 -0
- package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
- package/temp/rtk/docs/FEATURES.md +1410 -0
- package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
- package/temp/rtk/docs/filter-workflow.md +102 -0
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +583 -0
- package/temp/rtk/hooks/opencode-rtk.ts +39 -0
- package/temp/rtk/hooks/rtk-awareness.md +29 -0
- package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
- package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
- package/temp/rtk/install.sh +124 -0
- package/temp/rtk/release-please-config.json +10 -0
- package/temp/rtk/scripts/benchmark.sh +592 -0
- package/temp/rtk/scripts/check-installation.sh +162 -0
- package/temp/rtk/scripts/install-local.sh +37 -0
- package/temp/rtk/scripts/rtk-economics.sh +137 -0
- package/temp/rtk/scripts/test-all.sh +561 -0
- package/temp/rtk/scripts/test-aristote.sh +227 -0
- package/temp/rtk/scripts/test-tracking.sh +79 -0
- package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
- package/temp/rtk/scripts/validate-docs.sh +73 -0
- package/temp/rtk/src/aws_cmd.rs +880 -0
- package/temp/rtk/src/binlog.rs +1645 -0
- package/temp/rtk/src/cargo_cmd.rs +1727 -0
- package/temp/rtk/src/cc_economics.rs +1157 -0
- package/temp/rtk/src/ccusage.rs +340 -0
- package/temp/rtk/src/config.rs +187 -0
- package/temp/rtk/src/container.rs +855 -0
- package/temp/rtk/src/curl_cmd.rs +134 -0
- package/temp/rtk/src/deps.rs +268 -0
- package/temp/rtk/src/diff_cmd.rs +367 -0
- package/temp/rtk/src/discover/mod.rs +274 -0
- package/temp/rtk/src/discover/provider.rs +388 -0
- package/temp/rtk/src/discover/registry.rs +2022 -0
- package/temp/rtk/src/discover/report.rs +202 -0
- package/temp/rtk/src/discover/rules.rs +667 -0
- package/temp/rtk/src/display_helpers.rs +402 -0
- package/temp/rtk/src/dotnet_cmd.rs +1771 -0
- package/temp/rtk/src/dotnet_format_report.rs +133 -0
- package/temp/rtk/src/dotnet_trx.rs +593 -0
- package/temp/rtk/src/env_cmd.rs +204 -0
- package/temp/rtk/src/filter.rs +462 -0
- package/temp/rtk/src/filters/README.md +52 -0
- package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
- package/temp/rtk/src/filters/basedpyright.toml +47 -0
- package/temp/rtk/src/filters/biome.toml +45 -0
- package/temp/rtk/src/filters/brew-install.toml +37 -0
- package/temp/rtk/src/filters/composer-install.toml +40 -0
- package/temp/rtk/src/filters/df.toml +16 -0
- package/temp/rtk/src/filters/dotnet-build.toml +64 -0
- package/temp/rtk/src/filters/du.toml +16 -0
- package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
- package/temp/rtk/src/filters/gcc.toml +49 -0
- package/temp/rtk/src/filters/gcloud.toml +22 -0
- package/temp/rtk/src/filters/hadolint.toml +24 -0
- package/temp/rtk/src/filters/helm.toml +29 -0
- package/temp/rtk/src/filters/iptables.toml +27 -0
- package/temp/rtk/src/filters/jj.toml +28 -0
- package/temp/rtk/src/filters/jq.toml +24 -0
- package/temp/rtk/src/filters/make.toml +41 -0
- package/temp/rtk/src/filters/markdownlint.toml +24 -0
- package/temp/rtk/src/filters/mix-compile.toml +27 -0
- package/temp/rtk/src/filters/mix-format.toml +15 -0
- package/temp/rtk/src/filters/mvn-build.toml +44 -0
- package/temp/rtk/src/filters/oxlint.toml +43 -0
- package/temp/rtk/src/filters/ping.toml +63 -0
- package/temp/rtk/src/filters/pio-run.toml +40 -0
- package/temp/rtk/src/filters/poetry-install.toml +50 -0
- package/temp/rtk/src/filters/pre-commit.toml +35 -0
- package/temp/rtk/src/filters/ps.toml +16 -0
- package/temp/rtk/src/filters/quarto-render.toml +41 -0
- package/temp/rtk/src/filters/rsync.toml +48 -0
- package/temp/rtk/src/filters/shellcheck.toml +27 -0
- package/temp/rtk/src/filters/shopify-theme.toml +29 -0
- package/temp/rtk/src/filters/skopeo.toml +45 -0
- package/temp/rtk/src/filters/sops.toml +16 -0
- package/temp/rtk/src/filters/ssh.toml +44 -0
- package/temp/rtk/src/filters/stat.toml +34 -0
- package/temp/rtk/src/filters/swift-build.toml +41 -0
- package/temp/rtk/src/filters/systemctl-status.toml +33 -0
- package/temp/rtk/src/filters/terraform-plan.toml +35 -0
- package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
- package/temp/rtk/src/filters/tofu-init.toml +38 -0
- package/temp/rtk/src/filters/tofu-plan.toml +35 -0
- package/temp/rtk/src/filters/tofu-validate.toml +17 -0
- package/temp/rtk/src/filters/trunk-build.toml +39 -0
- package/temp/rtk/src/filters/ty.toml +50 -0
- package/temp/rtk/src/filters/uv-sync.toml +37 -0
- package/temp/rtk/src/filters/xcodebuild.toml +99 -0
- package/temp/rtk/src/filters/yamllint.toml +25 -0
- package/temp/rtk/src/find_cmd.rs +598 -0
- package/temp/rtk/src/format_cmd.rs +386 -0
- package/temp/rtk/src/gain.rs +723 -0
- package/temp/rtk/src/gh_cmd.rs +1651 -0
- package/temp/rtk/src/git.rs +2012 -0
- package/temp/rtk/src/go_cmd.rs +592 -0
- package/temp/rtk/src/golangci_cmd.rs +254 -0
- package/temp/rtk/src/grep_cmd.rs +288 -0
- package/temp/rtk/src/gt_cmd.rs +810 -0
- package/temp/rtk/src/hook_audit_cmd.rs +283 -0
- package/temp/rtk/src/hook_check.rs +171 -0
- package/temp/rtk/src/init.rs +1859 -0
- package/temp/rtk/src/integrity.rs +537 -0
- package/temp/rtk/src/json_cmd.rs +231 -0
- package/temp/rtk/src/learn/detector.rs +628 -0
- package/temp/rtk/src/learn/mod.rs +119 -0
- package/temp/rtk/src/learn/report.rs +184 -0
- package/temp/rtk/src/lint_cmd.rs +694 -0
- package/temp/rtk/src/local_llm.rs +316 -0
- package/temp/rtk/src/log_cmd.rs +248 -0
- package/temp/rtk/src/ls.rs +324 -0
- package/temp/rtk/src/main.rs +2482 -0
- package/temp/rtk/src/mypy_cmd.rs +389 -0
- package/temp/rtk/src/next_cmd.rs +241 -0
- package/temp/rtk/src/npm_cmd.rs +236 -0
- package/temp/rtk/src/parser/README.md +267 -0
- package/temp/rtk/src/parser/error.rs +46 -0
- package/temp/rtk/src/parser/formatter.rs +336 -0
- package/temp/rtk/src/parser/mod.rs +311 -0
- package/temp/rtk/src/parser/types.rs +119 -0
- package/temp/rtk/src/pip_cmd.rs +302 -0
- package/temp/rtk/src/playwright_cmd.rs +479 -0
- package/temp/rtk/src/pnpm_cmd.rs +573 -0
- package/temp/rtk/src/prettier_cmd.rs +221 -0
- package/temp/rtk/src/prisma_cmd.rs +482 -0
- package/temp/rtk/src/psql_cmd.rs +382 -0
- package/temp/rtk/src/pytest_cmd.rs +384 -0
- package/temp/rtk/src/read.rs +217 -0
- package/temp/rtk/src/rewrite_cmd.rs +50 -0
- package/temp/rtk/src/ruff_cmd.rs +402 -0
- package/temp/rtk/src/runner.rs +271 -0
- package/temp/rtk/src/summary.rs +297 -0
- package/temp/rtk/src/tee.rs +405 -0
- package/temp/rtk/src/telemetry.rs +248 -0
- package/temp/rtk/src/toml_filter.rs +1655 -0
- package/temp/rtk/src/tracking.rs +1416 -0
- package/temp/rtk/src/tree.rs +209 -0
- package/temp/rtk/src/tsc_cmd.rs +259 -0
- package/temp/rtk/src/utils.rs +432 -0
- package/temp/rtk/src/verify_cmd.rs +47 -0
- package/temp/rtk/src/vitest_cmd.rs +385 -0
- package/temp/rtk/src/wc_cmd.rs +401 -0
- package/temp/rtk/src/wget_cmd.rs +260 -0
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
|
@@ -0,0 +1,1727 @@
|
|
|
1
|
+
use crate::tracking;
|
|
2
|
+
use crate::utils::truncate;
|
|
3
|
+
use anyhow::{Context, Result};
|
|
4
|
+
use std::collections::HashMap;
|
|
5
|
+
use std::ffi::OsString;
|
|
6
|
+
use std::process::Command;
|
|
7
|
+
use std::sync::OnceLock;
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone)]
|
|
10
|
+
pub enum CargoCommand {
|
|
11
|
+
Build,
|
|
12
|
+
Test,
|
|
13
|
+
Clippy,
|
|
14
|
+
Check,
|
|
15
|
+
Install,
|
|
16
|
+
Nextest,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn run(cmd: CargoCommand, args: &[String], verbose: u8) -> Result<()> {
|
|
20
|
+
match cmd {
|
|
21
|
+
CargoCommand::Build => run_build(args, verbose),
|
|
22
|
+
CargoCommand::Test => run_test(args, verbose),
|
|
23
|
+
CargoCommand::Clippy => run_clippy(args, verbose),
|
|
24
|
+
CargoCommand::Check => run_check(args, verbose),
|
|
25
|
+
CargoCommand::Install => run_install(args, verbose),
|
|
26
|
+
CargoCommand::Nextest => run_nextest(args, verbose),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Reconstruct args with `--` separator preserved from the original command line.
|
|
31
|
+
/// Clap strips `--` from parsed args, but cargo subcommands need it to separate
|
|
32
|
+
/// their own flags from test runner flags (e.g. `cargo test -- --nocapture`).
|
|
33
|
+
fn restore_double_dash(args: &[String]) -> Vec<String> {
|
|
34
|
+
let raw_args: Vec<String> = std::env::args().collect();
|
|
35
|
+
restore_double_dash_with_raw(args, &raw_args)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Testable version that takes raw_args explicitly.
|
|
39
|
+
fn restore_double_dash_with_raw(args: &[String], raw_args: &[String]) -> Vec<String> {
|
|
40
|
+
if args.is_empty() {
|
|
41
|
+
return args.to_vec();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find `--` in the original command line
|
|
45
|
+
let sep_pos = match raw_args.iter().position(|a| a == "--") {
|
|
46
|
+
Some(pos) => pos,
|
|
47
|
+
None => return args.to_vec(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Count how many of our parsed args appeared before `--` in the original.
|
|
51
|
+
// Args before `--` are positional (e.g. test name), args after are flags.
|
|
52
|
+
let args_before_sep = raw_args[..sep_pos]
|
|
53
|
+
.iter()
|
|
54
|
+
.filter(|a| args.contains(a))
|
|
55
|
+
.count();
|
|
56
|
+
|
|
57
|
+
let mut result = Vec::with_capacity(args.len() + 1);
|
|
58
|
+
result.extend_from_slice(&args[..args_before_sep]);
|
|
59
|
+
result.push("--".to_string());
|
|
60
|
+
result.extend_from_slice(&args[args_before_sep..]);
|
|
61
|
+
result
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Generic cargo command runner with filtering
|
|
65
|
+
fn run_cargo_filtered<F>(subcommand: &str, args: &[String], verbose: u8, filter_fn: F) -> Result<()>
|
|
66
|
+
where
|
|
67
|
+
F: Fn(&str) -> String,
|
|
68
|
+
{
|
|
69
|
+
let timer = tracking::TimedExecution::start();
|
|
70
|
+
|
|
71
|
+
let mut cmd = Command::new("cargo");
|
|
72
|
+
cmd.arg(subcommand);
|
|
73
|
+
|
|
74
|
+
let restored_args = restore_double_dash(args);
|
|
75
|
+
for arg in &restored_args {
|
|
76
|
+
cmd.arg(arg);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if verbose > 0 {
|
|
80
|
+
eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" "));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let output = cmd
|
|
84
|
+
.output()
|
|
85
|
+
.with_context(|| format!("Failed to run cargo {}", subcommand))?;
|
|
86
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
87
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
88
|
+
let raw = format!("{}\n{}", stdout, stderr);
|
|
89
|
+
|
|
90
|
+
let exit_code = output
|
|
91
|
+
.status
|
|
92
|
+
.code()
|
|
93
|
+
.unwrap_or(if output.status.success() { 0 } else { 1 });
|
|
94
|
+
let filtered = filter_fn(&raw);
|
|
95
|
+
|
|
96
|
+
if let Some(hint) = crate::tee::tee_and_hint(&raw, &format!("cargo_{}", subcommand), exit_code)
|
|
97
|
+
{
|
|
98
|
+
println!("{}\n{}", filtered, hint);
|
|
99
|
+
} else {
|
|
100
|
+
println!("{}", filtered);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
timer.track(
|
|
104
|
+
&format!("cargo {} {}", subcommand, restored_args.join(" ")),
|
|
105
|
+
&format!("rtk cargo {} {}", subcommand, restored_args.join(" ")),
|
|
106
|
+
&raw,
|
|
107
|
+
&filtered,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if !output.status.success() {
|
|
111
|
+
std::process::exit(exit_code);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Ok(())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn run_build(args: &[String], verbose: u8) -> Result<()> {
|
|
118
|
+
run_cargo_filtered("build", args, verbose, filter_cargo_build)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn run_test(args: &[String], verbose: u8) -> Result<()> {
|
|
122
|
+
run_cargo_filtered("test", args, verbose, filter_cargo_test)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn run_clippy(args: &[String], verbose: u8) -> Result<()> {
|
|
126
|
+
run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn run_check(args: &[String], verbose: u8) -> Result<()> {
|
|
130
|
+
run_cargo_filtered("check", args, verbose, filter_cargo_build)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn run_install(args: &[String], verbose: u8) -> Result<()> {
|
|
134
|
+
run_cargo_filtered("install", args, verbose, filter_cargo_install)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn run_nextest(args: &[String], verbose: u8) -> Result<()> {
|
|
138
|
+
run_cargo_filtered("nextest", args, verbose, filter_cargo_nextest)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Format crate name + version into a display string
|
|
142
|
+
fn format_crate_info(name: &str, version: &str, fallback: &str) -> String {
|
|
143
|
+
if name.is_empty() {
|
|
144
|
+
fallback.to_string()
|
|
145
|
+
} else if version.is_empty() {
|
|
146
|
+
name.to_string()
|
|
147
|
+
} else {
|
|
148
|
+
format!("{} {}", name, version)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Filter cargo install output - strip dep compilation, keep installed/replaced/errors
|
|
153
|
+
fn filter_cargo_install(output: &str) -> String {
|
|
154
|
+
let mut errors: Vec<String> = Vec::new();
|
|
155
|
+
let mut error_count = 0;
|
|
156
|
+
let mut compiled = 0;
|
|
157
|
+
let mut in_error = false;
|
|
158
|
+
let mut current_error = Vec::new();
|
|
159
|
+
let mut installed_crate = String::new();
|
|
160
|
+
let mut installed_version = String::new();
|
|
161
|
+
let mut replaced_lines: Vec<String> = Vec::new();
|
|
162
|
+
let mut already_installed = false;
|
|
163
|
+
let mut ignored_line = String::new();
|
|
164
|
+
|
|
165
|
+
for line in output.lines() {
|
|
166
|
+
let trimmed = line.trim_start();
|
|
167
|
+
|
|
168
|
+
// Strip noise: dep compilation, downloading, locking, etc.
|
|
169
|
+
if trimmed.starts_with("Compiling") {
|
|
170
|
+
compiled += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if trimmed.starts_with("Downloading")
|
|
174
|
+
|| trimmed.starts_with("Downloaded")
|
|
175
|
+
|| trimmed.starts_with("Locking")
|
|
176
|
+
|| trimmed.starts_with("Updating")
|
|
177
|
+
|| trimmed.starts_with("Adding")
|
|
178
|
+
|| trimmed.starts_with("Finished")
|
|
179
|
+
|| trimmed.starts_with("Blocking waiting for file lock")
|
|
180
|
+
{
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Keep: Installing line (extract crate name + version)
|
|
185
|
+
if trimmed.starts_with("Installing") {
|
|
186
|
+
let rest = trimmed.strip_prefix("Installing").unwrap_or("").trim();
|
|
187
|
+
if !rest.is_empty() && !rest.starts_with('/') {
|
|
188
|
+
if let Some((name, version)) = rest.split_once(' ') {
|
|
189
|
+
installed_crate = name.to_string();
|
|
190
|
+
installed_version = version.to_string();
|
|
191
|
+
} else {
|
|
192
|
+
installed_crate = rest.to_string();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Keep: Installed line (extract crate + version if not already set)
|
|
199
|
+
if trimmed.starts_with("Installed") {
|
|
200
|
+
let rest = trimmed.strip_prefix("Installed").unwrap_or("").trim();
|
|
201
|
+
if !rest.is_empty() && installed_crate.is_empty() {
|
|
202
|
+
let mut parts = rest.split_whitespace();
|
|
203
|
+
if let (Some(name), Some(version)) = (parts.next(), parts.next()) {
|
|
204
|
+
installed_crate = name.to_string();
|
|
205
|
+
installed_version = version.to_string();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Keep: Replacing/Replaced lines
|
|
212
|
+
if trimmed.starts_with("Replacing") || trimmed.starts_with("Replaced") {
|
|
213
|
+
replaced_lines.push(trimmed.to_string());
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Keep: "Ignored package" (already up to date)
|
|
218
|
+
if trimmed.starts_with("Ignored package") {
|
|
219
|
+
already_installed = true;
|
|
220
|
+
ignored_line = trimmed.to_string();
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Keep: actionable warnings (e.g., "be sure to add `/path` to your PATH")
|
|
225
|
+
// Skip summary lines like "warning: `crate` generated N warnings"
|
|
226
|
+
if line.starts_with("warning:") {
|
|
227
|
+
if !(line.contains("generated") && line.contains("warning")) {
|
|
228
|
+
replaced_lines.push(line.to_string());
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Detect error blocks
|
|
234
|
+
if line.starts_with("error[") || line.starts_with("error:") {
|
|
235
|
+
if line.contains("aborting due to") || line.contains("could not compile") {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if in_error && !current_error.is_empty() {
|
|
239
|
+
errors.push(current_error.join("\n"));
|
|
240
|
+
current_error.clear();
|
|
241
|
+
}
|
|
242
|
+
error_count += 1;
|
|
243
|
+
in_error = true;
|
|
244
|
+
current_error.push(line.to_string());
|
|
245
|
+
} else if in_error {
|
|
246
|
+
if line.trim().is_empty() && current_error.len() > 3 {
|
|
247
|
+
errors.push(current_error.join("\n"));
|
|
248
|
+
current_error.clear();
|
|
249
|
+
in_error = false;
|
|
250
|
+
} else {
|
|
251
|
+
current_error.push(line.to_string());
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if !current_error.is_empty() {
|
|
257
|
+
errors.push(current_error.join("\n"));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Already installed / up to date
|
|
261
|
+
if already_installed {
|
|
262
|
+
let info = ignored_line.split('`').nth(1).unwrap_or(&ignored_line);
|
|
263
|
+
return format!("✓ cargo install: {} already installed", info);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Errors
|
|
267
|
+
if error_count > 0 {
|
|
268
|
+
let crate_info = format_crate_info(&installed_crate, &installed_version, "");
|
|
269
|
+
let deps_info = if compiled > 0 {
|
|
270
|
+
format!(", {} deps compiled", compiled)
|
|
271
|
+
} else {
|
|
272
|
+
String::new()
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
let mut result = String::new();
|
|
276
|
+
if crate_info.is_empty() {
|
|
277
|
+
result.push_str(&format!(
|
|
278
|
+
"cargo install: {} error{}{}\n",
|
|
279
|
+
error_count,
|
|
280
|
+
if error_count > 1 { "s" } else { "" },
|
|
281
|
+
deps_info
|
|
282
|
+
));
|
|
283
|
+
} else {
|
|
284
|
+
result.push_str(&format!(
|
|
285
|
+
"cargo install: {} error{} ({}{})\n",
|
|
286
|
+
error_count,
|
|
287
|
+
if error_count > 1 { "s" } else { "" },
|
|
288
|
+
crate_info,
|
|
289
|
+
deps_info
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
result.push_str("═══════════════════════════════════════\n");
|
|
293
|
+
|
|
294
|
+
for (i, err) in errors.iter().enumerate().take(15) {
|
|
295
|
+
result.push_str(err);
|
|
296
|
+
result.push('\n');
|
|
297
|
+
if i < errors.len() - 1 {
|
|
298
|
+
result.push('\n');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if errors.len() > 15 {
|
|
303
|
+
result.push_str(&format!("\n... +{} more issues\n", errors.len() - 15));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result.trim().to_string();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Success
|
|
310
|
+
let crate_info = format_crate_info(&installed_crate, &installed_version, "package");
|
|
311
|
+
|
|
312
|
+
let mut result = format!(
|
|
313
|
+
"✓ cargo install ({}, {} deps compiled)",
|
|
314
|
+
crate_info, compiled
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
for line in &replaced_lines {
|
|
318
|
+
result.push_str(&format!("\n {}", line));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
result
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// Push a completed failure block (header + body) into the failures list, then clear the buffers.
|
|
325
|
+
fn flush_failure_block(header: &mut String, body: &mut Vec<String>, failures: &mut Vec<String>) {
|
|
326
|
+
if header.is_empty() {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
let mut block = header.clone();
|
|
330
|
+
if !body.is_empty() {
|
|
331
|
+
block.push('\n');
|
|
332
|
+
block.push_str(&body.join("\n"));
|
|
333
|
+
}
|
|
334
|
+
failures.push(block);
|
|
335
|
+
header.clear();
|
|
336
|
+
body.clear();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// Filter cargo nextest output - show failures + compact summary
|
|
340
|
+
fn filter_cargo_nextest(output: &str) -> String {
|
|
341
|
+
static SUMMARY_RE: OnceLock<regex::Regex> = OnceLock::new();
|
|
342
|
+
let summary_re = SUMMARY_RE.get_or_init(|| {
|
|
343
|
+
regex::Regex::new(
|
|
344
|
+
r"Summary \[\s*([\d.]+)s\]\s+(\d+) tests? run:\s+(\d+) passed(?:,\s+(\d+) failed)?(?:,\s+(\d+) skipped)?"
|
|
345
|
+
).expect("invalid nextest summary regex")
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
static STARTING_RE: OnceLock<regex::Regex> = OnceLock::new();
|
|
349
|
+
let starting_re = STARTING_RE.get_or_init(|| {
|
|
350
|
+
regex::Regex::new(r"Starting \d+ tests? across (\d+) binar(?:y|ies)")
|
|
351
|
+
.expect("invalid nextest starting regex")
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
let mut failures: Vec<String> = Vec::new();
|
|
355
|
+
let mut in_failure_block = false;
|
|
356
|
+
let mut past_summary = false;
|
|
357
|
+
let mut current_failure_header = String::new();
|
|
358
|
+
let mut current_failure_body = Vec::new();
|
|
359
|
+
let mut summary_line = String::new();
|
|
360
|
+
let mut binaries: u32 = 0;
|
|
361
|
+
let mut has_cancel_line = false;
|
|
362
|
+
|
|
363
|
+
for line in output.lines() {
|
|
364
|
+
let trimmed = line.trim();
|
|
365
|
+
|
|
366
|
+
// Strip compilation noise
|
|
367
|
+
if trimmed.starts_with("Compiling")
|
|
368
|
+
|| trimmed.starts_with("Downloading")
|
|
369
|
+
|| trimmed.starts_with("Downloaded")
|
|
370
|
+
|| trimmed.starts_with("Finished")
|
|
371
|
+
|| trimmed.starts_with("Locking")
|
|
372
|
+
|| trimmed.starts_with("Updating")
|
|
373
|
+
{
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Strip separator lines (────)
|
|
378
|
+
if trimmed.starts_with("────") {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Skip post-summary recap lines (FAIL duplicates + "error: test run failed")
|
|
383
|
+
if past_summary {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Parse binary count from Starting line
|
|
388
|
+
if trimmed.starts_with("Starting") {
|
|
389
|
+
if let Some(caps) = starting_re.captures(trimmed) {
|
|
390
|
+
if let Some(m) = caps.get(1) {
|
|
391
|
+
binaries = m.as_str().parse().unwrap_or(0);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Strip PASS lines
|
|
398
|
+
if trimmed.starts_with("PASS") {
|
|
399
|
+
if in_failure_block {
|
|
400
|
+
flush_failure_block(
|
|
401
|
+
&mut current_failure_header,
|
|
402
|
+
&mut current_failure_body,
|
|
403
|
+
&mut failures,
|
|
404
|
+
);
|
|
405
|
+
in_failure_block = false;
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Detect FAIL lines
|
|
411
|
+
if trimmed.starts_with("FAIL") {
|
|
412
|
+
// Close previous failure block if any
|
|
413
|
+
if in_failure_block {
|
|
414
|
+
flush_failure_block(
|
|
415
|
+
&mut current_failure_header,
|
|
416
|
+
&mut current_failure_body,
|
|
417
|
+
&mut failures,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
current_failure_header = trimmed.to_string();
|
|
421
|
+
in_failure_block = true;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Cancellation notice
|
|
426
|
+
if trimmed.starts_with("Cancelling") || trimmed.starts_with("Canceling") {
|
|
427
|
+
has_cancel_line = true;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Nextest run ID line
|
|
432
|
+
if trimmed.starts_with("Nextest run ID") {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Parse summary
|
|
437
|
+
if trimmed.starts_with("Summary") {
|
|
438
|
+
summary_line = trimmed.to_string();
|
|
439
|
+
if in_failure_block {
|
|
440
|
+
flush_failure_block(
|
|
441
|
+
&mut current_failure_header,
|
|
442
|
+
&mut current_failure_body,
|
|
443
|
+
&mut failures,
|
|
444
|
+
);
|
|
445
|
+
in_failure_block = false;
|
|
446
|
+
}
|
|
447
|
+
past_summary = true;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Collect failure body lines (stdout/stderr sections)
|
|
452
|
+
if in_failure_block {
|
|
453
|
+
current_failure_body.push(line.to_string());
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Close last failure block
|
|
458
|
+
if in_failure_block {
|
|
459
|
+
flush_failure_block(
|
|
460
|
+
&mut current_failure_header,
|
|
461
|
+
&mut current_failure_body,
|
|
462
|
+
&mut failures,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Parse summary with regex
|
|
467
|
+
if let Some(caps) = summary_re.captures(&summary_line) {
|
|
468
|
+
let duration = caps.get(1).map_or("?", |m| m.as_str());
|
|
469
|
+
let passed: u32 = caps
|
|
470
|
+
.get(3)
|
|
471
|
+
.and_then(|m| m.as_str().parse().ok())
|
|
472
|
+
.unwrap_or(0);
|
|
473
|
+
let failed: u32 = caps
|
|
474
|
+
.get(4)
|
|
475
|
+
.and_then(|m| m.as_str().parse().ok())
|
|
476
|
+
.unwrap_or(0);
|
|
477
|
+
let skipped: u32 = caps
|
|
478
|
+
.get(5)
|
|
479
|
+
.and_then(|m| m.as_str().parse().ok())
|
|
480
|
+
.unwrap_or(0);
|
|
481
|
+
|
|
482
|
+
let binary_text = if binaries == 1 {
|
|
483
|
+
"1 binary".to_string()
|
|
484
|
+
} else if binaries > 1 {
|
|
485
|
+
format!("{} binaries", binaries)
|
|
486
|
+
} else {
|
|
487
|
+
String::new()
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
if failed == 0 {
|
|
491
|
+
// All pass - compact single line
|
|
492
|
+
let mut parts = vec![format!("{} passed", passed)];
|
|
493
|
+
if skipped > 0 {
|
|
494
|
+
parts.push(format!("{} skipped", skipped));
|
|
495
|
+
}
|
|
496
|
+
let meta = if binary_text.is_empty() {
|
|
497
|
+
format!("{}s", duration)
|
|
498
|
+
} else {
|
|
499
|
+
format!("{}, {}s", binary_text, duration)
|
|
500
|
+
};
|
|
501
|
+
return format!("✓ cargo nextest: {} ({})", parts.join(", "), meta);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// With failures - show failure details then summary
|
|
505
|
+
let mut result = String::new();
|
|
506
|
+
|
|
507
|
+
for failure in &failures {
|
|
508
|
+
result.push_str(failure);
|
|
509
|
+
result.push('\n');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if has_cancel_line {
|
|
513
|
+
result.push_str("Cancelling due to test failure\n");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let mut summary_parts = vec![format!("{} passed", passed)];
|
|
517
|
+
if failed > 0 {
|
|
518
|
+
summary_parts.push(format!("{} failed", failed));
|
|
519
|
+
}
|
|
520
|
+
if skipped > 0 {
|
|
521
|
+
summary_parts.push(format!("{} skipped", skipped));
|
|
522
|
+
}
|
|
523
|
+
let meta = if binary_text.is_empty() {
|
|
524
|
+
format!("{}s", duration)
|
|
525
|
+
} else {
|
|
526
|
+
format!("{}, {}s", binary_text, duration)
|
|
527
|
+
};
|
|
528
|
+
result.push_str(&format!(
|
|
529
|
+
"cargo nextest: {} ({})",
|
|
530
|
+
summary_parts.join(", "),
|
|
531
|
+
meta
|
|
532
|
+
));
|
|
533
|
+
|
|
534
|
+
return result.trim().to_string();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Fallback: if summary regex didn't match, show what we have
|
|
538
|
+
if !failures.is_empty() {
|
|
539
|
+
let mut result = String::new();
|
|
540
|
+
for failure in &failures {
|
|
541
|
+
result.push_str(failure);
|
|
542
|
+
result.push('\n');
|
|
543
|
+
}
|
|
544
|
+
if !summary_line.is_empty() {
|
|
545
|
+
result.push_str(&summary_line);
|
|
546
|
+
}
|
|
547
|
+
return result.trim().to_string();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if !summary_line.is_empty() {
|
|
551
|
+
return summary_line;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Empty or unrecognized
|
|
555
|
+
String::new()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/// Filter cargo build/check output - strip "Compiling"/"Checking" lines, keep errors + summary
|
|
559
|
+
fn filter_cargo_build(output: &str) -> String {
|
|
560
|
+
let mut errors: Vec<String> = Vec::new();
|
|
561
|
+
let mut warnings = 0;
|
|
562
|
+
let mut error_count = 0;
|
|
563
|
+
let mut compiled = 0;
|
|
564
|
+
let mut in_error = false;
|
|
565
|
+
let mut current_error = Vec::new();
|
|
566
|
+
|
|
567
|
+
for line in output.lines() {
|
|
568
|
+
if line.trim_start().starts_with("Compiling") || line.trim_start().starts_with("Checking") {
|
|
569
|
+
compiled += 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if line.trim_start().starts_with("Downloading")
|
|
573
|
+
|| line.trim_start().starts_with("Downloaded")
|
|
574
|
+
{
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if line.trim_start().starts_with("Finished") {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Detect error/warning blocks
|
|
582
|
+
if line.starts_with("error[") || line.starts_with("error:") {
|
|
583
|
+
// Skip "error: aborting due to" summary lines
|
|
584
|
+
if line.contains("aborting due to") || line.contains("could not compile") {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if in_error && !current_error.is_empty() {
|
|
588
|
+
errors.push(current_error.join("\n"));
|
|
589
|
+
current_error.clear();
|
|
590
|
+
}
|
|
591
|
+
error_count += 1;
|
|
592
|
+
in_error = true;
|
|
593
|
+
current_error.push(line.to_string());
|
|
594
|
+
} else if line.starts_with("warning:")
|
|
595
|
+
&& line.contains("generated")
|
|
596
|
+
&& line.contains("warning")
|
|
597
|
+
{
|
|
598
|
+
// "warning: `crate` generated N warnings" summary line
|
|
599
|
+
continue;
|
|
600
|
+
} else if line.starts_with("warning:") || line.starts_with("warning[") {
|
|
601
|
+
if in_error && !current_error.is_empty() {
|
|
602
|
+
errors.push(current_error.join("\n"));
|
|
603
|
+
current_error.clear();
|
|
604
|
+
}
|
|
605
|
+
warnings += 1;
|
|
606
|
+
in_error = true;
|
|
607
|
+
current_error.push(line.to_string());
|
|
608
|
+
} else if in_error {
|
|
609
|
+
if line.trim().is_empty() && current_error.len() > 3 {
|
|
610
|
+
errors.push(current_error.join("\n"));
|
|
611
|
+
current_error.clear();
|
|
612
|
+
in_error = false;
|
|
613
|
+
} else {
|
|
614
|
+
current_error.push(line.to_string());
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if !current_error.is_empty() {
|
|
620
|
+
errors.push(current_error.join("\n"));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if error_count == 0 && warnings == 0 {
|
|
624
|
+
return format!("✓ cargo build ({} crates compiled)", compiled);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let mut result = String::new();
|
|
628
|
+
result.push_str(&format!(
|
|
629
|
+
"cargo build: {} errors, {} warnings ({} crates)\n",
|
|
630
|
+
error_count, warnings, compiled
|
|
631
|
+
));
|
|
632
|
+
result.push_str("═══════════════════════════════════════\n");
|
|
633
|
+
|
|
634
|
+
for (i, err) in errors.iter().enumerate().take(15) {
|
|
635
|
+
result.push_str(err);
|
|
636
|
+
result.push('\n');
|
|
637
|
+
if i < errors.len() - 1 {
|
|
638
|
+
result.push('\n');
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if errors.len() > 15 {
|
|
643
|
+
result.push_str(&format!("\n... +{} more issues\n", errors.len() - 15));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
result.trim().to_string()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/// Aggregated test results for compact display
|
|
650
|
+
#[derive(Debug, Default, Clone)]
|
|
651
|
+
struct AggregatedTestResult {
|
|
652
|
+
passed: usize,
|
|
653
|
+
failed: usize,
|
|
654
|
+
ignored: usize,
|
|
655
|
+
measured: usize,
|
|
656
|
+
filtered_out: usize,
|
|
657
|
+
suites: usize,
|
|
658
|
+
duration_secs: f64,
|
|
659
|
+
has_duration: bool,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
impl AggregatedTestResult {
|
|
663
|
+
/// Parse a test result summary line
|
|
664
|
+
/// Format: "test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s"
|
|
665
|
+
fn parse_line(line: &str) -> Option<Self> {
|
|
666
|
+
static RE: OnceLock<regex::Regex> = OnceLock::new();
|
|
667
|
+
let re = RE.get_or_init(|| {
|
|
668
|
+
regex::Regex::new(
|
|
669
|
+
r"test result: (\w+)\.\s+(\d+) passed;\s+(\d+) failed;\s+(\d+) ignored;\s+(\d+) measured;\s+(\d+) filtered out(?:;\s+finished in ([\d.]+)s)?"
|
|
670
|
+
).unwrap()
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
let caps = re.captures(line)?;
|
|
674
|
+
let status = caps.get(1)?.as_str();
|
|
675
|
+
|
|
676
|
+
// Only aggregate if status is "ok" (all tests passed)
|
|
677
|
+
if status != "ok" {
|
|
678
|
+
return None;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let passed = caps.get(2)?.as_str().parse().ok()?;
|
|
682
|
+
let failed = caps.get(3)?.as_str().parse().ok()?;
|
|
683
|
+
let ignored = caps.get(4)?.as_str().parse().ok()?;
|
|
684
|
+
let measured = caps.get(5)?.as_str().parse().ok()?;
|
|
685
|
+
let filtered_out = caps.get(6)?.as_str().parse().ok()?;
|
|
686
|
+
|
|
687
|
+
let (duration_secs, has_duration) = if let Some(duration_match) = caps.get(7) {
|
|
688
|
+
(duration_match.as_str().parse().unwrap_or(0.0), true)
|
|
689
|
+
} else {
|
|
690
|
+
(0.0, false)
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
Some(Self {
|
|
694
|
+
passed,
|
|
695
|
+
failed,
|
|
696
|
+
ignored,
|
|
697
|
+
measured,
|
|
698
|
+
filtered_out,
|
|
699
|
+
suites: 1,
|
|
700
|
+
duration_secs,
|
|
701
|
+
has_duration,
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/// Merge another test result into this one
|
|
706
|
+
fn merge(&mut self, other: &Self) {
|
|
707
|
+
self.passed += other.passed;
|
|
708
|
+
self.failed += other.failed;
|
|
709
|
+
self.ignored += other.ignored;
|
|
710
|
+
self.measured += other.measured;
|
|
711
|
+
self.filtered_out += other.filtered_out;
|
|
712
|
+
self.suites += other.suites;
|
|
713
|
+
self.duration_secs += other.duration_secs;
|
|
714
|
+
self.has_duration = self.has_duration && other.has_duration;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/// Format as compact single line
|
|
718
|
+
fn format_compact(&self) -> String {
|
|
719
|
+
let mut parts = vec![format!("{} passed", self.passed)];
|
|
720
|
+
|
|
721
|
+
if self.ignored > 0 {
|
|
722
|
+
parts.push(format!("{} ignored", self.ignored));
|
|
723
|
+
}
|
|
724
|
+
if self.filtered_out > 0 {
|
|
725
|
+
parts.push(format!("{} filtered out", self.filtered_out));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let counts = parts.join(", ");
|
|
729
|
+
|
|
730
|
+
let suite_text = if self.suites == 1 {
|
|
731
|
+
"1 suite".to_string()
|
|
732
|
+
} else {
|
|
733
|
+
format!("{} suites", self.suites)
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
if self.has_duration {
|
|
737
|
+
format!(
|
|
738
|
+
"✓ cargo test: {} ({}, {:.2}s)",
|
|
739
|
+
counts, suite_text, self.duration_secs
|
|
740
|
+
)
|
|
741
|
+
} else {
|
|
742
|
+
format!("✓ cargo test: {} ({})", counts, suite_text)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// Filter cargo test output - show failures + summary only
|
|
748
|
+
fn filter_cargo_test(output: &str) -> String {
|
|
749
|
+
let mut failures: Vec<String> = Vec::new();
|
|
750
|
+
let mut summary_lines: Vec<String> = Vec::new();
|
|
751
|
+
let mut in_failure_section = false;
|
|
752
|
+
let mut current_failure = Vec::new();
|
|
753
|
+
|
|
754
|
+
for line in output.lines() {
|
|
755
|
+
// Skip compilation lines
|
|
756
|
+
if line.trim_start().starts_with("Compiling")
|
|
757
|
+
|| line.trim_start().starts_with("Downloading")
|
|
758
|
+
|| line.trim_start().starts_with("Downloaded")
|
|
759
|
+
|| line.trim_start().starts_with("Finished")
|
|
760
|
+
{
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Skip "running N tests" and individual "test ... ok" lines
|
|
765
|
+
if line.starts_with("running ") || (line.starts_with("test ") && line.ends_with("... ok")) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Detect failures section
|
|
770
|
+
if line == "failures:" {
|
|
771
|
+
in_failure_section = true;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if in_failure_section {
|
|
776
|
+
if line.starts_with("test result:") {
|
|
777
|
+
in_failure_section = false;
|
|
778
|
+
summary_lines.push(line.to_string());
|
|
779
|
+
} else if line.starts_with(" ") || line.starts_with("---- ") {
|
|
780
|
+
current_failure.push(line.to_string());
|
|
781
|
+
} else if line.trim().is_empty() && !current_failure.is_empty() {
|
|
782
|
+
failures.push(current_failure.join("\n"));
|
|
783
|
+
current_failure.clear();
|
|
784
|
+
} else if !line.trim().is_empty() {
|
|
785
|
+
current_failure.push(line.to_string());
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Capture test result summary
|
|
790
|
+
if !in_failure_section && line.starts_with("test result:") {
|
|
791
|
+
summary_lines.push(line.to_string());
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if !current_failure.is_empty() {
|
|
796
|
+
failures.push(current_failure.join("\n"));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let mut result = String::new();
|
|
800
|
+
|
|
801
|
+
if failures.is_empty() && !summary_lines.is_empty() {
|
|
802
|
+
// All passed - try to aggregate
|
|
803
|
+
let mut aggregated: Option<AggregatedTestResult> = None;
|
|
804
|
+
let mut all_parsed = true;
|
|
805
|
+
|
|
806
|
+
for line in &summary_lines {
|
|
807
|
+
if let Some(parsed) = AggregatedTestResult::parse_line(line) {
|
|
808
|
+
if let Some(ref mut agg) = aggregated {
|
|
809
|
+
agg.merge(&parsed);
|
|
810
|
+
} else {
|
|
811
|
+
aggregated = Some(parsed);
|
|
812
|
+
}
|
|
813
|
+
} else {
|
|
814
|
+
all_parsed = false;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// If all lines parsed successfully and we have at least one suite, return compact format
|
|
820
|
+
if all_parsed {
|
|
821
|
+
if let Some(agg) = aggregated {
|
|
822
|
+
if agg.suites > 0 {
|
|
823
|
+
return agg.format_compact();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Fallback: use original behavior if regex failed
|
|
829
|
+
for line in &summary_lines {
|
|
830
|
+
result.push_str(&format!("✓ {}\n", line));
|
|
831
|
+
}
|
|
832
|
+
return result.trim().to_string();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if !failures.is_empty() {
|
|
836
|
+
result.push_str(&format!("FAILURES ({}):\n", failures.len()));
|
|
837
|
+
result.push_str("═══════════════════════════════════════\n");
|
|
838
|
+
for (i, failure) in failures.iter().enumerate().take(10) {
|
|
839
|
+
result.push_str(&format!("{}. {}\n", i + 1, truncate(failure, 200)));
|
|
840
|
+
}
|
|
841
|
+
if failures.len() > 10 {
|
|
842
|
+
result.push_str(&format!("\n... +{} more failures\n", failures.len() - 10));
|
|
843
|
+
}
|
|
844
|
+
result.push('\n');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
for line in &summary_lines {
|
|
848
|
+
result.push_str(&format!("{}\n", line));
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if result.trim().is_empty() {
|
|
852
|
+
// Fallback: show last meaningful lines
|
|
853
|
+
let meaningful: Vec<&str> = output
|
|
854
|
+
.lines()
|
|
855
|
+
.filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling"))
|
|
856
|
+
.collect();
|
|
857
|
+
for line in meaningful.iter().rev().take(5).rev() {
|
|
858
|
+
result.push_str(&format!("{}\n", line));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
result.trim().to_string()
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/// Filter cargo clippy output - group warnings by lint rule
|
|
866
|
+
fn filter_cargo_clippy(output: &str) -> String {
|
|
867
|
+
let mut by_rule: HashMap<String, Vec<String>> = HashMap::new();
|
|
868
|
+
let mut error_count = 0;
|
|
869
|
+
let mut warning_count = 0;
|
|
870
|
+
|
|
871
|
+
// Parse clippy output lines
|
|
872
|
+
// Format: "warning: description\n --> file:line:col\n |\n | code\n"
|
|
873
|
+
let mut current_rule = String::new();
|
|
874
|
+
|
|
875
|
+
for line in output.lines() {
|
|
876
|
+
// Skip compilation lines
|
|
877
|
+
if line.trim_start().starts_with("Compiling")
|
|
878
|
+
|| line.trim_start().starts_with("Checking")
|
|
879
|
+
|| line.trim_start().starts_with("Downloading")
|
|
880
|
+
|| line.trim_start().starts_with("Downloaded")
|
|
881
|
+
|| line.trim_start().starts_with("Finished")
|
|
882
|
+
{
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// "warning: unused variable [unused_variables]" or "warning: description [clippy::rule_name]"
|
|
887
|
+
if (line.starts_with("warning:") || line.starts_with("warning["))
|
|
888
|
+
|| (line.starts_with("error:") || line.starts_with("error["))
|
|
889
|
+
{
|
|
890
|
+
// Skip summary lines: "warning: `rtk` (bin) generated 5 warnings"
|
|
891
|
+
if line.contains("generated") && line.contains("warning") {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
// Skip "error: aborting" / "error: could not compile"
|
|
895
|
+
if line.contains("aborting due to") || line.contains("could not compile") {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
let is_error = line.starts_with("error");
|
|
900
|
+
if is_error {
|
|
901
|
+
error_count += 1;
|
|
902
|
+
} else {
|
|
903
|
+
warning_count += 1;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Extract rule name from brackets
|
|
907
|
+
current_rule = if let Some(bracket_start) = line.rfind('[') {
|
|
908
|
+
if let Some(bracket_end) = line.rfind(']') {
|
|
909
|
+
line[bracket_start + 1..bracket_end].to_string()
|
|
910
|
+
} else {
|
|
911
|
+
line.to_string()
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
// No bracket: use the message itself as the rule
|
|
915
|
+
let prefix = if is_error { "error: " } else { "warning: " };
|
|
916
|
+
line.strip_prefix(prefix).unwrap_or(line).to_string()
|
|
917
|
+
};
|
|
918
|
+
} else if line.trim_start().starts_with("--> ") {
|
|
919
|
+
let location = line.trim_start().trim_start_matches("--> ").to_string();
|
|
920
|
+
if !current_rule.is_empty() {
|
|
921
|
+
by_rule
|
|
922
|
+
.entry(current_rule.clone())
|
|
923
|
+
.or_default()
|
|
924
|
+
.push(location);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if error_count == 0 && warning_count == 0 {
|
|
930
|
+
return "✓ cargo clippy: No issues found".to_string();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
let mut result = String::new();
|
|
934
|
+
result.push_str(&format!(
|
|
935
|
+
"cargo clippy: {} errors, {} warnings\n",
|
|
936
|
+
error_count, warning_count
|
|
937
|
+
));
|
|
938
|
+
result.push_str("═══════════════════════════════════════\n");
|
|
939
|
+
|
|
940
|
+
// Sort rules by frequency
|
|
941
|
+
let mut rule_counts: Vec<_> = by_rule.iter().collect();
|
|
942
|
+
rule_counts.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
|
|
943
|
+
|
|
944
|
+
for (rule, locations) in rule_counts.iter().take(15) {
|
|
945
|
+
result.push_str(&format!(" {} ({}x)\n", rule, locations.len()));
|
|
946
|
+
for loc in locations.iter().take(3) {
|
|
947
|
+
result.push_str(&format!(" {}\n", loc));
|
|
948
|
+
}
|
|
949
|
+
if locations.len() > 3 {
|
|
950
|
+
result.push_str(&format!(" ... +{} more\n", locations.len() - 3));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if by_rule.len() > 15 {
|
|
955
|
+
result.push_str(&format!("\n... +{} more rules\n", by_rule.len() - 15));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
result.trim().to_string()
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/// Runs an unsupported cargo subcommand by passing it through directly
|
|
962
|
+
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> {
|
|
963
|
+
let timer = tracking::TimedExecution::start();
|
|
964
|
+
|
|
965
|
+
if verbose > 0 {
|
|
966
|
+
eprintln!("cargo passthrough: {:?}", args);
|
|
967
|
+
}
|
|
968
|
+
let status = Command::new("cargo")
|
|
969
|
+
.args(args)
|
|
970
|
+
.status()
|
|
971
|
+
.context("Failed to run cargo")?;
|
|
972
|
+
|
|
973
|
+
let args_str = tracking::args_display(args);
|
|
974
|
+
timer.track_passthrough(
|
|
975
|
+
&format!("cargo {}", args_str),
|
|
976
|
+
&format!("rtk cargo {} (passthrough)", args_str),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
if !status.success() {
|
|
980
|
+
std::process::exit(status.code().unwrap_or(1));
|
|
981
|
+
}
|
|
982
|
+
Ok(())
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
#[cfg(test)]
|
|
986
|
+
mod tests {
|
|
987
|
+
use super::*;
|
|
988
|
+
|
|
989
|
+
#[test]
|
|
990
|
+
fn test_restore_double_dash_with_separator() {
|
|
991
|
+
// rtk cargo test -- --nocapture → clap gives ["--nocapture"]
|
|
992
|
+
let args: Vec<String> = vec!["--nocapture".into()];
|
|
993
|
+
let raw = vec![
|
|
994
|
+
"rtk".into(),
|
|
995
|
+
"cargo".into(),
|
|
996
|
+
"test".into(),
|
|
997
|
+
"--".into(),
|
|
998
|
+
"--nocapture".into(),
|
|
999
|
+
];
|
|
1000
|
+
let result = restore_double_dash_with_raw(&args, &raw);
|
|
1001
|
+
assert_eq!(result, vec!["--", "--nocapture"]);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
#[test]
|
|
1005
|
+
fn test_restore_double_dash_with_test_name() {
|
|
1006
|
+
// rtk cargo test my_test -- --nocapture → clap gives ["my_test", "--nocapture"]
|
|
1007
|
+
let args: Vec<String> = vec!["my_test".into(), "--nocapture".into()];
|
|
1008
|
+
let raw = vec![
|
|
1009
|
+
"rtk".into(),
|
|
1010
|
+
"cargo".into(),
|
|
1011
|
+
"test".into(),
|
|
1012
|
+
"my_test".into(),
|
|
1013
|
+
"--".into(),
|
|
1014
|
+
"--nocapture".into(),
|
|
1015
|
+
];
|
|
1016
|
+
let result = restore_double_dash_with_raw(&args, &raw);
|
|
1017
|
+
assert_eq!(result, vec!["my_test", "--", "--nocapture"]);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[test]
|
|
1021
|
+
fn test_restore_double_dash_without_separator() {
|
|
1022
|
+
// rtk cargo test my_test → no --, args unchanged
|
|
1023
|
+
let args: Vec<String> = vec!["my_test".into()];
|
|
1024
|
+
let raw = vec![
|
|
1025
|
+
"rtk".into(),
|
|
1026
|
+
"cargo".into(),
|
|
1027
|
+
"test".into(),
|
|
1028
|
+
"my_test".into(),
|
|
1029
|
+
];
|
|
1030
|
+
let result = restore_double_dash_with_raw(&args, &raw);
|
|
1031
|
+
assert_eq!(result, vec!["my_test"]);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
#[test]
|
|
1035
|
+
fn test_restore_double_dash_empty_args() {
|
|
1036
|
+
let args: Vec<String> = vec![];
|
|
1037
|
+
let raw = vec!["rtk".into(), "cargo".into(), "test".into()];
|
|
1038
|
+
let result = restore_double_dash_with_raw(&args, &raw);
|
|
1039
|
+
assert!(result.is_empty());
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
#[test]
|
|
1043
|
+
fn test_restore_double_dash_clippy() {
|
|
1044
|
+
// rtk cargo clippy -- -D warnings → clap gives ["-D", "warnings"]
|
|
1045
|
+
let args: Vec<String> = vec!["-D".into(), "warnings".into()];
|
|
1046
|
+
let raw = vec![
|
|
1047
|
+
"rtk".into(),
|
|
1048
|
+
"cargo".into(),
|
|
1049
|
+
"clippy".into(),
|
|
1050
|
+
"--".into(),
|
|
1051
|
+
"-D".into(),
|
|
1052
|
+
"warnings".into(),
|
|
1053
|
+
];
|
|
1054
|
+
let result = restore_double_dash_with_raw(&args, &raw);
|
|
1055
|
+
assert_eq!(result, vec!["--", "-D", "warnings"]);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
#[test]
|
|
1059
|
+
fn test_filter_cargo_build_success() {
|
|
1060
|
+
let output = r#" Compiling libc v0.2.153
|
|
1061
|
+
Compiling cfg-if v1.0.0
|
|
1062
|
+
Compiling rtk v0.5.0
|
|
1063
|
+
Finished dev [unoptimized + debuginfo] target(s) in 15.23s
|
|
1064
|
+
"#;
|
|
1065
|
+
let result = filter_cargo_build(output);
|
|
1066
|
+
assert!(result.contains("✓ cargo build"));
|
|
1067
|
+
assert!(result.contains("3 crates compiled"));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
#[test]
|
|
1071
|
+
fn test_filter_cargo_build_errors() {
|
|
1072
|
+
let output = r#" Compiling rtk v0.5.0
|
|
1073
|
+
error[E0308]: mismatched types
|
|
1074
|
+
--> src/main.rs:10:5
|
|
1075
|
+
|
|
|
1076
|
+
10| "hello"
|
|
1077
|
+
| ^^^^^^^ expected `i32`, found `&str`
|
|
1078
|
+
|
|
1079
|
+
error: aborting due to 1 previous error
|
|
1080
|
+
"#;
|
|
1081
|
+
let result = filter_cargo_build(output);
|
|
1082
|
+
assert!(result.contains("1 errors"));
|
|
1083
|
+
assert!(result.contains("E0308"));
|
|
1084
|
+
assert!(result.contains("mismatched types"));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
#[test]
|
|
1088
|
+
fn test_filter_cargo_test_all_pass() {
|
|
1089
|
+
let output = r#" Compiling rtk v0.5.0
|
|
1090
|
+
Finished test [unoptimized + debuginfo] target(s) in 2.53s
|
|
1091
|
+
Running target/debug/deps/rtk-abc123
|
|
1092
|
+
|
|
1093
|
+
running 15 tests
|
|
1094
|
+
test utils::tests::test_truncate_short_string ... ok
|
|
1095
|
+
test utils::tests::test_truncate_long_string ... ok
|
|
1096
|
+
test utils::tests::test_strip_ansi_simple ... ok
|
|
1097
|
+
|
|
1098
|
+
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
|
|
1099
|
+
"#;
|
|
1100
|
+
let result = filter_cargo_test(output);
|
|
1101
|
+
assert!(
|
|
1102
|
+
result.contains("✓ cargo test: 15 passed (1 suite, 0.01s)"),
|
|
1103
|
+
"Expected compact format, got: {}",
|
|
1104
|
+
result
|
|
1105
|
+
);
|
|
1106
|
+
assert!(!result.contains("Compiling"));
|
|
1107
|
+
assert!(!result.contains("test utils"));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
#[test]
|
|
1111
|
+
fn test_filter_cargo_test_failures() {
|
|
1112
|
+
let output = r#"running 5 tests
|
|
1113
|
+
test foo::test_a ... ok
|
|
1114
|
+
test foo::test_b ... FAILED
|
|
1115
|
+
test foo::test_c ... ok
|
|
1116
|
+
|
|
1117
|
+
failures:
|
|
1118
|
+
|
|
1119
|
+
---- foo::test_b stdout ----
|
|
1120
|
+
thread 'foo::test_b' panicked at 'assert_eq!(1, 2)'
|
|
1121
|
+
|
|
1122
|
+
failures:
|
|
1123
|
+
foo::test_b
|
|
1124
|
+
|
|
1125
|
+
test result: FAILED. 4 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
|
1126
|
+
"#;
|
|
1127
|
+
let result = filter_cargo_test(output);
|
|
1128
|
+
assert!(result.contains("FAILURES"));
|
|
1129
|
+
assert!(result.contains("test_b"));
|
|
1130
|
+
assert!(result.contains("test result:"));
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
#[test]
|
|
1134
|
+
fn test_filter_cargo_test_multi_suite_all_pass() {
|
|
1135
|
+
let output = r#" Compiling rtk v0.5.0
|
|
1136
|
+
Finished test [unoptimized + debuginfo] target(s) in 2.53s
|
|
1137
|
+
Running unittests src/lib.rs (target/debug/deps/rtk-abc123)
|
|
1138
|
+
|
|
1139
|
+
running 50 tests
|
|
1140
|
+
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
|
|
1141
|
+
|
|
1142
|
+
Running unittests src/main.rs (target/debug/deps/rtk-def456)
|
|
1143
|
+
|
|
1144
|
+
running 30 tests
|
|
1145
|
+
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
|
|
1146
|
+
|
|
1147
|
+
Running tests/integration.rs (target/debug/deps/integration-ghi789)
|
|
1148
|
+
|
|
1149
|
+
running 25 tests
|
|
1150
|
+
test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
|
|
1151
|
+
|
|
1152
|
+
Doc-tests rtk
|
|
1153
|
+
|
|
1154
|
+
running 32 tests
|
|
1155
|
+
test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
|
|
1156
|
+
"#;
|
|
1157
|
+
let result = filter_cargo_test(output);
|
|
1158
|
+
assert!(
|
|
1159
|
+
result.contains("✓ cargo test: 137 passed (4 suites, 1.45s)"),
|
|
1160
|
+
"Expected aggregated format, got: {}",
|
|
1161
|
+
result
|
|
1162
|
+
);
|
|
1163
|
+
assert!(!result.contains("running"));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[test]
|
|
1167
|
+
fn test_filter_cargo_test_multi_suite_with_failures() {
|
|
1168
|
+
let output = r#" Running unittests src/lib.rs
|
|
1169
|
+
|
|
1170
|
+
running 20 tests
|
|
1171
|
+
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
|
|
1172
|
+
|
|
1173
|
+
Running unittests src/main.rs
|
|
1174
|
+
|
|
1175
|
+
running 15 tests
|
|
1176
|
+
test foo::test_bad ... FAILED
|
|
1177
|
+
|
|
1178
|
+
failures:
|
|
1179
|
+
|
|
1180
|
+
---- foo::test_bad stdout ----
|
|
1181
|
+
thread panicked at 'assertion failed'
|
|
1182
|
+
|
|
1183
|
+
test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
|
|
1184
|
+
|
|
1185
|
+
Running tests/integration.rs
|
|
1186
|
+
|
|
1187
|
+
running 10 tests
|
|
1188
|
+
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
|
|
1189
|
+
"#;
|
|
1190
|
+
let result = filter_cargo_test(output);
|
|
1191
|
+
// Should NOT aggregate when there are failures
|
|
1192
|
+
assert!(result.contains("FAILURES"), "got: {}", result);
|
|
1193
|
+
assert!(result.contains("test_bad"), "got: {}", result);
|
|
1194
|
+
assert!(result.contains("test result:"), "got: {}", result);
|
|
1195
|
+
// Should show individual summaries
|
|
1196
|
+
assert!(result.contains("20 passed"), "got: {}", result);
|
|
1197
|
+
assert!(result.contains("14 passed"), "got: {}", result);
|
|
1198
|
+
assert!(result.contains("10 passed"), "got: {}", result);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
#[test]
|
|
1202
|
+
fn test_filter_cargo_test_all_suites_zero_tests() {
|
|
1203
|
+
let output = r#" Running unittests src/empty1.rs
|
|
1204
|
+
|
|
1205
|
+
running 0 tests
|
|
1206
|
+
|
|
1207
|
+
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
|
1208
|
+
|
|
1209
|
+
Running unittests src/empty2.rs
|
|
1210
|
+
|
|
1211
|
+
running 0 tests
|
|
1212
|
+
|
|
1213
|
+
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
|
1214
|
+
|
|
1215
|
+
Running tests/empty3.rs
|
|
1216
|
+
|
|
1217
|
+
running 0 tests
|
|
1218
|
+
|
|
1219
|
+
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
|
1220
|
+
"#;
|
|
1221
|
+
let result = filter_cargo_test(output);
|
|
1222
|
+
assert!(
|
|
1223
|
+
result.contains("✓ cargo test: 0 passed (3 suites, 0.00s)"),
|
|
1224
|
+
"Expected compact format for zero tests, got: {}",
|
|
1225
|
+
result
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
#[test]
|
|
1230
|
+
fn test_filter_cargo_test_with_ignored_and_filtered() {
|
|
1231
|
+
let output = r#" Running unittests src/lib.rs
|
|
1232
|
+
|
|
1233
|
+
running 50 tests
|
|
1234
|
+
test result: ok. 45 passed; 0 failed; 3 ignored; 0 measured; 2 filtered out; finished in 0.50s
|
|
1235
|
+
|
|
1236
|
+
Running tests/integration.rs
|
|
1237
|
+
|
|
1238
|
+
running 20 tests
|
|
1239
|
+
test result: ok. 18 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.20s
|
|
1240
|
+
"#;
|
|
1241
|
+
let result = filter_cargo_test(output);
|
|
1242
|
+
assert!(
|
|
1243
|
+
result.contains("✓ cargo test: 63 passed, 5 ignored, 2 filtered out (2 suites, 0.70s)"),
|
|
1244
|
+
"Expected compact format with ignored and filtered, got: {}",
|
|
1245
|
+
result
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
#[test]
|
|
1250
|
+
fn test_filter_cargo_test_single_suite_compact() {
|
|
1251
|
+
let output = r#" Running unittests src/main.rs
|
|
1252
|
+
|
|
1253
|
+
running 15 tests
|
|
1254
|
+
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
|
|
1255
|
+
"#;
|
|
1256
|
+
let result = filter_cargo_test(output);
|
|
1257
|
+
assert!(
|
|
1258
|
+
result.contains("✓ cargo test: 15 passed (1 suite, 0.01s)"),
|
|
1259
|
+
"Expected singular 'suite', got: {}",
|
|
1260
|
+
result
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
#[test]
|
|
1265
|
+
fn test_filter_cargo_test_regex_fallback() {
|
|
1266
|
+
let output = r#" Running unittests src/main.rs
|
|
1267
|
+
|
|
1268
|
+
running 15 tests
|
|
1269
|
+
test result: MALFORMED LINE WITHOUT PROPER FORMAT
|
|
1270
|
+
"#;
|
|
1271
|
+
let result = filter_cargo_test(output);
|
|
1272
|
+
// Should fallback to original behavior (show line with checkmark)
|
|
1273
|
+
assert!(
|
|
1274
|
+
result.contains("✓ test result: MALFORMED"),
|
|
1275
|
+
"Expected fallback format, got: {}",
|
|
1276
|
+
result
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
#[test]
|
|
1281
|
+
fn test_filter_cargo_clippy_clean() {
|
|
1282
|
+
let output = r#" Checking rtk v0.5.0
|
|
1283
|
+
Finished dev [unoptimized + debuginfo] target(s) in 1.53s
|
|
1284
|
+
"#;
|
|
1285
|
+
let result = filter_cargo_clippy(output);
|
|
1286
|
+
assert!(result.contains("✓ cargo clippy: No issues found"));
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
#[test]
|
|
1290
|
+
fn test_filter_cargo_clippy_warnings() {
|
|
1291
|
+
let output = r#" Checking rtk v0.5.0
|
|
1292
|
+
warning: unused variable: `x` [unused_variables]
|
|
1293
|
+
--> src/main.rs:10:9
|
|
1294
|
+
|
|
|
1295
|
+
10| let x = 5;
|
|
1296
|
+
| ^ help: if this is intentional, prefix it with an underscore: `_x`
|
|
1297
|
+
|
|
1298
|
+
warning: this function has too many arguments [clippy::too_many_arguments]
|
|
1299
|
+
--> src/git.rs:16:1
|
|
1300
|
+
|
|
|
1301
|
+
16| pub fn run(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32) {}
|
|
1302
|
+
|
|
|
1303
|
+
|
|
1304
|
+
warning: `rtk` (bin) generated 2 warnings
|
|
1305
|
+
Finished dev [unoptimized + debuginfo] target(s) in 1.53s
|
|
1306
|
+
"#;
|
|
1307
|
+
let result = filter_cargo_clippy(output);
|
|
1308
|
+
assert!(result.contains("0 errors, 2 warnings"));
|
|
1309
|
+
assert!(result.contains("unused_variables"));
|
|
1310
|
+
assert!(result.contains("clippy::too_many_arguments"));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
#[test]
|
|
1314
|
+
fn test_filter_cargo_install_success() {
|
|
1315
|
+
let output = r#" Installing rtk v0.11.0
|
|
1316
|
+
Downloading crates ...
|
|
1317
|
+
Downloaded anyhow v1.0.80
|
|
1318
|
+
Downloaded clap v4.5.0
|
|
1319
|
+
Compiling libc v0.2.153
|
|
1320
|
+
Compiling cfg-if v1.0.0
|
|
1321
|
+
Compiling anyhow v1.0.80
|
|
1322
|
+
Compiling clap v4.5.0
|
|
1323
|
+
Compiling rtk v0.11.0
|
|
1324
|
+
Finished `release` profile [optimized] target(s) in 45.23s
|
|
1325
|
+
Replacing /Users/user/.cargo/bin/rtk
|
|
1326
|
+
Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
|
|
1327
|
+
"#;
|
|
1328
|
+
let result = filter_cargo_install(output);
|
|
1329
|
+
assert!(result.contains("✓ cargo install"), "got: {}", result);
|
|
1330
|
+
assert!(result.contains("rtk v0.11.0"), "got: {}", result);
|
|
1331
|
+
assert!(result.contains("5 deps compiled"), "got: {}", result);
|
|
1332
|
+
assert!(result.contains("Replaced"), "got: {}", result);
|
|
1333
|
+
assert!(!result.contains("Compiling"), "got: {}", result);
|
|
1334
|
+
assert!(!result.contains("Downloading"), "got: {}", result);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
#[test]
|
|
1338
|
+
fn test_filter_cargo_install_replace() {
|
|
1339
|
+
let output = r#" Installing rtk v0.11.0
|
|
1340
|
+
Compiling rtk v0.11.0
|
|
1341
|
+
Finished `release` profile [optimized] target(s) in 10.0s
|
|
1342
|
+
Replacing /Users/user/.cargo/bin/rtk
|
|
1343
|
+
Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
|
|
1344
|
+
"#;
|
|
1345
|
+
let result = filter_cargo_install(output);
|
|
1346
|
+
assert!(result.contains("✓ cargo install"), "got: {}", result);
|
|
1347
|
+
assert!(result.contains("Replacing"), "got: {}", result);
|
|
1348
|
+
assert!(result.contains("Replaced"), "got: {}", result);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
#[test]
|
|
1352
|
+
fn test_filter_cargo_install_error() {
|
|
1353
|
+
let output = r#" Installing rtk v0.11.0
|
|
1354
|
+
Compiling rtk v0.11.0
|
|
1355
|
+
error[E0308]: mismatched types
|
|
1356
|
+
--> src/main.rs:10:5
|
|
1357
|
+
|
|
|
1358
|
+
10| "hello"
|
|
1359
|
+
| ^^^^^^^ expected `i32`, found `&str`
|
|
1360
|
+
|
|
1361
|
+
error: aborting due to 1 previous error
|
|
1362
|
+
"#;
|
|
1363
|
+
let result = filter_cargo_install(output);
|
|
1364
|
+
assert!(result.contains("cargo install: 1 error"), "got: {}", result);
|
|
1365
|
+
assert!(result.contains("E0308"), "got: {}", result);
|
|
1366
|
+
assert!(result.contains("mismatched types"), "got: {}", result);
|
|
1367
|
+
assert!(!result.contains("aborting"), "got: {}", result);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
#[test]
|
|
1371
|
+
fn test_filter_cargo_install_already_installed() {
|
|
1372
|
+
let output = r#" Ignored package `rtk v0.11.0`, is already installed
|
|
1373
|
+
"#;
|
|
1374
|
+
let result = filter_cargo_install(output);
|
|
1375
|
+
assert!(result.contains("already installed"), "got: {}", result);
|
|
1376
|
+
assert!(result.contains("rtk v0.11.0"), "got: {}", result);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
#[test]
|
|
1380
|
+
fn test_filter_cargo_install_up_to_date() {
|
|
1381
|
+
let output = r#" Ignored package `cargo-deb v2.1.0 (/Users/user/cargo-deb)`, is already installed
|
|
1382
|
+
"#;
|
|
1383
|
+
let result = filter_cargo_install(output);
|
|
1384
|
+
assert!(result.contains("already installed"), "got: {}", result);
|
|
1385
|
+
assert!(result.contains("cargo-deb v2.1.0"), "got: {}", result);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
#[test]
|
|
1389
|
+
fn test_filter_cargo_install_empty_output() {
|
|
1390
|
+
let result = filter_cargo_install("");
|
|
1391
|
+
assert!(result.contains("✓ cargo install"), "got: {}", result);
|
|
1392
|
+
assert!(result.contains("0 deps compiled"), "got: {}", result);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
#[test]
|
|
1396
|
+
fn test_filter_cargo_install_path_warning() {
|
|
1397
|
+
let output = r#" Installing rtk v0.11.0
|
|
1398
|
+
Compiling rtk v0.11.0
|
|
1399
|
+
Finished `release` profile [optimized] target(s) in 10.0s
|
|
1400
|
+
Replacing /Users/user/.cargo/bin/rtk
|
|
1401
|
+
Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
|
|
1402
|
+
warning: be sure to add `/Users/user/.cargo/bin` to your PATH
|
|
1403
|
+
"#;
|
|
1404
|
+
let result = filter_cargo_install(output);
|
|
1405
|
+
assert!(result.contains("✓ cargo install"), "got: {}", result);
|
|
1406
|
+
assert!(
|
|
1407
|
+
result.contains("be sure to add"),
|
|
1408
|
+
"PATH warning should be kept: {}",
|
|
1409
|
+
result
|
|
1410
|
+
);
|
|
1411
|
+
assert!(result.contains("Replaced"), "got: {}", result);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
#[test]
|
|
1415
|
+
fn test_filter_cargo_install_multiple_errors() {
|
|
1416
|
+
let output = r#" Installing rtk v0.11.0
|
|
1417
|
+
Compiling rtk v0.11.0
|
|
1418
|
+
error[E0308]: mismatched types
|
|
1419
|
+
--> src/main.rs:10:5
|
|
1420
|
+
|
|
|
1421
|
+
10| "hello"
|
|
1422
|
+
| ^^^^^^^ expected `i32`, found `&str`
|
|
1423
|
+
|
|
1424
|
+
error[E0425]: cannot find value `foo`
|
|
1425
|
+
--> src/lib.rs:20:9
|
|
1426
|
+
|
|
|
1427
|
+
20| foo
|
|
1428
|
+
| ^^^ not found in this scope
|
|
1429
|
+
|
|
1430
|
+
error: aborting due to 2 previous errors
|
|
1431
|
+
"#;
|
|
1432
|
+
let result = filter_cargo_install(output);
|
|
1433
|
+
assert!(
|
|
1434
|
+
result.contains("2 errors"),
|
|
1435
|
+
"should show 2 errors: {}",
|
|
1436
|
+
result
|
|
1437
|
+
);
|
|
1438
|
+
assert!(result.contains("E0308"), "got: {}", result);
|
|
1439
|
+
assert!(result.contains("E0425"), "got: {}", result);
|
|
1440
|
+
assert!(!result.contains("aborting"), "got: {}", result);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
#[test]
|
|
1444
|
+
fn test_filter_cargo_install_locking_and_blocking() {
|
|
1445
|
+
let output = r#" Locking 45 packages to latest compatible versions
|
|
1446
|
+
Blocking waiting for file lock on package cache
|
|
1447
|
+
Downloading crates ...
|
|
1448
|
+
Downloaded serde v1.0.200
|
|
1449
|
+
Compiling serde v1.0.200
|
|
1450
|
+
Compiling rtk v0.11.0
|
|
1451
|
+
Finished `release` profile [optimized] target(s) in 30.0s
|
|
1452
|
+
Installing rtk v0.11.0
|
|
1453
|
+
"#;
|
|
1454
|
+
let result = filter_cargo_install(output);
|
|
1455
|
+
assert!(result.contains("✓ cargo install"), "got: {}", result);
|
|
1456
|
+
assert!(!result.contains("Locking"), "got: {}", result);
|
|
1457
|
+
assert!(!result.contains("Blocking"), "got: {}", result);
|
|
1458
|
+
assert!(!result.contains("Downloading"), "got: {}", result);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
#[test]
|
|
1462
|
+
fn test_filter_cargo_install_from_path() {
|
|
1463
|
+
let output = r#" Installing /Users/user/projects/rtk
|
|
1464
|
+
Compiling rtk v0.11.0
|
|
1465
|
+
Finished `release` profile [optimized] target(s) in 10.0s
|
|
1466
|
+
"#;
|
|
1467
|
+
let result = filter_cargo_install(output);
|
|
1468
|
+
// Path-based install: crate info not extracted from path
|
|
1469
|
+
assert!(result.contains("✓ cargo install"), "got: {}", result);
|
|
1470
|
+
assert!(result.contains("1 deps compiled"), "got: {}", result);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
#[test]
|
|
1474
|
+
fn test_format_crate_info() {
|
|
1475
|
+
assert_eq!(format_crate_info("rtk", "v0.11.0", ""), "rtk v0.11.0");
|
|
1476
|
+
assert_eq!(format_crate_info("rtk", "", ""), "rtk");
|
|
1477
|
+
assert_eq!(format_crate_info("", "", "package"), "package");
|
|
1478
|
+
assert_eq!(format_crate_info("", "v0.1.0", "fallback"), "fallback");
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
#[test]
|
|
1482
|
+
fn test_filter_cargo_nextest_all_pass() {
|
|
1483
|
+
let output = r#" Compiling rtk v0.15.2
|
|
1484
|
+
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
|
|
1485
|
+
────────────────────────────
|
|
1486
|
+
Starting 301 tests across 1 binary
|
|
1487
|
+
PASS [ 0.009s] (1/301) rtk::bin/rtk cargo_cmd::tests::test_one
|
|
1488
|
+
PASS [ 0.008s] (2/301) rtk::bin/rtk cargo_cmd::tests::test_two
|
|
1489
|
+
PASS [ 0.007s] (301/301) rtk::bin/rtk cargo_cmd::tests::test_last
|
|
1490
|
+
────────────────────────────
|
|
1491
|
+
Summary [ 0.192s] 301 tests run: 301 passed, 0 skipped
|
|
1492
|
+
"#;
|
|
1493
|
+
let result = filter_cargo_nextest(output);
|
|
1494
|
+
assert_eq!(
|
|
1495
|
+
result, "✓ cargo nextest: 301 passed (1 binary, 0.192s)",
|
|
1496
|
+
"got: {}",
|
|
1497
|
+
result
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
#[test]
|
|
1502
|
+
fn test_filter_cargo_nextest_with_failures() {
|
|
1503
|
+
let output = r#" Starting 4 tests across 1 binary (1 test skipped)
|
|
1504
|
+
PASS [ 0.006s] (1/4) test-proj tests::passing_test
|
|
1505
|
+
FAIL [ 0.006s] (2/4) test-proj tests::failing_test
|
|
1506
|
+
|
|
1507
|
+
stderr ───
|
|
1508
|
+
|
|
1509
|
+
thread 'tests::failing_test' panicked at src/lib.rs:15:9:
|
|
1510
|
+
assertion `left == right` failed
|
|
1511
|
+
left: 1
|
|
1512
|
+
right: 2
|
|
1513
|
+
|
|
1514
|
+
Cancelling due to test failure: 2 tests still running
|
|
1515
|
+
PASS [ 0.007s] (3/4) test-proj tests::another_passing
|
|
1516
|
+
FAIL [ 0.006s] (4/4) test-proj tests::another_failing
|
|
1517
|
+
|
|
1518
|
+
stderr ───
|
|
1519
|
+
|
|
1520
|
+
thread 'tests::another_failing' panicked at src/lib.rs:20:9:
|
|
1521
|
+
something went wrong
|
|
1522
|
+
|
|
1523
|
+
────────────────────────────
|
|
1524
|
+
Summary [ 0.007s] 4 tests run: 2 passed, 2 failed, 1 skipped
|
|
1525
|
+
FAIL [ 0.006s] (2/4) test-proj tests::failing_test
|
|
1526
|
+
FAIL [ 0.006s] (4/4) test-proj tests::another_failing
|
|
1527
|
+
error: test run failed
|
|
1528
|
+
"#;
|
|
1529
|
+
let result = filter_cargo_nextest(output);
|
|
1530
|
+
assert!(
|
|
1531
|
+
result.contains("tests::failing_test"),
|
|
1532
|
+
"should contain first failure: {}",
|
|
1533
|
+
result
|
|
1534
|
+
);
|
|
1535
|
+
assert!(
|
|
1536
|
+
result.contains("tests::another_failing"),
|
|
1537
|
+
"should contain second failure: {}",
|
|
1538
|
+
result
|
|
1539
|
+
);
|
|
1540
|
+
assert!(
|
|
1541
|
+
result.contains("panicked"),
|
|
1542
|
+
"should contain stderr detail: {}",
|
|
1543
|
+
result
|
|
1544
|
+
);
|
|
1545
|
+
assert!(
|
|
1546
|
+
result.contains("2 passed, 2 failed, 1 skipped"),
|
|
1547
|
+
"should contain summary: {}",
|
|
1548
|
+
result
|
|
1549
|
+
);
|
|
1550
|
+
assert!(
|
|
1551
|
+
!result.contains("PASS"),
|
|
1552
|
+
"should not contain PASS lines: {}",
|
|
1553
|
+
result
|
|
1554
|
+
);
|
|
1555
|
+
// Post-summary FAIL recaps must not create duplicate FAIL header entries
|
|
1556
|
+
// (test names may appear in both header and stderr body naturally)
|
|
1557
|
+
assert_eq!(
|
|
1558
|
+
result.matches("FAIL [").count(),
|
|
1559
|
+
2,
|
|
1560
|
+
"should have exactly 2 FAIL headers (no post-summary duplicates): {}",
|
|
1561
|
+
result
|
|
1562
|
+
);
|
|
1563
|
+
assert!(
|
|
1564
|
+
!result.contains("error: test run failed"),
|
|
1565
|
+
"should not contain post-summary error line: {}",
|
|
1566
|
+
result
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
#[test]
|
|
1571
|
+
fn test_filter_cargo_nextest_with_skipped() {
|
|
1572
|
+
let output = r#" Starting 50 tests across 2 binaries (3 tests skipped)
|
|
1573
|
+
PASS [ 0.010s] (1/50) rtk::bin/rtk test_one
|
|
1574
|
+
PASS [ 0.010s] (50/50) rtk::bin/rtk test_last
|
|
1575
|
+
────────────────────────────
|
|
1576
|
+
Summary [ 0.500s] 50 tests run: 50 passed, 3 skipped
|
|
1577
|
+
"#;
|
|
1578
|
+
let result = filter_cargo_nextest(output);
|
|
1579
|
+
assert_eq!(
|
|
1580
|
+
result, "✓ cargo nextest: 50 passed, 3 skipped (2 binaries, 0.500s)",
|
|
1581
|
+
"got: {}",
|
|
1582
|
+
result
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
#[test]
|
|
1587
|
+
fn test_filter_cargo_nextest_single_failure_detail() {
|
|
1588
|
+
let output = r#" Starting 2 tests across 1 binary
|
|
1589
|
+
PASS [ 0.005s] (1/2) proj tests::good
|
|
1590
|
+
FAIL [ 0.005s] (2/2) proj tests::bad
|
|
1591
|
+
|
|
1592
|
+
stderr ───
|
|
1593
|
+
|
|
1594
|
+
thread 'tests::bad' panicked at src/lib.rs:5:9:
|
|
1595
|
+
assertion failed: false
|
|
1596
|
+
|
|
1597
|
+
────────────────────────────
|
|
1598
|
+
Summary [ 0.010s] 2 tests run: 1 passed, 1 failed
|
|
1599
|
+
FAIL [ 0.005s] (2/2) proj tests::bad
|
|
1600
|
+
error: test run failed
|
|
1601
|
+
"#;
|
|
1602
|
+
let result = filter_cargo_nextest(output);
|
|
1603
|
+
assert!(
|
|
1604
|
+
result.contains("assertion failed: false"),
|
|
1605
|
+
"should show panic message: {}",
|
|
1606
|
+
result
|
|
1607
|
+
);
|
|
1608
|
+
assert!(
|
|
1609
|
+
result.contains("1 passed, 1 failed"),
|
|
1610
|
+
"should show summary: {}",
|
|
1611
|
+
result
|
|
1612
|
+
);
|
|
1613
|
+
// Post-summary recap must not duplicate FAIL headers
|
|
1614
|
+
assert_eq!(
|
|
1615
|
+
result.matches("FAIL [").count(),
|
|
1616
|
+
1,
|
|
1617
|
+
"should have exactly 1 FAIL header (no post-summary duplicate): {}",
|
|
1618
|
+
result
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
#[test]
|
|
1623
|
+
fn test_filter_cargo_nextest_multiple_binaries() {
|
|
1624
|
+
let output = r#" Starting 100 tests across 5 binaries
|
|
1625
|
+
PASS [ 0.010s] (100/100) test_last
|
|
1626
|
+
────────────────────────────
|
|
1627
|
+
Summary [ 1.234s] 100 tests run: 100 passed, 0 skipped
|
|
1628
|
+
"#;
|
|
1629
|
+
let result = filter_cargo_nextest(output);
|
|
1630
|
+
assert_eq!(
|
|
1631
|
+
result, "✓ cargo nextest: 100 passed (5 binaries, 1.234s)",
|
|
1632
|
+
"got: {}",
|
|
1633
|
+
result
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
#[test]
|
|
1638
|
+
fn test_filter_cargo_nextest_compilation_stripped() {
|
|
1639
|
+
let output = r#" Compiling serde v1.0.200
|
|
1640
|
+
Compiling rtk v0.15.2
|
|
1641
|
+
Downloading crates ...
|
|
1642
|
+
Finished `test` profile [unoptimized + debuginfo] target(s) in 5.00s
|
|
1643
|
+
────────────────────────────
|
|
1644
|
+
Starting 10 tests across 1 binary
|
|
1645
|
+
PASS [ 0.010s] (10/10) test_last
|
|
1646
|
+
────────────────────────────
|
|
1647
|
+
Summary [ 0.050s] 10 tests run: 10 passed, 0 skipped
|
|
1648
|
+
"#;
|
|
1649
|
+
let result = filter_cargo_nextest(output);
|
|
1650
|
+
assert!(
|
|
1651
|
+
!result.contains("Compiling"),
|
|
1652
|
+
"should strip Compiling: {}",
|
|
1653
|
+
result
|
|
1654
|
+
);
|
|
1655
|
+
assert!(
|
|
1656
|
+
!result.contains("Downloading"),
|
|
1657
|
+
"should strip Downloading: {}",
|
|
1658
|
+
result
|
|
1659
|
+
);
|
|
1660
|
+
assert!(
|
|
1661
|
+
!result.contains("Finished"),
|
|
1662
|
+
"should strip Finished: {}",
|
|
1663
|
+
result
|
|
1664
|
+
);
|
|
1665
|
+
assert!(
|
|
1666
|
+
result.contains("✓ cargo nextest: 10 passed"),
|
|
1667
|
+
"got: {}",
|
|
1668
|
+
result
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
#[test]
|
|
1673
|
+
fn test_filter_cargo_nextest_empty() {
|
|
1674
|
+
let result = filter_cargo_nextest("");
|
|
1675
|
+
assert!(result.is_empty(), "got: {}", result);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
#[test]
|
|
1679
|
+
fn test_filter_cargo_nextest_cancellation_notice() {
|
|
1680
|
+
let output = r#" Starting 3 tests across 1 binary
|
|
1681
|
+
FAIL [ 0.005s] (1/3) proj tests::bad
|
|
1682
|
+
|
|
1683
|
+
stderr ───
|
|
1684
|
+
|
|
1685
|
+
thread panicked at 'oops'
|
|
1686
|
+
|
|
1687
|
+
Cancelling due to test failure: 2 tests still running
|
|
1688
|
+
────────────────────────────
|
|
1689
|
+
Summary [ 0.010s] 3 tests run: 2 passed, 1 failed
|
|
1690
|
+
FAIL [ 0.005s] (1/3) proj tests::bad
|
|
1691
|
+
error: test run failed
|
|
1692
|
+
"#;
|
|
1693
|
+
let result = filter_cargo_nextest(output);
|
|
1694
|
+
assert!(
|
|
1695
|
+
result.contains("Cancelling due to test failure"),
|
|
1696
|
+
"should include cancel notice: {}",
|
|
1697
|
+
result
|
|
1698
|
+
);
|
|
1699
|
+
assert!(
|
|
1700
|
+
result.contains("1 failed"),
|
|
1701
|
+
"should show failure count: {}",
|
|
1702
|
+
result
|
|
1703
|
+
);
|
|
1704
|
+
// Post-summary recap must not duplicate FAIL headers
|
|
1705
|
+
assert_eq!(
|
|
1706
|
+
result.matches("FAIL [").count(),
|
|
1707
|
+
1,
|
|
1708
|
+
"should have exactly 1 FAIL header (no post-summary duplicate): {}",
|
|
1709
|
+
result
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
#[test]
|
|
1714
|
+
fn test_filter_cargo_nextest_summary_regex_fallback() {
|
|
1715
|
+
let output = r#" Starting 5 tests across 1 binary
|
|
1716
|
+
PASS [ 0.005s] (5/5) test_last
|
|
1717
|
+
────────────────────────────
|
|
1718
|
+
Summary MALFORMED LINE
|
|
1719
|
+
"#;
|
|
1720
|
+
let result = filter_cargo_nextest(output);
|
|
1721
|
+
assert!(
|
|
1722
|
+
result.contains("Summary MALFORMED"),
|
|
1723
|
+
"should fall back to raw summary: {}",
|
|
1724
|
+
result
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
}
|