Browse Source

Merge pull request #640 from aya-rs/lossy-conversions

integration-test: Remove integration-test-macros
Alessandro Decina 1 year ago
parent
commit
ed70a47845

+ 26 - 4
Cargo.toml

@@ -1,12 +1,34 @@
 [workspace]
 members = [
-    "aya", "aya-obj", "aya-tool", "aya-log", "aya-log-common", "aya-log-parser", "test/integration-test", "test/integration-test-macros", "xtask",
+    "aya",
+    "aya-obj",
+    "aya-tool",
+    "aya-log",
+    "aya-log-common",
+    "aya-log-parser",
+    "test/integration-test",
+    "xtask",
+
     # macros
-    "aya-bpf-macros", "aya-log-ebpf-macros",
+    "aya-bpf-macros",
+    "aya-log-ebpf-macros",
+
     # ebpf crates
-    "bpf/aya-bpf", "bpf/aya-bpf-bindings", "bpf/aya-log-ebpf", "test/integration-ebpf"
+    "bpf/aya-bpf",
+    "bpf/aya-bpf-bindings",
+    "bpf/aya-log-ebpf",
+    "test/integration-ebpf",
+]
+resolver = "2"
+
+default-members = [
+    "aya",
+    "aya-obj",
+    "aya-tool",
+    "aya-log",
+    "aya-bpf-macros",
+    "aya-log-ebpf-macros",
 ]
-default-members = ["aya", "aya-obj", "aya-tool", "aya-log", "aya-bpf-macros", "aya-log-ebpf-macros"]
 
 [profile.dev]
 panic = "abort"

+ 1 - 1
aya-log-parser/Cargo.toml

@@ -1,7 +1,7 @@
 [package]
 name = "aya-log-parser"
 version = "0.1.11-dev.0"
-edition = "2018"
+edition = "2021"
 
 [dependencies]
 aya-log-common = { path = "../aya-log-common" }

+ 1 - 0
bpf/aya-bpf-cty/Cargo.toml

@@ -1,5 +1,6 @@
 [package]
 authors = ["Jorge Aparicio <[email protected]>"]
+edition = "2021"
 categories = ["embedded", "external-ffi-bindings" ,"no-std"]
 description = "Type aliases to C types like c_int for use with bindgen"
 documentation = "https://docs.rs/cty"

+ 3 - 3
bpf/aya-bpf-cty/src/lib.rs

