github.com/getgauge/gauge@v1.6.9/env/env.go (about)

     1  /*----------------------------------------------------------------
     2   *  Copyright (c) ThoughtWorks, Inc.
     3   *  Licensed under the Apache License, Version 2.0
     4   *  See LICENSE in the project root for license information.
     5   *----------------------------------------------------------------*/
     6  
     7  package env
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strconv"
    16  
    17  	"strings"
    18  
    19  	"github.com/getgauge/common"
    20  	"github.com/getgauge/gauge/config"
    21  	"github.com/getgauge/gauge/logger"
    22  	"github.com/getgauge/gauge/manifest"
    23  	"github.com/magiconair/properties"
    24  )
    25  
    26  const (
    27  	// SpecsDir holds the location of spec files
    28  	SpecsDir = "gauge_specs_dir"
    29  	// ConceptsDir holds the location of concept files
    30  	ConceptsDir = "gauge_concepts_dir"
    31  	// GaugeReportsDir holds the location of reports
    32  	GaugeReportsDir = "gauge_reports_dir"
    33  	// GaugeEnvironment holds the name of the current environment
    34  	GaugeEnvironment = "gauge_environment"
    35  	// LogsDirectory holds the location of log files
    36  	LogsDirectory = "logs_directory"
    37  	// OverwriteReports = false will create a new directory for reports
    38  	// for every run.
    39  	OverwriteReports = "overwrite_reports"
    40  	// ScreenshotOnFailure indicates if failure should invoke screenshot
    41  	ScreenshotOnFailure = "screenshot_on_failure"
    42  	saveExecutionResult = "save_execution_result"
    43  	// CsvDelimiter holds delimiter used to parse csv files
    44  	CsvDelimiter                   = "csv_delimiter"
    45  	allowCaseSensitiveTags         = "allow_case_sensitive_tags"
    46  	allowMultilineStep             = "allow_multiline_step"
    47  	allowScenarioDatatable         = "allow_scenario_datatable"
    48  	allowFilteredParallelExecution = "allow_filtered_parallel_execution"
    49  	enableMultithreading           = "enable_multithreading"
    50  	// GaugeScreenshotsDir holds the location of screenshots dir
    51  	GaugeScreenshotsDir     = "gauge_screenshots_dir"
    52  	gaugeSpecFileExtensions = "gauge_spec_file_extensions"
    53  	gaugeDataDir            = "gauge_data_dir"
    54  	envDirEnvVar            = "gauge_env_dir"
    55  )
    56  
    57  var envVars map[string]string
    58  var expansionVars map[string]string
    59  
    60  var currentEnvironments = []string{}
    61  
    62  // LoadEnv first generates the map of the env vars that needs to be set.
    63  // It starts by populating the map with the env passed by the user in --env flag.
    64  // It then adds the default values of the env vars which are required by Gauge,
    65  // but are not present in the map.
    66  //
    67  // Finally, all the env vars present in the map are actually set in the shell.
    68  func LoadEnv(envName string, errorHandler properties.ErrorHandlerFunc) error {
    69  	properties.ErrorHandler = errorHandler
    70  	allEnvs := strings.Split(envName, ",")
    71  
    72  	envVars = make(map[string]string)
    73  	expansionVars = make(map[string]string)
    74  
    75  	defaultEnvLoaded := false
    76  	for _, env := range allEnvs {
    77  		env = strings.TrimSpace(env)
    78  
    79  		err := loadEnvDir(env)
    80  		if err != nil {
    81  			return fmt.Errorf("Failed to load env. %s", err.Error())
    82  		}
    83  
    84  		if env == common.DefaultEnvDir {
    85  			defaultEnvLoaded = true
    86  		} else {
    87  			currentEnvironments = append(currentEnvironments, env)
    88  		}
    89  	}
    90  
    91  	if !defaultEnvLoaded {
    92  		err := loadEnvDir(common.DefaultEnvDir)
    93  		if err != nil {
    94  			return fmt.Errorf("Failed to load env. %s", err.Error())
    95  		}
    96  	}
    97  
    98  	loadDefaultEnvVars()
    99  	err := checkEnvVarsExpanded()
   100  	if err != nil {
   101  		return fmt.Errorf("Failed to load env. %s", err.Error())
   102  	}
   103  	err = setEnvVars()
   104  	if err != nil {
   105  		return fmt.Errorf("Failed to load env. %s", err.Error())
   106  	}
   107  	return nil
   108  }
   109  
   110  func loadDefaultEnvVars() {
   111  	addEnvVar(SpecsDir, "specs")
   112  	addEnvVar(GaugeReportsDir, "reports")
   113  	addEnvVar(GaugeEnvironment, common.DefaultEnvDir)
   114  	addEnvVar(LogsDirectory, "logs")
   115  	addEnvVar(OverwriteReports, "true")
   116  	addEnvVar(ScreenshotOnFailure, "true")
   117  	addEnvVar(saveExecutionResult, "false")
   118  	addEnvVar(CsvDelimiter, ",")
   119  	addEnvVar(allowMultilineStep, "false")
   120  	addEnvVar(allowScenarioDatatable, "false")
   121  	addEnvVar(allowFilteredParallelExecution, "false")
   122  	defaultScreenshotDir := filepath.Join(config.ProjectRoot, common.DotGauge, "screenshots")
   123  	addEnvVar(GaugeScreenshotsDir, defaultScreenshotDir)
   124  	addEnvVar(gaugeSpecFileExtensions, ".spec, .md")
   125  	addEnvVar(allowCaseSensitiveTags, "false")
   126  	err := os.MkdirAll(defaultScreenshotDir, 0750)
   127  	if err != nil {
   128  		logger.Warningf(true, "Could not create screenshot dir at %s", err.Error())
   129  	}
   130  }
   131  
   132  func loadEnvDir(envName string) error {
   133  	e, err := getEnvDir()
   134  	if err != nil {
   135  		return err
   136  	}
   137  	envDirPath := filepath.Join(config.ProjectRoot, e, envName)
   138  	if !common.DirExists(envDirPath) {
   139  		if envName != common.DefaultEnvDir {
   140  			return fmt.Errorf("%s environment does not exist", envName)
   141  		}
   142  		return nil
   143  	}
   144  	addEnvVar(GaugeEnvironment, envName)
   145  	logger.Debugf(true, "'%s' set to '%s'", GaugeEnvironment, envName)
   146  	files := common.FindFilesInDir(envDirPath,
   147  		isPropertiesFile,
   148  		func(p string, f os.FileInfo) bool { return false },
   149  	)
   150  	gaugeProperties := properties.MustLoadFiles(files, properties.UTF8, false)
   151  	processedProperties, err := GetProcessedPropertiesMap(gaugeProperties)
   152  	if err != nil {
   153  		return fmt.Errorf("Failed to parse properties in %s. %s", envDirPath, err.Error())
   154  	}
   155  	LoadEnvProperties(processedProperties)
   156  	return nil
   157  }
   158  
   159  func getEnvDir() (string, error) {
   160  	envDir := os.Getenv(envDirEnvVar)
   161  	if envDir != "" {
   162  		if filepath.IsAbs(envDir) {
   163  			return "", fmt.Errorf("'%s' environment variable is set to an absolute path. It must be relative to project root.", envDir)
   164  		}
   165  		logger.Debugf(true, "'%s' env variable is set to '%s'. env will be loaded from this location.", envDirEnvVar, envDir)
   166  		return envDir, nil
   167  	}
   168  	m, err := manifest.ProjectManifest()
   169  	if err != nil {
   170  		logger.Debugf(true, "Failed to load env from manifest - %s\nenv will be loaded from default directory 'env'", err.Error())
   171  		return common.EnvDirectoryName, nil
   172  	}
   173  	if m.EnvironmentDir != "" {
   174  		logger.Debugf(true, "'EnvironmentDir' is set to '%s' in manifest.json. env will be loaded from this location.", m.EnvironmentDir)
   175  		return m.EnvironmentDir, nil
   176  	}
   177  	logger.Debugf(true, "env will be loaded from default directory 'env'")
   178  	return common.EnvDirectoryName, nil
   179  }
   180  
   181  func GetProcessedPropertiesMap(propertiesMap *properties.Properties) (*properties.Properties, error) {
   182  	for propertyKey := range propertiesMap.Map() {
   183  		// Update properties if an env var is set.
   184  		if envVarValue, present := os.LookupEnv(propertyKey); present && len(envVarValue) > 0 {
   185  			if _, _, err := propertiesMap.Set(propertyKey, envVarValue); err != nil {
   186  				return propertiesMap, fmt.Errorf("%s", err.Error())
   187  			}
   188  		}
   189  		// Update the properties if it has already been added to envVars map.
   190  		if _, ok := envVars[propertyKey]; ok {
   191  			if _, _, err := propertiesMap.Set(propertyKey, envVars[propertyKey]); err != nil {
   192  				return propertiesMap, fmt.Errorf("%s", err.Error())
   193  			}
   194  		}
   195  	}
   196  	return propertiesMap, nil
   197  }
   198  
   199  func LoadEnvProperties(propertiesMap *properties.Properties) {
   200  	for propertyKey, propertyValue := range propertiesMap.Map() {
   201  		if contains, matches := containsEnvVar(propertyValue); contains {
   202  			for _, match := range matches {
   203  				key, defaultValue := match[1], match[0]
   204  				// Dont need to add to expansions if it's already set by env var
   205  				if !isPropertySet(key) {
   206  					expansionVars[key] = propertiesMap.GetString(key, defaultValue)
   207  				}
   208  			}
   209  		}
   210  		addEnvVar(propertyKey, propertiesMap.GetString(propertyKey, propertyValue))
   211  	}
   212  }
   213  
   214  func checkEnvVarsExpanded() error {
   215  	for key, value := range expansionVars {
   216  		if _, ok := envVars[key]; ok {
   217  			delete(expansionVars, key)
   218  		}
   219  		if err := isCircular(key, value); err != nil {
   220  			return err
   221  		}
   222  	}
   223  	if len(expansionVars) > 0 {
   224  		keys := make([]string, 0, len(expansionVars))
   225  		for key := range expansionVars {
   226  			keys = append(keys, key)
   227  		}
   228  		return fmt.Errorf("[%s] env variable(s) are not set", strings.Join(keys, ", "))
   229  	}
   230  	return nil
   231  }
   232  
   233  func isCircular(key, value string) error {
   234  	if keyValue, exists := envVars[key]; exists {
   235  		if len(keyValue) > 0 {
   236  			value = keyValue
   237  		}
   238  		_, err := properties.LoadString(fmt.Sprintf("%s=%s", key, value))
   239  		if err != nil {
   240  			return errors.New(err.Error())
   241  		}
   242  	}
   243  	return nil
   244  }
   245  
   246  func containsEnvVar(value string) (contains bool, matches [][]string) {
   247  	// match for any ${foo}
   248  	rStr := `\$\{(\w+)\}`
   249  	r, err := regexp.Compile(rStr)
   250  	if err != nil {
   251  		logger.Errorf(false, "Unable to compile regex %s: %s", rStr, err.Error())
   252  	}
   253  	contains = r.MatchString(value)
   254  	if contains {
   255  		matches = r.FindAllStringSubmatch(value, -1)
   256  	}
   257  	return
   258  }
   259  
   260  func addEnvVar(name, value string) {
   261  	if _, ok := envVars[name]; !ok {
   262  		envVars[name] = value
   263  	}
   264  }
   265  
   266  func isPropertiesFile(path string) bool {
   267  	return filepath.Ext(path) == ".properties"
   268  }
   269  
   270  func setEnvVars() error {
   271  	for name, value := range envVars {
   272  		if !isPropertySet(name) {
   273  			err := common.SetEnvVariable(name, value)
   274  			if err != nil {
   275  				return fmt.Errorf("%s", err.Error())
   276  			}
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  func isPropertySet(property string) bool {
   283  	return len(os.Getenv(property)) > 0
   284  }
   285  
   286  // comma-separated value of environments
   287  func CurrentEnvironments() string {
   288  	if len(currentEnvironments) == 0 {
   289  		currentEnvironments = append(currentEnvironments, common.DefaultEnvDir)
   290  	}
   291  	return strings.Join(currentEnvironments, ",")
   292  }
   293  
   294  func convertToBool(property string, defaultValue bool) bool {
   295  	v := os.Getenv(property)
   296  	boolValue, err := strconv.ParseBool(strings.TrimSpace(v))
   297  	if err != nil {
   298  		logger.Warningf(true, "Incorrect value for %s in property file. Cannot convert %s to boolean.", property, v)
   299  		logger.Warningf(true, "Using default value %v for property %s.", defaultValue, property)
   300  		return defaultValue
   301  	}
   302  	return boolValue
   303  }
   304  
   305  // AllowFilteredParallelExecution - feature toggle for filtered parallel execution
   306  var AllowFilteredParallelExecution = func() bool {
   307  	return convertToBool(allowFilteredParallelExecution, false)
   308  }
   309  
   310  // AllowScenarioDatatable -feature toggle for datatables in scenario
   311  var AllowScenarioDatatable = func() bool {
   312  	return convertToBool(allowScenarioDatatable, false)
   313  }
   314  
   315  // AllowMultiLineStep - feature toggle for newline in step text
   316  var AllowMultiLineStep = func() bool {
   317  	return convertToBool(allowMultilineStep, false)
   318  }
   319  
   320  // SaveExecutionResult determines if last run result should be saved
   321  var SaveExecutionResult = func() bool {
   322  	return convertToBool(saveExecutionResult, false)
   323  }
   324  
   325  // EnableMultiThreadedExecution determines if threads should be used instead of process
   326  // for each parallel stream
   327  var EnableMultiThreadedExecution = func() bool {
   328  	return convertToBool(enableMultithreading, false)
   329  }
   330  
   331  var GaugeSpecFileExtensions = func() []string {
   332  	e := os.Getenv(gaugeSpecFileExtensions)
   333  	if e == "" {
   334  		e = ".spec, .md" //this was earlier hardcoded, this is a failsafe if env isn't set
   335  	}
   336  	exts := strings.Split(strings.TrimSpace(e), ",")
   337  	var allowedExts = []string{}
   338  	for _, ext := range exts {
   339  		e := strings.TrimSpace(ext)
   340  		if e != "" {
   341  			allowedExts = append(allowedExts, e)
   342  		}
   343  	}
   344  	return allowedExts
   345  }
   346  
   347  // AllowCaseSensitiveTags determines if the casing is ignored in tags filtering
   348  var AllowCaseSensitiveTags = func() bool {
   349  	return convertToBool(allowCaseSensitiveTags, false)
   350  }
   351  
   352  // GaugeDataDir gets the data files location. This location should be relative to GAUGE_PROJECT_ROOT
   353  var GaugeDataDir = func() string {
   354  	d := os.Getenv(gaugeDataDir)
   355  	if d == "" {
   356  		return "."
   357  	}
   358  	return d
   359  }