feat: implement virtual clock timing architecture (Phases 5-8)

Introduces a VirtualClock-based timing system replacing wall-clock
Instant-based timing for hardware-accurate simulation.

New modules:
- virtual_clock.rs: Shared virtual time source with condvar sync
- real_time_governor.rs: Thread that advances virtual time
- component_timing.rs: Per-component timing constraints

Key changes:
- Component trait: added update_at_time(), timing(), set_virtual_clock_optional()
- All Intel 400x components migrated to virtual-time-aware operation
- ConfigurableSystem: start() returns Result, spawns governor thread
- GUI: timing controls (speed slider, mode selector, step button)
- Console: speed/mode/step/time commands
- Backward compatible: legacy fallback for components without VirtualClock

Reviewed-by: code review found 2 warnings (clock resume() dead code,
reset() safety docs) — both fixed.
This commit is contained in:
Jan-Peter von Hunnius 2026-05-14 21:00:22 +02:00
parent add7ace674
commit 08f53d68fe
28 changed files with 7186 additions and 270 deletions

View file

@ -81,8 +81,8 @@ mod intel_4001_integration_tests {
// Test access time configuration
assert_eq!(rom.get_access_time(), 1);
// Test address latch time handling
let test_time = std::time::Instant::now();
// Test address latch time handling (virtual time: Duration-based)
let test_time = std::time::Duration::from_nanos(500);
rom.set_address_latch_time(Some(test_time));
assert_eq!(rom.get_address_latch_time(), Some(test_time));
}

View file

