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