@hasna/terminal 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +64 -16
- package/package.json +1 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
package/temp/rtk/src/binlog.rs
DELETED
|
@@ -1,1645 +0,0 @@
|
|
|
1
|
-
use crate::utils::strip_ansi;
|
|
2
|
-
use anyhow::{Context, Result};
|
|
3
|
-
use flate2::read::GzDecoder;
|
|
4
|
-
use lazy_static::lazy_static;
|
|
5
|
-
use regex::Regex;
|
|
6
|
-
use std::collections::HashSet;
|
|
7
|
-
use std::io::{Cursor, Read};
|
|
8
|
-
use std::path::Path;
|
|
9
|
-
|
|
10
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
11
|
-
pub struct BinlogIssue {
|
|
12
|
-
pub code: String,
|
|
13
|
-
pub file: String,
|
|
14
|
-
pub line: u32,
|
|
15
|
-
pub column: u32,
|
|
16
|
-
pub message: String,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
#[derive(Debug, Clone, Default)]
|
|
20
|
-
pub struct BuildSummary {
|
|
21
|
-
pub succeeded: bool,
|
|
22
|
-
pub project_count: usize,
|
|
23
|
-
pub errors: Vec<BinlogIssue>,
|
|
24
|
-
pub warnings: Vec<BinlogIssue>,
|
|
25
|
-
pub duration_text: Option<String>,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
29
|
-
pub struct FailedTest {
|
|
30
|
-
pub name: String,
|
|
31
|
-
pub details: Vec<String>,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
#[derive(Debug, Clone, Default)]
|
|
35
|
-
pub struct TestSummary {
|
|
36
|
-
pub passed: usize,
|
|
37
|
-
pub failed: usize,
|
|
38
|
-
pub skipped: usize,
|
|
39
|
-
pub total: usize,
|
|
40
|
-
pub project_count: usize,
|
|
41
|
-
pub failed_tests: Vec<FailedTest>,
|
|
42
|
-
pub duration_text: Option<String>,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
#[derive(Debug, Clone, Default)]
|
|
46
|
-
pub struct RestoreSummary {
|
|
47
|
-
pub restored_projects: usize,
|
|
48
|
-
pub warnings: usize,
|
|
49
|
-
pub errors: usize,
|
|
50
|
-
pub duration_text: Option<String>,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
lazy_static! {
|
|
54
|
-
static ref ISSUE_RE: Regex = Regex::new(
|
|
55
|
-
r"(?m)^\s*(?P<file>[^\r\n:(]+)\((?P<line>\d+),(?P<column>\d+)\):\s*(?P<kind>error|warning)\s*(?:(?P<code>[A-Za-z]+\d+)\s*:\s*)?(?P<msg>.*)$"
|
|
56
|
-
)
|
|
57
|
-
.expect("valid regex");
|
|
58
|
-
static ref BUILD_SUMMARY_RE: Regex = Regex::new(r"(?mi)^\s*(?P<count>\d+)\s+(?P<kind>warning|error)\(s\)")
|
|
59
|
-
.expect("valid regex");
|
|
60
|
-
static ref ERROR_COUNT_RE: Regex =
|
|
61
|
-
Regex::new(r"(?i)\b(?P<count>\d+)\s+error\(s\)").expect("valid regex");
|
|
62
|
-
static ref WARNING_COUNT_RE: Regex =
|
|
63
|
-
Regex::new(r"(?i)\b(?P<count>\d+)\s+warning\(s\)").expect("valid regex");
|
|
64
|
-
static ref FALLBACK_ERROR_LINE_RE: Regex =
|
|
65
|
-
Regex::new(r"(?mi)^.+\(\d+,\d+\):\s*error(?:\s+[A-Za-z]{2,}\d{3,})?(?:\s*:.*)?$")
|
|
66
|
-
.expect("valid regex");
|
|
67
|
-
static ref FALLBACK_WARNING_LINE_RE: Regex =
|
|
68
|
-
Regex::new(r"(?mi)^.+\(\d+,\d+\):\s*warning(?:\s+[A-Za-z]{2,}\d{3,})?(?:\s*:.*)?$")
|
|
69
|
-
.expect("valid regex");
|
|
70
|
-
static ref DURATION_RE: Regex =
|
|
71
|
-
Regex::new(r"(?m)^\s*Time Elapsed\s+(?P<duration>[^\r\n]+)$").expect("valid regex");
|
|
72
|
-
static ref TEST_RESULT_RE: Regex = Regex::new(
|
|
73
|
-
r"(?m)(?:Passed!|Failed!)\s*-\s*Failed:\s*(?P<failed>\d+),\s*Passed:\s*(?P<passed>\d+),\s*Skipped:\s*(?P<skipped>\d+),\s*Total:\s*(?P<total>\d+),\s*Duration:\s*(?P<duration>[^\r\n-]+)"
|
|
74
|
-
)
|
|
75
|
-
.expect("valid regex");
|
|
76
|
-
static ref TEST_SUMMARY_RE: Regex = Regex::new(
|
|
77
|
-
r"(?mi)^\s*Test summary:\s*total:\s*(?P<total>\d+),\s*failed:\s*(?P<failed>\d+),\s*(?:succeeded|passed):\s*(?P<passed>\d+),\s*skipped:\s*(?P<skipped>\d+),\s*duration:\s*(?P<duration>[^\r\n]+)$"
|
|
78
|
-
)
|
|
79
|
-
.expect("valid regex");
|
|
80
|
-
static ref FAILED_TEST_HEAD_RE: Regex = Regex::new(
|
|
81
|
-
r"(?m)^\s*Failed\s+(?P<name>[^\r\n\[]+)\s+\[[^\]\r\n]+\]\s*$"
|
|
82
|
-
)
|
|
83
|
-
.expect("valid regex");
|
|
84
|
-
static ref RESTORE_PROJECT_RE: Regex =
|
|
85
|
-
Regex::new(r"(?m)^\s*Restored\s+.+\.csproj\s*\(").expect("valid regex");
|
|
86
|
-
static ref RESTORE_DIAGNOSTIC_RE: Regex = Regex::new(
|
|
87
|
-
r"(?mi)^\s*(?:(?P<file>.+?)\s+:\s+)?(?P<kind>warning|error)\s+(?P<code>[A-Za-z]{2,}\d{3,})\s*:\s*(?P<msg>.+)$"
|
|
88
|
-
)
|
|
89
|
-
.expect("valid regex");
|
|
90
|
-
static ref PROJECT_PATH_RE: Regex =
|
|
91
|
-
Regex::new(r"(?m)^\s*([A-Za-z]:)?[^\r\n]*\.csproj(?:\s|$)").expect("valid regex");
|
|
92
|
-
static ref PRINTABLE_RUN_RE: Regex = Regex::new(r"[\x20-\x7E]{5,}").expect("valid regex");
|
|
93
|
-
static ref DIAGNOSTIC_CODE_RE: Regex =
|
|
94
|
-
Regex::new(r"^[A-Za-z]{2,}\d{3,}$").expect("valid regex");
|
|
95
|
-
static ref SOURCE_FILE_RE: Regex = Regex::new(r"(?i)([A-Za-z]:)?[/\\][^\s]+\.(cs|vb|fs)")
|
|
96
|
-
.expect("valid regex");
|
|
97
|
-
static ref SENSITIVE_ENV_RE: Regex = {
|
|
98
|
-
let keys = SENSITIVE_ENV_VARS
|
|
99
|
-
.iter()
|
|
100
|
-
.map(|key| regex::escape(key))
|
|
101
|
-
.collect::<Vec<_>>()
|
|
102
|
-
.join("|");
|
|
103
|
-
Regex::new(&format!(
|
|
104
|
-
r"(?P<prefix>\b(?:{})\s*(?:=|:)\s*)(?P<value>[^\s;]+)",
|
|
105
|
-
keys
|
|
106
|
-
))
|
|
107
|
-
.expect("valid regex")
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const SENSITIVE_ENV_VARS: &[&str] = &[
|
|
112
|
-
"PATH",
|
|
113
|
-
"HOME",
|
|
114
|
-
"USERPROFILE",
|
|
115
|
-
"USERNAME",
|
|
116
|
-
"USER",
|
|
117
|
-
"APPDATA",
|
|
118
|
-
"LOCALAPPDATA",
|
|
119
|
-
"TEMP",
|
|
120
|
-
"TMP",
|
|
121
|
-
"SSH_AUTH_SOCK",
|
|
122
|
-
"SSH_AGENT_LAUNCHER",
|
|
123
|
-
"GH_TOKEN",
|
|
124
|
-
"GITHUB_TOKEN",
|
|
125
|
-
"GITHUB_PAT",
|
|
126
|
-
"NUGET_API_KEY",
|
|
127
|
-
"NUGET_AUTH_TOKEN",
|
|
128
|
-
"VSS_NUGET_EXTERNAL_FEED_ENDPOINTS",
|
|
129
|
-
"AZURE_DEVOPS_TOKEN",
|
|
130
|
-
"AZURE_CLIENT_SECRET",
|
|
131
|
-
"AZURE_TENANT_ID",
|
|
132
|
-
"AZURE_CLIENT_ID",
|
|
133
|
-
"AWS_ACCESS_KEY_ID",
|
|
134
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
135
|
-
"AWS_SESSION_TOKEN",
|
|
136
|
-
"API_TOKEN",
|
|
137
|
-
"AUTH_TOKEN",
|
|
138
|
-
"ACCESS_TOKEN",
|
|
139
|
-
"BEARER_TOKEN",
|
|
140
|
-
"PASSWORD",
|
|
141
|
-
"CONNECTION_STRING",
|
|
142
|
-
"DATABASE_URL",
|
|
143
|
-
"DOCKER_CONFIG",
|
|
144
|
-
"KUBECONFIG",
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
const RECORD_END_OF_FILE: i32 = 0;
|
|
148
|
-
const RECORD_BUILD_STARTED: i32 = 1;
|
|
149
|
-
const RECORD_BUILD_FINISHED: i32 = 2;
|
|
150
|
-
const RECORD_PROJECT_STARTED: i32 = 3;
|
|
151
|
-
const RECORD_PROJECT_FINISHED: i32 = 4;
|
|
152
|
-
const RECORD_ERROR: i32 = 9;
|
|
153
|
-
const RECORD_WARNING: i32 = 10;
|
|
154
|
-
const RECORD_MESSAGE: i32 = 11;
|
|
155
|
-
const RECORD_CRITICAL_BUILD_MESSAGE: i32 = 13;
|
|
156
|
-
const RECORD_PROJECT_IMPORT_ARCHIVE: i32 = 17;
|
|
157
|
-
const RECORD_NAME_VALUE_LIST: i32 = 23;
|
|
158
|
-
const RECORD_STRING: i32 = 24;
|
|
159
|
-
|
|
160
|
-
const FLAG_BUILD_EVENT_CONTEXT: i32 = 1 << 0;
|
|
161
|
-
const FLAG_MESSAGE: i32 = 1 << 2;
|
|
162
|
-
const FLAG_TIMESTAMP: i32 = 1 << 5;
|
|
163
|
-
const FLAG_ARGUMENTS: i32 = 1 << 14;
|
|
164
|
-
const FLAG_IMPORTANCE: i32 = 1 << 15;
|
|
165
|
-
const FLAG_EXTENDED: i32 = 1 << 16;
|
|
166
|
-
|
|
167
|
-
const STRING_RECORD_START_INDEX: i32 = 10;
|
|
168
|
-
|
|
169
|
-
pub fn parse_build(binlog_path: &Path) -> Result<BuildSummary> {
|
|
170
|
-
let parsed = parse_events_from_binlog(binlog_path)
|
|
171
|
-
.with_context(|| format!("Failed to parse binlog at {}", binlog_path.display()))?;
|
|
172
|
-
let strings_blob = parsed.string_records.join("\n");
|
|
173
|
-
let text_fallback = parse_build_from_text(&strings_blob);
|
|
174
|
-
|
|
175
|
-
let duration_text = match (parsed.build_started_ticks, parsed.build_finished_ticks) {
|
|
176
|
-
(Some(start), Some(end)) if end >= start => Some(format_ticks_duration(end - start)),
|
|
177
|
-
_ => None,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
let parsed_project_count = parsed.project_files.len();
|
|
181
|
-
|
|
182
|
-
Ok(BuildSummary {
|
|
183
|
-
succeeded: parsed.build_succeeded.unwrap_or(false),
|
|
184
|
-
project_count: if parsed_project_count > 0 {
|
|
185
|
-
parsed_project_count
|
|
186
|
-
} else {
|
|
187
|
-
text_fallback.project_count
|
|
188
|
-
},
|
|
189
|
-
errors: select_best_issues(parsed.errors, text_fallback.errors),
|
|
190
|
-
warnings: select_best_issues(parsed.warnings, text_fallback.warnings),
|
|
191
|
-
duration_text,
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
fn select_best_issues(primary: Vec<BinlogIssue>, fallback: Vec<BinlogIssue>) -> Vec<BinlogIssue> {
|
|
196
|
-
if primary.is_empty() {
|
|
197
|
-
return fallback;
|
|
198
|
-
}
|
|
199
|
-
if fallback.is_empty() {
|
|
200
|
-
return primary;
|
|
201
|
-
}
|
|
202
|
-
if primary.iter().all(is_suspicious_issue) && fallback.iter().any(is_contextual_issue) {
|
|
203
|
-
return fallback;
|
|
204
|
-
}
|
|
205
|
-
if issues_quality_score(&fallback) > issues_quality_score(&primary) {
|
|
206
|
-
fallback
|
|
207
|
-
} else {
|
|
208
|
-
primary
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
fn issues_quality_score(issues: &[BinlogIssue]) -> usize {
|
|
213
|
-
issues.iter().map(issue_quality_score).sum()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
fn issue_quality_score(issue: &BinlogIssue) -> usize {
|
|
217
|
-
let mut score = 0;
|
|
218
|
-
if is_contextual_issue(issue) {
|
|
219
|
-
score += 4;
|
|
220
|
-
}
|
|
221
|
-
if !issue.code.is_empty() && is_likely_diagnostic_code(&issue.code) {
|
|
222
|
-
score += 2;
|
|
223
|
-
}
|
|
224
|
-
if issue.line > 0 {
|
|
225
|
-
score += 1;
|
|
226
|
-
}
|
|
227
|
-
if issue.column > 0 {
|
|
228
|
-
score += 1;
|
|
229
|
-
}
|
|
230
|
-
if !issue.message.is_empty() && issue.message != "Build issue" {
|
|
231
|
-
score += 1;
|
|
232
|
-
}
|
|
233
|
-
score
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
fn is_contextual_issue(issue: &BinlogIssue) -> bool {
|
|
237
|
-
!issue.file.is_empty() && !is_likely_diagnostic_code(&issue.file)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
fn is_suspicious_issue(issue: &BinlogIssue) -> bool {
|
|
241
|
-
issue.code.is_empty() && is_likely_diagnostic_code(&issue.file)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
pub fn parse_test(binlog_path: &Path) -> Result<TestSummary> {
|
|
245
|
-
let parsed = parse_events_from_binlog(binlog_path)
|
|
246
|
-
.with_context(|| format!("Failed to parse binlog at {}", binlog_path.display()))?;
|
|
247
|
-
let blob = parsed.string_records.join("\n");
|
|
248
|
-
let mut summary = parse_test_from_text(&blob);
|
|
249
|
-
let parsed_project_count = parsed.project_files.len();
|
|
250
|
-
if parsed_project_count > 0 {
|
|
251
|
-
summary.project_count = parsed_project_count;
|
|
252
|
-
}
|
|
253
|
-
Ok(summary)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
pub fn parse_restore(binlog_path: &Path) -> Result<RestoreSummary> {
|
|
257
|
-
let parsed = parse_events_from_binlog(binlog_path)
|
|
258
|
-
.with_context(|| format!("Failed to parse binlog at {}", binlog_path.display()))?;
|
|
259
|
-
let blob = parsed.string_records.join("\n");
|
|
260
|
-
let mut summary = parse_restore_from_text(&blob);
|
|
261
|
-
let parsed_project_count = parsed.project_files.len();
|
|
262
|
-
if parsed_project_count > 0 {
|
|
263
|
-
summary.restored_projects = parsed_project_count;
|
|
264
|
-
}
|
|
265
|
-
Ok(summary)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
#[derive(Default)]
|
|
269
|
-
struct ParsedBinlog {
|
|
270
|
-
string_records: Vec<String>,
|
|
271
|
-
messages: Vec<String>,
|
|
272
|
-
project_files: HashSet<String>,
|
|
273
|
-
errors: Vec<BinlogIssue>,
|
|
274
|
-
warnings: Vec<BinlogIssue>,
|
|
275
|
-
build_succeeded: Option<bool>,
|
|
276
|
-
build_started_ticks: Option<i64>,
|
|
277
|
-
build_finished_ticks: Option<i64>,
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
#[derive(Default)]
|
|
281
|
-
struct ParsedEventFields {
|
|
282
|
-
message: Option<String>,
|
|
283
|
-
timestamp_ticks: Option<i64>,
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
fn parse_events_from_binlog(path: &Path) -> Result<ParsedBinlog> {
|
|
287
|
-
let bytes = std::fs::read(path)
|
|
288
|
-
.with_context(|| format!("Failed to read binlog at {}", path.display()))?;
|
|
289
|
-
if bytes.is_empty() {
|
|
290
|
-
anyhow::bail!("Failed to parse binlog at {}: empty file", path.display());
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
let mut decoder = GzDecoder::new(bytes.as_slice());
|
|
294
|
-
let mut payload = Vec::new();
|
|
295
|
-
decoder.read_to_end(&mut payload).with_context(|| {
|
|
296
|
-
format!(
|
|
297
|
-
"Failed to parse binlog at {}: gzip decode failed",
|
|
298
|
-
path.display()
|
|
299
|
-
)
|
|
300
|
-
})?;
|
|
301
|
-
|
|
302
|
-
let mut reader = BinReader::new(&payload);
|
|
303
|
-
let file_format_version = reader
|
|
304
|
-
.read_i32_le()
|
|
305
|
-
.context("binlog header missing file format version")?;
|
|
306
|
-
let _minimum_reader_version = reader
|
|
307
|
-
.read_i32_le()
|
|
308
|
-
.context("binlog header missing minimum reader version")?;
|
|
309
|
-
|
|
310
|
-
if file_format_version < 18 {
|
|
311
|
-
anyhow::bail!(
|
|
312
|
-
"Failed to parse binlog at {}: unsupported binlog format {}",
|
|
313
|
-
path.display(),
|
|
314
|
-
file_format_version
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let mut parsed = ParsedBinlog::default();
|
|
319
|
-
|
|
320
|
-
while !reader.is_eof() {
|
|
321
|
-
let kind = reader
|
|
322
|
-
.read_7bit_i32()
|
|
323
|
-
.context("failed to read record kind")?;
|
|
324
|
-
if kind == RECORD_END_OF_FILE {
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
match kind {
|
|
329
|
-
RECORD_STRING => {
|
|
330
|
-
let text = reader
|
|
331
|
-
.read_dotnet_string()
|
|
332
|
-
.context("failed to read string record")?;
|
|
333
|
-
parsed.string_records.push(text);
|
|
334
|
-
}
|
|
335
|
-
RECORD_NAME_VALUE_LIST | RECORD_PROJECT_IMPORT_ARCHIVE => {
|
|
336
|
-
let len = reader
|
|
337
|
-
.read_7bit_i32()
|
|
338
|
-
.context("failed to read record length")?;
|
|
339
|
-
if len < 0 {
|
|
340
|
-
anyhow::bail!("negative record length: {}", len);
|
|
341
|
-
}
|
|
342
|
-
reader
|
|
343
|
-
.skip(len as usize)
|
|
344
|
-
.context("failed to skip auxiliary record payload")?;
|
|
345
|
-
}
|
|
346
|
-
_ => {
|
|
347
|
-
let len = reader
|
|
348
|
-
.read_7bit_i32()
|
|
349
|
-
.context("failed to read event length")?;
|
|
350
|
-
if len < 0 {
|
|
351
|
-
anyhow::bail!("negative event length: {}", len);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
let payload = reader
|
|
355
|
-
.read_exact(len as usize)
|
|
356
|
-
.context("failed to read event payload")?;
|
|
357
|
-
let mut event_reader = BinReader::new(payload);
|
|
358
|
-
let _ =
|
|
359
|
-
parse_event_record(kind, &mut event_reader, file_format_version, &mut parsed);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
Ok(parsed)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
fn parse_event_record(
|
|
368
|
-
kind: i32,
|
|
369
|
-
reader: &mut BinReader<'_>,
|
|
370
|
-
file_format_version: i32,
|
|
371
|
-
parsed: &mut ParsedBinlog,
|
|
372
|
-
) -> Result<()> {
|
|
373
|
-
match kind {
|
|
374
|
-
RECORD_BUILD_STARTED => {
|
|
375
|
-
let fields = read_event_fields(reader, file_format_version, parsed, false)?;
|
|
376
|
-
parsed.build_started_ticks = fields.timestamp_ticks;
|
|
377
|
-
}
|
|
378
|
-
RECORD_BUILD_FINISHED => {
|
|
379
|
-
let fields = read_event_fields(reader, file_format_version, parsed, false)?;
|
|
380
|
-
parsed.build_finished_ticks = fields.timestamp_ticks;
|
|
381
|
-
parsed.build_succeeded = Some(reader.read_bool()?);
|
|
382
|
-
}
|
|
383
|
-
RECORD_PROJECT_STARTED => {
|
|
384
|
-
let _fields = read_event_fields(reader, file_format_version, parsed, false)?;
|
|
385
|
-
if reader.read_bool()? {
|
|
386
|
-
skip_build_event_context(reader, file_format_version)?;
|
|
387
|
-
}
|
|
388
|
-
if let Some(project_file) = read_optional_string(reader, parsed)? {
|
|
389
|
-
if !project_file.is_empty() {
|
|
390
|
-
parsed.project_files.insert(project_file);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
RECORD_PROJECT_FINISHED => {
|
|
395
|
-
let _fields = read_event_fields(reader, file_format_version, parsed, false)?;
|
|
396
|
-
if let Some(project_file) = read_optional_string(reader, parsed)? {
|
|
397
|
-
if !project_file.is_empty() {
|
|
398
|
-
parsed.project_files.insert(project_file);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
let _ = reader.read_bool()?;
|
|
402
|
-
}
|
|
403
|
-
RECORD_ERROR | RECORD_WARNING => {
|
|
404
|
-
let fields = read_event_fields(reader, file_format_version, parsed, false)?;
|
|
405
|
-
|
|
406
|
-
let _subcategory = read_optional_string(reader, parsed)?;
|
|
407
|
-
let code = read_optional_string(reader, parsed)?.unwrap_or_default();
|
|
408
|
-
let file = read_optional_string(reader, parsed)?.unwrap_or_default();
|
|
409
|
-
let _project_file = read_optional_string(reader, parsed)?;
|
|
410
|
-
let line = reader.read_7bit_i32()?.max(0) as u32;
|
|
411
|
-
let column = reader.read_7bit_i32()?.max(0) as u32;
|
|
412
|
-
let _ = reader.read_7bit_i32()?;
|
|
413
|
-
let _ = reader.read_7bit_i32()?;
|
|
414
|
-
|
|
415
|
-
let issue = BinlogIssue {
|
|
416
|
-
code,
|
|
417
|
-
file,
|
|
418
|
-
line,
|
|
419
|
-
column,
|
|
420
|
-
message: fields.message.unwrap_or_default(),
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
if kind == RECORD_ERROR {
|
|
424
|
-
parsed.errors.push(issue);
|
|
425
|
-
} else {
|
|
426
|
-
parsed.warnings.push(issue);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
RECORD_MESSAGE => {
|
|
430
|
-
let fields = read_event_fields(reader, file_format_version, parsed, true)?;
|
|
431
|
-
if let Some(message) = fields.message {
|
|
432
|
-
parsed.messages.push(message);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
RECORD_CRITICAL_BUILD_MESSAGE => {
|
|
436
|
-
let fields = read_event_fields(reader, file_format_version, parsed, false)?;
|
|
437
|
-
if let Some(message) = fields.message {
|
|
438
|
-
parsed.messages.push(message);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
_ => {}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
Ok(())
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
fn read_event_fields(
|
|
448
|
-
reader: &mut BinReader<'_>,
|
|
449
|
-
file_format_version: i32,
|
|
450
|
-
parsed: &ParsedBinlog,
|
|
451
|
-
read_importance: bool,
|
|
452
|
-
) -> Result<ParsedEventFields> {
|
|
453
|
-
let flags = reader.read_7bit_i32()?;
|
|
454
|
-
let mut result = ParsedEventFields::default();
|
|
455
|
-
|
|
456
|
-
if flags & FLAG_MESSAGE != 0 {
|
|
457
|
-
result.message = read_deduplicated_string(reader, parsed)?;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if flags & FLAG_BUILD_EVENT_CONTEXT != 0 {
|
|
461
|
-
skip_build_event_context(reader, file_format_version)?;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if flags & FLAG_TIMESTAMP != 0 {
|
|
465
|
-
result.timestamp_ticks = Some(reader.read_i64_le()?);
|
|
466
|
-
let _ = reader.read_7bit_i32()?;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if flags & FLAG_EXTENDED != 0 {
|
|
470
|
-
let _ = read_optional_string(reader, parsed)?;
|
|
471
|
-
skip_string_dictionary(reader, file_format_version)?;
|
|
472
|
-
let _ = read_optional_string(reader, parsed)?;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if flags & FLAG_ARGUMENTS != 0 {
|
|
476
|
-
let count = reader.read_7bit_i32()?.max(0) as usize;
|
|
477
|
-
for _ in 0..count {
|
|
478
|
-
let _ = read_deduplicated_string(reader, parsed)?;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (file_format_version < 13 && read_importance) || (flags & FLAG_IMPORTANCE != 0) {
|
|
483
|
-
let _ = reader.read_7bit_i32()?;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
Ok(result)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
fn skip_build_event_context(reader: &mut BinReader<'_>, file_format_version: i32) -> Result<()> {
|
|
490
|
-
let count = if file_format_version > 1 { 7 } else { 6 };
|
|
491
|
-
for _ in 0..count {
|
|
492
|
-
let _ = reader.read_7bit_i32()?;
|
|
493
|
-
}
|
|
494
|
-
Ok(())
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
fn skip_string_dictionary(reader: &mut BinReader<'_>, file_format_version: i32) -> Result<()> {
|
|
498
|
-
if file_format_version < 10 {
|
|
499
|
-
anyhow::bail!("legacy dictionary format is unsupported");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
let _ = reader.read_7bit_i32()?;
|
|
503
|
-
Ok(())
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
fn read_optional_string(
|
|
507
|
-
reader: &mut BinReader<'_>,
|
|
508
|
-
parsed: &ParsedBinlog,
|
|
509
|
-
) -> Result<Option<String>> {
|
|
510
|
-
read_deduplicated_string(reader, parsed)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
fn read_deduplicated_string(
|
|
514
|
-
reader: &mut BinReader<'_>,
|
|
515
|
-
parsed: &ParsedBinlog,
|
|
516
|
-
) -> Result<Option<String>> {
|
|
517
|
-
let index = reader.read_7bit_i32()?;
|
|
518
|
-
if index == 0 {
|
|
519
|
-
return Ok(None);
|
|
520
|
-
}
|
|
521
|
-
if index == 1 {
|
|
522
|
-
return Ok(Some(String::new()));
|
|
523
|
-
}
|
|
524
|
-
if index < STRING_RECORD_START_INDEX {
|
|
525
|
-
return Ok(None);
|
|
526
|
-
}
|
|
527
|
-
let record_idx = (index - STRING_RECORD_START_INDEX) as usize;
|
|
528
|
-
parsed
|
|
529
|
-
.string_records
|
|
530
|
-
.get(record_idx)
|
|
531
|
-
.cloned()
|
|
532
|
-
.map(Some)
|
|
533
|
-
.with_context(|| format!("invalid string record index {}", index))
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
fn format_ticks_duration(ticks: i64) -> String {
|
|
537
|
-
let total_seconds = ticks.div_euclid(10_000_000);
|
|
538
|
-
let centiseconds = ticks.rem_euclid(10_000_000) / 100_000;
|
|
539
|
-
let hours = total_seconds / 3600;
|
|
540
|
-
let minutes = (total_seconds % 3600) / 60;
|
|
541
|
-
let seconds = total_seconds % 60;
|
|
542
|
-
format!(
|
|
543
|
-
"{:02}:{:02}:{:02}.{:02}",
|
|
544
|
-
hours, minutes, seconds, centiseconds
|
|
545
|
-
)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
struct BinReader<'a> {
|
|
549
|
-
cursor: Cursor<&'a [u8]>,
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
impl<'a> BinReader<'a> {
|
|
553
|
-
fn new(bytes: &'a [u8]) -> Self {
|
|
554
|
-
Self {
|
|
555
|
-
cursor: Cursor::new(bytes),
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
fn is_eof(&self) -> bool {
|
|
560
|
-
(self.cursor.position() as usize) >= self.cursor.get_ref().len()
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
|
|
564
|
-
let start = self.cursor.position() as usize;
|
|
565
|
-
let end = start.saturating_add(len);
|
|
566
|
-
if end > self.cursor.get_ref().len() {
|
|
567
|
-
anyhow::bail!("unexpected end of stream");
|
|
568
|
-
}
|
|
569
|
-
self.cursor.set_position(end as u64);
|
|
570
|
-
Ok(&self.cursor.get_ref()[start..end])
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
fn skip(&mut self, len: usize) -> Result<()> {
|
|
574
|
-
let _ = self.read_exact(len)?;
|
|
575
|
-
Ok(())
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
fn read_u8(&mut self) -> Result<u8> {
|
|
579
|
-
Ok(self.read_exact(1)?[0])
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
fn read_bool(&mut self) -> Result<bool> {
|
|
583
|
-
Ok(self.read_u8()? != 0)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
fn read_i32_le(&mut self) -> Result<i32> {
|
|
587
|
-
let b = self.read_exact(4)?;
|
|
588
|
-
Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
fn read_i64_le(&mut self) -> Result<i64> {
|
|
592
|
-
let b = self.read_exact(8)?;
|
|
593
|
-
Ok(i64::from_le_bytes([
|
|
594
|
-
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
|
|
595
|
-
]))
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
fn read_7bit_i32(&mut self) -> Result<i32> {
|
|
599
|
-
let mut value: u32 = 0;
|
|
600
|
-
let mut shift = 0;
|
|
601
|
-
loop {
|
|
602
|
-
let byte = self.read_u8()?;
|
|
603
|
-
value |= ((byte & 0x7F) as u32) << shift;
|
|
604
|
-
if (byte & 0x80) == 0 {
|
|
605
|
-
return Ok(value as i32);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
shift += 7;
|
|
609
|
-
if shift >= 35 {
|
|
610
|
-
anyhow::bail!("invalid 7-bit encoded integer");
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
fn read_dotnet_string(&mut self) -> Result<String> {
|
|
616
|
-
let len = self.read_7bit_i32()?;
|
|
617
|
-
if len < 0 {
|
|
618
|
-
anyhow::bail!("negative string length: {}", len);
|
|
619
|
-
}
|
|
620
|
-
let bytes = self.read_exact(len as usize)?;
|
|
621
|
-
String::from_utf8(bytes.to_vec()).context("invalid UTF-8 string")
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
pub fn scrub_sensitive_env_vars(input: &str) -> String {
|
|
626
|
-
SENSITIVE_ENV_RE
|
|
627
|
-
.replace_all(input, "${prefix}[REDACTED]")
|
|
628
|
-
.into_owned()
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
pub fn parse_build_from_text(text: &str) -> BuildSummary {
|
|
632
|
-
let clean = strip_ansi(text);
|
|
633
|
-
let scrubbed = scrub_sensitive_env_vars(&clean);
|
|
634
|
-
let mut seen_errors: HashSet<(String, String, u32, u32, String)> = HashSet::new();
|
|
635
|
-
let mut seen_warnings: HashSet<(String, String, u32, u32, String)> = HashSet::new();
|
|
636
|
-
let mut summary = BuildSummary {
|
|
637
|
-
succeeded: scrubbed.contains("Build succeeded") && !scrubbed.contains("Build FAILED"),
|
|
638
|
-
project_count: count_projects(&scrubbed),
|
|
639
|
-
errors: Vec::new(),
|
|
640
|
-
warnings: Vec::new(),
|
|
641
|
-
duration_text: extract_duration(&scrubbed),
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
for captures in ISSUE_RE.captures_iter(&scrubbed) {
|
|
645
|
-
let issue = BinlogIssue {
|
|
646
|
-
code: captures
|
|
647
|
-
.name("code")
|
|
648
|
-
.map(|m| m.as_str().to_string())
|
|
649
|
-
.unwrap_or_default(),
|
|
650
|
-
file: captures
|
|
651
|
-
.name("file")
|
|
652
|
-
.map(|m| m.as_str().to_string())
|
|
653
|
-
.unwrap_or_default(),
|
|
654
|
-
line: captures
|
|
655
|
-
.name("line")
|
|
656
|
-
.and_then(|m| m.as_str().parse::<u32>().ok())
|
|
657
|
-
.unwrap_or(0),
|
|
658
|
-
column: captures
|
|
659
|
-
.name("column")
|
|
660
|
-
.and_then(|m| m.as_str().parse::<u32>().ok())
|
|
661
|
-
.unwrap_or(0),
|
|
662
|
-
message: captures
|
|
663
|
-
.name("msg")
|
|
664
|
-
.map(|m| {
|
|
665
|
-
let msg = m.as_str().trim();
|
|
666
|
-
if msg.is_empty() {
|
|
667
|
-
"diagnostic without message".to_string()
|
|
668
|
-
} else {
|
|
669
|
-
msg.to_string()
|
|
670
|
-
}
|
|
671
|
-
})
|
|
672
|
-
.unwrap_or_default(),
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
let key = (
|
|
676
|
-
issue.code.clone(),
|
|
677
|
-
issue.file.clone(),
|
|
678
|
-
issue.line,
|
|
679
|
-
issue.column,
|
|
680
|
-
issue.message.clone(),
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
match captures.name("kind").map(|m| m.as_str()) {
|
|
684
|
-
Some("error") => {
|
|
685
|
-
if seen_errors.insert(key) {
|
|
686
|
-
summary.errors.push(issue);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
Some("warning") => {
|
|
690
|
-
if seen_warnings.insert(key) {
|
|
691
|
-
summary.warnings.push(issue);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
_ => {}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if summary.errors.is_empty() || summary.warnings.is_empty() {
|
|
699
|
-
let mut warning_count_from_summary = 0;
|
|
700
|
-
let mut error_count_from_summary = 0;
|
|
701
|
-
|
|
702
|
-
for captures in BUILD_SUMMARY_RE.captures_iter(&scrubbed) {
|
|
703
|
-
let count = captures
|
|
704
|
-
.name("count")
|
|
705
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
706
|
-
.unwrap_or(0);
|
|
707
|
-
|
|
708
|
-
match captures
|
|
709
|
-
.name("kind")
|
|
710
|
-
.map(|m| m.as_str().to_ascii_lowercase())
|
|
711
|
-
.as_deref()
|
|
712
|
-
{
|
|
713
|
-
Some("warning") => {
|
|
714
|
-
warning_count_from_summary = warning_count_from_summary.max(count)
|
|
715
|
-
}
|
|
716
|
-
Some("error") => error_count_from_summary = error_count_from_summary.max(count),
|
|
717
|
-
_ => {}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
let inline_error_count = ERROR_COUNT_RE
|
|
722
|
-
.captures_iter(&scrubbed)
|
|
723
|
-
.filter_map(|captures| {
|
|
724
|
-
captures
|
|
725
|
-
.name("count")
|
|
726
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
727
|
-
})
|
|
728
|
-
.max()
|
|
729
|
-
.unwrap_or(0);
|
|
730
|
-
let inline_warning_count = WARNING_COUNT_RE
|
|
731
|
-
.captures_iter(&scrubbed)
|
|
732
|
-
.filter_map(|captures| {
|
|
733
|
-
captures
|
|
734
|
-
.name("count")
|
|
735
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
736
|
-
})
|
|
737
|
-
.max()
|
|
738
|
-
.unwrap_or(0);
|
|
739
|
-
|
|
740
|
-
warning_count_from_summary = warning_count_from_summary.max(inline_warning_count);
|
|
741
|
-
error_count_from_summary = error_count_from_summary.max(inline_error_count);
|
|
742
|
-
|
|
743
|
-
if summary.errors.is_empty() {
|
|
744
|
-
for idx in 0..error_count_from_summary {
|
|
745
|
-
summary.errors.push(BinlogIssue {
|
|
746
|
-
code: String::new(),
|
|
747
|
-
file: String::new(),
|
|
748
|
-
line: 0,
|
|
749
|
-
column: 0,
|
|
750
|
-
message: format!("Build error #{} (details omitted)", idx + 1),
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if summary.warnings.is_empty() {
|
|
756
|
-
for idx in 0..warning_count_from_summary {
|
|
757
|
-
summary.warnings.push(BinlogIssue {
|
|
758
|
-
code: String::new(),
|
|
759
|
-
file: String::new(),
|
|
760
|
-
line: 0,
|
|
761
|
-
column: 0,
|
|
762
|
-
message: format!("Build warning #{} (details omitted)", idx + 1),
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if summary.errors.is_empty() {
|
|
768
|
-
let fallback_error_lines = FALLBACK_ERROR_LINE_RE.captures_iter(&scrubbed).count();
|
|
769
|
-
for idx in 0..fallback_error_lines {
|
|
770
|
-
summary.errors.push(BinlogIssue {
|
|
771
|
-
code: String::new(),
|
|
772
|
-
file: String::new(),
|
|
773
|
-
line: 0,
|
|
774
|
-
column: 0,
|
|
775
|
-
message: format!("Build error #{} (details omitted)", idx + 1),
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if summary.warnings.is_empty() {
|
|
781
|
-
let fallback_warning_lines = FALLBACK_WARNING_LINE_RE.captures_iter(&scrubbed).count();
|
|
782
|
-
for idx in 0..fallback_warning_lines {
|
|
783
|
-
summary.warnings.push(BinlogIssue {
|
|
784
|
-
code: String::new(),
|
|
785
|
-
file: String::new(),
|
|
786
|
-
line: 0,
|
|
787
|
-
column: 0,
|
|
788
|
-
message: format!("Build warning #{} (details omitted)", idx + 1),
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
let has_error_signal = scrubbed.contains("Build FAILED")
|
|
795
|
-
|| scrubbed.contains(": error ")
|
|
796
|
-
|| BUILD_SUMMARY_RE.captures_iter(&scrubbed).any(|captures| {
|
|
797
|
-
let is_error = matches!(
|
|
798
|
-
captures
|
|
799
|
-
.name("kind")
|
|
800
|
-
.map(|m| m.as_str().to_ascii_lowercase())
|
|
801
|
-
.as_deref(),
|
|
802
|
-
Some("error")
|
|
803
|
-
);
|
|
804
|
-
let count = captures
|
|
805
|
-
.name("count")
|
|
806
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
807
|
-
.unwrap_or(0);
|
|
808
|
-
is_error && count > 0
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
if summary.errors.is_empty() || summary.warnings.is_empty() {
|
|
812
|
-
let (diagnostic_errors, diagnostic_warnings) = parse_restore_issues_from_text(&scrubbed);
|
|
813
|
-
|
|
814
|
-
if summary.errors.is_empty() {
|
|
815
|
-
summary.errors = diagnostic_errors;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
if summary.warnings.is_empty() {
|
|
819
|
-
summary.warnings = diagnostic_warnings;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if summary.errors.is_empty() && !summary.succeeded && has_error_signal {
|
|
824
|
-
summary.errors = extract_binary_like_issues(&scrubbed);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if summary.project_count == 0
|
|
828
|
-
&& (scrubbed.contains("Build succeeded")
|
|
829
|
-
|| scrubbed.contains("Build FAILED")
|
|
830
|
-
|| scrubbed.contains(" -> "))
|
|
831
|
-
{
|
|
832
|
-
summary.project_count = 1;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
summary
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
pub fn parse_test_from_text(text: &str) -> TestSummary {
|
|
839
|
-
let clean = strip_ansi(text);
|
|
840
|
-
let scrubbed = scrub_sensitive_env_vars(&clean);
|
|
841
|
-
let mut summary = TestSummary {
|
|
842
|
-
passed: 0,
|
|
843
|
-
failed: 0,
|
|
844
|
-
skipped: 0,
|
|
845
|
-
total: 0,
|
|
846
|
-
project_count: count_projects(&scrubbed).max(1),
|
|
847
|
-
failed_tests: Vec::new(),
|
|
848
|
-
duration_text: extract_duration(&scrubbed),
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
let mut found_summary_line = false;
|
|
852
|
-
let mut fallback_duration = None;
|
|
853
|
-
for captures in TEST_RESULT_RE.captures_iter(&scrubbed) {
|
|
854
|
-
found_summary_line = true;
|
|
855
|
-
summary.passed += captures
|
|
856
|
-
.name("passed")
|
|
857
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
858
|
-
.unwrap_or(0);
|
|
859
|
-
summary.failed += captures
|
|
860
|
-
.name("failed")
|
|
861
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
862
|
-
.unwrap_or(0);
|
|
863
|
-
summary.skipped += captures
|
|
864
|
-
.name("skipped")
|
|
865
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
866
|
-
.unwrap_or(0);
|
|
867
|
-
summary.total += captures
|
|
868
|
-
.name("total")
|
|
869
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
870
|
-
.unwrap_or(0);
|
|
871
|
-
|
|
872
|
-
if let Some(duration) = captures.name("duration") {
|
|
873
|
-
fallback_duration = Some(duration.as_str().trim().to_string());
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
if found_summary_line && summary.duration_text.is_none() {
|
|
878
|
-
summary.duration_text = fallback_duration;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if let Some(captures) = TEST_SUMMARY_RE.captures_iter(&scrubbed).last() {
|
|
882
|
-
summary.passed = captures
|
|
883
|
-
.name("passed")
|
|
884
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
885
|
-
.unwrap_or(summary.passed);
|
|
886
|
-
summary.failed = captures
|
|
887
|
-
.name("failed")
|
|
888
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
889
|
-
.unwrap_or(summary.failed);
|
|
890
|
-
summary.skipped = captures
|
|
891
|
-
.name("skipped")
|
|
892
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
893
|
-
.unwrap_or(summary.skipped);
|
|
894
|
-
summary.total = captures
|
|
895
|
-
.name("total")
|
|
896
|
-
.and_then(|m| m.as_str().parse::<usize>().ok())
|
|
897
|
-
.unwrap_or(summary.total);
|
|
898
|
-
|
|
899
|
-
if let Some(duration) = captures.name("duration") {
|
|
900
|
-
summary.duration_text = Some(duration.as_str().trim().to_string());
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
let lines: Vec<&str> = scrubbed.lines().collect();
|
|
905
|
-
let mut idx = 0;
|
|
906
|
-
while idx < lines.len() {
|
|
907
|
-
let line = lines[idx];
|
|
908
|
-
if let Some(captures) = FAILED_TEST_HEAD_RE.captures(line) {
|
|
909
|
-
let name = captures
|
|
910
|
-
.name("name")
|
|
911
|
-
.map(|m| m.as_str().trim().to_string())
|
|
912
|
-
.unwrap_or_else(|| "unknown".to_string());
|
|
913
|
-
let mut details = Vec::new();
|
|
914
|
-
idx += 1;
|
|
915
|
-
while idx < lines.len() {
|
|
916
|
-
let detail_line = lines[idx].trim_end();
|
|
917
|
-
if FAILED_TEST_HEAD_RE.is_match(detail_line) {
|
|
918
|
-
idx = idx.saturating_sub(1);
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
921
|
-
let detail_trimmed = detail_line.trim_start();
|
|
922
|
-
if detail_trimmed.starts_with("Failed! -")
|
|
923
|
-
|| detail_trimmed.starts_with("Passed! -")
|
|
924
|
-
|| detail_trimmed.starts_with("Test summary:")
|
|
925
|
-
|| detail_trimmed.starts_with("Build ")
|
|
926
|
-
{
|
|
927
|
-
idx = idx.saturating_sub(1);
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if detail_line.trim().is_empty() {
|
|
932
|
-
if !details.is_empty() {
|
|
933
|
-
details.push(String::new());
|
|
934
|
-
}
|
|
935
|
-
} else {
|
|
936
|
-
details.push(detail_line.trim().to_string());
|
|
937
|
-
}
|
|
938
|
-
if details.len() >= 20 {
|
|
939
|
-
break;
|
|
940
|
-
}
|
|
941
|
-
idx += 1;
|
|
942
|
-
}
|
|
943
|
-
summary.failed_tests.push(FailedTest { name, details });
|
|
944
|
-
}
|
|
945
|
-
idx += 1;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if summary.failed == 0 {
|
|
949
|
-
summary.failed = summary.failed_tests.len();
|
|
950
|
-
}
|
|
951
|
-
if summary.total == 0 {
|
|
952
|
-
summary.total = summary.passed + summary.failed + summary.skipped;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
summary
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
pub fn parse_restore_from_text(text: &str) -> RestoreSummary {
|
|
959
|
-
let (errors, warnings) = parse_restore_issues_from_text(text);
|
|
960
|
-
let clean = strip_ansi(text);
|
|
961
|
-
let scrubbed = scrub_sensitive_env_vars(&clean);
|
|
962
|
-
|
|
963
|
-
RestoreSummary {
|
|
964
|
-
restored_projects: RESTORE_PROJECT_RE.captures_iter(&scrubbed).count(),
|
|
965
|
-
warnings: warnings.len(),
|
|
966
|
-
errors: errors.len(),
|
|
967
|
-
duration_text: extract_duration(&scrubbed),
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
pub fn parse_restore_issues_from_text(text: &str) -> (Vec<BinlogIssue>, Vec<BinlogIssue>) {
|
|
972
|
-
let clean = strip_ansi(text);
|
|
973
|
-
let scrubbed = scrub_sensitive_env_vars(&clean);
|
|
974
|
-
let mut errors = Vec::new();
|
|
975
|
-
let mut warnings = Vec::new();
|
|
976
|
-
let mut seen_errors: HashSet<(String, String, u32, u32, String)> = HashSet::new();
|
|
977
|
-
let mut seen_warnings: HashSet<(String, String, u32, u32, String)> = HashSet::new();
|
|
978
|
-
|
|
979
|
-
for captures in RESTORE_DIAGNOSTIC_RE.captures_iter(&scrubbed) {
|
|
980
|
-
let issue = BinlogIssue {
|
|
981
|
-
code: captures
|
|
982
|
-
.name("code")
|
|
983
|
-
.map(|m| m.as_str().trim().to_string())
|
|
984
|
-
.unwrap_or_default(),
|
|
985
|
-
file: captures
|
|
986
|
-
.name("file")
|
|
987
|
-
.map(|m| m.as_str().trim().to_string())
|
|
988
|
-
.unwrap_or_default(),
|
|
989
|
-
line: 0,
|
|
990
|
-
column: 0,
|
|
991
|
-
message: captures
|
|
992
|
-
.name("msg")
|
|
993
|
-
.map(|m| m.as_str().trim().to_string())
|
|
994
|
-
.unwrap_or_default(),
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
let key = (
|
|
998
|
-
issue.code.clone(),
|
|
999
|
-
issue.file.clone(),
|
|
1000
|
-
issue.line,
|
|
1001
|
-
issue.column,
|
|
1002
|
-
issue.message.clone(),
|
|
1003
|
-
);
|
|
1004
|
-
|
|
1005
|
-
match captures
|
|
1006
|
-
.name("kind")
|
|
1007
|
-
.map(|m| m.as_str().to_ascii_lowercase())
|
|
1008
|
-
{
|
|
1009
|
-
Some(kind) if kind == "error" => {
|
|
1010
|
-
if seen_errors.insert(key) {
|
|
1011
|
-
errors.push(issue);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
Some(kind) if kind == "warning" => {
|
|
1015
|
-
if seen_warnings.insert(key) {
|
|
1016
|
-
warnings.push(issue);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
_ => {}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
(errors, warnings)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
fn count_projects(text: &str) -> usize {
|
|
1027
|
-
PROJECT_PATH_RE.captures_iter(text).count()
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
fn extract_duration(text: &str) -> Option<String> {
|
|
1031
|
-
DURATION_RE
|
|
1032
|
-
.captures(text)
|
|
1033
|
-
.and_then(|c| c.name("duration"))
|
|
1034
|
-
.map(|m| m.as_str().trim().to_string())
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
fn extract_printable_runs(text: &str) -> Vec<String> {
|
|
1038
|
-
let mut runs = Vec::new();
|
|
1039
|
-
for captures in PRINTABLE_RUN_RE.captures_iter(text) {
|
|
1040
|
-
let Some(matched) = captures.get(0) else {
|
|
1041
|
-
continue;
|
|
1042
|
-
};
|
|
1043
|
-
|
|
1044
|
-
let run = matched.as_str().trim();
|
|
1045
|
-
if run.len() < 5 {
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
runs.push(run.to_string());
|
|
1049
|
-
}
|
|
1050
|
-
runs
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
fn extract_binary_like_issues(text: &str) -> Vec<BinlogIssue> {
|
|
1054
|
-
let runs = extract_printable_runs(text);
|
|
1055
|
-
if runs.is_empty() {
|
|
1056
|
-
return Vec::new();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
let mut issues = Vec::new();
|
|
1060
|
-
let mut seen: HashSet<(String, String, String)> = HashSet::new();
|
|
1061
|
-
|
|
1062
|
-
for idx in 0..runs.len() {
|
|
1063
|
-
let code = runs[idx].trim();
|
|
1064
|
-
if !DIAGNOSTIC_CODE_RE.is_match(code) || !is_likely_diagnostic_code(code) {
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
let message = (1..=4)
|
|
1069
|
-
.filter_map(|delta| idx.checked_sub(delta))
|
|
1070
|
-
.map(|j| runs[j].trim())
|
|
1071
|
-
.find(|candidate| {
|
|
1072
|
-
!DIAGNOSTIC_CODE_RE.is_match(candidate)
|
|
1073
|
-
&& !SOURCE_FILE_RE.is_match(candidate)
|
|
1074
|
-
&& candidate.chars().any(|c| c.is_ascii_alphabetic())
|
|
1075
|
-
&& candidate.contains(' ')
|
|
1076
|
-
&& !candidate.contains("Copyright")
|
|
1077
|
-
&& !candidate.contains("Compiler version")
|
|
1078
|
-
})
|
|
1079
|
-
.unwrap_or("Build issue")
|
|
1080
|
-
.to_string();
|
|
1081
|
-
|
|
1082
|
-
let file = (1..=4)
|
|
1083
|
-
.filter_map(|delta| runs.get(idx + delta))
|
|
1084
|
-
.find_map(|candidate| {
|
|
1085
|
-
SOURCE_FILE_RE
|
|
1086
|
-
.captures(candidate)
|
|
1087
|
-
.and_then(|caps| caps.get(0))
|
|
1088
|
-
.map(|m| m.as_str().to_string())
|
|
1089
|
-
})
|
|
1090
|
-
.unwrap_or_default();
|
|
1091
|
-
|
|
1092
|
-
if file.is_empty() && message == "Build issue" {
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
let key = (code.to_string(), file.clone(), message.clone());
|
|
1097
|
-
if !seen.insert(key) {
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
issues.push(BinlogIssue {
|
|
1102
|
-
code: code.to_string(),
|
|
1103
|
-
file,
|
|
1104
|
-
line: 0,
|
|
1105
|
-
column: 0,
|
|
1106
|
-
message,
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
issues
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
fn is_likely_diagnostic_code(code: &str) -> bool {
|
|
1114
|
-
const ALLOWED_PREFIXES: &[&str] = &[
|
|
1115
|
-
"CS", "MSB", "NU", "FS", "BC", "CA", "SA", "IDE", "IL", "VB", "AD", "TS", "C", "LNK",
|
|
1116
|
-
];
|
|
1117
|
-
|
|
1118
|
-
ALLOWED_PREFIXES
|
|
1119
|
-
.iter()
|
|
1120
|
-
.any(|prefix| code.starts_with(prefix))
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
#[cfg(test)]
|
|
1124
|
-
mod tests {
|
|
1125
|
-
use super::*;
|
|
1126
|
-
use flate2::write::GzEncoder;
|
|
1127
|
-
use flate2::Compression;
|
|
1128
|
-
use std::io::Write;
|
|
1129
|
-
|
|
1130
|
-
fn write_7bit_i32(buf: &mut Vec<u8>, value: i32) {
|
|
1131
|
-
let mut v = value as u32;
|
|
1132
|
-
while v >= 0x80 {
|
|
1133
|
-
buf.push(((v as u8) & 0x7F) | 0x80);
|
|
1134
|
-
v >>= 7;
|
|
1135
|
-
}
|
|
1136
|
-
buf.push(v as u8);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
fn write_dotnet_string(buf: &mut Vec<u8>, value: &str) {
|
|
1140
|
-
write_7bit_i32(buf, value.len() as i32);
|
|
1141
|
-
buf.extend_from_slice(value.as_bytes());
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
fn write_event_record(target: &mut Vec<u8>, kind: i32, payload: &[u8]) {
|
|
1145
|
-
write_7bit_i32(target, kind);
|
|
1146
|
-
write_7bit_i32(target, payload.len() as i32);
|
|
1147
|
-
target.extend_from_slice(payload);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
fn build_minimal_binlog(records: &[u8]) -> Vec<u8> {
|
|
1151
|
-
let mut plain = Vec::new();
|
|
1152
|
-
plain.extend_from_slice(&25_i32.to_le_bytes());
|
|
1153
|
-
plain.extend_from_slice(&18_i32.to_le_bytes());
|
|
1154
|
-
plain.extend_from_slice(records);
|
|
1155
|
-
|
|
1156
|
-
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
1157
|
-
encoder.write_all(&plain).expect("write plain payload");
|
|
1158
|
-
encoder.finish().expect("finish gzip")
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
#[test]
|
|
1162
|
-
fn test_scrub_sensitive_env_vars_masks_values() {
|
|
1163
|
-
let input = "PATH=/usr/local/bin HOME: /Users/daniel GITHUB_TOKEN=ghp_123";
|
|
1164
|
-
let scrubbed = scrub_sensitive_env_vars(input);
|
|
1165
|
-
|
|
1166
|
-
assert!(scrubbed.contains("PATH=[REDACTED]"));
|
|
1167
|
-
assert!(scrubbed.contains("HOME: [REDACTED]"));
|
|
1168
|
-
assert!(scrubbed.contains("GITHUB_TOKEN=[REDACTED]"));
|
|
1169
|
-
assert!(!scrubbed.contains("/usr/local/bin"));
|
|
1170
|
-
assert!(!scrubbed.contains("ghp_123"));
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
#[test]
|
|
1174
|
-
fn test_scrub_sensitive_env_vars_masks_token_and_connection_values() {
|
|
1175
|
-
let input = "GH_TOKEN=ghs_abc AWS_SESSION_TOKEN=aws_xyz CONNECTION_STRING=Server=localhost";
|
|
1176
|
-
let scrubbed = scrub_sensitive_env_vars(input);
|
|
1177
|
-
|
|
1178
|
-
assert!(scrubbed.contains("GH_TOKEN=[REDACTED]"));
|
|
1179
|
-
assert!(scrubbed.contains("AWS_SESSION_TOKEN=[REDACTED]"));
|
|
1180
|
-
assert!(scrubbed.contains("CONNECTION_STRING=[REDACTED]"));
|
|
1181
|
-
assert!(!scrubbed.contains("ghs_abc"));
|
|
1182
|
-
assert!(!scrubbed.contains("aws_xyz"));
|
|
1183
|
-
assert!(!scrubbed.contains("Server=localhost"));
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
#[test]
|
|
1187
|
-
fn test_parse_build_from_text_extracts_issues() {
|
|
1188
|
-
let input = r#"
|
|
1189
|
-
Build FAILED.
|
|
1190
|
-
src/Program.cs(42,15): error CS0103: The name 'foo' does not exist
|
|
1191
|
-
src/Program.cs(25,10): warning CS0219: Variable 'x' is assigned but never used
|
|
1192
|
-
1 Warning(s)
|
|
1193
|
-
1 Error(s)
|
|
1194
|
-
Time Elapsed 00:00:03.45
|
|
1195
|
-
"#;
|
|
1196
|
-
|
|
1197
|
-
let summary = parse_build_from_text(input);
|
|
1198
|
-
assert!(!summary.succeeded);
|
|
1199
|
-
assert_eq!(summary.errors.len(), 1);
|
|
1200
|
-
assert_eq!(summary.warnings.len(), 1);
|
|
1201
|
-
assert_eq!(summary.errors[0].code, "CS0103");
|
|
1202
|
-
assert_eq!(summary.warnings[0].code, "CS0219");
|
|
1203
|
-
assert_eq!(summary.duration_text.as_deref(), Some("00:00:03.45"));
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
#[test]
|
|
1207
|
-
fn test_parse_build_from_text_extracts_warning_without_code() {
|
|
1208
|
-
let input = r#"
|
|
1209
|
-
/Users/dev/sdk/Microsoft.TestPlatform.targets(48,5): warning
|
|
1210
|
-
Build succeeded with 1 warning(s) in 0.5s
|
|
1211
|
-
"#;
|
|
1212
|
-
|
|
1213
|
-
let summary = parse_build_from_text(input);
|
|
1214
|
-
assert_eq!(summary.warnings.len(), 1);
|
|
1215
|
-
assert_eq!(
|
|
1216
|
-
summary.warnings[0].file,
|
|
1217
|
-
"/Users/dev/sdk/Microsoft.TestPlatform.targets"
|
|
1218
|
-
);
|
|
1219
|
-
assert_eq!(summary.warnings[0].code, "");
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
#[test]
|
|
1223
|
-
fn test_parse_build_from_text_extracts_inline_warning_counts() {
|
|
1224
|
-
let input = r#"
|
|
1225
|
-
Build failed with 1 error(s) and 4 warning(s) in 4.7s
|
|
1226
|
-
"#;
|
|
1227
|
-
|
|
1228
|
-
let summary = parse_build_from_text(input);
|
|
1229
|
-
assert_eq!(summary.errors.len(), 1);
|
|
1230
|
-
assert_eq!(summary.warnings.len(), 4);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
#[test]
|
|
1234
|
-
fn test_parse_build_from_text_extracts_msbuild_global_error() {
|
|
1235
|
-
let input = r#"
|
|
1236
|
-
MSBUILD : error MSB1009: Project file does not exist.
|
|
1237
|
-
Switch: /tmp/nonexistent.csproj
|
|
1238
|
-
"#;
|
|
1239
|
-
|
|
1240
|
-
let summary = parse_build_from_text(input);
|
|
1241
|
-
assert_eq!(summary.errors.len(), 1);
|
|
1242
|
-
assert_eq!(summary.errors[0].code, "MSB1009");
|
|
1243
|
-
assert_eq!(summary.errors[0].file, "MSBUILD");
|
|
1244
|
-
assert!(summary.errors[0]
|
|
1245
|
-
.message
|
|
1246
|
-
.contains("Project file does not exist"));
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
#[test]
|
|
1250
|
-
fn test_parse_test_from_text_extracts_failure_summary() {
|
|
1251
|
-
let input = r#"
|
|
1252
|
-
Failed! - Failed: 2, Passed: 245, Skipped: 0, Total: 247, Duration: 1 s
|
|
1253
|
-
Failed MyApp.Tests.UnitTests.CalculatorTests.Add_ShouldReturnSum [5 ms]
|
|
1254
|
-
Error Message:
|
|
1255
|
-
Assert.Equal() Failure: Expected 5, Actual 4
|
|
1256
|
-
|
|
1257
|
-
Failed MyApp.Tests.IntegrationTests.DatabaseTests.CanConnect [20 ms]
|
|
1258
|
-
Error Message:
|
|
1259
|
-
System.InvalidOperationException: Connection refused
|
|
1260
|
-
"#;
|
|
1261
|
-
|
|
1262
|
-
let summary = parse_test_from_text(input);
|
|
1263
|
-
assert_eq!(summary.passed, 245);
|
|
1264
|
-
assert_eq!(summary.failed, 2);
|
|
1265
|
-
assert_eq!(summary.total, 247);
|
|
1266
|
-
assert_eq!(summary.failed_tests.len(), 2);
|
|
1267
|
-
assert!(summary.failed_tests[0]
|
|
1268
|
-
.name
|
|
1269
|
-
.contains("CalculatorTests.Add_ShouldReturnSum"));
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
#[test]
|
|
1273
|
-
fn test_parse_test_from_text_keeps_multiline_failure_details() {
|
|
1274
|
-
let input = r#"
|
|
1275
|
-
Failed! - Failed: 1, Passed: 10, Skipped: 0, Total: 11, Duration: 1 s
|
|
1276
|
-
Failed MyApp.Tests.SampleTests.ShouldFail [5 ms]
|
|
1277
|
-
Error Message:
|
|
1278
|
-
Assert.That(messageInstance, Is.Null)
|
|
1279
|
-
Expected: null
|
|
1280
|
-
But was: <MyApp.Tests.SampleTests+Impl>
|
|
1281
|
-
|
|
1282
|
-
Stack Trace:
|
|
1283
|
-
at MyApp.Tests.SampleTests.ShouldFail() in /repo/SampleTests.cs:line 42
|
|
1284
|
-
"#;
|
|
1285
|
-
|
|
1286
|
-
let summary = parse_test_from_text(input);
|
|
1287
|
-
assert_eq!(summary.failed, 1);
|
|
1288
|
-
assert_eq!(summary.failed_tests.len(), 1);
|
|
1289
|
-
let details = summary.failed_tests[0].details.join("\n");
|
|
1290
|
-
assert!(details.contains("Expected: null"));
|
|
1291
|
-
assert!(details.contains("But was:"));
|
|
1292
|
-
assert!(details.contains("Stack Trace:"));
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
#[test]
|
|
1296
|
-
fn test_parse_test_from_text_ignores_non_test_failed_prefix_lines() {
|
|
1297
|
-
let input = r#"
|
|
1298
|
-
Passed! - Failed: 0, Passed: 940, Skipped: 7, Total: 947, Duration: 1 s
|
|
1299
|
-
Failed to load prune package data from PrunePackageData folder, loading from targeting packs instead
|
|
1300
|
-
"#;
|
|
1301
|
-
|
|
1302
|
-
let summary = parse_test_from_text(input);
|
|
1303
|
-
assert_eq!(summary.failed, 0);
|
|
1304
|
-
assert!(summary.failed_tests.is_empty());
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
#[test]
|
|
1308
|
-
fn test_parse_test_from_text_aggregates_multiple_project_summaries() {
|
|
1309
|
-
let input = r#"
|
|
1310
|
-
Passed! - Failed: 0, Passed: 914, Skipped: 7, Total: 921, Duration: 00:00:08.20
|
|
1311
|
-
Failed! - Failed: 1, Passed: 26, Skipped: 0, Total: 27, Duration: 00:00:00.54
|
|
1312
|
-
Time Elapsed 00:00:12.34
|
|
1313
|
-
"#;
|
|
1314
|
-
|
|
1315
|
-
let summary = parse_test_from_text(input);
|
|
1316
|
-
assert_eq!(summary.passed, 940);
|
|
1317
|
-
assert_eq!(summary.failed, 1);
|
|
1318
|
-
assert_eq!(summary.skipped, 7);
|
|
1319
|
-
assert_eq!(summary.total, 948);
|
|
1320
|
-
assert_eq!(summary.duration_text.as_deref(), Some("00:00:12.34"));
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
#[test]
|
|
1324
|
-
fn test_parse_test_from_text_prefers_test_summary_duration_and_counts() {
|
|
1325
|
-
let input = r#"
|
|
1326
|
-
Failed! - Failed: 1, Passed: 940, Skipped: 7, Total: 948, Duration: 1 s
|
|
1327
|
-
Test summary: total: 949, failed: 1, succeeded: 940, skipped: 7, duration: 2.7s
|
|
1328
|
-
Build failed with 1 error(s) and 4 warning(s) in 6.0s
|
|
1329
|
-
"#;
|
|
1330
|
-
|
|
1331
|
-
let summary = parse_test_from_text(input);
|
|
1332
|
-
assert_eq!(summary.passed, 940);
|
|
1333
|
-
assert_eq!(summary.failed, 1);
|
|
1334
|
-
assert_eq!(summary.skipped, 7);
|
|
1335
|
-
assert_eq!(summary.total, 949);
|
|
1336
|
-
assert_eq!(summary.duration_text.as_deref(), Some("2.7s"));
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
#[test]
|
|
1340
|
-
fn test_parse_restore_from_text_extracts_project_count() {
|
|
1341
|
-
let input = r#"
|
|
1342
|
-
Restored /tmp/App/App.csproj (in 1.1 sec).
|
|
1343
|
-
Restored /tmp/App.Tests/App.Tests.csproj (in 1.2 sec).
|
|
1344
|
-
"#;
|
|
1345
|
-
|
|
1346
|
-
let summary = parse_restore_from_text(input);
|
|
1347
|
-
assert_eq!(summary.restored_projects, 2);
|
|
1348
|
-
assert_eq!(summary.errors, 0);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
#[test]
|
|
1352
|
-
fn test_parse_restore_from_text_extracts_nuget_error_diagnostic() {
|
|
1353
|
-
let input = r#"
|
|
1354
|
-
/Users/dev/src/App/App.csproj : error NU1101: Unable to find package Foo.Bar. No packages exist with this id in source(s): nuget.org
|
|
1355
|
-
|
|
1356
|
-
Restore failed with 1 error(s) in 1.0s
|
|
1357
|
-
"#;
|
|
1358
|
-
|
|
1359
|
-
let summary = parse_restore_from_text(input);
|
|
1360
|
-
assert_eq!(summary.errors, 1);
|
|
1361
|
-
assert_eq!(summary.warnings, 0);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
#[test]
|
|
1365
|
-
fn test_parse_restore_issues_ignores_summary_warning_error_counts() {
|
|
1366
|
-
let input = r#"
|
|
1367
|
-
0 Warning(s)
|
|
1368
|
-
1 Error(s)
|
|
1369
|
-
|
|
1370
|
-
Time Elapsed 00:00:01.23
|
|
1371
|
-
"#;
|
|
1372
|
-
|
|
1373
|
-
let (errors, warnings) = parse_restore_issues_from_text(input);
|
|
1374
|
-
assert_eq!(errors.len(), 0);
|
|
1375
|
-
assert_eq!(warnings.len(), 0);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
#[test]
|
|
1379
|
-
fn test_parse_build_fails_when_binlog_is_unparseable() {
|
|
1380
|
-
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
|
1381
|
-
let binlog_path = temp_dir.path().join("build.binlog");
|
|
1382
|
-
std::fs::write(&binlog_path, [0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00])
|
|
1383
|
-
.expect("write binary file");
|
|
1384
|
-
|
|
1385
|
-
let err = parse_build(&binlog_path).expect_err("parse should fail");
|
|
1386
|
-
assert!(
|
|
1387
|
-
err.to_string().contains("Failed to parse binlog"),
|
|
1388
|
-
"unexpected error: {}",
|
|
1389
|
-
err
|
|
1390
|
-
);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
#[test]
|
|
1394
|
-
fn test_parse_build_fails_when_binlog_missing() {
|
|
1395
|
-
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
|
1396
|
-
let binlog_path = temp_dir.path().join("build.binlog");
|
|
1397
|
-
|
|
1398
|
-
let err = parse_build(&binlog_path).expect_err("parse should fail");
|
|
1399
|
-
assert!(
|
|
1400
|
-
err.to_string().contains("Failed to parse binlog"),
|
|
1401
|
-
"unexpected error: {}",
|
|
1402
|
-
err
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
#[test]
|
|
1407
|
-
fn test_parse_build_reads_structured_events() {
|
|
1408
|
-
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
|
1409
|
-
let binlog_path = temp_dir.path().join("build.binlog");
|
|
1410
|
-
|
|
1411
|
-
let mut records = Vec::new();
|
|
1412
|
-
|
|
1413
|
-
// String records (index starts at 10)
|
|
1414
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1415
|
-
write_dotnet_string(&mut records, "Build started"); // 10
|
|
1416
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1417
|
-
write_dotnet_string(&mut records, "Build finished"); // 11
|
|
1418
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1419
|
-
write_dotnet_string(&mut records, "src/App.csproj"); // 12
|
|
1420
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1421
|
-
write_dotnet_string(&mut records, "The name 'foo' does not exist"); // 13
|
|
1422
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1423
|
-
write_dotnet_string(&mut records, "CS0103"); // 14
|
|
1424
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1425
|
-
write_dotnet_string(&mut records, "src/Program.cs"); // 15
|
|
1426
|
-
|
|
1427
|
-
// BuildStarted (message + timestamp)
|
|
1428
|
-
let mut build_started = Vec::new();
|
|
1429
|
-
write_7bit_i32(&mut build_started, FLAG_MESSAGE | FLAG_TIMESTAMP);
|
|
1430
|
-
write_7bit_i32(&mut build_started, 10);
|
|
1431
|
-
build_started.extend_from_slice(&1_000_000_000_i64.to_le_bytes());
|
|
1432
|
-
write_7bit_i32(&mut build_started, 1);
|
|
1433
|
-
write_event_record(&mut records, RECORD_BUILD_STARTED, &build_started);
|
|
1434
|
-
|
|
1435
|
-
// ProjectFinished
|
|
1436
|
-
let mut project_finished = Vec::new();
|
|
1437
|
-
write_7bit_i32(&mut project_finished, 0);
|
|
1438
|
-
write_7bit_i32(&mut project_finished, 12);
|
|
1439
|
-
project_finished.push(1);
|
|
1440
|
-
write_event_record(&mut records, RECORD_PROJECT_FINISHED, &project_finished);
|
|
1441
|
-
|
|
1442
|
-
// Error event
|
|
1443
|
-
let mut error_event = Vec::new();
|
|
1444
|
-
write_7bit_i32(&mut error_event, FLAG_MESSAGE);
|
|
1445
|
-
write_7bit_i32(&mut error_event, 13);
|
|
1446
|
-
write_7bit_i32(&mut error_event, 0); // subcategory
|
|
1447
|
-
write_7bit_i32(&mut error_event, 14); // code
|
|
1448
|
-
write_7bit_i32(&mut error_event, 15); // file
|
|
1449
|
-
write_7bit_i32(&mut error_event, 0); // project file
|
|
1450
|
-
write_7bit_i32(&mut error_event, 42);
|
|
1451
|
-
write_7bit_i32(&mut error_event, 10);
|
|
1452
|
-
write_7bit_i32(&mut error_event, 42);
|
|
1453
|
-
write_7bit_i32(&mut error_event, 10);
|
|
1454
|
-
write_event_record(&mut records, RECORD_ERROR, &error_event);
|
|
1455
|
-
|
|
1456
|
-
// BuildFinished (message + timestamp + succeeded)
|
|
1457
|
-
let mut build_finished = Vec::new();
|
|
1458
|
-
write_7bit_i32(&mut build_finished, FLAG_MESSAGE | FLAG_TIMESTAMP);
|
|
1459
|
-
write_7bit_i32(&mut build_finished, 11);
|
|
1460
|
-
build_finished.extend_from_slice(&1_010_000_000_i64.to_le_bytes());
|
|
1461
|
-
write_7bit_i32(&mut build_finished, 1);
|
|
1462
|
-
build_finished.push(1);
|
|
1463
|
-
write_event_record(&mut records, RECORD_BUILD_FINISHED, &build_finished);
|
|
1464
|
-
|
|
1465
|
-
write_7bit_i32(&mut records, RECORD_END_OF_FILE);
|
|
1466
|
-
|
|
1467
|
-
let binlog_bytes = build_minimal_binlog(&records);
|
|
1468
|
-
std::fs::write(&binlog_path, binlog_bytes).expect("write binlog");
|
|
1469
|
-
|
|
1470
|
-
let summary = parse_build(&binlog_path).expect("parse should succeed");
|
|
1471
|
-
assert!(summary.succeeded);
|
|
1472
|
-
assert_eq!(summary.project_count, 1);
|
|
1473
|
-
assert_eq!(summary.errors.len(), 1);
|
|
1474
|
-
assert_eq!(summary.errors[0].code, "CS0103");
|
|
1475
|
-
assert_eq!(summary.duration_text.as_deref(), Some("00:00:01.00"));
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
#[test]
|
|
1479
|
-
fn test_parse_test_reads_message_events() {
|
|
1480
|
-
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
|
1481
|
-
let binlog_path = temp_dir.path().join("test.binlog");
|
|
1482
|
-
|
|
1483
|
-
let mut records = Vec::new();
|
|
1484
|
-
write_7bit_i32(&mut records, RECORD_STRING);
|
|
1485
|
-
write_dotnet_string(
|
|
1486
|
-
&mut records,
|
|
1487
|
-
"Failed! - Failed: 1, Passed: 2, Skipped: 0, Total: 3, Duration: 1 s",
|
|
1488
|
-
); // 10
|
|
1489
|
-
|
|
1490
|
-
let mut message_event = Vec::new();
|
|
1491
|
-
write_7bit_i32(&mut message_event, FLAG_MESSAGE | FLAG_IMPORTANCE);
|
|
1492
|
-
write_7bit_i32(&mut message_event, 10);
|
|
1493
|
-
write_7bit_i32(&mut message_event, 1);
|
|
1494
|
-
write_event_record(&mut records, RECORD_MESSAGE, &message_event);
|
|
1495
|
-
|
|
1496
|
-
write_7bit_i32(&mut records, RECORD_END_OF_FILE);
|
|
1497
|
-
let binlog_bytes = build_minimal_binlog(&records);
|
|
1498
|
-
std::fs::write(&binlog_path, binlog_bytes).expect("write binlog");
|
|
1499
|
-
|
|
1500
|
-
let summary = parse_test(&binlog_path).expect("parse should succeed");
|
|
1501
|
-
assert_eq!(summary.failed, 1);
|
|
1502
|
-
assert_eq!(summary.passed, 2);
|
|
1503
|
-
assert_eq!(summary.total, 3);
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
#[test]
|
|
1507
|
-
fn test_parse_test_fails_when_binlog_missing() {
|
|
1508
|
-
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
|
1509
|
-
let binlog_path = temp_dir.path().join("test.binlog");
|
|
1510
|
-
|
|
1511
|
-
let err = parse_test(&binlog_path).expect_err("parse should fail");
|
|
1512
|
-
assert!(
|
|
1513
|
-
err.to_string().contains("Failed to parse binlog"),
|
|
1514
|
-
"unexpected error: {}",
|
|
1515
|
-
err
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
#[test]
|
|
1520
|
-
fn test_parse_restore_fails_when_binlog_missing() {
|
|
1521
|
-
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
|
1522
|
-
let binlog_path = temp_dir.path().join("restore.binlog");
|
|
1523
|
-
|
|
1524
|
-
let err = parse_restore(&binlog_path).expect_err("parse should fail");
|
|
1525
|
-
assert!(
|
|
1526
|
-
err.to_string().contains("Failed to parse binlog"),
|
|
1527
|
-
"unexpected error: {}",
|
|
1528
|
-
err
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
#[test]
|
|
1533
|
-
fn test_parse_build_from_fixture_text() {
|
|
1534
|
-
let input = include_str!("../tests/fixtures/dotnet/build_failed.txt");
|
|
1535
|
-
let summary = parse_build_from_text(input);
|
|
1536
|
-
|
|
1537
|
-
assert_eq!(summary.errors.len(), 1);
|
|
1538
|
-
assert_eq!(summary.errors[0].code, "CS1525");
|
|
1539
|
-
assert_eq!(summary.duration_text.as_deref(), Some("00:00:00.76"));
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
#[test]
|
|
1543
|
-
fn test_parse_build_sets_project_count_floor() {
|
|
1544
|
-
let input = r#"
|
|
1545
|
-
RtkDotnetSmoke -> /tmp/RtkDotnetSmoke.dll
|
|
1546
|
-
|
|
1547
|
-
Build succeeded.
|
|
1548
|
-
0 Warning(s)
|
|
1549
|
-
0 Error(s)
|
|
1550
|
-
|
|
1551
|
-
Time Elapsed 00:00:00.12
|
|
1552
|
-
"#;
|
|
1553
|
-
|
|
1554
|
-
let summary = parse_build_from_text(input);
|
|
1555
|
-
assert_eq!(summary.project_count, 1);
|
|
1556
|
-
assert!(summary.succeeded);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
#[test]
|
|
1560
|
-
fn test_parse_build_does_not_infer_binary_errors_on_successful_build() {
|
|
1561
|
-
let input = "\x0bInvalid expression term ';'\x18\x06CS1525\x18%/tmp/App/Broken.cs\x09\nBuild succeeded.\n 0 Warning(s)\n 0 Error(s)\n";
|
|
1562
|
-
|
|
1563
|
-
let summary = parse_build_from_text(input);
|
|
1564
|
-
assert!(summary.succeeded);
|
|
1565
|
-
assert!(summary.errors.is_empty());
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
#[test]
|
|
1569
|
-
fn test_parse_test_from_fixture_text() {
|
|
1570
|
-
let input = include_str!("../tests/fixtures/dotnet/test_failed.txt");
|
|
1571
|
-
let summary = parse_test_from_text(input);
|
|
1572
|
-
|
|
1573
|
-
assert_eq!(summary.failed, 1);
|
|
1574
|
-
assert_eq!(summary.passed, 0);
|
|
1575
|
-
assert_eq!(summary.total, 1);
|
|
1576
|
-
assert_eq!(summary.failed_tests.len(), 1);
|
|
1577
|
-
assert!(summary.failed_tests[0]
|
|
1578
|
-
.name
|
|
1579
|
-
.contains("RtkDotnetSmoke.UnitTest1.Test1"));
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
#[test]
|
|
1583
|
-
fn test_extract_binary_like_issues_recovers_code_message_and_path() {
|
|
1584
|
-
let noisy =
|
|
1585
|
-
"\x0bInvalid expression term ';'\x18\x06CS1525\x18%/tmp/RtkDotnetSmoke/Broken.cs\x09";
|
|
1586
|
-
let issues = extract_binary_like_issues(noisy);
|
|
1587
|
-
|
|
1588
|
-
assert_eq!(issues.len(), 1);
|
|
1589
|
-
assert_eq!(issues[0].code, "CS1525");
|
|
1590
|
-
assert_eq!(issues[0].file, "/tmp/RtkDotnetSmoke/Broken.cs");
|
|
1591
|
-
assert!(issues[0].message.contains("Invalid expression term"));
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
#[test]
|
|
1595
|
-
fn test_is_likely_diagnostic_code_filters_framework_monikers() {
|
|
1596
|
-
assert!(is_likely_diagnostic_code("CS1525"));
|
|
1597
|
-
assert!(is_likely_diagnostic_code("MSB4018"));
|
|
1598
|
-
assert!(!is_likely_diagnostic_code("NET451"));
|
|
1599
|
-
assert!(!is_likely_diagnostic_code("NET10"));
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
#[test]
|
|
1603
|
-
fn test_select_best_issues_prefers_fallback_when_primary_loses_context() {
|
|
1604
|
-
let primary = vec![BinlogIssue {
|
|
1605
|
-
code: String::new(),
|
|
1606
|
-
file: "CS1525".to_string(),
|
|
1607
|
-
line: 51,
|
|
1608
|
-
column: 1,
|
|
1609
|
-
message: "Invalid expression term ';'".to_string(),
|
|
1610
|
-
}];
|
|
1611
|
-
|
|
1612
|
-
let fallback = vec![BinlogIssue {
|
|
1613
|
-
code: "CS1525".to_string(),
|
|
1614
|
-
file: "/Users/dev/project/src/NServiceBus.Core/Class1.cs".to_string(),
|
|
1615
|
-
line: 1,
|
|
1616
|
-
column: 9,
|
|
1617
|
-
message: "Invalid expression term ';'".to_string(),
|
|
1618
|
-
}];
|
|
1619
|
-
|
|
1620
|
-
let selected = select_best_issues(primary, fallback.clone());
|
|
1621
|
-
assert_eq!(selected, fallback);
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
#[test]
|
|
1625
|
-
fn test_select_best_issues_keeps_primary_when_context_is_good() {
|
|
1626
|
-
let primary = vec![BinlogIssue {
|
|
1627
|
-
code: "CS0103".to_string(),
|
|
1628
|
-
file: "src/Program.cs".to_string(),
|
|
1629
|
-
line: 42,
|
|
1630
|
-
column: 15,
|
|
1631
|
-
message: "The name 'foo' does not exist".to_string(),
|
|
1632
|
-
}];
|
|
1633
|
-
|
|
1634
|
-
let fallback = vec![BinlogIssue {
|
|
1635
|
-
code: "CS0103".to_string(),
|
|
1636
|
-
file: String::new(),
|
|
1637
|
-
line: 0,
|
|
1638
|
-
column: 0,
|
|
1639
|
-
message: "Build error #1 (details omitted)".to_string(),
|
|
1640
|
-
}];
|
|
1641
|
-
|
|
1642
|
-
let selected = select_best_issues(primary.clone(), fallback);
|
|
1643
|
-
assert_eq!(selected, primary);
|
|
1644
|
-
}
|
|
1645
|
-
}
|