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  }