@@ -46,7 +46,7 @@ mod ad {
     target_arch = "riscv64"
 ))]
 mod ad {
-    pub type c_char = ::c_uchar;
+    pub type c_char = super::c_uchar;
 
     pub type c_int = i32;
     pub type c_uint = u32;
@@ -63,7 +63,7 @@ mod ad {
     target_arch = "xtensa"
 ))]
 mod ad {
-    pub type c_char = ::c_schar;
+    pub type c_char = super::c_schar;
 
     pub type c_int = i32;
     pub type c_uint = u32;
@@ -71,7 +71,7 @@ mod ad {
 
 #[cfg(target_arch = "msp430")]
 mod ad {
-    pub type c_char = ::c_uchar;
+    pub type c_char = super::c_uchar;
 
     pub type c_int = i16;
     pub type c_uint = u16;

+ 11 - 10
test/README.md

@@ -35,19 +35,20 @@ cargo xtask integration-test --libbpf-dir /path/to/libbpf
 
 ### Virtualized
 
-
 ```
 ./test/run.sh /path/to/libbpf
 ```
-### Writing a test
+
+### Writing an integration test
 
 Tests should follow these guidelines:
 
-- Rust eBPF code should live in `integration-ebpf/${NAME}.rs` and included in `integration-ebpf/Cargo.toml`
-- C eBPF code should live in `integration-test/src/bpf/${NAME}.bpf.c`. It's automatically compiled and made available as `${OUT_DIR}/${NAME}.bpf.o`.
-- Any bytecode should be included in the integration test binary using `include_bytes_aligned!`
-- Tests should be added to `integration-test/src/test`
-- You may add a new module, or use an existing one
-- Integration tests must use the `#[integration_test]` macro to be included in the build
-- Test functions should return `anyhow::Result<()>` since this allows the use of `?` to return errors.
-- You may either `panic!` when an assertion fails or `bail!`. The former is preferred since the stack trace will point directly to the failed line.
+- Rust eBPF code should live in `integration-ebpf/${NAME}.rs` and included in
+  `integration-ebpf/Cargo.toml`.
+- C eBPF code should live in `integration-ebpf/src/bpf/${NAME}.bpf.c`. It's automatically compiled
+  and made available as `${OUT_DIR}/${NAME}.bpf.o`.
+- Any bytecode should be included in the integration test binary using `include_bytes_aligned!`.
+- Tests should be added to `integration-test/tests`.
+- You may add a new module, or use an existing one.
+- Test functions should not return `anyhow::Result<()>` since this produces errors without stack
+  traces. Prefer to `panic!` instead.

+ 0 - 13
test/integration-test-macros/Cargo.toml

@@ -1,13 +0,0 @@
-[package]
-name = "integration-test-macros"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[dependencies]
-quote = "1"
-proc-macro2 = "1.0"
-syn = {version = "2.0", features = ["full"]}
-
-[lib]
-proc-macro = true

+ 0 - 46
test/integration-test-macros/src/lib.rs

@@ -1,46 +0,0 @@
-use proc_macro::TokenStream;
-use proc_macro2::Span;
-use quote::quote;
-use syn::{parse_macro_input, Ident, ItemFn};
-
-#[proc_macro_attribute]
-pub fn integration_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let item = parse_macro_input!(item as ItemFn);
-    let name = &item.sig.ident;
-    let name_str = &item.sig.ident.to_string();
-    let expanded = quote! {
-        #item
-
-        inventory::submit!(crate::IntegrationTest {
-            name: concat!(module_path!(), "::", #name_str),
-            test_fn: #name,
-        });
-    };
-    TokenStream::from(expanded)
-}
-
-#[proc_macro_attribute]
-pub fn tokio_integration_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let item = parse_macro_input!(item as ItemFn);
-    let name = &item.sig.ident;
-    let name_str = &item.sig.ident.to_string();
-    let sync_name_str = format!("sync_{name_str}");
-    let sync_name = Ident::new(&sync_name_str, Span::call_site());
-    let expanded = quote! {
-        #item
-
-        fn #sync_name() {
-            let rt = tokio::runtime::Builder::new_current_thread()
-                .enable_all()
-                .build()
-                .unwrap();
-            rt.block_on(#name());
-        }
-
-        inventory::submit!(crate::IntegrationTest {
-            name: concat!(module_path!(), "::", #sync_name_str),
-            test_fn: #sync_name,
-        });
-    };
-    TokenStream::from(expanded)
-}

+ 0 - 6
test/integration-test/Cargo.toml

@@ -9,16 +9,10 @@ anyhow = "1"
 aya = { path = "../../aya" }
 aya-log = { path = "../../aya-log" }
 aya-obj = { path = "../../aya-obj" }
-clap = { version = "4", features = ["derive"] }
-env_logger = "0.10"
-futures-core = "0.3"
-inventory = "0.3"
-integration-test-macros = { path = "../integration-test-macros" }
 libc = { version = "0.2.105" }
 log = "0.4"
 object = { version = "0.31", default-features = false, features = ["std", "read_core", "elf"] }
 rbpf = "0.2.0"
 regex = "1"
 tempfile = "3.3.0"
-libtest-mimic = "0.6.0"
 tokio = { version = "1.24", features = ["rt", "rt-multi-thread", "sync", "time"] }

+ 1 - 0
test/integration-test/src/lib.rs

@@ -0,0 +1 @@
+

+ 0 - 21
test/integration-test/src/main.rs

@@ -1,21 +0,0 @@
-use libtest_mimic::{Arguments, Trial};
-
-mod tests;
-use tests::IntegrationTest;
-
-fn main() {
-    env_logger::init();
-    let mut args = Arguments::from_args();
-    // Force to run single-threaded
-    args.test_threads = Some(1);
-    let tests = inventory::iter::<IntegrationTest>
-        .into_iter()
-        .map(|test| {
-            Trial::test(test.name, move || {
-                (test.test_fn)();
-                Ok(())
-            })
-        })
-        .collect();
-    libtest_mimic::run(&args, tests).exit();
-}

+ 10 - 11
test/integration-test/src/tests/bpf_probe_read.rs → test/integration-test/tests/bpf_probe_read.rs

@@ -6,7 +6,6 @@ use aya::{
     programs::{ProgramError, UProbe},
     Bpf,
 };
-use integration_test_macros::integration_test;
 
 const RESULT_BUF_LEN: usize = 1024;
 
@@ -20,13 +19,13 @@ struct TestResult {
 
 unsafe impl aya::Pod for TestResult {}
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_user_str_bytes() {
     let bpf = set_user_buffer(b"foo\0", RESULT_BUF_LEN);
     assert_eq!(result_bytes(&bpf), b"foo");
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_user_str_bytes_truncate() {
     let s = vec![b'a'; RESULT_BUF_LEN];
     let bpf = set_user_buffer(&s, RESULT_BUF_LEN);
@@ -34,25 +33,25 @@ fn bpf_probe_read_user_str_bytes_truncate() {
     assert_eq!(result_bytes(&bpf), &s[..RESULT_BUF_LEN - 1]);
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_user_str_bytes_empty_string() {
     let bpf = set_user_buffer(b"\0", RESULT_BUF_LEN);
     assert_eq!(result_bytes(&bpf), b"");
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_user_str_bytes_empty_dest() {
     let bpf = set_user_buffer(b"foo\0", 0);
     assert_eq!(result_bytes(&bpf), b"");
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_kernel_str_bytes() {
     let bpf = set_kernel_buffer(b"foo\0", RESULT_BUF_LEN);
     assert_eq!(result_bytes(&bpf), b"foo");
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_kernel_str_bytes_truncate() {
     let s = vec![b'a'; RESULT_BUF_LEN];
     let bpf = set_kernel_buffer(&s, RESULT_BUF_LEN);
@@ -60,13 +59,13 @@ fn bpf_probe_read_kernel_str_bytes_truncate() {
     assert_eq!(result_bytes(&bpf), &s[..RESULT_BUF_LEN - 1]);
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_kernel_str_bytes_empty_string() {
     let bpf = set_kernel_buffer(b"\0", RESULT_BUF_LEN);
     assert_eq!(result_bytes(&bpf), b"");
 }
 
-#[integration_test]
+#[test]
 fn bpf_probe_read_kernel_str_bytes_empty_dest() {
     let bpf = set_kernel_buffer(b"foo\0", 0);
     assert_eq!(result_bytes(&bpf), b"");
@@ -76,7 +75,7 @@ fn set_user_buffer(bytes: &[u8], dest_len: usize) -> Bpf {
     let bpf = load_and_attach_uprobe(
         "test_bpf_probe_read_user_str_bytes",
         "trigger_bpf_probe_read_user",
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/bpf_probe_read"),
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/bpf_probe_read"),
     );
     trigger_bpf_probe_read_user(bytes.as_ptr(), dest_len);
     bpf
@@ -86,7 +85,7 @@ fn set_kernel_buffer(bytes: &[u8], dest_len: usize) -> Bpf {
     let mut bpf = load_and_attach_uprobe(
         "test_bpf_probe_read_kernel_str_bytes",
         "trigger_bpf_probe_read_kernel",
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/bpf_probe_read"),
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/bpf_probe_read"),
     );
     set_kernel_buffer_element(&mut bpf, bytes);
     trigger_bpf_probe_read_kernel(dest_len);

+ 7 - 9
test/integration-test/src/tests/btf_relocations.rs → test/integration-test/tests/btf_relocations.rs

@@ -4,12 +4,10 @@ use tempfile::TempDir;
 
 use aya::{maps::Array, programs::TracePoint, BpfLoader, Btf, Endianness};
 
-use super::integration_test;
-
 // In the tests below we often use values like 0xAAAAAAAA or -0x7AAAAAAA. Those values have no
 // special meaning, they just have "nice" bit patterns that can be helpful while debugging.
 
-#[integration_test]
+#[test]
 fn relocate_field() {
     let test = RelocationTest {
         local_definition: r#"
@@ -40,7 +38,7 @@ fn relocate_field() {
     assert_eq!(test.run_no_btf().unwrap(), 3);
 }
 
-#[integration_test]
+#[test]
 fn relocate_enum() {
     let test = RelocationTest {
         local_definition: r#"
@@ -60,7 +58,7 @@ fn relocate_enum() {
     assert_eq!(test.run_no_btf().unwrap(), 0xAAAAAAAA);
 }
 
-#[integration_test]
+#[test]
 fn relocate_enum_signed() {
     let test = RelocationTest {
         local_definition: r#"
@@ -80,7 +78,7 @@ fn relocate_enum_signed() {
     assert_eq!(test.run_no_btf().unwrap() as i64, -0x7AAAAAAAi64);
 }
 
-#[integration_test]
+#[test]
 fn relocate_enum64() {
     let test = RelocationTest {
         local_definition: r#"
@@ -100,7 +98,7 @@ fn relocate_enum64() {
     assert_eq!(test.run_no_btf().unwrap(), 0xAAAAAAAABBBBBBBB);
 }
 
-#[integration_test]
+#[test]
 fn relocate_enum64_signed() {
     let test = RelocationTest {
         local_definition: r#"
@@ -120,7 +118,7 @@ fn relocate_enum64_signed() {
     assert_eq!(test.run_no_btf().unwrap() as i64, -0xAAAAAAABBBBBBBBi64);
 }
 
-#[integration_test]
+#[test]
 fn relocate_pointer() {
     let test = RelocationTest {
         local_definition: r#"
@@ -143,7 +141,7 @@ fn relocate_pointer() {
     assert_eq!(test.run_no_btf().unwrap(), 42);
 }
 
-#[integration_test]
+#[test]
 fn relocate_struct_flavors() {
     let definition = r#"
         struct foo {};

+ 1 - 20
test/integration-test/src/tests/mod.rs → test/integration-test/tests/common.rs

@@ -3,24 +3,7 @@ use libc::{uname, utsname};
 use regex::Regex;
 use std::{cell::OnceCell, ffi::CStr, mem};
 
-pub mod bpf_probe_read;
-pub mod btf_relocations;
-pub mod elf;
-pub mod load;
-pub mod log;
-pub mod rbpf;
-pub mod relocations;
-pub mod smoke;
-
-pub use integration_test_macros::{integration_test, tokio_integration_test};
-
-#[derive(Debug)]
-pub struct IntegrationTest {
-    pub name: &'static str,
-    pub test_fn: fn(),
-}
-
-pub(crate) fn kernel_version() -> anyhow::Result<(u8, u8, u8)> {
+pub fn kernel_version() -> anyhow::Result<(u8, u8, u8)> {
     static mut RE: OnceCell<Regex> = OnceCell::new();
     let re =
         unsafe { &mut RE }.get_or_init(|| Regex::new(r"^([0-9]+)\.([0-9]+)\.([0-9]+)").unwrap());
@@ -38,5 +21,3 @@ pub(crate) fn kernel_version() -> anyhow::Result<(u8, u8, u8)> {
         bail!("no kernel version found");
     }
 }
-
-inventory::collect!(IntegrationTest);

+ 2 - 4
test/integration-test/src/tests/elf.rs → test/integration-test/tests/elf.rs

@@ -1,11 +1,9 @@
-use super::integration_test;
-
 use aya::include_bytes_aligned;
 use object::{Object, ObjectSymbol};
 
-#[integration_test]
+#[test]
 fn test_maps() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/map_test");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/map_test");
     let obj_file = object::File::parse(bytes).unwrap();
     if obj_file.section_by_name("maps").is_none() {
         panic!("No 'maps' ELF section");

+ 16 - 18
test/integration-test/src/tests/load.rs → test/integration-test/tests/load.rs

@@ -9,18 +9,16 @@ use aya::{
     },
     Bpf,
 };
-use log::warn;
 
-use crate::tests::kernel_version;
-
-use super::integration_test;
+mod common;
+use common::kernel_version;
 
 const MAX_RETRIES: u32 = 100;
 const RETRY_DURATION_MS: u64 = 10;
 
-#[integration_test]
+#[test]
 fn long_name() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/name_test");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/name_test");
     let mut bpf = Bpf::load(bytes).unwrap();
     let name_prog: &mut Xdp = bpf
         .program_mut("ihaveaverylongname")
@@ -35,10 +33,10 @@ fn long_name() {
     // Therefore, as long as we were able to load the program, this is good enough.
 }
 
-#[integration_test]
+#[test]
 fn multiple_btf_maps() {
     let bytes =
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/multimap-btf.bpf.o");
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/multimap-btf.bpf.o");
     let mut bpf = Bpf::load(bytes).unwrap();
 
     let map_1: Array<_, u64> = bpf.take_map("map_1").unwrap().try_into().unwrap();
@@ -73,9 +71,9 @@ macro_rules! assert_loaded {
     };
 }
 
-#[integration_test]
+#[test]
 fn unload_xdp() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/test");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/test");
     let mut bpf = Bpf::load(bytes).unwrap();
     let prog: &mut Xdp = bpf
         .program_mut("test_unload_xdp")
@@ -103,9 +101,9 @@ fn unload_xdp() {
     assert_loaded!("test_unload_xdp", false);
 }
 
-#[integration_test]
+#[test]
 fn unload_kprobe() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/test");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/test");
     let mut bpf = Bpf::load(bytes).unwrap();
     let prog: &mut KProbe = bpf
         .program_mut("test_unload_kpr")
@@ -133,14 +131,14 @@ fn unload_kprobe() {
     assert_loaded!("test_unload_kpr", false);
 }
 
-#[integration_test]
+#[test]
 fn pin_link() {
     if kernel_version().unwrap() < (5, 9, 0) {
-        warn!("skipping test, XDP uses netlink");
+        eprintln!("skipping test, XDP uses netlink");
         return;
     }
 
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/test");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/test");
     let mut bpf = Bpf::load(bytes).unwrap();
     let prog: &mut Xdp = bpf
         .program_mut("test_unload_xdp")
@@ -168,14 +166,14 @@ fn pin_link() {
     assert_loaded!("test_unload_xdp", false);
 }
 
-#[integration_test]
+#[test]
 fn pin_lifecycle() {
     if kernel_version().unwrap() < (5, 9, 0) {
-        warn!("skipping test, XDP uses netlink");
+        eprintln!("skipping test, XDP uses netlink");
         return;
     }
 
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/pass");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/pass");
 
     // 1. Load Program and Pin
     {

+ 2 - 4
test/integration-test/src/tests/log.rs → test/integration-test/tests/log.rs

@@ -5,8 +5,6 @@ use aya_log::BpfLogger;
 use log::{Level, Log, Record};
 use tokio::time::{sleep, Duration};
 
-use super::tokio_integration_test;
-
 const MAX_ATTEMPTS: usize = 10;
 const TIMEOUT_MS: u64 = 10;
 
@@ -89,9 +87,9 @@ impl Log for TestingLogger {
     }
 }
 
-#[tokio_integration_test]
+#[tokio::test]
 async fn log() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/log");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/log");
     let mut bpf = Bpf::load(bytes).unwrap();
 
     let (logger, captured_logs) = TestingLogger::with_capacity(5);

+ 4 - 6
test/integration-test/src/tests/rbpf.rs → test/integration-test/tests/rbpf.rs

@@ -4,11 +4,9 @@ use std::collections::HashMap;
 use aya::include_bytes_aligned;
 use aya_obj::{generated::bpf_insn, Object, ProgramSection};
 
-use super::integration_test;
-
-#[integration_test]
+#[test]
 fn run_with_rbpf() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/pass");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/pass");
     let object = Object::parse(bytes).unwrap();
 
     assert_eq!(object.programs.len(), 1);
@@ -37,10 +35,10 @@ fn run_with_rbpf() {
 
 static mut MULTIMAP_MAPS: [*mut Vec<u64>; 2] = [null_mut(), null_mut()];
 
-#[integration_test]
+#[test]
 fn use_map_with_rbpf() {
     let bytes =
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/multimap-btf.bpf.o");
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/multimap-btf.bpf.o");
     let mut object = Object::parse(bytes).unwrap();
 
     assert_eq!(object.programs.len(), 1);

+ 4 - 5
test/integration-test/src/tests/relocations.rs → test/integration-test/tests/relocations.rs

@@ -5,13 +5,12 @@ use aya::{
     programs::{ProgramError, UProbe},
     Bpf,
 };
-use integration_test_macros::integration_test;
 
-#[integration_test]
+#[test]
 fn relocations() {
     let bpf = load_and_attach(
         "test_64_32_call_relocs",
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/relocations"),
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/relocations"),
     );
 
     trigger_relocations_program();
@@ -23,11 +22,11 @@ fn relocations() {
     assert_eq!(m.get(&2, 0).unwrap(), 3);
 }
 
-#[integration_test]
+#[test]
 fn text_64_64_reloc() {
     let mut bpf = load_and_attach(
         "test_text_64_64_reloc",
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/text_64_64_reloc.o"),
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/text_64_64_reloc.o"),
     );
 
     let mut m = aya::maps::Array::<_, u64>::try_from(bpf.map_mut("RESULTS").unwrap()).unwrap();

+ 8 - 9
test/integration-test/src/tests/smoke.rs → test/integration-test/tests/smoke.rs

@@ -3,24 +3,24 @@ use aya::{
     programs::{Extension, Xdp, XdpFlags},
     Bpf, BpfLoader,
 };
-use log::info;
 
-use super::{integration_test, kernel_version};
+mod common;
+use common::kernel_version;
 
-#[integration_test]
+#[test]
 fn xdp() {
-    let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/pass");
+    let bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/pass");
     let mut bpf = Bpf::load(bytes).unwrap();
     let dispatcher: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap();
     dispatcher.load().unwrap();
     dispatcher.attach("lo", XdpFlags::default()).unwrap();
 }
 
-#[integration_test]
+#[test]
 fn extension() {
     let (major, minor, _) = kernel_version().unwrap();
     if major < 5 || (minor == 5 && minor < 9) {
-        info!(
+        eprintln!(
             "skipping as {}.{} does not meet version requirement of 5.9",
             major, minor
         );
@@ -28,14 +28,13 @@ fn extension() {
     }
     // TODO: Check kernel version == 5.9 or later
     let main_bytes =
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/main.bpf.o");
+        include_bytes_aligned!("../../../target/bpfel-unknown-none/release/main.bpf.o");
     let mut bpf = Bpf::load(main_bytes).unwrap();
     let pass: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap();
     pass.load().unwrap();
     pass.attach("lo", XdpFlags::default()).unwrap();
 
-    let ext_bytes =
-        include_bytes_aligned!("../../../../target/bpfel-unknown-none/release/ext.bpf.o");
+    let ext_bytes = include_bytes_aligned!("../../../target/bpfel-unknown-none/release/ext.bpf.o");
     let mut bpf = BpfLoader::new().extension("drop").load(ext_bytes).unwrap();
     let drop_: &mut Extension = bpf.program_mut("drop").unwrap().try_into().unwrap();
     drop_.load(pass.fd().unwrap(), "xdp_pass").unwrap();

+ 5 - 4
xtask/Cargo.toml

@@ -5,11 +5,12 @@ authors = ["Alessandro Decina <[email protected]>"]
 edition = "2021"
 
 [dependencies]
+anyhow = "1"
 aya-tool = { path = "../aya-tool" }
+cargo_metadata = "0.15.4"
 clap = { version = "4", features = ["derive"] }
-anyhow = "1"
-syn = "2"
-quote = "1"
-proc-macro2 = "1"
 indoc = "2.0"
+proc-macro2 = "1"
+quote = "1"
 serde_json = "1"
+syn = "2"

+ 9 - 5
xtask/src/build_ebpf.rs

@@ -4,7 +4,7 @@ use std::{
     ffi::{OsStr, OsString},
     fs,
     path::{Path, PathBuf},
-    process::Command,
+    process::{Command, Output},
 };
 
 use anyhow::{bail, Context};
@@ -148,16 +148,20 @@ fn compile_with_clang<P: Clone + AsRef<Path>>(
         .arg("-o")
         .arg(out.as_ref().as_os_str());
 
-    let output = cmd.output().context("Failed to execute clang")?;
-    if !output.status.success() {
+    let Output {
+        status,
+        stdout,
+        stderr,
+    } = cmd.output().context("Failed to execute clang")?;
+    if !status.success() {
         bail!(
             "Failed to compile eBPF programs\n \
             stdout=\n \
             {}\n \
             stderr=\n \
             {}\n",
-            String::from_utf8(output.stdout).unwrap(),
-            String::from_utf8(output.stderr).unwrap()
+            String::from_utf8(stdout).unwrap(),
+            String::from_utf8(stderr).unwrap()
         );
     }
 

+ 7 - 8
xtask/src/main.rs

@@ -24,15 +24,14 @@ enum Command {
 }
 
 fn main() {
-    let opts = XtaskOptions::parse();
+    let XtaskOptions { command } = Parser::parse();
 
-    use Command::*;
-    let ret = match opts.command {
-        Codegen(opts) => codegen::codegen(opts),
-        Docs => docs::docs(),
-        BuildIntegrationTest(opts) => build_test::build_test(opts),
-        BuildIntegrationTestEbpf(opts) => build_ebpf::build_ebpf(opts),
-        IntegrationTest(opts) => run::run(opts),
+    let ret = match command {
+        Command::Codegen(opts) => codegen::codegen(opts),
+        Command::Docs => docs::docs(),
+        Command::BuildIntegrationTest(opts) => build_test::build_test(opts),
+        Command::BuildIntegrationTestEbpf(opts) => build_ebpf::build_ebpf(opts),
+        Command::IntegrationTest(opts) => run::run(opts),
     };
 
     if let Err(e) = ret {

+ 104 - 34
xtask/src/run.rs

@@ -1,6 +1,12 @@
-use std::{os::unix::process::CommandExt, path::PathBuf, process::Command};
+use std::{
+    fmt::Write as _,
+    io::BufReader,
+    path::PathBuf,
+    process::{Command, Stdio},
+};
 
 use anyhow::Context as _;
+use cargo_metadata::{Artifact, CompilerMessage, Message, Target};
 use clap::Parser;
 
 use crate::build_ebpf::{build_ebpf, Architecture, BuildEbpfOptions as BuildOptions};
@@ -18,59 +24,123 @@ pub struct Options {
     pub runner: String,
     /// libbpf directory
     #[clap(long, action)]
-    pub libbpf_dir: String,
+    pub libbpf_dir: PathBuf,
     /// Arguments to pass to your application
     #[clap(name = "args", last = true)]
     pub run_args: Vec<String>,
 }
 
 /// Build the project
-fn build(opts: &Options) -> Result<(), anyhow::Error> {
-    let mut args = vec!["build"];
-    if opts.release {
-        args.push("--release")
+fn build(release: bool) -> Result<Vec<(PathBuf, PathBuf)>, anyhow::Error> {
+    let mut cmd = Command::new("cargo");
+    cmd.arg("build")
+        .arg("--tests")
+        .arg("--message-format=json")
+        .arg("--package=integration-test");
+    if release {
+        cmd.arg("--release");
     }
-    args.push("-p");
-    args.push("integration-test");
-    let status = Command::new("cargo")
-        .args(&args)
-        .status()
-        .expect("failed to build userspace");
+    let mut cmd = cmd
+        .stdout(Stdio::piped())
+        .spawn()
+        .with_context(|| format!("failed to spawn {cmd:?}"))?;
+
+    let reader = BufReader::new(cmd.stdout.take().unwrap());
+    let mut executables = Vec::new();
+    let mut compiler_messages = String::new();
+    for message in Message::parse_stream(reader) {
+        #[allow(clippy::collapsible_match)]
+        match message.context("valid JSON")? {
+            Message::CompilerArtifact(Artifact {
+                executable,
+                target: Target { src_path, .. },
+                ..
+            }) => {
+                if let Some(executable) = executable {
+                    executables.push((src_path.into(), executable.into()));
+                }
+            }
+            Message::CompilerMessage(CompilerMessage { message, .. }) => {
+                assert_eq!(writeln!(&mut compiler_messages, "{message}"), Ok(()));
+            }
+            _ => {}
+        }
+    }
+
+    let status = cmd
+        .wait()
+        .with_context(|| format!("failed to wait for {cmd:?}"))?;
+
     match status.code() {
         Some(code) => match code {
-            0 => Ok(()),
-            code => Err(anyhow::anyhow!("exited with status code: {code}")),
+            0 => Ok(executables),
+            code => Err(anyhow::anyhow!(
+                "{cmd:?} exited with status code {code}:\n{compiler_messages}"
+            )),
         },
-        None => Err(anyhow::anyhow!("process terminated by signal")),
+        None => Err(anyhow::anyhow!("{cmd:?} terminated by signal")),
     }
 }
 
 /// Build and run the project
 pub fn run(opts: Options) -> Result<(), anyhow::Error> {
+    let Options {
+        bpf_target,
+        release,
+        runner,
+        libbpf_dir,
+        run_args,
+    } = opts;
+
     // build our ebpf program followed by our application
     build_ebpf(BuildOptions {
-        target: opts.bpf_target,
-        libbpf_dir: PathBuf::from(&opts.libbpf_dir),
+        target: bpf_target,
+        libbpf_dir,
     })
-    .context("Error while building eBPF program")?;
-    build(&opts).context("Error while building userspace application")?;
-    // profile we are building (release or debug)
-    let profile = if opts.release { "release" } else { "debug" };
-    let bin_path = format!("target/{profile}/integration-test");
+    .context("error while building eBPF program")?;
 
-    // arguments to pass to the application
-    let mut run_args: Vec<_> = opts.run_args.iter().map(String::as_str).collect();
+    let binaries = build(release).context("error while building userspace application")?;
+    let mut args = runner.trim().split_terminator(' ');
+    let runner = args.next().ok_or(anyhow::anyhow!("no first argument"))?;
+    let args = args.collect::<Vec<_>>();
 
-    // configure args
-    let mut args: Vec<_> = opts.runner.trim().split_terminator(' ').collect();
-    args.push(bin_path.as_str());
-    args.append(&mut run_args);
+    let mut failures = String::new();
+    for (src_path, binary) in binaries {
+        let mut cmd = Command::new(runner);
+        let cmd = cmd
+            .args(args.iter())
+            .arg(binary)
+            .args(run_args.iter())
+            .arg("--test-threads=1");
 
-    // spawn the command
-    let err = Command::new(args.first().expect("No first argument"))
-        .args(args.iter().skip(1))
-        .exec();
+        println!("{} running {cmd:?}", src_path.display());
 
-    // we shouldn't get here unless the command failed to spawn
-    Err(anyhow::Error::from(err).context(format!("Failed to run `{}`", args.join(" "))))
+        let status = cmd
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .status()
+            .context("failed to run {cmd:?}")?;
+        match status.code() {
+            Some(code) => match code {
+                0 => {}
+                code => assert_eq!(
+                    writeln!(
+                        &mut failures,
+                        "{} exited with status code {code}",
+                        src_path.display()
+                    ),
+                    Ok(())
+                ),
+            },
+            None => assert_eq!(
+                writeln!(&mut failures, "{} terminated by signal", src_path.display()),
+                Ok(())
+            ),
+        }
+    }
+    if failures.is_empty() {
+        Ok(())
+    } else {
+        Err(anyhow::anyhow!("failures:\n{}", failures))
+    }
 }