-
Notifications
You must be signed in to change notification settings - Fork 2
plugin testing
Pre-Alpha. This page describes behavior that may change.
The Ze plugin SDK is designed to be testable. The net.Pipe() pair is the usual trick: one end becomes the plugin, the other end is the test driving the engine side. You simulate the five-stage startup, deliver events, execute commands, and assert against what the plugin did. No real daemon needed.
This page covers the patterns that work. The full in-tree testing guide is at main/docs/plugin-development/testing.md.
Every test starts with a net.Pipe() and an sdk.NewWithConn() call. A small helper factors out the boilerplate.
import (
"net"
"testing"
"codeberg.org/thomas-mangin/ze/pkg/plugin/rpc"
"codeberg.org/thomas-mangin/ze/pkg/plugin/sdk"
)
func newTestPair(t *testing.T) (*sdk.Plugin, *rpc.MuxConn) {
t.Helper()
pluginEnd, engineEnd := net.Pipe()
t.Cleanup(func() {
pluginEnd.Close()
engineEnd.Close()
})
p := sdk.NewWithConn("test-plugin", pluginEnd)
engineConn := rpc.NewConn(engineEnd, engineEnd)
engineMux := rpc.NewMuxConn(engineConn)
t.Cleanup(func() { engineMux.Close() })
return p, engineMux
}engineMux is the engine-side driver. You use it to send callbacks into the plugin (CallRPC) and to read the plugin's outgoing requests (Requests()).
The pattern is four steps: register the callback, run the plugin in a goroutine, drive the startup from the engine side, and deliver an event.
func TestEventHandler(t *testing.T) {
p, engineMux := newTestPair(t)
eventReceived := make(chan string, 1)
p.OnEvent(func(event string) error {
eventReceived <- event
return nil
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
errCh := make(chan error, 1)
go func() {
errCh <- p.Run(ctx, sdk.Registration{})
}()
completeStartup(t, ctx, engineMux)
eventInput := struct {
Event string `json:"event"`
}{
Event: `{"type":"bgp","bgp":{"peer":{"address":"10.0.0.1"}}}`,
}
_, err := engineMux.CallRPC(ctx, "ze-plugin-callback:deliver-event", eventInput)
require.NoError(t, err)
select {
case got := <-eventReceived:
assert.Contains(t, got, "10.0.0.1")
case <-time.After(time.Second):
t.Fatal("event callback not called")
}
// Shutdown cleanly.
byeInput := struct {
Reason string `json:"reason"`
}{Reason: "test-done"}
_, _ = engineMux.CallRPC(ctx, "ze-plugin-callback:bye", byeInput)
require.NoError(t, <-errCh)
}completeStartup drives stages 1 through 5. The helper lives in pkg/plugin/sdk/sdk_test.go and is the one you borrow when writing new tests.
The handful of things that pay off most.
The happy path for every command. OnExecuteCommand with each registered command name, with valid arguments, asserting on the (status, data) return.
The error paths for every command. Missing arguments, invalid values, unknown commands. Assert on the "error" status string, not on a Go error.
OnConfigure with your YANG. Load your YANG file, feed a matching JSON payload through CallRPC("ze-plugin-callback:configure", ...), assert that your plugin's internal state reflects the config.
Event subscription. If your plugin subscribes to events at startup (SetStartupSubscriptions), assert that the subscription made it into the stage-5 ready payload that the plugin sent.
OnConfigVerify rejects bad config. The engine's reload path depends on this. A verify handler that never rejects anything does not actually protect you.
OnBye. The plugin should release its external resources on shutdown. Deliver a bye and assert that the hook ran.
Things that belong in a different test layer:
- The wire format of the plugin protocol. That is
pkg/plugin/rpc's job. - The 5-stage handshake itself. The SDK test suite already covers it.
- Integration with a real daemon. Use the functional test suite (
ze-test) for that. It spins up a real Ze and drives it through real commands.
When you want to test your plugin against the real engine, ze-test is the driver. It has a wait-established synchroniser that lets you write a test that does not race the FSM.
ze-test mcp --wait-established ...
ze-test functional my-plugin ...The functional tests live under main/test/. They are slower than the SDK unit tests, and they test real behaviour against a compiled binary, which is exactly what you want before shipping a plugin.
If your plugin parses external input (NLRI bytes, capability payloads, JSON from another process), write a fuzz harness. Ze has project-wide fuzz testing and the plugin's parser is no exception. The pattern is the same as any Go fuzz target.
func FuzzDecodeNLRI(f *testing.F) {
f.Add([]byte{0x00, 0x18, 0x0a, 0x00, 0x00})
f.Fuzz(func(t *testing.T, data []byte) {
_, _ = decodeNLRI(data) // Must not panic.
})
}make ze-fuzz-test runs every fuzz target in the tree.
-
Handlers for the
On*callbacks you will test. - Commands for the command surface.
- Go plugins for the SDK reference.
- In-tree testing guide for the full version.
Adapted from main/docs/plugin-development/testing.md.
Unreviewed draft. This wiki was authored in bulk and has not been reviewed. File corrections on the issue tracker.
- Overview
- YANG Model
- Editor Workflow
- Archive and Rollback
- System
- Interfaces
- BFD
- FIB
- Firewall
- Traffic Control
- L2TP/PPP
- VPP Data Plane
- RPKI
- TACACS+ AAA
- Fleet
- BGP
- Starting and Stopping
- Show Commands
- Monitoring
- Logging
- Operational Reports
- Healthcheck
- MRT Analysis
- Upgrade and Restart
- Storage
- Policy
- Core
- Resilience
- Validation
- Capabilities
- Address Families
- Protocol
- Subsystems
- Infrastructure
- Route Server at an IXP
- Transit Edge with RPKI
- Public Looking Glass
- ExaBGP Migration Walkthrough
- FlowSpec Injection
- Chaos-Tested Peering
- AS Path Topology