Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c53c105
feat: add --coverage flag for code coverage tracking POC
sohil-kshirsagar Apr 1, 2026
a812bc7
refactor: revert to v8.takeCoverage() approach, add TUI coverage display
sohil-kshirsagar Apr 1, 2026
81ddfa5
feat: switch coverage from NYC to V8 native approach
sohil-kshirsagar Apr 1, 2026
6d8b569
feat: add baseline snapshot for coverage denominator
sohil-kshirsagar Apr 1, 2026
c4812cb
chore: code review cleanup + unit tests for coverage
sohil-kshirsagar Apr 1, 2026
224f16a
fix: add baseline snapshot to environment-based replay path
sohil-kshirsagar Apr 1, 2026
d588ca7
fix: enforce concurrency=1, retry baseline, normalize file paths
sohil-kshirsagar Apr 1, 2026
6c3a212
fix: use git root for file path normalization
sohil-kshirsagar Apr 1, 2026
27f76b6
refactor: extract GetGitRootDir to shared utils package
sohil-kshirsagar Apr 1, 2026
1b15d28
refactor: remove file output, keep coverage data in memory only
sohil-kshirsagar Apr 1, 2026
0be4ab3
feat: add branch coverage tracking to CLI
sohil-kshirsagar Apr 2, 2026
f197047
feat: set TS_NODE_EMIT=true for ts-node coverage support
sohil-kshirsagar Apr 2, 2026
99bf7bb
feat: migrate coverage from HTTP to protobuf channel
sohil-kshirsagar Apr 2, 2026
ff88435
fix: remove debug logging from server.go
sohil-kshirsagar Apr 2, 2026
67eea36
fix: critical bugs from code review
sohil-kshirsagar Apr 2, 2026
cc4f32c
refactor: split printCoverageSummary, add tests, cleanup
sohil-kshirsagar Apr 2, 2026
a7d4185
fix: prod readiness - overflow guard, parse error logging
sohil-kshirsagar Apr 2, 2026
19cb788
fix: warn user when baseline fails and coverage denominator is incomp…
sohil-kshirsagar Apr 2, 2026
61eeba8
feat: add TUSK_COVERAGE env var as language-agnostic coverage signal
sohil-kshirsagar Apr 2, 2026
117766d
docs: add code coverage reference documentation
sohil-kshirsagar Apr 3, 2026
757626e
feat: config-driven coverage activation and code quality fixes
sohil-kshirsagar Apr 3, 2026
8d2c4c6
docs: clean up AI writing patterns in coverage doc
sohil-kshirsagar Apr 3, 2026
5771f78
fix: address bugbot review feedback
sohil-kshirsagar Apr 3, 2026
2ae28c7
feat: coverage backend upload integration
sohil-kshirsagar Apr 6, 2026
73ae6a2
chore: update tusk-drift-schemas to v0.1.34, remove local replace
sohil-kshirsagar Apr 7, 2026
519c422
Merge remote-tracking branch 'origin/main' into feat/code-coverage-tr…
sohil-kshirsagar Apr 7, 2026
a871e6f
fix: address lint errors, integer overflow warnings, and coverage cal…
sohil-kshirsagar Apr 7, 2026
b40e506
Merge remote-tracking branch 'origin/main' into feat/code-coverage-tr…
sohil-kshirsagar Apr 7, 2026
6c34383
style: fix remaining gofumpt formatting issues
sohil-kshirsagar Apr 7, 2026
c704076
fix: add bounds check for strconv.Atoi to int32 conversion (CodeQL)
sohil-kshirsagar Apr 7, 2026
0582823
fix: branch coverage max instead of sum, populate validation commit S…
sohil-kshirsagar Apr 7, 2026
d6ffbd4
fix: revert branch coverage to sum+clamp, add LCOV/JSON export tests
sohil-kshirsagar Apr 7, 2026
174bd69
fix: separate startup coverage from test-covered lines in baseline up…
sohil-kshirsagar Apr 7, 2026
5821c20
fix: address review comments — mutex race, response race, JSON summar…
sohil-kshirsagar Apr 7, 2026
e8f5560
fix: baseline retry deadline, mutex on reads, concurrency override wa…
sohil-kshirsagar Apr 7, 2026
eba00ac
remove concurrency override warning
sohil-kshirsagar Apr 7, 2026
900d4a2
increase baseline retry deadline to 90s for large codebases
sohil-kshirsagar Apr 7, 2026
843cc85
fix: address review feedback — context.WithDeadline, skip aggregate w…
sohil-kshirsagar Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 121 additions & 3 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ var (
// Validation mode
validateSuiteIfDefaultBranch bool
validateSuite bool

// Coverage mode
showCoverage bool
coverageOutputPath string
)

