github.com/kubeshop/testkube@v1.17.23/contrib/executor/cypress/pkg/runner/cypress.go (about) 1 package runner 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/joshdk/go-junit" 11 "github.com/pkg/errors" 12 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 NewCypressRunner(ctx context.Context, dependency string, params envs.Params) (*CypressRunner, error) { 27 output.PrintLogf("%s Preparing test runner", ui.IconTruck) 28 29 var err error 30 r := &CypressRunner{ 31 Params: params, 32 dependency: dependency, 33 } 34 35 r.Scraper, err = factory.TryGetScrapper(ctx, params) 36 if err != nil { 37 return nil, err 38 } 39 40 return r, nil 41 } 42 43 // CypressRunner - implements runner interface used in worker to start test execution 44 type CypressRunner struct { 45 Params envs.Params 46 Scraper scraper.Scraper 47 dependency string 48 } 49 50 func (r *CypressRunner) 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.PrintLogf("%s Preparing for test run", ui.IconTruck) 55 // make some validation 56 err = r.Validate(execution) 57 if err != nil { 58 return result, err 59 } 60 61 output.PrintLogf("%s Checking test content from %s...", ui.IconBox, execution.Content.Type_) 62 63 runPath, workingDir, err := content.GetPathAndWorkingDir(execution.Content, r.Params.DataDir) 64 if err != nil { 65 output.PrintLogf("%s Failed to resolve absolute directory for %s, using the path directly", ui.IconWarning, r.Params.DataDir) 66 } 67 68 projectPath := runPath 69 if workingDir != "" { 70 runPath = workingDir 71 } 72 73 fileInfo, err := os.Stat(projectPath) 74 if err != nil { 75 return result, err 76 } 77 78 if !fileInfo.IsDir() { 79 output.PrintLogf("%s Using file...", ui.IconTruck) 80 81 // TODO add cypress project structure 82 // TODO checkout this repo with `skeleton` path 83 // TODO overwrite skeleton/cypress/integration/test.js 84 // file with execution content git file 85 output.PrintLogf("%s Passing Cypress test as single file not implemented yet", ui.IconCross) 86 return result, errors.Errorf("passing cypress test as single file not implemented yet") 87 } 88 89 output.PrintLogf("%s Test content checked", ui.IconCheckMark) 90 91 out, err := r.installModule(runPath) 92 if err != nil { 93 return result, errors.Errorf("cypress module install error: %v\n\n%s", err, out) 94 } 95 96 // handle project local Cypress version install (`Cypress` app) 97 command, args := executor.MergeCommandAndArgs(execution.Command, []string{"install"}) 98 out, err = executor.Run(runPath, command, nil, args...) 99 if err != nil { 100 return result, errors.Errorf("cypress binary install error: %v\n\n%s", err, out) 101 } 102 103 envManager := env.NewManagerWithVars(execution.Variables) 104 envManager.GetReferenceVars(envManager.Variables) 105 envVars := make([]string, 0, len(envManager.Variables)) 106 for _, value := range envManager.Variables { 107 if !value.IsSecret() { 108 output.PrintLogf("%s=%s", value.Name, value.Value) 109 } 110 envVars = append(envVars, fmt.Sprintf("%s=%s", value.Name, value.Value)) 111 } 112 113 junitReportDir := filepath.Join(projectPath, "results") 114 junitReportPath := filepath.Join(projectPath, "results/junit-[hash].xml") 115 116 var project string 117 if workingDir != "" { 118 project = projectPath 119 } 120 121 // append args from execution 122 args = execution.Args 123 hasJunit := false 124 hasReporter := false 125 for i := len(args) - 1; i >= 0; i-- { 126 if project == "" && (args[i] == "--project" || args[i] == "<projectPath>") { 127 args = append(args[:i], args[i+1:]...) 128 continue 129 } 130 131 if args[i] == "<projectPath>" { 132 args[i] = project 133 } 134 135 if args[i] == "junit" { 136 hasJunit = true 137 } 138 139 if args[i] == "--reporter" { 140 hasReporter = true 141 } 142 143 if strings.Contains(args[i], "<reportFile>") { 144 args[i] = strings.ReplaceAll(args[i], "<reportFile>", junitReportPath) 145 } 146 147 if args[i] == "<envVars>" { 148 args[i] = strings.Join(envVars, ",") 149 } 150 151 args[i] = os.ExpandEnv(args[i]) 152 } 153 154 // run cypress inside repo directory ignore execution error in case of failed test 155 command, args = executor.MergeCommandAndArgs(execution.Command, args) 156 output.PrintLogf("%s Test run command %s %s", ui.IconRocket, command, strings.Join(envManager.ObfuscateStringSlice(args), " ")) 157 out, err = executor.Run(runPath, command, envManager, args...) 158 out = envManager.ObfuscateSecrets(out) 159 160 var suites []junit.Suite 161 var serr error 162 if hasJunit && hasReporter { 163 suites, serr = junit.IngestDir(junitReportDir) 164 result = MapJunitToExecutionResults(out, suites) 165 } else { 166 result = makeSuccessExecution(out) 167 } 168 169 output.PrintLogf("%s Mapped Junit to Execution Results...", ui.IconCheckMark) 170 171 if steps := result.FailedSteps(); len(steps) > 0 { 172 output.PrintLogf("Test Failed steps") 173 for _, s := range steps { 174 errorMessage := "" 175 for _, a := range s.AssertionResults { 176 errorMessage += a.ErrorMessage 177 } 178 output.PrintLog("step: " + s.Name + " error: " + errorMessage) 179 } 180 } 181 182 var rerr error 183 if execution.PostRunScript != "" && execution.ExecutePostRunScriptBeforeScraping { 184 output.PrintLog(fmt.Sprintf("%s Running post run script...", ui.IconCheckMark)) 185 186 if rerr = agent.RunScript(execution.PostRunScript, r.Params.WorkingDir); rerr != nil { 187 output.PrintLogf("%s Failed to execute post run script %s", ui.IconWarning, rerr) 188 } 189 } 190 191 // scrape artifacts first even if there are errors above 192 if r.Params.ScrapperEnabled { 193 directories := []string{ 194 junitReportDir, 195 filepath.Join(projectPath, "cypress/videos"), 196 filepath.Join(projectPath, "cypress/screenshots"), 197 } 198 199 var masks []string 200 if execution.ArtifactRequest != nil { 201 directories = append(directories, execution.ArtifactRequest.Dirs...) 202 masks = execution.ArtifactRequest.Masks 203 } 204 205 output.PrintLogf("Scraping directories: %v", directories) 206 207 if err := r.Scraper.Scrape(ctx, directories, masks, execution); err != nil { 208 return *result.WithErrors(err), nil 209 } 210 } 211 212 return *result.WithErrors(err, serr, rerr), nil 213 } 214 215 func (r *CypressRunner) installModule(runPath string) (out []byte, err error) { 216 if _, err = os.Stat(filepath.Join(runPath, "package.json")); err == nil { 217 // be gentle to different cypress versions, run from local npm deps 218 if r.dependency == "npm" { 219 out, err = executor.Run(runPath, "npm", nil, "install") 220 if err != nil { 221 return nil, errors.Errorf("npm install error: %v\n\n%s", err, out) 222 } 223 } 224 225 if r.dependency == "yarn" { 226 out, err = executor.Run(runPath, "yarn", nil, "install") 227 if err != nil { 228 return nil, errors.Errorf("yarn install error: %v\n\n%s", err, out) 229 } 230 } 231 } else if errors.Is(err, os.ErrNotExist) { 232 if r.dependency == "npm" { 233 out, err = executor.Run(runPath, "npm", nil, "init", "--yes") 234 if err != nil { 235 return nil, errors.Errorf("npm init error: %v\n\n%s", err, out) 236 } 237 238 out, err = executor.Run(runPath, "npm", nil, "install", "cypress", "--save-dev") 239 if err != nil { 240 return nil, errors.Errorf("npm install cypress error: %v\n\n%s", err, out) 241 } 242 } 243 244 if r.dependency == "yarn" { 245 out, err = executor.Run(runPath, "yarn", nil, "init", "--yes") 246 if err != nil { 247 return nil, errors.Errorf("yarn init error: %v\n\n%s", err, out) 248 } 249 250 out, err = executor.Run(runPath, "yarn", nil, "add", "cypress", "--dev") 251 if err != nil { 252 return nil, errors.Errorf("yarn add cypress error: %v\n\n%s", err, out) 253 } 254 } 255 } else { 256 output.PrintLogf("%s failed checking package.json file: %s", ui.IconCross, err.Error()) 257 return nil, errors.Errorf("checking package.json file: %v", err) 258 } 259 return 260 } 261 262 // Validate checks if Execution has valid data in context of Cypress executor. 263 // Cypress executor runs currently only based on Cypress project 264 func (r *CypressRunner) Validate(execution testkube.Execution) error { 265 266 if execution.Content == nil { 267 output.PrintLogf("%s Invalid input: can't find any content to run in execution data", ui.IconCross) 268 return errors.Errorf("can't find any content to run in execution data: %+v", execution) 269 } 270 271 if execution.Content.Repository == nil { 272 output.PrintLogf("%s Cypress executor handles only repository based tests, but repository is nil", ui.IconCross) 273 return errors.Errorf("cypress executor handles only repository based tests, but repository is nil") 274 } 275 276 if execution.Content.Repository.Branch == "" && execution.Content.Repository.Commit == "" { 277 output.PrintLogf("%s can't find branch or commit in params, repo:%+v", ui.IconCross, execution.Content.Repository) 278 return errors.Errorf("can't find branch or commit in params, repo:%+v", execution.Content.Repository) 279 } 280 281 return nil 282 } 283 284 func makeSuccessExecution(out []byte) (result testkube.ExecutionResult) { 285 status := testkube.PASSED_ExecutionStatus 286 result.Status = &status 287 288 return result 289 } 290 291 func MapJunitToExecutionResults(out []byte, suites []junit.Suite) (result testkube.ExecutionResult) { 292 result = makeSuccessExecution(out) 293 294 for _, suite := range suites { 295 for _, test := range suite.Tests { 296 297 result.Steps = append( 298 result.Steps, 299 testkube.ExecutionStepResult{ 300 Name: fmt.Sprintf("%s - %s", suite.Name, test.Name), 301 Duration: test.Duration.String(), 302 Status: MapStatus(test.Status), 303 }) 304 } 305 306 // TODO parse sub suites recursively 307 308 } 309 310 return result 311 } 312 313 func MapStatus(in junit.Status) (out string) { 314 switch string(in) { 315 case "passed": 316 return string(testkube.PASSED_ExecutionStatus) 317 case "pending": 318 return string(testkube.QUEUED_ExecutionStatus) 319 case "skipped": 320 return string(testkube.SKIPPED_ExecutionStatus) 321 default: 322 return string(testkube.FAILED_ExecutionStatus) 323 } 324 } 325 326 // GetType returns runner type 327 func (r *CypressRunner) GetType() runner.Type { 328 return runner.TypeMain 329 }