From 41aea639c93e9eb0e9007c6dea29c9d59de0e815 Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Sun, 11 Oct 2026 10:22:59 +0200 Subject: [PATCH 1/2] ci: add GitHub Actions workflow (test, clippy, release build) Adds CI for the neodev crate itself: - Test: cargo test --locked - Clippy: cargo clippy --all-targets --locked -- -D warnings - Build: cargo build --release --locked Also fixes the five clippy warnings that would have failed the new gate: unused UNIX_EPOCH import, manual div_ceil, needless borrow, needless range loop, and a complex tuple type in image.rs factored into FreeRegion / FreelistPlan aliases. cargo fmt --check is not gated yet: 262 diff hunks need their own sweep. Closes #38 --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 8 ++++++ src/image.rs | 18 +++++++++---- src/vmm/vbox.rs | 1 - 4 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..55c98dc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: cargo test + run: cargo test --locked + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: cargo clippy (-D warnings) + run: cargo clippy --all-targets --locked -- -D warnings + + build: + name: Build (release) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: cargo build --release + run: cargo build --release --locked diff --git a/CHANGELOG.md b/CHANGELOG.md index 7caeefd..398a8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ ### Added +- **GitHub Actions CI (#38).** Pull requests and pushes to `main` now run + `cargo test --locked`, `cargo clippy --all-targets --locked -- -D warnings` and + `cargo build --release --locked` on stable Rust with cached artifacts. The five + clippy warnings that would have broken the new gate are fixed as well: unused + `UNIX_EPOCH` import, manual `div_ceil`, needless borrow, needless range loop, + and a complex tuple type in `image.rs` factored into `FreeRegion` / + `FreelistPlan` aliases. + - **Image builder: package `snapshot.nxe` (#569).** Add `snapshot` to the `System/Tools` list so the NeoFS v2 snapshot command is installed at `C:\System\Tools\snapshot.nxe` (NeoDOS #569). diff --git a/src/image.rs b/src/image.rs index 3aff57a..ccb335b 100644 --- a/src/image.rs +++ b/src/image.rs @@ -23,19 +23,25 @@ const PERM_X: u16 = 0x0004; /// `(4096 - 8 header - 8 next_lba) / 12`. const REGIONS_PER_NODE: usize = 340; +/// One NE2 free-list region: `(start_lba, length_in_sectors)`. +type FreeRegion = (u64, u32); + +/// The plan produced by [`plan_freelist`]: `(node_lbas, adjusted_regions)`. +type FreelistPlan = (Vec, Vec); + /// Plan the free-list node chain. /// /// The first node is placed at `first_lba`. If the region list needs more than /// one node, the extra nodes are reserved from the front of the first free /// region (which is shrunk accordingly). Returns `(node_lbas, adjusted_regions)`. fn plan_freelist( - mut free_regions: Vec<(u64, u32)>, + mut free_regions: Vec, first_lba: u64, -) -> Result<(Vec, Vec<(u64, u32)>)> { +) -> Result { if free_regions.is_empty() { return Ok((Vec::new(), free_regions)); } - let num_nodes = (free_regions.len() + REGIONS_PER_NODE - 1) / REGIONS_PER_NODE; + let num_nodes = free_regions.len().div_ceil(REGIONS_PER_NODE); let mut node_lbas = vec![first_lba]; let extra = num_nodes - 1; if extra > 0 { @@ -552,7 +558,7 @@ fn collect_files(cfg: &Config, _disc: &Discovery) -> Result> { ("net.nxl", "net.nxl"), ]; let mut nxl_files: Vec<(String, std::path::PathBuf)> = Vec::new(); - if let Ok(entries) = std::fs::read_dir(&root) { + if let Ok(entries) = std::fs::read_dir(root) { for entry in entries.flatten() { let p = entry.path(); if p.is_file() && p.extension().map(|e| e == "nxl").unwrap_or(false) { @@ -997,7 +1003,9 @@ mod tests { assert!(ikeys[0].is_empty()); // Leaves hold all 60 keys in order. let mut all = Vec::new(); - for l in 0..chunks.len() { all.extend(parse_leaf_keys(&nodes[l])); } + for node in nodes.iter().take(chunks.len()) { + all.extend(parse_leaf_keys(node)); + } assert_eq!(all.len(), 60); let mut sorted = all.clone(); sorted.sort(); diff --git a/src/vmm/vbox.rs b/src/vmm/vbox.rs index 02c4cc4..f459bd7 100644 --- a/src/vmm/vbox.rs +++ b/src/vmm/vbox.rs @@ -535,7 +535,6 @@ fn has_ip_address(iface: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use std::time::UNIX_EPOCH; #[test] fn test_detect_bridge_interface_not_empty() { From 81d0bdb5541db8d3dc77e1f6e5b21c3484c59478 Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Sun, 11 Oct 2026 10:24:31 +0200 Subject: [PATCH 2/2] test(vbox): make interface detection test host-independent test_detect_returns_real_ethernet asserted `selected != "eth0"`, which fails on hosts where the live interface is itself named eth0 -- GitHub Actions runners included. The CI added in this PR caught it on its first run. Assert instead that detection picks one of the live interfaces the test found (mirroring detect_bridge_interface's exclusions), which is what the test actually means. Refs #38 --- src/vmm/vbox.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/vmm/vbox.rs b/src/vmm/vbox.rs index f459bd7..0787b05 100644 --- a/src/vmm/vbox.rs +++ b/src/vmm/vbox.rs @@ -552,7 +552,11 @@ mod tests { let parts: Vec<&str> = line.split(':').collect(); if parts.len() < 2 { return None; } let iface = parts[1].trim().to_string(); - if iface == "lo" { return None; } + // Mirror the exclusions of `detect_bridge_interface` so both sets agree. + if iface == "lo" || iface.starts_with("tap") || iface.starts_with("docker") + || iface.starts_with("vbox") || iface.starts_with("virbr") || iface.starts_with("br-") { + return None; + } let up = std::fs::read_to_string(format!("/sys/class/net/{}/operstate", iface)).map(|s| s.trim() == "up").unwrap_or(false); let car = std::fs::read_to_string(format!("/sys/class/net/{}/carrier", iface)).map(|s| s.trim() == "1").unwrap_or(false); if up && car { Some(iface) } else { None } @@ -561,7 +565,15 @@ mod tests { if !active_eth.is_empty() { let selected = detect_bridge_interface(); assert!(!selected.is_empty()); - assert_ne!(selected, "eth0", "should detect real interface, not fallback"); + // With a live interface present, detection must pick one of them rather + // than the hardcoded default. Comparing against the literal "eth0" was + // wrong: on some hosts — GitHub Actions runners included — the live + // interface is itself named eth0, so the old assertion failed even + // though detection had worked. + assert!( + active_eth.contains(&selected), + "detection must pick a live interface, not the hardcoded fallback: selected {selected}, live {active_eth:?}" + ); } }