feat: add parameterization support for test cases and steps with configuration options

This commit is contained in:
lilong.129
2025-06-23 21:14:29 +08:00
parent 93007d5eb7
commit b320bbda31
8 changed files with 772 additions and 46 deletions

195
runner.go
View File

@@ -11,6 +11,7 @@ import (
"os"
"os/signal"
"reflect"
"sort"
"strings"
"syscall"
"testing"
@@ -734,6 +735,98 @@ const (
RUN_STEP_END = "run step end"
)
// executionTask holds the necessary information for a single step execution.
type executionTask struct {
stepName string
parameters map[string]interface{}
}
// formatParameters formats parameter values into a string for display in step names.
// e.g. {"foo": "bar", "age": 18} -> "bar-18"
func formatParameters(params map[string]interface{}) string {
if len(params) == 0 {
return ""
}
// sort keys to ensure consistent order
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var values []string
for _, k := range keys {
values = append(values, fmt.Sprintf("%v", params[k]))
}
return strings.Join(values, "-")
}
// generateExecutionTasks generates a list of execution tasks based on step parameters and loops.
func (r *SessionRunner) generateExecutionTasks(step IStep) ([]executionTask, error) {
stepConfig := step.Config()
stepName := step.Name()
// determine effective loop times
loopTimes := stepConfig.Loops
if loopTimes <= 0 {
loopTimes = 1 // default to 1 if not set
}
// initialize parameters iterator
parametersIterator, err := r.caseRunner.parser.InitParametersIterator(&TConfig{
Parameters: stepConfig.Parameters,
ParametersSetting: stepConfig.ParametersSetting,
Variables: stepConfig.Variables,
})
if err != nil {
return nil, errors.Wrap(err, "failed to initialize parameters iterator")
}
// collect all parameter combinations first
var allParameters []map[string]interface{}
if parametersIterator != nil {
for parametersIterator.HasNext() {
allParameters = append(allParameters, parametersIterator.Next())
}
}
// if no parameters are specified, but loop times are set,
// we should run the step loopTimes with empty parameters.
if len(allParameters) == 0 && loopTimes > 0 {
allParameters = append(allParameters, make(map[string]interface{}))
}
// generate execution tasks
var tasks []executionTask
for loopIndex := 1; loopIndex <= loopTimes; loopIndex++ {
for _, params := range allParameters {
// determine step name based on parameters and loops
currentStepName := stepName
hasParameters := len(params) > 0
hasLoops := loopTimes > 1
if hasParameters {
paramStr := formatParameters(params)
if hasLoops {
currentStepName = fmt.Sprintf("%s [loop_%d_params_%s]", stepName, loopIndex, paramStr)
} else {
currentStepName = fmt.Sprintf("%s [params_%s]", stepName, paramStr)
}
} else if hasLoops {
currentStepName = fmt.Sprintf("%s_loop_%d", stepName, loopIndex)
}
tasks = append(tasks, executionTask{
stepName: currentStepName,
parameters: params,
})
}
}
return tasks, nil
}
func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error) {
// check for interrupt signal before running step
select {
@@ -753,56 +846,90 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
stepName := step.Name()
stepType := string(step.Type())
log.Info().Str("step", stepName).Str("type", stepType).Msg(RUN_STEP_START)
// run times of step
loopTimes := step.Config().Loops
if loopTimes == 0 {
loopTimes = 1 // default run once
} else if loopTimes > 1 {
log.Info().Int("loops", loopTimes).Msg("set multiple loop times")
// execute step with parameters iterator
tasks, err := r.generateExecutionTasks(step)
if err != nil {
return nil, errors.Wrap(err, "failed to generate execution tasks")
}
// run step with specified loop times
for i := 1; i <= loopTimes; i++ {
var loopIndex string
if loopTimes > 1 {
log.Info().Int("index", i).Msg("start running step in loop")
loopIndex = fmt.Sprintf("_loop_%d", i)
var stepResults []*StepResult
// execute with loops as outer iteration
for _, task := range tasks {
// execute step with merged variables
stepResult, err := r.executeStepWithVariables(step, task.stepName, task.parameters)
if err != nil {
if r.caseRunner.hrpRunner.failfast {
return nil, errors.Wrap(err, "execute step failed")
}
log.Error().Err(err).Str("step", task.stepName).Msg("execute step failed")
}
// run step
stepResult, err = step.Run(r)
stepResult.Name = stepName + loopIndex
stepResults = append(stepResults, stepResult)
}
// add step result to summary
r.summary.AddStepResult(stepResult)
// update extracted variables
for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v
// return the last step result, or nil if no steps were executed
if len(stepResults) > 0 {
// add all step results to summary
for _, result := range stepResults {
r.summary.AddStepResult(result)
// update extracted variables from the last result
for k, v := range result.ExportVars {
r.sessionVariables[k] = v
}
}
// run step success
if err == nil {
// log final result
lastResult := stepResults[len(stepResults)-1]
if lastResult.Success {
log.Info().Str("step", stepName).
Str("type", stepType).
Bool("success", true).
Int64("elapsed(ms)", stepResult.Elapsed).
Interface("exportVars", stepResult.ExportVars).
Int64("elapsed(ms)", lastResult.Elapsed).
Interface("exportVars", lastResult.ExportVars).
Msg(RUN_STEP_END)
} else {
log.Error().Str("step", stepName).
Str("type", stepType).
Bool("success", false).
Int64("elapsed(ms)", lastResult.Elapsed).
Msg(RUN_STEP_END)
continue
}
// run step failed
log.Error().Err(err).Str("step", stepName).
Str("type", stepType).
Bool("success", false).
Int64("elapsed(ms)", stepResult.Elapsed).
Msg(RUN_STEP_END)
return stepResult, err
return lastResult, nil
}
return stepResult, nil
return nil, errors.New("no steps were executed")
}
// executeStepWithVariables executes a single step with given parameters
// parameters will override step variables with the same name
func (r *SessionRunner) executeStepWithVariables(step IStep, stepName string, parameters map[string]interface{}) (stepResult *StepResult, err error) {
stepConfig := step.Config()
// backup original variables
originalVariables := make(map[string]interface{})
for k, v := range stepConfig.Variables {
originalVariables[k] = v
}
// merge parameters into step variables
// parameters have higher priority than variables
for k, v := range parameters {
stepConfig.Variables[k] = v
}
// execute step
stepResult, err = step.Run(r)
stepResult.Name = stepName
// restore original variables to avoid side effects
stepConfig.Variables = originalVariables
return stepResult, err
}
func (r *SessionRunner) GetSummary() *TestCaseSummary {