github.com/coreos/mantle@v0.13.0/harness/suite.go (about)

     1  // Copyright 2017 CoreOS, Inc.
     2  // Copyright 2009 The Go Authors.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package harness
    17  
    18  import (
    19  	"errors"
    20  	"flag"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  	"runtime"
    26  	"runtime/debug"
    27  	"runtime/pprof"
    28  	"runtime/trace"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/coreos/mantle/harness/reporters"
    34  	"github.com/coreos/mantle/harness/testresult"
    35  )
    36  
    37  const (
    38  	defaultOutputDir = "_harness_temp"
    39  )
    40  
    41  var (
    42  	SuiteEmpty  = errors.New("harness: no tests to run")
    43  	SuiteFailed = errors.New("harness: test suite failed")
    44  )
    45  
    46  // Options
    47  type Options struct {
    48  	// The temporary directory in which to write profile files, logs, etc.
    49  	OutputDir string
    50  
    51  	// Report as tests are run; default is silent for success.
    52  	Verbose bool
    53  
    54  	// Run only tests matching a regexp.
    55  	Match string
    56  
    57  	// Enable memory profiling.
    58  	MemProfile     bool
    59  	MemProfileRate int
    60  
    61  	// Enable CPU profiling.
    62  	CpuProfile bool
    63  
    64  	// Enable goroutine block profiling.
    65  	BlockProfile     bool
    66  	BlockProfileRate int
    67  
    68  	// Enable execution trace.
    69  	ExecutionTrace bool
    70  
    71  	// Panic Suite execution after a timeout (0 means unlimited).
    72  	Timeout time.Duration
    73  
    74  	// Limit number of tests to run in parallel (0 means GOMAXPROCS).
    75  	Parallel int
    76  
    77  	Reporters reporters.Reporters
    78  }
    79  
    80  // FlagSet can be used to setup options via command line flags.
    81  // An optional prefix can be prepended to each flag.
    82  // Defaults can be specified prior to calling FlagSet.
    83  func (o *Options) FlagSet(prefix string, errorHandling flag.ErrorHandling) *flag.FlagSet {
    84  	o.init()
    85  	name := strings.Trim(prefix, ".-")
    86  	f := flag.NewFlagSet(name, errorHandling)
    87  	f.StringVar(&o.OutputDir, prefix+"outputdir", o.OutputDir,
    88  		"write profiles, logs, and other data to temporary `dir`")
    89  	f.BoolVar(&o.Verbose, prefix+"v", o.Verbose,
    90  		"verbose: print additional output")
    91  	f.StringVar(&o.Match, prefix+"run", o.Match,
    92  		"run only tests matching `regexp`")
    93  	f.BoolVar(&o.MemProfile, prefix+"memprofile", o.MemProfile,
    94  		"write a memory profile to 'dir/mem.prof'")
    95  	f.IntVar(&o.MemProfileRate, prefix+"memprofilerate", o.MemProfileRate,
    96  		"set memory profiling `rate` (see runtime.MemProfileRate)")
    97  	f.BoolVar(&o.CpuProfile, prefix+"cpuprofile", o.CpuProfile,
    98  		"write a cpu profile to 'dir/cpu.prof'")
    99  	f.BoolVar(&o.BlockProfile, prefix+"blockprofile", o.BlockProfile,
   100  		"write a goroutine blocking profile to 'dir/block.prof'")
   101  	f.IntVar(&o.BlockProfileRate, prefix+"blockprofilerate", o.BlockProfileRate,
   102  		"set blocking profile `rate` (see runtime.SetBlockProfileRate)")
   103  	f.BoolVar(&o.ExecutionTrace, prefix+"trace", o.ExecutionTrace,
   104  		"write an execution trace to 'dir/exec.trace'")
   105  	f.DurationVar(&o.Timeout, prefix+"timeout", o.Timeout,
   106  		"fail test binary execution after duration `d` (0 means unlimited)")
   107  	f.IntVar(&o.Parallel, prefix+"parallel", o.Parallel,
   108  		"run at most `n` tests in parallel")
   109  	return f
   110  }
   111  
   112  // init fills in any default values that shouldn't be the zero value.
   113  func (o *Options) init() {
   114  	if o.OutputDir == "" {
   115  		o.OutputDir = defaultOutputDir
   116  	}
   117  	if o.MemProfileRate < 1 {
   118  		o.MemProfileRate = runtime.MemProfileRate
   119  	}
   120  	if o.BlockProfileRate < 1 {
   121  		o.BlockProfileRate = 1
   122  	}
   123  	if o.Parallel < 1 {
   124  		o.Parallel = runtime.GOMAXPROCS(0)
   125  	}
   126  }
   127  
   128  // Suite is a type passed to a TestMain function to run the actual tests.
   129  // Suite manages the execution of a set of test functions.
   130  type Suite struct {
   131  	opts  Options
   132  	tests Tests
   133  	match *matcher
   134  
   135  	// mu protects the following fields which are used to manage
   136  	// parallel test execution.
   137  	mu sync.Mutex
   138  
   139  	// Channel used to signal tests that are ready to be run in parallel.
   140  	startParallel chan bool
   141  
   142  	// running is the number of tests currently running in parallel.
   143  	// This does not include tests that are waiting for subtests to complete.
   144  	running int
   145  
   146  	// waiting is the number tests waiting to be run in parallel.
   147  	waiting int
   148  }
   149  
   150  func (c *Suite) waitParallel() {
   151  	c.mu.Lock()
   152  	if c.running < c.opts.Parallel {
   153  		c.running++
   154  		c.mu.Unlock()
   155  		return
   156  	}
   157  	c.waiting++
   158  	c.mu.Unlock()
   159  	<-c.startParallel
   160  }
   161  
   162  func (c *Suite) release() {
   163  	c.mu.Lock()
   164  	if c.waiting == 0 {
   165  		c.running--
   166  		c.mu.Unlock()
   167  		return
   168  	}
   169  	c.waiting--
   170  	c.mu.Unlock()
   171  	c.startParallel <- true // Pick a waiting test to be run.
   172  }
   173  
   174  // NewSuite creates a new test suite.
   175  // All parameters in Options cannot be modified once given to Suite.
   176  func NewSuite(opts Options, tests Tests) *Suite {
   177  	opts.init()
   178  	return &Suite{
   179  		opts:          opts,
   180  		tests:         tests,
   181  		match:         newMatcher(opts.Match, "Match"),
   182  		startParallel: make(chan bool),
   183  	}
   184  }
   185  
   186  // Run runs the tests. Returns SuiteFailed for any test failure.
   187  func (s *Suite) Run() (err error) {
   188  	flushProfile := func(name string, f *os.File) {
   189  		err2 := pprof.Lookup(name).WriteTo(f, 0)
   190  		if err == nil && err2 != nil {
   191  			err = fmt.Errorf("harness: can't write %s profile: %v", name, err2)
   192  		}
   193  		f.Close()
   194  	}
   195  
   196  	outputDir, err := CleanOutputDir(s.opts.OutputDir)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	s.opts.OutputDir = outputDir
   201  
   202  	tap, err := os.Create(s.outputPath("test.tap"))
   203  	if err != nil {
   204  		return err
   205  	}
   206  	defer tap.Close()
   207  	if _, err := fmt.Fprintf(tap, "1..%d\n", len(s.tests)); err != nil {
   208  		return err
   209  	}
   210  
   211  	reportDir := s.outputPath("reports")
   212  	if err := os.Mkdir(reportDir, 0777); err != nil {
   213  		return err
   214  	}
   215  	defer func() {
   216  		if reportErr := s.opts.Reporters.Output(reportDir); reportErr != nil && err != nil {
   217  			err = reportErr
   218  		}
   219  	}()
   220  
   221  	if s.opts.MemProfile {
   222  		runtime.MemProfileRate = s.opts.MemProfileRate
   223  		f, err := os.Create(s.outputPath("mem.prof"))
   224  		if err != nil {
   225  			return err
   226  		}
   227  		defer func() {
   228  			runtime.GC() // materialize all statistics
   229  			flushProfile("heap", f)
   230  		}()
   231  	}
   232  	if s.opts.BlockProfile {
   233  		f, err := os.Create(s.outputPath("block.prof"))
   234  		if err != nil {
   235  			return err
   236  		}
   237  		runtime.SetBlockProfileRate(s.opts.BlockProfileRate)
   238  		defer func() {
   239  			runtime.SetBlockProfileRate(0) // stop profile
   240  			flushProfile("block", f)
   241  		}()
   242  	}
   243  	if s.opts.CpuProfile {
   244  		f, err := os.Create(s.outputPath("cpu.prof"))
   245  		if err != nil {
   246  			return err
   247  		}
   248  		defer f.Close()
   249  		if err := pprof.StartCPUProfile(f); err != nil {
   250  			return fmt.Errorf("harness: can't start cpu profile: %v", err)
   251  		}
   252  		defer pprof.StopCPUProfile() // flushes profile to disk
   253  	}
   254  	if s.opts.ExecutionTrace {
   255  		f, err := os.Create(s.outputPath("exec.trace"))
   256  		if err != nil {
   257  			return err
   258  		}
   259  		defer f.Close()
   260  		if err := trace.Start(f); err != nil {
   261  			return fmt.Errorf("harness: can't start tacing: %v", err)
   262  		}
   263  		defer trace.Stop() // flushes trace to disk
   264  	}
   265  	if s.opts.Timeout > 0 {
   266  		timer := time.AfterFunc(s.opts.Timeout, func() {
   267  			debug.SetTraceback("all")
   268  			panic(fmt.Sprintf("harness: tests timed out after %v", s.opts.Timeout))
   269  		})
   270  		defer timer.Stop()
   271  	}
   272  
   273  	return s.runTests(os.Stdout, tap)
   274  }
   275  
   276  func (s *Suite) runTests(out, tap io.Writer) error {
   277  	s.running = 1 // Set the count to 1 for the main (sequential) test.
   278  	t := &H{
   279  		signal:    make(chan bool),
   280  		barrier:   make(chan bool),
   281  		w:         out,
   282  		tap:       tap,
   283  		suite:     s,
   284  		reporters: s.opts.Reporters,
   285  	}
   286  	tRunner(t, func(t *H) {
   287  		for name, test := range s.tests {
   288  			t.Run(name, test)
   289  		}
   290  		// Run catching the signal rather than the tRunner as a separate
   291  		// goroutine to avoid adding a goroutine during the sequential
   292  		// phase as this pollutes the stacktrace output when aborting.
   293  		go func() { <-t.signal }()
   294  	})
   295  	if !t.ran {
   296  		return SuiteEmpty
   297  	}
   298  	if t.Failed() {
   299  		s.opts.Reporters.SetResult(testresult.Fail)
   300  		return SuiteFailed
   301  	}
   302  
   303  	s.opts.Reporters.SetResult(testresult.Pass)
   304  
   305  	return nil
   306  }
   307  
   308  // outputPath returns the file name under Options.OutputDir.
   309  func (s *Suite) outputPath(path string) string {
   310  	return filepath.Join(s.opts.OutputDir, path)
   311  }