From 4a3ac162f3f65d209d72aae2678de150af8edaab Mon Sep 17 00:00:00 2001 From: Nick Zavaritsky Date: Sun, 28 Sep 2025 16:48:38 +0200 Subject: [PATCH] ebpftest: update docs Signed-off-by: Nick Zavaritsky --- testing/ebpftest/README.md | 276 ++++++++++++++++++++++++------------- 1 file changed, 181 insertions(+), 95 deletions(-) diff --git a/testing/ebpftest/README.md b/testing/ebpftest/README.md index 122dd40..e0f0eb0 100644 --- a/testing/ebpftest/README.md +++ b/testing/ebpftest/README.md @@ -1,143 +1,229 @@ # ebpftest -The `ebpftest` package contains golang utilities and helpers that simplify the testing of eBPF programs from golang userpace. +`ebpftest` package contains Golang utilities and helpers that simplify testing of eBPF programs from Golang userpace. -This package contains: -- `Mock` that brings mocking functionality to eBPF -- helpers to be used in tests, such as `RequireVariableEqual` and `RequireNoError` +One can invoke many eBPF program types using [`BPF_PROG_TEST_RUN`](https://docs.ebpf.io/linux/syscall/BPF_PROG_TEST_RUN/) kernel feature. +It is exposed as `.Test` and `.Run` methods in `cilium/ebpf`'s `ebpf.Program`. +This package additionally offers: +- helpers to be used in tests, such as `SkipIfIncapable`, `RequireNoError`, and `RequireVariableEqual`; +- `Mock` that brings mocking functionality to eBPF. -## Effortlessly mocking eBPF programs +## SkipIfIncapable -This package allows golang tests to replace a eBPF program or sub-program (eBPF function) on-demand with a stub, without having to modify eBPF code. -In addition, the user can: -- assert that the mock has been called -- assert input arguments -- specify the return value of the mock +Skips current test if not running as `root`. +```go +func TestFrob(t *testing.T) { + ebpftest.SkipIfIncapable(t) + ... +} +``` -### Usage +Many eBPF features are unavailable unless running as `root` (or bearing additional +capabilities such as [`CAP_BPF`](https://man7.org/linux/man-pages/man7/capabilities.7.html)). +Golang developers have a habit of running tests in a project in bulk via `go test ./...` +Gate eBPF tests with `SkipIfIncapable` to avoid misleading failures. -The following example will replace eBPF functions `foo` and `bar` with mocks. -```golang -func Test(t *testing.T) { +Note: one can run eBPF tests with `go test -exec sudo`. +Another option to consider is [`vimto`](https://github.com/lmb/vimto/tree/main). +It is more secure, allows to use a specific kernel version, and is reasonably fast +as long as KVM-based virtualization is available. - type FunctionsToMock struct { - Foo func() `ebpf:"foo"` - Bar func(any, any) `ebpf:"bar"` - } +## RequireNoError - collectionSpec = - mock = ebpftest.NewMock[FunctionsToMock](t, collectionSpec) -} +Asserts that a function returned no error, similar to `testify`'s +[`require.NoError`](https://pkg.go.dev/github.com/stretchr/testify/require#NoErrorf). +We suggest to use this helper when loading eBPF programs +(either separately or as a part of a collection). + +`ebpf.VerifierError` from `cilium/ebpf` outputs condensed summary when formatted. +This helper ensures that a complete verifier log is captured to aid troubleshooting. + +## SetVariable and RequireVariableEqual + +Manipulate global eBPF variables. + +`cilium/ebpf` exposes global variables +as `ebpf.Variable` instances. They are available in `ebpf.Collection` and are also included +in structures generated by `bpf2go`. + +Variables are handy when testing individual bits of eBPF code unit-test style. +Imagine we wish to excercise a function such as + +```c +static int froblinate(const struct foo *input, struct bar *output); ``` -One can then set the expectation that a mock gets called: -```golang -mock.Expect().Foo() +Unfortunately, `froblinate` is not a proper eBPF program, +we need a wrapper: + +```c +struct foo test_input; +struct bar test_output. +SEC("tc") int test_froblinate(struct __sk_buff *ctx) { + return froblinate(&test_input, &test_output); +} ``` -One can also set expectations on all (or some) of the input arguments -```golang -mock.Expect().Bar(uint32(42), ebpftest.Any) +Use `SetVariable` on `TestInput` to provide test input. + +Use `RequireVaribaleEqual` on `TestOutput` to verify the result. + + +## Mock + +eBPF code can get complex. As with other software, +it is a good idea to cover individual units with tests in addition +to excercising the whole thing end to end. +It would be nice if we could replace some calls with stubs when testing complex +functions. + +This package allows Golang tests to replace a eBPF program or sub-program on-demand with a stub, without having to modify eBPF code. +In addition, the user can: +- assert that a stub has been called; +- assert input arguments; +- specify the return value of the stub. + +### Mocking tail calls + +"A [tail call](https://docs.ebpf.io/linux/concepts/tail-calls/) +is a mechanism that allows eBPF authors to break up their logic into multiple parts and go from one to the other." + +```c +SEC("tc") int foo(struct __sk_buff *ctx) { + return TCX_PASS; +} + +struct { + __uint(type, BPF_MAP_TYPE_PROG_ARRAY); + __uint(max_entries, 1); + __type(key, int); + __type(value, int); + __array(values, int (struct __sk_buff *)); +} progs SEC(".maps") = { + .values = { foo } +}; + +SEC("tc") int mainfunc(struct __sk_buff *ctx) { + // complex logic HERE + + bpf_tail_call(ctx, &progs, 0); // jumps to foo + return TCX_DROP; +} ``` -Mocks return 0 by default. One can set a different return value by declaring a function with `ebpftest.Res[T]` result: -```golang +A supposedly complex `mainfunc` defers to `foo` for certain inputs. + +We wish to excercise `mainfunc` in isolation for a more robust and focused test. `Mock` +can replace `foo` with a stub, enabling the test to assert that `foo` was called. +The real `foo` is skipped, the test observes the resulting packet exactly as `mainfunc` left it. + +To set up a mock, list functions to intercept: + +```go type FunctionsToMock struct { - Foo func() ebpftest.Res[uint32] `ebpf:"foo"` + Foo func(any) `ebpf:"foo"` } +``` + +Use `ebpftest.NewMock` to instantiate a mock. It rewrites the spec to implement intercepts: -mock.Expect().Foo().Return(42) +```go +func Test(t *testing.T) { + var spec *ebpf.CollectionSpec + spec = loadSpec() + mock = ebpftest.NewMock[FunctionsToMock](t, spec) + ... + mock.Expect().Foo(ebpftest.Any) +} ``` -### Example +Use the augmented spec to instantiate eBPF objects as usual. +Communicate expected calls with `mock.Expect()`. -Here is a full example, showcasing both the eBPF part and the golang part. -```C -// test.c -#include +Note: `FunctionsToMock` includes as many functions as needed. +A test fails if an expected call is missing or an unexpected call was performed. -__noinline int foo(void) -{ - int res = 42; - barrier_var(res); - return res; -} +### Mocking subprograms + +One can break up the logic into multiple parts with regular functions as well. +The feature is known as "eBPF subprograms", available since kernel 5.2: -struct custom_struct { - int member; +```c +struct params { + int bar; }; -__noinline static int bar(int param1, struct custom_struct *param2) -{ - barrier_var(param1); - barrier_var(param2); - return param1 + param2.member; +int foo(struct __sk_buff *ctx, struct params *params) { + return 0; } -SEC("tc") -int test(void *ctx) -{ - int x = foo(); - barrier_var(x); - struct custom_struct y = {.member = 24}; - return bar(x, &y); +SEC("tc") int mainfunc(struct __sk_buff *ctx) { + // complex logic HERE + + struct params params = { .bar = 42 }; + if (foo(ctx, ¶ms) < 0) { + return TCX_DROP; + } + return TCX_PASS; } ``` -After generating the golang bindings, e.g. with -``` -go run github.com/cilium/ebpf/cmd/bpf2go t test.c -``` -the userspace can mock `foo` and `bar` and set expectations that the mocks get called. -```golang -type customStruct struct { - Member uint32 + +List functions to intercept, similarly to **mocking tail calls**. +The number of function parameters must match eBPF definition: + +```go +type FunctionsToMock struct { + Foo func(any, any) `ebpf:"foo"` } +``` + +Use `ebpftest.NewMock` to instantiate a mock. It rewrites the spec to implement intercepts: -type mockedFuncs struct { - Foo func() Res[uint32] `ebpf:"foo"` - Bar func(uint32, *customStruct) `ebpf:"bar"` +```go +type Params struct { + Bar int32 } -func TestMock(t *testing.T) { - spec, err := loadT() // loads specs generated by ebpf2go +func Test(t *testing.T) { + var spec *ebpf.CollectionSpec + spec = loadSpec() + mock = ebpftest.NewMock[FunctionsToMock](t, spec) + ... + mock.Expect().Foo(ebpftest.Any, &Params{Bar: 42}) +} +``` - mock := NewMock[mockedFuncs](t, spec) +Use the augmented spec to instantiate eBPF objects as usual. +Communicate expected calls with `mock.Expect()`: - var prog struct { - *ebpf.Program `ebpf:"test"` - } - ebpftest.RequireNoError(t, spec.LoadAndAssign(&prog, nil)) - defer prog.Close() +```go +mock.Expect().Foo(ebpftest.Any, &Params{Bar: 42}) +``` - // Register expectations - mock.Expect().Foo() - mock.Expect().Bar(0, &foo{Member: 24}) +Stubs return 0 by default. One can set a different return value by declaring a function with `ebpftest.Res[T]` result. +To simulate `foo` failure: - /* Run the program - Note 1: the kernel expects at least 14 bytes input for XDP and SKB programs. - Note 2: one can also use prog.Run - */ - _,_,_ = prog.Test(make([]byte, 14)) - // Register new expectations. This time, the Foo mock returns a custom value. - mock.Reset() - mock.Expect().Foo().Return(1) - mock.Expect().Bar(1, &foo{Member: 24}) - _,_,_ = prog.Test(make([]byte, 14)) +```go +type FunctionsToMock struct { + Foo func(any, any) ebpftest.Res[int32] `ebpf:"foo"` } + +mock.Expect().Foo(ebpftest.Any, ebpftest.Any).Return(-1) ``` -If they user is not interested in setting expectations on mock arguments, one can define mocks and set expectations as following: -```golang +So far we declared parameters in `FunctionsToMock` as `any`. +Declaring a paramater with a specific type instead of `any` is also an option. +It makes a test easier to maintain thanks to static type checking, but `ebpftest.Any` is no longer accepted: + +```go type FunctionsToMock struct { - Bar func(any, any) `ebpf:"bar"` + Foo func(any, *Params) `ebpf:"foo"` } -mock.Expect().Bar(ebpftest.Any, ebpftest.Any) ``` ### Limitations -- can't mock inlined functions. -- mocked calls are evaluated once. If the user wants to setup another expectation on a mock `m`, they can call `m.Reset()`, followed by a new `m.Expect()` -- mocks are simple stubs, can't have custom eBPF code as part of the mock (i.e. no equivalent of `.Do` like in `gomock`). +- It is only possible to assert that a function was called once or was not called at all. +- No custom logic in stubs (i.e. no equivalent of `.Do` like in `gomock`).