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..0787b05 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() { @@ -553,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 } @@ -562,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:?}" + ); } }