use alloc::boxed::Box;
use alloc::sync::Arc;
use core::fmt;
use crate::core::device::{Device, DeviceClass, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::Props;
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
use crate::machine::realize::Instance;
const CLASS_NAME: &str = "wdc.w65c22";
const STATE_VERSION: u32 = 1;
pub const REGISTER_COUNT: u64 = 16;
pub const IRQ_PIN: &str = "irq";
const IFR_T1: u8 = 0x40;
const IFR_T2: u8 = 0x20;
const IFR_ANY: u8 = 0x80;
const ACR_T1_FREE_RUN: u8 = 0x40;
const ACR_T2_PULSE: u8 = 0x20;
const NO_EVENT: u64 = u64::MAX;
#[derive(Debug)]
pub struct Via {
shared: Arc<Shared>,
region: RegionRef,
}
struct Shared {
state: Mutex<State>,
ticks: AtomicU64,
next_event: AtomicU64,
irq: Mutex<Option<WireSource>>,
lazy: Mutex<Option<LazyHandle>>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct State {
ticks: u64,
ora: u8,
orb: u8,
ddra: u8,
ddrb: u8,
pa_in: u8,
pb_in: u8,
t1_counter: u16,
t1_latch: u16,
t1_fired: bool,
t2_counter: u16,
t2_latch_low: u8,
t2_fired: bool,
sr: u8,
acr: u8,
pcr: u8,
ifr: u8,
ier: u8,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Shared");
match self.state.try_lock() {
Some(state) => s.field("state", &*state).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
impl Via {
pub fn new(props: &Props) -> Result<Via> {
props.reader().finish()?;
Ok(Via::bare())
}
#[must_use]
pub fn bare() -> Via {
let shared = Arc::new(Shared {
state: Mutex::with_rank(LockRank::DEVICE, State::default()),
ticks: AtomicU64::new(0),
next_event: AtomicU64::new(NO_EVENT),
irq: Mutex::with_rank(LockRank::WIRE, None),
lazy: Mutex::with_rank(LockRank::WIRE, None),
});
shared.publish(&shared.state.lock());
let port = Arc::new(ViaPort {
shared: Arc::clone(&shared),
});
let region = Arc::new(Region::io("via", REGISTER_COUNT, port as Arc<dyn MemOps>));
Via { shared, region }
}
pub fn set_port_a(&self, level: u8) {
self.shared.state.lock().pa_in = level;
}
pub fn set_port_b(&self, level: u8) {
self.shared.state.lock().pb_in = level;
}
#[must_use]
pub fn port_a(&self) -> u8 {
let state = self.shared.state.lock();
(state.ora & state.ddra) | (state.pa_in & !state.ddra)
}
#[must_use]
pub fn port_b(&self) -> u8 {
let state = self.shared.state.lock();
(state.orb & state.ddrb) | (state.pb_in & !state.ddrb)
}
#[must_use]
pub fn timer1(&self) -> u16 {
self.shared.state.lock().t1_counter
}
#[must_use]
pub fn timer2(&self) -> u16 {
self.shared.state.lock().t2_counter
}
#[must_use]
pub fn ifr(&self) -> u8 {
Shared::visible_ifr(&self.shared.state.lock())
}
#[must_use]
pub fn ticks(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
pub fn connect_irq(&self, source: WireSource) {
*self.shared.irq.lock() = Some(source);
self.shared.refresh_irq();
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
#[must_use]
pub fn irq_level(&self) -> Level {
Shared::level(&self.shared.state.lock())
}
pub fn advance_to(&self, target: u64) {
self.shared.advance_to(target);
}
}
impl Shared {
fn publish(&self, state: &State) {
self.ticks.store(state.ticks, Ordering::Relaxed);
self.next_event
.store(State::next_event(state), Ordering::Relaxed);
}
fn level(state: &State) -> Level {
if state.ifr & state.ier & !IFR_ANY != 0 {
Level::High
} else {
Level::Low
}
}
fn visible_ifr(state: &State) -> u8 {
let mut value = state.ifr;
if Shared::level(state) == Level::High {
value |= IFR_ANY;
}
value
}
fn refresh_irq(&self) {
let level = Shared::level(&self.state.lock());
let port = self.irq.lock().clone();
if let Some(port) = port {
port.set(level);
}
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
fn advance_to(&self, target: u64) {
let moved = {
let mut state = self.state.lock();
if target <= state.ticks {
return;
}
let elapsed = target - state.ticks;
let before = Shared::level(&state);
state.run(elapsed);
state.ticks = target;
self.publish(&state);
before != Shared::level(&state)
};
if moved {
self.refresh_irq();
}
}
}
impl State {
fn run(&mut self, elapsed: u64) {
if self.acr & ACR_T1_FREE_RUN != 0 {
let period = u64::from(self.t1_latch) + 1;
let first = u64::from(self.t1_counter) + 1;
if elapsed < first {
self.t1_counter -= elapsed as u16;
} else {
let after = elapsed - first;
self.t1_counter = self.t1_latch - (after % period) as u16;
self.ifr |= IFR_T1;
self.t1_fired = true;
}
} else {
let timed_out = elapsed > u64::from(self.t1_counter);
self.t1_counter = self.t1_counter.wrapping_sub((elapsed % 0x1_0000) as u16);
if timed_out && !self.t1_fired {
self.ifr |= IFR_T1;
self.t1_fired = true;
}
}
if self.acr & ACR_T2_PULSE == 0 {
let timed_out = elapsed > u64::from(self.t2_counter);
self.t2_counter = self.t2_counter.wrapping_sub((elapsed % 0x1_0000) as u16);
if timed_out && !self.t2_fired {
self.ifr |= IFR_T2;
self.t2_fired = true;
}
}
}
fn next_event(&self) -> u64 {
let mut next = NO_EVENT;
if self.acr & ACR_T1_FREE_RUN != 0 || !self.t1_fired {
next = next.min(self.ticks + u64::from(self.t1_counter) + 1);
}
if self.acr & ACR_T2_PULSE == 0 && !self.t2_fired {
next = next.min(self.ticks + u64::from(self.t2_counter) + 1);
}
next
}
}
struct ViaPort {
shared: Arc<Shared>,
}
impl fmt::Debug for ViaPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ViaPort").finish_non_exhaustive()
}
}
impl ViaPort {
fn read_register(&self, index: u8, debug: bool) -> u8 {
let mut state = self.shared.state.lock();
match index {
0x0 => (state.orb & state.ddrb) | (state.pb_in & !state.ddrb),
0x1 | 0xf => (state.ora & state.ddra) | (state.pa_in & !state.ddra),
0x2 => state.ddrb,
0x3 => state.ddra,
0x4 => {
if !debug {
state.ifr &= !IFR_T1;
}
state.t1_counter as u8
}
0x5 => (state.t1_counter >> 8) as u8,
0x6 => state.t1_latch as u8,
0x7 => (state.t1_latch >> 8) as u8,
0x8 => {
if !debug {
state.ifr &= !IFR_T2;
}
state.t2_counter as u8
}
0x9 => (state.t2_counter >> 8) as u8,
0xa => state.sr,
0xb => state.acr,
0xc => state.pcr,
0xd => Shared::visible_ifr(&state),
_ => state.ier | IFR_ANY,
}
}
fn write_register(&self, index: u8, value: u8) {
let mut state = self.shared.state.lock();
match index {
0x0 => state.orb = value,
0x1 | 0xf => state.ora = value,
0x2 => state.ddrb = value,
0x3 => state.ddra = value,
0x4 | 0x6 => state.t1_latch = (state.t1_latch & 0xff00) | u16::from(value),
0x5 => {
state.t1_latch = (state.t1_latch & 0x00ff) | (u16::from(value) << 8);
state.t1_counter = state.t1_latch;
state.t1_fired = false;
state.ifr &= !IFR_T1;
}
0x7 => {
state.t1_latch = (state.t1_latch & 0x00ff) | (u16::from(value) << 8);
state.ifr &= !IFR_T1;
}
0x8 => state.t2_latch_low = value,
0x9 => {
state.t2_counter = (u16::from(value) << 8) | u16::from(state.t2_latch_low);
state.t2_fired = false;
state.ifr &= !IFR_T2;
}
0xa => state.sr = value,
0xb => state.acr = value,
0xc => state.pcr = value,
0xd => state.ifr &= !(value & !IFR_ANY),
_ => {
if value & IFR_ANY != 0 {
state.ier |= value & !IFR_ANY;
} else {
state.ier &= !(value & !IFR_ANY);
}
}
}
self.shared.publish(&state);
}
}
impl MemOps for ViaPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
*byte = self.read_register((offset & 0xf) as u8, attrs.debug);
if !attrs.debug {
self.shared.refresh_irq();
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
self.write_register((offset & 0xf) as u8, *value);
self.shared.refresh_irq();
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
impl Device for Via {
fn class(&self) -> &'static DeviceClass {
&VIA_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
let mut state = self.shared.state.lock();
let keep = *state;
*state = State {
ticks: keep.ticks,
t1_counter: keep.t1_counter,
t1_latch: keep.t1_latch,
t1_fired: keep.t1_fired,
t2_counter: keep.t2_counter,
t2_latch_low: keep.t2_latch_low,
t2_fired: keep.t2_fired,
sr: keep.sr,
..State::default()
};
self.shared.publish(&state);
drop(state);
self.shared.refresh_irq();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = *self.shared.state.lock();
w.write_u64(state.ticks)?;
w.write_u8(state.ora)?;
w.write_u8(state.orb)?;
w.write_u8(state.ddra)?;
w.write_u8(state.ddrb)?;
w.write_u16(state.t1_counter)?;
w.write_u16(state.t1_latch)?;
w.write_bool(state.t1_fired)?;
w.write_u16(state.t2_counter)?;
w.write_u8(state.t2_latch_low)?;
w.write_bool(state.t2_fired)?;
w.write_u8(state.sr)?;
w.write_u8(state.acr)?;
w.write_u8(state.pcr)?;
w.write_u8(state.ifr)?;
w.write_u8(state.ier)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let mut state = self.shared.state.lock();
*state = State {
ticks: r.read_u64()?,
ora: r.read_u8()?,
orb: r.read_u8()?,
ddra: r.read_u8()?,
ddrb: r.read_u8()?,
pa_in: 0,
pb_in: 0,
t1_counter: r.read_u16()?,
t1_latch: r.read_u16()?,
t1_fired: r.read_bool()?,
t2_counter: r.read_u16()?,
t2_latch_low: r.read_u8()?,
t2_fired: r.read_bool()?,
sr: r.read_u8()?,
acr: r.read_u8()?,
pcr: r.read_u8()?,
ifr: r.read_u8()?,
ier: r.read_u8()?,
};
self.shared.publish(&state);
drop(state);
self.shared.refresh_irq();
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != IRQ_PIN {
return Err(Error::Config {
at: alloc::string::String::from(port),
message: alloc::format!("the VIA drives only `{IRQ_PIN}`"),
});
}
self.connect_irq(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == IRQ_PIN {
self.shared.refresh_irq();
}
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.ticks.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
Via::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
match self.shared.next_event.load(Ordering::Relaxed) {
NO_EVENT => None,
tick => Some(tick),
}
}
fn attach_lazy(&self, handle: LazyHandle) {
Via::attach_lazy(self, handle);
}
}
impl Instance for Via {}
pub static VIA_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "WDC W65C22 VIA: two 8-bit ports with data direction, and both timers",
properties: &[],
construct: |props| Ok(Box::new(Via::new(props)?)),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&VIA_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Via::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir};
ClassSchema::new(CLASS_NAME)
.port(IRQ_PIN, PortDir::Out)
.region("")
.region("regs")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::wire::{Wire, WireId};
use alloc::string::ToString;
use alloc::vec::Vec;
fn port(via: &Via) -> Arc<ViaPort> {
Arc::new(ViaPort {
shared: Arc::clone(&via.shared),
})
}
fn peek(via: &Via, index: u64) -> u8 {
let mut byte = [0u8; 1];
port(via)
.read(index, &mut byte, MemAttrs::DEFAULT)
.expect("a byte read is legal");
byte[0]
}
fn peek_debug(via: &Via, index: u64) -> u8 {
let mut byte = [0u8; 1];
port(via)
.read(index, &mut byte, MemAttrs::DEBUG)
.expect("a byte read is legal");
byte[0]
}
fn poke(via: &Via, index: u64, value: u8) {
port(via)
.write(index, &[value], MemAttrs::DEFAULT)
.expect("a byte write is legal");
}
fn dummy_source() -> WireSource {
let src = WireId::new(1);
WireSource::new(Wire::builder().source(src).build_shared(), src)
}
#[test]
fn the_two_ports_read_back_differently_and_that_is_the_point() {
let via = Via::bare();
poke(&via, 0x2, 0x0f); poke(&via, 0x0, 0xff); via.set_port_b(0xa0);
assert_eq!(peek(&via, 0x0), 0xaf, "0xA from the pins, 0xF from ORB");
assert_eq!(via.port_b(), 0xaf);
poke(&via, 0x3, 0xf0); poke(&via, 0x1, 0x55); via.set_port_a(0x0f);
assert_eq!(peek(&via, 0x1), 0x5f);
assert_eq!(peek(&via, 0xf), 0x5f, "$F is $1 without the handshake");
assert_eq!(peek(&via, 0x2), 0x0f);
assert_eq!(peek(&via, 0x3), 0xf0);
}
#[test]
fn timer_one_counts_phi2_down_and_sets_its_flag_once() {
let via = Via::bare();
assert_eq!(via.shared.next_event.load(Ordering::Relaxed), 1);
via.advance_to(1);
assert_eq!(via.ifr() & (IFR_T1 | IFR_T2), IFR_T1 | IFR_T2);
poke(&via, 0xd, IFR_T1 | IFR_T2);
assert_eq!(via.shared.next_event.load(Ordering::Relaxed), NO_EVENT);
poke(&via, 0x4, 0x09); poke(&via, 0x5, 0x00); assert_eq!(via.timer1(), 9);
assert_eq!(via.ifr() & IFR_T1, 0);
assert_eq!(
via.shared.next_event.load(Ordering::Relaxed),
11,
"a counter of 9 loaded on tick 1 times out on tick 11"
);
via.advance_to(10);
assert_eq!(via.timer1(), 0);
assert_eq!(via.ifr() & IFR_T1, 0, "not yet");
via.advance_to(11);
assert_eq!(via.ifr() & IFR_T1, IFR_T1, "IFR6 on the count to zero");
assert_eq!(via.timer1(), 0xffff, "and it rolls over");
poke(&via, 0xd, IFR_T1); assert_eq!(via.ifr() & IFR_T1, 0);
via.advance_to(11 + 0x1_0000);
assert_eq!(via.ifr() & IFR_T1, 0, "one shot means one");
assert_eq!(via.shared.next_event.load(Ordering::Relaxed), NO_EVENT);
}
#[test]
fn reading_the_low_counter_clears_the_flag_and_reading_the_latch_does_not() {
let via = Via::bare();
poke(&via, 0x4, 0x02);
poke(&via, 0x5, 0x00);
via.advance_to(3);
assert_eq!(via.ifr() & IFR_T1, IFR_T1);
assert_eq!(peek(&via, 0x6), 0x02, "T1L-L");
assert_eq!(peek(&via, 0x7), 0x00, "T1L-H");
assert_eq!(via.ifr() & IFR_T1, IFR_T1, "a latch read clears nothing");
let _ = peek(&via, 0x4);
assert_eq!(via.ifr() & IFR_T1, 0, "a counter read does");
}
#[test]
fn free_run_reloads_from_the_latches_and_keeps_going() {
let via = Via::bare();
poke(&via, 0xb, ACR_T1_FREE_RUN);
poke(&via, 0x4, 0x03);
poke(&via, 0x5, 0x00); via.advance_to(4);
assert_eq!(via.ifr() & IFR_T1, IFR_T1);
assert_eq!(via.timer1(), 3, "reloaded from the latches");
poke(&via, 0xd, IFR_T1);
via.advance_to(8);
assert_eq!(via.ifr() & IFR_T1, IFR_T1, "and again");
assert_eq!(via.timer1(), 3);
poke(&via, 0xd, IFR_T1);
via.advance_to(8 + 4 * 1000 + 2);
assert_eq!(via.ifr() & IFR_T1, IFR_T1);
assert_eq!(via.timer1(), 1);
}
#[test]
fn timer_two_counts_phi2_unless_it_is_told_to_count_pb6() {
let via = Via::bare();
poke(&via, 0x8, 0x05); poke(&via, 0x9, 0x00); assert_eq!(via.timer2(), 5);
via.advance_to(6);
assert_eq!(via.ifr() & IFR_T2, IFR_T2);
let _ = peek(&via, 0x8);
assert_eq!(via.ifr() & IFR_T2, 0);
let via = Via::bare();
poke(&via, 0xb, ACR_T2_PULSE);
poke(&via, 0x8, 0x05);
poke(&via, 0x9, 0x00);
via.advance_to(1000);
assert_eq!(via.timer2(), 5, "no pulses, no counting");
assert_eq!(via.ifr() & IFR_T2, 0);
}
#[test]
fn the_enable_register_sets_and_clears_by_its_top_bit() {
let via = Via::bare();
assert_eq!(peek(&via, 0xe), 0x80, "read back with bit 7 set");
poke(&via, 0xe, 0x80 | IFR_T1 | IFR_T2); assert_eq!(peek(&via, 0xe), 0x80 | IFR_T1 | IFR_T2);
poke(&via, 0xe, IFR_T2); assert_eq!(peek(&via, 0xe), 0x80 | IFR_T1);
poke(&via, 0xe, 0x00);
assert_eq!(peek(&via, 0xe), 0x80 | IFR_T1, "a zero changes nothing");
}
#[test]
fn the_interrupt_output_follows_the_flags_and_their_enables() {
let via = Via::bare();
via.connect_irq(dummy_source());
assert_eq!(via.irq_level(), Level::Low);
poke(&via, 0x4, 0x01);
poke(&via, 0x5, 0x00);
via.advance_to(2);
assert_eq!(via.ifr() & IFR_T1, IFR_T1);
assert_eq!(via.ifr() & IFR_ANY, 0, "IFR7 is the *enabled* wired-OR");
assert_eq!(via.irq_level(), Level::Low);
poke(&via, 0xe, 0x80 | IFR_T1);
assert_eq!(via.irq_level(), Level::High);
assert_eq!(via.ifr() & IFR_ANY, IFR_ANY);
poke(&via, 0xd, IFR_T1);
assert_eq!(via.irq_level(), Level::Low);
}
#[test]
fn a_debug_access_advances_nothing_and_clears_nothing() {
let via = Via::bare();
poke(&via, 0x4, 0x01);
poke(&via, 0x5, 0x00);
via.advance_to(2);
assert_eq!(via.ifr() & IFR_T1, IFR_T1);
assert_eq!(peek_debug(&via, 0x4), 0xff, "the counter rolled over");
assert_eq!(via.ifr() & IFR_T1, IFR_T1, "and the flag is still there");
assert_eq!(
port(&via).write(0x5, &[0x00], MemAttrs::DEBUG),
Err(BusError::BadAccess)
);
}
#[test]
fn only_byte_accesses_are_accepted() {
let via = Via::bare();
let p = port(&via);
assert_eq!(
p.read(0, &mut [0u8; 2], MemAttrs::DEFAULT),
Err(BusError::BadAccess)
);
assert_eq!(
p.write(0, &[0, 0], MemAttrs::DEFAULT),
Err(BusError::BadAccess)
);
assert_eq!(p.constraints().min, Width::U8);
}
#[test]
fn a_reset_clears_the_control_registers_and_leaves_the_timers() {
let via = Via::bare();
poke(&via, 0x2, 0xff);
poke(&via, 0x0, 0xa5);
poke(&via, 0x4, 0x34);
poke(&via, 0x5, 0x12);
poke(&via, 0xe, 0x80 | IFR_T1);
via.reset(ResetKind::Cold);
assert_eq!(peek(&via, 0x2), 0, "DDRB");
assert_eq!(peek(&via, 0xe), 0x80, "IER");
assert_eq!(via.timer1(), 0x1234, "§3.9: the counters survive it");
assert_eq!(peek(&via, 0x6), 0x34, "and so do the latches");
}
#[test]
fn the_whole_register_block_is_the_region() {
let via = Via::bare();
assert_eq!(via.region("").expect("mapped").len(), REGISTER_COUNT);
assert!(via.region("regs").is_some());
assert!(via.region("porta").is_none());
}
#[test]
fn a_snapshot_round_trips_to_identical_state() {
let saved = Via::bare();
poke(&saved, 0x3, 0xff);
poke(&saved, 0x1, 0x5a);
poke(&saved, 0xb, ACR_T1_FREE_RUN);
poke(&saved, 0x4, 0xff);
poke(&saved, 0x5, 0x01);
poke(&saved, 0xe, 0x80 | IFR_T1);
saved.advance_to(200);
let mut shape = MachineShape::new();
shape.add_device("via", CLASS_NAME).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("via", CLASS_NAME, STATE_VERSION).unwrap();
saved.save(&mut chunk).unwrap();
}
let bytes = w.to_vec().unwrap();
let restored = Via::bare();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load("via", CLASS_NAME, STATE_VERSION, &Migrations::new())
.unwrap();
restored.load(&mut chunk.reader()).unwrap();
let before: Vec<u8> = (0..16).map(|i| peek_debug(&saved, i)).collect();
let after: Vec<u8> = (0..16).map(|i| peek_debug(&restored, i)).collect();
assert_eq!(before, after);
assert_eq!(restored.ticks(), 200, "and it resumes from the same tick");
saved.advance_to(1000);
restored.advance_to(1000);
assert_eq!(saved.timer1(), restored.timer1());
assert_eq!(saved.ifr(), restored.ifr());
}
#[test]
fn the_class_is_registrable_and_takes_no_properties() {
let mut registry = crate::core::Registry::new();
register(&mut registry).expect("a fresh registry");
let class = registry.get(CLASS_NAME).expect("registered");
assert_eq!(class.version, STATE_VERSION);
assert!(class.properties.is_empty());
let device = (class.construct)(&Props::new()).expect("nothing to give it");
assert_eq!(device.class().name, CLASS_NAME);
assert!(device.is_lazy(), "the timers are sampled");
assert!(device.connect("ca1", dummy_source()).is_err());
let e = Via::new(&Props::new().with("port", "console"))
.expect_err("a property it does not have")
.to_string();
assert!(e.contains("port"), "{e}");
}
}