github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/fstest/test_all/run.go (about)

     1  // Run a test
     2  
     3  // +build go1.11
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/build"
    11  	"io"
    12  	"log"
    13  	"os"
    14  	"os/exec"
    15  	"path"
    16  	"regexp"
    17  	"runtime"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/rclone/rclone/fs"
    25  	"github.com/rclone/rclone/fstest/testserver"
    26  )
    27  
    28  // Control concurrency per backend if required
    29  var (
    30  	oneOnlyMu sync.Mutex
    31  	oneOnly   = map[string]*sync.Mutex{}
    32  )
    33  
    34  // Run holds info about a running test
    35  //
    36  // A run just runs one command line, but it can be run multiple times
    37  // if retries are needed.
    38  type Run struct {
    39  	// Config
    40  	Remote    string // name of the test remote
    41  	Backend   string // name of the backend
    42  	Path      string // path to the source directory
    43  	FastList  bool   // add -fast-list to tests
    44  	Short     bool   // add -short
    45  	NoRetries bool   // don't retry if set
    46  	OneOnly   bool   // only run test for this backend at once
    47  	NoBinary  bool   // set to not build a binary
    48  	SizeLimit int64  // maximum test file size
    49  	Ignore    map[string]struct{}
    50  	// Internals
    51  	cmdLine     []string
    52  	cmdString   string
    53  	try         int
    54  	err         error
    55  	output      []byte
    56  	failedTests []string
    57  	runFlag     string
    58  	logDir      string   // directory to place the logs
    59  	trialName   string   // name/log file name of current trial
    60  	trialNames  []string // list of all the trials
    61  }
    62  
    63  // Runs records multiple Run objects
    64  type Runs []*Run
    65  
    66  // Sort interface
    67  func (rs Runs) Len() int      { return len(rs) }
    68  func (rs Runs) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
    69  func (rs Runs) Less(i, j int) bool {
    70  	a, b := rs[i], rs[j]
    71  	if a.Backend < b.Backend {
    72  		return true
    73  	} else if a.Backend > b.Backend {
    74  		return false
    75  	}
    76  	if a.Remote < b.Remote {
    77  		return true
    78  	} else if a.Remote > b.Remote {
    79  		return false
    80  	}
    81  	if a.Path < b.Path {
    82  		return true
    83  	} else if a.Path > b.Path {
    84  		return false
    85  	}
    86  	if !a.FastList && b.FastList {
    87  		return true
    88  	} else if a.FastList && !b.FastList {
    89  		return false
    90  	}
    91  	return false
    92  }
    93  
    94  // dumpOutput prints the error output
    95  func (r *Run) dumpOutput() {
    96  	log.Println("------------------------------------------------------------")
    97  	log.Printf("---- %q ----", r.cmdString)
    98  	log.Println(string(r.output))
    99  	log.Println("------------------------------------------------------------")
   100  }
   101  
   102  // This converts a slice of test names into a regexp which matches
   103  // them.
   104  func testsToRegexp(tests []string) string {
   105  	var split []map[string]struct{}
   106  	// Make a slice with maps of the used parts at each level
   107  	for _, test := range tests {
   108  		for i, name := range strings.Split(test, "/") {
   109  			if i >= len(split) {
   110  				split = append(split, make(map[string]struct{}))
   111  			}
   112  			split[i][name] = struct{}{}
   113  		}
   114  	}
   115  	var out []string
   116  	for _, level := range split {
   117  		var testsInLevel = []string{}
   118  		for name := range level {
   119  			testsInLevel = append(testsInLevel, name)
   120  		}
   121  		sort.Strings(testsInLevel)
   122  		if len(testsInLevel) > 1 {
   123  			out = append(out, "^("+strings.Join(testsInLevel, "|")+")$")
   124  		} else {
   125  			out = append(out, "^"+testsInLevel[0]+"$")
   126  		}
   127  	}
   128  	return strings.Join(out, "/")
   129  }
   130  
   131  var failRe = regexp.MustCompile(`(?m)^\s*--- FAIL: (Test.*?) \(`)
   132  
   133  // findFailures looks for all the tests which failed
   134  func (r *Run) findFailures() {
   135  	oldFailedTests := r.failedTests
   136  	r.failedTests = nil
   137  	excludeParents := map[string]struct{}{}
   138  	ignored := 0
   139  	for _, matches := range failRe.FindAllSubmatch(r.output, -1) {
   140  		failedTest := string(matches[1])
   141  		// Skip any ignored failures
   142  		if _, found := r.Ignore[failedTest]; found {
   143  			ignored++
   144  		} else {
   145  			r.failedTests = append(r.failedTests, failedTest)
   146  		}
   147  		// Find all the parents of this test
   148  		parts := strings.Split(failedTest, "/")
   149  		for i := len(parts) - 1; i >= 1; i-- {
   150  			excludeParents[strings.Join(parts[:i], "/")] = struct{}{}
   151  		}
   152  	}
   153  	// Exclude the parents
   154  	var newTests = r.failedTests[:0]
   155  	for _, failedTest := range r.failedTests {
   156  		if _, excluded := excludeParents[failedTest]; !excluded {
   157  			newTests = append(newTests, failedTest)
   158  		}
   159  	}
   160  	r.failedTests = newTests
   161  	if len(r.failedTests) == 0 && ignored > 0 {
   162  		log.Printf("%q - Found %d ignored errors only - marking as good", r.cmdString, ignored)
   163  		r.err = nil
   164  		r.dumpOutput()
   165  		return
   166  	}
   167  	if len(r.failedTests) != 0 {
   168  		r.runFlag = testsToRegexp(r.failedTests)
   169  	} else {
   170  		r.runFlag = ""
   171  	}
   172  	if r.passed() && len(r.failedTests) != 0 {
   173  		log.Printf("%q - Expecting no errors but got: %v", r.cmdString, r.failedTests)
   174  		r.dumpOutput()
   175  	} else if !r.passed() && len(r.failedTests) == 0 {
   176  		log.Printf("%q - Expecting errors but got none: %v", r.cmdString, r.failedTests)
   177  		r.dumpOutput()
   178  		r.failedTests = oldFailedTests
   179  	}
   180  }
   181  
   182  // nextCmdLine returns the next command line
   183  func (r *Run) nextCmdLine() []string {
   184  	cmdLine := r.cmdLine
   185  	if r.runFlag != "" {
   186  		cmdLine = append(cmdLine, "-test.run", r.runFlag)
   187  	}
   188  	return cmdLine
   189  }
   190  
   191  // trial runs a single test
   192  func (r *Run) trial() {
   193  	cmdLine := r.nextCmdLine()
   194  	cmdString := toShell(cmdLine)
   195  	msg := fmt.Sprintf("%q - Starting (try %d/%d)", cmdString, r.try, *maxTries)
   196  	log.Println(msg)
   197  	logName := path.Join(r.logDir, r.trialName)
   198  	out, err := os.Create(logName)
   199  	if err != nil {
   200  		log.Fatalf("Couldn't create log file: %v", err)
   201  	}
   202  	defer func() {
   203  		err := out.Close()
   204  		if err != nil {
   205  			log.Fatalf("Failed to close log file: %v", err)
   206  		}
   207  	}()
   208  	_, _ = fmt.Fprintln(out, msg)
   209  
   210  	// Early exit if --try-run
   211  	if *dryRun {
   212  		log.Printf("Not executing as --dry-run: %v", cmdLine)
   213  		_, _ = fmt.Fprintln(out, "--dry-run is set - not running")
   214  		return
   215  	}
   216  
   217  	// Start the test server if required
   218  	finish, err := testserver.Start(r.Remote)
   219  	if err != nil {
   220  		log.Printf("%s: Failed to start test server: %v", r.Remote, err)
   221  		_, _ = fmt.Fprintf(out, "%s: Failed to start test server: %v\n", r.Remote, err)
   222  		r.err = err
   223  		return
   224  	}
   225  	defer finish()
   226  
   227  	// Internal buffer
   228  	var b bytes.Buffer
   229  	multiOut := io.MultiWriter(out, &b)
   230  
   231  	cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
   232  	cmd.Stderr = multiOut
   233  	cmd.Stdout = multiOut
   234  	cmd.Dir = r.Path
   235  	start := time.Now()
   236  	r.err = cmd.Run()
   237  	r.output = b.Bytes()
   238  	duration := time.Since(start)
   239  	r.findFailures()
   240  	if r.passed() {
   241  		msg = fmt.Sprintf("%q - Finished OK in %v (try %d/%d)", cmdString, duration, r.try, *maxTries)
   242  	} else {
   243  		msg = fmt.Sprintf("%q - Finished ERROR in %v (try %d/%d): %v: Failed %v", cmdString, duration, r.try, *maxTries, r.err, r.failedTests)
   244  	}
   245  	log.Println(msg)
   246  	_, _ = fmt.Fprintln(out, msg)
   247  }
   248  
   249  // passed returns true if the test passed
   250  func (r *Run) passed() bool {
   251  	return r.err == nil
   252  }
   253  
   254  // GOPATH returns the current GOPATH
   255  func GOPATH() string {
   256  	gopath := os.Getenv("GOPATH")
   257  	if gopath == "" {
   258  		gopath = build.Default.GOPATH
   259  	}
   260  	return gopath
   261  }
   262  
   263  // BinaryName turns a package name into a binary name
   264  func (r *Run) BinaryName() string {
   265  	binary := path.Base(r.Path) + ".test"
   266  	if runtime.GOOS == "windows" {
   267  		binary += ".exe"
   268  	}
   269  	return binary
   270  }
   271  
   272  // BinaryPath turns a package name into a binary path
   273  func (r *Run) BinaryPath() string {
   274  	return path.Join(r.Path, r.BinaryName())
   275  }
   276  
   277  // PackagePath returns the path to the package
   278  func (r *Run) PackagePath() string {
   279  	return path.Join(GOPATH(), "src", r.Path)
   280  }
   281  
   282  // MakeTestBinary makes the binary we will run
   283  func (r *Run) MakeTestBinary() {
   284  	binary := r.BinaryPath()
   285  	binaryName := r.BinaryName()
   286  	log.Printf("%s: Making test binary %q", r.Path, binaryName)
   287  	cmdLine := []string{"go", "test", "-c"}
   288  	if *dryRun {
   289  		log.Printf("Not executing: %v", cmdLine)
   290  		return
   291  	}
   292  	cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
   293  	cmd.Dir = r.Path
   294  	err := cmd.Run()
   295  	if err != nil {
   296  		log.Fatalf("Failed to make test binary: %v", err)
   297  	}
   298  	if _, err := os.Stat(binary); err != nil {
   299  		log.Fatalf("Couldn't find test binary %q", binary)
   300  	}
   301  }
   302  
   303  // RemoveTestBinary removes the binary made in makeTestBinary
   304  func (r *Run) RemoveTestBinary() {
   305  	if *dryRun {
   306  		return
   307  	}
   308  	binary := r.BinaryPath()
   309  	err := os.Remove(binary) // Delete the binary when finished
   310  	if err != nil {
   311  		log.Printf("Error removing test binary %q: %v", binary, err)
   312  	}
   313  }
   314  
   315  // Name returns the run name as a file name friendly string
   316  func (r *Run) Name() string {
   317  	ns := []string{
   318  		r.Backend,
   319  		strings.Replace(r.Path, "/", ".", -1),
   320  		r.Remote,
   321  	}
   322  	if r.FastList {
   323  		ns = append(ns, "fastlist")
   324  	}
   325  	ns = append(ns, fmt.Sprintf("%d", r.try))
   326  	s := strings.Join(ns, "-")
   327  	s = strings.Replace(s, ":", "", -1)
   328  	return s
   329  }
   330  
   331  // Init the Run
   332  func (r *Run) Init() {
   333  	prefix := "-test."
   334  	if r.NoBinary {
   335  		prefix = "-"
   336  		r.cmdLine = []string{"go", "test"}
   337  	} else {
   338  		r.cmdLine = []string{"./" + r.BinaryName()}
   339  	}
   340  	r.cmdLine = append(r.cmdLine, prefix+"v", prefix+"timeout", timeout.String(), "-remote", r.Remote)
   341  	r.try = 1
   342  	if *verbose {
   343  		r.cmdLine = append(r.cmdLine, "-verbose")
   344  		fs.Config.LogLevel = fs.LogLevelDebug
   345  	}
   346  	if *runOnly != "" {
   347  		r.cmdLine = append(r.cmdLine, prefix+"run", *runOnly)
   348  	}
   349  	if r.FastList {
   350  		r.cmdLine = append(r.cmdLine, "-fast-list")
   351  	}
   352  	if r.Short {
   353  		r.cmdLine = append(r.cmdLine, "-short")
   354  	}
   355  	if r.SizeLimit > 0 {
   356  		r.cmdLine = append(r.cmdLine, "-size-limit", strconv.FormatInt(r.SizeLimit, 10))
   357  	}
   358  	r.cmdString = toShell(r.cmdLine)
   359  }
   360  
   361  // Logs returns all the log names
   362  func (r *Run) Logs() []string {
   363  	return r.trialNames
   364  }
   365  
   366  // FailedTests returns the failed tests as a comma separated string, limiting the number
   367  func (r *Run) FailedTests() string {
   368  	const maxTests = 5
   369  	ts := r.failedTests
   370  	if len(ts) > maxTests {
   371  		ts = ts[:maxTests:maxTests]
   372  		ts = append(ts, fmt.Sprintf("… (%d more)", len(r.failedTests)-maxTests))
   373  	}
   374  	return strings.Join(ts, ", ")
   375  }
   376  
   377  // Run runs all the trials for this test
   378  func (r *Run) Run(logDir string, result chan<- *Run) {
   379  	if r.OneOnly {
   380  		oneOnlyMu.Lock()
   381  		mu := oneOnly[r.Backend]
   382  		if mu == nil {
   383  			mu = new(sync.Mutex)
   384  			oneOnly[r.Backend] = mu
   385  		}
   386  		oneOnlyMu.Unlock()
   387  		mu.Lock()
   388  		defer mu.Unlock()
   389  	}
   390  	r.Init()
   391  	r.logDir = logDir
   392  	for r.try = 1; r.try <= *maxTries; r.try++ {
   393  		r.trialName = r.Name() + ".txt"
   394  		r.trialNames = append(r.trialNames, r.trialName)
   395  		log.Printf("Starting run with log %q", r.trialName)
   396  		r.trial()
   397  		if r.passed() || r.NoRetries {
   398  			break
   399  		}
   400  	}
   401  	if !r.passed() {
   402  		r.dumpOutput()
   403  	}
   404  	result <- r
   405  }