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  }