//go:embed short_docs/drift/drift_run.md
Expand Down Expand Up @@ -116,6 +120,10 @@ func bindRunFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&validateSuiteIfDefaultBranch, "validate-suite-if-default-branch", false, "[Cloud] Validate traces on default branch before adding to suite")
cmd.Flags().BoolVar(&validateSuite, "validate-suite", false, "[Cloud] Force validation mode regardless of branch")

// Coverage mode
cmd.Flags().BoolVar(&showCoverage, "show-coverage", false, "Collect and display code coverage during test execution")
cmd.Flags().StringVar(&coverageOutputPath, "coverage-output", "", "Write coverage data to file (LCOV by default, JSON if path ends in .json)")

_ = cmd.Flags().MarkHidden("client-id")
cmd.Flags().SortFlags = false
}
Expand Down Expand Up @@ -239,11 +247,12 @@ func runTests(cmd *cobra.Command, args []string) error {
var req *backend.CreateDriftRunRequest

if isValidation {
commitSha = getCommitSHAFromEnv()
req = &backend.CreateDriftRunRequest{
ObservableServiceId: cfg.Service.ID,
CliVersion: version.Version,
IsValidationRun: true,
CommitSha: stringPtr(getCommitSHAFromEnv()),
CommitSha: stringPtr(commitSha),
BranchName: stringPtr(getBranchFromEnv()),
}
} else {
Expand Down Expand Up @@ -314,6 +323,39 @@ func runTests(cmd *cobra.Command, args []string) error {

executor.SetEnableServiceLogs(enableServiceLogs || debug)

// Coverage activation:
// - Config-driven: coverage.enabled=true in config activates during validation runs (silent, for upload)
// - Flag-driven: --show-coverage or --coverage-output activates anytime (for local dev/debugging)
coverageFromConfig := getConfigErr == nil && cfg.Coverage.Enabled && isValidation
coverageFromFlags := showCoverage || coverageOutputPath != ""
coverageEnabled := coverageFromConfig || coverageFromFlags
if coverageEnabled {
executor.SetCoverageEnabled(true)
executor.SetShowCoverage(showCoverage)
if coverageOutputPath != "" {
executor.SetCoverageOutputPath(coverageOutputPath)
}
if getConfigErr == nil {
if len(cfg.Coverage.Include) > 0 {
executor.SetCoverageIncludePatterns(cfg.Coverage.Include)
}
if len(cfg.Coverage.Exclude) > 0 {
executor.SetCoverageExcludePatterns(cfg.Coverage.Exclude)
}
if cfg.Coverage.StripPathPrefix != "" {
executor.SetCoverageStripPrefix(cfg.Coverage.StripPathPrefix)
}
}
// Coverage requires serial execution (concurrency=1) because per-test
// snapshots rely on the SDK resetting counters between tests.
executor.SetConcurrency(1)
if showCoverage {
log.Stderrln("➤ Coverage collection enabled (concurrency forced to 1)")
} else {
log.Debug("Coverage collection enabled via config (concurrency forced to 1)")
}
}

// Initialize results saving (--save-results json|agent)
var agentWriter *runner.AgentWriter
var saveResultsDir string
Expand Down Expand Up @@ -454,6 +496,51 @@ func runTests(cmd *cobra.Command, args []string) error {
})
}

// Coverage: wrap the OnTestCompleted callback to take snapshots between tests.
// Snapshot runs BEFORE the existing callback (which uploads results) so that
// per-test coverage data is available when building the upload proto.
if coverageEnabled {
existingCallback := executor.OnTestCompleted
executor.SetOnTestCompleted(func(res runner.TestResult, test runner.Test) {
// Take coverage snapshot FIRST so data is available for upload.
// Always continue to existingCallback even on error so test results still upload.
lineCounts, err := executor.TakeCoverageSnapshot()
if err != nil {
log.Warn("Failed to take coverage snapshot", "testID", test.TraceID, "error", err)
}

if err == nil {
executor.AddCoverageRecord(runner.CoverageTestRecord{
TestID: test.TraceID,
TestName: test.DisplayName,
SuiteStatus: test.SuiteStatus,
Coverage: lineCounts,
})

// Store detail for TUI display
detail := runner.SnapshotToCoverageDetail(lineCounts)
executor.SetTestCoverageDetail(test.TraceID, detail)

// Print sub-line in --print mode when --show-coverage is active
if !interactive && showCoverage {
totalLines := 0
for _, fd := range detail {
totalLines += fd.CoveredCount
}
if totalLines > 0 {
log.UserProgress(fmt.Sprintf(" ↳ coverage: %d lines across %d files", totalLines, len(detail)))
}
}
}

// Now run the existing callback (which uploads results).
// Coverage data is available via GetTestCoverageDetail() for the upload.
if existingCallback != nil {
existingCallback(res, test)
}
})
}

var tests []runner.Test
var err error

Expand Down Expand Up @@ -781,7 +868,11 @@ func runTests(cmd *cobra.Command, args []string) error {
passed, failed := countPassedFailed(results)
statusMessage = fmt.Sprintf("Validation complete: %d passed, %d failed", passed, failed)
}
if err := runner.ReportDriftRunSuccess(context.Background(), client, driftRunID, authOptions, results, statusMessage); err != nil {
var interactiveCoverageBaseline, interactiveCoverageOriginal runner.CoverageSnapshot
if coverageEnabled && isValidation {
interactiveCoverageBaseline, interactiveCoverageOriginal = executor.GetCoverageBaselineForUpload()
}
if err := runner.ReportDriftRunSuccess(context.Background(), client, driftRunID, authOptions, results, interactiveCoverageBaseline, interactiveCoverageOriginal, commitSha, statusMessage); err != nil {
log.Warn("Interactive: cloud finalize failed", "error", err)
}
mu.Lock()
Expand Down Expand Up @@ -896,6 +987,19 @@ func runTests(cmd *cobra.Command, args []string) error {
log.Stderrln(fmt.Sprintf("➤ Running %d tests (concurrency: %d)...\n", len(tests), executor.GetConcurrency()))
}

// Coverage: take baseline with ?baseline=true to capture ALL coverable lines
// (including uncovered at count=0) for the aggregate denominator.
// This also resets counters so the first test gets clean data.
if coverageEnabled {
baseline, err := executor.TakeCoverageBaseline()
if err != nil {
log.Warn("Failed to take baseline coverage snapshot", "error", err)
} else {
executor.SetCoverageBaseline(baseline)
log.Debug("Coverage baseline taken (counters reset, all coverable lines captured)")
}
}

results, err = executor.RunTests(tests)
if err != nil {
cmd.SilenceUsage = true
Expand Down Expand Up @@ -946,6 +1050,15 @@ func runTests(cmd *cobra.Command, args []string) error {
_ = os.Stdout.Sync()
time.Sleep(1 * time.Millisecond)

// Coverage: print summary and write output file
if coverageEnabled {
if records := executor.GetCoverageRecords(); len(records) > 0 {
if err := executor.ProcessCoverage(records); err != nil {
log.Warn("Failed to process coverage", "error", err)
}
}
}

var outputErr error
if !interactive {
// Results already streamed, just print summary
Expand All @@ -966,7 +1079,12 @@ func runTests(cmd *cobra.Command, args []string) error {
}
// streamed is always true here so this only updates the CI status
// Does NOT upload results to the backend as they are already uploaded via UploadSingleTestResult during the callback
if err := runner.ReportDriftRunSuccess(context.Background(), client, driftRunID, authOptions, results, statusMessage); err != nil {
// Coverage baseline (if enabled) is piggybacked on this status update
var headlessCoverageBaseline, headlessCoverageOriginal runner.CoverageSnapshot
if coverageEnabled && isValidation {
headlessCoverageBaseline, headlessCoverageOriginal = executor.GetCoverageBaselineForUpload()
}
if err := runner.ReportDriftRunSuccess(context.Background(), client, driftRunID, authOptions, results, headlessCoverageBaseline, headlessCoverageOriginal, commitSha, statusMessage); err != nil {
log.Warn("Headless: cloud finalize failed", "error", err)
}
if isValidation {
Expand Down
41 changes: 41 additions & 0 deletions docs/drift/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,47 @@ This will not affect CLI behavior. See SDK for more details:
</tbody>
</table>

## Coverage

Configuration for code coverage collection. See [`docs/drift/coverage.md`](coverage.md) for full documentation.

<table>
<thead>
<tr>
<th>Key</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>coverage.enabled</code></td>
<td>bool</td>
<td><code>false</code></td>
<td>When <code>true</code>, automatically collect coverage during suite validation runs on the default branch. No CI changes needed.</td>
</tr>
<tr>
<td><code>coverage.include</code></td>
<td>string[]</td>
<td>(all files)</td>
<td>Only include files matching at least one pattern. Supports <code>**</code> for recursive matching. Paths are git-relative.</td>
</tr>
<tr>
<td><code>coverage.exclude</code></td>
<td>string[]</td>
<td>(none)</td>
<td>Exclude files matching any pattern. Applied after include. Supports <code>**</code> for recursive matching.</td>
</tr>
<tr>
<td><code>coverage.strip_path_prefix</code></td>
<td>string</td>
<td>(none)</td>
<td>Strip this prefix from coverage file paths. Required for Docker Compose — set to the container mount point (e.g., <code>/app</code>).</td>
</tr>
</tbody>
</table>

## Config overrides

### Flags that override config
Expand Down
Loading
Loading