diff --git a/.gitignore b/.gitignore
index 4d5336a..6936222 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,11 @@
*.exe
monolize
-go.sum
spark
spark_*
__pycache__/
**/__pycache__/
+.task/
+reports/
node_modules/
site/
package-lock.json
diff --git a/AGENTS.md b/AGENTS.md
index 0e11cf5..521720f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -84,7 +84,10 @@ spark-cli/
│ ├── registry/ # repo 命令的扫描/读写/merge 逻辑
│ ├── script/ # 脚本发现(配置 + scripts/ 目录)与执行
│ ├── templates/ # 嵌入的 dotfiles(nvim + ghostty)
+│ ├── testreport/ # Ginkgo JSON 报告 → 自包含 HTML 渲染器(含 BDD 测试)
│ └── witr/ # Why-Is-This-Running 进程诊断引擎
+├── tools/
+│ └── testreport/ # test-report CLI:读取 ginkgo --json-report 输出,生成 HTML
├── pkg/
│ └── witr/model/ # witr 的共享数据模型
├── docs/ # docmd 文档站点(中英双语)
@@ -207,6 +210,20 @@ spark
- `spark docs`(`cmd/docs/`)提供 `init` / `site`,用于 docmd 站点脚手架。
- `spark version`(`cmd/version.go`)打印 version / commit / build date。
+### 7. Windows 测试与构建根治 (2026-10-20)
+- **构建稳定**:`go.sum` 从 `.gitignore` 移除并提交(此前 fresh clone 必然构建失败);
+ `task build` 移除 sources/generates 指纹(此前二次运行会跳过安装)。
+- **测试根治**(均为 Windows 平台假设错误,非删除用例):
+ - `TestAddNestedRepos`:`git update-index --cacheinfo` 拒绝反斜杠路径——
+ `AddExistingRepoAsSubmodule` 与 `isSubmoduleGitlink` 现统一 `filepath.ToSlash`,
+ `.gitmodules` 始终写正斜杠路径。
+ - `TestIntegration_ReadProcessNonexistent`:PID 0 在 Windows 是可读的 System Idle
+ Process——改用 `math.MaxInt32`(任何平台都不存在)。
+ - `TestGetCmdlineSelf`:ginkgo CLI 编译的测试二进制无 `.exe` 后缀——同时接受 `.test`。
+- **HTML 测试报告**:`make test-report` / `task test-report` 全量跑 Ginkgo 并生成
+ `reports/test-report.html`(套件/用例状态、时长、失败详情、状态过滤 + 搜索)。
+ Ginkgo 在 Windows 上必须配 `--output-dir`(其内部 `path.Dir` bug),由 target 预建目录。
+
## CLI 命令完整参考
### 全局选项
@@ -697,6 +714,9 @@ spark witr nginx --json
### 构建命令
+> 注意:`go.sum` 必须提交到仓库(不得加入 `.gitignore`),否则全新 clone 后 `go build` 会因
+> "missing go.sum entry" 失败,导致 Windows/macOS/Linux 构建都不稳定。
+
```bash
make build # 编译 + 打 version/commit/date ldflags + 安装到 ~/.local/bin/spark
make build-linux # 交叉编译 Linux amd64 (spark_linux)
@@ -730,9 +750,16 @@ spark --version # -> spark version v0.3.2
```bash
make test # go test ./... -v
make test-bdd # ginkgo -v ./internal/...
+make test-report # ginkgo 全量 + 生成 HTML 报告 reports/test-report.html
make lint # go vet ./...
```
+`test-report` 原理:Ginkgo CLI 以 `--output-dir=reports --json-report=ginkgo-report.json`
+跑 `./...`(注意必须先用 `--output-dir` 预建目录——Ginkgo 内部用 `path.Dir` 而非
+`filepath.Dir` 创建目录,在 Windows 上直接写 `reports/xxx.json` 会因目录不存在而失败),
+再由 `tools/testreport` 把 JSON 渲染为自包含 HTML(汇总卡片、套件/用例表格、
+失败详情、按状态过滤 + 文本搜索)。渲染器在 `internal/testreport`(含 BDD 测试)。
+
运行单个测试:
```bash
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d35bc8..cf2a034 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Fixed
+
+- Stable cross-platform build — `go.sum` removed from `.gitignore` and committed so `go build` no longer fails with "missing go.sum entry" on fresh clones (fixes #21)
+- `Taskfile.yml` — `build` task no longer skips rebuild/reinstall via sources/generates fingerprinting; `task build` always rebuilds and reinstalls, matching Makefile behavior
+- `spark git submodule add` (folder mode) — nested repos on Windows no longer fail with "Invalid path": all git-facing submodule paths are normalized to forward slashes (`update-index --cacheinfo` rejects backslashes; `rm`/`ls-files` pathspecs silently fail to match them)
+- Windows test stability — `TestIntegration_ReadProcessNonexistent` now uses `math.MaxInt32` instead of PID 0 (PID 0 is the readable System Idle Process on Windows); `TestGetCmdlineSelf` also accepts the `.test` binary suffix used by the ginkgo CLI
+
+### Added
+
+- HTML test report — `make test-report` / `task test-report` run all suites via Ginkgo and write a self-contained interactive HTML report (suite/spec states, durations, failure details, filtering) to `reports/test-report.html`; backed by `internal/testreport` (renderer + BDD tests) and `tools/testreport` (JSON→HTML converter)
+
## [0.3.2] - 2026-09-17
### Added
diff --git a/Makefile b/Makefile
index f9bb0db..f63854b 100644
--- a/Makefile
+++ b/Makefile
@@ -60,6 +60,11 @@ test:
test-bdd:
$(GINKGO) -v ./internal/...
+test-report:
+ @mkdir -p reports
+ -$(GINKGO) -v --keep-going --output-dir=reports --json-report=ginkgo-report.json ./...
+ $(GO) run ./tools/testreport -in reports/ginkgo-report.json -out reports/test-report.html
+
lint:
$(GO) vet ./...
@@ -106,5 +111,6 @@ help:
@echo " build-darwin - Cross-compile for macOS (amd64)"
@echo " test - Run all tests"
@echo " test-bdd - Run tests with BDD output"
+ @echo " test-report - Run BDD tests and generate HTML report at reports/test-report.html"
@echo " lint - Run go vet"
@echo " clean - Remove binary and build artifacts"
diff --git a/Taskfile.yml b/Taskfile.yml
index 3b3c728..c73546d 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -32,11 +32,6 @@ tasks:
- cmd: 'bash scripts/verify-install.sh {{.BINARY_NAME}}{{exeExt}} $HOME/.local/bin'
platforms: [linux, darwin]
- echo 'Installed {{.BINARY_NAME}} to ~/.local/bin'
- sources:
- - '**/*.go'
- - go.mod
- generates:
- - '{{.BINARY_NAME}}{{exeExt}}'
build:linux:
desc: Cross-compile for Linux (amd64)
@@ -112,6 +107,17 @@ tasks:
cmds:
- '{{.GO}} run github.com/onsi/ginkgo/v2/ginkgo -v ./internal/...'
+ test-report:
+ desc: Run BDD tests and generate an HTML report at reports/test-report.html
+ cmds:
+ - cmd: powershell -NoProfile -Command "New-Item -ItemType Directory -Force reports | Out-Null"
+ platforms: [windows]
+ - cmd: 'mkdir -p reports'
+ platforms: [linux, darwin]
+ - cmd: '{{.GO}} run github.com/onsi/ginkgo/v2/ginkgo -v --keep-going --output-dir=reports --json-report=ginkgo-report.json ./...'
+ ignore_error: true
+ - '{{.GO}} run ./tools/testreport -in reports/ginkgo-report.json -out reports/test-report.html'
+
lint:
desc: Run go vet
cmds:
diff --git a/cmd/git/submodule.go b/cmd/git/submodule.go
index 78be26d..227bf5a 100644
--- a/cmd/git/submodule.go
+++ b/cmd/git/submodule.go
@@ -198,7 +198,8 @@ func addNestedRepos(repoPath, absFolder string) error {
continue
}
- relPath := filepath.Join(relFolder, name)
+ // git submodule paths must use forward slashes on every platform.
+ relPath := filepath.ToSlash(filepath.Join(relFolder, name))
if isSubmoduleGitlink(repoPath, relPath) {
fmt.Printf("Skipping %s: already as submodule\n", relPath)
@@ -218,7 +219,7 @@ func addNestedRepos(repoPath, absFolder string) error {
// isSubmoduleGitlink reports whether path is already staged in the index of
// repoPath as a submodule gitlink (mode 160000).
func isSubmoduleGitlink(repoPath, path string) bool {
- cmd := exec.Command("git", "ls-files", "--stage", "--", path)
+ cmd := exec.Command("git", "ls-files", "--stage", "--", filepath.ToSlash(path))
cmd.Dir = repoPath
out, err := cmd.Output()
if err != nil {
diff --git a/cmd/git/submodule_test.go b/cmd/git/submodule_test.go
index e6689e2..e218000 100644
--- a/cmd/git/submodule_test.go
+++ b/cmd/git/submodule_test.go
@@ -46,7 +46,8 @@ func TestAddNestedRepos(t *testing.T) {
t.Fatalf("read .gitmodules: %v", err)
}
for _, name := range repos {
- path := filepath.Join("research", name)
+ // git submodule paths are always forward-slash (see .gitmodules spec).
+ path := filepath.ToSlash(filepath.Join("research", name))
if !strings.Contains(string(gm), fmt.Sprintf("path = %s", path)) {
t.Errorf(".gitmodules missing entry for %s:\n%s", path, gm)
}
@@ -59,7 +60,7 @@ func TestAddNestedRepos(t *testing.T) {
t.Fatalf("ls-files --stage: %v", err)
}
for _, name := range repos {
- path := filepath.Join("research", name)
+ path := filepath.ToSlash(filepath.Join("research", name))
if !strings.Contains(string(out), "160000") || !strings.Contains(string(out), "\t"+path) {
t.Errorf("expected gitlink for %s, got:\n%s", path, out)
}
diff --git a/go.mod b/go.mod
index c85a6cc..f143662 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
github.com/pterm/pterm v0.12.83
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
+ go.yaml.in/yaml/v3 v3.0.4
modernc.org/sqlite v1.53.0
)
@@ -63,7 +64,6 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
- go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..6922afd
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,296 @@
+atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
+atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
+atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
+atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
+atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
+atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
+atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
+atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
+github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
+github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
+github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
+github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
+github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
+github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
+github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
+github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
+github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
+github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
+github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
+github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
+github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
+github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
+github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
+github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
+github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
+github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
+github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
+github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
+github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
+github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
+github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
+github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
+github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
+github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
+github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
+github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
+github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
+github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
+github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
+github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
+github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
+github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
+github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
+github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
+github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
+github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
+github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
+github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
+github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
+github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
+github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
+github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
+github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
+github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
+github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA=
+github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
+github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
+github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
+github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
+golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
+google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
+modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
+modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
+modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
+modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
+modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
+modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
+modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
+modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
+modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/internal/git/submodule.go b/internal/git/submodule.go
index 35b77eb..585d10a 100644
--- a/internal/git/submodule.go
+++ b/internal/git/submodule.go
@@ -219,6 +219,7 @@ type SubmoduleInfo struct {
// the fallback used when `git submodule add` cannot run because the target
// directory already exists.
func AddExistingRepoAsSubmodule(repoPath, name, url string) error {
+ name = filepath.ToSlash(name)
if err := ensureGitmodulesEntry(repoPath, name, url); err != nil {
return fmt.Errorf("failed to update .gitmodules: %w", err)
}
@@ -234,6 +235,9 @@ func AddExistingRepoAsSubmodule(repoPath, name, url string) error {
// Drop any regular-file tracking of name so the path can become a gitlink.
// --ignore-unmatch makes this a no-op when nothing is tracked.
+ // name is slash-normalized because git pathspecs and .gitmodules entries
+ // require forward slashes; backslashes are rejected (update-index) or
+ // silently fail to match (rm/ls-files).
rm := exec.Command("git", "rm", "--cached", "-r", "--ignore-unmatch", "--", name)
rm.Dir = repoPath
rm.Run() // best-effort
diff --git a/internal/testreport/testreport.go b/internal/testreport/testreport.go
new file mode 100644
index 0000000..5359a1c
--- /dev/null
+++ b/internal/testreport/testreport.go
@@ -0,0 +1,304 @@
+package testreport
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/onsi/ginkgo/v2/types"
+)
+
+type summary struct {
+ Suites int
+ Specs int
+ Passed int
+ Failed int
+ Skipped int
+ Pending int
+ Other int
+ Duration string
+ Succeeded bool
+ GeneratedText string
+}
+
+type specRow struct {
+ Suite string
+ Description string
+ State string
+ Class string
+ Duration string
+ Location string
+ Message string
+ Output string
+}
+
+type suiteRow struct {
+ Description string
+ State string
+ Class string
+ Specs int
+ Duration string
+ Message string
+}
+
+type view struct {
+ Summary summary
+ Suites []suiteRow
+ Specs []specRow
+}
+
+func isFailedState(s types.SpecState) bool {
+ switch s {
+ case types.SpecStateFailed, types.SpecStatePanicked, types.SpecStateInterrupted,
+ types.SpecStateAborted, types.SpecStateTimedout:
+ return true
+ default:
+ return false
+ }
+}
+
+func stateClass(s types.SpecState) string {
+ switch {
+ case s == types.SpecStatePassed:
+ return "passed"
+ case isFailedState(s):
+ return "failed"
+ case s == types.SpecStateSkipped:
+ return "skipped"
+ case s == types.SpecStatePending:
+ return "pending"
+ default:
+ return "other"
+ }
+}
+
+func specDescription(r types.SpecReport) string {
+ parts := append([]string{}, r.ContainerHierarchyTexts...)
+ if r.LeafNodeText != "" {
+ parts = append(parts, r.LeafNodeText)
+ }
+ return strings.Join(parts, " ")
+}
+
+func summarize(reports []types.Report) view {
+ v := view{Summary: summary{Suites: len(reports), Succeeded: true}}
+
+ var total int64
+ for _, r := range reports {
+ if !r.SuiteSucceeded {
+ v.Summary.Succeeded = false
+ }
+ total += int64(r.RunTime)
+
+ s := suiteRow{
+ Description: r.SuiteDescription,
+ Specs: len(r.SpecReports),
+ Duration: fmtDuration(int64(r.RunTime)),
+ Message: strings.Join(r.SpecialSuiteFailureReasons, "\n"),
+ }
+ if r.SuiteSucceeded {
+ s.State, s.Class = "PASSED", "passed"
+ } else {
+ s.State, s.Class = "FAILED", "failed"
+ }
+ v.Suites = append(v.Suites, s)
+
+ for _, sr := range r.SpecReports {
+ row := specRow{
+ Suite: r.SuiteDescription,
+ Description: specDescription(sr),
+ State: sr.State.String(),
+ Class: stateClass(sr.State),
+ Duration: fmtDuration(int64(sr.RunTime)),
+ Location: sr.LeafNodeLocation.String(),
+ Message: sr.Failure.Message,
+ Output: sr.CapturedStdOutErr,
+ }
+ if row.Description == "" {
+ row.Description = "(suite node)"
+ }
+ v.Specs = append(v.Specs, row)
+
+ switch {
+ case sr.State == types.SpecStatePassed:
+ v.Summary.Passed++
+ case isFailedState(sr.State):
+ v.Summary.Failed++
+ case sr.State == types.SpecStateSkipped:
+ v.Summary.Skipped++
+ case sr.State == types.SpecStatePending:
+ v.Summary.Pending++
+ default:
+ v.Summary.Other++
+ }
+ }
+ v.Summary.Specs += len(r.SpecReports)
+ }
+
+ v.Summary.Duration = fmtDuration(total)
+ v.Summary.GeneratedText = "Generated by spark testreport"
+ return v
+}
+
+// Render converts Ginkgo reports into a self-contained HTML document that
+// lists every suite and spec with its state, duration and failure details.
+func Render(reports []types.Report) (string, error) {
+ v := summarize(reports)
+
+ var buf bytes.Buffer
+ if err := pageTemplate.Execute(&buf, v); err != nil {
+ return "", fmt.Errorf("render template: %w", err)
+ }
+ return buf.String(), nil
+}
+
+func fmtDuration(ns int64) string {
+ d := ns / 1000 / 1000
+ if d < 1000 {
+ return fmt.Sprintf("%dms", d)
+ }
+ return fmt.Sprintf("%.2fs", float64(d)/1000)
+}
+
+// WriteFile renders the reports and writes the HTML document to dest,
+// creating parent directories as needed.
+func WriteFile(reports []types.Report, dest string) error {
+ html, err := Render(reports)
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
+ return err
+ }
+ return os.WriteFile(dest, []byte(html), 0644)
+}
+
+var pageTemplate = template.Must(template.New("report").Funcs(template.FuncMap{
+ "nl2br": func(s string) string { return strings.ReplaceAll(template.HTMLEscapeString(s), "\n", "
") },
+}).Parse(`
+
+
+
+Ginkgo Test Report
+
+
+
+
+Ginkgo Test Report
+
+
{{.Summary.Suites}}
Suites
+
+
{{.Summary.Passed}}
Passed
+
{{.Summary.Failed}}
Failed
+
{{.Summary.Skipped}}
Skipped
+
{{.Summary.Pending}}
Pending
+
{{.Summary.Duration}}
Duration
+
+
+
+Suites
+
+| Suite | State | Specs | Duration |
+{{range .Suites}}
+| {{.Description}}{{if .Message}} {{nl2br .Message}} {{end}} |
+{{.State}} | {{.Specs}} | {{.Duration}} |
+{{end}}
+
+
+Specs
+
+
+
+
+
+
+
+
+
+| Spec | Suite | State | Duration | Details |
+{{range .Specs}}
+
+| {{.Description}}{{if .Message}}{{end}} |
+{{.Suite}}{{if .Location}} {{.Location}}{{end}} |
+{{.State}} |
+{{.Duration}} |
+{{if .Output}}stdout/stderr{{.Output}} {{end}} |
+
+{{end}}
+
+
+
+
+
+`))
diff --git a/internal/testreport/testreport_test.go b/internal/testreport/testreport_test.go
new file mode 100644
index 0000000..cf4386f
--- /dev/null
+++ b/internal/testreport/testreport_test.go
@@ -0,0 +1,126 @@
+package testreport_test
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/onsi/ginkgo/v2/types"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "spark/internal/testreport"
+)
+
+func TestTestreport(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Testreport Suite")
+}
+
+func sampleReports() []types.Report {
+ return []types.Report{
+ {
+ SuiteDescription: "Config Suite",
+ SuiteSucceeded: true,
+ StartTime: time.Now(),
+ EndTime: time.Now().Add(2 * time.Second),
+ RunTime: 2 * time.Second,
+ SpecReports: types.SpecReports{
+ {
+ ContainerHierarchyTexts: []string{"Config", "Load"},
+ LeafNodeText: "reads the default path",
+ State: types.SpecStatePassed,
+ RunTime: time.Millisecond * 12,
+ },
+ {
+ ContainerHierarchyTexts: []string{"Config", "Load"},
+ LeafNodeText: "fails on bad yaml",
+ State: types.SpecStateFailed,
+ RunTime: time.Millisecond * 5,
+ Failure: types.Failure{Message: "expected map, got string"},
+ },
+ {
+ ContainerHierarchyTexts: []string{"Config", "Migrate"},
+ LeafNodeText: "skips when no old file",
+ State: types.SpecStateSkipped,
+ Failure: types.Failure{Message: "not applicable"},
+ },
+ {
+ ContainerHierarchyTexts: []string{"Config", "Migrate"},
+ LeafNodeText: "pending spec",
+ State: types.SpecStatePending,
+ },
+ },
+ },
+ {
+ SuiteDescription: "Git Suite",
+ SuiteSucceeded: false,
+ RunTime: 3 * time.Second,
+ SpecReports: types.SpecReports{
+ {
+ LeafNodeText: "clones a repository",
+ State: types.SpecStatePassed,
+ },
+ },
+ },
+ }
+}
+
+var _ = Describe("Render", func() {
+ It("produces an HTML document with suites and every spec", func() {
+ html, err := testreport.Render(sampleReports())
+ Expect(err).NotTo(HaveOccurred())
+ Expect(html).To(ContainSubstring(""))
+ Expect(html).To(ContainSubstring("Config Suite"))
+ Expect(html).To(ContainSubstring("Git Suite"))
+ Expect(html).To(ContainSubstring("reads the default path"))
+ Expect(html).To(ContainSubstring("fails on bad yaml"))
+ Expect(html).To(ContainSubstring("skips when no old file"))
+ Expect(html).To(ContainSubstring("clones a repository"))
+ Expect(html).To(ContainSubstring(`class="badge failed"`))
+ Expect(html).To(ContainSubstring("expected map, got string"))
+ })
+
+ It("escapes HTML in spec text and failure messages", func() {
+ reports := []types.Report{{
+ SuiteDescription: "Escaping",
+ SuiteSucceeded: false,
+ SpecReports: types.SpecReports{{
+ LeafNodeText: "uses ",
+ State: types.SpecStateFailed,
+ Failure: types.Failure{Message: " boom"},
+ }},
+ }}
+ html, err := testreport.Render(reports)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(html).NotTo(ContainSubstring(""))
+ Expect(html).To(ContainSubstring("<script>"))
+ Expect(html).To(ContainSubstring("<b> boom"))
+ })
+
+ It("reports correct totals in the summary cards", func() {
+ html, err := testreport.Render(sampleReports())
+ Expect(err).NotTo(HaveOccurred())
+ Expect(html).To(ContainSubstring(`2
Suites
`))
+ Expect(html).To(ContainSubstring(`5
Specs
`))
+ })
+
+ It("renders an empty report without error", func() {
+ html, err := testreport.Render(nil)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(html).To(ContainSubstring("Ginkgo Test Report"))
+ })
+})
+
+var _ = Describe("WriteFile", func() {
+ It("writes the HTML file and creates parent directories", func() {
+ dest := filepath.Join(GinkgoT().TempDir(), "nested", "report.html")
+ Expect(testreport.WriteFile(sampleReports(), dest)).To(Succeed())
+
+ data, err := os.ReadFile(dest)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(strings.Contains(string(data), "Config Suite")).To(BeTrue())
+ })
+})
diff --git a/internal/witr/proc/cmdline_windows_test.go b/internal/witr/proc/cmdline_windows_test.go
index cb1078e..b940fc2 100644
--- a/internal/witr/proc/cmdline_windows_test.go
+++ b/internal/witr/proc/cmdline_windows_test.go
@@ -13,8 +13,11 @@ func TestGetCmdlineSelf(t *testing.T) {
if got == "" || got == "(unknown)" {
t.Errorf("GetCmdline(self) = %q, want a non-empty exe basename", got)
}
- if !strings.HasSuffix(strings.ToLower(got), ".exe") {
- t.Errorf("GetCmdline(self) = %q, expected a .exe suffix", got)
+ // Under `go test` the binary is .test.exe, but the ginkgo CLI builds
+ // it as .test (no .exe suffix on Windows) — accept both.
+ lower := strings.ToLower(got)
+ if !strings.HasSuffix(lower, ".exe") && !strings.HasSuffix(lower, ".test") {
+ t.Errorf("GetCmdline(self) = %q, expected a .exe or .test suffix", got)
}
}
diff --git a/internal/witr/proc/integration_test.go b/internal/witr/proc/integration_test.go
index 16213cf..055b0ab 100644
--- a/internal/witr/proc/integration_test.go
+++ b/internal/witr/proc/integration_test.go
@@ -3,6 +3,7 @@
package proc
import (
+ "math"
"net"
"os"
"testing"
@@ -56,11 +57,12 @@ func TestIntegration_ReadProcessSelf(t *testing.T) {
}
// TestIntegration_ReadProcessNonexistent verifies error handling for a PID
-// that cannot exist (PID 0 is reserved by the kernel and never represents a
-// userland process).
+// that cannot exist. PID 0 is reserved on Unix but is the readable System
+// Idle Process on Windows, so use the largest possible PID instead — no
+// platform assigns it.
func TestIntegration_ReadProcessNonexistent(t *testing.T) {
- if _, err := ReadProcess(0); err == nil {
- t.Errorf("ReadProcess(0) returned no error; want an error")
+ if _, err := ReadProcess(math.MaxInt32); err == nil {
+ t.Errorf("ReadProcess(%d) returned no error; want an error", math.MaxInt32)
}
}
diff --git a/tools/testreport/main.go b/tools/testreport/main.go
new file mode 100644
index 0000000..bd0574d
--- /dev/null
+++ b/tools/testreport/main.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/onsi/ginkgo/v2/types"
+
+ "spark/internal/testreport"
+)
+
+func main() {
+ in := flag.String("in", "reports/ginkgo-report.json", "ginkgo --json-report output file")
+ out := flag.String("out", "reports/test-report.html", "destination HTML report")
+ flag.Parse()
+
+ data, err := os.ReadFile(*in)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "read %s: %v\n", *in, err)
+ os.Exit(1)
+ }
+
+ var reports []types.Report
+ if err := json.Unmarshal(data, &reports); err != nil {
+ fmt.Fprintf(os.Stderr, "parse %s: %v\n", *in, err)
+ os.Exit(1)
+ }
+
+ if err := testreport.WriteFile(reports, *out); err != nil {
+ fmt.Fprintf(os.Stderr, "write %s: %v\n", *out, err)
+ os.Exit(1)
+ }
+
+ total, failed := 0, 0
+ for _, r := range reports {
+ total += len(r.SpecReports)
+ for _, sr := range r.SpecReports {
+ if !sr.State.Is(types.SpecStatePassed | types.SpecStateSkipped | types.SpecStatePending) {
+ failed++
+ }
+ }
+ }
+ fmt.Printf("HTML report written to %s (%d specs, %d failed)\n", *out, total, failed)
+}