This repository has been archived by the owner on May 12, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 374
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enhance the `katatestutils` package to provide the ability to skip tests based on either user or distro the tests are running on. Fixes #1586. Signed-off-by: James O. D. Hunt <[email protected]>
- Loading branch information
1 parent
b5aa8d4
commit 5619a5e
Showing
1 changed file
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,355 @@ | ||
// Copyright (c) 2019 Intel Corporation | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
package katatestutils | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"strings" | ||
) | ||
|
||
// Operator represents an operator to apply to a test constraint value. | ||
type Operator int | ||
|
||
const ( | ||
TestDisabledNeedRoot = "Test disabled as requires root user" | ||
TestDisabledNeedNonRoot = "Test disabled as requires non-root user" | ||
|
||
equalOperator Operator = iota | ||
notEqualOperator Operator = iota | ||
|
||
osRelease = "/etc/os-release" | ||
|
||
// Clear Linux has a different path (for stateless support) | ||
osReleaseClr = "/usr/lib/os-release" | ||
) | ||
|
||
// String converts the operator to a human-readable value. | ||
func (o Operator) String() (s string) { | ||
switch o { | ||
case equalOperator: | ||
s = "==" | ||
case notEqualOperator: | ||
s = "!=" | ||
} | ||
|
||
return s | ||
} | ||
|
||
// Constraints encapsulates all information about a test constraint. | ||
type Constraints struct { | ||
Issue string | ||
|
||
UID int | ||
|
||
// Not ideal: set when UID needs to be checked. This allows | ||
// a test for UID 0 to be detected. | ||
UIDSet bool | ||
|
||
Distro string | ||
|
||
Operator Operator | ||
} | ||
|
||
// Result is the outcome of a Constraint test | ||
type Result struct { | ||
// Details of the constraint | ||
// (human-readable result of testing for a Constraint). | ||
Description string | ||
|
||
// true if constraint was valid | ||
Success bool | ||
} | ||
|
||
// Constraint is a function that operates on a Constraints object to set | ||
// particular values. | ||
type Constraint func(c *Constraints) | ||
|
||
// TestConstraint records details about test constraints. | ||
type TestConstraint struct { | ||
Debug bool | ||
|
||
// Used to record all passed and failed constraints in | ||
// human-readable form. | ||
Passed []Result | ||
Failed []Result | ||
|
||
Issue string | ||
} | ||
|
||
// NewKataTest creates a new TestConstraint object and is the main interface | ||
// to the test constraints feature. | ||
func NewTestConstraint(debug bool) TestConstraint { | ||
return TestConstraint{ | ||
Debug: debug, | ||
} | ||
} | ||
|
||
// GetFileContents return the file contents as a string. | ||
func getFileContents(file string) (string, error) { | ||
bytes, err := ioutil.ReadFile(file) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return string(bytes), nil | ||
} | ||
|
||
// getDistroDetails returns the distributions name and version string. | ||
// If it is not possible to determine both values an error is | ||
// returned. | ||
func getDistroDetails() (name, version string, err error) { | ||
files := []string{osRelease, osReleaseClr} | ||
name = "" | ||
version = "" | ||
|
||
for _, file := range files { | ||
contents, err := getFileContents(file) | ||
if err != nil { | ||
if os.IsNotExist(err) { | ||
continue | ||
} | ||
|
||
return "", "", err | ||
} | ||
|
||
lines := strings.Split(contents, "\n") | ||
|
||
for _, line := range lines { | ||
if strings.HasPrefix(line, "NAME=") && name == "" { | ||
fields := strings.Split(line, "=") | ||
name = strings.Trim(fields[1], `"`) | ||
} else if strings.HasPrefix(line, "VERSION_ID=") && version == "" { | ||
fields := strings.Split(line, "=") | ||
version = strings.Trim(fields[1], `"`) | ||
} | ||
} | ||
|
||
if name != "" && version != "" { | ||
return name, version, nil | ||
} | ||
} | ||
|
||
if name == "" { | ||
return "", "", errors.New("unknown distro name") | ||
} | ||
|
||
if version == "" { | ||
return "", "", errors.New("unknown distro version") | ||
} | ||
|
||
return name, version, nil | ||
} | ||
|
||
// handleDistro checks that the current distro is compatible with | ||
// the constraint specified by the arguments. | ||
func (tc *TestConstraint) handleDistro(distro string, op Operator) (result Result, err error) { | ||
if distro == "" { | ||
return Result{}, fmt.Errorf("distro cannot be blank") | ||
} | ||
|
||
distro = strings.ToLower(distro) | ||
|
||
name, _, err := getDistroDetails() | ||
if err != nil { | ||
return Result{}, err | ||
} | ||
|
||
name = strings.ToLower(name) | ||
|
||
var success bool | ||
|
||
switch op { | ||
case equalOperator: | ||
success = distro == name | ||
case notEqualOperator: | ||
success = distro != name | ||
default: | ||
return Result{}, fmt.Errorf("invalid operator: %+v\n", op) | ||
} | ||
|
||
descr := fmt.Sprintf("need distro %s %q, got distro %q", op, distro, name) | ||
|
||
result = Result{ | ||
Description: descr, | ||
Success: success, | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
// handleUID checks that the current UID is compatible with the constraint | ||
// specified by the arguments. | ||
func (tc *TestConstraint) handleUID(uid int, op Operator) (result Result, err error) { | ||
if uid < 0 { | ||
return Result{}, fmt.Errorf("uid must be >= 0, got %d", uid) | ||
} | ||
|
||
actualEUID := os.Geteuid() | ||
|
||
var success bool | ||
|
||
switch op { | ||
case equalOperator: | ||
success = actualEUID == uid | ||
case notEqualOperator: | ||
success = actualEUID != uid | ||
default: | ||
return Result{}, fmt.Errorf("invalid operator: %+v\n", op) | ||
} | ||
|
||
descr := fmt.Sprintf("need uid %s %d, got euid %d", op, uid, actualEUID) | ||
|
||
result = Result{ | ||
Description: descr, | ||
Success: success, | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
// handleResults is the common handler for all constraint types. It deals with | ||
// errors found trying to check constraints, stores results and displays | ||
// details of valid constraints. | ||
func (tc *TestConstraint) handleResults(result Result, err error) { | ||
if err != nil { | ||
var extra string | ||
|
||
if tc.Issue != "" { | ||
extra = fmt.Sprintf(" (issue %s)", tc.Issue) | ||
} | ||
|
||
panic(fmt.Sprintf("failed to check test constraints: error: %s%s\n", err, extra)) | ||
} | ||
|
||
if !result.Success { | ||
tc.Failed = append(tc.Failed, result) | ||
} else { | ||
tc.Passed = append(tc.Passed, result) | ||
} | ||
|
||
if tc.Debug { | ||
var outcome string | ||
|
||
if result.Success { | ||
outcome = "valid" | ||
} else { | ||
outcome = "invalid" | ||
} | ||
|
||
fmt.Printf("Constraint %s: %s\n", outcome, result.Description) | ||
} | ||
} | ||
|
||
// constraintInvalid handles the specified constraint, returning true if the | ||
// constraint fails, else false. | ||
func (tc *TestConstraint) constraintInvalid(fn Constraint) bool { | ||
c := Constraints{} | ||
|
||
// Call the constraint function that sets the Constraints values | ||
fn(&c) | ||
|
||
if c.Issue != "" { | ||
// Just record it | ||
tc.Issue = c.Issue | ||
} | ||
|
||
if c.UIDSet { | ||
result, err := tc.handleUID(c.UID, c.Operator) | ||
tc.handleResults(result, err) | ||
if !result.Success { | ||
return true | ||
} | ||
} | ||
|
||
if c.Distro != "" { | ||
result, err := tc.handleDistro(c.Distro, c.Operator) | ||
tc.handleResults(result, err) | ||
if !result.Success { | ||
return true | ||
} | ||
} | ||
|
||
// Constraint is valid | ||
return false | ||
} | ||
|
||
// NeedUID skips the test unless running as a user with the specified user ID. | ||
func NeedUID(uid int, op Operator) Constraint { | ||
return func(c *Constraints) { | ||
c.Operator = op | ||
c.UID = uid | ||
c.UIDSet = true | ||
} | ||
} | ||
|
||
// NeedNonRoot skips the test unless running as root. | ||
func NeedRoot() Constraint { | ||
return NeedUID(0, equalOperator) | ||
} | ||
|
||
// NeedNonRoot skips the test if running as the root user. | ||
func NeedNonRoot() Constraint { | ||
return NeedUID(0, notEqualOperator) | ||
} | ||
|
||
// NeedDistroWithOp skips the test unless the distro constraint specified by | ||
// the arguments is true. | ||
func NeedDistroWithOp(distro string, op Operator) Constraint { | ||
return func(c *Constraints) { | ||
c.Distro = strings.ToLower(distro) | ||
c.Operator = op | ||
} | ||
} | ||
|
||
// NeedDistroEquals will skip the test unless running on the specified distro. | ||
func NeedDistroEquals(distro string) Constraint { | ||
return NeedDistroWithOp(distro, equalOperator) | ||
} | ||
|
||
// NeedDistroNotEquals will skip the test unless run a distro that does not | ||
// match the specified name. | ||
func NeedDistroNotEquals(distro string) Constraint { | ||
return NeedDistroWithOp(distro, notEqualOperator) | ||
} | ||
|
||
// NeedDistro will skip the test unless running on the specified distro. | ||
func NeedDistro(distro string) Constraint { | ||
return NeedDistroEquals(distro) | ||
} | ||
|
||
// WithIssue allows the specification of an issue URL. | ||
// | ||
// Note that the issue is not checked for validity. | ||
func WithIssue(issue string) Constraint { | ||
return func(c *Constraints) { | ||
c.Issue = issue | ||
} | ||
} | ||
|
||
// NotValid checks if the specified list of constraints are all valid, | ||
// returning true if any _fail_. | ||
// | ||
// Notes: | ||
// | ||
// - Constraints are applied in the order specified. | ||
// - A constraint type (user, distro) can only be specified once. | ||
// - If the function fails to determine whether it can check the constraints, | ||
// it will panic. Since this is facility is used for testing, this seems like | ||
// the best approach as it unburdens the caller from checking for an error | ||
// (which should never be ignored). | ||
func (tc *TestConstraint) NotValid(constraints ...Constraint) bool { | ||
for _, c := range constraints { | ||
invalid := tc.constraintInvalid(c) | ||
if invalid { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} |