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