@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,384 @@
|
|
|
1
|
+
use crate::tracking;
|
|
2
|
+
use crate::utils::truncate;
|
|
3
|
+
use anyhow::{Context, Result};
|
|
4
|
+
use std::process::Command;
|
|
5
|
+
|
|
6
|
+
#[derive(Debug, PartialEq)]
|
|
7
|
+
enum ParseState {
|
|
8
|
+
Header,
|
|
9
|
+
TestProgress,
|
|
10
|
+
Failures,
|
|
11
|
+
Summary,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
|
15
|
+
let timer = tracking::TimedExecution::start();
|
|
16
|
+
|
|
17
|
+
// Try to detect pytest command (could be "pytest", "python -m pytest", etc.)
|
|
18
|
+
let mut cmd = if which_command("pytest").is_some() {
|
|
19
|
+
Command::new("pytest")
|
|
20
|
+
} else {
|
|
21
|
+
// Fallback to python -m pytest
|
|
22
|
+
let mut c = Command::new("python");
|
|
23
|
+
c.arg("-m").arg("pytest");
|
|
24
|
+
c
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Force short traceback and quiet mode for compact output
|
|
28
|
+
let has_tb_flag = args.iter().any(|a| a.starts_with("--tb"));
|
|
29
|
+
let has_quiet_flag = args.iter().any(|a| a == "-q" || a == "--quiet");
|
|
30
|
+
|
|
31
|
+
if !has_tb_flag {
|
|
32
|
+
cmd.arg("--tb=short");
|
|
33
|
+
}
|
|
34
|
+
if !has_quiet_flag {
|
|
35
|
+
cmd.arg("-q");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for arg in args {
|
|
39
|
+
cmd.arg(arg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if verbose > 0 {
|
|
43
|
+
eprintln!("Running: pytest --tb=short -q {}", args.join(" "));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let output = cmd
|
|
47
|
+
.output()
|
|
48
|
+
.context("Failed to run pytest. Is it installed? Try: pip install pytest")?;
|
|
49
|
+
|
|
50
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
51
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
52
|
+
let raw = format!("{}\n{}", stdout, stderr);
|
|
53
|
+
|
|
54
|
+
let filtered = filter_pytest_output(&stdout);
|
|
55
|
+
|
|
56
|
+
let exit_code = output
|
|
57
|
+
.status
|
|
58
|
+
.code()
|
|
59
|
+
.unwrap_or(if output.status.success() { 0 } else { 1 });
|
|
60
|
+
if let Some(hint) = crate::tee::tee_and_hint(&raw, "pytest", exit_code) {
|
|
61
|
+
println!("{}\n{}", filtered, hint);
|
|
62
|
+
} else {
|
|
63
|
+
println!("{}", filtered);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Include stderr if present (import errors, etc.)
|
|
67
|
+
if !stderr.trim().is_empty() {
|
|
68
|
+
eprintln!("{}", stderr.trim());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
timer.track(
|
|
72
|
+
&format!("pytest {}", args.join(" ")),
|
|
73
|
+
&format!("rtk pytest {}", args.join(" ")),
|
|
74
|
+
&raw,
|
|
75
|
+
&filtered,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Preserve exit code for CI/CD
|
|
79
|
+
if !output.status.success() {
|
|
80
|
+
std::process::exit(exit_code);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Ok(())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Check if a command exists in PATH
|
|
87
|
+
fn which_command(cmd: &str) -> Option<String> {
|
|
88
|
+
Command::new("which")
|
|
89
|
+
.arg(cmd)
|
|
90
|
+
.output()
|
|
91
|
+
.ok()
|
|
92
|
+
.filter(|o| o.status.success())
|
|
93
|
+
.and_then(|o| String::from_utf8(o.stdout).ok())
|
|
94
|
+
.map(|s| s.trim().to_string())
|
|
95
|
+
.filter(|s| !s.is_empty())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Parse pytest output using state machine
|
|
99
|
+
fn filter_pytest_output(output: &str) -> String {
|
|
100
|
+
let mut state = ParseState::Header;
|
|
101
|
+
let mut test_files: Vec<String> = Vec::new();
|
|
102
|
+
let mut failures: Vec<String> = Vec::new();
|
|
103
|
+
let mut current_failure: Vec<String> = Vec::new();
|
|
104
|
+
let mut summary_line = String::new();
|
|
105
|
+
|
|
106
|
+
for line in output.lines() {
|
|
107
|
+
let trimmed = line.trim();
|
|
108
|
+
|
|
109
|
+
// State transitions
|
|
110
|
+
if trimmed.starts_with("===") && trimmed.contains("test session starts") {
|
|
111
|
+
state = ParseState::Header;
|
|
112
|
+
continue;
|
|
113
|
+
} else if trimmed.starts_with("===") && trimmed.contains("FAILURES") {
|
|
114
|
+
state = ParseState::Failures;
|
|
115
|
+
continue;
|
|
116
|
+
} else if trimmed.starts_with("===") && trimmed.contains("short test summary") {
|
|
117
|
+
state = ParseState::Summary;
|
|
118
|
+
// Save current failure if any
|
|
119
|
+
if !current_failure.is_empty() {
|
|
120
|
+
failures.push(current_failure.join("\n"));
|
|
121
|
+
current_failure.clear();
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
} else if trimmed.starts_with("===")
|
|
125
|
+
&& (trimmed.contains("passed") || trimmed.contains("failed"))
|
|
126
|
+
{
|
|
127
|
+
summary_line = trimmed.to_string();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Process based on state
|
|
132
|
+
match state {
|
|
133
|
+
ParseState::Header => {
|
|
134
|
+
if trimmed.starts_with("collected") {
|
|
135
|
+
state = ParseState::TestProgress;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
ParseState::TestProgress => {
|
|
139
|
+
// Lines like "tests/test_foo.py .... [ 40%]"
|
|
140
|
+
if !trimmed.is_empty()
|
|
141
|
+
&& !trimmed.starts_with("===")
|
|
142
|
+
&& (trimmed.contains(".py") || trimmed.contains("%]"))
|
|
143
|
+
{
|
|
144
|
+
test_files.push(trimmed.to_string());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
ParseState::Failures => {
|
|
148
|
+
// Collect failure details
|
|
149
|
+
if trimmed.starts_with("___") {
|
|
150
|
+
// New failure section
|
|
151
|
+
if !current_failure.is_empty() {
|
|
152
|
+
failures.push(current_failure.join("\n"));
|
|
153
|
+
current_failure.clear();
|
|
154
|
+
}
|
|
155
|
+
current_failure.push(trimmed.to_string());
|
|
156
|
+
} else if !trimmed.is_empty() && !trimmed.starts_with("===") {
|
|
157
|
+
current_failure.push(trimmed.to_string());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
ParseState::Summary => {
|
|
161
|
+
// FAILED test lines
|
|
162
|
+
if trimmed.starts_with("FAILED") || trimmed.starts_with("ERROR") {
|
|
163
|
+
failures.push(trimmed.to_string());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Save last failure if any
|
|
170
|
+
if !current_failure.is_empty() {
|
|
171
|
+
failures.push(current_failure.join("\n"));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build compact output
|
|
175
|
+
build_pytest_summary(&summary_line, &test_files, &failures)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn build_pytest_summary(summary: &str, _test_files: &[String], failures: &[String]) -> String {
|
|
179
|
+
// Parse summary line
|
|
180
|
+
let (passed, failed, skipped) = parse_summary_line(summary);
|
|
181
|
+
|
|
182
|
+
if failed == 0 && passed > 0 {
|
|
183
|
+
return format!("✓ Pytest: {} passed", passed);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if passed == 0 && failed == 0 {
|
|
187
|
+
return "Pytest: No tests collected".to_string();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let mut result = String::new();
|
|
191
|
+
result.push_str(&format!("Pytest: {} passed, {} failed", passed, failed));
|
|
192
|
+
if skipped > 0 {
|
|
193
|
+
result.push_str(&format!(", {} skipped", skipped));
|
|
194
|
+
}
|
|
195
|
+
result.push('\n');
|
|
196
|
+
result.push_str("═══════════════════════════════════════\n");
|
|
197
|
+
|
|
198
|
+
if failures.is_empty() {
|
|
199
|
+
return result.trim().to_string();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Show failures (limit to key information)
|
|
203
|
+
result.push_str("\nFailures:\n");
|
|
204
|
+
|
|
205
|
+
for (i, failure) in failures.iter().take(5).enumerate() {
|
|
206
|
+
// Extract test name and key error info
|
|
207
|
+
let lines: Vec<&str> = failure.lines().collect();
|
|
208
|
+
|
|
209
|
+
// First line is usually test name (after ___)
|
|
210
|
+
if let Some(first_line) = lines.first() {
|
|
211
|
+
if first_line.starts_with("___") {
|
|
212
|
+
// Extract test name between ___
|
|
213
|
+
let test_name = first_line.trim_matches('_').trim();
|
|
214
|
+
result.push_str(&format!("{}. ❌ {}\n", i + 1, test_name));
|
|
215
|
+
} else if first_line.starts_with("FAILED") {
|
|
216
|
+
// Summary format: "FAILED tests/test_foo.py::test_bar - AssertionError"
|
|
217
|
+
let parts: Vec<&str> = first_line.split(" - ").collect();
|
|
218
|
+
if let Some(test_path) = parts.first() {
|
|
219
|
+
let test_name = test_path.trim_start_matches("FAILED ");
|
|
220
|
+
result.push_str(&format!("{}. ❌ {}\n", i + 1, test_name));
|
|
221
|
+
}
|
|
222
|
+
if parts.len() > 1 {
|
|
223
|
+
result.push_str(&format!(" {}\n", truncate(parts[1], 100)));
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Show relevant error lines (assertions, errors, file locations)
|
|
230
|
+
let mut relevant_lines = 0;
|
|
231
|
+
for line in &lines[1..] {
|
|
232
|
+
let line_lower = line.to_lowercase();
|
|
233
|
+
let is_relevant = line.trim().starts_with('>')
|
|
234
|
+
|| line.trim().starts_with('E')
|
|
235
|
+
|| line_lower.contains("assert")
|
|
236
|
+
|| line_lower.contains("error")
|
|
237
|
+
|| line.contains(".py:");
|
|
238
|
+
|
|
239
|
+
if is_relevant && relevant_lines < 3 {
|
|
240
|
+
result.push_str(&format!(" {}\n", truncate(line, 100)));
|
|
241
|
+
relevant_lines += 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if i < failures.len() - 1 {
|
|
246
|
+
result.push('\n');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if failures.len() > 5 {
|
|
251
|
+
result.push_str(&format!("\n... +{} more failures\n", failures.len() - 5));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
result.trim().to_string()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn parse_summary_line(summary: &str) -> (usize, usize, usize) {
|
|
258
|
+
let mut passed = 0;
|
|
259
|
+
let mut failed = 0;
|
|
260
|
+
let mut skipped = 0;
|
|
261
|
+
|
|
262
|
+
// Parse lines like "=== 4 passed, 1 failed in 0.50s ==="
|
|
263
|
+
let parts: Vec<&str> = summary.split(',').collect();
|
|
264
|
+
|
|
265
|
+
for part in parts {
|
|
266
|
+
let words: Vec<&str> = part.split_whitespace().collect();
|
|
267
|
+
for (i, word) in words.iter().enumerate() {
|
|
268
|
+
if i > 0 {
|
|
269
|
+
if word.contains("passed") {
|
|
270
|
+
if let Ok(n) = words[i - 1].parse::<usize>() {
|
|
271
|
+
passed = n;
|
|
272
|
+
}
|
|
273
|
+
} else if word.contains("failed") {
|
|
274
|
+
if let Ok(n) = words[i - 1].parse::<usize>() {
|
|
275
|
+
failed = n;
|
|
276
|
+
}
|
|
277
|
+
} else if word.contains("skipped") {
|
|
278
|
+
if let Ok(n) = words[i - 1].parse::<usize>() {
|
|
279
|
+
skipped = n;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
(passed, failed, skipped)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[cfg(test)]
|
|
290
|
+
mod tests {
|
|
291
|
+
use super::*;
|
|
292
|
+
|
|
293
|
+
#[test]
|
|
294
|
+
fn test_filter_pytest_all_pass() {
|
|
295
|
+
let output = r#"=== test session starts ===
|
|
296
|
+
platform darwin -- Python 3.11.0
|
|
297
|
+
collected 5 items
|
|
298
|
+
|
|
299
|
+
tests/test_foo.py ..... [100%]
|
|
300
|
+
|
|
301
|
+
=== 5 passed in 0.50s ==="#;
|
|
302
|
+
|
|
303
|
+
let result = filter_pytest_output(output);
|
|
304
|
+
assert!(result.contains("✓ Pytest"));
|
|
305
|
+
assert!(result.contains("5 passed"));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[test]
|
|
309
|
+
fn test_filter_pytest_with_failures() {
|
|
310
|
+
let output = r#"=== test session starts ===
|
|
311
|
+
collected 5 items
|
|
312
|
+
|
|
313
|
+
tests/test_foo.py ..F.. [100%]
|
|
314
|
+
|
|
315
|
+
=== FAILURES ===
|
|
316
|
+
___ test_something ___
|
|
317
|
+
|
|
318
|
+
def test_something():
|
|
319
|
+
> assert False
|
|
320
|
+
E assert False
|
|
321
|
+
|
|
322
|
+
tests/test_foo.py:10: AssertionError
|
|
323
|
+
|
|
324
|
+
=== short test summary info ===
|
|
325
|
+
FAILED tests/test_foo.py::test_something - assert False
|
|
326
|
+
=== 4 passed, 1 failed in 0.50s ==="#;
|
|
327
|
+
|
|
328
|
+
let result = filter_pytest_output(output);
|
|
329
|
+
assert!(result.contains("4 passed, 1 failed"));
|
|
330
|
+
assert!(result.contains("test_something"));
|
|
331
|
+
assert!(result.contains("assert False"));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[test]
|
|
335
|
+
fn test_filter_pytest_multiple_failures() {
|
|
336
|
+
let output = r#"=== test session starts ===
|
|
337
|
+
collected 3 items
|
|
338
|
+
|
|
339
|
+
tests/test_foo.py FFF [100%]
|
|
340
|
+
|
|
341
|
+
=== FAILURES ===
|
|
342
|
+
___ test_one ___
|
|
343
|
+
E AssertionError: expected 5
|
|
344
|
+
|
|
345
|
+
___ test_two ___
|
|
346
|
+
E ValueError: invalid value
|
|
347
|
+
|
|
348
|
+
=== short test summary info ===
|
|
349
|
+
FAILED tests/test_foo.py::test_one - AssertionError: expected 5
|
|
350
|
+
FAILED tests/test_foo.py::test_two - ValueError: invalid value
|
|
351
|
+
FAILED tests/test_foo.py::test_three - KeyError
|
|
352
|
+
=== 3 failed in 0.20s ==="#;
|
|
353
|
+
|
|
354
|
+
let result = filter_pytest_output(output);
|
|
355
|
+
assert!(result.contains("3 failed"));
|
|
356
|
+
assert!(result.contains("test_one"));
|
|
357
|
+
assert!(result.contains("test_two"));
|
|
358
|
+
assert!(result.contains("expected 5"));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn test_filter_pytest_no_tests() {
|
|
363
|
+
let output = r#"=== test session starts ===
|
|
364
|
+
collected 0 items
|
|
365
|
+
|
|
366
|
+
=== no tests ran in 0.00s ==="#;
|
|
367
|
+
|
|
368
|
+
let result = filter_pytest_output(output);
|
|
369
|
+
assert!(result.contains("No tests collected"));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#[test]
|
|
373
|
+
fn test_parse_summary_line() {
|
|
374
|
+
assert_eq!(parse_summary_line("=== 5 passed in 0.50s ==="), (5, 0, 0));
|
|
375
|
+
assert_eq!(
|
|
376
|
+
parse_summary_line("=== 4 passed, 1 failed in 0.50s ==="),
|
|
377
|
+
(4, 1, 0)
|
|
378
|
+
);
|
|
379
|
+
assert_eq!(
|
|
380
|
+
parse_summary_line("=== 3 passed, 1 failed, 2 skipped in 1.0s ==="),
|
|
381
|
+
(3, 1, 2)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
use crate::filter::{self, FilterLevel, Language};
|
|
2
|
+
use crate::tracking;
|
|
3
|
+
use anyhow::{Context, Result};
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::Path;
|
|
6
|
+
|
|
7
|
+
pub fn run(
|
|
8
|
+
file: &Path,
|
|
9
|
+
level: FilterLevel,
|
|
10
|
+
max_lines: Option<usize>,
|
|
11
|
+
tail_lines: Option<usize>,
|
|
12
|
+
line_numbers: bool,
|
|
13
|
+
verbose: u8,
|
|
14
|
+
) -> Result<()> {
|
|
15
|
+
let timer = tracking::TimedExecution::start();
|
|
16
|
+
|
|
17
|
+
if verbose > 0 {
|
|
18
|
+
eprintln!("Reading: {} (filter: {})", file.display(), level);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Read file content
|
|
22
|
+
let content = fs::read_to_string(file)
|
|
23
|
+
.with_context(|| format!("Failed to read file: {}", file.display()))?;
|
|
24
|
+
|
|
25
|
+
// Detect language from extension
|
|
26
|
+
let lang = file
|
|
27
|
+
.extension()
|
|
28
|
+
.and_then(|e| e.to_str())
|
|
29
|
+
.map(Language::from_extension)
|
|
30
|
+
.unwrap_or(Language::Unknown);
|
|
31
|
+
|
|
32
|
+
if verbose > 1 {
|
|
33
|
+
eprintln!("Detected language: {:?}", lang);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Apply filter
|
|
37
|
+
let filter = filter::get_filter(level);
|
|
38
|
+
let mut filtered = filter.filter(&content, &lang);
|
|
39
|
+
|
|
40
|
+
if verbose > 0 {
|
|
41
|
+
let original_lines = content.lines().count();
|
|
42
|
+
let filtered_lines = filtered.lines().count();
|
|
43
|
+
let reduction = if original_lines > 0 {
|
|
44
|
+
((original_lines - filtered_lines) as f64 / original_lines as f64) * 100.0
|
|
45
|
+
} else {
|
|
46
|
+
0.0
|
|
47
|
+
};
|
|
48
|
+
eprintln!(
|
|
49
|
+
"Lines: {} -> {} ({:.1}% reduction)",
|
|
50
|
+
original_lines, filtered_lines, reduction
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
filtered = apply_line_window(&filtered, max_lines, tail_lines, &lang);
|
|
55
|
+
|
|
56
|
+
let rtk_output = if line_numbers {
|
|
57
|
+
format_with_line_numbers(&filtered)
|
|
58
|
+
} else {
|
|
59
|
+
filtered.clone()
|
|
60
|
+
};
|
|
61
|
+
println!("{}", rtk_output);
|
|
62
|
+
timer.track(
|
|
63
|
+
&format!("cat {}", file.display()),
|
|
64
|
+
"rtk read",
|
|
65
|
+
&content,
|
|
66
|
+
&rtk_output,
|
|
67
|
+
);
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn run_stdin(
|
|
72
|
+
level: FilterLevel,
|
|
73
|
+
max_lines: Option<usize>,
|
|
74
|
+
tail_lines: Option<usize>,
|
|
75
|
+
line_numbers: bool,
|
|
76
|
+
verbose: u8,
|
|
77
|
+
) -> Result<()> {
|
|
78
|
+
use std::io::{self, Read as IoRead};
|
|
79
|
+
|
|
80
|
+
let timer = tracking::TimedExecution::start();
|
|
81
|
+
|
|
82
|
+
if verbose > 0 {
|
|
83
|
+
eprintln!("Reading from stdin (filter: {})", level);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read from stdin
|
|
87
|
+
let mut content = String::new();
|
|
88
|
+
io::stdin()
|
|
89
|
+
.lock()
|
|
90
|
+
.read_to_string(&mut content)
|
|
91
|
+
.context("Failed to read from stdin")?;
|
|
92
|
+
|
|
93
|
+
// No file extension, so use Unknown language
|
|
94
|
+
let lang = Language::Unknown;
|
|
95
|
+
|
|
96
|
+
if verbose > 1 {
|
|
97
|
+
eprintln!("Language: {:?} (stdin has no extension)", lang);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Apply filter
|
|
101
|
+
let filter = filter::get_filter(level);
|
|
102
|
+
let mut filtered = filter.filter(&content, &lang);
|
|
103
|
+
|
|
104
|
+
if verbose > 0 {
|
|
105
|
+
let original_lines = content.lines().count();
|
|
106
|
+
let filtered_lines = filtered.lines().count();
|
|
107
|
+
let reduction = if original_lines > 0 {
|
|
108
|
+
((original_lines - filtered_lines) as f64 / original_lines as f64) * 100.0
|
|
109
|
+
} else {
|
|
110
|
+
0.0
|
|
111
|
+
};
|
|
112
|
+
eprintln!(
|
|
113
|
+
"Lines: {} -> {} ({:.1}% reduction)",
|
|
114
|
+
original_lines, filtered_lines, reduction
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
filtered = apply_line_window(&filtered, max_lines, tail_lines, &lang);
|
|
119
|
+
|
|
120
|
+
let rtk_output = if line_numbers {
|
|
121
|
+
format_with_line_numbers(&filtered)
|
|
122
|
+
} else {
|
|
123
|
+
filtered.clone()
|
|
124
|
+
};
|
|
125
|
+
println!("{}", rtk_output);
|
|
126
|
+
|
|
127
|
+
timer.track("cat - (stdin)", "rtk read -", &content, &rtk_output);
|
|
128
|
+
Ok(())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn format_with_line_numbers(content: &str) -> String {
|
|
132
|
+
let lines: Vec<&str> = content.lines().collect();
|
|
133
|
+
let width = lines.len().to_string().len();
|
|
134
|
+
let mut out = String::new();
|
|
135
|
+
for (i, line) in lines.iter().enumerate() {
|
|
136
|
+
out.push_str(&format!("{:>width$} │ {}\n", i + 1, line, width = width));
|
|
137
|
+
}
|
|
138
|
+
out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn apply_line_window(
|
|
142
|
+
content: &str,
|
|
143
|
+
max_lines: Option<usize>,
|
|
144
|
+
tail_lines: Option<usize>,
|
|
145
|
+
lang: &Language,
|
|
146
|
+
) -> String {
|
|
147
|
+
if let Some(tail) = tail_lines {
|
|
148
|
+
if tail == 0 {
|
|
149
|
+
return String::new();
|
|
150
|
+
}
|
|
151
|
+
let lines: Vec<&str> = content.lines().collect();
|
|
152
|
+
let start = lines.len().saturating_sub(tail);
|
|
153
|
+
let mut result = lines[start..].join("\n");
|
|
154
|
+
if content.ends_with('\n') {
|
|
155
|
+
result.push('\n');
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if let Some(max) = max_lines {
|
|
161
|
+
return filter::smart_truncate(content, max, lang);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
content.to_string()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[cfg(test)]
|
|
168
|
+
mod tests {
|
|
169
|
+
use super::*;
|
|
170
|
+
use std::io::Write;
|
|
171
|
+
use tempfile::NamedTempFile;
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn test_read_rust_file() -> Result<()> {
|
|
175
|
+
let mut file = NamedTempFile::with_suffix(".rs")?;
|
|
176
|
+
writeln!(
|
|
177
|
+
file,
|
|
178
|
+
r#"// Comment
|
|
179
|
+
fn main() {{
|
|
180
|
+
println!("Hello");
|
|
181
|
+
}}"#
|
|
182
|
+
)?;
|
|
183
|
+
|
|
184
|
+
// Just verify it doesn't panic
|
|
185
|
+
run(file.path(), FilterLevel::Minimal, None, None, false, 0)?;
|
|
186
|
+
Ok(())
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[test]
|
|
190
|
+
fn test_stdin_support_signature() {
|
|
191
|
+
// Test that run_stdin has correct signature and compiles
|
|
192
|
+
// We don't actually run it because it would hang waiting for stdin
|
|
193
|
+
// Compile-time verification that the function exists with correct signature
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn test_apply_line_window_tail_lines() {
|
|
198
|
+
let input = "a\nb\nc\nd\n";
|
|
199
|
+
let output = apply_line_window(input, None, Some(2), &Language::Unknown);
|
|
200
|
+
assert_eq!(output, "c\nd\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn test_apply_line_window_tail_lines_no_trailing_newline() {
|
|
205
|
+
let input = "a\nb\nc\nd";
|
|
206
|
+
let output = apply_line_window(input, None, Some(2), &Language::Unknown);
|
|
207
|
+
assert_eq!(output, "c\nd");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[test]
|
|
211
|
+
fn test_apply_line_window_max_lines_still_works() {
|
|
212
|
+
let input = "a\nb\nc\nd\n";
|
|
213
|
+
let output = apply_line_window(input, Some(2), None, &Language::Unknown);
|
|
214
|
+
assert!(output.starts_with("a\n"));
|
|
215
|
+
assert!(output.contains("more lines"));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
use crate::discover::registry;
|
|
2
|
+
|
|
3
|
+
/// Run the `rtk rewrite` command.
|
|
4
|
+
///
|
|
5
|
+
/// Prints the RTK-rewritten command to stdout and exits 0.
|
|
6
|
+
/// Exits 1 (without output) if the command has no RTK equivalent.
|
|
7
|
+
///
|
|
8
|
+
/// Used by shell hooks to rewrite commands transparently:
|
|
9
|
+
/// ```bash
|
|
10
|
+
/// REWRITTEN=$(rtk rewrite "$CMD") || exit 0
|
|
11
|
+
/// [ "$CMD" = "$REWRITTEN" ] && exit 0 # already RTK, skip
|
|
12
|
+
/// ```
|
|
13
|
+
pub fn run(cmd: &str) -> anyhow::Result<()> {
|
|
14
|
+
let excluded = crate::config::Config::load()
|
|
15
|
+
.map(|c| c.hooks.exclude_commands)
|
|
16
|
+
.unwrap_or_default();
|
|
17
|
+
|
|
18
|
+
match registry::rewrite_command(cmd, &excluded) {
|
|
19
|
+
Some(rewritten) => {
|
|
20
|
+
print!("{}", rewritten);
|
|
21
|
+
Ok(())
|
|
22
|
+
}
|
|
23
|
+
None => {
|
|
24
|
+
std::process::exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[cfg(test)]
|
|
30
|
+
mod tests {
|
|
31
|
+
use super::*;
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn test_run_supported_command_succeeds() {
|
|
35
|
+
assert!(registry::rewrite_command("git status", &[]).is_some());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn test_run_unsupported_returns_none() {
|
|
40
|
+
assert!(registry::rewrite_command("htop", &[]).is_none());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn test_run_already_rtk_returns_some() {
|
|
45
|
+
assert_eq!(
|
|
46
|
+
registry::rewrite_command("rtk git status", &[]),
|
|
47
|
+
Some("rtk git status".into())
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|