use std::collections::{BTreeMap, VecDeque};
use std::fmt;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use harn_clock::{Clock, PausedClock, RealClock};
use time::OffsetDateTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HarnessKind {
Root,
Stdio,
Clock,
Fs,
Env,
Random,
Net,
}
impl HarnessKind {
pub const fn type_name(self) -> &'static str {
match self {
HarnessKind::Root => "Harness",
HarnessKind::Stdio => "HarnessStdio",
HarnessKind::Clock => "HarnessClock",
HarnessKind::Fs => "HarnessFs",
HarnessKind::Env => "HarnessEnv",
HarnessKind::Random => "HarnessRandom",
HarnessKind::Net => "HarnessNet",
}
}
pub const fn field_name(self) -> Option<&'static str> {
match self {
HarnessKind::Root => None,
HarnessKind::Stdio => Some("stdio"),
HarnessKind::Clock => Some("clock"),
HarnessKind::Fs => Some("fs"),
HarnessKind::Env => Some("env"),
HarnessKind::Random => Some("random"),
HarnessKind::Net => Some("net"),
}
}
pub fn from_field_name(name: &str) -> Option<Self> {
match name {
"stdio" => Some(HarnessKind::Stdio),
"clock" => Some(HarnessKind::Clock),
"fs" => Some(HarnessKind::Fs),
"env" => Some(HarnessKind::Env),
"random" => Some(HarnessKind::Random),
"net" => Some(HarnessKind::Net),
_ => None,
}
}
pub const SUB_HANDLES: &'static [HarnessKind] = &[
HarnessKind::Stdio,
HarnessKind::Clock,
HarnessKind::Fs,
HarnessKind::Env,
HarnessKind::Random,
HarnessKind::Net,
];
pub const ALL: &'static [HarnessKind] = &[
HarnessKind::Root,
HarnessKind::Stdio,
HarnessKind::Clock,
HarnessKind::Fs,
HarnessKind::Env,
HarnessKind::Random,
HarnessKind::Net,
];
}
#[derive(Debug)]
pub struct HarnessInner {
clock: Arc<dyn Clock>,
mode: HarnessMode,
}
impl HarnessInner {
pub fn clock(&self) -> &Arc<dyn Clock> {
&self.clock
}
pub(crate) fn mode(&self) -> &HarnessMode {
&self.mode
}
}
#[derive(Debug)]
pub(crate) enum HarnessMode {
Real,
Null(NullHarnessState),
Mock(Box<MockHarnessState>),
}
#[derive(Debug, Default)]
pub(crate) struct NullHarnessState {
deny_events: Mutex<Vec<DenyEvent>>,
}
impl NullHarnessState {
pub(crate) fn record_deny(
&self,
sub_handle: HarnessKind,
method: &str,
args: &[crate::VmValue],
) {
self.deny_events
.lock()
.expect("deny events poisoned")
.push(DenyEvent::new(
sub_handle,
method,
args.iter().map(crate::VmValue::display).collect(),
));
}
pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
self.deny_events
.lock()
.expect("deny events poisoned")
.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DenyEvent {
pub sub_handle: HarnessKind,
pub method: String,
pub args: Vec<String>,
}
impl DenyEvent {
fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
Self {
sub_handle,
method: method.to_string(),
args,
}
}
}
#[derive(Debug)]
pub(crate) struct MockHarnessState {
calls: Mutex<Vec<HarnessCall>>,
clock: Arc<PausedClock>,
env: BTreeMap<String, String>,
fs_reads: BTreeMap<String, Vec<u8>>,
net_gets: BTreeMap<String, String>,
random_u64: Mutex<VecDeque<u64>>,
stdin_lines: Mutex<VecDeque<String>>,
stdio: Mutex<String>,
stderr: Mutex<String>,
}
impl MockHarnessState {
pub(crate) fn record_call(
&self,
sub_handle: HarnessKind,
method: &str,
args: &[crate::VmValue],
) {
self.calls
.lock()
.expect("calls poisoned")
.push(HarnessCall::new(
sub_handle,
method,
args.iter().map(crate::VmValue::display).collect(),
));
}
pub(crate) fn calls(&self) -> Vec<HarnessCall> {
self.calls.lock().expect("calls poisoned").clone()
}
pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
self.env.get(key).map(String::as_str)
}
pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
self.fs_reads.get(path).map(Vec::as_slice)
}
pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
self.net_gets.get(url).map(String::as_str)
}
pub(crate) fn next_random_u64(&self) -> Option<u64> {
let mut values = self.random_u64.lock().expect("random values poisoned");
values.pop_front()
}
pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
self.clock.advance(duration);
}
pub(crate) fn push_stdio(&self, text: &str) {
self.stdio
.lock()
.expect("stdio buffer poisoned")
.push_str(text);
}
pub(crate) fn stdio(&self) -> String {
self.stdio.lock().expect("stdio buffer poisoned").clone()
}
pub(crate) fn push_stderr(&self, text: &str) {
self.stderr
.lock()
.expect("stderr buffer poisoned")
.push_str(text);
}
pub(crate) fn stderr(&self) -> String {
self.stderr.lock().expect("stderr buffer poisoned").clone()
}
pub(crate) fn pop_stdin_line(&self) -> Option<String> {
self.stdin_lines
.lock()
.expect("stdin queue poisoned")
.pop_front()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HarnessCall {
pub sub_handle: HarnessKind,
pub method: String,
pub args: Vec<String>,
}
impl HarnessCall {
fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
Self {
sub_handle,
method: method.to_string(),
args,
}
}
}
#[derive(Debug)]
pub struct MockHarnessBuilder {
clock: Arc<PausedClock>,
env: BTreeMap<String, String>,
fs_reads: BTreeMap<String, Vec<u8>>,
net_gets: BTreeMap<String, String>,
random_u64: Vec<u64>,
stdin_lines: Vec<String>,
}
impl MockHarnessBuilder {
fn new() -> Self {
Self {
clock: paused_clock_at_unix_ms(0),
env: BTreeMap::new(),
fs_reads: BTreeMap::new(),
net_gets: BTreeMap::new(),
random_u64: Vec::new(),
stdin_lines: Vec::new(),
}
}
pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
self.clock = paused_clock_at_unix_ms(unix_ms);
self
}
pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
self.clock = PausedClock::new(origin);
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
self.fs_reads.insert(path.into(), data.into());
self
}
pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
self.net_gets.insert(url.into(), body.into());
self
}
pub fn random_u64(mut self, value: u64) -> Self {
self.random_u64.push(value);
self
}
pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
self.stdin_lines.push(line.into());
self
}
pub fn build(self) -> Harness {
let clock = self.clock;
Harness::with_mode(
clock.clone() as Arc<dyn Clock>,
HarnessMode::Mock(Box::new(MockHarnessState {
calls: Mutex::new(Vec::new()),
clock,
env: self.env,
fs_reads: self.fs_reads,
net_gets: self.net_gets,
random_u64: Mutex::new(self.random_u64.into()),
stdin_lines: Mutex::new(self.stdin_lines.into()),
stdio: Mutex::new(String::new()),
stderr: Mutex::new(String::new()),
})),
)
}
}
#[derive(Debug, Clone)]
pub struct Harness {
inner: Arc<HarnessInner>,
}
impl Harness {
pub fn real() -> Self {
Self::with_mode(
Arc::new(MockAwareClock::new(RealClock::new())),
HarnessMode::Real,
)
}
pub fn null() -> Self {
Self::with_mode(
paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
HarnessMode::Null(NullHarnessState::default()),
)
}
pub fn mock() -> MockHarnessBuilder {
MockHarnessBuilder::new()
}
pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
Self::with_mode(clock, HarnessMode::Real)
}
fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
Self {
inner: Arc::new(HarnessInner { clock, mode }),
}
}
pub fn deny_events(&self) -> Vec<DenyEvent> {
match self.inner.mode() {
HarnessMode::Null(state) => state.deny_events(),
HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
}
}
pub fn calls(&self) -> Vec<HarnessCall> {
match self.inner.mode() {
HarnessMode::Mock(state) => state.calls(),
HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
}
}
pub fn captured_stdio(&self) -> String {
match self.inner.mode() {
HarnessMode::Mock(state) => state.stdio(),
HarnessMode::Real | HarnessMode::Null(_) => String::new(),
}
}
pub fn captured_stderr(&self) -> String {
match self.inner.mode() {
HarnessMode::Mock(state) => state.stderr(),
HarnessMode::Real | HarnessMode::Null(_) => String::new(),
}
}
pub fn test() -> (Self, Arc<PausedClock>) {
Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
}
pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
let paused = PausedClock::new(origin);
let as_dyn: Arc<dyn Clock> = paused.clone();
(Self::with_clock(as_dyn), paused)
}
pub fn stdio(&self) -> HarnessStdio {
HarnessStdio {
inner: Arc::clone(&self.inner),
}
}
pub fn clock(&self) -> HarnessClock {
HarnessClock {
inner: Arc::clone(&self.inner),
}
}
pub fn fs(&self) -> HarnessFs {
HarnessFs {
inner: Arc::clone(&self.inner),
}
}
pub fn env(&self) -> HarnessEnv {
HarnessEnv {
inner: Arc::clone(&self.inner),
}
}
pub fn random(&self) -> HarnessRandom {
HarnessRandom {
inner: Arc::clone(&self.inner),
}
}
pub fn net(&self) -> HarnessNet {
HarnessNet {
inner: Arc::clone(&self.inner),
}
}
pub fn into_vm_value(self) -> crate::value::VmValue {
crate::value::VmValue::Harness(VmHarness {
inner: self.inner,
kind: HarnessKind::Root,
})
}
}
fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
let nanos = (unix_ms as i128).saturating_mul(1_000_000);
let origin =
OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
PausedClock::new(origin)
}
pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
crate::VmValue::String(Rc::from(value.into()))
}
impl Default for Harness {
fn default() -> Self {
Self::real()
}
}
#[derive(Debug, Clone)]
pub struct HarnessStdio {
inner: Arc<HarnessInner>,
}
#[derive(Debug, Clone)]
pub struct HarnessClock {
inner: Arc<HarnessInner>,
}
impl HarnessClock {
pub fn clock(&self) -> &Arc<dyn Clock> {
self.inner.clock()
}
}
#[derive(Debug, Clone)]
pub struct HarnessFs {
inner: Arc<HarnessInner>,
}
#[derive(Debug, Clone)]
pub struct HarnessEnv {
inner: Arc<HarnessInner>,
}
#[derive(Debug, Clone)]
pub struct HarnessRandom {
inner: Arc<HarnessInner>,
}
#[derive(Debug, Clone)]
pub struct HarnessNet {
inner: Arc<HarnessInner>,
}
macro_rules! sub_handle_inner {
($($ty:ty),* $(,)?) => {
$(
impl $ty {
#[allow(dead_code)]
pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
&self.inner
}
}
)*
};
}
sub_handle_inner!(
HarnessStdio,
HarnessFs,
HarnessEnv,
HarnessRandom,
HarnessNet,
);
impl HarnessClock {
#[allow(dead_code)]
pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
&self.inner
}
}
#[derive(Clone)]
pub struct VmHarness {
inner: Arc<HarnessInner>,
kind: HarnessKind,
}
impl VmHarness {
pub fn kind(&self) -> HarnessKind {
self.kind
}
pub fn type_name(&self) -> &'static str {
self.kind.type_name()
}
pub fn inner(&self) -> &Arc<HarnessInner> {
&self.inner
}
pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
if self.kind != HarnessKind::Root {
return None;
}
let kind = HarnessKind::from_field_name(field)?;
Some(VmHarness {
inner: Arc::clone(&self.inner),
kind,
})
}
}
impl fmt::Debug for VmHarness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VmHarness")
.field("kind", &self.kind)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct MockAwareClock<C: Clock + 'static> {
inner: C,
}
impl<C: Clock + 'static> MockAwareClock<C> {
pub fn new(inner: C) -> Self {
Self { inner }
}
}
#[async_trait]
impl<C: Clock + 'static> Clock for MockAwareClock<C> {
fn now_utc(&self) -> OffsetDateTime {
if let Some(mock) = crate::clock_mock::active_mock_clock() {
return mock.now_utc();
}
self.inner.now_utc()
}
fn monotonic_ms(&self) -> i64 {
if let Some(mock) = crate::clock_mock::active_mock_clock() {
return mock.monotonic_ms();
}
self.inner.monotonic_ms()
}
async fn sleep(&self, duration: Duration) {
if duration.is_zero() {
return;
}
if let Some(mock) = crate::clock_mock::active_mock_clock() {
mock.advance_std_sync(duration);
return;
}
self.inner.sleep(duration).await;
}
async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
if let Some(mock) = crate::clock_mock::active_mock_clock() {
let now = mock.now_utc();
if deadline > now {
if let Ok(delta) = Duration::try_from(deadline - now) {
mock.advance_std_sync(delta);
}
}
return;
}
self.inner.sleep_until_utc(deadline).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn real_constructs_without_panic() {
let _harness = Harness::real();
}
#[test]
fn sub_handles_share_inner_state() {
let harness = Harness::real();
let stdio_inner = Arc::as_ptr(harness.stdio().inner());
let clock_inner = Arc::as_ptr(harness.clock().inner());
assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
}
#[test]
fn kinds_round_trip_through_field_names() {
for kind in HarnessKind::SUB_HANDLES {
let field = kind.field_name().unwrap();
assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
}
assert!(HarnessKind::from_field_name("nope").is_none());
assert!(HarnessKind::Root.field_name().is_none());
}
#[test]
fn vm_harness_property_access_returns_sub_handle() {
let root = match Harness::real().into_vm_value() {
crate::value::VmValue::Harness(h) => h,
other => panic!("expected Harness variant, got {}", other.type_name()),
};
let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
assert_eq!(stdio.kind(), HarnessKind::Stdio);
assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
assert!(root.sub_handle("not_a_field").is_none());
}
#[test]
fn test_constructor_clock_advances_under_paused_clock_advance() {
let (harness, paused) = Harness::test();
let clock = harness.clock();
let start_wall = clock.clock().now_utc();
assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
assert_eq!(clock.clock().monotonic_ms(), 0);
paused.advance(Duration::from_millis(1_500));
assert_eq!(clock.clock().monotonic_ms(), 1_500);
let after_wall = clock.clock().now_utc();
assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
}
#[test]
fn with_paused_clock_pins_origin() {
let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
let (harness, paused) = Harness::with_paused_clock(origin);
assert_eq!(harness.clock().clock().now_utc(), origin);
paused.advance(Duration::from_secs(60));
assert_eq!(
harness.clock().clock().now_utc() - origin,
time::Duration::seconds(60)
);
}
#[test]
fn null_harness_records_deny_events_for_every_sub_handle() {
let harness = Harness::null();
for source in [
r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
r#"fn main(harness: Harness) { harness.clock.now_ms() }"#,
r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
] {
let error = run_harness_source(source, harness.clone()).expect_err("call denied");
assert!(
error.contains("NullHarness denied"),
"unexpected deny error: {error}"
);
}
let events = harness.deny_events();
let observed: Vec<_> = events
.iter()
.map(|event| (event.sub_handle, event.method.as_str()))
.collect();
assert_eq!(
observed,
vec![
(HarnessKind::Stdio, "println"),
(HarnessKind::Clock, "now_ms"),
(HarnessKind::Fs, "read_text"),
(HarnessKind::Env, "get"),
(HarnessKind::Random, "gen_u64"),
(HarnessKind::Net, "get"),
]
);
assert_eq!(events[0].args, vec!["blocked"]);
assert_eq!(events[2].args, vec!["/x"]);
}
#[test]
fn mock_harness_replays_canned_responses_and_records_calls() {
let harness = Harness::mock()
.clock_at_unix_ms(1_700_000_000_000)
.env("KEY", "value")
.fs_read("/x", b"data".to_vec())
.random_u64(42)
.net_get("https://example.test", "body")
.build();
let output = run_harness_source(
r#"
fn main(harness: Harness) {
harness.stdio.print("partial ")
harness.stdio.println("line")
println(harness.clock.now_ms())
harness.clock.sleep_ms(250)
println(harness.clock.now_ms())
println(harness.clock.monotonic_ms())
println(harness.env.get("KEY"))
println(harness.fs.read_text("/x"))
println(harness.fs.exists("/missing"))
println(harness.random.gen_u64())
println(harness.net.get("https://example.test"))
}
"#,
harness.clone(),
)
.expect("mock harness run succeeds");
assert_eq!(harness.captured_stdio(), "partial line\n");
assert_eq!(
output,
"1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\n"
);
let observed: Vec<_> = harness
.calls()
.into_iter()
.map(|call| (call.sub_handle, call.method))
.collect();
assert_eq!(
observed,
vec![
(HarnessKind::Stdio, "print".to_string()),
(HarnessKind::Stdio, "println".to_string()),
(HarnessKind::Clock, "now_ms".to_string()),
(HarnessKind::Clock, "sleep_ms".to_string()),
(HarnessKind::Clock, "now_ms".to_string()),
(HarnessKind::Clock, "monotonic_ms".to_string()),
(HarnessKind::Env, "get".to_string()),
(HarnessKind::Fs, "read_text".to_string()),
(HarnessKind::Fs, "exists".to_string()),
(HarnessKind::Random, "gen_u64".to_string()),
(HarnessKind::Net, "get".to_string()),
]
);
}
#[test]
fn mock_harness_replays_random_values_fifo() {
let harness = Harness::mock()
.random_u64(7)
.random_u64(11)
.random_u64(u64::MAX)
.build();
let output = run_harness_source(
r#"
fn main(harness: Harness) {
println(harness.random.gen_u64())
println(harness.random.gen_u64())
println(harness.random.gen_u64())
}
"#,
harness,
)
.expect("mock random succeeds");
assert_eq!(output, "7\n11\n9223372036854775807\n");
}
#[test]
fn mock_harness_reports_missing_canned_responses() {
let cases = [
(
r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
"MockHarness has no fs_read response for /missing",
),
(
r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
"MockHarness has no random_u64 response",
),
(
r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
"MockHarness has no net_get response for https://missing.test",
),
];
for (source, expected) in cases {
let error = run_harness_source(source, Harness::mock().build())
.expect_err("missing mock response fails");
assert!(
error.contains(expected),
"expected `{expected}` in `{error}`"
);
}
}
#[test]
fn mock_harness_records_failed_calls() {
let harness = Harness::mock().build();
let error = run_harness_source(
r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
harness.clone(),
)
.expect_err("missing mock response fails");
assert!(error.contains("MockHarness has no net_get response"));
assert_eq!(
harness.calls(),
vec![HarnessCall {
sub_handle: HarnessKind::Net,
method: "get".to_string(),
args: vec!["https://missing.test".to_string()],
}]
);
}
#[test]
fn mock_harness_captures_stderr_separately_from_stdout() {
let harness = Harness::mock().build();
run_harness_source(
r#"
fn main(harness: Harness) {
harness.stdio.println("stdout line")
harness.stdio.eprint("err ")
harness.stdio.eprintln("trail")
}
"#,
harness.clone(),
)
.expect("stderr capture run succeeds");
assert_eq!(harness.captured_stdio(), "stdout line\n");
assert_eq!(harness.captured_stderr(), "err trail\n");
}
#[test]
fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
let harness = Harness::mock()
.stdin_line("first")
.stdin_line("second")
.build();
let output = run_harness_source(
r#"
fn main(harness: Harness) {
harness.stdio.println(harness.stdio.read_line())
harness.stdio.println(harness.stdio.prompt("answer: "))
let eof = harness.stdio.read_line({trim: false})
harness.stdio.println(eof.status)
}
"#,
harness.clone(),
)
.expect("stdin replay succeeds");
assert_eq!(output, "");
assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
}
#[test]
fn mock_harness_rejects_wrong_argument_types() {
let error = run_harness_source(
r#"fn main(harness: Harness) { harness.fs.read_text(1) }"#,
Harness::mock().build(),
)
.expect_err("wrong argument type fails");
assert!(error.contains("HarnessFs.read_text expects string argument 1, got int"));
}
fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let local = tokio::task::LocalSet::new();
local
.run_until(async move {
let chunk = crate::compile_source(source)?;
let mut vm = crate::Vm::new();
crate::stdlib::register_vm_stdlib(&mut vm);
vm.set_harness(harness);
vm.execute(&chunk)
.await
.map_err(|error| error.to_string())?;
Ok(vm.output().to_string())
})
.await
})
}
}