github.com/ixpectus/declarate@v0.0.0-20240422152255-708027d7c068/run/run.go (about) 1 package run 2 3 import ( 4 "fmt" 5 "os" 6 "runtime/debug" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/ixpectus/declarate/compare" 12 "github.com/ixpectus/declarate/condition" 13 "github.com/ixpectus/declarate/contract" 14 "github.com/ixpectus/declarate/report" 15 "github.com/ixpectus/declarate/tools" 16 "gopkg.in/yaml.v2" 17 ) 18 19 // it is global because used in run/config::UnmarshalYAML 20 var builders []contract.CommandBuilder 21 22 type Runner struct { 23 config RunnerConfig 24 output contract.Output 25 currentVars contract.Vars 26 } 27 28 type RunnerConfig struct { 29 Variables contract.Vars 30 Builders []contract.CommandBuilder 31 Output contract.Output 32 Wrapper contract.TestWrapper 33 T *testing.T 34 comparer contract.Comparer 35 pollComparer contract.Comparer 36 Report contract.Report 37 } 38 39 func New(c RunnerConfig) *Runner { 40 builders = c.Builders 41 if c.comparer == nil { 42 c.comparer = compare.New(contract.CompareParams{ 43 IgnoreArraysOrdering: tools.To(true), 44 DisallowExtraFields: tools.To(false), 45 AllowArrayExtraItems: tools.To(true), 46 }, c.Variables) 47 } 48 if c.pollComparer == nil { 49 c.pollComparer = compare.New(contract.CompareParams{ 50 IgnoreArraysOrdering: tools.To(true), 51 DisallowExtraFields: tools.To(false), 52 AllowArrayExtraItems: tools.To(true), 53 }, c.Variables) 54 } 55 if c.Report == nil { 56 c.Report = report.NewEmptyReport() 57 } 58 c.Output.SetReport(c.Report) 59 return &Runner{ 60 config: c, 61 output: c.Output, 62 } 63 } 64 65 func (r *Runner) buildRunConfigs(fileName string) ([]runConfig, error) { 66 file, err := os.ReadFile(fileName) 67 if err != nil { 68 return nil, fmt.Errorf("file open: %w", err) 69 } 70 r.currentVars = r.config.Variables 71 configs := []runConfig{} 72 if err := yaml.Unmarshal(file, &configs); err != nil { 73 return nil, fmt.Errorf("unmarshall failed for file %s: %w", fileName, err) 74 } 75 76 return configs, nil 77 } 78 79 func (r *Runner) Run(fileName string, t *testing.T) (bool, error) { 80 configs, err := r.buildRunConfigs(fileName) 81 if err != nil { 82 return true, fmt.Errorf("unmarshall failed for file %s: %w", fileName, err) 83 } 84 for _, v := range configs { 85 if len(v.Commands) == 0 && len(v.Steps) == 0 { 86 // nothing to do 87 continue 88 } 89 if v.Condition != "" && !condition.IsTrue(r.currentVars, v.Condition) { 90 r.logSkip(v.Name, fileName, 0) 91 continue 92 } 93 v.Name = r.currentVars.Apply(v.Name) 94 var testResult *Result 95 res := true 96 var err error 97 action := func() { 98 testResult, err = r.run(v, fileName) 99 if err != nil { 100 r.logRunFail(v.Name, fileName, err, testResult) 101 if t != nil { 102 t.FailNow() 103 } 104 res = false 105 } 106 if testResult.Err != nil { 107 r.logErr(*testResult) 108 if t != nil { 109 t.FailNow() 110 } 111 res = false 112 } else { 113 r.logPass(v.Name, fileName, testResult, 0) 114 } 115 } 116 r.config.Report.Step( 117 report.ReportOptions{ 118 Description: v.Name, 119 }, 120 action, 121 ) 122 if !res { 123 return false, nil 124 } 125 126 } 127 return false, nil 128 } 129 130 func (r *Runner) run( 131 v runConfig, 132 fileName string, 133 ) (*Result, error) { 134 r.beforeTest(fileName, &v, 0) 135 var ( 136 err error 137 testResult *Result 138 ) 139 if v.Name != "" { 140 r.logStart(fileName, v, 0) 141 } 142 if len(v.Poll.PollInterval()) > 0 { 143 testResult, err = r.runWithPollInterval(v, fileName) 144 } else { 145 testResult, err = r.runOne(v, 0, fileName, false) 146 } 147 148 if err != nil { 149 return testResult, fmt.Errorf("run test for file %s: %w", fileName, err) 150 } 151 r.afterTest(fileName, v, *testResult) 152 return testResult, nil 153 } 154 155 func (r *Runner) runWithPollInterval(v runConfig, fileName string) (*Result, error) { 156 var err error 157 var testResult *Result 158 v.Poll.comparer = r.config.pollComparer 159 start := time.Now() 160 finish := start 161 for _, d := range v.Poll.PollInterval() { 162 finish = finish.Add(d) 163 } 164 // stores poll information, used for logs and reports 165 pollInfo := contract.PollInfo{ 166 Start: start, 167 Finish: finish, 168 } 169 pollResult := contract.PollResult{ 170 Start: start, 171 PlannedFinish: finish, 172 } 173 for i, d := range v.Poll.PollInterval() { 174 isPolling := true 175 if len(v.Poll.PollInterval())-1 == i { // last poll step 176 isPolling = false 177 } 178 179 estimated := finish.Sub(time.Now()) 180 181 testResult, err = r.runOne( 182 v, 183 0, 184 fileName, 185 isPolling, 186 ) 187 188 // unexpected test error run 189 if err != nil { 190 pollResult.Finish = time.Now() 191 testResult.PollResult = &pollResult 192 return nil, err 193 } 194 // test not passed 195 if testResult.Err != nil { 196 if v.Poll.ResponseRegexp != "" || v.Poll.ResponseTmpls != nil { 197 res, errs, _ := v.Poll.pollContinue(testResult.Response) 198 if !res { 199 if len(errs) > 0 { 200 testResult.PollConditionFailed = true 201 testResult.Err = errs[0] 202 } 203 break 204 } 205 } 206 r.logPoll(fileName, v, pollInfo, d, estimated) 207 time.Sleep(d) 208 } else { 209 break 210 // if v.Poll.ResponseRegexp != "" || v.Poll.ResponseTmpls != nil { 211 // break 212 // } 213 } 214 } 215 pollResult.Finish = time.Now() 216 testResult.PollResult = &pollResult 217 218 return testResult, err 219 } 220 221 func (r *Runner) setupCommand(cmd contract.Doer) contract.Doer { 222 cmd.SetVars(r.currentVars) 223 cmd.SetReport(r.config.Report) 224 225 return cmd 226 } 227 228 func (r *Runner) runCommand(cmd contract.Doer) (*string, error) { 229 cmd = r.setupCommand(cmd) 230 231 if err := cmd.Do(); err != nil { 232 return nil, err 233 } 234 responseBody := cmd.ResponseBody() 235 236 if err := cmd.Check(); err != nil { 237 return responseBody, err 238 } 239 return responseBody, nil 240 } 241 242 func (r *Runner) runOne( 243 conf runConfig, 244 lvl int, 245 fileName string, 246 isPolling bool, 247 ) (*Result, error) { 248 var ( 249 commandResponseBody *string 250 firstErrResult *Result 251 ) 252 253 defer func() { 254 if rr := recover(); rr != nil { 255 fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) 256 } 257 }() 258 conf.Name = r.currentVars.Apply(conf.Name) 259 260 for _, command := range conf.Commands { 261 r.beforeTestStep(fileName, &conf, lvl) 262 var err error 263 264 commandResponseBody, err = r.runCommand(command) 265 if err != nil { 266 res := &Result{ 267 Err: err, 268 Name: conf.Name, 269 Lvl: lvl, 270 FileName: fileName, 271 Response: commandResponseBody, 272 } 273 r.afterTestStep(fileName, &conf, *res, isPolling) 274 return res, nil 275 } 276 } 277 278 if len(conf.Steps) > 0 { 279 results := []string{} 280 for _, stepRunConfig := range conf.Steps { 281 if stepRunConfig.Condition != "" && !condition.IsTrue(r.config.Variables, stepRunConfig.Condition) { 282 r.logSkip(stepRunConfig.Name, fileName, lvl+1) 283 continue 284 } 285 if stepRunConfig.Name != "" && !isPolling { 286 r.logStart(fileName, stepRunConfig, lvl+1) 287 } 288 var testResult *Result 289 var err error 290 action := func() { 291 testResult, err = r.runOne(stepRunConfig, lvl+1, fileName, isPolling) 292 } 293 r.config.Report.Step(report.ReportOptions{Description: stepRunConfig.Name}, action) 294 295 if testResult.Err != nil && isPolling { 296 firstErrResult = testResult 297 if testResult.Response != nil { 298 results = append(results, *testResult.Response) 299 } else { 300 results = append(results, "") 301 } 302 continue 303 } 304 if testResult.Err != nil { 305 r.afterTestStep(fileName, &conf, *testResult, isPolling) 306 return testResult, nil 307 } 308 if testResult.Response != nil { 309 results = append(results, *testResult.Response) 310 } else { 311 results = append(results, "") 312 } 313 if err != nil { 314 return nil, err 315 } 316 if !isPolling { 317 r.logPass(stepRunConfig.Name, fileName, testResult, lvl+1) 318 } 319 } 320 if len(results) > 0 { 321 s := "[" + strings.Join(results, ", ") + "]" 322 commandResponseBody = &s 323 } 324 } 325 326 if err := r.fillAllVariables( 327 commandResponseBody, 328 conf, 329 ); err != nil { 330 res := &Result{ 331 Err: err, 332 Name: conf.Name, 333 Lvl: lvl, 334 FileName: fileName, 335 } 336 r.afterTestStep(fileName, &conf, *res, isPolling) 337 return res, nil 338 } 339 340 if firstErrResult != nil { 341 firstErrResult.Response = commandResponseBody 342 r.afterTestStep(fileName, &conf, *firstErrResult, isPolling) 343 return firstErrResult, nil 344 } 345 346 res := &Result{ 347 Response: commandResponseBody, 348 Lvl: lvl, 349 FileName: fileName, 350 } 351 352 r.afterTestStep(fileName, &conf, *res, isPolling) 353 return res, nil 354 }