Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
check:
name: Check (stable)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

Expand All @@ -42,10 +43,11 @@ jobs:
working-directory: src/vela/vela-core
run: cargo check --workspace --exclude vela-ffi

test:
name: Test (stable)
unit-test:
name: Unit Tests
runs-on: ubuntu-latest
needs: check
timeout-minutes: 25
steps:
- uses: actions/checkout@v4

Expand All @@ -59,16 +61,42 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-${{ hashFiles('src/vela/vela-core/Cargo.lock') }}
key: ${{ runner.os }}-cargo-${{ hashFiles('src/vela/vela-core/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Run unit tests (core crates only)
working-directory: src/vela/vela-core
run: cargo test -p vela-crypto -p vela-flashpack -p vela-attestation -p vela-lifecycle -p vela-slotmgr -p vela-hub -p vela-watchdog -p vela-core -- --test-threads=2

e2e-test:
name: E2E Tests
runs-on: ubuntu-latest
needs: check
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-e2e-${{ hashFiles('src/vela/vela-core/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Run tests
- name: Run e2e tests
working-directory: src/vela/vela-core
run: cargo test --workspace --exclude vela-ffi
run: cargo test -p vela-e2e -- --test-threads=2

lint:
name: Clippy + Fmt
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

Expand Down
1 change: 0 additions & 1 deletion src/vela/vela-core/crates/vela-e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ tracing-subscriber = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
reqwest = { workspace = true }
6 changes: 6 additions & 0 deletions src/vela/vela-core/crates/vela-e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@
//! 6. Configuration Validation — default configs, custom configs

// Suite modules
#[cfg(test)]
mod suite1_watchdog_bus;
#[cfg(test)]
mod suite2_slot_lifecycle;
#[cfg(test)]
mod suite3_hub_retry;
#[cfg(test)]
mod suite4_pipeline;
#[cfg(test)]
mod suite5_error_recovery;
#[cfg(test)]
mod suite6_config;

// Ensure workspace crate references compile
Expand Down
61 changes: 0 additions & 61 deletions src/vela/vela-core/crates/vela-e2e/src/suite1_watchdog_bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,64 +139,3 @@ fn test_history_preserves_event_order() {
assert_eq!(history[1].event_type(), "validation_complete");
assert_eq!(history[2].event_type(), "install_started");
}

#[test]
fn test_all_event_variants_displayable() {
let events = vec![
SystemEvent::UpdateAvailable {
rollout_id: "r1".into(),
target_version: "1.0".into(),
flashpack_size: 100,
force_install: false,
},
SystemEvent::DownloadStarted {
rollout_id: "r1".into(),
total_bytes: 100,
},
SystemEvent::DownloadProgress {
rollout_id: "r1".into(),
downloaded_bytes: 50,
total_bytes: 100,
percent: 50.0,
},
SystemEvent::DownloadComplete {
rollout_id: "r1".into(),
},
SystemEvent::ValidationStarted {
rollout_id: "r1".into(),
},
SystemEvent::ValidationComplete {
rollout_id: "r1".into(),
valid: true,
},
SystemEvent::InstallStarted {
rollout_id: "r1".into(),
target_slot: "alternate".into(),
},
SystemEvent::InstallComplete {
rollout_id: "r1".into(),
},
SystemEvent::RebootRequired {
target_slot: "alternate".into(),
},
SystemEvent::HealthPulseSent { sequence: 1 },
SystemEvent::WatchdogTriggered {
last_pet_secs_ago: 10,
},
SystemEvent::FallbackActivated {
reason: "timeout".into(),
},
SystemEvent::AttestationComplete {
device_id: "dev-01".into(),
},
];

for ev in events {
let display = ev.to_string();
assert!(
!display.is_empty(),
"Event {} should have display",
ev.event_type()
);
}
}
111 changes: 4 additions & 107 deletions src/vela/vela-core/crates/vela-e2e/src/suite2_slot_lifecycle.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
//! Suite 2: Slot Manager + Lifecycle integration tests.
//!
//! Validates that slot transitions trigger lifecycle phase changes,
//! inactive slot selection is stable, and write+verify round-trip works.
//! Validates slot transitions trigger lifecycle phase changes
//! and mock slot detection/swap works end-to-end.

use vela_lifecycle::{
LifecycleConfig, LifecycleContext, LifecycleEngine, LifecycleMetrics, LifecycleOutcome,
UpdatePhase,
LifecycleConfig, LifecycleContext, LifecycleEngine, LifecycleMetrics, UpdatePhase,
};
use vela_slotmgr::{MockSlotProvider, SlotLabel, SlotManager, SlotProvider};
use vela_slotmgr::{MockSlotProvider, SlotProvider};

#[tokio::test]
async fn test_slot_transitions_trigger_lifecycle_changes() {
Expand Down Expand Up @@ -47,49 +46,6 @@ async fn test_slot_transitions_trigger_lifecycle_changes() {
}
}

#[test]
fn test_inactive_slot_selection_is_stable() {
let mgr = SlotManager::default();

// Default: active = Primary, inactive = Alternate
for _ in 0..100 {
assert_eq!(mgr.select_inactive_slot(), SlotLabel::Alternate);
}
}

#[test]
fn test_inactive_slot_after_swap() {
let mut mgr = SlotManager::default();

mgr.swap_active();
// After swap: active = Alternate, inactive = Primary
for _ in 0..100 {
assert_eq!(mgr.select_inactive_slot(), SlotLabel::Primary);
}
}

#[test]
fn test_write_and_verify_on_slot() {
let mut mgr = SlotManager::default();
let data = b"vela-ota-slot-test-data-0123456789";

// Write to alternate slot
let result = mgr.write_slot(SlotLabel::Alternate, data);
assert!(result.is_ok(), "Write to alternate slot should succeed");
}

#[test]
fn test_write_large_data_fails_with_insufficient_space() {
let mock = MockSlotProvider::new();
mock.set_alternate_free_bytes(100); // only 100 bytes free

let mut mgr = SlotManager::with_mock(mock);
let large_data = vec![0u8; 200]; // 200 bytes > 100 free

let result = mgr.write_slot(SlotLabel::Alternate, &large_data);
assert!(result.is_err(), "Should fail due to insufficient space");
}

#[tokio::test]
async fn test_slot_mock_detect_and_swap() {
let provider = MockSlotProvider::with_versions("1.0.0", "1.0.0");
Expand All @@ -109,62 +65,3 @@ async fn test_slot_mock_detect_and_swap() {
vela_slotmgr::SlotId::Alternate
);
}
Comment on lines 65 to 67

#[tokio::test]
async fn test_lifecycle_context_metrics() {
let ctx = LifecycleContext {
update_id: "metrics-test".into(),
metrics: std::sync::Mutex::new(LifecycleMetrics::default()),
};

ctx.record_bytes_downloaded(1024);
ctx.record_bytes_written(512);
ctx.record_validation_time(150);

let metrics = ctx.metrics.lock().unwrap();
assert_eq!(metrics.bytes_downloaded, 1024);
assert_eq!(metrics.bytes_written, 512);
assert_eq!(metrics.validation_time_ms, 150);

// Errors increment retry count
drop(metrics);
ctx.record_error(&vela_lifecycle::LifecycleError::PhaseTimeout(
UpdatePhase::Validating,
));
assert_eq!(ctx.metrics.lock().unwrap().retry_count, 1);
}

#[tokio::test]
async fn test_lifecycle_terminal_states_reachable() {
let engine = LifecycleEngine::new(LifecycleConfig::default());

// Test Committing → Idle (Success)
let ctx_commit = LifecycleContext {
update_id: "commit-test".into(),
metrics: std::sync::Mutex::new(LifecycleMetrics::default()),
};
let result = engine
.execute_phase(&ctx_commit, UpdatePhase::Committing)
.await
.unwrap();
assert_eq!(result, UpdatePhase::Idle);
assert_eq!(
ctx_commit.metrics.lock().unwrap().outcome,
Some(LifecycleOutcome::Success)
);

// Test FallbackRecovery → Idle
let ctx_fallback = LifecycleContext {
update_id: "fallback-test".into(),
metrics: std::sync::Mutex::new(LifecycleMetrics::default()),
};
let result = engine
.execute_phase(&ctx_fallback, UpdatePhase::FallbackRecovery)
.await
.unwrap();
assert_eq!(result, UpdatePhase::Idle);
assert!(matches!(
ctx_fallback.metrics.lock().unwrap().outcome,
Some(LifecycleOutcome::FallbackRecovery { .. })
));
}
65 changes: 1 addition & 64 deletions src/vela/vela-core/crates/vela-e2e/src/suite3_hub_retry.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
//! Suite 3: Hub client + retry + download integration tests.
//! Suite 3: Hub client + retry integration tests.

use sha2::Digest;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use vela_hub::client::VelaHubClient;
use vela_hub::retry::RetryStrategy;
use vela_hub::*;

Expand Down Expand Up @@ -72,30 +71,6 @@ async fn test_retry_eventually_succeeds() {
assert_eq!(counter.load(Ordering::SeqCst), 4);
}

#[test]
fn test_download_state_tracking() {
let state = vela_hub::download::DownloadState {
url: "https://example.com/fp.fpk".into(),
expected_size: 1024,
expected_checksum: Some("abc123".into()),
downloaded_bytes: 512,
dest_path: std::path::PathBuf::from("/tmp/test.fpk"),
};
assert!(!state.is_complete());
}

#[test]
fn test_download_state_complete() {
let state = vela_hub::download::DownloadState {
url: "https://example.com/fp.fpk".into(),
expected_size: 1024,
expected_checksum: None,
downloaded_bytes: 1024,
dest_path: std::path::PathBuf::from("/tmp/test.fpk"),
};
assert!(state.is_complete());
}

#[tokio::test]
async fn test_checksum_verification_pass() {
let data = b"vela-ota-integration-test-data";
Expand All @@ -112,34 +87,6 @@ async fn test_checksum_mismatch() {
assert_ne!(actual, wrong_hash, "Checksum should not match wrong hash");
}

#[test]
fn test_hub_client_construction() {
let config = HubConfig::new("https://hub.vela-ota.dev").with_auth("test-token");
let client = VelaHubClient::new(config);
assert!(client.is_ok());
}

#[test]
fn test_hub_client_missing_auth_builds() {
let config = HubConfig::new("https://hub.vela-ota.dev");
let client = VelaHubClient::new(config);
assert!(client.is_ok());
}

#[test]
fn test_url_construction() {
let config = HubConfig::new("https://hub.example.com");
assert_eq!(
config.url("/api/v1/poll"),
"https://hub.example.com/api/v1/poll"
);
let config = HubConfig::new("https://hub.example.com/");
assert_eq!(
config.url("/api/v1/poll"),
"https://hub.example.com/api/v1/poll"
);
}

#[test]
fn test_rollout_manifest_serde() {
let manifest = RolloutManifest {
Expand Down Expand Up @@ -181,13 +128,3 @@ fn test_poll_outcome_serde() {
let json = serde_json::to_string(&no_update).unwrap();
assert!(json.contains("NoUpdate"));
}

#[test]
fn test_retry_delay_exponential_growth() {
let s = RetryStrategy::for_polling();
assert_eq!(s.max_retries, 2);
assert!(s.initial_delay < Duration::from_secs(1));
let d = RetryStrategy::for_download();
assert_eq!(d.max_retries, 5);
assert!(d.max_delay >= Duration::from_secs(60));
}
Loading
Loading