@ -6,7 +6,6 @@ use rusty_emu::pin::{Pin, PinValue};
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
// Mock component for testing trait implementations
#[derive(Debug)]
@ -247,7 +246,7 @@ mod tests {
let mut high_nibble: Option<u8> = None;
let mut low_nibble: Option<u8> = None;
let mut full_address_ready = false;
let mut address_latch_time: Option<Instant> = None;
let mut address_latch_time: Option<Duration> = None;
let access_time = Duration::from_nanos(100);
// Mock address handler for testing the algorithm
@ -275,6 +274,7 @@ mod tests {
&mut full_address_ready,
&mut address_latch_time,
access_time,
Duration::from_nanos(100), // virtual_time
);
assert_eq!(high_nibble, Some(0x12));
@ -282,7 +282,7 @@ mod tests {
assert_eq!(full_address_ready, false);
assert_eq!(address_latch_time, None);
// Second cycle: low nibble
// Second cycle: low nibble — address_latch_time is now set internally
handler.handle_address_latching(
0x34,
&mut high_nibble,
@ -290,12 +290,14 @@ mod tests {
&mut full_address_ready,
&mut address_latch_time,
access_time,
Duration::from_nanos(200), // virtual_time when address is fully latched
);
assert_eq!(high_nibble, None); // Should be cleared
assert_eq!(low_nibble, None); // Should be cleared
assert_eq!(full_address_ready, true);
assert!(address_latch_time.is_some());
// address_latch_time is now set by handle_address_latching() internally
assert_eq!(address_latch_time, Some(Duration::from_nanos(200)));
// Verify the assembled address
let assembled = handler.assemble_full_address(Some(0x02), Some(0x04));
@ -311,26 +313,37 @@ mod tests {
Intel400xAddressHandling::handle_latency_wait(
&MockComponent::new(),
&None,
access_time
access_time,
Duration::from_nanos(200),
),
false
);
// Test with recent latch time - need to ensure enough time has elapsed
// Create the latch time, then sleep to guarantee elapsed >= access_time
let recent_time = Some(Instant::now());
thread::sleep(Duration::from_micros(1));
// Test with latch time and sufficient virtual time elapsed
let latch_time = Some(Duration::from_nanos(50));
let virtual_time = Duration::from_nanos(200);
assert_eq!(
Intel400xAddressHandling::handle_latency_wait(
&MockComponent::new(),
&recent_time,
access_time
&latch_time,
access_time,
virtual_time,
),
true
);
// Note: Testing with elapsed time would require sleeping or mocking time
// This demonstrates the logic structure is correct
// Test with latch time but insufficient virtual time elapsed
let latch_time = Some(Duration::from_nanos(150));
let virtual_time = Duration::from_nanos(200);
assert_eq!(
Intel400xAddressHandling::handle_latency_wait(
&MockComponent::new(),
&latch_time,
access_time,
virtual_time,
),
false
);
}
#[test]

View file

@ -17,6 +17,7 @@ mod intel_400x_tests;
// Common test utilities and helpers
pub mod test_utils {
use std::sync::Arc;
use std::time::{Duration, Instant};
/// Create a deterministic time source for testing
@ -34,6 +35,57 @@ pub mod test_utils {
std::thread::sleep(duration);
}
/// Create a VirtualClock in MaxSpeed mode for fast deterministic tests.
///
/// The clock starts in stopped state. Call `clock.start()` after spawning
/// the governor thread.
///
/// # Parameters
/// - `tick_size_ns`: Virtual time advance per tick (default: 333ns for 750 kHz)
pub fn create_maxspeed_clock(tick_size_ns: u64) -> Arc<rusty_emu::virtual_clock::VirtualClock> {
let clock = Arc::new(rusty_emu::virtual_clock::VirtualClock::new(
Duration::from_nanos(tick_size_ns),
));
clock.set_mode(rusty_emu::virtual_clock::SimulationMode::MaxSpeed);
clock
}
/// Create a VirtualClock in SingleStep mode for precise state verification.
///
/// Each call to `clock.trigger_step()` advances virtual time by one tick.
pub fn create_singlestep_clock(tick_size_ns: u64) -> Arc<rusty_emu::virtual_clock::VirtualClock> {
let clock = Arc::new(rusty_emu::virtual_clock::VirtualClock::new(
Duration::from_nanos(tick_size_ns),
));
clock.set_mode(rusty_emu::virtual_clock::SimulationMode::SingleStep);
clock
}
/// Spawn a RealTimeGovernor thread for the given clock.
///
/// Returns a JoinHandle that can be used to wait for the governor to exit.
/// The governor will exit when `clock.stop()` is called.
pub fn spawn_governor(
clock: Arc<rusty_emu::virtual_clock::VirtualClock>,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let governor = rusty_emu::real_time_governor::RealTimeGovernor::with_defaults(clock);
governor.run();
})
}
/// Stop the virtual clock and wait for the governor thread to finish.
///
/// This ensures clean shutdown of the governor thread before the test ends.
pub fn stop_governor(
clock: &Arc<rusty_emu::virtual_clock::VirtualClock>,
handle: std::thread::JoinHandle<()>,
) {
clock.stop();
// Give the governor thread a moment to exit cleanly
let _ = handle.join();
}
/// Verify that a component implements all required traits
pub fn verify_intel400x_traits<C>(component: &C)
where

888
tests/virtual_time_tests.rs Normal file
View file

@ -0,0 +1,888 @@
//! Virtual Time Integration Tests
//!
//! Tests for the virtual time architecture (Phase 9):
//! - MaxSpeed mode for fast deterministic execution
//! - SingleStep mode for precise state verification
//! - Overclocking tests verifying component timing violations
//! - Virtual clock integration with ConfigurableSystem
use std::sync::Arc;
use std::time::Duration;
use rusty_emu::component::Component;
use rusty_emu::component_timing::ComponentTiming;
use rusty_emu::components::common::intel_400x::*;
use rusty_emu::components::memory::intel_4001::Intel4001;
use rusty_emu::components::memory::intel_4002::Intel4002;
use rusty_emu::components::memory::intel_4003::Intel4003;
use rusty_emu::real_time_governor::RealTimeGovernor;
use rusty_emu::system_config::SystemFactory;
use rusty_emu::virtual_clock::{SimulationMode, VirtualClock};
// =============================================================================
// MaxSpeed Mode Tests
// =============================================================================
#[cfg(test)]
mod maxspeed_tests {
use super::*;
/// Verify that a VirtualClock in MaxSpeed mode advances time
/// when driven by a RealTimeGovernor.
#[test]
fn test_maxspeed_clock_advances() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Let the governor run for a bit (it calls clock.start() internally)
std::thread::sleep(Duration::from_millis(50));
// Virtual time should have advanced significantly
let vt = clock.now_ns();
assert!(vt > 0, "Virtual time should have advanced past 0");
// Verify we can read the time
assert!(clock.now() > Duration::ZERO, "Virtual time should be > 0");
clock.stop();
let _ = handle.join();
}
/// Verify that multiple time targets are reached in MaxSpeed mode.
#[test]
fn test_maxspeed_multiple_targets() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Let the governor advance time
std::thread::sleep(Duration::from_millis(50));
// Virtual time should have advanced well past multiple targets
let vt = clock.now_ns();
assert!(vt >= 1000, "Should have passed 1000ns, got {}", vt);
assert!(vt >= 5000, "Should have passed 5000ns, got {}", vt);
assert!(vt >= 10000, "Should have passed 10000ns, got {}", vt);
clock.stop();
let _ = handle.join();
}
/// Verify that component update_at_time works with virtual time in MaxSpeed mode.
/// Tests that a ROM component can be updated with specific virtual times.
#[test]
fn test_maxspeed_component_update_at_time() {
let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 500);
// Load test data
rom.load_rom_data(vec![0xAB, 0xCD], 0).unwrap();
// Simulate address latching at virtual time T=0
rom.set_timing_state(TimingState::Idle);
rom.set_address_high_nibble(Some(0x0));
rom.set_address_low_nibble(Some(0x0));
rom.set_full_address_ready(true);
rom.set_address_latch_time(Some(Duration::ZERO));
// At T=0 (before access_time), data should not be ready
let timing = ComponentTiming::intel_4001();
let elapsed = Duration::ZERO;
assert!(
timing.check_access_time(elapsed).is_err(),
"Access time should not have elapsed at T=0"
);
// At T=500ns (exactly access_time), data should be ready
let elapsed_500 = Duration::from_nanos(500);
assert!(
timing.check_access_time(elapsed_500).is_ok(),
"Access time should have elapsed at T=500ns"
);
// At T=600ns (after access_time), data should definitely be ready
let elapsed_600 = Duration::from_nanos(600);
assert!(
timing.check_access_time(elapsed_600).is_ok(),
"Access time should have elapsed at T=600ns"
);
}
/// Verify that a ROM with 1ns access time works correctly through
/// the full state machine in MaxSpeed mode.
#[test]
fn test_maxspeed_rom_fast_access() {
// Create ROM with 1ns access time (fast for testing)
let mut rom = Intel4001::new_with_access_time("FastROM".to_string(), 1);
rom.load_rom_data(vec![0x42], 0).unwrap();
// Drive through state machine with virtual time
// T=0: Idle state
assert_eq!(rom.get_timing_state(), TimingState::Idle);
// T=100ns: Set address (simulating PHI1 rising edge)
let t = Duration::from_nanos(100);
rom.set_address_high_nibble(Some(0x0));
rom.set_address_low_nibble(Some(0x0));
rom.set_full_address_ready(true);
rom.set_address_latch_time(Some(t));
rom.set_timing_state(TimingState::AddressPhase);
assert_eq!(rom.get_timing_state(), TimingState::AddressPhase);
// T=200ns: Wait for access time (1ns already elapsed)
rom.set_timing_state(TimingState::WaitLatency);
// T=300ns: Drive data
rom.set_timing_state(TimingState::DriveData);
assert_eq!(rom.get_timing_state(), TimingState::DriveData);
// Verify data is accessible
assert_eq!(rom.read_rom(0x00).unwrap(), 0x42);
}
}
// =============================================================================
// SingleStep Mode Tests
// =============================================================================
#[cfg(test)]
mod singlestep_tests {
use super::*;
/// Verify that each trigger_step() advances virtual time by exactly one tick.
#[test]
fn test_singlestep_advances_one_tick() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
clock.set_mode(SimulationMode::SingleStep);
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Wait for governor to start
std::thread::sleep(Duration::from_millis(50));
// Initial time should be 0
assert_eq!(clock.now_ns(), 0, "Initial time should be 0");
// Trigger one step
clock.trigger_step();
std::thread::sleep(Duration::from_millis(50));
assert_eq!(clock.now_ns(), 333, "After 1 step, time should be 333ns");
// Trigger another step
clock.trigger_step();
std::thread::sleep(Duration::from_millis(50));
assert_eq!(clock.now_ns(), 666, "After 2 steps, time should be 666ns");
// Trigger a third step
clock.trigger_step();
std::thread::sleep(Duration::from_millis(50));
assert_eq!(clock.now_ns(), 999, "After 3 steps, time should be 999ns");
clock.stop();
let _ = handle.join();
}
/// Verify that ROM state can be inspected at specific virtual times
/// using SingleStep mode.
#[test]
fn test_singlestep_rom_state_at_specific_time() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100)));
clock.set_mode(SimulationMode::SingleStep);
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Wait for governor to start
std::thread::sleep(Duration::from_millis(50));
// Create ROM with 500ns access time (standard Intel 4001)
let mut rom = Intel4001::new_with_access_time("StepROM".to_string(), 500);
rom.load_rom_data(vec![0xAB], 0).unwrap();
// Trigger 1 step → T=100ns — latch address
clock.trigger_step();
std::thread::sleep(Duration::from_millis(20));
let t_latch = clock.now();
assert!(t_latch >= Duration::from_nanos(100));
rom.set_address_latch_time(Some(t_latch));
rom.set_address_high_nibble(Some(0x0));
rom.set_address_low_nibble(Some(0x0));
rom.set_full_address_ready(true);
// At T=100ns, 0ns have elapsed since latch — access time NOT met
let timing = ComponentTiming::intel_4001();
assert!(
timing.check_access_time(Duration::ZERO).is_err(),
"At T=100ns (0ns since latch), access time (500ns) should NOT be met"
);
// Trigger 4 more steps → T=500ns (4 × 100ns = 400ns more)
for _ in 0..4 {
clock.trigger_step();
std::thread::sleep(Duration::from_millis(20));
}
// At T=500ns, 400ns have elapsed since latch at T=100ns — still NOT enough
let elapsed_400 = Duration::from_nanos(400);
assert!(
timing.check_access_time(elapsed_400).is_err(),
"At T=500ns (400ns since latch at T=100ns), access time (500ns) should NOT be met"
);
// Trigger 1 more step → T=600ns
clock.trigger_step();
std::thread::sleep(Duration::from_millis(20));
// At T=600ns, 500ns have elapsed since latch at T=100ns — exactly access time
let elapsed_500 = Duration::from_nanos(500);
assert!(
timing.check_access_time(elapsed_500).is_ok(),
"At T=600ns (500ns since latch at T=100ns), access time (500ns) SHOULD be met"
);
clock.stop();
let _ = handle.join();
}
/// Verify that trigger_step returns false when not in SingleStep mode.
#[test]
fn test_trigger_step_only_works_in_singlestep() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
// Default mode is RealTime — trigger_step should fail
assert!(
!clock.trigger_step(),
"trigger_step should return false in RealTime mode"
);
// Switch to MaxSpeed — trigger_step should still fail
clock.set_mode(SimulationMode::MaxSpeed);
assert!(
!clock.trigger_step(),
"trigger_step should return false in MaxSpeed mode"
);
// Switch to SingleStep — now it should work
clock.set_mode(SimulationMode::SingleStep);
assert!(
clock.trigger_step(),
"trigger_step should return true in SingleStep mode"
);
}
/// Verify precise tick count tracking in SingleStep mode.
#[test]
fn test_singlestep_tick_count() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100)));
clock.set_mode(SimulationMode::SingleStep);
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Wait for governor to start
std::thread::sleep(Duration::from_millis(50));
assert_eq!(clock.tick_count(), 0, "Initial tick count should be 0");
// Trigger 5 steps
for _ in 0..5 {
clock.trigger_step();
std::thread::sleep(Duration::from_millis(20));
}
// After 5 ticks of 100ns each
assert_eq!(clock.now_ns(), 500, "After 5 steps, time should be 500ns");
assert_eq!(clock.tick_count(), 5, "After 5 steps, tick_count should be 5");
clock.stop();
let _ = handle.join();
}
}
// =============================================================================
// Overclocking Tests
// =============================================================================
#[cfg(test)]
mod overclocking_tests {
use super::*;
/// Verify that ROM (500ns access time) fails timing check at 2 MHz.
///
/// At 2 MHz, the clock phase is 250ns. The ROM needs 500ns from address
/// valid to data ready, but only gets 250ns — a timing violation.
#[test]
fn test_overclocking_rom_fails_at_2mhz() {
let rom_timing = ComponentTiming::intel_4001();
// 2 MHz clock → 500ns cycle → 250ns per phase
let phase_time = Duration::from_nanos(250);
// ROM has 500ns access time, but phase is only 250ns
assert!(
rom_timing.check_access_time(phase_time).is_err(),
"ROM (500ns access) should FAIL at 2 MHz (250ns phase)"
);
// Also verify: ROM does NOT support 2 MHz
assert!(
!rom_timing.supports_frequency(2_000_000.0),
"ROM should NOT support 2 MHz (max is 750 kHz)"
);
}
/// Verify that RAM (500ns access time) fails timing check at 2 MHz.
#[test]
fn test_overclocking_ram_fails_at_2mhz() {
let ram_timing = ComponentTiming::intel_4002();
// 2 MHz clock → 250ns per phase
let phase_time = Duration::from_nanos(250);
// RAM has 500ns access time, but phase is only 250ns
assert!(
ram_timing.check_access_time(phase_time).is_err(),
"RAM (500ns access) should FAIL at 2 MHz (250ns phase)"
);
assert!(
!ram_timing.supports_frequency(2_000_000.0),
"RAM should NOT support 2 MHz (max is 750 kHz)"
);
}
/// Verify that the shift register (200ns access time) STILL WORKS at 2 MHz.
///
/// The Intel 4003 has a faster 200ns access time, so at 2 MHz (250ns phase),
/// there's still enough time for data to become valid. This models real
/// hardware behavior where faster chips continue working when slower ones fail.
#[test]
fn test_shift_register_works_at_higher_frequency() {
let sr_timing = ComponentTiming::intel_4003();
// 2 MHz clock → 250ns per phase
let phase_time = Duration::from_nanos(250);
// Shift register has 200ns access time, phase is 250ns — PLENTY of time
assert!(
sr_timing.check_access_time(phase_time).is_ok(),
"Shift register (200ns access) should WORK at 2 MHz (250ns phase)"
);
// Shift register supports 1 MHz
assert!(
sr_timing.supports_frequency(1_000_000.0),
"Shift register should support 1 MHz"
);
// But NOT 2 MHz (max is 1 MHz)
assert!(
!sr_timing.supports_frequency(2_000_000.0),
"Shift register should NOT support 2 MHz (max is 1 MHz)"
);
// However, the access time check passes because 250ns > 200ns.
// This is the key insight: access_time and max_clock_frequency are
// independent constraints. The access time is met (250ns >= 200ns)
// even though the frequency exceeds the spec.
}
/// Verify the overclocking scenario from the timing architecture plan:
/// At 2 MHz, ROM and RAM fail while the shift register keeps working.
#[test]
fn test_overclocking_scenario_comprehensive() {
let rom_timing = ComponentTiming::intel_4001();
let ram_timing = ComponentTiming::intel_4002();
let sr_timing = ComponentTiming::intel_4003();
let cpu_timing = ComponentTiming::intel_4004();
// 2 MHz clock: phase = 250ns
let phase_time_2mhz = Duration::from_nanos(250);
// ROM: 500ns access, 250ns available → FAIL
assert!(rom_timing.check_access_time(phase_time_2mhz).is_err());
// RAM: 500ns access, 250ns available → FAIL
assert!(ram_timing.check_access_time(phase_time_2mhz).is_err());
// Shift register: 200ns access, 250ns available → OK
assert!(sr_timing.check_access_time(phase_time_2mhz).is_ok());
// CPU: 0ns access time → always OK (CPU doesn't have "access time")
assert!(cpu_timing.check_access_time(phase_time_2mhz).is_ok());
// Now at standard 750 kHz: phase = 666ns
let phase_time_750khz = Duration::from_nanos(666);
// All components should work at 750 kHz
assert!(rom_timing.check_access_time(phase_time_750khz).is_ok());
assert!(ram_timing.check_access_time(phase_time_750khz).is_ok());
assert!(sr_timing.check_access_time(phase_time_750khz).is_ok());
assert!(cpu_timing.check_access_time(phase_time_750khz).is_ok());
}
/// Verify that ComponentTiming::min_clock_period() returns correct values.
#[test]
fn test_min_clock_period_per_component() {
let rom_timing = ComponentTiming::intel_4001();
let ram_timing = ComponentTiming::intel_4002();
let sr_timing = ComponentTiming::intel_4003();
// ROM: max 750 kHz → min period ~1333ns
let rom_period = rom_timing.min_clock_period();
assert!(
rom_period > Duration::from_nanos(1300) && rom_period < Duration::from_nanos(1400),
"ROM min clock period should be ~1333ns, got {:?}",
rom_period
);
// RAM: max 750 kHz → min period ~1333ns
let ram_period = ram_timing.min_clock_period();
assert!(
ram_period > Duration::from_nanos(1300) && ram_period < Duration::from_nanos(1400),
"RAM min clock period should be ~1333ns, got {:?}",
ram_period
);
// Shift register: max 1 MHz → min period 1000ns
let sr_period = sr_timing.min_clock_period();
assert!(
sr_period > Duration::from_nanos(900) && sr_period < Duration::from_nanos(1100),
"Shift register min clock period should be ~1000ns, got {:?}",
sr_period
);
}
/// Verify setup time and hold time checks work correctly.
#[test]
fn test_setup_and_hold_time_checks() {
let rom_timing = ComponentTiming::intel_4001();
// Setup time: 100ns
assert!(rom_timing.check_setup_time(Duration::from_nanos(150)).is_ok());
assert!(rom_timing.check_setup_time(Duration::from_nanos(100)).is_ok());
assert!(rom_timing.check_setup_time(Duration::from_nanos(50)).is_err());
// Hold time: 50ns
assert!(rom_timing.check_hold_time(Duration::from_nanos(100)).is_ok());
assert!(rom_timing.check_hold_time(Duration::from_nanos(50)).is_ok());
assert!(rom_timing.check_hold_time(Duration::from_nanos(25)).is_err());
}
/// Verify that timing violations contain meaningful error information.
#[test]
fn test_timing_violation_error_details() {
let rom_timing = ComponentTiming::intel_4001();
let err = rom_timing.check_access_time(Duration::from_nanos(100));
assert!(err.is_err());
let err_msg = format!("{:?}", err.unwrap_err());
assert!(
err_msg.contains("Intel 4001 ROM") || err_msg.contains("4001"),
"Violation should mention component name: {}",
err_msg
);
}
}
// =============================================================================
// Virtual Clock Integration Tests
// =============================================================================
#[cfg(test)]
mod virtual_clock_integration_tests {
use super::*;
/// Verify that ConfigurableSystem creates and exposes a VirtualClock.
#[test]
fn test_configurable_system_creates_virtual_clock() {
let factory = SystemFactory::new();
let system = factory.create_from_json("configs/mcs4_basic.json").unwrap();
// The system should have a virtual clock
let clock = system.get_virtual_clock();
assert!(Arc::strong_count(clock) >= 1, "VirtualClock Arc should exist");
// Clock should have correct tick size for 750 kHz (333ns per quarter-cycle)
let tick_size = clock.tick_size();
assert!(
tick_size > Duration::ZERO,
"Tick size should be non-zero"
);
// For 750 kHz, cycle time is ~1333ns, quarter-cycle is ~333ns
assert_eq!(
tick_size,
Duration::from_nanos(333),
"Tick size should be 333ns for 750 kHz"
);
}
/// Verify that ConfigurableSystem's VirtualClock can be put in MaxSpeed mode.
#[test]
fn test_configurable_system_maxspeed_mode() {
let factory = SystemFactory::new();
let system = factory.create_from_json("configs/mcs4_basic.json").unwrap();
let clock = system.get_virtual_clock();
clock.set_mode(SimulationMode::MaxSpeed);
assert_eq!(clock.mode(), SimulationMode::MaxSpeed);
}
/// Verify that clock.stop() causes the governor thread to exit cleanly.
#[test]
fn test_virtual_clock_stop_exits_governor() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Let the governor start and advance time
std::thread::sleep(Duration::from_millis(50));
assert!(clock.now_ns() > 0, "Clock should have advanced");
// Stop the clock
clock.stop();
// Governor thread should exit cleanly
let join_result = handle.join();
assert!(
join_result.is_ok(),
"Governor thread should exit cleanly after stop"
);
}
/// Verify that clock.stop() causes wait_until to return None.
#[test]
fn test_stop_unblocks_wait_until() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let clock_clone = Arc::clone(&clock);
let handle = std::thread::spawn(move || {
governor.run();
});
// Let the governor start
std::thread::sleep(Duration::from_millis(50));
// Now stop the clock
clock.stop();
// wait_until for a far-future time should return None immediately
let result = clock_clone.wait_until(Duration::from_secs(100));
assert!(
result.is_none(),
"wait_until should return None after clock is stopped"
);
let _ = handle.join();
}
/// Verify pause/resume behavior: time stops advancing when paused.
#[test]
fn test_pause_resume_behavior() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Let it run
std::thread::sleep(Duration::from_millis(50));
let time_before_pause = clock.now_ns();
assert!(time_before_pause > 0);
// Pause the clock
clock.pause();
assert!(clock.is_paused());
// Wait a short wall-clock time, then check virtual time hasn't advanced
std::thread::sleep(Duration::from_millis(50));
let time_after_pause = clock.now_ns();
// Virtual time should not have advanced significantly while paused
// (may have advanced by 1-2 ticks due to race, but not more)
let diff = time_after_pause.saturating_sub(time_before_pause);
assert!(
diff < 1000,
"Virtual time should not advance significantly while paused. \
Before: {}, After: {}, Diff: {}",
time_before_pause, time_after_pause, diff
);
// Resume the clock
clock.resume();
assert!(!clock.is_paused());
// Wait for time to advance again
std::thread::sleep(Duration::from_millis(50));
let time_after_resume = clock.now_ns();
assert!(
time_after_resume > time_after_pause,
"Virtual time should advance after resume"
);
clock.stop();
let _ = handle.join();
}
/// Verify that the VirtualClock reset works correctly.
#[test]
fn test_virtual_clock_reset() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Advance time
std::thread::sleep(Duration::from_millis(50));
assert!(clock.now_ns() > 0);
// Stop and reset
clock.stop();
let _ = handle.join();
// Reset time to zero
clock.reset();
// Verify time is back to zero
assert_eq!(clock.now(), Duration::ZERO);
assert_eq!(clock.tick_count(), 0);
}
/// Verify speed multiplier can be changed dynamically.
#[test]
fn test_speed_multiplier_change() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
// Default speed is 1.0x
assert!((clock.speed_multiplier() - 1.0).abs() < 0.001);
// Set to 0.5x
clock.set_speed_multiplier(0.5);
assert!((clock.speed_multiplier() - 0.5).abs() < 0.001);
// Set to 10.0x
clock.set_speed_multiplier(10.0);
assert!((clock.speed_multiplier() - 10.0).abs() < 0.01);
// Set to 0.001x (very slow)
clock.set_speed_multiplier(0.001);
assert!((clock.speed_multiplier() - 0.001).abs() < 0.0001);
}
/// Verify that has_reached works correctly for non-blocking time checks.
#[test]
fn test_has_reached_nonblocking() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = std::thread::spawn(move || {
governor.run();
});
// Initially, time is 0
assert!(clock.has_reached(Duration::ZERO));
assert!(!clock.has_reached(Duration::from_nanos(1)));
// Wait for time to advance
std::thread::sleep(Duration::from_millis(50));
// Now it should have reached various targets
assert!(clock.has_reached(Duration::from_nanos(100)));
assert!(clock.has_reached(Duration::from_nanos(1000)));
assert!(clock.has_reached(Duration::from_micros(5)));
clock.stop();
let _ = handle.join();
}
/// Verify tick_size can be changed dynamically.
#[test]
fn test_tick_size_change() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
assert_eq!(clock.tick_size(), Duration::from_nanos(333));
clock.set_tick_size(Duration::from_nanos(100));
assert_eq!(clock.tick_size(), Duration::from_nanos(100));
clock.set_tick_size(Duration::from_nanos(500));
assert_eq!(clock.tick_size(), Duration::from_nanos(500));
}
}
// =============================================================================
// Component Timing with Virtual Time Tests
// =============================================================================
#[cfg(test)]
mod component_virtual_time_tests {
use super::*;
/// Verify that Intel4001 implements the timing() method correctly.
#[test]
fn test_intel4001_timing_profile() {
let rom = Intel4001::new("TestROM".to_string());
let timing = rom.timing();
assert_eq!(timing.name, "Intel 4001 ROM");
assert_eq!(timing.access_time, Duration::from_nanos(500));
assert_eq!(timing.propagation_delay, Duration::from_nanos(200));
assert_eq!(timing.setup_time, Duration::from_nanos(100));
assert_eq!(timing.hold_time, Duration::from_nanos(50));
assert!((timing.max_clock_frequency - 750_000.0).abs() < 1.0);
}
/// Verify that Intel4002 implements the timing() method correctly.
#[test]
fn test_intel4002_timing_profile() {
let ram = Intel4002::new("TestRAM".to_string());
let timing = ram.timing();
assert_eq!(timing.name, "Intel 4002 RAM");
assert_eq!(timing.access_time, Duration::from_nanos(500));
assert_eq!(timing.propagation_delay, Duration::from_nanos(200));
}
/// Verify that Intel4003 implements the timing() method correctly.
#[test]
fn test_intel4003_timing_profile() {
let sr = Intel4003::new("TestSR".to_string());
let timing = sr.timing();
assert_eq!(timing.name, "Intel 4003 Shift Register");
assert_eq!(timing.access_time, Duration::from_nanos(200));
assert_eq!(timing.propagation_delay, Duration::from_nanos(100));
assert!((timing.max_clock_frequency - 1_000_000.0).abs() < 1.0);
}
/// Verify that update_at_time() works correctly for ROM component.
/// This tests the virtual-time-aware update path.
#[test]
fn test_rom_update_at_time() {
let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 1);
rom.load_rom_data(vec![0x42], 0).unwrap();
rom.set_running(true);
// Call update_at_time at various virtual times
// This should work without panicking
rom.update_at_time(Duration::from_nanos(0));
rom.update_at_time(Duration::from_nanos(333));
rom.update_at_time(Duration::from_nanos(666));
rom.update_at_time(Duration::from_nanos(999));
// Component should still function correctly
assert_eq!(rom.read_rom(0x00).unwrap(), 0x42);
}
/// Verify that update_at_time() works correctly for RAM component.
#[test]
fn test_ram_update_at_time() {
let mut ram = Intel4002::new("TestRAM".to_string());
ram.set_running(true);
// Call update_at_time at various virtual times
rom_update_at_time_helper(&mut ram);
}
fn rom_update_at_time_helper(ram: &mut Intel4002) {
ram.update_at_time(Duration::from_nanos(0));
ram.update_at_time(Duration::from_nanos(333));
ram.update_at_time(Duration::from_nanos(666));
}
/// Verify that update_at_time() works correctly for shift register component.
#[test]
fn test_sr_update_at_time() {
let mut sr = Intel4003::new("TestSR".to_string());
sr.set_running(true);
// Call update_at_time at various virtual times
sr.update_at_time(Duration::from_nanos(0));
sr.update_at_time(Duration::from_nanos(333));
sr.update_at_time(Duration::from_nanos(666));
// Should not panic
}
/// Test that address latch time tracking works with virtual time.
#[test]
fn test_address_latch_time_virtual_time() {
let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 500);
// Set address latch time to a specific virtual time
let latch_time = Duration::from_nanos(1000);
rom.set_address_latch_time(Some(latch_time));
assert_eq!(rom.get_address_latch_time(), Some(latch_time));
// Clear it
rom.set_address_latch_time(None);
assert_eq!(rom.get_address_latch_time(), None);
}
/// Verify that the timing state machine works correctly with virtual time.
#[test]
fn test_timing_state_machine_with_virtual_time() {
let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 500);
// Start in Idle
assert_eq!(rom.get_timing_state(), TimingState::Idle);
// Simulate a complete memory access cycle with virtual time
let t0 = Duration::ZERO; // Start
let _t1 = Duration::from_nanos(333); // Address phase
let _t2 = Duration::from_nanos(666); // Wait latency
let t3 = Duration::from_nanos(999); // Drive data
// T0: Idle
rom.update_at_time(t0);
assert_eq!(rom.get_timing_state(), TimingState::Idle);
// T1: Address latched
let t1 = Duration::from_nanos(333);
rom.set_address_latch_time(Some(t1));
rom.set_timing_state(TimingState::AddressPhase);
assert_eq!(rom.get_timing_state(), TimingState::AddressPhase);
// T2: Waiting for access time
rom.set_timing_state(TimingState::WaitLatency);
assert_eq!(rom.get_timing_state(), TimingState::WaitLatency);
// T3: Access time (666ns) > 500ns → data ready
let elapsed = t3.saturating_sub(t1);
let timing = ComponentTiming::intel_4001();
assert!(
timing.check_access_time(elapsed).is_ok(),
"After 666ns, access time (500ns) should be met"
);
rom.set_timing_state(TimingState::DriveData);
assert_eq!(rom.get_timing_state(), TimingState::DriveData);
// Return to idle
rom.set_timing_state(TimingState::Idle);
assert_eq!(rom.get_timing_state(), TimingState::Idle);
}
}