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  }