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 }