github.com/kubeshop/testkube@v1.17.23/contrib/executor/jmeter/pkg/runner/runner.go (about)

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/kubeshop/testkube/contrib/executor/jmeter/pkg/parser"
    13  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    14  	"github.com/kubeshop/testkube/pkg/envs"
    15  	"github.com/kubeshop/testkube/pkg/executor"
    16  	"github.com/kubeshop/testkube/pkg/executor/agent"
    17  	"github.com/kubeshop/testkube/pkg/executor/content"
    18  	"github.com/kubeshop/testkube/pkg/executor/env"
    19  	"github.com/kubeshop/testkube/pkg/executor/output"
    20  	"github.com/kubeshop/testkube/pkg/executor/runner"
    21  	"github.com/kubeshop/testkube/pkg/executor/scraper"
    22  	"github.com/kubeshop/testkube/pkg/executor/scraper/factory"
    23  	"github.com/kubeshop/testkube/pkg/ui"
    24  )
    25  
    26  func NewRunner(ctx context.Context, params envs.Params) (*JMeterRunner, error) {
    27  	output.PrintLog(fmt.Sprintf("%s Preparing test runner", ui.IconTruck))
    28  
    29  	var err error
    30  	r := &JMeterRunner{
    31  		Params: params,
    32  	}
    33  
    34  	r.Scraper, err = factory.TryGetScrapper(ctx, params)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	return r, nil
    40  }
    41  
    42  // JMeterRunner runner
    43  type JMeterRunner struct {
    44  	Params  envs.Params
    45  	Scraper scraper.Scraper
    46  }
    47  
    48  var _ runner.Runner = &JMeterRunner{}
    49  
    50  func (r *JMeterRunner) Run(ctx context.Context, execution testkube.Execution) (result testkube.ExecutionResult, err error) {
    51  	if r.Scraper != nil {
    52  		defer r.Scraper.Close()
    53  	}
    54  	output.PrintEvent(
    55  		fmt.Sprintf("%s Running with config", ui.IconTruck),
    56  		"scraperEnabled", r.Params.ScrapperEnabled,
    57  		"dataDir", r.Params.DataDir,
    58  		"SSL", r.Params.Ssl,
    59  		"endpoint", r.Params.Endpoint,
    60  	)
    61  
    62  	envManager := env.NewManagerWithVars(execution.Variables)
    63  	envManager.GetReferenceVars(envManager.Variables)
    64  
    65  	path, workingDir, err := content.GetPathAndWorkingDir(execution.Content, r.Params.DataDir)
    66  	if err != nil {
    67  		output.PrintLogf("%s Failed to resolve absolute directory for %s, using the path directly", ui.IconWarning, r.Params.DataDir)
    68  	}
    69  
    70  	fileInfo, err := os.Stat(path)
    71  	if err != nil {
    72  		return result, err
    73  	}
    74  
    75  	if fileInfo.IsDir() {
    76  		scriptName := execution.Args[len(execution.Args)-1]
    77  		if workingDir != "" {
    78  			path = ""
    79  			if execution.Content != nil && execution.Content.Repository != nil {
    80  				scriptName = filepath.Join(execution.Content.Repository.Path, scriptName)
    81  			}
    82  		}
    83  
    84  		execution.Args = execution.Args[:len(execution.Args)-1]
    85  		output.PrintLogf("%s It is a directory test - trying to find file from the last executor argument %s in directory %s", ui.IconWorld, scriptName, path)
    86  
    87  		// sanity checking for test script
    88  		scriptFile := filepath.Join(path, workingDir, scriptName)
    89  		fileInfo, errFile := os.Stat(scriptFile)
    90  		if errors.Is(errFile, os.ErrNotExist) || fileInfo.IsDir() {
    91  			output.PrintLogf("%s Could not find file %s in the directory, error: %s", ui.IconCross, scriptName, errFile)
    92  			return *result.Err(errors.Errorf("could not find file %s in the directory: %v", scriptName, errFile)), nil
    93  		}
    94  		path = scriptFile
    95  	}
    96  
    97  	// compose parameters passed to JMeter with -J
    98  	params := make([]string, 0, len(envManager.Variables))
    99  	for _, value := range envManager.Variables {
   100  		params = append(params, fmt.Sprintf("-J%s=%s", value.Name, value.Value))
   101  	}
   102  
   103  	runPath := r.Params.DataDir
   104  	if workingDir != "" {
   105  		runPath = workingDir
   106  	}
   107  
   108  	outputDir := filepath.Join(runPath, "output")
   109  	// clean output directory it already exists, only useful for local development
   110  	_, err = os.Stat(outputDir)
   111  	if err == nil {
   112  		if err = os.RemoveAll(outputDir); err != nil {
   113  			output.PrintLogf("%s Failed to clean output directory %s", ui.IconWarning, outputDir)
   114  		}
   115  	}
   116  	// recreate output directory with wide permissions so JMeter can create report files
   117  	if err = os.Mkdir(outputDir, 0777); err != nil {
   118  		return *result.Err(errors.Errorf("could not create directory %s: %v", outputDir, err)), nil
   119  	}
   120  
   121  	jtlPath := filepath.Join(outputDir, "report.jtl")
   122  	reportPath := filepath.Join(outputDir, "report")
   123  	jmeterLogPath := filepath.Join(outputDir, "jmeter.log")
   124  	args := execution.Args
   125  	hasJunit := false
   126  	hasReport := false
   127  	for i := range args {
   128  		if args[i] == "<runPath>" {
   129  			args[i] = path
   130  		}
   131  
   132  		if args[i] == "<jtlFile>" {
   133  			args[i] = jtlPath
   134  		}
   135  
   136  		if args[i] == "<reportFile>" {
   137  			args[i] = reportPath
   138  			hasReport = true
   139  		}
   140  
   141  		if args[i] == "<logFile>" {
   142  			args[i] = jmeterLogPath
   143  		}
   144  
   145  		if args[i] == "-l" {
   146  			hasJunit = true
   147  		}
   148  	}
   149  
   150  	for i := range args {
   151  		if args[i] == "<envVars>" {
   152  			newArgs := make([]string, len(args)+len(params)-1)
   153  			copy(newArgs, args[:i])
   154  			copy(newArgs[i:], params)
   155  			copy(newArgs[i+len(params):], args[i+1:])
   156  			args = newArgs
   157  			break
   158  		}
   159  	}
   160  
   161  	for i := range args {
   162  		args[i] = os.ExpandEnv(args[i])
   163  	}
   164  
   165  	output.PrintLogf("%s Using arguments: %v", ui.IconWorld, envManager.ObfuscateStringSlice(args))
   166  
   167  	entryPoint := getEntryPoint()
   168  	for i := range execution.Command {
   169  		if execution.Command[i] == "<entryPoint>" {
   170  			execution.Command[i] = entryPoint
   171  		}
   172  	}
   173  
   174  	command, args := executor.MergeCommandAndArgs(execution.Command, args)
   175  	// run JMeter inside repo directory ignore execution error in case of failed test
   176  	output.PrintLogf("%s Test run command %s %s", ui.IconRocket, command, strings.Join(envManager.ObfuscateStringSlice(args), " "))
   177  	out, err := executor.Run(runPath, command, envManager, args...)
   178  	if err != nil {
   179  		return *result.WithErrors(errors.Errorf("jmeter run error: %v", err)), nil
   180  	}
   181  	out = envManager.ObfuscateSecrets(out)
   182  
   183  	var executionResult testkube.ExecutionResult
   184  	if hasJunit && hasReport {
   185  		output.PrintLogf("%s Getting report %s", ui.IconFile, jtlPath)
   186  		f, err := os.Open(jtlPath)
   187  		if err != nil {
   188  			return *result.WithErrors(errors.Errorf("getting jtl report error: %v", err)), nil
   189  		}
   190  
   191  		results, err := parser.ParseCSV(f)
   192  		f.Close()
   193  
   194  		if err != nil {
   195  			data, err := os.ReadFile(jtlPath)
   196  			if err != nil {
   197  				return *result.WithErrors(errors.Errorf("getting jtl report error: %v", err)), nil
   198  			}
   199  
   200  			testResults, err := parser.ParseXML(data)
   201  			if err != nil {
   202  				return *result.WithErrors(errors.Errorf("parsing jtl report error: %v", err)), nil
   203  			}
   204  
   205  			executionResult = MapTestResultsToExecutionResults(out, testResults)
   206  		} else {
   207  			executionResult = MapResultsToExecutionResults(out, results)
   208  		}
   209  	} else {
   210  		executionResult = makeSuccessExecution(out)
   211  	}
   212  
   213  	output.PrintLogf("%s Mapped JMeter results to Execution Results...", ui.IconCheckMark)
   214  
   215  	var rerr error
   216  	if execution.PostRunScript != "" && execution.ExecutePostRunScriptBeforeScraping {
   217  		output.PrintLog(fmt.Sprintf("%s Running post run script...", ui.IconCheckMark))
   218  
   219  		if rerr = agent.RunScript(execution.PostRunScript, r.Params.WorkingDir); rerr != nil {
   220  			output.PrintLogf("%s Failed to execute post run script %s", ui.IconWarning, rerr)
   221  		}
   222  	}
   223  
   224  	// scrape artifacts first even if there are errors above
   225  	if r.Params.ScrapperEnabled {
   226  		directories := []string{
   227  			outputDir,
   228  		}
   229  		var masks []string
   230  		if execution.ArtifactRequest != nil {
   231  			directories = append(directories, execution.ArtifactRequest.Dirs...)
   232  			masks = execution.ArtifactRequest.Masks
   233  		}
   234  
   235  		output.PrintLogf("Scraping directories: %v with masks: %v", directories, masks)
   236  		if err := r.Scraper.Scrape(ctx, directories, masks, execution); err != nil {
   237  			return *executionResult.Err(err), errors.Wrap(err, "error scraping artifacts for JMeter executor")
   238  		}
   239  	}
   240  
   241  	if rerr != nil {
   242  		return *result.Err(rerr), nil
   243  	}
   244  
   245  	return executionResult, nil
   246  }
   247  
   248  func getEntryPoint() (entrypoint string) {
   249  	if entrypoint = os.Getenv("ENTRYPOINT_CMD"); entrypoint != "" {
   250  		return entrypoint
   251  	}
   252  	wd, err := os.Getwd()
   253  	if err != nil {
   254  		wd = "."
   255  	}
   256  	return filepath.Join(wd, "testkube/contrib/executor/jmeter/scripts/entrypoint.sh")
   257  }
   258  
   259  func MapResultsToExecutionResults(out []byte, results parser.Results) (result testkube.ExecutionResult) {
   260  	result = makeSuccessExecution(out)
   261  	if results.HasError {
   262  		result.Status = testkube.ExecutionStatusFailed
   263  		result.ErrorMessage = results.LastErrorMessage
   264  	}
   265  
   266  	for _, r := range results.Results {
   267  		result.Steps = append(
   268  			result.Steps,
   269  			testkube.ExecutionStepResult{
   270  				Name:     r.Label,
   271  				Duration: r.Duration.String(),
   272  				Status:   MapResultStatus(r),
   273  				AssertionResults: []testkube.AssertionResult{{
   274  					Name:   r.Label,
   275  					Status: MapResultStatus(r),
   276  				}},
   277  			})
   278  	}
   279  
   280  	return result
   281  }
   282  
   283  func MapTestResultsToExecutionResults(out []byte, results parser.TestResults) (result testkube.ExecutionResult) {
   284  	result = makeSuccessExecution(out)
   285  
   286  	samples := append(results.HTTPSamples, results.Samples...)
   287  	for _, r := range samples {
   288  		if !r.Success {
   289  			result.Status = testkube.ExecutionStatusFailed
   290  			if r.AssertionResult != nil {
   291  				result.ErrorMessage = r.AssertionResult.FailureMessage
   292  			}
   293  		}
   294  
   295  		result.Steps = append(
   296  			result.Steps,
   297  			testkube.ExecutionStepResult{
   298  				Name:     r.Label,
   299  				Duration: fmt.Sprintf("%dms", r.Time),
   300  				Status:   MapTestResultStatus(r.Success),
   301  				AssertionResults: []testkube.AssertionResult{{
   302  					Name:   r.Label,
   303  					Status: MapTestResultStatus(r.Success),
   304  				}},
   305  			})
   306  	}
   307  
   308  	return result
   309  }
   310  
   311  func MapResultStatus(result parser.Result) string {
   312  	if result.Success {
   313  		return string(testkube.PASSED_ExecutionStatus)
   314  	}
   315  
   316  	return string(testkube.FAILED_ExecutionStatus)
   317  }
   318  
   319  func MapTestResultStatus(success bool) string {
   320  	if success {
   321  		return string(testkube.PASSED_ExecutionStatus)
   322  	}
   323  
   324  	return string(testkube.FAILED_ExecutionStatus)
   325  }
   326  
   327  // GetType returns runner type
   328  func (r *JMeterRunner) GetType() runner.Type {
   329  	return runner.TypeMain
   330  }
   331  
   332  func makeSuccessExecution(out []byte) (result testkube.ExecutionResult) {
   333  	status := testkube.PASSED_ExecutionStatus
   334  	result.Status = &status
   335  	result.Output = string(out)
   336  	result.OutputType = "text/plain"
   337  
   338  	return result
   339  }