Skip to content
Merged
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
276 changes: 181 additions & 95 deletions testing/ebpftest/README.md
Original file line number Diff line number Diff line change
@@ -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 = <specs of the eBPF programs provided by Cilium/ebpf>
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 <bpf/bpf_helpers.h>
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, &params) < 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`).