Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: add a way to write portable feature tests #1597

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ jobs:
needs: build-and-lint
timeout-minutes: 15
env:
EBPF_TEST_IGNORE_KERNEL_VERSION: 'TestKprobeMulti,TestKprobeMultiErrors,TestKprobeMultiCookie,TestKprobeMultiProgramCall,TestHaveBPFLinkKprobeMulti'
EBPF_TEST_IGNORE_VERSION: 'TestKprobeMulti,TestKprobeMultiErrors,TestKprobeMultiCookie,TestKprobeMultiProgramCall,TestHaveBPFLinkKprobeMulti'
steps:
- uses: actions/checkout@v4

Expand Down
20 changes: 10 additions & 10 deletions btf/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import (

// haveBTF attempts to load a BTF blob containing an Int. It should pass on any
// kernel that supports BPF_BTF_LOAD.
var haveBTF = internal.NewFeatureTest("BTF", "4.18", func() error {
var haveBTF = internal.NewFeatureTest("BTF", func() error {
// 0-length anonymous integer
err := probeBTF(&Int{})
if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
return internal.ErrNotSupported
}
return err
})
}, "4.18")

// haveMapBTF attempts to load a minimal BTF blob containing a Var. It is
// used as a proxy for .bss, .data and .rodata map support, which generally
// come with a Var and Datasec. These were introduced in Linux 5.2.
var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", "5.2", func() error {
var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", func() error {
if err := haveBTF(); err != nil {
return err
}
Expand All @@ -40,12 +40,12 @@ var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", "5.2", func()
return internal.ErrNotSupported
}
return err
})
}, "5.2")

// haveProgBTF attempts to load a BTF blob containing a Func and FuncProto. It
// is used as a proxy for ext_info (func_info) support, which depends on
// Func(Proto) by definition.
var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", "5.0", func() error {
var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", func() error {
if err := haveBTF(); err != nil {
return err
}
Expand All @@ -60,9 +60,9 @@ var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", "5.0",
return internal.ErrNotSupported
}
return err
})
}, "5.0")

var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", "5.6", func() error {
var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", func() error {
if err := haveProgBTF(); err != nil {
return err
}
Expand All @@ -78,9 +78,9 @@ var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", "5.6", func()
return internal.ErrNotSupported
}
return err
})
}, "5.6")

