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
227 changes: 190 additions & 37 deletions internal/runner/analyze.go

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions internal/runner/analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,94 @@ func TestDigestIterationJSONL(t *testing.T) {
assert.Equal(t, 1, d.SkipTests)
assert.Equal(t, "pass", d.Result)
})

t.Run("data race only", func(t *testing.T) {
t.Parallel()
jsonl := `{"Action":"output","Package":"pkg/r","Test":"TestRace","Output":"WARNING: DATA RACE\n"}
{"Action":"fail","Package":"pkg/r","Test":"TestRace","Elapsed":0.1}
`
d, err := DigestIterationJSONL(strings.NewReader(jsonl), 30*time.Second)
require.NoError(t, err)
assert.Equal(t, "race", d.Result)
assert.Equal(t, 1, d.Races)
assert.Equal(t, 0, d.FailTests)
assert.Equal(t, []string{"TestRace"}, d.RacingTests)
assert.Empty(t, d.FailingTests)
})

t.Run("data race only with package failure", func(t *testing.T) {
t.Parallel()
jsonl := `{"Action":"output","Package":"pkg/r","Test":"TestRace","Output":"WARNING: DATA RACE\n"}
{"Action":"fail","Package":"pkg/r","Test":"TestRace","Elapsed":0.1}
{"Action":"fail","Package":"pkg/r","Elapsed":0.1}
`
d, err := DigestIterationJSONL(strings.NewReader(jsonl), 30*time.Second)
require.NoError(t, err)
assert.Equal(t, "race", d.Result)
assert.Equal(t, 1, d.Races)
assert.Equal(t, 0, d.FailTests)
assert.Equal(t, []string{"TestRace"}, d.RacingTests)
assert.Empty(t, d.FailingTests)
})

t.Run("fail and race", func(t *testing.T) {
t.Parallel()
jsonl := `{"Action":"fail","Package":"p","Test":"TestFail","Elapsed":0.1}
{"Action":"output","Package":"p","Test":"TestRace","Output":"WARNING: DATA RACE\n"}
{"Action":"fail","Package":"p","Test":"TestRace","Elapsed":0.1}
`
d, err := DigestIterationJSONL(strings.NewReader(jsonl), 30*time.Second)
require.NoError(t, err)
assert.Equal(t, "fail+race", d.Result)
assert.Equal(t, 1, d.Races)
assert.Equal(t, 1, d.FailTests)
assert.Equal(t, []string{"TestFail"}, d.FailingTests)
assert.Equal(t, []string{"TestRace"}, d.RacingTests)
})

t.Run("timeout and race", func(t *testing.T) {
t.Parallel()
jsonl := `{"Action":"output","Package":"p","Test":"TestHang","Output":"panic: test timed out after 2m0s\n"}
{"Action":"output","Package":"p","Test":"TestHang","Output":"WARNING: DATA RACE\n"}
{"Action":"fail","Package":"p","Test":"TestHang","Elapsed":120.0}
`
d, err := DigestIterationJSONL(strings.NewReader(jsonl), 30*time.Second)
require.NoError(t, err)
assert.Equal(t, "timeout+race", d.Result)
assert.Equal(t, 1, d.Races)
assert.Equal(t, 0, d.FailTests)
assert.Equal(t, 1, d.TimeoutTests)
assert.Equal(t, []string{"TestHang"}, d.TimedOutTests)
})
}

func TestAnalyzeTimeoutRaceGoesToRaces(t *testing.T) {
t.Parallel()
rep, _ := analyze(t, readers(
`{"Action":"output","Package":"p","Test":"TestHang","Output":"panic: test timed out after 2m0s\n"}
{"Action":"output","Package":"p","Test":"TestHang","Output":"WARNING: DATA RACE\n"}
{"Action":"fail","Package":"p","Test":"TestHang","Elapsed":120.0}`,
), 30*time.Second)
require.Len(t, rep.Races, 1)
assert.Equal(t, "TestHang", rep.Races[0].Test)
assert.Empty(t, rep.Timeouts)
assert.Empty(t, rep.Failures)
require.NotNil(t, rep.Summary)
assert.Equal(t, 1, rep.Summary.RaceNamedCount)
}

func TestAnalyzeDataRaceSeparation(t *testing.T) {
t.Parallel()
rep, _ := analyze(t, readers(
`{"Action":"output","Package":"pkg/r","Test":"TestRace","Output":"WARNING: DATA RACE\n"}
{"Action":"fail","Package":"pkg/r","Test":"TestRace","Elapsed":0.1}`,
), 30*time.Second)
require.Len(t, rep.Races, 1)
assert.Equal(t, "TestRace", rep.Races[0].Test)
assert.Empty(t, rep.Failures)
require.Len(t, rep.IterationSummaries, 1)
assert.Equal(t, "race", rep.IterationSummaries[0].Result)
assert.Equal(t, 1, rep.IterationSummaries[0].Races)
}

func TestAnalyze(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions internal/runner/diagnose_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type aiDiagnoseComplete struct {

type aiFindings struct {
Broken []aiFinding `json:"broken,omitempty"`
Races []aiFinding `json:"races,omitempty"`
Flaky []aiFinding `json:"flaky,omitempty"`
Timeouts []aiFinding `json:"timeouts,omitempty"`
Slow []aiFinding `json:"slow,omitempty"`
Expand Down Expand Up @@ -61,6 +62,7 @@ func aiFindingsFromReport(rep *Report) aiFindings {
}
return aiFindings{
Broken: aiFindingsFromEntries(rep.Failures, aiFindingFromEntry),
Races: aiFindingsFromEntries(rep.Races, aiFindingFromEntry),
Flaky: aiFindingsFromEntries(rep.Flakes, aiFindingFromEntry),
Timeouts: aiFindingsFromEntries(rep.Timeouts, aiFindingFromEntry),
Slow: aiFindingsFromEntries(rep.Slow, aiFindingFromSlowEntry),
Expand Down
19 changes: 17 additions & 2 deletions internal/runner/diagnose_overall_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
overallRateBroken overallRateKind = iota
overallRateFlaky
overallRateSlow
overallRateRace
)

type overallRateRow struct {
Expand Down Expand Up @@ -82,6 +83,15 @@ func buildOverallRateRows(rep *Report) []overallRateRow {
num: s.FlakeNamedCount,
kind: overallRateFlaky,
})
if rep.Run != nil && rep.Run.Race && s.RaceNamedCount > 0 {
rows = append(rows, overallRateRow{
label: "Races",
count: fmt.Sprintf("%d", s.RaceNamedCount),
rate: formatOverallRatePct(s.RaceNamedCount, denom),
num: s.RaceNamedCount,
kind: overallRateRace,
})
}
if rep.SlowThreshold > 0 && s.SlowPrevalence != nil {
rows = append(rows, overallRateRow{
label: "Slow Tests",
Expand Down Expand Up @@ -134,6 +144,9 @@ func overallRatePctStyle(kind overallRateKind, num int) lipgloss.Style {
if kind == overallRateSlow {
return termstyle.Flaky
}
if kind == overallRateRace {
return termstyle.Flaky
}
return termstyle.Bad
}

Expand Down Expand Up @@ -167,10 +180,12 @@ func overallRatesTableStyle(row, _ int) lipgloss.Style {

func overallRateCountCell(r overallRateRow) string {
if r.num > 0 {
if r.kind == overallRateSlow {
switch r.kind {
case overallRateSlow, overallRateRace:
return termstyle.Flaky.Render(r.count)
default:
return termstyle.Bad.Render(r.count)
}
return termstyle.Bad.Render(r.count)
}
return termstyle.OK.Render(r.count)
}
Expand Down
9 changes: 6 additions & 3 deletions internal/runner/diagnose_overall_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,24 @@ func TestBuildOverallRateRows_flakyIterationsLast(t *testing.T) {
rep := &Report{
Iterations: 2,
SlowThreshold: time.Second,
Run: &RunMeta{Race: true},
Summary: &ReportSummary{
DistinctNamedTests: 1,
FlakeIterationTotal: 2,
FlakeFailingIterations: 0,
SlowCount: 0,
SlowPrevalence: &slowPrev,
RaceNamedCount: 1,
},
}
flakeIterRate := 0.0
rep.Summary.FlakeIterationFailRate = &flakeIterRate

rows := buildOverallRateRows(rep)
require.Len(t, rows, 4)
assert.Equal(t, "Slow Tests", rows[2].label)
assert.Equal(t, "Flaky Iterations", rows[3].label)
require.Len(t, rows, 5)
assert.Equal(t, "Races", rows[2].label)
assert.Equal(t, "Slow Tests", rows[3].label)
assert.Equal(t, "Flaky Iterations", rows[4].label)
}

func TestFormatOverallFlakyIterRate_CIColoredByGap(t *testing.T) {
Expand Down
53 changes: 39 additions & 14 deletions internal/runner/diagnose_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,59 @@ import (
"github.com/smartcontractkit/testrig/internal/termstyle"
)

// DiagnoseTableOpts configures the streaming diagnose iteration table.
type DiagnoseTableOpts struct {
RaceEnabled bool
}

// Fixed column widths for streaming rows (each row is formatted independently).
const (
diagnoseColIter = 5
diagnoseColResult = 8
diagnoseColTests = 8
diagnoseColCount = 8
diagnoseColRuntime = 10
diagnoseColIter = 5
diagnoseColResult = 8
diagnoseColResultRace = 12
diagnoseColTests = 8
diagnoseColCount = 8
diagnoseColRuntime = 10
)

func printDiagnoseIterationTableHeader(out *output.Printer) {
func printDiagnoseIterationTableHeader(out *output.Printer, opts DiagnoseTableOpts) {
if out.AIOutput() {
return
}
out.HumanStderr(termstyle.Muted.Render(diagnoseTableHeaderPlain()))
out.HumanStderr(termstyle.Muted.Render(strings.Repeat("─", len(diagnoseTableHeaderPlain()))))
header := diagnoseTableHeaderPlain(opts)
out.HumanStderr(termstyle.Muted.Render(header))
out.HumanStderr(termstyle.Muted.Render(strings.Repeat("─", len(header))))
}

func diagnoseTableHeaderPlain() string {
func diagnoseTableHeaderPlain(opts DiagnoseTableOpts) string {
if opts.RaceEnabled {
return fmt.Sprintf("%5s %-12s %8s %8s %8s %8s %8s %8s %10s",
"Iter", "Result", "Tests", "Skipped", "Failures", "Races", "Timeouts", "Slow", "Runtime")
}
return fmt.Sprintf("%5s %-8s %8s %8s %8s %8s %8s %10s",
"Iter", "Result", "Tests", "Skipped", "Failures", "Timeouts", "Slow", "Runtime")
}

func formatDiagnoseIterationTableRow(iter int, d IterationDigest, dur time.Duration) string {
func formatDiagnoseIterationTableRow(iter int, d IterationDigest, dur time.Duration, opts DiagnoseTableOpts) string {
resultWidth := diagnoseColResult
if opts.RaceEnabled {
resultWidth = diagnoseColResultRace
}
iterCol := lipgloss.PlaceHorizontal(diagnoseColIter, lipgloss.Right, termstyle.Label.Render(strconv.Itoa(iter)))
resCol := lipgloss.PlaceHorizontal(diagnoseColResult, lipgloss.Left, renderIterationResultHuman(d.Result))
resCol := lipgloss.PlaceHorizontal(resultWidth, lipgloss.Left, renderIterationResultHuman(d.Result))
testsCol := lipgloss.PlaceHorizontal(
diagnoseColTests,
lipgloss.Right,
termstyle.Muted.Render(strconv.Itoa(d.RanTests)),
)
skipCol := lipgloss.PlaceHorizontal(diagnoseColCount, lipgloss.Right, diagnoseTableCountStyled(d.SkipTests, "skip"))
failCol := lipgloss.PlaceHorizontal(diagnoseColCount, lipgloss.Right, diagnoseTableCountStyled(d.FailTests, "fail"))
gap := " "
parts := []string{iterCol, gap, resCol, gap, testsCol, gap, skipCol, gap, failCol}
if opts.RaceEnabled {
raceCol := lipgloss.PlaceHorizontal(diagnoseColCount, lipgloss.Right, diagnoseTableCountStyled(d.Races, "race"))
parts = append(parts, gap, raceCol)
}
toCol := lipgloss.PlaceHorizontal(
diagnoseColCount,
lipgloss.Right,
Expand All @@ -53,9 +74,8 @@ func formatDiagnoseIterationTableRow(iter int, d IterationDigest, dur time.Durat
slowCol := lipgloss.PlaceHorizontal(diagnoseColCount, lipgloss.Right, diagnoseTableCountStyled(d.SlowTests, "slow"))
rt := termstyle.Muted.Render(dur.Round(time.Second).String())
rtCol := lipgloss.PlaceHorizontal(diagnoseColRuntime, lipgloss.Right, rt)
gap := " "
return lipgloss.JoinHorizontal(lipgloss.Top,
iterCol, gap, resCol, gap, testsCol, gap, skipCol, gap, failCol, gap, toCol, gap, slowCol, gap, rtCol)
parts = append(parts, gap, toCol, gap, slowCol, gap, rtCol)
return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
}

func diagnoseTableCountStyled(n int, kind string) string {
Expand All @@ -66,6 +86,11 @@ func diagnoseTableCountStyled(n int, kind string) string {
return termstyle.OK.Render(s)
}
return termstyle.Bad.Render(s)
case "race":
if n == 0 {
return termstyle.OK.Render(s)
}
return termstyle.Flaky.Render(s)
case "slow":
if n == 0 {
return termstyle.OK.Render(s)
Expand Down
60 changes: 52 additions & 8 deletions internal/runner/diagnose_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,35 @@ func stripANSI(s string) string {

func TestDiagnoseTableHeaderPlain(t *testing.T) {
t.Parallel()
got := diagnoseTableHeaderPlain()
got := diagnoseTableHeaderPlain(DiagnoseTableOpts{})
want := fmt.Sprintf("%5s %-8s %8s %8s %8s %8s %8s %10s",
"Iter", "Result", "Tests", "Skipped", "Failures", "Timeouts", "Slow", "Runtime")
assert.Equal(t, want, got)
assert.Len(t, got, len(want))

raceGot := diagnoseTableHeaderPlain(DiagnoseTableOpts{RaceEnabled: true})
raceWant := fmt.Sprintf("%5s %-12s %8s %8s %8s %8s %8s %8s %10s",
"Iter", "Result", "Tests", "Skipped", "Failures", "Races", "Timeouts", "Slow", "Runtime")
assert.Equal(t, raceWant, raceGot)
}

func TestPrintDiagnoseIterationTableHeader(t *testing.T) {
t.Parallel()
var stderr strings.Builder
p := output.NewForTest(false, &strings.Builder{}, &stderr, false)
printDiagnoseIterationTableHeader(p)
printDiagnoseIterationTableHeader(p, DiagnoseTableOpts{})
s := strings.TrimRight(stripANSI(stderr.String()), "\n")
lines := strings.Split(s, "\n")
require.Len(t, lines, 2)
assert.Equal(t, diagnoseTableHeaderPlain(), lines[0])
assert.Equal(t, strings.Repeat("─", len(diagnoseTableHeaderPlain())), lines[1])
assert.Equal(t, diagnoseTableHeaderPlain(DiagnoseTableOpts{}), lines[0])
assert.Equal(t, strings.Repeat("─", len(diagnoseTableHeaderPlain(DiagnoseTableOpts{}))), lines[1])
}

func TestFormatDiagnoseIterationTableRow(t *testing.T) {
t.Parallel()
cases := []struct {
name string
opts DiagnoseTableOpts
iter int
d IterationDigest
dur time.Duration
Expand Down Expand Up @@ -79,21 +85,55 @@ func TestFormatDiagnoseIterationTableRow(t *testing.T) {
dur: time.Hour,
wantSans: " 3 timeout 0 0 0 1 0 1h0m0s",
},
{
name: "race_enabled",
opts: DiagnoseTableOpts{RaceEnabled: true},
iter: 5,
d: IterationDigest{
Result: "fail+race", RanTests: 4, FailTests: 1, Races: 2, TimeoutTests: 0, SkipTests: 0, SlowTests: 0,
},
dur: 45 * time.Second,
wantSans: " 5 fail+race 4 0 1 2 0 0 45s",
},
{
name: "timeout_race_enabled",
opts: DiagnoseTableOpts{RaceEnabled: true},
iter: 2,
d: IterationDigest{
Result: "timeout+race",
RanTests: 1,
FailTests: 0,
Races: 1,
TimeoutTests: 1,
SkipTests: 0,
SlowTests: 0,
},
dur: 2 * time.Minute,
wantSans: " 2 timeout+race 1 0 0 1 1 0 2m0s",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := stripANSI(formatDiagnoseIterationTableRow(tc.iter, tc.d, tc.dur))
got := stripANSI(formatDiagnoseIterationTableRow(tc.iter, tc.d, tc.dur, tc.opts))
assert.Equal(t, tc.wantSans, got)
})
}
}

func TestRenderIterationResultHuman_combinedRace(t *testing.T) {
t.Parallel()
assert.Contains(t, stripANSI(renderIterationResultHuman("fail+race")), "fail")
assert.Contains(t, stripANSI(renderIterationResultHuman("fail+race")), "race")
assert.Contains(t, stripANSI(renderIterationResultHuman("timeout+race")), "timeout")
assert.Contains(t, stripANSI(renderIterationResultHuman("timeout+race")), "race")
}

func TestFormatDiagnoseProblemTestsSuffix(t *testing.T) {
t.Parallel()
baseRowANSI := formatDiagnoseIterationTableRow(19, IterationDigest{
Result: "fail", RanTests: 8657, SkipTests: 103, FailTests: 4, TimeoutTests: 0, SlowTests: 14,
}, 3*time.Minute)
}, 3*time.Minute, DiagnoseTableOpts{})
baseRow := stripANSI(baseRowANSI)
cases := []struct {
name string
Expand Down Expand Up @@ -163,10 +203,14 @@ func TestPrintIterationDigestHuman_withFailingTests(t *testing.T) {
FailTests: 2,
FailingTests: []string{"Testxxx", "Testyyy"},
}
printIterationDigestHuman(out, 19, d, 3*time.Minute)
printIterationDigestHuman(out, 19, d, 3*time.Minute, DiagnoseTableOpts{})

got := stderr.String()
require.Contains(t, got, termstyle.Bad.Render("Testxxx"))
require.Contains(t, got, termstyle.Bad.Render("Testyyy"))
require.Contains(t, stripANSI(got), stripANSI(formatDiagnoseIterationTableRow(19, d, 3*time.Minute)))
require.Contains(
t,
stripANSI(got),
stripANSI(formatDiagnoseIterationTableRow(19, d, 3*time.Minute, DiagnoseTableOpts{})),
)
}
Loading
Loading