@ -23,7 +23,6 @@ import (
"time"
"time"
"golang.org/x/exp/maps"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/cmd/testwrapper/flakytest"
)
)
@ -34,6 +33,8 @@ type testAttempt struct {
outcome string // "pass", "fail", "skip"
outcome string // "pass", "fail", "skip"
logs bytes . Buffer
logs bytes . Buffer
isMarkedFlaky bool // set if the test is marked as flaky
isMarkedFlaky bool // set if the test is marked as flaky
pkgFinished bool
}
}
type testName struct {
type testName struct {
@ -60,7 +61,12 @@ type goTestOutput struct {
var debug = os . Getenv ( "TS_TESTWRAPPER_DEBUG" ) != ""
var debug = os . Getenv ( "TS_TESTWRAPPER_DEBUG" ) != ""
func runTests ( ctx context . Context , attempt int , pt * packageTests , otherArgs [ ] string ) [ ] * testAttempt {
// runTests runs the tests in pt and sends the results on ch. It sends a
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
// set to true.
// It calls close(ch) when it's done.
func runTests ( ctx context . Context , attempt int , pt * packageTests , otherArgs [ ] string , ch chan <- * testAttempt ) {
defer close ( ch )
args := [ ] string { "test" , "-json" , pt . pattern }
args := [ ] string { "test" , "-json" , pt . pattern }
args = append ( args , otherArgs ... )
args = append ( args , otherArgs ... )
if len ( pt . tests ) > 0 {
if len ( pt . tests ) > 0 {
@ -92,7 +98,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
jd := json . NewDecoder ( r )
jd := json . NewDecoder ( r )
resultMap := make ( map [ testName ] * testAttempt )
resultMap := make ( map [ testName ] * testAttempt )
var out [ ] * testAttempt
for {
for {
var goOutput goTestOutput
var goOutput goTestOutput
if err := jd . Decode ( & goOutput ) ; err != nil {
if err := jd . Decode ( & goOutput ) ; err != nil {
@ -102,6 +107,16 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
panic ( err )
panic ( err )
}
}
if goOutput . Test == "" {
if goOutput . Test == "" {
switch goOutput . Action {
case "fail" , "pass" , "skip" :
ch <- & testAttempt {
name : testName {
pkg : goOutput . Package ,
} ,
outcome : goOutput . Action ,
pkgFinished : true ,
}
}
continue
continue
}
}
name := testName {
name := testName {
@ -124,7 +139,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
}
}
case "skip" , "pass" , "fail" :
case "skip" , "pass" , "fail" :
resultMap [ name ] . outcome = goOutput . Action
resultMap [ name ] . outcome = goOutput . Action
out = append ( out , resultMap [ name ] )
ch <- resultMap [ name ]
case "output" :
case "output" :
if strings . TrimSpace ( goOutput . Output ) == flakytest . FlakyTestLogMessage {
if strings . TrimSpace ( goOutput . Output ) == flakytest . FlakyTestLogMessage {
resultMap [ name ] . isMarkedFlaky = true
resultMap [ name ] . isMarkedFlaky = true
@ -134,7 +149,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
}
}
}
}
<- done
<- done
return out
}
}
func main ( ) {
func main ( ) {
@ -186,13 +200,22 @@ func main() {
attempt : 1 ,
attempt : 1 ,
} ,
} ,
}
}
printPkgOutcome := func ( pkg , outcome string , attempt int ) {
printPkgStatus := func ( pkgName string , failed bool ) {
if outcome == "skip" {
if failed {
fmt . Printf ( "?\t%s [skipped/no tests] \n" , pkg )
fmt . Println ( "FAIL\t" , pkgName )
return
} else {
}
fmt . Println ( "ok\t" , pkgName )
if outcome == "pass" {
outcome = "ok"
}
if outcome == "fail" {
outcome = "FAIL"
}
if attempt > 1 {
fmt . Printf ( "%s\t%s [attempt=%d]\n" , outcome , pkg , attempt )
return
}
}
fmt . Printf ( "%s\t%s\n" , outcome , pkg )
}
}
for len ( toRun ) > 0 {
for len ( toRun ) > 0 {
@ -210,25 +233,12 @@ func main() {
failed := false
failed := false
toRetry := make ( map [ string ] [ ] string ) // pkg -> tests to retry
toRetry := make ( map [ string ] [ ] string ) // pkg -> tests to retry
for _ , pt := range thisRun . tests {
for _ , pt := range thisRun . tests {
output := runTests ( ctx , thisRun . attempt , pt , otherArgs )
ch := make ( chan * testAttempt )
slices . SortFunc ( output , func ( i , j * testAttempt ) bool {
go runTests ( ctx , thisRun . attempt , pt , otherArgs , ch )
if c := strings . Compare ( i . name . pkg , j . name . pkg ) ; c < 0 {
for tr := range ch {
return true
if tr . pkgFinished {
} else if c > 0 {
printPkgOutcome ( tr . name . pkg , tr . outcome , thisRun . attempt )
return false
continue
}
return strings . Compare ( i . name . name , j . name . name ) <= 0
} )
lastPkg := ""
lastPkgFailed := false
for _ , tr := range output {
if lastPkg == "" {
lastPkg = tr . name . pkg
} else if lastPkg != tr . name . pkg {
printPkgStatus ( lastPkg , lastPkgFailed )
lastPkg = tr . name . pkg
lastPkgFailed = false
}
}
if * v || tr . outcome == "fail" {
if * v || tr . outcome == "fail" {
io . Copy ( os . Stdout , & tr . logs )
io . Copy ( os . Stdout , & tr . logs )
@ -236,14 +246,12 @@ func main() {
if tr . outcome != "fail" {
if tr . outcome != "fail" {
continue
continue
}
}
lastPkgFailed = true
if tr . isMarkedFlaky {
if tr . isMarkedFlaky {
toRetry [ tr . name . pkg ] = append ( toRetry [ tr . name . pkg ] , tr . name . name )
toRetry [ tr . name . pkg ] = append ( toRetry [ tr . name . pkg ] , tr . name . name )
} else {
} else {
failed = true
failed = true
}
}
}
}
printPkgStatus ( lastPkg , lastPkgFailed )
}
}
if failed {
if failed {
fmt . Println ( "\n\nNot retrying flaky tests because non-flaky tests failed." )
fmt . Println ( "\n\nNot retrying flaky tests because non-flaky tests failed." )