|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | +"bufio" |
| 5 | +"encoding/json" |
| 6 | +"errors" |
| 7 | +"fmt" |
| 8 | +"io" |
| 9 | +"os" |
| 10 | +"strings" |
| 11 | +"time" |
| 12 | + |
| 13 | +"golang.org/x/exp/slices" |
| 14 | +"golang.org/x/xerrors" |
| 15 | +) |
| 16 | + |
| 17 | +funcmain() { |
| 18 | +iflen(os.Args)!=2 { |
| 19 | +_,_=fmt.Println("usage: ci-report <gotests.json>") |
| 20 | +os.Exit(1) |
| 21 | +} |
| 22 | +name:=os.Args[1] |
| 23 | + |
| 24 | +goTests,err:=parseGoTestJSON(name) |
| 25 | +iferr!=nil { |
| 26 | +_,_=fmt.Printf("error parsing gotestsum report: %v",err) |
| 27 | +os.Exit(1) |
| 28 | +} |
| 29 | + |
| 30 | +rep,err:=parseCIReport(goTests) |
| 31 | +iferr!=nil { |
| 32 | +_,_=fmt.Printf("error parsing ci report: %v",err) |
| 33 | +os.Exit(1) |
| 34 | +} |
| 35 | + |
| 36 | +err=printCIReport(os.Stdout,rep) |
| 37 | +iferr!=nil { |
| 38 | +_,_=fmt.Printf("error printing report: %v",err) |
| 39 | +os.Exit(1) |
| 40 | +} |
| 41 | +} |
| 42 | + |
| 43 | +funcparseGoTestJSON(namestring) (GotestsumReport,error) { |
| 44 | +f,err:=os.Open(name) |
| 45 | +iferr!=nil { |
| 46 | +returnGotestsumReport{},xerrors.Errorf("error opening gotestsum json file: %w",err) |
| 47 | +} |
| 48 | +deferf.Close() |
| 49 | + |
| 50 | +dec:=json.NewDecoder(f) |
| 51 | +varreportGotestsumReport |
| 52 | +for { |
| 53 | +vareGotestsumReportEntry |
| 54 | +err=dec.Decode(&e) |
| 55 | +iferrors.Is(err,io.EOF) { |
| 56 | +break |
| 57 | +} |
| 58 | +iferr!=nil { |
| 59 | +returnGotestsumReport{},xerrors.Errorf("error decoding json: %w",err) |
| 60 | +} |
| 61 | +e.Package=strings.TrimPrefix(e.Package,"github.com/coder/coder/") |
| 62 | +report=append(report,e) |
| 63 | +} |
| 64 | + |
| 65 | +returnreport,nil |
| 66 | +} |
| 67 | + |
| 68 | +funcparseCIReport(reportGotestsumReport) (CIReport,error) { |
| 69 | +packagesSortedByName:= []string{} |
| 70 | +packageTimes:=map[string]float64{} |
| 71 | +packageFail:=map[string]int{} |
| 72 | +packageSkip:=map[string]bool{} |
| 73 | +testTimes:=map[string]float64{} |
| 74 | +testSkip:=map[string]bool{} |
| 75 | +testOutput:=map[string]string{} |
| 76 | +testSortedByName:= []string{} |
| 77 | +timeouts:=map[string]string{} |
| 78 | +timeoutRunningTests:=map[string]bool{} |
| 79 | +fori,e:=rangereport { |
| 80 | +switche.Action { |
| 81 | +// A package/test may fail or pass. |
| 82 | +caseFail: |
| 83 | +ife.Test=="" { |
| 84 | +packageTimes[e.Package]=*e.Elapsed |
| 85 | +}else { |
| 86 | +packageFail[e.Package]++ |
| 87 | +name:=e.Package+"."+e.Test |
| 88 | +testTimes[name]=*e.Elapsed |
| 89 | +} |
| 90 | +casePass: |
| 91 | +ife.Test=="" { |
| 92 | +packageTimes[e.Package]=*e.Elapsed |
| 93 | +}else { |
| 94 | +name:=e.Package+"."+e.Test |
| 95 | +delete(testOutput,name) |
| 96 | +testTimes[name]=*e.Elapsed |
| 97 | +} |
| 98 | + |
| 99 | +// Gather all output (deleted when irrelevant). |
| 100 | +caseOutput: |
| 101 | +name:=e.Package+"."+e.Test// May be pkg.Test or pkg. |
| 102 | +if_,ok:=timeouts[name];ok||strings.HasPrefix(e.Output,"panic: test timed out") { |
| 103 | +timeouts[name]+=e.Output |
| 104 | +continue |
| 105 | +} |
| 106 | +ife.Test!="" { |
| 107 | +name:=e.Package+"."+e.Test |
| 108 | +testOutput[name]+=e.Output |
| 109 | +} |
| 110 | + |
| 111 | +// Packages start, tests run and either may be skipped. |
| 112 | +caseStart: |
| 113 | +packagesSortedByName=append(packagesSortedByName,e.Package) |
| 114 | +caseRun: |
| 115 | +name:=e.Package+"."+e.Test |
| 116 | +testSortedByName=append(testSortedByName,name) |
| 117 | +caseSkip: |
| 118 | +ife.Test=="" { |
| 119 | +packageSkip[e.Package]=true |
| 120 | +}else { |
| 121 | +name:=e.Package+"."+e.Test |
| 122 | +testSkip[name]=true |
| 123 | +delete(testOutput,name) |
| 124 | +} |
| 125 | + |
| 126 | +// Ignore. |
| 127 | +caseCont: |
| 128 | +casePause: |
| 129 | + |
| 130 | +default: |
| 131 | +returnCIReport{},xerrors.Errorf("unknown action: %v in entry %d (%v)",e.Action,i,e) |
| 132 | +} |
| 133 | +} |
| 134 | + |
| 135 | +// Normalize timeout from "pkg." or "pkg.Test" to "pkg". |
| 136 | +timeoutsNorm:=make(map[string]string) |
| 137 | +fork,v:=rangetimeouts { |
| 138 | +names:=strings.SplitN(k,".",2) |
| 139 | +pkg:=names[0] |
| 140 | +if_,ok:=timeoutsNorm[pkg];ok { |
| 141 | +panic("multiple timeouts for package: "+pkg) |
| 142 | +} |
| 143 | +timeoutsNorm[pkg]=v |
| 144 | + |
| 145 | +// Mark all running tests as timed out. |
| 146 | +// panic: test timed out after 2s\nrunning tests:\n\tTestAgent_Session_TTY_Hushlogin (0s)\n\n ... |
| 147 | +parts:=strings.SplitN(v,"\n",3) |
| 148 | +iflen(parts)==3&&strings.HasPrefix(parts[1],"running tests:") { |
| 149 | +s:=bufio.NewScanner(strings.NewReader(parts[2])) |
| 150 | +fors.Scan() { |
| 151 | +name:=s.Text() |
| 152 | +if!strings.HasPrefix(name,"\tTest") { |
| 153 | +break |
| 154 | +} |
| 155 | +name=strings.TrimPrefix(name,"\t") |
| 156 | +name=strings.SplitN(name," ",2)[0] |
| 157 | +timeoutRunningTests[pkg+"."+name]=true |
| 158 | +packageFail[pkg]++ |
| 159 | +} |
| 160 | +} |
| 161 | +} |
| 162 | +timeouts=timeoutsNorm |
| 163 | + |
| 164 | +sortAZ:=func(a,bstring)bool {returna<b } |
| 165 | +slices.SortFunc(packagesSortedByName,sortAZ) |
| 166 | +slices.SortFunc(testSortedByName,sortAZ) |
| 167 | + |
| 168 | +varrepCIReport |
| 169 | + |
| 170 | +for_,pkg:=rangepackagesSortedByName { |
| 171 | +output,timeout:=timeouts[pkg] |
| 172 | +rep.Packages=append(rep.Packages,PackageReport{ |
| 173 | +Name:pkg, |
| 174 | +Time:packageTimes[pkg], |
| 175 | +Skip:packageSkip[pkg], |
| 176 | +Fail:packageFail[pkg]>0, |
| 177 | +Timeout:timeout, |
| 178 | +Output:output, |
| 179 | +NumFailed:packageFail[pkg], |
| 180 | +}) |
| 181 | +} |
| 182 | + |
| 183 | +for_,test:=rangetestSortedByName { |
| 184 | +names:=strings.SplitN(test,".",2) |
| 185 | +skip:=testSkip[test] |
| 186 | +out,fail:=testOutput[test] |
| 187 | +rep.Tests=append(rep.Tests,TestReport{ |
| 188 | +Package:names[0], |
| 189 | +Name:names[1], |
| 190 | +Time:testTimes[test], |
| 191 | +Skip:skip, |
| 192 | +Fail:fail, |
| 193 | +Timeout:timeoutRunningTests[test], |
| 194 | +Output:out, |
| 195 | +}) |
| 196 | +} |
| 197 | + |
| 198 | +returnrep,nil |
| 199 | +} |
| 200 | + |
| 201 | +funcprintCIReport(dst io.Writer,repCIReport)error { |
| 202 | +enc:=json.NewEncoder(dst) |
| 203 | +enc.SetIndent(""," ") |
| 204 | +err:=enc.Encode(rep) |
| 205 | +iferr!=nil { |
| 206 | +returnxerrors.Errorf("error encoding json: %w",err) |
| 207 | +} |
| 208 | +returnnil |
| 209 | +} |
| 210 | + |
| 211 | +typeCIReportstruct { |
| 212 | +Packages []PackageReport`json:"packages"` |
| 213 | +Tests []TestReport`json:"tests"` |
| 214 | +} |
| 215 | + |
| 216 | +typePackageReportstruct { |
| 217 | +Namestring`json:"name"` |
| 218 | +Timefloat64`json:"time"` |
| 219 | +Skipbool`json:"skip,omitempty"` |
| 220 | +Failbool`json:"fail,omitempty"` |
| 221 | +NumFailedint`json:"num_failed,omitempty"` |
| 222 | +Timeoutbool`json:"timeout,omitempty"` |
| 223 | +Outputstring`json:"output,omitempty"`// Output present e.g. for timeout. |
| 224 | +} |
| 225 | + |
| 226 | +typeTestReportstruct { |
| 227 | +Packagestring`json:"package"` |
| 228 | +Namestring`json:"name"` |
| 229 | +Timefloat64`json:"time"` |
| 230 | +Skipbool`json:"skip,omitempty"` |
| 231 | +Failbool`json:"fail,omitempty"` |
| 232 | +Timeoutbool`json:"timeout,omitempty"` |
| 233 | +Outputstring`json:"output,omitempty"` |
| 234 | +} |
| 235 | + |
| 236 | +typeGotestsumReport []GotestsumReportEntry |
| 237 | + |
| 238 | +typeGotestsumReportEntrystruct { |
| 239 | +Time time.Time`json:"Time"` |
| 240 | +ActionAction`json:"Action"` |
| 241 | +Packagestring`json:"Package"` |
| 242 | +Teststring`json:"Test,omitempty"` |
| 243 | +Outputstring`json:"Output,omitempty"` |
| 244 | +Elapsed*float64`json:"Elapsed,omitempty"` |
| 245 | +} |
| 246 | + |
| 247 | +typeActionstring |
| 248 | + |
| 249 | +const ( |
| 250 | +ContAction="cont" |
| 251 | +FailAction="fail" |
| 252 | +OutputAction="output" |
| 253 | +PassAction="pass" |
| 254 | +PauseAction="pause" |
| 255 | +RunAction="run" |
| 256 | +SkipAction="skip" |
| 257 | +StartAction="start" |
| 258 | +) |