github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fstest/test_all/run.go (about)

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