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:
parent
add7ace674
commit
08f53d68fe
28 changed files with 7186 additions and 270 deletions
888
tests/virtual_time_tests.rs
Normal file
888
tests/virtual_time_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue