Files
temporal/tools/ci-notify/datarace_test.go
Alan Wu 00d09082cc Add data race summary to CI report (#11211)
## What changed?
Add data race summary to CI report

## Why?
Notify when CI detects data race issues in main.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [ ] added new functional test(s)
2026-08-26 10:02:29 -07:00

211 lines
7.5 KiB
Go

package cinotify
import (
"archive/zip"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.temporal.io/server/tools/common/github"
)
func TestDataRacesFromRows(t *testing.T) {
rows := []summaryRow{
{Kind: "Failed", Name: "TestFoo (final)", Final: true},
{
Kind: "DATA RACE",
Name: "DATA RACE: Data race detected — in go.temporal.io/server/service/history.TestBar",
Details: "WARNING: DATA RACE\nRead at 0x00c0...",
},
{Kind: "OOM", Name: "OOM prevention"},
{Kind: "PANIC", Name: "PANIC: boom"},
}
races := dataRacesFromRows(rows, "job-1")
require.Len(t, races, 1)
require.Equal(t, "service/history.TestBar", races[0].Location)
require.Equal(t, "job-1", races[0].JobID)
require.Contains(t, races[0].Details, "WARNING: DATA RACE")
}
func TestDataRacesFromRowsNone(t *testing.T) {
rows := []summaryRow{
{Kind: "Failed", Name: "TestFoo (final)", Final: true},
{Kind: "OOM", Name: "OOM prevention"},
}
require.Empty(t, dataRacesFromRows(rows, "job-1"))
}
func TestDataRaceLocation(t *testing.T) {
require.Equal(t,
"service/history.TestBar",
dataRaceLocation("DATA RACE: Data race detected — in go.temporal.io/server/service/history.TestBar"),
)
// A race not attributed to a temporal package keeps its qualified name.
require.Equal(t,
"example.com/foo.TestBaz",
dataRaceLocation("DATA RACE: Data race detected — in example.com/foo.TestBaz"),
)
// A name without the "— in" locator falls back to the summary text.
require.Equal(t, "Data race detected", dataRaceLocation("DATA RACE: Data race detected"))
}
func TestJobIDFromArtifactName(t *testing.T) {
require.Equal(t, "12345", jobIDFromArtifactName("test-summary-json--999--12345--1--unit-test"))
require.Empty(t, jobIDFromArtifactName("test-summary-json--999"))
}
func TestUniqueDataRaces(t *testing.T) {
// The same race reported by two shards: identical source lines, but the race
// detector prints different memory addresses and goroutine ids each run.
shard1 := readWriteRaceDetails
shard2 := strings.NewReplacer(
"0x00c0001121e8", "0xdeadbeef",
"goroutine 8", "goroutine 42",
"goroutine 7", "goroutine 99",
).Replace(shard1)
// A race at a different source line is a distinct race.
otherLine := strings.ReplaceAll(shard1, "mutable_state.go:130", "mutable_state.go:200")
races := []DataRace{
{Location: "service/history.TestMS", Details: shard1, JobID: "1"},
{Location: "service/history.TestMS", Details: shard2, JobID: "2"}, // dup of shard1
{Location: "service/history.TestMS", Details: otherLine}, // different line
{Location: "service/matching.TestCache", Details: shard1}, // different test
}
unique := uniqueDataRaces(races)
require.Len(t, unique, 3)
require.Equal(t, "1", unique[0].JobID) // first occurrence wins
require.Equal(t, "service/history.TestMS", unique[1].Location)
require.Equal(t, "service/matching.TestCache", unique[2].Location)
}
func TestSummaryRowsFromZipParsesDataRaceDetails(t *testing.T) {
dir := t.TempDir()
zipPath := filepath.Join(dir, "artifact.zip")
file, err := os.Create(zipPath)
require.NoError(t, err)
writer := zip.NewWriter(file)
summaryFile, err := writer.Create("test-summary.json")
require.NoError(t, err)
_, err = summaryFile.Write([]byte(`{
"rows": [
{
"kind": "DATA RACE",
"name": "DATA RACE: Data race detected — in go.temporal.io/server/pkg.TestRacy",
"details": "WARNING: DATA RACE\nWrite at 0x00c000"
}
]
}`))
require.NoError(t, err)
require.NoError(t, writer.Close())
require.NoError(t, file.Close())
rows, err := summaryRowsFromZip(zipPath)
require.NoError(t, err)
races := dataRacesFromRows(rows, "job-9")
require.Len(t, races, 1)
require.Equal(t, "pkg.TestRacy", races[0].Location)
require.Contains(t, races[0].Details, "WARNING: DATA RACE")
}
// readWriteRaceDetails is a read/write race whose top frame is the race
// detector's runtime shim (which must be skipped) and whose source paths use the
// CI checkout prefix (which must be trimmed).
const readWriteRaceDetails = `==================
WARNING: DATA RACE
Read at 0x00c0001121e8 by goroutine 8:
runtime.raceread()
/usr/local/go/src/runtime/race_amd64.s:260 +0x21
go.temporal.io/server/service/history.(*ms).Get()
/home/runner/work/temporal/temporal/service/history/mutable_state.go:127 +0x2c
Previous write at 0x00c0001121e8 by goroutine 7:
runtime.racewrite()
/usr/local/go/src/runtime/race_amd64.s:269 +0x21
go.temporal.io/server/service/history.(*ms).Set()
/home/runner/work/temporal/temporal/service/history/mutable_state.go:130 +0x3c
Goroutine 8 (running) created at:
go.temporal.io/server/service/history.TestMutableState()
/home/runner/work/temporal/temporal/service/history/mutable_state_test.go:41 +0x88
==================`
// writeWriteRaceDetails is a race between two concurrent writes.
const writeWriteRaceDetails = `==================
WARNING: DATA RACE
Write at 0x00c000000180 by goroutine 7:
go.temporal.io/server/service/matching.(*cache).put()
/home/runner/work/temporal/temporal/service/matching/cache.go:88 +0x66
Previous write at 0x00c000000180 by goroutine 12:
go.temporal.io/server/service/matching.(*cache).put()
/home/runner/work/temporal/temporal/service/matching/cache.go:88 +0x66
==================`
func TestRaceSites(t *testing.T) {
require.Equal(t, []string{
"Read at (goroutine 8): service/history/mutable_state.go:127",
"Previous write at (goroutine 7): service/history/mutable_state.go:130",
}, raceSites(readWriteRaceDetails))
// A write/write race is still a race; both writes must be surfaced.
require.Equal(t, []string{
"Write at (goroutine 7): service/matching/cache.go:88",
"Previous write at (goroutine 12): service/matching/cache.go:88",
}, raceSites(writeWriteRaceDetails))
// Unparseable details yield no sites; the caller falls back to location + link.
require.Empty(t, raceSites("some unrelated text"))
}
func TestBuildDataRaceMessageLinksToJob(t *testing.T) {
report := &DataRaceReport{
Run: github.Run{
DatabaseID: 123456,
HeadSHA: "abc1234567890defghijk",
URL: "https://github.com/temporalio/temporal/actions/runs/123456",
},
Author: "Test Author",
Title: "Some commit title",
DataRaces: []DataRace{
{Location: "service/history.TestMutableState", Details: readWriteRaceDetails, JobID: "789"},
},
}
rendered := BuildDataRaceMessage(report).RenderMarkdown()
require.Contains(t, rendered, "Data Race Detected on Main Branch")
require.Contains(t, rendered, "Test Author")
require.Contains(t, rendered, "service/history.TestMutableState")
require.Contains(t, rendered, "Read at (goroutine 8): service/history/mutable_state.go:127")
require.Contains(t, rendered, "Previous write at (goroutine 7): service/history/mutable_state.go:130")
require.Contains(t, rendered, "abc1234") // short SHA link
// Links to the specific job, not just the top-level run.
require.Contains(t, rendered, "actions/runs/123456/job/789")
// Runtime shim frames and raw stacktrace noise are not dumped into Slack.
require.NotContains(t, rendered, "race_amd64.s")
}
func TestBuildDataRaceMessageFallsBackToRunLink(t *testing.T) {
report := &DataRaceReport{
Run: github.Run{DatabaseID: 123456, HeadSHA: "abc1234567890"},
DataRaces: []DataRace{{Location: "pkg.TestRacy", Details: "unparseable"}}, // no JobID
}
rendered := BuildDataRaceMessage(report).RenderMarkdown()
require.Contains(t, rendered, "Unknown") // author
require.Contains(t, rendered, "pkg.TestRacy")
require.Contains(t, rendered, "actions/runs/123456")
require.NotContains(t, rendered, "/job/")
}