Skip to content

Commit

Permalink
skip: introduce a skip package with utilities for skipping test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
shoenig committed Apr 1, 2024
1 parent 2ceae04 commit 0aa41bd
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 1 deletion.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@

`test` is a modern and generics oriented testing assertions library for Go.

There are four key packages,
There are five key packages,

- `must` - assertions causing test failure and halt the test case immediately
- `test` - assertions causing test failure and allow the test case to continue
- `wait` - utilities for waiting on conditionals in tests
- `skip` - utilities for skipping test cases in some situations
- `portal` - utilities for allocating free ports for network listeners in tests

### Changes

:ballot_box_with_check: v1.8.0 introduces the `skip` package for skipping tests!

- New helper functions for skipping out tests based on environment detection

:ballot_box_with_check: v1.7.0 marks the first stable release!

- Going forward no breaking changes will be made without a v2 major version
Expand Down Expand Up @@ -155,6 +160,21 @@ must.Eq(t, exp, result, must.Func(func() string {
})
```

### Skip

Sometimes it makes sense to just skip running a certain test case. Maybe the
operating system is all wrong or a certain necessary command is not currently
installed. The `skip` package provides utilities for skipping tests under some
given circumstances.

```go
skip.OperatingSystem(t, "windows")
```

```go
skip.CommandNotFound(t, "java")
```

### Wait

Sometimes a test needs to wait on a condition for a non-deterministic amount of time.
Expand Down
179 changes: 179 additions & 0 deletions skip/skip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright (c) The Test Authors
// SPDX-License-Identifier: MPL-2.0

// Package skip provides helper functions for skipping test cases in
// environments that meet certain conditions.
package skip

import (
"context"
"errors"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"testing"
"time"
)

// OperatingSystem will skip the test if the Go runtime detects the operating system
// matches one of the given names.
func OperatingSystem(t *testing.T, names ...string) {
os := runtime.GOOS
for _, name := range names {
if os == strings.ToLower(name) {
t.Skipf("operating system excluded from tests %q", os)
}
}
}

// NotOperatingSystem will skip the test if the Go runtime detects the operating
// system does not match one of the given names.
func NotOperatingSystem(t *testing.T, names ...string) {
os := runtime.GOOS
for _, name := range names {
if os == strings.ToLower(name) {
return
}
}
t.Skipf("operating excluded from tests %q", os)
}

// RootUser will skip the test if the test is being run as the root user.
//
// Uses the effective UID value to determine user.
func RootUser(t *testing.T) {
euid := os.Geteuid()
if euid == 0 {
t.Skip("test must not run as root")
}
}

// NotRootUser will skip the test if the test is not being run as the root user.
//
// Uses the effective UID value to determine user.
func NotRootUser(t *testing.T) {
euid := os.Geteuid()
if euid != 0 {
t.Skip("test must run as root")
}
}

// Architecture will skip the test if the Go runtime detects the system
// architecture matches one of the given names.
func Architecture(t *testing.T, names ...string) {
arch := runtime.GOARCH
for _, name := range names {
if arch == strings.ToLower(name) {
t.Skipf("arch excluded from tests %q", arch)
}
}
}

// NotArchitecture will skip the test if the Go runtime the system architecture
// does not match one of the given names.
func NotArchitecture(t *testing.T, names ...string) {
arch := runtime.GOARCH
for _, name := range names {
if arch == strings.ToLower(name) {
return
}
}
t.Skipf("arch excluded from tests %q", arch)
}

func cmdAvailable(name string) bool {
_, err := exec.LookPath(name)
return !errors.Is(err, exec.ErrNotFound)
}

// CommandNotFound will skip the test if the given command cannot be found on
// the system.
func CommandNotFound(t *testing.T, command string) {
if !cmdAvailable(command) {
t.Skipf("command %q not detected on system", command)
}
}

// DockerNotFound will skip the test if the docker command cannot be found on
// the system path.
func DockerNotFound(t *testing.T) {
if !cmdAvailable("docker") {
t.Skip("docker not detected on system")
}
}

// PodmanNotFound will skip the test if the podman command cannot be found on
// the system path.
func PodmanNotFound(t *testing.T) {
if !cmdAvailable("podman") {
t.Skip("podman not detected on system")
}
}

// MinimumCores will skip the test if the system does not meet the minimum
// number of CPU cores.
func MinimumCores(t *testing.T, num int) {
cpus := runtime.NumCPU()
if cpus < num {
t.Skip("system does not meet minimum cpu cores")
}
}

// MaximumCores will skip the test if the system does not meet the maximum
// number of cpu cores.
func MaximumCores(t *testing.T, num int) {
cpus := runtime.NumCPU()
if cpus > num {
t.Skip("system exceeds maximum cpu cores")
}
}

// CgroupsVersion will skip the test if the system does not match the given
// cgroups version.
func CgroupsVersion(t *testing.T, version int) {
if runtime.GOOS != "linux" {
t.Skip("cgroups requires linux")
}

mType := mountType(t, "/sys/fs/cgroup")

switch mType {
case "tmpfs":
// this is a cgroups v1 system
if version == 2 {
t.Skip("system does not match cgroups version 2")
}
case "cgroup2":
// this is a cgroups v2 system
if version == 1 {
t.Skip("system does not match cgroups version 1")
}
default:
t.Fatalf("unknown cgroups mount type %q", mType)
}
}

func mountType(t *testing.T, path string) string {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, "df", "-T", path)
b, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("unable to run df command: %v", err)
}

// need the first token of the second line
output := string(b)
tokenRe := regexp.MustCompile(`on\s+([\w]+)\s+`)
results := tokenRe.FindStringSubmatch(output)
if len(results) != 2 {
t.Fatal("no mount type for path")
}
return results[1]
}

// TODO
// - landlock detection
73 changes: 73 additions & 0 deletions skip/skip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) The Test Authors
// SPDX-License-Identifier: MPL-2.0

//go:build linux || macos

package skip

import "testing"

func TestSkip_OperatingSystem(t *testing.T) {
OperatingSystem(t, "darwin", "linux", "windows")
t.Fatal("expected to skip test")
}

func TestSkip_NotOperatingSystem(t *testing.T) {
NotOperatingSystem(t, "windows")
t.Fatal("expected to skip test")
}

func TestSkip_RootUser(t *testing.T) {
t.Skip("requires root")
RootUser(t)
t.Fatal("expected to skip test")
}

func TestSkip_NotRootUser(t *testing.T) {
NotRootUser(t)
t.Fatal("expected to skip test")
}

func TestSkip_Architecture(t *testing.T) {
Architecture(t, "arm64", "amd64")
t.Fatal("expected to skip test")
}

func TestSkip_NotArchitecture(t *testing.T) {
NotArchitecture(t, "itanium", "mips")
t.Fatal("expected to skip test")
}

func TestSkip_DockerNotFound(t *testing.T) {
t.Skip("skip docker test") // gha runner

DockerNotFound(t)
t.Fatal("expected to skip test")
}

func TestSkip_PodmanNotFound(t *testing.T) {
t.Skip("skip podman test") // gha runner

PodmanNotFound(t)
t.Fatal("expected to skip test")
}

func TestSkip_CommandNotFound(t *testing.T) {
CommandNotFound(t, "doesnotexist")
t.Fatal("expected to skip test")
}

func TestSkip_MinimumCores(t *testing.T) {
MinimumCores(t, 200)
t.Fatal("expected to skip test")
}

func TestSkip_MaximumCores(t *testing.T) {
MaximumCores(t, 2)
t.Fatal("expected to skip test")
}

func TestSkip_CgroupsVersion(t *testing.T) {
CgroupsVersion(t, 1)
t.Fatal("expected to skip test")
}

0 comments on commit 0aa41bd

Please sign in to comment.