Extracted common 400x behavior like timing and latching into common module Added maxed MCS-4
423 lines
No EOL
17 KiB
Rust
423 lines
No EOL
17 KiB
Rust
//! Property-based tests for Intel 400x state machine verification
|
|
//!
|
|
//! These tests use property-based testing to verify the correctness
|
|
//! of state machine behavior, timing properties, and system invariants.
|
|
|
|
use rusty_emu::components::common::intel_400x::*;
|
|
use rusty_emu::pin::PinValue;
|
|
use crate::mocks::*;
|
|
use proptest::prelude::*;
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[cfg(test)]
|
|
mod state_machine_properties {
|
|
use super::*;
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_timing_state_invariants(
|
|
state in proptest_helpers::arb_timing_state()
|
|
) {
|
|
// Test that state queries are consistent
|
|
match state {
|
|
TimingState::Idle => {
|
|
prop_assert!(state.is_idle());
|
|
prop_assert!(!state.is_address_phase());
|
|
prop_assert!(!state.is_waiting_latency());
|
|
prop_assert!(!state.is_driving_data());
|
|
}
|
|
TimingState::AddressPhase => {
|
|
prop_assert!(!state.is_idle());
|
|
prop_assert!(state.is_address_phase());
|
|
prop_assert!(!state.is_waiting_latency());
|
|
prop_assert!(!state.is_driving_data());
|
|
}
|
|
TimingState::WaitLatency => {
|
|
prop_assert!(!state.is_idle());
|
|
prop_assert!(!state.is_address_phase());
|
|
prop_assert!(state.is_waiting_latency());
|
|
prop_assert!(!state.is_driving_data());
|
|
}
|
|
TimingState::DriveData => {
|
|
prop_assert!(!state.is_idle());
|
|
prop_assert!(!state.is_address_phase());
|
|
prop_assert!(!state.is_waiting_latency());
|
|
prop_assert!(state.is_driving_data());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_memory_state_conversions_roundtrip(
|
|
mem_state in proptest_helpers::arb_memory_state()
|
|
) {
|
|
// Test that conversions to TimingState and back preserve the state
|
|
let timing_state: TimingState = mem_state.into();
|
|
let back_to_mem: MemoryState = timing_state.into();
|
|
|
|
// They should be equivalent (though not necessarily identical due to enum differences)
|
|
match (&mem_state, &back_to_mem) {
|
|
(MemoryState::Idle, MemoryState::Idle) => prop_assert!(true),
|
|
(MemoryState::AddressPhase, MemoryState::AddressPhase) => prop_assert!(true),
|
|
(MemoryState::WaitLatency, MemoryState::WaitLatency) => prop_assert!(true),
|
|
(MemoryState::DriveData, MemoryState::DriveData) => prop_assert!(true),
|
|
_ => prop_assert!(false, "State conversion mismatch: {:?} -> {:?}", mem_state, back_to_mem),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ram_state_conversions_roundtrip(
|
|
ram_state in proptest_helpers::arb_ram_state()
|
|
) {
|
|
// Test that conversions to TimingState and back work correctly
|
|
let timing_state: TimingState = ram_state.into();
|
|
let back_to_ram: RamState = timing_state.into();
|
|
|
|
// Verify the conversion maintains the essential state information
|
|
match (&ram_state, &back_to_ram) {
|
|
(RamState::Idle, RamState::Idle) => prop_assert!(true),
|
|
(RamState::AddressPhase, RamState::AddressPhase) => prop_assert!(true),
|
|
(RamState::WaitLatency, RamState::WaitLatency) => prop_assert!(true),
|
|
// ReadData, WriteData, and OutputPort all map to DriveData in TimingState
|
|
(RamState::ReadData, RamState::ReadData) => prop_assert!(true),
|
|
(RamState::WriteData, RamState::ReadData) => prop_assert!(true), // This is expected
|
|
(RamState::OutputPort, RamState::ReadData) => prop_assert!(true), // This is expected
|
|
_ => prop_assert!(false, "Unexpected state conversion: {:?} -> {:?}", ram_state, back_to_ram),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_address_assembly_properties() {
|
|
let mut scenario = MockScenario::new("TestAddressAssembly");
|
|
|
|
// Test that address assembly is deterministic and correct
|
|
let test_cases = vec![
|
|
(0x00, 0x00, 0x0000),
|
|
(0x12, 0x34, 0x1234),
|
|
(0xFF, 0xFF, 0xFFFF),
|
|
(0x0A, 0x0B, 0x0A0B),
|
|
(0x01, 0x23, 0x0123),
|
|
];
|
|
|
|
for (high, low, expected) in test_cases {
|
|
scenario.component.set_address_high_nibble(Some(high));
|
|
scenario.component.set_address_low_nibble(Some(low));
|
|
scenario.component.set_full_address_ready(true);
|
|
|
|
// In a real implementation, this would test the actual trait method
|
|
// For now, we verify our mock state is consistent
|
|
assert_eq!(scenario.component.get_address_high_nibble(), Some(high));
|
|
assert_eq!(scenario.component.get_address_low_nibble(), Some(low));
|
|
assert_eq!(scenario.component.get_full_address_ready(), true);
|
|
|
|
// Clear for next test
|
|
scenario.component.set_address_high_nibble(None);
|
|
scenario.component.set_address_low_nibble(None);
|
|
scenario.component.set_full_address_ready(false);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_address_nibble_assembly(
|
|
high_nibble in proptest_helpers::arb_address_nibble(),
|
|
low_nibble in proptest_helpers::arb_address_nibble()
|
|
) {
|
|
// Test that address assembly works for all valid nibble combinations
|
|
let expected_address = ((high_nibble as u16) << 4) | (low_nibble as u16);
|
|
|
|
let mut scenario = MockScenario::new("TestNibbleAssembly");
|
|
scenario.component.set_address_high_nibble(Some(high_nibble));
|
|
scenario.component.set_address_low_nibble(Some(low_nibble));
|
|
|
|
// Verify the nibbles are stored correctly
|
|
prop_assert_eq!(scenario.component.get_address_high_nibble(), Some(high_nibble));
|
|
prop_assert_eq!(scenario.component.get_address_low_nibble(), Some(low_nibble));
|
|
|
|
// The expected address should be correctly calculated
|
|
prop_assert_eq!(expected_address, ((high_nibble as u16) << 4) | (low_nibble as u16));
|
|
}
|
|
|
|
#[test]
|
|
fn test_data_bus_bit_pattern_consistency(
|
|
data_value in proptest_helpers::arb_data_value()
|
|
) {
|
|
let mut scenario = MockScenario::new("TestDataBusConsistency");
|
|
|
|
// Set data bus value
|
|
scenario.set_data_bus_value(data_value);
|
|
|
|
// Read it back
|
|
let read_value = scenario.get_data_bus_value();
|
|
|
|
// Should be identical (roundtrip consistency)
|
|
prop_assert_eq!(data_value, read_value);
|
|
|
|
// Verify individual bits
|
|
for i in 0..4 {
|
|
let expected_bit = (data_value >> i) & 1;
|
|
let pin_name = format!("D{}", i);
|
|
let actual_pin_value = scenario.component.get_pin_value(&pin_name);
|
|
|
|
match (expected_bit, actual_pin_value) {
|
|
(0, Some(PinValue::Low)) => prop_assert!(true),
|
|
(1, Some(PinValue::High)) => prop_assert!(true),
|
|
_ => prop_assert!(false, "Bit {} mismatch for value 0x{:02X}", i, data_value),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_timing_duration_properties(
|
|
duration_nanos in proptest_helpers::arb_duration()
|
|
) {
|
|
let duration = Duration::from_nanos(duration_nanos);
|
|
|
|
// Test duration properties
|
|
prop_assert!(duration.as_nanos() >= 0);
|
|
prop_assert_eq!(duration.as_nanos(), duration_nanos as u128);
|
|
|
|
// Test that our timing constants are reasonable
|
|
prop_assert!(TimingConstants::DEFAULT_ACCESS_TIME > Duration::from_nanos(0));
|
|
prop_assert!(TimingConstants::FAST_ACCESS_TIME > Duration::from_nanos(0));
|
|
prop_assert!(TimingConstants::ADDRESS_SETUP > Duration::from_nanos(0));
|
|
prop_assert!(TimingConstants::DATA_VALID > Duration::from_nanos(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod timing_invariant_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_state_machine_never_invalid() {
|
|
// Test that our state machine never enters invalid states
|
|
let mut scenario = MockScenario::new("TestStateMachineValidity");
|
|
|
|
let valid_states = vec![
|
|
TimingState::Idle,
|
|
TimingState::AddressPhase,
|
|
TimingState::WaitLatency,
|
|
TimingState::DriveData,
|
|
];
|
|
|
|
for state in valid_states {
|
|
scenario.component.set_timing_state(state);
|
|
|
|
// State should be queryable and consistent
|
|
assert_eq!(scenario.component.get_timing_state(), state);
|
|
|
|
// State queries should be mutually exclusive where appropriate
|
|
let idle = state.is_idle();
|
|
let address = state.is_address_phase();
|
|
let waiting = state.is_waiting_latency();
|
|
let driving = state.is_driving_data();
|
|
|
|
// Only one "active" state should be true at a time
|
|
let active_states = vec![address, waiting, driving];
|
|
let active_count = active_states.iter().filter(|&&x| x).count();
|
|
|
|
if idle {
|
|
assert_eq!(active_count, 0, "Idle state should not have active flags");
|
|
} else {
|
|
assert!(active_count > 0, "Non-idle state should have at least one active flag");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_address_latch_invariants() {
|
|
let mut scenario = MockScenario::new("TestAddressLatchInvariants");
|
|
|
|
// Test address latch state consistency
|
|
scenario.component.set_address_high_nibble(Some(0x12));
|
|
scenario.component.set_address_low_nibble(Some(0x34));
|
|
scenario.component.set_full_address_ready(true);
|
|
|
|
// All three should be consistent
|
|
assert_eq!(scenario.component.get_address_high_nibble(), Some(0x12));
|
|
assert_eq!(scenario.component.get_address_low_nibble(), Some(0x34));
|
|
assert_eq!(scenario.component.get_full_address_ready(), true);
|
|
|
|
// Test partial state
|
|
scenario.component.set_address_high_nibble(Some(0x56));
|
|
scenario.component.set_address_low_nibble(None);
|
|
scenario.component.set_full_address_ready(false);
|
|
|
|
assert_eq!(scenario.component.get_address_high_nibble(), Some(0x56));
|
|
assert_eq!(scenario.component.get_address_low_nibble(), None);
|
|
assert_eq!(scenario.component.get_full_address_ready(), false);
|
|
|
|
// Test empty state
|
|
scenario.component.set_address_high_nibble(None);
|
|
scenario.component.set_address_low_nibble(None);
|
|
scenario.component.set_full_address_ready(false);
|
|
|
|
assert_eq!(scenario.component.get_address_high_nibble(), None);
|
|
assert_eq!(scenario.component.get_address_low_nibble(), None);
|
|
assert_eq!(scenario.component.get_full_address_ready(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pin_state_consistency() {
|
|
let scenario = MockScenario::new("TestPinConsistency");
|
|
|
|
// Test that pin states are consistent
|
|
let test_values = vec![
|
|
PinValue::Low,
|
|
PinValue::High,
|
|
PinValue::HighZ,
|
|
];
|
|
|
|
for value in test_values {
|
|
scenario.component.set_pin_value("D0", value);
|
|
assert_eq!(scenario.component.get_pin_value("D0"), Some(value));
|
|
|
|
scenario.component.set_pin_value("PHI1", value);
|
|
assert_eq!(scenario.component.get_pin_value("PHI1"), Some(value));
|
|
|
|
scenario.set_pin_value("SYNC", value);
|
|
assert_eq!(scenario.component.get_pin_value("SYNC"), Some(value));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_bus_operation_invariants(
|
|
initial_value in proptest_helpers::arb_data_value(),
|
|
new_value in proptest_helpers::arb_data_value()
|
|
) {
|
|
let mut scenario = MockScenario::new("TestBusInvariants");
|
|
|
|
// Set initial value
|
|
scenario.set_data_bus_value(initial_value);
|
|
prop_assert_eq!(scenario.get_data_bus_value(), initial_value);
|
|
|
|
// Change to new value
|
|
scenario.set_data_bus_value(new_value);
|
|
prop_assert_eq!(scenario.get_data_bus_value(), new_value);
|
|
|
|
// Values should be different (unless they happen to be the same)
|
|
if initial_value != new_value {
|
|
// This is just documenting expected behavior
|
|
prop_assert!(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod edge_case_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_boundary_timing_values() {
|
|
// Test boundary conditions for timing
|
|
let zero_duration = Duration::from_nanos(0);
|
|
let small_duration = Duration::from_nanos(1);
|
|
let large_duration = Duration::from_nanos(1_000_000_000); // 1 second
|
|
|
|
// Test that our timing system can handle these values
|
|
let mut scenario = MockScenario::new("TestBoundaryTiming");
|
|
|
|
scenario.time_provider.advance(zero_duration);
|
|
scenario.time_provider.advance(small_duration);
|
|
scenario.time_provider.advance(large_duration);
|
|
|
|
// Verify time advancement works
|
|
assert!(scenario.time_provider.now() > Instant::now() - large_duration * 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_maximum_address_values() {
|
|
let mut scenario = MockScenario::new("TestMaxAddress");
|
|
|
|
// Test maximum address values
|
|
scenario.component.set_address_high_nibble(Some(0xFF));
|
|
scenario.component.set_address_low_nibble(Some(0xFF));
|
|
scenario.component.set_full_address_ready(true);
|
|
|
|
assert_eq!(scenario.component.get_address_high_nibble(), Some(0xFF));
|
|
assert_eq!(scenario.component.get_address_low_nibble(), Some(0xFF));
|
|
assert_eq!(scenario.component.get_full_address_ready(), true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_minimum_address_values() {
|
|
let mut scenario = MockScenario::new("TestMinAddress");
|
|
|
|
// Test minimum address values
|
|
scenario.component.set_address_high_nibble(Some(0x00));
|
|
scenario.component.set_address_low_nibble(Some(0x00));
|
|
scenario.component.set_full_address_ready(true);
|
|
|
|
assert_eq!(scenario.component.get_address_high_nibble(), Some(0x00));
|
|
assert_eq!(scenario.component.get_address_low_nibble(), Some(0x00));
|
|
assert_eq!(scenario.component.get_full_address_ready(), true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rapid_state_transitions() {
|
|
let mut scenario = MockScenario::new("TestRapidTransitions");
|
|
|
|
// Test rapid state changes
|
|
let states = vec![
|
|
TimingState::Idle,
|
|
TimingState::AddressPhase,
|
|
TimingState::WaitLatency,
|
|
TimingState::DriveData,
|
|
TimingState::Idle,
|
|
];
|
|
|
|
for state in states {
|
|
scenario.component.set_timing_state(state);
|
|
assert_eq!(scenario.component.get_timing_state(), state);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod concurrency_safety_tests {
|
|
use super::*;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
|
|
#[test]
|
|
fn test_mock_component_thread_safety() {
|
|
let scenario = Arc::new(MockScenario::new("TestThreadSafety"));
|
|
|
|
// Test that our mock can handle concurrent access patterns
|
|
let handles: Vec<_> = (0..4).map(|i| {
|
|
let scenario_clone: std::sync::Arc<MockScenario> = Arc::clone(&scenario);
|
|
thread::spawn(move || {
|
|
// Each thread tests different aspects
|
|
match i {
|
|
0 => {
|
|
// Note: This would need proper Arc<Mutex<>> handling for thread safety
|
|
// For now, we'll skip the mutable operation in this test
|
|
// Note: Thread safety testing would require proper Arc<Mutex<>> handling
|
|
}
|
|
1 => {
|
|
scenario_clone.component.set_pin_value("D0", PinValue::High);
|
|
assert_eq!(scenario_clone.component.get_pin_value("D0"), Some(PinValue::High));
|
|
}
|
|
2 => {
|
|
scenario_clone.component.set_address_high_nibble(Some(0x12));
|
|
assert_eq!(scenario_clone.component.get_address_high_nibble(), Some(0x12));
|
|
}
|
|
3 => {
|
|
scenario_clone.advance_time(Duration::from_nanos(100));
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
})
|
|
}).collect();
|
|
|
|
// Wait for all threads to complete
|
|
for handle in handles {
|
|
handle.join().expect("Thread should complete successfully");
|
|
}
|
|
}
|
|
} |