internal/testrunner/runners/system/runner.go (317 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package system import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "time" "github.com/elastic/elastic-package/internal/elasticsearch" "github.com/elastic/elastic-package/internal/kibana" "github.com/elastic/elastic-package/internal/logger" "github.com/elastic/elastic-package/internal/packages" "github.com/elastic/elastic-package/internal/profile" "github.com/elastic/elastic-package/internal/resources" "github.com/elastic/elastic-package/internal/servicedeployer" "github.com/elastic/elastic-package/internal/testrunner" ) type runner struct { profile *profile.Profile packageRootPath string kibanaClient *kibana.Client esAPI *elasticsearch.API esClient *elasticsearch.Client dataStreams []string serviceVariant string globalTestConfig testrunner.GlobalRunnerTestConfig failOnMissingTests bool deferCleanup time.Duration generateTestResult bool withCoverage bool coverageType string configFilePath string runSetup bool runTearDown bool runTestsOnly bool resourcesManager *resources.Manager serviceStateFilePath string } // Ensures that runner implements testrunner.TestRunner interface var _ testrunner.TestRunner = new(runner) type SystemTestRunnerOptions struct { Profile *profile.Profile PackageRootPath string KibanaClient *kibana.Client API *elasticsearch.API // FIXME: Keeping Elasticsearch client to be able to do low-level requests for parameters not supported yet by the API. ESClient *elasticsearch.Client DataStreams []string ServiceVariant string RunSetup bool RunTearDown bool RunTestsOnly bool ConfigFilePath string GlobalTestConfig testrunner.GlobalRunnerTestConfig FailOnMissingTests bool GenerateTestResult bool DeferCleanup time.Duration WithCoverage bool CoverageType string } func NewSystemTestRunner(options SystemTestRunnerOptions) *runner { r := runner{ packageRootPath: options.PackageRootPath, kibanaClient: options.KibanaClient, esAPI: options.API, esClient: options.ESClient, profile: options.Profile, dataStreams: options.DataStreams, serviceVariant: options.ServiceVariant, configFilePath: options.ConfigFilePath, runSetup: options.RunSetup, runTestsOnly: options.RunTestsOnly, runTearDown: options.RunTearDown, failOnMissingTests: options.FailOnMissingTests, generateTestResult: options.GenerateTestResult, deferCleanup: options.DeferCleanup, globalTestConfig: options.GlobalTestConfig, withCoverage: options.WithCoverage, coverageType: options.CoverageType, } r.resourcesManager = resources.NewManager() r.resourcesManager.RegisterProvider(resources.DefaultKibanaProviderName, &resources.KibanaProvider{Client: r.kibanaClient}) r.serviceStateFilePath = filepath.Join(stateFolderPath(r.profile.ProfilePath), serviceStateFileName) return &r } // SetupRunner prepares global resources required by the test runner. func (r *runner) SetupRunner(ctx context.Context) error { if r.runTearDown { logger.Debug("Skip installing package") return nil } // Install the package before creating the policy, so we control exactly what is being // installed. logger.Debug("Installing package...") resourcesOptions := resourcesOptions{ // Install it unless we are running the tear down only. installedPackage: !r.runTearDown, } _, err := r.resourcesManager.ApplyCtx(ctx, r.resources(resourcesOptions)) if err != nil { return fmt.Errorf("can't install the package: %w", err) } return nil } // TearDownRunner cleans up any global test runner resources. It must be called // after the test runner has finished executing all its tests. func (r *runner) TearDownRunner(ctx context.Context) error { logger.Debug("Uninstalling package...") resourcesOptions := resourcesOptions{ // Keep it installed only if we were running setup, or tests only. installedPackage: r.runSetup || r.runTestsOnly, } _, err := r.resourcesManager.ApplyCtx(ctx, r.resources(resourcesOptions)) if err != nil { return err } return nil } func (r *runner) GetTests(ctx context.Context) ([]testrunner.Tester, error) { var folders []testrunner.TestFolder manifest, err := packages.ReadPackageManifestFromPackageRoot(r.packageRootPath) if err != nil { return nil, fmt.Errorf("reading package manifest failed (path: %s): %w", r.packageRootPath, err) } hasDataStreams, err := testrunner.PackageHasDataStreams(manifest) if err != nil { return nil, fmt.Errorf("cannot determine if package has data streams: %w", err) } if r.runSetup || r.runTearDown || r.runTestsOnly { _, err := os.Stat(r.serviceStateFilePath) logger.Debugf("Service state data exists in %s: %v", r.serviceStateFilePath, !os.IsNotExist(err)) if r.runSetup && !os.IsNotExist(err) { return nil, fmt.Errorf("failed to run --setup, required to tear down previous setup") } if r.runTestsOnly && os.IsNotExist(err) { return nil, fmt.Errorf("failed to run tests with --no-provision, setup first with --setup") } if r.runTearDown && os.IsNotExist(err) { return nil, fmt.Errorf("failed to run --tear-down, setup not found") } } else { if _, err = os.Stat(r.serviceStateFilePath); !os.IsNotExist(err) { return nil, fmt.Errorf("failed to run tests, required to tear down previous state run (path: %s)", r.serviceStateFilePath) } } var serviceState ServiceState if r.runTearDown || r.runTestsOnly { serviceState, err = readServiceStateData(r.serviceStateFilePath) if err != nil { return nil, fmt.Errorf("failed to read service state: %w", err) } } if hasDataStreams { var dataStreams []string if r.runSetup || r.runTearDown || r.runTestsOnly { configFilePath := r.configFilePath if r.runTearDown || r.runTestsOnly { configFilePath = serviceState.ConfigFilePath } dataStream := testrunner.ExtractDataStreamFromPath(configFilePath, r.packageRootPath) dataStreams = append(dataStreams, dataStream) } else if len(r.dataStreams) > 0 { dataStreams = r.dataStreams } folders, err = testrunner.FindTestFolders(r.packageRootPath, dataStreams, r.Type()) if err != nil { return nil, fmt.Errorf("unable to determine test folder paths: %w", err) } if r.failOnMissingTests && len(folders) == 0 { if len(dataStreams) > 0 { return nil, fmt.Errorf("no %s tests found for %s data stream(s)", r.Type(), strings.Join(dataStreams, ",")) } return nil, fmt.Errorf("no %s tests found", r.Type()) } } else { folders, err = testrunner.FindTestFolders(r.packageRootPath, nil, r.Type()) if err != nil { return nil, fmt.Errorf("unable to determine test folder paths: %w", err) } if r.failOnMissingTests && len(folders) == 0 { return nil, fmt.Errorf("no %s tests found", r.Type()) } } if r.runSetup || r.runTearDown || r.runTestsOnly { // variant flag is not checked here since there are packages that do not have variants if len(folders) != 1 { return nil, fmt.Errorf("wrong number of test folders (expected 1): %d", len(folders)) } } var testers []testrunner.Tester for _, t := range folders { var variants []string var cfgFiles []string if r.runTestsOnly || r.runTearDown { variants = []string{serviceState.VariantName} cfgFiles = []string{filepath.Base(serviceState.ConfigFilePath)} } else { variants, err = r.getAllVariants(t) if err != nil { return nil, fmt.Errorf("failed to retrieve variants from %s: %w", t.Path, err) } cfgFiles, err = r.getAllConfigFiles(t) if err != nil { return nil, fmt.Errorf("failed to retrieve config files from %s: %w", t.Path, err) } } for _, variant := range variants { for _, config := range cfgFiles { logger.Debugf("System runner: data stream %q config file %q variant %q", t.DataStream, config, variant) tester, err := NewSystemTester(SystemTesterOptions{ Profile: r.profile, PackageRootPath: r.packageRootPath, KibanaClient: r.kibanaClient, API: r.esAPI, ESClient: r.esClient, TestFolder: t, ServiceVariant: variant, GenerateTestResult: r.generateTestResult, DeferCleanup: r.deferCleanup, RunSetup: r.runSetup, RunTestsOnly: r.runTestsOnly, RunTearDown: r.runTearDown, ConfigFileName: config, GlobalTestConfig: r.globalTestConfig, WithCoverage: r.withCoverage, CoverageType: r.coverageType, }) if err != nil { return nil, fmt.Errorf( "failed to create system runner for sdata stream %q variant %q config file %q: %w", t.DataStream, variant, config, err) } testers = append(testers, tester) } } } return testers, nil } // Type returns the type of test that can be run by this test runner. func (r *runner) Type() testrunner.TestType { return TestType } func (r *runner) resources(opts resourcesOptions) resources.Resources { return resources.Resources{ &resources.FleetPackage{ RootPath: r.packageRootPath, Absent: !opts.installedPackage, Force: opts.installedPackage, // Force re-installation, in case there are code changes in the same package version. }, } } func (r *runner) selectVariants(variantsFile *servicedeployer.VariantsFile) []string { if variantsFile == nil || variantsFile.Variants == nil { return []string{""} // empty variants file switches to no-variant mode } var variantNames []string for k := range variantsFile.Variants { if r.serviceVariant != "" && r.serviceVariant != k { continue } variantNames = append(variantNames, k) } return variantNames } func (r *runner) getAllVariants(folder testrunner.TestFolder) ([]string, error) { var variants []string dataStreamPath, found, err := packages.FindDataStreamRootForPath(folder.Path) if err != nil { return nil, fmt.Errorf("locating data stream root failed: %w", err) } if found { logger.Debugf("Running system tests for data stream %q", folder.DataStream) } else { logger.Debug("Running system tests for package") } devDeployPath, err := servicedeployer.FindDevDeployPath(servicedeployer.FactoryOptions{ PackageRootPath: r.packageRootPath, DataStreamRootPath: dataStreamPath, DevDeployDir: DevDeployDir, }) switch { case errors.Is(err, os.ErrNotExist): variants = r.selectVariants(nil) case err != nil: return nil, fmt.Errorf("failed fo find service deploy path: %w", err) default: variantsFile, err := servicedeployer.ReadVariantsFile(devDeployPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("can't read service variant: %w", err) } variants = r.selectVariants(variantsFile) } if r.serviceVariant != "" && len(variants) == 0 { return nil, fmt.Errorf("not found variant definition %q", r.serviceVariant) } if r.runSetup { // variant information in runTestOnly or runTearDown modes is retrieved from serviceOptions (file in setup dir) if len(variants) > 1 { return nil, fmt.Errorf("a variant must be selected or trigger the test in no-variant mode (available variants: %s)", strings.Join(variants, ", ")) } if len(variants) == 1 && variants[0] == "" { logger.Debug("No variant mode") } } return variants, nil } func (r *runner) getAllConfigFiles(folder testrunner.TestFolder) ([]string, error) { var cfgFiles []string var err error if r.configFilePath != "" { allCfgFiles, err := listConfigFiles(filepath.Dir(r.configFilePath)) if err != nil { return nil, fmt.Errorf("failed listing test case config cfgFiles: %w", err) } baseFile := filepath.Base(r.configFilePath) for _, cfg := range allCfgFiles { if cfg == baseFile { cfgFiles = append(cfgFiles, baseFile) } } } else { cfgFiles, err = listConfigFiles(folder.Path) if err != nil { return nil, fmt.Errorf("failed listing test case config cfgFiles: %w", err) } } return cfgFiles, nil }