var haveEnum64 = internal.NewFeatureTest("ENUM64", "6.0", func() error {
var haveEnum64 = internal.NewFeatureTest("ENUM64", func() error {
if err := haveBTF(); err != nil {
return err
}
Expand All @@ -97,7 +97,7 @@ var haveEnum64 = internal.NewFeatureTest("ENUM64", "6.0", func() error {
return internal.ErrNotSupported
}
return err
})
}, "6.0")

func probeBTF(typ Type) error {
b, err := NewBuilder([]Type{typ})
Expand Down
16 changes: 8 additions & 8 deletions features/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func HaveLargeInstructions() error {
return haveLargeInstructions()
}

var haveLargeInstructions = internal.NewFeatureTest(">4096 instructions", "5.2", func() error {
var haveLargeInstructions = internal.NewFeatureTest(">4096 instructions", func() error {
const maxInsns = 4096

insns := make(asm.Instructions, maxInsns, maxInsns+1)
Expand All @@ -29,7 +29,7 @@ var haveLargeInstructions = internal.NewFeatureTest(">4096 instructions", "5.2",
Type: ebpf.SocketFilter,
Instructions: insns,
})
})
}, "5.2")

// HaveBoundedLoops probes the running kernel if bounded loops are supported.
//
Expand All @@ -40,7 +40,7 @@ func HaveBoundedLoops() error {
return haveBoundedLoops()
}

var haveBoundedLoops = internal.NewFeatureTest("bounded loops", "5.3", func() error {
var haveBoundedLoops = internal.NewFeatureTest("bounded loops", func() error {
return probeProgram(&ebpf.ProgramSpec{
Type: ebpf.SocketFilter,
Instructions: asm.Instructions{
Expand All @@ -50,7 +50,7 @@ var haveBoundedLoops = internal.NewFeatureTest("bounded loops", "5.3", func() er
asm.Return(),
},
})
})
}, "5.3")

// HaveV2ISA probes the running kernel if instructions of the v2 ISA are supported.
//
Expand All @@ -61,7 +61,7 @@ func HaveV2ISA() error {
return haveV2ISA()
}

var haveV2ISA = internal.NewFeatureTest("v2 ISA", "4.14", func() error {
var haveV2ISA = internal.NewFeatureTest("v2 ISA", func() error {
return probeProgram(&ebpf.ProgramSpec{
Type: ebpf.SocketFilter,
Instructions: asm.Instructions{
Expand All @@ -71,7 +71,7 @@ var haveV2ISA = internal.NewFeatureTest("v2 ISA", "4.14", func() error {
asm.Return().WithSymbol("exit"),
},
})
})
}, "4.14")

// HaveV3ISA probes the running kernel if instructions of the v3 ISA are supported.
//
Expand All @@ -82,7 +82,7 @@ func HaveV3ISA() error {
return haveV3ISA()
}

var haveV3ISA = internal.NewFeatureTest("v3 ISA", "5.1", func() error {
var haveV3ISA = internal.NewFeatureTest("v3 ISA", func() error {
return probeProgram(&ebpf.ProgramSpec{
Type: ebpf.SocketFilter,
Instructions: asm.Instructions{
Expand All @@ -92,4 +92,4 @@ var haveV3ISA = internal.NewFeatureTest("v3 ISA", "5.1", func() error {
asm.Return().WithSymbol("exit"),
},
})
})
}, "5.1")
4 changes: 2 additions & 2 deletions info.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ func EnableStats(which uint32) (io.Closer, error) {
return fd, nil
}

var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", "4.15", func() error {
var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", func() error {
prog, err := progLoad(asm.Instructions{
asm.LoadImm(asm.R0, 0, asm.DWord),
asm.Return(),
Expand All @@ -669,4 +669,4 @@ var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", "
}

return err
})
}, "4.15")
55 changes: 49 additions & 6 deletions internal/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@ package internal
import (
"errors"
"fmt"
"runtime"
"strings"
"sync"
)

// ErrNotSupported indicates that a feature is not supported by the current kernel.
// ErrNotSupported indicates that a feature is not supported.
var ErrNotSupported = errors.New("not supported")

// ErrNotSupportedOnOS indicates that a feature is not supported on the current
// operating system.
var ErrNotSupportedOnOS = fmt.Errorf("%w on %s", ErrNotSupported, runtime.GOOS)

// UnsupportedFeatureError is returned by FeatureTest() functions.
type UnsupportedFeatureError struct {
// The minimum Linux mainline version required for this feature.
// The minimum version required for this feature.
//
// On Linux this refers to the mainline kernel version, on other platforms
// to the version of the runtime.
//
// Used for the error string, and for sanity checking during testing.
MinimumVersion Version

Expand Down Expand Up @@ -58,11 +68,44 @@ type FeatureTest struct {
type FeatureTestFn func() error

// NewFeatureTest is a convenient way to create a single [FeatureTest].
func NewFeatureTest(name, version string, fn FeatureTestFn) func() error {
//
// versions specifies in which version of a BPF runtime a feature appeared.
// The format is "GOOS:Major.Minor[.Patch]". GOOS may be omitted when targeting
// Linux. Returns [ErrNotSupportedOnOS] if there is no version specified for the
// current OS.
func NewFeatureTest(name string, fn FeatureTestFn, versions ...string) func() error {
const nativePrefix = runtime.GOOS + ":"

if len(versions) == 0 {
return func() error {
return fmt.Errorf("feature test %q: no versions specified", name)
}
}

ft := &FeatureTest{
Name: name,
Version: version,
Fn: fn,
Name: name,
Fn: fn,
}

for _, version := range versions {
if strings.HasPrefix(version, nativePrefix) {
ft.Version = strings.TrimPrefix(version, nativePrefix)
break
}

if runtime.GOOS == "linux" && !strings.ContainsRune(version, ':') {
// Allow version numbers without a GOOS prefix on Linux.
ft.Version = version
break
}
}

if ft.Version == "" {
return func() error {
// We don't return an UnsupportedFeatureError here, since that will
// trigger version checks which don't make sense.
return fmt.Errorf("%s: %w", name, ErrNotSupportedOnOS)
}
}

return ft.execute
Expand Down
34 changes: 26 additions & 8 deletions internal/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package internal

import (
"errors"
"runtime"
"strings"
"testing"

"github.com/go-quicktest/qt"

"github.com/cilium/ebpf/internal/testutils/fdtrace"
)

Expand All @@ -15,27 +18,30 @@ func TestMain(m *testing.M) {
func TestFeatureTest(t *testing.T) {
var called bool

fn := NewFeatureTest("foo", "1.0", func() error {
fn := NewFeatureTest("foo", func() error {
called = true
return nil
})
}, "1.0")

if called {
t.Error("Function was called too early")
}

err := fn()
if !called {
t.Error("Function wasn't called")
if errors.Is(err, ErrNotSupportedOnOS) {
qt.Assert(t, qt.IsFalse(called))
return
}

qt.Assert(t, qt.IsTrue(called), qt.Commentf("function should be invoked"))

if err != nil {
t.Error("Unexpected negative result:", err)
}

fn = NewFeatureTest("bar", "2.1.1", func() error {
fn = NewFeatureTest("bar", func() error {
return ErrNotSupported
})
}, "2.1.1")

err = fn()
if err == nil {
Expand All @@ -60,12 +66,24 @@ func TestFeatureTest(t *testing.T) {
t.Error("Didn't cache an error wrapping ErrNotSupported")
}

fn = NewFeatureTest("bar", "2.1.1", func() error {
fn = NewFeatureTest("bar", func() error {
return errors.New("foo")
})
}, "2.1.1")

err1, err2 := fn(), fn()
if err1 == err2 {
t.Error("Cached result of unsuccessful execution")
}
}

func TestFeatureTestNotSupportedOnOS(t *testing.T) {
sentinel := errors.New("quux")
fn := func() error { return sentinel }

qt.Assert(t, qt.IsNotNil(NewFeatureTest("foo", fn)()))
qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, "froz:1.0.0")(), ErrNotSupportedOnOS))
qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, runtime.GOOS+":1.0")(), sentinel))
if runtime.GOOS == "linux" {
qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, "1.0")(), sentinel))
}
}
38 changes: 27 additions & 11 deletions internal/testutils/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

const (
ignoreKernelVersionEnvVar = "EBPF_TEST_IGNORE_KERNEL_VERSION"
ignoreVersionEnvVar = "EBPF_TEST_IGNORE_VERSION"
)

func CheckFeatureTest(t *testing.T, fn func() error) {
Expand All @@ -25,7 +25,7 @@ func checkFeatureTestError(t *testing.T, err error) {

var ufe *internal.UnsupportedFeatureError
if errors.As(err, &ufe) {
checkKernelVersion(t, ufe)
checkVersion(t, ufe)
} else {
t.Error("Feature test failed:", err)
}
Expand All @@ -50,23 +50,36 @@ func SkipIfNotSupported(tb testing.TB, err error) {

var ufe *internal.UnsupportedFeatureError
if errors.As(err, &ufe) {
checkKernelVersion(tb, ufe)
checkVersion(tb, ufe)
tb.Skip(ufe.Error())
}
if errors.Is(err, internal.ErrNotSupported) {
tb.Skip(err.Error())
}
}

func checkKernelVersion(tb testing.TB, ufe *internal.UnsupportedFeatureError) {
func SkipIfNotSupportedOnOS(tb testing.TB, err error) {
tb.Helper()

if err == internal.ErrNotSupportedOnOS {
tb.Fatal("Unwrapped ErrNotSupportedOnOS")
}

if errors.Is(err, internal.ErrNotSupportedOnOS) {
tb.Skip(err.Error())
}
}

func checkVersion(tb testing.TB, ufe *internal.UnsupportedFeatureError) {
if ufe.MinimumVersion.Unspecified() {
return
}

tb.Helper()

if ignoreKernelVersionCheck(tb.Name()) {
tb.Skipf("Ignoring error due to %s: %s", ignoreKernelVersionEnvVar, ufe.Error())
if ignoreVersionCheck(tb.Name()) {
tb.Logf("Ignoring error due to %s: %s", ignoreVersionEnvVar, ufe.Error())
return
}

if !isKernelLessThan(tb, ufe.MinimumVersion) {
Expand Down Expand Up @@ -121,12 +134,15 @@ func kernelVersion(tb testing.TB) internal.Version {
return v
}

// ignoreKernelVersionCheck checks if test name should be ignored for kernel version check by checking against environment var EBPF_TEST_IGNORE_KERNEL_VERSION.
// EBPF_TEST_IGNORE_KERNEL_VERSION is a comma (,) separated list of test names for which kernel version check should be ignored.
// ignoreVersionCheck checks whether to omit the version check for a test.
//
// It reads a comma separated list of test names from an environment variable.
//
// For example:
//
// eg: EBPF_TEST_IGNORE_KERNEL_VERSION=TestABC,TestXYZ
func ignoreKernelVersionCheck(tName string) bool {
tNames := os.Getenv(ignoreKernelVersionEnvVar)
// EBPF_TEST_IGNORE_VERSION=TestABC,TestXYZ go test ...
func ignoreVersionCheck(tName string) bool {
tNames := os.Getenv(ignoreVersionEnvVar)
if tNames == "" {
return false
}
Expand Down
Loading