github.com/lonnblad/godog@v0.7.14-0.20200306004719-1b0cb3259847/run.go (about)

     1  package godog
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/lonnblad/godog/colors"
    14  )
    15  
    16  const (
    17  	exitSuccess int = iota
    18  	exitFailure
    19  	exitOptionError
    20  )
    21  
    22  type initializer func(*Suite)
    23  
    24  type runner struct {
    25  	randomSeed            int64
    26  	stopOnFailure, strict bool
    27  	features              []*feature
    28  	fmt                   Formatter
    29  	initializer           initializer
    30  }
    31  
    32  func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool) {
    33  	var useFmtCopy bool
    34  	var copyLock sync.Mutex
    35  
    36  	// special mode for concurrent-formatter
    37  	if _, ok := r.fmt.(ConcurrentFormatter); ok {
    38  		useFmtCopy = true
    39  	}
    40  
    41  	queue := make(chan int, rate)
    42  	for i, ft := range r.features {
    43  		queue <- i // reserve space in queue
    44  		ft := *ft
    45  
    46  		go func(fail *bool, feat *feature) {
    47  			var fmtCopy Formatter
    48  			defer func() {
    49  				<-queue // free a space in queue
    50  			}()
    51  			if r.stopOnFailure && *fail {
    52  				return
    53  			}
    54  			suite := &Suite{
    55  				fmt:           r.fmt,
    56  				randomSeed:    r.randomSeed,
    57  				stopOnFailure: r.stopOnFailure,
    58  				strict:        r.strict,
    59  				features:      []*feature{feat},
    60  			}
    61  			if useFmtCopy {
    62  				fmtCopy = formatterFn()
    63  				suite.fmt = fmtCopy
    64  
    65  				concurrentDestFmt, dOk := fmtCopy.(ConcurrentFormatter)
    66  				concurrentSourceFmt, sOk := r.fmt.(ConcurrentFormatter)
    67  
    68  				if dOk && sOk {
    69  					concurrentDestFmt.Sync(concurrentSourceFmt)
    70  				}
    71  			} else {
    72  				suite.fmt = r.fmt
    73  			}
    74  
    75  			r.initializer(suite)
    76  			suite.run()
    77  			if suite.failed {
    78  				*fail = true
    79  			}
    80  			if useFmtCopy {
    81  				copyLock.Lock()
    82  
    83  				concurrentDestFmt, dOk := r.fmt.(ConcurrentFormatter)
    84  				concurrentSourceFmt, sOk := fmtCopy.(ConcurrentFormatter)
    85  
    86  				if dOk && sOk {
    87  					concurrentDestFmt.Copy(concurrentSourceFmt)
    88  				} else if !dOk {
    89  					panic("cant cast dest formatter to progress-typed")
    90  				} else if !sOk {
    91  					panic("cant cast source formatter to progress-typed")
    92  				}
    93  
    94  				copyLock.Unlock()
    95  			}
    96  		}(&failed, &ft)
    97  	}
    98  	// wait until last are processed
    99  	for i := 0; i < rate; i++ {
   100  		queue <- i
   101  	}
   102  	close(queue)
   103  
   104  	// print summary
   105  	r.fmt.Summary()
   106  	return
   107  }
   108  
   109  func (r *runner) run() bool {
   110  	suite := &Suite{
   111  		fmt:           r.fmt,
   112  		randomSeed:    r.randomSeed,
   113  		stopOnFailure: r.stopOnFailure,
   114  		strict:        r.strict,
   115  		features:      r.features,
   116  	}
   117  	r.initializer(suite)
   118  	suite.run()
   119  
   120  	r.fmt.Summary()
   121  	return suite.failed
   122  }
   123  
   124  // RunWithOptions is same as Run function, except
   125  // it uses Options provided in order to run the
   126  // test suite without parsing flags
   127  //
   128  // This method is useful in case if you run
   129  // godog in for example TestMain function together
   130  // with go tests
   131  //
   132  // The exit codes may vary from:
   133  //  0 - success
   134  //  1 - failed
   135  //  2 - command line usage error
   136  //  128 - or higher, os signal related error exit codes
   137  //
   138  // If there are flag related errors they will
   139  // be directed to os.Stderr
   140  func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Options) int {
   141  	var output io.Writer = os.Stdout
   142  	if nil != opt.Output {
   143  		output = opt.Output
   144  	}
   145  
   146  	if opt.NoColors {
   147  		output = colors.Uncolored(output)
   148  	} else {
   149  		output = colors.Colored(output)
   150  	}
   151  
   152  	if opt.ShowStepDefinitions {
   153  		s := &Suite{}
   154  		contextInitializer(s)
   155  		s.printStepDefinitions(output)
   156  		return exitOptionError
   157  	}
   158  
   159  	if len(opt.Paths) == 0 {
   160  		inf, err := os.Stat("features")
   161  		if err == nil && inf.IsDir() {
   162  			opt.Paths = []string{"features"}
   163  		}
   164  	}
   165  
   166  	if opt.Concurrency > 1 && !supportsConcurrency(opt.Format) {
   167  		fmt.Fprintln(os.Stderr, fmt.Errorf("format \"%s\" does not support concurrent execution", opt.Format))
   168  		return exitOptionError
   169  	}
   170  	formatter := FindFmt(opt.Format)
   171  	if nil == formatter {
   172  		var names []string
   173  		for name := range AvailableFormatters() {
   174  			names = append(names, name)
   175  		}
   176  		fmt.Fprintln(os.Stderr, fmt.Errorf(
   177  			`unregistered formatter name: "%s", use one of: %s`,
   178  			opt.Format,
   179  			strings.Join(names, ", "),
   180  		))
   181  		return exitOptionError
   182  	}
   183  
   184  	features, err := parseFeatures(opt.Tags, opt.Paths)
   185  	if err != nil {
   186  		fmt.Fprintln(os.Stderr, err)
   187  		return exitOptionError
   188  	}
   189  
   190  	// user may have specified -1 option to create random seed
   191  	randomize := opt.Randomize
   192  	if randomize == -1 {
   193  		randomize = makeRandomSeed()
   194  	}
   195  
   196  	r := runner{
   197  		fmt:           formatter(suite, output),
   198  		initializer:   contextInitializer,
   199  		features:      features,
   200  		randomSeed:    randomize,
   201  		stopOnFailure: opt.StopOnFailure,
   202  		strict:        opt.Strict,
   203  	}
   204  
   205  	// store chosen seed in environment, so it could be seen in formatter summary report
   206  	os.Setenv("GODOG_SEED", strconv.FormatInt(r.randomSeed, 10))
   207  	// determine tested package
   208  	_, filename, _, _ := runtime.Caller(1)
   209  	os.Setenv("GODOG_TESTED_PACKAGE", runsFromPackage(filename))
   210  
   211  	var failed bool
   212  	if opt.Concurrency > 1 {
   213  		failed = r.concurrent(opt.Concurrency, func() Formatter { return formatter(suite, output) })
   214  	} else {
   215  		failed = r.run()
   216  	}
   217  
   218  	// @TODO: should prevent from having these
   219  	os.Setenv("GODOG_SEED", "")
   220  	os.Setenv("GODOG_TESTED_PACKAGE", "")
   221  	if failed && opt.Format != "events" {
   222  		return exitFailure
   223  	}
   224  	return exitSuccess
   225  }
   226  
   227  func runsFromPackage(fp string) string {
   228  	dir := filepath.Dir(fp)
   229  	for _, gp := range gopaths {
   230  		gp = filepath.Join(gp, "src")
   231  		if strings.Index(dir, gp) == 0 {
   232  			return strings.TrimLeft(strings.Replace(dir, gp, "", 1), string(filepath.Separator))
   233  		}
   234  	}
   235  	return dir
   236  }
   237  
   238  // Run creates and runs the feature suite.
   239  // Reads all configuration options from flags.
   240  // uses contextInitializer to register contexts
   241  //
   242  // the concurrency option allows runner to
   243  // initialize a number of suites to be run
   244  // separately. Only progress formatter
   245  // is supported when concurrency level is
   246  // higher than 1
   247  //
   248  // contextInitializer must be able to register
   249  // the step definitions and event handlers.
   250  //
   251  // The exit codes may vary from:
   252  //  0 - success
   253  //  1 - failed
   254  //  2 - command line usage error
   255  //  128 - or higher, os signal related error exit codes
   256  //
   257  // If there are flag related errors they will
   258  // be directed to os.Stderr
   259  func Run(suite string, contextInitializer func(suite *Suite)) int {
   260  	var opt Options
   261  	opt.Output = colors.Colored(os.Stdout)
   262  	flagSet := FlagSet(&opt)
   263  	if err := flagSet.Parse(os.Args[1:]); err != nil {
   264  		fmt.Fprintln(os.Stderr, err)
   265  		return exitOptionError
   266  	}
   267  
   268  	opt.Paths = flagSet.Args()
   269  
   270  	return RunWithOptions(suite, contextInitializer, opt)
   271  }
   272  
   273  func supportsConcurrency(format string) bool {
   274  	switch format {
   275  	case "progress", "junit":
   276  		return true
   277  	case "events", "pretty", "cucumber":
   278  		return false
   279  	default:
   280  		return true // enables concurrent custom formatters to work
   281  	}
   282  }