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 }