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 }