package main import ( "encoding/json" "fmt" "html/template" "log" "os" "os/exec" "path" "regexp" "runtime" "sort" "time" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/lib/file" "github.com/skratchdot/open-golang/open" ) const timeFormat = "2006-01-02-150405" // Report holds the info to make a report on a series of test runs type Report struct { LogDir string // output directory for logs and report StartTime time.Time // time started DateTime string // directory name for output Duration time.Duration // time the run took Failed Runs // failed runs Passed Runs // passed runs Runs []ReportRun // runs to report Version string // rclone version Previous string // previous test name if known IndexHTML string // path to the index.html file URL string // online version Branch string // rclone branch Commit string // rclone commit GOOS string // Go OS GOARCH string // Go Arch GoVersion string // Go Version } // ReportRun is used in the templates to report on a test run type ReportRun struct { Name string Runs Runs } // Parse version numbers // v1.49.0 // v1.49.0-031-g2298834e-beta // v1.49.0-032-g20793a5f-sharefile-beta // match 1 is commit number // match 2 is branch name var parseVersion = regexp.MustCompile(`^v(?:[0-9.]+)-(?:\d+)-g([0-9a-f]+)(?:-(.*))?-beta$`) // FIXME take -issue or -pr parameter... // NewReport initialises and returns a Report func NewReport() *Report { r := &Report{ StartTime: time.Now(), Version: fs.Version, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, GoVersion: runtime.Version(), } r.DateTime = r.StartTime.Format(timeFormat) // Find previous log directory if possible names, err := os.ReadDir(*outputDir) if err == nil && len(names) > 0 { r.Previous = names[len(names)-1].Name() } // Create output directory for logs and report r.LogDir = path.Join(*outputDir, r.DateTime) err = file.MkdirAll(r.LogDir, 0777) if err != nil { log.Fatalf("Failed to make log directory: %v", err) } // Online version r.URL = *urlBase + r.DateTime + "/index.html" // Get branch/commit out of version parts := parseVersion.FindStringSubmatch(r.Version) if len(parts) >= 3 { r.Commit = parts[1] r.Branch = parts[2] } if r.Branch == "" { r.Branch = "master" } return r } // End should be called when the tests are complete func (r *Report) End() { r.Duration = time.Since(r.StartTime) sort.Sort(r.Failed) sort.Sort(r.Passed) r.Runs = []ReportRun{ {Name: "Failed", Runs: r.Failed}, {Name: "Passed", Runs: r.Passed}, } } // AllPassed returns true if there were no failed tests func (r *Report) AllPassed() bool { return len(r.Failed) == 0 } // RecordResult should be called with a Run when it has finished to be // recorded into the Report func (r *Report) RecordResult(t *Run) { if !t.passed() { r.Failed = append(r.Failed, t) } else { r.Passed = append(r.Passed, t) } } // Title returns a human-readable summary title for the Report func (r *Report) Title() string { if r.AllPassed() { return fmt.Sprintf("PASS: All tests finished OK in %v", r.Duration) } return fmt.Sprintf("FAIL: %d tests failed in %v", len(r.Failed), r.Duration) } // LogSummary writes the summary to the log file func (r *Report) LogSummary() { log.Printf("Logs in %q", r.LogDir) // Summarise results log.Printf("SUMMARY") log.Println(r.Title()) if !r.AllPassed() { for _, t := range r.Failed { log.Printf(" * %s", toShell(t.nextCmdLine())) log.Printf(" * Failed tests: %v", t.FailedTests) } } } // LogJSON writes the summary to index.json in LogDir func (r *Report) LogJSON() { out, err := json.MarshalIndent(r, "", "\t") if err != nil { log.Fatalf("Failed to marshal data for index.json: %v", err) } err = os.WriteFile(path.Join(r.LogDir, "index.json"), out, 0666) if err != nil { log.Fatalf("Failed to write index.json: %v", err) } } // LogHTML writes the summary to index.html in LogDir func (r *Report) LogHTML() { r.IndexHTML = path.Join(r.LogDir, "index.html") out, err := os.Create(r.IndexHTML) if err != nil { log.Fatalf("Failed to open index.html: %v", err) } defer func() { err := out.Close() if err != nil { log.Fatalf("Failed to close index.html: %v", err) } }() err = reportTemplate.Execute(out, r) if err != nil { log.Fatalf("Failed to execute template: %v", err) } _ = open.Start("file://" + r.IndexHTML) } var reportHTML = ` {{ .Title }}

{{ .Title }}

{{ if .Commit}}{{ end }} {{ if .Previous}}{{ end }}
Version{{ .Version }}
Test{{ .DateTime}}
Branch{{ .Branch }}
Commit{{ .Commit }}
Go{{ .GoVersion }} {{ .GOOS }}/{{ .GOARCH }}
Duration{{ .Duration }}
Previous{{ .Previous }}
UpOlder Tests
{{ range .Runs }} {{ if .Runs }}

{{ .Name }}: {{ len .Runs }}

{{ $prevBackend := "" }} {{ $prevRemote := "" }} {{ range .Runs}} {{ end }}
Backend Remote Test FastList Failed Logs
{{ if ne $prevBackend .Backend }}{{ .Backend }}{{ end }}{{ $prevBackend = .Backend }} {{ if ne $prevRemote .Remote }}{{ .Remote }}{{ end }}{{ $prevRemote = .Remote }} {{ .Path }} {{ .FastList }} {{ .FailedTestsCSV }} {{ range $i, $v := .Logs }}#{{ $i }} {{ end }}
{{ end }} {{ end }} ` var reportTemplate = template.Must(template.New("Report").Parse(reportHTML)) // EmailHTML sends the summary report to the email address supplied func (r *Report) EmailHTML() { if *emailReport == "" || r.IndexHTML == "" { return } log.Printf("Sending email summary to %q", *emailReport) cmdLine := []string{"mail", "-a", "Content-Type: text/html", *emailReport, "-s", "rclone integration tests: " + r.Title()} cmd := exec.Command(cmdLine[0], cmdLine[1:]...) in, err := os.Open(r.IndexHTML) if err != nil { log.Fatalf("Failed to open index.html: %v", err) } cmd.Stdin = in cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { log.Fatalf("Failed to send email: %v", err) } _ = in.Close() } // uploadTo uploads a copy of the report online to the dir given func (r *Report) uploadTo(uploadDir string) { dst := path.Join(*uploadPath, uploadDir) log.Printf("Uploading results to %q", dst) cmdLine := []string{"rclone", "sync", "--stats-log-level", "NOTICE", r.LogDir, dst} cmd := exec.Command(cmdLine[0], cmdLine[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { log.Fatalf("Failed to upload results: %v", err) } } // Upload uploads a copy of the report online func (r *Report) Upload() { if *uploadPath == "" || r.IndexHTML == "" { return } // Upload into dated directory r.uploadTo(r.DateTime) // And again into current r.uploadTo("current") }