Added more tests

Extracted common 400x behavior like timing and latching into common module
Added maxed MCS-4
This commit is contained in:
Jan-Peter von Hunnius 2025-09-29 11:53:46 +02:00
parent dd93e33087
commit c039e9069a
25 changed files with 5694 additions and 494 deletions

View file

@ -0,0 +1,423 @@
//! 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");
}
}
}