github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/findflakes/html.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "html/template" 9 "io" 10 "log" 11 "reflect" 12 ) 13 14 // TODO: OS/Arch counts 15 16 const htmlReport = ` 17 <html> 18 <head> 19 <meta charset="utf-8" /> 20 <title>Top test failures</title> 21 <style> 22 body { 23 font-family: sans-serif; 24 color: #222; 25 } 26 a { 27 text-decoration: none; 28 } 29 table { 30 border-spacing: 0; 31 border-collapse: collapse; 32 } 33 table#failures { 34 width: 100%; 35 max-width: 100%; 36 } 37 table>caption { 38 padding-top: 8px; 39 padding-bottom: 8px; 40 color: #777; 41 text-align: left; 42 } 43 table>tbody>tr>td, table>tbody>tr>th, table>thead>tr>th { 44 padding: 8px; 45 vertical-align: top; 46 line-height: 1.4; 47 } 48 table.lined>tbody>tr:not(.expand)>td, table.lined>tbody>tr:not(.expand)>th { 49 border-top: 1px solid #ddd; 50 } 51 table.lined>thead>tr>th { 52 vertical-align: bottom; 53 border-bottom: 2px solid #ddd; 54 border-top: 0px; 55 } 56 th { 57 text-align: left; 58 } 59 table#failures>tbody>tr:not(.expand) { 60 cursor: pointer; 61 } 62 table#failures>tbody>tr:not(.expand):hover { 63 color: #337ab7; 64 } 65 td.pct, th.pct { 66 text-align: right; 67 } 68 td.plus { 69 color: #337ab7; 70 } 71 tr.expand { 72 display: none; 73 } 74 table>tbody>tr.expand>td { 75 padding-top: 0px; 76 } 77 a.hash { 78 font-family: monospace; 79 font-size: 120%; 80 } 81 .toggleRow { 82 display: none; 83 } 84 </style> 85 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> 86 </head> 87 <body> 88 <table id="failures" class="lined"> 89 <caption>Test failures as of {{(lastRev .).Date.Format "02 Jan 15:04 2006"}}, sorted by chance the failure is still happening. Click row for details and culprits.</caption> 90 <thead> 91 <tr><th></th><th class="pct">P(current)</th><th class="pct">P(failure)</th><th style="width:100%">Failure</th></tr> 92 </thead> 93 {{range $i, $class := .}} 94 {{$failuresByT := groupByT .Failures}} 95 <tr><td class="plus">+</td><td class="pct">{{pct .Current}}</td><td class="pct">{{pct .Latest.FailureProbability}}</td><td>{{.Class.String}}</td></tr> 96 <tr class="expand"><td></td><td colspan="3"> 97 <table> 98 <tr><th>Chance failure is still happening</th><td>{{pct .Current}}</td></tr> 99 {{with .Latest}} 100 <tr><th>Failure probability</th><td>{{pct .FailureProbability}} ({{.Failures}} of {{numCommits .}} commits)</td></tr> 101 {{if eq (numCommits .) 1}} 102 <tr><th>Observed</th><td>{{template "observation" (index $failuresByT .First)}}</td></tr> 103 {{else}} 104 <tr><th>First observed</th><td>{{template "observation" (index $failuresByT .First)}}</td></tr> 105 {{if ge (numCommits .) 2}} 106 <tr><th></th><td><a href="#" class="toggleRows">show other observations</a></td></tr> 107 {{range $_, $t := (slice .Times 1 -1)}} 108 <tr class="toggleRow"><th></th><td>{{template "observation" (index $failuresByT $t)}}</td></tr> 109 {{end}} 110 {{end}} 111 <tr><th>Last observed</th><td>{{template "observation" (index $failuresByT .Last)}}</td></tr> 112 <tr><th>Likely culprits</th> 113 <td style="padding:0px"> 114 <table> 115 {{range (.Culprits 0.9 10)}} 116 <tr><td class="pct">{{pct .P}}</td><td>{{template "revSubject" (index $class.Revs .T)}}</td></tr> 117 {{end}} 118 </table> 119 </td> 120 </tr> 121 {{end}}{{/* numCommits == 1*/}} 122 {{end}}{{/* with .Latest */}} 123 {{with (slice .Test.All 1 (len .Test.All))}} 124 <tr><th>{{len .}} past failure(s)</th><td><a href="#" class="toggleRows">show</a></td></tr> 125 {{range .}} 126 <tr class="toggleRow"><th></th><td>{{template "observation" (index $failuresByT .First)}} to {{template "observation" (index $failuresByT .Last)}}; {{pct .FailureProbability}} failure probability</td></tr> 127 {{end}} 128 {{else}} 129 <tr><th>No known past failures</th></tr> 130 {{end}} 131 </table> 132 </td></tr> 133 {{end}} 134 </table> 135 <script> 136 $("#failures").click(function(ev) { 137 var target = $(ev.target); 138 if (target.closest("table").filter("#failures").length === 0) 139 return; 140 141 ev.stopPropagation(); 142 var tr = target.closest("tr"); 143 144 if (!tr.hasClass("expand")) { 145 tr.next().toggle(); 146 } 147 }); 148 $("a.toggleRows").click(function(ev) { 149 ev.stopPropagation(); 150 $(ev.target).closest("tr").nextUntil(":not(.toggleRow)").toggle(); 151 var text = $(ev.target).text(); 152 text = text.replace(/show|hide/, function(x) { return x === "show" ? "hide" : "show"; }); 153 $(ev.target).text(text); 154 return false; 155 }); 156 </script> 157 </body> 158 </html> 159 160 {{/* observation expands a []*failure in to an observation line. */}} 161 {{define "observation"}} 162 {{$first := (index . 0)}} 163 {{template "revDate" $first.Rev}} ({{$first.CommitsAgo}} commits ago) on{{range .}} <a href="{{.Build.LogURL}}">{{.Build.Builder}}</a>{{end}} 164 {{end}} 165 {{/* revLink expands a *Revision to a link to that commit. */}} 166 {{define "revLink"}} 167 <a href="https://github.com/golang/go/commit/{{.Revision}}" class="hash">{{printf "%.7s" .Revision}}</a> 168 {{end}} 169 {{/* revDate expands a *Revision to the commit's hash and date. */}} 170 {{define "revDate"}} 171 {{template "revLink" .}} {{.Date.Format "02 Jan 15:04 2006"}} 172 {{end}} 173 {{/* revSubject expands a *Revision to the commit's hash and subject. */}} 174 {{define "revSubject"}} 175 {{template "revLink" .}} {{.Subject}} 176 {{end}} 177 ` 178 179 var htmlFuncs = template.FuncMap(map[string]interface{}{ 180 "pct": pct, 181 "lastRev": func(classes []*failureClass) *Revision { 182 // TODO: Ugh. It's lame that the same Revs is in every 183 // failureClass. 184 revs := classes[0].Revs 185 return revs[len(revs)-1] 186 }, 187 "numCommits": func(r FlakeRegion) int { 188 return r.Last - r.First + 1 189 }, 190 "groupByT": func(failures []*failure) map[int][]*failure { 191 out := make(map[int][]*failure) 192 if len(failures) == 0 { 193 return out 194 } 195 lastI, lastT := 0, failures[0].T 196 for i := 1; i < len(failures); i++ { 197 if failures[i].T != lastT { 198 out[lastT] = failures[lastI:i] 199 lastI, lastT = i, failures[i].T 200 } 201 } 202 out[lastT] = failures[lastI:] 203 return out 204 }, 205 "slice": func(v interface{}, start, end int) interface{} { 206 val := reflect.ValueOf(v) 207 if start < 0 { 208 start = val.Len() + start 209 } 210 if end < 0 { 211 end = val.Len() + end 212 } 213 return val.Slice(start, end).Interface() 214 }, 215 }) 216 217 var htmlTemplate = template.Must(template.New("report").Funcs(htmlFuncs).Parse(htmlReport)) 218 219 func printHTMLReport(w io.Writer, classes []*failureClass) { 220 err := htmlTemplate.Execute(w, classes) 221 if err != nil { 222 log.Fatal(err) 223 } 224 }