github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/fstest/test_all/report.go (about) 1 // +build go1.11 2 3 package main 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "html/template" 9 "io/ioutil" 10 "log" 11 "os" 12 "os/exec" 13 "path" 14 "regexp" 15 "runtime" 16 "sort" 17 "time" 18 19 "github.com/rclone/rclone/fs" 20 "github.com/skratchdot/open-golang/open" 21 ) 22 23 const timeFormat = "2006-01-02-150405" 24 25 // Report holds the info to make a report on a series of test runs 26 type Report struct { 27 LogDir string // output directory for logs and report 28 StartTime time.Time // time started 29 DateTime string // directory name for output 30 Duration time.Duration // time the run took 31 Failed Runs // failed runs 32 Passed Runs // passed runs 33 Runs []ReportRun // runs to report 34 Version string // rclone version 35 Previous string // previous test name if known 36 IndexHTML string // path to the index.html file 37 URL string // online version 38 Branch string // rclone branch 39 Commit string // rclone commit 40 GOOS string // Go OS 41 GOARCH string // Go Arch 42 GoVersion string // Go Version 43 } 44 45 // ReportRun is used in the templates to report on a test run 46 type ReportRun struct { 47 Name string 48 Runs Runs 49 } 50 51 // Parse version numbers 52 // v1.49.0 53 // v1.49.0-031-g2298834e-beta 54 // v1.49.0-032-g20793a5f-sharefile-beta 55 // match 1 is commit number 56 // match 2 is branch name 57 var parseVersion = regexp.MustCompile(`^v(?:[0-9.]+)-(?:\d+)-g([0-9a-f]+)(?:-(.*))?-beta$`) 58 59 // FIXME take -issue or -pr parameter... 60 61 // NewReport initialises and returns a Report 62 func NewReport() *Report { 63 r := &Report{ 64 StartTime: time.Now(), 65 Version: fs.Version, 66 GOOS: runtime.GOOS, 67 GOARCH: runtime.GOARCH, 68 GoVersion: runtime.Version(), 69 } 70 r.DateTime = r.StartTime.Format(timeFormat) 71 72 // Find previous log directory if possible 73 names, err := ioutil.ReadDir(*outputDir) 74 if err == nil && len(names) > 0 { 75 r.Previous = names[len(names)-1].Name() 76 } 77 78 // Create output directory for logs and report 79 r.LogDir = path.Join(*outputDir, r.DateTime) 80 err = os.MkdirAll(r.LogDir, 0777) 81 if err != nil { 82 log.Fatalf("Failed to make log directory: %v", err) 83 } 84 85 // Online version 86 r.URL = *urlBase + r.DateTime + "/index.html" 87 88 // Get branch/commit out of version 89 parts := parseVersion.FindStringSubmatch(r.Version) 90 if len(parts) >= 3 { 91 r.Commit = parts[1] 92 r.Branch = parts[2] 93 } 94 if r.Branch == "" { 95 r.Branch = "master" 96 } 97 98 return r 99 } 100 101 // End should be called when the tests are complete 102 func (r *Report) End() { 103 r.Duration = time.Since(r.StartTime) 104 sort.Sort(r.Failed) 105 sort.Sort(r.Passed) 106 r.Runs = []ReportRun{ 107 {Name: "Failed", Runs: r.Failed}, 108 {Name: "Passed", Runs: r.Passed}, 109 } 110 } 111 112 // AllPassed returns true if there were no failed tests 113 func (r *Report) AllPassed() bool { 114 return len(r.Failed) == 0 115 } 116 117 // RecordResult should be called with a Run when it has finished to be 118 // recorded into the Report 119 func (r *Report) RecordResult(t *Run) { 120 if !t.passed() { 121 r.Failed = append(r.Failed, t) 122 } else { 123 r.Passed = append(r.Passed, t) 124 } 125 } 126 127 // Title returns a human readable summary title for the Report 128 func (r *Report) Title() string { 129 if r.AllPassed() { 130 return fmt.Sprintf("PASS: All tests finished OK in %v", r.Duration) 131 } 132 return fmt.Sprintf("FAIL: %d tests failed in %v", len(r.Failed), r.Duration) 133 } 134 135 // LogSummary writes the summary to the log file 136 func (r *Report) LogSummary() { 137 log.Printf("Logs in %q", r.LogDir) 138 139 // Summarise results 140 log.Printf("SUMMARY") 141 log.Println(r.Title()) 142 if !r.AllPassed() { 143 for _, t := range r.Failed { 144 log.Printf(" * %s", toShell(t.nextCmdLine())) 145 log.Printf(" * Failed tests: %v", t.failedTests) 146 } 147 } 148 } 149 150 // LogJSON writes the summary to index.json in LogDir 151 func (r *Report) LogJSON() { 152 out, err := json.MarshalIndent(r, "", "\t") 153 if err != nil { 154 log.Fatalf("Failed to marshal data for index.json: %v", err) 155 } 156 err = ioutil.WriteFile(path.Join(r.LogDir, "index.json"), out, 0666) 157 if err != nil { 158 log.Fatalf("Failed to write index.json: %v", err) 159 } 160 } 161 162 // LogHTML writes the summary to index.html in LogDir 163 func (r *Report) LogHTML() { 164 r.IndexHTML = path.Join(r.LogDir, "index.html") 165 out, err := os.Create(r.IndexHTML) 166 if err != nil { 167 log.Fatalf("Failed to open index.html: %v", err) 168 } 169 defer func() { 170 err := out.Close() 171 if err != nil { 172 log.Fatalf("Failed to close index.html: %v", err) 173 } 174 }() 175 err = reportTemplate.Execute(out, r) 176 if err != nil { 177 log.Fatalf("Failed to execute template: %v", err) 178 } 179 _ = open.Start("file://" + r.IndexHTML) 180 } 181 182 var reportHTML = `<!DOCTYPE html> 183 <html lang="en"> 184 <head> 185 <meta charset="utf-8"> 186 <title>{{ .Title }}</title> 187 <style> 188 table { 189 border-collapse: collapse; 190 border-spacing: 0; 191 border: 1px solid #ddd; 192 } 193 table.tests { 194 width: 100%; 195 } 196 table, th, td { 197 border: 1px solid #264653; 198 } 199 .Failed { 200 color: #BE5B43; 201 } 202 .Passed { 203 color: #17564E; 204 } 205 .false { 206 font-weight: lighter; 207 } 208 .true { 209 font-weight: bold; 210 } 211 212 th, td { 213 text-align: left; 214 padding: 4px; 215 } 216 217 tr:nth-child(even) { 218 background-color: #f2f2f2; 219 } 220 221 a { 222 color: #5B1955; 223 text-decoration: none; 224 } 225 a:hover, a:focus { 226 color: #F4A261; 227 text-decoration:underline; 228 } 229 a:focus { 230 outline: thin dotted; 231 outline: 5px auto; 232 } 233 </style> 234 </head> 235 <body> 236 <h1>{{ .Title }}</h1> 237 238 <table> 239 <tr><th>Version</th><td>{{ .Version }}</td></tr> 240 <tr><th>Test</th><td><a href="{{ .URL }}">{{ .DateTime}}</a></td></tr> 241 <tr><th>Branch</th><td><a href="https://github.com/rclone/rclone/tree/{{ .Branch }}">{{ .Branch }}</a></td></tr> 242 {{ if .Commit}}<tr><th>Commit</th><td><a href="https://github.com/rclone/rclone/commit/{{ .Commit }}">{{ .Commit }}</a></td></tr>{{ end }} 243 <tr><th>Go</th><td>{{ .GoVersion }} {{ .GOOS }}/{{ .GOARCH }}</td></tr> 244 <tr><th>Duration</th><td>{{ .Duration }}</td></tr> 245 {{ if .Previous}}<tr><th>Previous</th><td><a href="../{{ .Previous }}/index.html">{{ .Previous }}</a></td></tr>{{ end }} 246 <tr><th>Up</th><td><a href="../">Older Tests</a></td></tr> 247 </table> 248 249 {{ range .Runs }} 250 {{ if .Runs }} 251 <h2 class="{{ .Name }}">{{ .Name }}: {{ len .Runs }}</h2> 252 <table class="{{ .Name }} tests"> 253 <tr> 254 <th>Backend</th> 255 <th>Remote</th> 256 <th>Test</th> 257 <th>FastList</th> 258 <th>Failed</th> 259 <th>Logs</th> 260 </tr> 261 {{ $prevBackend := "" }} 262 {{ $prevRemote := "" }} 263 {{ range .Runs}} 264 <tr> 265 <td>{{ if ne $prevBackend .Backend }}{{ .Backend }}{{ end }}{{ $prevBackend = .Backend }}</td> 266 <td>{{ if ne $prevRemote .Remote }}{{ .Remote }}{{ end }}{{ $prevRemote = .Remote }}</td> 267 <td>{{ .Path }}</td> 268 <td><span class="{{ .FastList }}">{{ .FastList }}</span></td> 269 <td>{{ .FailedTests }}</td> 270 <td>{{ range $i, $v := .Logs }}<a href="{{ $v }}">#{{ $i }}</a> {{ end }}</td> 271 </tr> 272 {{ end }} 273 </table> 274 {{ end }} 275 {{ end }} 276 </body> 277 </html> 278 ` 279 280 var reportTemplate = template.Must(template.New("Report").Parse(reportHTML)) 281 282 // EmailHTML sends the summary report to the email address supplied 283 func (r *Report) EmailHTML() { 284 if *emailReport == "" || r.IndexHTML == "" { 285 return 286 } 287 log.Printf("Sending email summary to %q", *emailReport) 288 cmdLine := []string{"mail", "-a", "Content-Type: text/html", *emailReport, "-s", "rclone integration tests: " + r.Title()} 289 cmd := exec.Command(cmdLine[0], cmdLine[1:]...) 290 in, err := os.Open(r.IndexHTML) 291 if err != nil { 292 log.Fatalf("Failed to open index.html: %v", err) 293 } 294 cmd.Stdin = in 295 cmd.Stdout = os.Stdout 296 cmd.Stderr = os.Stderr 297 err = cmd.Run() 298 if err != nil { 299 log.Fatalf("Failed to send email: %v", err) 300 } 301 _ = in.Close() 302 } 303 304 // uploadTo uploads a copy of the report online to the dir given 305 func (r *Report) uploadTo(uploadDir string) { 306 dst := path.Join(*uploadPath, uploadDir) 307 log.Printf("Uploading results to %q", dst) 308 cmdLine := []string{"rclone", "sync", "--stats-log-level", "NOTICE", r.LogDir, dst} 309 cmd := exec.Command(cmdLine[0], cmdLine[1:]...) 310 cmd.Stdout = os.Stdout 311 cmd.Stderr = os.Stderr 312 err := cmd.Run() 313 if err != nil { 314 log.Fatalf("Failed to upload results: %v", err) 315 } 316 } 317 318 // Upload uploads a copy of the report online 319 func (r *Report) Upload() { 320 if *uploadPath == "" || r.IndexHTML == "" { 321 return 322 } 323 // Upload into dated directory 324 r.uploadTo(r.DateTime) 325 // And again into current 326 r.uploadTo("current") 327 }