ci: Out of the tarpit (#2923)

* Lint fixes

* Use latest go version for go-check

Fixes nil pointer issue in staticcheck

* Add test_analysis helper script

* Use custom go-test-template

* Add some tests to the test_analysis script

* Always upload test_results db

* Attempt to fix test on windows

* Better if statement

* Try to fix flaky test

* Disable caching setup-go on Windows

* Better if statement

* Tweak

* Always upload summary and artifact

* Close db

* No extra newline
This commit is contained in:
Marco Munizaga
2024-08-20 11:13:32 -07:00
committed by GitHub
parent 99433a2f99
commit 6c12e2237a
14 changed files with 613 additions and 11 deletions
+6
View File
@@ -9,3 +9,9 @@ runs:
shell: bash
# This matches only tests with "NoCover" in their test name to avoid running all tests again.
run: go test -tags nocover -run NoCover -v ./...
- name: Install testing tools
shell: bash
run: cd scripts/test_analysis && go install ./cmd/gotest2sql
- name: Install test_analysis
shell: bash
run: cd scripts/test_analysis && go install .
+1
View File
@@ -17,4 +17,5 @@ jobs:
go-check:
uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v1.0
with:
go-version: "1.22.x"
go-generate-ignore-protoc-version-comments: true
+154
View File
@@ -0,0 +1,154 @@
name: Go Test
on:
workflow_call:
inputs:
go-versions:
required: false
type: string
default: '["this", "next"]'
secrets:
CODECOV_TOKEN:
required: false
defaults:
run:
shell: bash
jobs:
unit:
strategy:
fail-fast: false
matrix:
os: ["ubuntu", "macos", "windows"]
go: ${{ fromJSON(inputs.go-versions) }}
env:
GOTESTFLAGS: -cover -coverprofile=module-coverage.txt -coverpkg=./...
GO386FLAGS: ""
GORACEFLAGS: ""
runs-on: ${{ fromJSON(vars[format('UCI_GO_TEST_RUNNER_{0}', matrix.os)] || format('"{0}-latest"', matrix.os)) }}
name: ${{ matrix.os }} (go ${{ matrix.go }})
steps:
- name: Use msys2 on windows
if: matrix.os == 'windows'
# The executable for msys2 is also called bash.cmd
# https://github.com/actions/virtual-environments/blob/main/images/win/Windows2019-Readme.md#shells
# If we prepend its location to the PATH
# subsequent 'shell: bash' steps will use msys2 instead of gitbash
run: echo "C:/msys64/usr/bin" >> $GITHUB_PATH
- name: Check out the repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Check out the latest stable version of Go
uses: actions/setup-go@v5
with:
go-version: stable
cache: ${{ matrix.os != 'windows' }} # Windows VMs are slow to use caching. Can add ~15m to the job
- name: Read the Unified GitHub Workflows configuration
id: config
uses: ipdxco/unified-github-workflows/.github/actions/read-config@main
- name: Read the go.mod file
id: go-mod
uses: ipdxco/unified-github-workflows/.github/actions/read-go-mod@main
- name: Determine the Go version to use based on the go.mod file
id: go
env:
MATRIX_GO: ${{ matrix.go }}
GO_MOD_VERSION: ${{ fromJSON(steps.go-mod.outputs.json).Go }}
run: |
if [[ "$MATRIX_GO" == "this" ]]; then
echo "version=$GO_MOD_VERSION.x" >> $GITHUB_OUTPUT
elif [[ "$MATRIX_GO" == "next" ]]; then
MAJOR="${GO_MOD_VERSION%.[0-9]*}"
MINOR="${GO_MOD_VERSION#[0-9]*.}"
echo "version=$MAJOR.$(($MINOR+1)).x" >> $GITHUB_OUTPUT
elif [[ "$MATRIX_GO" == "prev" ]]; then
MAJOR="${GO_MOD_VERSION%.[0-9]*}"
MINOR="${GO_MOD_VERSION#[0-9]*.}"
echo "version=$MAJOR.$(($MINOR-1)).x" >> $GITHUB_OUTPUT
else
echo "version=$MATRIX_GO" >> $GITHUB_OUTPUT
fi
- name: Enable shuffle flag for go test command
if: toJSON(fromJSON(steps.config.outputs.json).shuffle) != 'false'
run: |
echo "GOTESTFLAGS=-shuffle=on $GOTESTFLAGS" >> $GITHUB_ENV
echo "GO386FLAGS=-shuffle=on $GO386FLAGS" >> $GITHUB_ENV
echo "GORACEFLAGS=-shuffle=on $GORACEFLAGS" >> $GITHUB_ENV
- name: Enable verbose flag for go test command
if: toJSON(fromJSON(steps.config.outputs.json).verbose) != 'false'
run: |
echo "GOTESTFLAGS=-v $GOTESTFLAGS" >> $GITHUB_ENV
echo "GO386FLAGS=-v $GO386FLAGS" >> $GITHUB_ENV
echo "GORACEFLAGS=-v $GORACEFLAGS" >> $GITHUB_ENV
- name: Set extra flags for go test command
if: fromJSON(steps.config.outputs.json).gotestflags != ''
run: |
echo "GOTESTFLAGS=${{ fromJSON(steps.config.outputs.json).gotestflags }} $GOTESTFLAGS" >> $GITHUB_ENV
- name: Set extra flags for go test race command
if: fromJSON(steps.config.outputs.json).goraceflags != ''
run: |
echo "GORACEFLAGS=${{ fromJSON(steps.config.outputs.json).goraceflags }} $GORACEFLAGS" >> $GITHUB_ENV
- name: Set up the Go version read from the go.mod file
uses: actions/setup-go@v5
with:
go-version: ${{ steps.go.outputs.version }}
cache: ${{ matrix.os != 'windows' }} # Windows VMs are slow to use caching. Can add ~15m to the job
- name: Display the Go version and environment
run: |
go version
go env
- name: Run repo-specific setup
uses: ./.github/actions/go-test-setup
if: hashFiles('./.github/actions/go-test-setup') != ''
- name: Run tests
id: test
if: contains(fromJSON(steps.config.outputs.json).skipOSes, matrix.os) == false
uses: protocol/multiple-go-modules@v1.4
with:
run: test_analysis ${{ env.GOTESTFLAGS }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}_${{ matrix.go }}_test_results.db
path: ./test_results.db
- name: Add failure summary
if: always()
run: |
echo "### Failure Summary" >> $GITHUB_STEP_SUMMARY
test_analysis summarize >> $GITHUB_STEP_SUMMARY
- name: Remove test results
run: rm ./test_results.db
- name: Run tests with race detector
# speed things up. Windows and OSX VMs are slow
if: matrix.os == 'ubuntu' &&
fromJSON(steps.config.outputs.json).skipRace != true &&
contains(fromJSON(steps.config.outputs.json).skipOSes, matrix.os) == false
uses: protocol/multiple-go-modules@v1.4
id: race
with:
run: test_analysis -race ${{ env.GORACEFLAGS }} ./...
- name: Upload test results (Race)
if: (steps.race.conclusion == 'success' || steps.race.conclusion == 'failure')
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}_${{ matrix.go }}_test_results_race.db
path: ./test_results.db
- name: Add failure summary
if: (steps.race.conclusion == 'success' || steps.race.conclusion == 'failure')
run: |
echo "# Tests with race detector failure summary" >> $GITHUB_STEP_SUMMARY
test_analysis summarize >> $GITHUB_STEP_SUMMARY
- name: Adding Link to Run Analysis
run: echo "### [Test flakiness analysis](https://observablehq.com/d/d74435ea5bbf24c7?run-id=$GITHUB_RUN_ID)" >> $GITHUB_STEP_SUMMARY
- name: Collect coverage files
id: coverages
run: echo "files=$(find . -type f -name 'module-coverage.txt' | tr -s '\n' ',' | sed 's/,$//')" >> $GITHUB_OUTPUT
- name: Upload coverage to Codecov
uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0
with:
files: ${{ steps.coverages.outputs.files }}
env_vars: OS=${{ matrix.os }}, GO=${{ steps.go.outputs.version }}
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
+1 -1
View File
@@ -15,7 +15,7 @@ concurrency:
jobs:
go-test:
uses: libp2p/uci/.github/workflows/go-test.yml@v1.0
uses: ./.github/workflows/go-test-template.yml
with:
go-versions: '["1.21.x", "1.22.x"]'
secrets:
+25 -6
View File
@@ -5,6 +5,8 @@ import (
"crypto/rand"
"errors"
"fmt"
"net"
"net/netip"
"regexp"
"strconv"
"strings"
@@ -488,10 +490,23 @@ func TestHostAddrsFactoryAddsCerthashes(t *testing.T) {
h.Close()
}
func newRandomPort(t *testing.T) string {
t.Helper()
// Find an available port
c, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
c.LocalAddr().Network()
ipPort := netip.MustParseAddrPort(c.LocalAddr().String())
port := strconv.Itoa(int(ipPort.Port()))
require.NoError(t, c.Close())
return port
}
func TestWebRTCReuseAddrWithQUIC(t *testing.T) {
port := newRandomPort(t)
order := [][]string{
{"/ip4/127.0.0.1/udp/54322/quic-v1", "/ip4/127.0.0.1/udp/54322/webrtc-direct"},
{"/ip4/127.0.0.1/udp/54322/webrtc-direct", "/ip4/127.0.0.1/udp/54322/quic-v1"},
{"/ip4/127.0.0.1/udp/" + port + "/quic-v1", "/ip4/127.0.0.1/udp/" + port + "/webrtc-direct"},
{"/ip4/127.0.0.1/udp/" + port + "/webrtc-direct", "/ip4/127.0.0.1/udp/" + port + "/quic-v1"},
// We do not support WebRTC automatically reusing QUIC addresses if port is not specified, yet.
// {"/ip4/127.0.0.1/udp/0/webrtc-direct", "/ip4/127.0.0.1/udp/0/quic-v1"},
}
@@ -542,16 +557,18 @@ func TestWebRTCReuseAddrWithQUIC(t *testing.T) {
})
}
swapPort := func(addrStrs []string, newPort string) []string {
swapPort := func(addrStrs []string, oldPort, newPort string) []string {
out := make([]string, 0, len(addrStrs))
for _, addrStr := range addrStrs {
out = append(out, strings.Replace(addrStr, "54322", newPort, 1))
out = append(out, strings.Replace(addrStr, oldPort, newPort, 1))
}
return out
}
t.Run("setup with no reuseport. Should fail", func(t *testing.T) {
h1, err := New(ListenAddrStrings(swapPort(order[0], "54323")...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New), QUICReuse(quicreuse.NewConnManager, quicreuse.DisableReuseport()))
oldPort := port
newPort := newRandomPort(t)
h1, err := New(ListenAddrStrings(swapPort(order[0], oldPort, newPort)...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New), QUICReuse(quicreuse.NewConnManager, quicreuse.DisableReuseport()))
require.NoError(t, err) // It's a bug/feature that swarm.Listen does not error if at least one transport succeeds in listening.
defer h1.Close()
// Check that webrtc did fail to listen
@@ -560,7 +577,9 @@ func TestWebRTCReuseAddrWithQUIC(t *testing.T) {
})
t.Run("setup with autonat", func(t *testing.T) {
h1, err := New(EnableAutoNATv2(), ListenAddrStrings(swapPort(order[0], "54324")...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New), QUICReuse(quicreuse.NewConnManager, quicreuse.DisableReuseport()))
oldPort := port
newPort := newRandomPort(t)
h1, err := New(EnableAutoNATv2(), ListenAddrStrings(swapPort(order[0], oldPort, newPort)...), Transport(quic.NewTransport), Transport(libp2pwebrtc.New), QUICReuse(quicreuse.NewConnManager, quicreuse.DisableReuseport()))
require.NoError(t, err) // It's a bug/feature that swarm.Listen does not error if at least one transport succeeds in listening.
defer h1.Close()
// Check that webrtc did fail to listen
+1 -1
View File
@@ -995,7 +995,7 @@ func (ids *idService) addConnWithLock(c network.Conn) {
}
func signedPeerRecordFromMessage(msg *pb.Identify) (*record.Envelope, error) {
if msg.SignedPeerRecord == nil || len(msg.SignedPeerRecord) == 0 {
if len(msg.SignedPeerRecord) == 0 {
return nil, nil
}
env, _, err := record.ConsumeEnvelope(msg.SignedPeerRecord, peer.PeerRecordEnvelopeDomain)
+1 -1
View File
@@ -960,7 +960,7 @@ func waitForAddrInStream(t *testing.T, s <-chan ma.Multiaddr, expected ma.Multia
}
continue
case <-time.After(timeout):
t.Fatalf(failMsg)
t.Fatal(failMsg)
}
}
}
+1 -1
View File
@@ -14,6 +14,6 @@ func main() {
return
}
if err := cmdlib.RunClient(os.Args[1], os.Args[2]); err != nil {
log.Fatalf(err.Error())
log.Fatal(err.Error())
}
}
+1 -1
View File
@@ -14,6 +14,6 @@ func main() {
return
}
if err := cmdlib.RunServer(os.Args[1], nil); err != nil {
log.Fatalf(err.Error())
log.Fatal(err.Error())
}
}
@@ -0,0 +1,100 @@
// gotest2sql inserts the output of go test -json ./... into a sqlite database
package main
import (
"bufio"
"database/sql"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"time"
_ "github.com/glebarez/go-sqlite"
)
type TestEvent struct {
Time time.Time // encodes as an RFC3339-format string
Action string
Package string
Test string
Elapsed float64 // seconds
Output string
}
func main() {
outputPath := flag.String("output", "", "output db file")
verbose := flag.Bool("v", false, "Print test output to stdout")
flag.Parse()
if *outputPath == "" {
log.Fatal("-output path is required")
}
db, err := sql.Open("sqlite", *outputPath)
if err != nil {
log.Fatal(err)
}
// Create a table to store test results.
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS test_results (
Time TEXT,
Action TEXT,
Package TEXT,
Test TEXT,
Elapsed REAL,
Output TEXT,
BatchInsertTime TEXT
)`)
if err != nil {
log.Fatal(err)
}
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Prepare the insert statement once
insertTime := time.Now().Format(time.RFC3339Nano)
stmt, err := tx.Prepare(`
INSERT INTO test_results (Time, Action, Package, Test, Elapsed, Output, BatchInsertTime)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // Ensure the statement is closed after use
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
line := s.Bytes()
var ev TestEvent
err = json.Unmarshal(line, &ev)
if err != nil {
log.Fatal(err)
}
if *verbose && ev.Action == "output" {
fmt.Print(ev.Output)
}
_, err = stmt.Exec(
ev.Time.Format(time.RFC3339Nano),
ev.Action,
ev.Package,
ev.Test,
ev.Elapsed,
ev.Output,
insertTime,
)
if err != nil {
log.Fatal(err)
}
}
// Commit the transaction
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
}
+17
View File
@@ -0,0 +1,17 @@
module github.com/libp2p/go-libp2p/scripts/test_analysis
go 1.22.1
require github.com/glebarez/go-sqlite v1.22.0
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.15.0 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
)
+23
View File
@@ -0,0 +1,23 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+226
View File
@@ -0,0 +1,226 @@
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"os"
"os/exec"
"regexp"
"strings"
_ "github.com/glebarez/go-sqlite"
)
const dbPath = "./test_results.db"
const retryCount = 4 // For a total of 5 runs
var coverRegex = regexp.MustCompile(`-cover`)
func main() {
var t tester
if len(os.Args) >= 2 {
if os.Args[1] == "summarize" {
md, err := t.summarize()
if err != nil {
log.Fatal(err)
}
fmt.Print(md)
return
}
}
passThruFlags := os.Args[1:]
err := t.runTests(passThruFlags)
if err != nil {
log.Fatal(err)
}
}
type tester struct {
Dir string
}
func (t *tester) runTests(passThruFlags []string) error {
err := t.goTestAll(passThruFlags)
if err == nil {
// No failed tests, nothing to do
return nil
}
log.Printf("Not all tests passed: %v", err)
failedTests, err := t.findFailedTests(context.Background())
if err != nil {
return err
}
log.Printf("Found %d failed tests. Retrying them %d times", len(failedTests), retryCount)
hasOneNonFlakyFailure := false
loggedFlaky := map[string]struct{}{}
for _, ft := range failedTests {
isFlaky := false
for i := 0; i < retryCount; i++ {
log.Printf("Retrying %s.%s", ft.Package, ft.Test)
if err := t.goTestPkgTest(ft.Package, ft.Test, filterOutFlags(passThruFlags, coverRegex)); err != nil {
log.Printf("Failed to run %s.%s: %v", ft.Package, ft.Test, err)
} else {
isFlaky = true
flakyName := ft.Package + "." + ft.Test
if _, ok := loggedFlaky[flakyName]; !ok {
loggedFlaky[flakyName] = struct{}{}
log.Printf("Test %s.%s is flaky.", ft.Package, ft.Test)
}
}
}
if !isFlaky {
hasOneNonFlakyFailure = true
}
}
// A test consistently failed, so we should exit with a non-zero exit code.
if hasOneNonFlakyFailure {
return errors.New("one or more tests consistently failed")
}
return nil
}
func (t *tester) goTestAll(extraFlags []string) error {
flags := []string{"./..."}
flags = append(flags, extraFlags...)
return t.goTest(flags)
}
func (t *tester) goTestPkgTest(pkg, testname string, extraFlags []string) error {
flags := []string{
pkg, "-run", "^" + testname + "$", "-count", "1",
}
flags = append(flags, extraFlags...)
return t.goTest(flags)
}
func (t *tester) goTest(extraFlags []string) error {
flags := []string{
"test", "-json",
}
flags = append(flags, extraFlags...)
cmd := exec.Command("go", flags...)
cmd.Dir = t.Dir
cmd.Stderr = os.Stderr
gotest2sql := exec.Command("gotest2sql", "-v", "-output", dbPath)
gotest2sql.Dir = t.Dir
gotest2sql.Stdin, _ = cmd.StdoutPipe()
gotest2sql.Stdout = os.Stdout
gotest2sql.Stderr = os.Stderr
err := gotest2sql.Start()
if err != nil {
return err
}
err = cmd.Run()
return errors.Join(err, gotest2sql.Wait())
}
type failedTest struct {
Package string
Test string
}
func (t *tester) findFailedTests(ctx context.Context) ([]failedTest, error) {
db, err := sql.Open("sqlite", t.Dir+dbPath)
if err != nil {
return nil, err
}
defer db.Close()
rows, err := db.QueryContext(ctx, "SELECT DISTINCT Package, Test FROM test_results where Action='fail' and Test != ''")
if err != nil {
return nil, err
}
var out []failedTest
for rows.Next() {
var pkg, test string
if err := rows.Scan(&pkg, &test); err != nil {
return nil, err
}
out = append(out, failedTest{pkg, test})
}
return out, nil
}
func filterOutFlags(flags []string, exclude *regexp.Regexp) []string {
out := make([]string, 0, len(flags))
for _, f := range flags {
if !exclude.MatchString(f) {
out = append(out, f)
}
}
return out
}
// summarize returns a markdown string of the test results.
func (t *tester) summarize() (string, error) {
ctx := context.Background()
var out strings.Builder
testFailures, err := t.findFailedTests(ctx)
if err != nil {
return "", err
}
plural := "s"
if len(testFailures) == 1 {
plural = ""
}
out.WriteString(fmt.Sprintf("## %d Test Failure%s\n\n", len(testFailures), plural))
db, err := sql.Open("sqlite", t.Dir+dbPath)
if err != nil {
return "", err
}
defer db.Close()
rows, err := db.QueryContext(ctx, `SELECT
tr_output.Package,
tr_output.Test,
GROUP_CONCAT(tr_output.Output, "") AS Outputs
FROM
test_results tr_fail
JOIN
test_results tr_output
ON
tr_fail.Test = tr_output.Test
AND tr_fail.BatchInsertTime = tr_output.BatchInsertTime
AND tr_fail.Package = tr_output.Package
WHERE
tr_fail.Action = 'fail'
AND tr_output.Test != ''
GROUP BY
tr_output.BatchInsertTime,
tr_output.Package,
tr_output.Test
ORDER BY
MIN(tr_output.Time);`)
if err != nil {
return "", err
}
for rows.Next() {
var pkg, test, outputs string
if err := rows.Scan(&pkg, &test, &outputs); err != nil {
return "", err
}
_, err = out.WriteString(fmt.Sprintf(`<details>
<summary>%s.%s</summary>
<pre>
%s
</pre>
</details>`, pkg, test, outputs))
if err != nil {
return "", err
}
}
return out.String(), nil
}
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"os"
"testing"
)
func TestFailsOnConsistentFailure(t *testing.T) {
tmpDir := t.TempDir() + "/"
os.WriteFile(tmpDir+"/main.go", []byte(`package main
func main() {}`), 0644)
// Add a test that fails consistently.
os.WriteFile(tmpDir+"/main_test.go", []byte(`package main
import (
"testing"
)
func TestConsistentFailure(t *testing.T) {
t.Fatal("consistent failure")
}`), 0644)
os.WriteFile(tmpDir+"/go.mod", []byte(`module example.com/test`), 0644)
tstr := tester{Dir: tmpDir}
err := tstr.runTests(nil)
if err == nil {
t.Fatal("Should have failed with a consistent failure")
}
}
func TestPassesOnFlakyFailure(t *testing.T) {
tmpDir := t.TempDir() + "/"
os.WriteFile(tmpDir+"/main.go", []byte(`package main
func main() {
}`), 0644)
// Add a test that fails the first time.
os.WriteFile(tmpDir+"/main_test.go", []byte(`package main
import (
"os"
"testing"
)
func TestFlakyFailure(t *testing.T) {
_, err := os.Stat("foo")
if err != nil {
os.WriteFile("foo", []byte("hello"), 0644)
t.Fatal("flaky failure")
}
}`), 0644)
os.WriteFile(tmpDir+"/go.mod", []byte(`module example.com/test`), 0644)
// Run the test.
tstr := tester{Dir: tmpDir}
err := tstr.runTests(nil)
if err != nil {
t.Fatal("Should have passed with a flaky test")
}
}