// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // // Contributor: // - Aaron Meihm ameihm@mozilla.com package scribe import ( "fmt" "strings" ) // TestTag describes arbitrary key value tags that can be associated with a test type TestTag struct { Key string `json:"key,omitempty" yaml:"key,omitempty"` Value string `json:"value,omitempty" yaml:"value,omitempty"` } // Test is a test within the policy document that will be executed. Tests specify // various criteria, and then compare this criteria against the data returned by the // object the test references. type Test struct { TestID string `json:"test" yaml:"test"` // The ID for this test. TestName string `json:"name" yaml:"name"` // An optional name for this test Object string `json:"object" yaml:"object"` // The object this test references. Description string `json:"description,omitempty" yaml:"description,omitempty"` // Evaluators EVR EVRTest `json:"evr,omitempty" yaml:"evr,omitempty"` // EVR version comparison Regexp Regex `json:"regexp,omitempty" yaml:"regexp,omitempty"` // Regular expression comparison EMatch ExactMatch `json:"exactmatch,omitempty" yaml:"exactmatch,omitempty"` // Exact string match Tags []TestTag `json:"tags,omitempty" yaml:"tags,omitempty"` // Tags associated with the test If []string `json:"if,omitempty" yaml:"if,omitempty"` // Slice of test names for dependencies // These values are optional but can be set to use the expected result // callback handler. These are primarily used for testing but can also // be used to trigger scribecmd to return and a non-zero exit status // if a test does not evaluate to the desired value. ExpectedResult bool `json:"expectedresult,omitempty" yaml:"expectedresult,omitempty"` // Expected master result for test ExpectError bool `json:"expecterror,omitempty" yaml:"expecterror,omitempty"` // True if test should result in error prepared bool // True if test has been prepared. evaluated bool // True if test has been evaluated at least once. err error // The last error condition encountered during preparation or execution. // The final result for this test, a rolled up version of the results // of this test for any identified candidates. If at least one // candidate for the test evaluated to true, the master result will be // true. masterResult bool // The final result for the test. hasTrueResults bool // True if at least one result evaluated to true. results []evaluationResult // A slice of results for the test. } // The result of evaluation of a test. There can be more then one // EvaluationResult present in the results of a test, if the source // information returned more than one matching object. type evaluationResult struct { criteria evaluationCriteria // Criteria used during evaluation. result bool // The result of the evaluation. } // Generic criteria for an evaluation. A source object should always support // conversion from the specific type to a set of evaluation criteria. // // An identifier is used to track the source of an evaluation. For example, // this may be a filename or a package name. In those examples, the testValue // may be matched content from the file, or a package version string. type evaluationCriteria struct { identifier string // The identifier used to track the source. testValue string // the actual test data passed to the evaluator. } type genericEvaluator interface { evaluate(evaluationCriteria) (evaluationResult, error) } func (t *Test) validate(d *Document) error { if len(t.TestID) == 0 { return fmt.Errorf("a test in document has no identifier") } if t.getEvaluationInterface() == nil { return fmt.Errorf("%v: no valid evaluation interface", t.TestID) } for _, x := range t.If { ptr, err := d.GetTest(x) if err != nil { return fmt.Errorf("%v: %v", t.TestID, err) } if ptr == t { return fmt.Errorf("%v: test cannot reference itself", t.TestID) } } // Ensure the tags only contain valid characters for _, x := range t.Tags { if strings.ContainsRune(x.Key, '"') { return fmt.Errorf("%v: test tag key cannot contain quote", t.TestID) } if strings.ContainsRune(x.Value, '"') { return fmt.Errorf("%v: test tag value cannot contain quote", t.TestID) } } return nil } func (t *Test) getEvaluationInterface() genericEvaluator { if t.EVR.Value != "" { return &t.EVR } else if t.Regexp.Value != "" { return &t.Regexp } else if t.EMatch.Value != "" { return &t.EMatch } // If no evaluation criteria exists, use a no op evaluator // which will always return true for the test if any source objects // are identified. return &noop{} } func (t *Test) errorHandler(d *Document) error { if sRuntime.excall == nil { return t.err } if !t.ExpectError { tr, err := GetResults(d, t.TestID) if err != nil { panic("GetResults() in errorHandler") } sRuntime.excall(tr) } return t.err } func (t *Test) runTest(d *Document) error { if t.evaluated { return nil } // If this test has failed at some point, return the error. if t.err != nil { return t.err } debugPrint("runTest(): running \"%v\"\n", t.TestID) t.evaluated = true // First, see if this test has any dependencies. If so, run those // before we execute this one. for _, x := range t.If { dt, err := d.GetTest(x) if err != nil { t.err = err return t.errorHandler(d) } err = dt.runTest(d) if err != nil { t.err = fmt.Errorf("a test dependency failed (\"%v\")", x) return t.errorHandler(d) } } ev := t.getEvaluationInterface() if ev == nil { t.err = fmt.Errorf("test has no valid evaluation interface") return t.errorHandler(d) } // Make sure the object is prepared before we use it. flag, err := d.objectPrepared(t.Object) if err != nil { t.err = err return t.errorHandler(d) } if !flag { t.err = fmt.Errorf("object not prepared") return t.errorHandler(d) } si, _ := d.getObjectInterface(t.Object) if si == nil { t.err = fmt.Errorf("test has no valid source interface") return t.errorHandler(d) } for _, x := range si.getCriteria() { res, err := ev.evaluate(x) if err != nil { t.err = err return t.errorHandler(d) } t.results = append(t.results, res) } // Set the master result for the test. If any of the dependent tests // are false from a master result perspective, this one is also false. // If at least one result for this test is true, the master result for // the test is true. t.hasTrueResults = false for _, x := range t.results { if x.result { t.hasTrueResults = true } } t.masterResult = false if t.hasTrueResults { t.masterResult = true } for _, x := range t.If { dt, err := d.GetTest(x) if err != nil { t.err = err t.masterResult = false return t.errorHandler(d) } if !dt.masterResult { t.masterResult = false break } } // See if there is a test expected result handler installed, if so // validate it and call the handler if required. if sRuntime.excall != nil { if (t.masterResult != t.ExpectedResult) || t.ExpectError { tr, err := GetResults(d, t.TestID) if err != nil { panic("GetResults() in expected handler") } sRuntime.excall(tr) } } return nil }