beamr 0.4.9

A Rust runtime with the BEAM's execution model, targeting Gleam
Documentation
use std::io::Write;
use std::time::Instant;

use crate::atom::Atom;
use crate::native::ExceptionClass;
use crate::replay::{
    NativeOutcome, RecordedDeliveryKind, RecordedMessageDelivery, RecordedNativeCall,
    RecordedSchedule, RecordedSelect, RecordedTimerExpiry, ReplayEvent, ReplayLog,
};
use crate::term::Term;
use crate::timer::{ExpiredTimer, TimerRef};

const MAGIC: &[u8; 8] = b"BMRRPLY\0";

#[test]
fn replay_log_save_load_round_trips_all_event_variants() {
    let path = std::env::temp_dir().join(format!(
        "beamr-replay-{}-{}.rlog",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos()
    ));
    let now = Instant::now();
    let log = ReplayLog::new(vec![
        ReplayEvent::Select(RecordedSelect {
            pid: 1,
            index: 0,
            message: Term::small_int(10),
        }),
        ReplayEvent::MessageDelivery(RecordedMessageDelivery {
            order: 2,
            kind: RecordedDeliveryKind::RuntimeMessage,
            sender_pid: None,
            receiver_pid: 3,
            sender_clock: 0,
            receiver_clock: 4,
            message: Term::atom(Atom::OK),
        }),
        ReplayEvent::Schedule(RecordedSchedule {
            pid: 3,
            scheduler_index: 0,
            reduction_budget: 100,
            reductions_consumed: 7,
        }),
        ReplayEvent::TimerExpiry(RecordedTimerExpiry {
            now,
            expired: vec![ExpiredTimer {
                reference: TimerRef::from_id(9),
                target_pid: 3,
                message: Term::small_int(20),
                expires_at: now,
            }],
        }),
        ReplayEvent::NativeCall(RecordedNativeCall {
            pid: 3,
            module: Atom::MODULE,
            function: Atom::OK,
            arity: 0,
            outcome: NativeOutcome::err(Term::atom(Atom::BADARG), ExceptionClass::Error, Term::NIL),
        }),
    ]);

    log.save(&path).expect("save replay log");
    let loaded = ReplayLog::load(&path).expect("load replay log");
    let _ = std::fs::remove_file(path);

    assert_eq!(loaded.len(), log.len());
    assert_eq!(loaded.events()[0], log.events()[0]);
    assert_eq!(loaded.events()[1], log.events()[1]);
    assert_eq!(loaded.events()[2], log.events()[2]);
    assert_timer_fields_round_trip(&loaded.events()[3], &log.events()[3]);
    assert_eq!(loaded.events()[4], log.events()[4]);
}

#[test]
fn replay_log_save_load_preserves_cli_transcript() {
    let path = std::env::temp_dir().join(format!(
        "beamr-replay-cli-{}-{}.rlog",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos()
    ));
    let log = ReplayLog::with_cli_result(Vec::new(), "ok\n".to_owned(), 7);

    log.save(&path).expect("save replay log with transcript");
    let loaded = ReplayLog::load(&path).expect("load replay log with transcript");
    let _ = std::fs::remove_file(path);
    let result = loaded.cli_result().expect("transcript is present");

    assert_eq!(result.output(), "ok\n");
    assert_eq!(result.exit_code(), 7);
}

#[test]
fn replay_log_load_rejects_unknown_header_flags() {
    let path = temp_replay_path("unknown-flags");
    let mut bytes = Vec::new();
    bytes.extend_from_slice(MAGIC);
    bytes.extend_from_slice(&ReplayLog::format_version().to_le_bytes());
    bytes.push(0x80);
    bytes.extend_from_slice(&0_u64.to_le_bytes());
    std::fs::File::create(&path)
        .and_then(|mut file| file.write_all(&bytes))
        .expect("write malformed replay log");

    let error = ReplayLog::load(&path).expect_err("unknown flags should be rejected");
    let _ = std::fs::remove_file(path);

    assert!(error.to_string().contains("unknown replay log flags"));
}

fn assert_timer_fields_round_trip(loaded: &ReplayEvent, original: &ReplayEvent) {
    match (loaded, original) {
        (ReplayEvent::TimerExpiry(loaded), ReplayEvent::TimerExpiry(original)) => {
            assert_eq!(loaded.expired.len(), original.expired.len());
            assert_eq!(loaded.expired[0].reference, original.expired[0].reference);
            assert_eq!(loaded.expired[0].target_pid, original.expired[0].target_pid);
            assert_eq!(loaded.expired[0].message, original.expired[0].message);
        }
        other => panic!("unexpected timer events: {other:?}"),
    }
}

fn temp_replay_path(label: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!(
        "beamr-replay-{label}-{}-{}.rlog",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos()
    ))
}