Skip to content
This repository has been archived by the owner on May 12, 2021. It is now read-only.

Commit

Permalink
pkg: Add test constraints feature
Browse files Browse the repository at this point in the history
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
jodh-intel committed Apr 25, 2019
1 parent b5aa8d4 commit 5619a5e
Showing 1 changed file with 355 additions and 0 deletions.
355 changes: 355 additions & 0 deletions pkg/katatestutils/constraints.go
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
}

0 comments on commit 5619a5e

Please sign in to comment.