From a7ee58d29edf0f16f456b0eb0cc11cebe38c12f7 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 13:07:46 +0000 Subject: [PATCH] feat(cli): add core go cov command - Add `core go cov` for coverage reports - Generate HTML report with --html - Open in browser with --open - Fail on threshold with --threshold 80 - Colour-coded coverage output - Update SKILL.md documentation Co-Authored-By: Claude Opus 4.5 --- .claude/skills/core/SKILL.md | 25 ++++++- cmd/core/cmd/go.go | 131 ++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/.claude/skills/core/SKILL.md b/.claude/skills/core/SKILL.md index 4c3e890a..9a6c4b75 100644 --- a/.claude/skills/core/SKILL.md +++ b/.claude/skills/core/SKILL.md @@ -14,7 +14,7 @@ The `core` command provides a unified interface for Go/Wails development, multi- | Task | Command | Notes | |------|---------|-------| | Run Go tests | `core go test` | Sets macOS deployment target, filters warnings | -| Run Go tests with coverage | `core go test --coverage` | Per-package breakdown | +| Run Go tests with coverage | `core go cov` | HTML report, thresholds | | Format Go code | `core go fmt --fix` | Uses goimports/gofmt | | Lint Go code | `core go lint` | Uses golangci-lint | | Tidy Go modules | `core go mod tidy` | go mod tidy wrapper | @@ -233,8 +233,8 @@ core pkg outdated | Task | Command | Notes | |------|---------|-------| -| Run tests | `core go test` | CGO_ENABLED=0, filters warnings | -| Run tests with coverage | `core go test --coverage` | Per-package breakdown | +| Run tests | `core go test` | Filters warnings, colour output | +| Coverage report | `core go cov` | HTML report, thresholds | | Format code | `core go fmt --fix` | Uses goimports if available | | Lint code | `core go lint` | Uses golangci-lint | | Install binary | `core go install` | Auto-detects cmd/, --no-cgo option | @@ -257,6 +257,25 @@ core go install --no-cgo core go install -v ``` +### Coverage + +```bash +# Run tests with coverage summary +core go cov + +# Generate HTML report +core go cov --html + +# Generate and open in browser +core go cov --open + +# Fail if coverage below threshold +core go cov --threshold 80 + +# Specific package +core go cov --pkg ./pkg/release +``` + ### Testing ```bash diff --git a/cmd/core/cmd/go.go b/cmd/core/cmd/go.go index a3e96d95..fece9a9c 100644 --- a/cmd/core/cmd/go.go +++ b/cmd/core/cmd/go.go @@ -17,14 +17,16 @@ func AddGoCommands(parent *clir.Cli) { goCmd := parent.NewSubCommand("go", "Go development tools") goCmd.LongDescription("Go development tools with enhanced output and environment setup.\n\n" + "Commands:\n" + - " test Run tests with coverage\n" + + " test Run tests\n" + + " cov Run tests with coverage report\n" + " fmt Format Go code\n" + " lint Run golangci-lint\n" + - " install Install Go binary (CGO_ENABLED=0)\n" + + " install Install Go binary\n" + " mod Module management (tidy, download, verify)\n" + " work Workspace management") addGoTestCommand(goCmd) + addGoCovCommand(goCmd) addGoFmtCommand(goCmd) addGoLintCommand(goCmd) addGoInstallCommand(goCmd) @@ -186,6 +188,131 @@ func parseOverallCoverage(output string) float64 { return total / float64(len(matches)) } +func addGoCovCommand(parent *clir.Command) { + var ( + pkg string + html bool + open bool + threshold float64 + ) + + covCmd := parent.NewSubCommand("cov", "Run tests with coverage report") + covCmd.LongDescription("Run tests and generate coverage report.\n\n" + + "Examples:\n" + + " core go cov # Run with coverage summary\n" + + " core go cov --html # Generate HTML report\n" + + " core go cov --open # Generate and open HTML report\n" + + " core go cov --threshold 80 # Fail if coverage < 80%") + + covCmd.StringFlag("pkg", "Package to test (default: ./...)", &pkg) + covCmd.BoolFlag("html", "Generate HTML coverage report", &html) + covCmd.BoolFlag("open", "Generate and open HTML report in browser", &open) + covCmd.Float64Flag("threshold", "Minimum coverage percentage (exit 1 if below)", &threshold) + + covCmd.Action(func() error { + if pkg == "" { + pkg = "./..." + } + + // Create temp file for coverage data + covFile, err := os.CreateTemp("", "coverage-*.out") + if err != nil { + return fmt.Errorf("failed to create coverage file: %w", err) + } + covPath := covFile.Name() + covFile.Close() + defer os.Remove(covPath) + + fmt.Printf("%s Running tests with coverage\n", dimStyle.Render("Coverage:")) + fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), pkg) + fmt.Println() + + // Run tests with coverage + args := []string{"test", "-coverprofile=" + covPath, "-covermode=atomic", pkg} + cmd := exec.Command("go", args...) + cmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + testErr := cmd.Run() + + // Get coverage percentage + covCmd := exec.Command("go", "tool", "cover", "-func="+covPath) + covOutput, err := covCmd.Output() + if err != nil { + if testErr != nil { + return testErr + } + return fmt.Errorf("failed to get coverage: %w", err) + } + + // Parse total coverage from last line + lines := strings.Split(strings.TrimSpace(string(covOutput)), "\n") + var totalCov float64 + if len(lines) > 0 { + lastLine := lines[len(lines)-1] + // Format: "total: (statements) XX.X%" + if strings.Contains(lastLine, "total:") { + parts := strings.Fields(lastLine) + if len(parts) >= 3 { + covStr := strings.TrimSuffix(parts[len(parts)-1], "%") + fmt.Sscanf(covStr, "%f", &totalCov) + } + } + } + + // Print coverage summary + fmt.Println() + covStyle := successStyle + if totalCov < 50 { + covStyle = errorStyle + } else if totalCov < 80 { + covStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f59e0b")) + } + fmt.Printf(" %s %s\n", dimStyle.Render("Total:"), covStyle.Render(fmt.Sprintf("%.1f%%", totalCov))) + + // Generate HTML if requested + if html || open { + htmlPath := "coverage.html" + htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) + if err := htmlCmd.Run(); err != nil { + return fmt.Errorf("failed to generate HTML: %w", err) + } + fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath) + + if open { + // Open in browser + var openCmd *exec.Cmd + switch { + case exec.Command("which", "open").Run() == nil: + openCmd = exec.Command("open", htmlPath) + case exec.Command("which", "xdg-open").Run() == nil: + openCmd = exec.Command("xdg-open", htmlPath) + default: + fmt.Printf(" %s\n", dimStyle.Render("(open manually)")) + } + if openCmd != nil { + openCmd.Run() + } + } + } + + // Check threshold + if threshold > 0 && totalCov < threshold { + fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n", + errorStyle.Render("FAIL"), totalCov, threshold) + return fmt.Errorf("coverage below threshold") + } + + if testErr != nil { + return testErr + } + + fmt.Printf("\n%s\n", successStyle.Render("OK")) + return nil + }) +} + func addGoFmtCommand(parent *clir.Command) { var ( fix bool