Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 67 additions & 20 deletions internal/services/covgate/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"runtime"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -89,15 +90,7 @@ func (r *runner) run(opts Opts) error {
return err
}

if r.emitProgress {
_, _ = fmt.Fprintf(
w, "Running %d packages with parallelism=%d; "+
"progress will appear as packages finish:\n",
len(pkgs), parallelism,
)
}

printHeader(w)
r.writeRunHeader(w, len(pkgs), parallelism)

ctx := checkPackageCtx{
module: module,
Expand Down Expand Up @@ -169,6 +162,8 @@ func (r *runner) runPackages(
sem := make(chan struct{}, parallelism)
var wg sync.WaitGroup
var progressMu sync.Mutex
countWidth := len(strconv.Itoa(total))
colWidth := progressColWidth(total)

for i, pkg := range pkgs {
wg.Add(1)
Expand All @@ -178,13 +173,11 @@ func (r *runner) runPackages(
defer func() { <-sem }()
results[idx] = r.checkPackage(p, ctx)
if r.emitProgress {
label := fmt.Sprintf("[%*d/%d]", countWidth, idx+1, total)
progressMu.Lock()
_, _ = fmt.Fprintf(
w, "[%d/%d] %s %s %s\n",
idx+1, total,
progressStatus(results[idx].output),
gocover.RelPkg(p, ctx.module),
fmtDuration(results[idx].duration),
w, "%-*s %s",
colWidth, label, firstLine(results[idx].output),
)
progressMu.Unlock()
}
Expand All @@ -194,10 +187,36 @@ func (r *runner) runPackages(
return results
}

// progressStatus extracts the first whitespace-delimited token of
// the result's first output line, which is the status keyword
// (PASS, FAIL, LOOSE) printed by checkPackage.
func progressStatus(output string) string { return strings.Fields(output)[0] }
// writeRunHeader writes the announce line and the progress table
// header when progress is enabled; otherwise it writes the
// standard table header.
func (r *runner) writeRunHeader(w io.Writer, total, parallelism int) {
if !r.emitProgress {
printHeader(w)
return
}
_, _ = fmt.Fprintf(
w, "Running %d packages with parallelism=%d; progress:\n",
total, parallelism,
)
printProgressHeader(w, total)
}

// progressColWidth returns the width of the COUNT column for total
// packages. The value 3 + 2*ndigits is always >= 5 ("COUNT") for
// total >= 0, so no clamp is needed.
func progressColWidth(total int) int { return 3 + 2*len(strconv.Itoa(total)) }

// firstLine returns the first line of s, including the trailing
// newline. s must contain at least one '\n' — every checkResult's
// output is built from a "%...\n" format string, so this holds.
func firstLine(s string) string { return s[:strings.IndexByte(s, '\n')+1] }

// restOfOutput returns everything after the first '\n' in s. Used
// in progress mode to preserve trailing FAIL detail (multi-line
// raw test output) without reprinting the row that was already
// streamed. Same '\n' precondition as firstLine.
func restOfOutput(s string) string { return s[strings.IndexByte(s, '\n')+1:] }

func (r *runner) printResults(
w io.Writer,
Expand All @@ -208,14 +227,23 @@ func (r *runner) printResults(
) error {
hasFailures := false
for _, res := range results {
_, _ = fmt.Fprint(w, res.output)
if r.emitProgress {
_, _ = fmt.Fprint(w, restOfOutput(res.output))
} else {
_, _ = fmt.Fprint(w, res.output)
}
if !res.passed {
hasFailures = true
}
}

indent := ""
if r.emitProgress {
indent = strings.Repeat(" ", progressColWidth(len(results))) + " "
}
for _, pkg := range excluded {
_, _ = fmt.Fprint(w, skippedRow(gocover.RelPkg(pkg, module)))
row := skippedRow(gocover.RelPkg(pkg, module))
_, _ = fmt.Fprintf(w, "%s%s", indent, row)
}

_, _ = fmt.Fprintf(w, "\nTotal time: %s\n", fmtDuration(totalTime))
Expand Down Expand Up @@ -243,6 +271,25 @@ func printHeader(w io.Writer) {
)
}

// printProgressHeader prints the table header used during the live
// progress stream. It adds a leading COUNT column ahead of the
// standard columns so each progress line carries an [N/total] tag
// aligned under "COUNT".
func printProgressHeader(w io.Writer, total int) {
cw := progressColWidth(total)
dashes := strings.Repeat("-", cw)
_, _ = fmt.Fprintf(
w, "%-*s %-7s %8s %8s %8s %s\n",
cw, "COUNT",
"STATUS", "COVERAGE", "REQUIRED", "TIME", "PACKAGE",
)
_, _ = fmt.Fprintf(
w, "%-*s %-7s %8s %8s %8s %s\n",
cw, dashes,
"-------", "--------", "--------", "--------", "-------",
)
}

// checkResult holds the output and pass/fail status for a single package check.
type checkResult struct {
output string // formatted line(s) to print
Expand Down
55 changes: 47 additions & 8 deletions internal/services/covgate/covgate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,18 +497,35 @@ func TestRun_EmitsProgress_WhenAutoParallelism(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
out := buf.String()
if !strings.Contains(out, "[1/2]") {
t.Errorf("output missing [1/2] progress line:\n%s", out)
if !hasProgressLineWith(out, "[1/2]", "90.0%") {
t.Errorf(
"expected [1/2] progress line to include coverage "+
"data (90.0%%):\n%s", out,
)
}
if !hasProgressLineWith(out, "[2/2]", "90.0%") {
t.Errorf(
"expected [2/2] progress line to include coverage "+
"data (90.0%%):\n%s", out,
)
}
if !strings.Contains(out, "COUNT") {
t.Errorf("expected progress header to include COUNT column:\n%s", out)
}
if !strings.Contains(out, "[2/2]") {
t.Errorf("output missing [2/2] progress line:\n%s", out)
// STATUS header should appear exactly once — the progress
// stream is the single canonical table.
if got := strings.Count(out, "STATUS"); got != 1 {
t.Errorf(
"expected exactly one STATUS header (progress IS "+
"the table); got %d:\n%s", got, out,
)
}
// Leading announcement should appear before the STATUS header.
// Leading announcement should appear before the COUNT header.
idxAnnounce := strings.Index(out, "Running 2 packages with parallelism=")
idxHeader := strings.Index(out, "STATUS")
idxHeader := strings.Index(out, "COUNT")
if idxAnnounce < 0 || idxHeader < 0 || idxAnnounce >= idxHeader {
t.Errorf(
"expected leading announcement before STATUS header "+
"expected leading announcement before COUNT header "+
"(announce=%d, header=%d):\n%s",
idxAnnounce, idxHeader, out,
)
Expand All @@ -527,6 +544,19 @@ func TestRun_EmitsProgress_WhenAutoParallelism(t *testing.T) {
}
}

// hasProgressLineWith reports whether out contains a single line
// that matches both the count marker and an additional substring
// (used to verify progress lines carry full row data, not just the
// counter).
func hasProgressLineWith(out, marker, contains string) bool {
for _, line := range strings.Split(out, "\n") {
if strings.Contains(line, marker) && strings.Contains(line, contains) {
return true
}
}
return false
}

func TestRun_SuppressesProgress_WhenExplicitParallelism(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)
Expand Down Expand Up @@ -557,9 +587,18 @@ func TestRun_SuppressesProgress_WhenExplicitParallelism(t *testing.T) {
if strings.Contains(out, "[1/2]") || strings.Contains(out, "[2/2]") {
t.Errorf("output unexpectedly contains progress prefix:\n%s", out)
}
if strings.Contains(out, "progress will appear") {
if strings.Contains(out, "Running 2 packages with parallelism=") {
t.Errorf("output unexpectedly contains leading announcement:\n%s", out)
}
if strings.Contains(out, "COUNT") {
t.Errorf("output unexpectedly contains COUNT progress header:\n%s", out)
}
if got := strings.Count(out, "STATUS"); got != 1 {
t.Errorf(
"expected exactly one STATUS header (no progress "+
"section); got %d:\n%s", got, out,
)
}
}

func TestMeasure_Pass(t *testing.T) {
Expand Down
75 changes: 52 additions & 23 deletions internal/services/covratchet/covratchet.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,28 +74,19 @@ func (r *runner) run(opts Opts) error {
return err
}

if r.emitProgress {
_, _ = fmt.Fprintf(
w, "Running %d packages with parallelism=%d; "+
"progress will appear as packages finish:\n",
len(pkgs), parallelism,
)
}

printHeader(w)

r.writeRunHeader(w, len(pkgs), parallelism)
ctx := ratchetCtx{
module: module,
srcPrefix: opts.SrcPrefix,
testDir: opts.TestDir,
}
results := r.runPackages(pkgs, ctx, parallelism, w)

updated := 0
unchanged := 0
failed := 0
updated, unchanged, failed := 0, 0, 0
for _, res := range results {
_, _ = fmt.Fprint(w, res.output)
if !r.emitProgress {
_, _ = fmt.Fprint(w, res.output)
}
updated += res.updated
unchanged += res.unchanged
failed += res.failed
Expand All @@ -112,6 +103,21 @@ func (r *runner) run(opts Opts) error {
return nil
}

// writeRunHeader writes the announce line and the progress table
// header when progress is enabled; otherwise it writes the
// standard table header.
func (r *runner) writeRunHeader(w io.Writer, total, parallelism int) {
if !r.emitProgress {
printHeader(w)
return
}
_, _ = fmt.Fprintf(
w, "Running %d packages with parallelism=%d; progress:\n",
total, parallelism,
)
printProgressHeader(w, total)
}

func printHeader(w io.Writer) {
_, _ = fmt.Fprintf(
w, "%-6s %8s %8s %s\n",
Expand All @@ -123,6 +129,33 @@ func printHeader(w io.Writer) {
)
}

// printProgressHeader prints the table header used during the live
// progress stream. It adds a leading COUNT column ahead of the
// standard columns so each progress line carries an [N/total] tag
// aligned under "COUNT".
func printProgressHeader(w io.Writer, total int) {
cw := progressColWidth(total)
dashes := strings.Repeat("-", cw)
_, _ = fmt.Fprintf(
w, "%-*s %-6s %8s %8s %s\n",
cw, "COUNT", "STATUS", "PREVIOUS", "CURRENT", "PACKAGE",
)
_, _ = fmt.Fprintf(
w, "%-*s %-6s %8s %8s %s\n",
cw, dashes, "------", "--------", "-------", "-------",
)
}

// progressColWidth returns the width of the COUNT column for total
// packages. The value 3 + 2*ndigits is always >= 5 ("COUNT") for
// total >= 0, so no clamp is needed.
func progressColWidth(total int) int { return 3 + 2*len(strconv.Itoa(total)) }

// firstLine returns the first line of s, including the trailing
// newline. s must contain at least one '\n' — every ratchetResult's
// output is built from a "%...\n" format string, so this holds.
func firstLine(s string) string { return s[:strings.IndexByte(s, '\n')+1] }

// ratchetCtx holds the per-run constants threaded into ratchetPackage.
type ratchetCtx struct {
module string
Expand All @@ -138,6 +171,8 @@ func (r *runner) runPackages(
sem := make(chan struct{}, parallelism)
var wg sync.WaitGroup
var progressMu sync.Mutex
countWidth := len(strconv.Itoa(total))
colWidth := progressColWidth(total)

for i, pkg := range pkgs {
wg.Add(1)
Expand All @@ -147,12 +182,11 @@ func (r *runner) runPackages(
defer func() { <-sem }()
results[idx] = r.ratchetPackage(p, ctx.module, ctx.srcPrefix, ctx.testDir)
if r.emitProgress {
label := fmt.Sprintf("[%*d/%d]", countWidth, idx+1, total)
progressMu.Lock()
_, _ = fmt.Fprintf(
w, "[%d/%d] %s %s\n",
idx+1, total,
progressStatus(results[idx].output),
gocover.RelPkg(p, ctx.module),
w, "%-*s %s",
colWidth, label, firstLine(results[idx].output),
)
progressMu.Unlock()
}
Expand All @@ -162,11 +196,6 @@ func (r *runner) runPackages(
return results
}

// progressStatus extracts the first whitespace-delimited token of
// the result's first output line, which is the status keyword
// (NEW, UP, OK, FAIL) printed by ratchetPackage.
func progressStatus(output string) string { return strings.Fields(output)[0] }

// ratchetResult holds the output and counts for a single package ratchet.
type ratchetResult struct {
output string
Expand Down
Loading