github.com/CycloneDX/sbom-utility@v0.16.0/cmd/root_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 /* 3 * Licensed to the Apache Software Foundation (ASF) under one or more 4 * contributor license agreements. See the NOTICE file distributed with 5 * this work for additional information regarding copyright ownership. 6 * The ASF licenses this file to You under the Apache License, Version 2.0 7 * (the "License"); you may not use this file except in compliance with 8 * the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package cmd 20 21 import ( 22 "bytes" 23 "flag" 24 "fmt" 25 "os" 26 "strconv" 27 "strings" 28 "sync" 29 "testing" 30 31 "github.com/CycloneDX/sbom-utility/common" 32 "github.com/CycloneDX/sbom-utility/schema" 33 "github.com/CycloneDX/sbom-utility/utils" 34 ) 35 36 // Default test output (i.e., --output) directory 37 const DEFAULT_TEMP_OUTPUT_PATH = "temp/" 38 39 // Test files that span commands 40 const ( 41 TEST_INPUT_FILE_NON_EXISTENT = "non-existent-sbom.json" 42 ) 43 44 // Assure test infrastructure (shared resources) are only initialized once 45 // This would help if tests are eventually run in parallel 46 var initTestInfra sync.Once 47 48 // !!! SECRET SAUCE !!! 49 // The "go test" framework uses the "flags" package where all flags 50 // MUST be declared (as a global) otherwise `go test` will error out when passed 51 // NOTE: The following flags flags serve this purpose, but they are only 52 // filled in after "flag.parse()" is called which MUST be done post any init() processing. 53 // In order to get --trace or --debug output during init() processing, we rely upon 54 // directly parsing "os.Args[1:] in the `log` package 55 // USAGE: to set on command line and have it parsed, simply append 56 // it as follows: '--args --trace' 57 var TestLogLevelDebug = flag.Bool(FLAG_DEBUG, false, "") 58 var TestLogLevelTrace = flag.Bool(FLAG_TRACE, false, "") 59 var TestLogQuiet = flag.Bool(FLAG_QUIET_MODE, false, "") 60 61 type CommonTestInfo struct { 62 InputFile string 63 ListSummary bool 64 OutputFile string 65 OutputFormat string 66 OutputIndent uint8 67 WhereClause string 68 ResultExpectedByteSize int 69 ResultExpectedError error 70 ResultExpectedIndentLength int 71 ResultExpectedIndentAtLineNum int 72 ResultExpectedLineCount int 73 ResultLineContainsValues []string 74 ResultLineContainsValuesAtLineNum int 75 Autofail bool 76 MockStdin bool 77 } 78 79 // defaults for TestInfo struct values 80 const ( 81 TI_LIST_SUMMARY_FALSE = false 82 TI_LIST_LINE_WRAP = false 83 TI_DEFAULT_WHERE_CLAUSE = "" 84 TI_DEFAULT_POLICY_FILE = "" 85 TI_DEFAULT_JSON_INDENT = DEFAULT_OUTPUT_INDENT_LENGTH // 4 86 TI_RESULT_DEFAULT_LINE_COUNT = -1 87 TI_RESULT_DEFAULT_LINE_CONTAINS = -1 // NOTE: -1 means "any" line 88 ) 89 90 func NewCommonTestInfo() *CommonTestInfo { 91 var ti = new(CommonTestInfo) 92 ti.OutputIndent = TI_DEFAULT_JSON_INDENT 93 ti.ResultExpectedLineCount = TI_RESULT_DEFAULT_LINE_COUNT 94 ti.ResultLineContainsValuesAtLineNum = TI_RESULT_DEFAULT_LINE_CONTAINS 95 return ti 96 } 97 98 func NewCommonTestInfoBasic(inputFile string) *CommonTestInfo { 99 var ti = NewCommonTestInfo() 100 ti.InputFile = inputFile 101 return ti 102 } 103 104 func NewCommonTestInfoBasicList(inputFile string, whereClause string, listFormat string, listSummary bool) *CommonTestInfo { 105 var ti = NewCommonTestInfo() 106 ti.InputFile = inputFile 107 ti.WhereClause = whereClause 108 ti.OutputFormat = listFormat 109 ti.ListSummary = listSummary 110 return ti 111 } 112 113 // Stringer interface for ResourceTestInfo (just display subset of key values) 114 func (ti *CommonTestInfo) String() string { 115 buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) 116 return buffer.String() 117 } 118 119 func (ti *CommonTestInfo) Init(inputFile string, listFormat string, listSummary bool, whereClause string, 120 resultContainsValues []string, resultExpectedLineCount int, resultExpectedError error) *CommonTestInfo { 121 ti.InputFile = inputFile 122 ti.OutputFormat = listFormat 123 ti.OutputIndent = TI_DEFAULT_JSON_INDENT 124 ti.ListSummary = listSummary 125 ti.WhereClause = whereClause 126 ti.ResultLineContainsValuesAtLineNum = TI_RESULT_DEFAULT_LINE_CONTAINS 127 ti.ResultExpectedLineCount = resultExpectedLineCount 128 ti.ResultExpectedError = resultExpectedError 129 return ti 130 } 131 132 func (ti *CommonTestInfo) InitBasic(inputFile string, format string, expectedError error) *CommonTestInfo { 133 ti.Init(inputFile, format, TI_LIST_SUMMARY_FALSE, TI_DEFAULT_WHERE_CLAUSE, 134 nil, TI_RESULT_DEFAULT_LINE_COUNT, expectedError) 135 return ti 136 } 137 138 func (ti *CommonTestInfo) CreateTemporaryTestOutputFilename(relativeFilename string) (tempFilename string) { 139 testFunctionName := utils.GetCallerFunctionName(3) 140 trimmedFilename := strings.TrimLeft(relativeFilename, strconv.QuoteRune(os.PathSeparator)) 141 if testFunctionName != "" { 142 lastIndex := strings.LastIndex(trimmedFilename, string(os.PathSeparator)) 143 // insert test function name (as a variant since test files are reused) as last path... 144 if lastIndex > 0 { 145 path := trimmedFilename[0:lastIndex] 146 base := trimmedFilename[lastIndex:] 147 trimmedFilename = path + string(os.PathSeparator) + testFunctionName + base 148 } 149 } 150 return DEFAULT_TEMP_OUTPUT_PATH + trimmedFilename 151 } 152 153 func TestMain(m *testing.M) { 154 // Note: getLogger(): if it is creating the logger, will also 155 // initialize the log "level" and set "quiet" mode from command line args. 156 getLogger().Enter() 157 defer getLogger().Exit() 158 159 // Set log/trace/debug settings as if the were set by command line flags 160 if !flag.Parsed() { 161 getLogger().Tracef("calling `flag.Parse()`...") 162 flag.Parse() 163 } 164 getLogger().Tracef("Setting Debug=`%t`, Trace=`%t`, Quiet=`%t`,", *TestLogLevelDebug, *TestLogLevelTrace, *TestLogQuiet) 165 utils.GlobalFlags.PersistentFlags.Trace = *TestLogLevelTrace 166 utils.GlobalFlags.PersistentFlags.Debug = *TestLogLevelDebug 167 utils.GlobalFlags.PersistentFlags.Quiet = *TestLogQuiet 168 169 // Load configs, create logger, etc. 170 // NOTE: Be sure ALL "go test" flags are parsed/processed BEFORE initializing 171 err := initTestInfrastructure() 172 if err != nil { 173 os.Exit(ERROR_APPLICATION) 174 } 175 176 // Run test 177 exitCode := m.Run() 178 getLogger().Tracef("exit code: `%v`", exitCode) 179 180 // Exit with exit value from tests 181 os.Exit(exitCode) 182 } 183 184 // NOTE: if we need to override test setup in our own "main" routine, you can create 185 // a function named "TestMain" (and you will need to manage Init() and other setup) 186 // See: https://pkg.go.dev/testing 187 func initTestInfrastructure() (err error) { 188 getLogger().Enter() 189 defer getLogger().Exit() 190 191 initTestInfra.Do(func() { 192 getLogger().Tracef("initTestInfra.Do(): Initializing shared resources...") 193 194 // Assures we are loading relative to the application's executable directory 195 // which may vary if using IDEs or "go test" 196 err = initTestApplicationDirectories() 197 if err != nil { 198 return 199 } 200 201 // Leverage the root command's init function to populate schemas, policies, etc. 202 // Note: This method cannot return values as it is used as a callback by the Cobra framework 203 initConfigurations() 204 }) 205 return 206 } 207 208 // Set the working directory to match where the executable is being called from 209 func initTestApplicationDirectories() (err error) { 210 getLogger().Enter() 211 defer getLogger().Exit() 212 213 // Only set the working directory path once 214 if utils.GlobalFlags.WorkingDir == "" { 215 // Need to change the working directory to the application root instead of 216 // the "cmd" directory where this "_test" file runs so that all test files 217 // as well as "config.json" and its referenced JSON schema files load properly. 218 err = os.Chdir("..") 219 220 if err != nil { 221 // unable to change working directory; test data will not be found 222 return 223 } 224 225 // Need 'workingDir' to prepend to relative test files 226 utils.GlobalFlags.WorkingDir, _ = os.Getwd() 227 getLogger().Infof("Set `utils.GlobalFlags.WorkingDir`: `%s`", utils.GlobalFlags.WorkingDir) 228 } 229 230 return 231 } 232 233 // Helper functions 234 // TODO seek to use same function for evaluating error and messages as we do for other commands 235 func EvaluateErrorAndKeyPhrases(t *testing.T, err error, messages []string) (matched bool) { 236 matched = true 237 if err == nil { 238 t.Errorf("error expected: %s", messages) 239 } else { 240 getLogger().Tracef("Testing error message for the following substrings:\n%v", messages) 241 errorMessage := err.Error() 242 for _, substring := range messages { 243 if !strings.Contains(errorMessage, substring) { 244 matched = false 245 t.Errorf("expected string: `%s` not found in error message: `%s`", substring, err.Error()) 246 } 247 } 248 } 249 return 250 } 251 252 func prepareWhereFilters(t *testing.T, testInfo *CommonTestInfo) (whereFilters []common.WhereFilter, err error) { 253 if testInfo.WhereClause != "" { 254 whereFilters, err = retrieveWhereFilters(testInfo.WhereClause) 255 if err != nil { 256 t.Errorf("test failed: %s: detail: %s ", testInfo, err.Error()) 257 return 258 } 259 } 260 return 261 } 262 263 const RESULT_LINE_CONTAINS_ANY = -1 264 265 func bufferLineContainsValues(buffer bytes.Buffer, lineNum int, values ...string) (int, bool) { 266 267 lines := strings.Split(buffer.String(), "\n") 268 getLogger().Tracef("output: %s", lines) 269 270 for curLineNum, line := range lines { 271 272 // if this is a line we need to test 273 if lineNum == RESULT_LINE_CONTAINS_ANY || curLineNum == lineNum { 274 // test that all values occur in the current line 275 for iValue, value := range values { 276 if !strings.Contains(line, value) { 277 // if we failed to match all values on the specified line return failure 278 if curLineNum == lineNum { 279 getLogger().Infof("Actual contents of line %v: `%s`", curLineNum, line) 280 return curLineNum, false 281 } 282 // else, keep checking next line 283 break 284 } 285 286 // If this is the last value to test for, then all values have matched 287 if iValue+1 == len(values) { 288 return curLineNum, true 289 } 290 } 291 } 292 } 293 return RESULT_LINE_CONTAINS_ANY, false 294 } 295 296 func bufferContainsValues(buffer bytes.Buffer, values ...string) bool { 297 sBuffer := buffer.String() 298 // test that all values occur in the current line 299 for _, value := range values { 300 if !strings.Contains(sBuffer, value) { 301 return false 302 } 303 } 304 return true 305 } 306 307 func numberOfLeadingSpaces(line string) (numSpaces int) { 308 for _, ch := range line { 309 if ch == ' ' { 310 numSpaces++ 311 } else { 312 break 313 } 314 } 315 return 316 } 317 318 func getBufferLinesAndCount(buffer bytes.Buffer) (numLines int, lines []string) { 319 if buffer.Len() > 0 { 320 lines = strings.Split(buffer.String(), "\n") 321 numLines = len(lines) 322 } 323 return 324 } 325 326 func bufferFile(fullFileName string) (buffer *bytes.Buffer, err error) { 327 sBytes, err := os.ReadFile(fullFileName) 328 if err != nil { 329 return 330 } 331 buffer = bytes.NewBuffer(sBytes) 332 return 333 } 334 335 func verifyFileLineCountAndIndentation(t *testing.T, buffer bytes.Buffer, cti *CommonTestInfo) (err error) { 336 numLines, lines := getBufferLinesAndCount(buffer) 337 338 if cti.ResultExpectedLineCount != TI_RESULT_DEFAULT_LINE_COUNT { 339 if numLines != cti.ResultExpectedLineCount { 340 err = fmt.Errorf("invalid test output result: expected: %v lines, actual: %v", cti.ResultExpectedLineCount, numLines) 341 t.Error(err) 342 } 343 getLogger().Tracef("success: output contained expected line count: %v", cti.ResultExpectedLineCount) 344 } 345 346 if numLines > cti.ResultExpectedIndentAtLineNum { 347 line := lines[cti.ResultExpectedIndentAtLineNum] 348 //fmt.Printf("testing indent: %v at %v\n", cti.ResultExpectedIndentLength, cti.ResultExpectedIndentAtLineNum) 349 if spaceCount := numberOfLeadingSpaces(line); spaceCount != cti.ResultExpectedIndentLength { 350 t.Errorf("invalid test result: expected indent: %v, actual: %v", cti.ResultExpectedIndentLength, spaceCount) 351 } 352 } 353 getLogger().Tracef("success: output contained expected indent length: %v, at line: %v", cti.ResultExpectedIndentLength, cti.ResultLineContainsValuesAtLineNum) 354 return 355 } 356 357 func LoadBOMOutputFile(originalTest CommonTestInfo) (bom *schema.BOM, err error) { 358 filename := originalTest.OutputFile 359 return LoadBOMFile(filename) 360 }