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  }