tests/
suite.rs

1// SPDX-License-Identifier: Apache-2.0
2use std::{path::PathBuf, sync::OnceLock};
3
4use anyhow::Result;
5use libbpf_rs::skel::Skel;
6use libtest_mimic::{Arguments, Failed, Trial};
7
8use super::state::{linux, rust, BPFState};
9
10/// Convenience macro for creating a [libtest_mimic::Trial] with
11/// the stringified name of the test function for use when printing
12#[macro_export]
13macro_rules! create_test {
14    ($func: path) => {
15        libtest_mimic::Trial::test(stringify!($func), move || $func())
16    };
17}
18
19/// Aggregated state after the Rust userspace is loaded from different
20/// perspectives that tests can use to determine if the expected output
21/// has been met.
22#[derive(PartialEq)]
23pub struct TestSystemState {
24    /// State gathered at runtime from the Rust program
25    pub rust_state: BPFState,
26    /// State gathered at runtime from Linux (bash, bpftool, etc.)
27    pub linux_state: BPFState,
28    /// State gathered from static file that is always true
29    pub ground_truth: BPFState,
30}
31
32impl TestSystemState {
33    /// Records the SeaBee state from the Linux and Rust perspectives
34    /// and optionally loads the ground truth to be used inside of tests
35    pub fn new(skel: &dyn Skel, pin_dir: &PathBuf, ground_truth_path: &str) -> Result<Self> {
36        let ground_truth = toml::from_str(&std::fs::read_to_string(ground_truth_path)?)?;
37        Ok(Self {
38            rust_state: rust::rust_state(skel, pin_dir, &ground_truth)?,
39            linux_state: linux::linux_state(skel, pin_dir, &ground_truth)?,
40            ground_truth,
41        })
42    }
43}
44
45/// Generic integration testing harness with default implementations for
46/// common test lifecycle management
47pub trait TestSuite {
48    type CustomTestState;
49
50    /// Provides a reference to a static [TestSystemState] needed by individual tests
51    /// since [libtest_mimic] does not provide a way to pass state into tests
52    fn system_state() -> &'static OnceLock<TestSystemState>;
53
54    /// Provides a reference to a static [Self::CustomTestState] needed by individual tests
55    /// since [libtest_mimic] does not provide a way to pass state into tests
56    fn custom_state() -> &'static OnceLock<Self::CustomTestState>;
57
58    /// Provides an iterator of tests to pass to the test harness for
59    /// different threads to run through.
60    fn tests() -> Vec<Trial>;
61
62    /// Provides individual tests an easy way to grab the static [TestSystemState]
63    fn get_system_state() -> Result<&'static TestSystemState, Failed> {
64        match Self::system_state().get() {
65            Some(inner) => Ok(inner),
66            None => Err("You must initialize the static first".into()),
67        }
68    }
69
70    /// Provides individual tests an easy way to grab the static [Self::CustomTestState]
71    fn get_custom_state() -> Result<&'static Self::CustomTestState, Failed> {
72        match Self::custom_state().get() {
73            Some(inner) => Ok(inner),
74            None => Err("You must initialize the static first".into()),
75        }
76    }
77
78    /// Checks whether the runtime state gathered from the Rust program
79    /// or from Linux has changed throughout the lifetime of the tests
80    fn check_args(final_args: TestSystemState) -> Result<(), Failed> {
81        let initial_args = Self::get_system_state()?;
82        if initial_args.ground_truth != final_args.ground_truth {
83            initial_args.ground_truth.diff(&final_args.ground_truth);
84            return Err("ground truth changed during testing".into());
85        }
86        if initial_args.linux_state != final_args.linux_state {
87            initial_args.linux_state.diff(&final_args.linux_state);
88            return Err("linux state changed during testing".into());
89        }
90        if initial_args.rust_state != final_args.rust_state {
91            initial_args.rust_state.diff(&final_args.rust_state);
92            return Err("rust state changed during testing".into());
93        }
94        Ok(())
95    }
96
97    /// Default test harness lifecycle implementation
98    ///
99    /// It is responsible for setting up the Rust userspace as well as checking
100    /// if all tests completed successfully.
101    ///
102    /// When this function exits, the SeaBee destructor will execute and
103    /// unload all of the eBPF programs load
104    fn run_tests(
105        args: &Arguments,
106        system_state: TestSystemState,
107        custom_state: Self::CustomTestState,
108    ) -> Result<(), Failed>
109    where
110        Self::CustomTestState: 'static,
111    {
112        if Self::system_state().set(system_state).is_err() {
113            return Err("Unable to initialize TestSystemState".into());
114        }
115
116        if Self::custom_state().set(custom_state).is_err() {
117            return Err("Unable to initialize CustomTestState".into());
118        }
119
120        let conclusion = libtest_mimic::run(args, Self::tests());
121
122        match conclusion.has_failed() {
123            true => Err("At least one test failed".into()),
124            false => Ok(()),
125        }
126    }
127}