diff --git a/flags.go b/flags.go index a839a5f..b56b3a9 100644 --- a/flags.go +++ b/flags.go @@ -26,6 +26,10 @@ var descTagsOption = "Filter scenarios by tags. Expression can be:\n" + s(4) + "- " + colors.Yellow(`"@wip && ~@new"`) + ": run wip scenarios, but exclude new\n" + s(4) + "- " + colors.Yellow(`"@wip,@undone"`) + ": run wip or undone scenarios" +var descRandomOption = "Randomly shuffle the scenario execution order.\n" + + "Specify SEED to reproduce the shuffling from a previous run.\n" + + s(4) + `e.g. ` + colors.Yellow(`--random`) + " or " + colors.Yellow(`--random=5738`) + // FlagSet allows to manage flags by external suite runner func FlagSet(opt *Options) *flag.FlagSet { descFormatOption := "How to format tests output. Available formats:\n" @@ -46,8 +50,7 @@ func FlagSet(opt *Options) *flag.FlagSet { set.BoolVar(&opt.ShowStepDefinitions, "d", false, "Print all available step definitions.") set.BoolVar(&opt.StopOnFailure, "stop-on-failure", false, "Stop processing on first failed scenario.") set.BoolVar(&opt.NoColors, "no-colors", false, "Disable ansi colors.") - set.BoolVar(&opt.Randomize, "random", false, "Randomize scenario execution order.") - set.Int64Var(&opt.RandomSeed, "seed", 0, "Specify random seed to reproduce shuffling from a previous run.\n(implies --random)") + set.Var(&opt.Randomize, "random", descRandomOption) set.Usage = usage(set, opt.Output) return set } @@ -67,12 +70,12 @@ func (f *flagged) name() string { name = fmt.Sprintf("-%s", f.short) } - if f.long == "seed" { - // `seed`` is special in that we will later assign it randomly + if f.long == "random" { + // `random` is special in that we will later assign it randomly // if the user specifies `--random` without specifying one, // so mask the "default" value here to avoid UI confusion about // what the value will end up being. - name += "=SEED" + name += "[=SEED]" } else if f.dflt != "true" && f.dflt != "false" { name += "=" + f.dflt } diff --git a/options.go b/options.go index b1d9625..b5899d9 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,11 @@ package godog -import "io" +import ( + "io" + "math/rand" + "strconv" + "time" +) // Options are suite run options // flags are mapped to these options. @@ -13,23 +18,22 @@ type Options struct { // Print step definitions found and exit ShowStepDefinitions bool - // Randomize causes scenarios to be run in random order. + // Randomize, if not `0`, will be used to run scenarios in a random order. // // Randomizing scenario order is especially helpful for detecting // situations where you have state leaking between scenarios, which can // cause flickering or fragile tests. - Randomize bool - - // RandomSeed allows specifying the seed to reproduce the random scenario - // shuffling from a previous run. // - // When `RandomSeed` is left at the nil value (`0`), but `Randomize` - // has been set to `true`, then godog will automatically pick a random - // seed between `1-99999` for ease of specification. + // The default value of `0` means "do not randomize". // - // If RandomSeed is set to anything other than the default nil value (`0`), - // then `Randomize = true` will be implied. - RandomSeed int64 + // The magic value of `-1` means "pick a random seed for me", and godog will + // assign a seed on it's own during the `RunWithOptions` phase, similar to if + // you specified `--random` on the command line. + // + // Any other value will be used as the random seed for shuffling. Re-using the + // same seed will allow you to reproduce the shuffle order of a previous run + // to isolate an error condition. + Randomize randomSeed // Stops on the first failure StopOnFailure bool @@ -53,3 +57,40 @@ type Options struct { // Where it should print formatter output Output io.Writer } + +// randomSeed implements `flag.Value`, see https://golang.org/pkg/flag/#Value +type randomSeed int64 + +// Choose randomly assigns a convenient pseudo-random seed value. +// The resulting seed will be between `1-99999` for later ease of specification. +func (rs *randomSeed) Choose() { + r := rand.New(rand.NewSource(time.Now().UTC().UnixNano())) + *rs = randomSeed(r.Int63n(99998) + 1) +} + +func (rs *randomSeed) Set(s string) error { + if s == "true" { + rs.Choose() + return nil + } + + if s == "false" { + *rs = 0 + return nil + } + + i, err := strconv.ParseInt(s, 10, 64) + *rs = randomSeed(i) + return err +} + +func (rs randomSeed) String() string { + return strconv.FormatInt(int64(rs), 10) +} + +// If a Value has an IsBoolFlag() bool method returning true, the command-line +// parser makes -name equivalent to -name=true rather than using the next +// command-line argument. +func (rs *randomSeed) IsBoolFlag() bool { + return *rs == 0 +} diff --git a/run.go b/run.go index a20fd34..0aca7f8 100644 --- a/run.go +++ b/run.go @@ -3,9 +3,7 @@ package godog import ( "fmt" "io" - "math/rand" "os" - "time" "github.com/DATA-DOG/godog/colors" ) @@ -111,23 +109,17 @@ func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Opt features, err := parseFeatures(opt.Tags, opt.Paths) fatal(err) - // the actual seed value which will be propogated - // if left as nil value, then scenarios run sequentially - var seed int64 - // use specified seed if exists, or assign one ourselves if - // none specified but user wants randomization - if opt.RandomSeed != 0 { - seed = opt.RandomSeed - } else if opt.Randomize && opt.RandomSeed == 0 { - r := rand.New(rand.NewSource(time.Now().UTC().UnixNano())) - seed = r.Int63n(99998) + 1 + // handle convenience condition where user specified `-1` in Options struct, + // asking us to choose the random value seed for them. + if opt.Randomize == -1 { + opt.Randomize.Choose() } r := runner{ fmt: formatter(suite, output), initializer: contextInitializer, features: features, - randomSeed: seed, + randomSeed: int64(opt.Randomize), stopOnFailure: opt.StopOnFailure, }