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  }