1use std::collections::{BTreeMap, VecDeque};
24use std::fmt;
25use std::rc::Rc;
26use std::sync::{Arc, Mutex};
27use std::time::Duration;
28
29use async_trait::async_trait;
30use harn_clock::{Clock, PausedClock, RealClock};
31use time::OffsetDateTime;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum HarnessKind {
39 Root,
40 Stdio,
41 Clock,
42 Fs,
43 Env,
44 Random,
45 Net,
46}
47
48impl HarnessKind {
49 pub const fn type_name(self) -> &'static str {
53 match self {
54 HarnessKind::Root => "Harness",
55 HarnessKind::Stdio => "HarnessStdio",
56 HarnessKind::Clock => "HarnessClock",
57 HarnessKind::Fs => "HarnessFs",
58 HarnessKind::Env => "HarnessEnv",
59 HarnessKind::Random => "HarnessRandom",
60 HarnessKind::Net => "HarnessNet",
61 }
62 }
63
64 pub const fn field_name(self) -> Option<&'static str> {
67 match self {
68 HarnessKind::Root => None,
69 HarnessKind::Stdio => Some("stdio"),
70 HarnessKind::Clock => Some("clock"),
71 HarnessKind::Fs => Some("fs"),
72 HarnessKind::Env => Some("env"),
73 HarnessKind::Random => Some("random"),
74 HarnessKind::Net => Some("net"),
75 }
76 }
77
78 pub fn from_field_name(name: &str) -> Option<Self> {
80 match name {
81 "stdio" => Some(HarnessKind::Stdio),
82 "clock" => Some(HarnessKind::Clock),
83 "fs" => Some(HarnessKind::Fs),
84 "env" => Some(HarnessKind::Env),
85 "random" => Some(HarnessKind::Random),
86 "net" => Some(HarnessKind::Net),
87 _ => None,
88 }
89 }
90
91 pub const SUB_HANDLES: &'static [HarnessKind] = &[
93 HarnessKind::Stdio,
94 HarnessKind::Clock,
95 HarnessKind::Fs,
96 HarnessKind::Env,
97 HarnessKind::Random,
98 HarnessKind::Net,
99 ];
100
101 pub const ALL: &'static [HarnessKind] = &[
103 HarnessKind::Root,
104 HarnessKind::Stdio,
105 HarnessKind::Clock,
106 HarnessKind::Fs,
107 HarnessKind::Env,
108 HarnessKind::Random,
109 HarnessKind::Net,
110 ];
111}
112
113#[derive(Debug)]
119pub struct HarnessInner {
120 clock: Arc<dyn Clock>,
121 mode: HarnessMode,
122}
123
124impl HarnessInner {
125 pub fn clock(&self) -> &Arc<dyn Clock> {
126 &self.clock
127 }
128
129 pub(crate) fn mode(&self) -> &HarnessMode {
130 &self.mode
131 }
132}
133
134#[derive(Debug)]
135pub(crate) enum HarnessMode {
136 Real,
137 Null(NullHarnessState),
138 Mock(Box<MockHarnessState>),
139}
140
141#[derive(Debug, Default)]
142pub(crate) struct NullHarnessState {
143 deny_events: Mutex<Vec<DenyEvent>>,
144}
145
146impl NullHarnessState {
147 pub(crate) fn record_deny(
148 &self,
149 sub_handle: HarnessKind,
150 method: &str,
151 args: &[crate::VmValue],
152 ) {
153 self.deny_events
154 .lock()
155 .expect("deny events poisoned")
156 .push(DenyEvent::new(
157 sub_handle,
158 method,
159 args.iter().map(crate::VmValue::display).collect(),
160 ));
161 }
162
163 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
164 self.deny_events
165 .lock()
166 .expect("deny events poisoned")
167 .clone()
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct DenyEvent {
173 pub sub_handle: HarnessKind,
174 pub method: String,
175 pub args: Vec<String>,
176}
177
178impl DenyEvent {
179 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
180 Self {
181 sub_handle,
182 method: method.to_string(),
183 args,
184 }
185 }
186}
187
188#[derive(Debug)]
189pub(crate) struct MockHarnessState {
190 calls: Mutex<Vec<HarnessCall>>,
191 clock: Arc<PausedClock>,
192 env: BTreeMap<String, String>,
193 fs_reads: BTreeMap<String, Vec<u8>>,
194 net_gets: BTreeMap<String, String>,
195 random_u64: Mutex<VecDeque<u64>>,
196 stdin_lines: Mutex<VecDeque<String>>,
197 stdio: Mutex<String>,
198 stderr: Mutex<String>,
199}
200
201impl MockHarnessState {
202 pub(crate) fn record_call(
203 &self,
204 sub_handle: HarnessKind,
205 method: &str,
206 args: &[crate::VmValue],
207 ) {
208 self.calls
209 .lock()
210 .expect("calls poisoned")
211 .push(HarnessCall::new(
212 sub_handle,
213 method,
214 args.iter().map(crate::VmValue::display).collect(),
215 ));
216 }
217
218 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
219 self.calls.lock().expect("calls poisoned").clone()
220 }
221
222 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
223 self.env.get(key).map(String::as_str)
224 }
225
226 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
227 self.fs_reads.get(path).map(Vec::as_slice)
228 }
229
230 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
231 self.net_gets.get(url).map(String::as_str)
232 }
233
234 pub(crate) fn next_random_u64(&self) -> Option<u64> {
235 let mut values = self.random_u64.lock().expect("random values poisoned");
236 values.pop_front()
237 }
238
239 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
240 self.clock.advance(duration);
241 }
242
243 pub(crate) fn push_stdio(&self, text: &str) {
244 self.stdio
245 .lock()
246 .expect("stdio buffer poisoned")
247 .push_str(text);
248 }
249
250 pub(crate) fn stdio(&self) -> String {
251 self.stdio.lock().expect("stdio buffer poisoned").clone()
252 }
253
254 pub(crate) fn push_stderr(&self, text: &str) {
255 self.stderr
256 .lock()
257 .expect("stderr buffer poisoned")
258 .push_str(text);
259 }
260
261 pub(crate) fn stderr(&self) -> String {
262 self.stderr.lock().expect("stderr buffer poisoned").clone()
263 }
264
265 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
266 self.stdin_lines
267 .lock()
268 .expect("stdin queue poisoned")
269 .pop_front()
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct HarnessCall {
275 pub sub_handle: HarnessKind,
276 pub method: String,
277 pub args: Vec<String>,
278}
279
280impl HarnessCall {
281 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
282 Self {
283 sub_handle,
284 method: method.to_string(),
285 args,
286 }
287 }
288}
289
290#[derive(Debug)]
291pub struct MockHarnessBuilder {
292 clock: Arc<PausedClock>,
293 env: BTreeMap<String, String>,
294 fs_reads: BTreeMap<String, Vec<u8>>,
295 net_gets: BTreeMap<String, String>,
296 random_u64: Vec<u64>,
297 stdin_lines: Vec<String>,
298}
299
300impl MockHarnessBuilder {
301 fn new() -> Self {
302 Self {
303 clock: paused_clock_at_unix_ms(0),
304 env: BTreeMap::new(),
305 fs_reads: BTreeMap::new(),
306 net_gets: BTreeMap::new(),
307 random_u64: Vec::new(),
308 stdin_lines: Vec::new(),
309 }
310 }
311
312 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
313 self.clock = paused_clock_at_unix_ms(unix_ms);
314 self
315 }
316
317 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
318 self.clock = PausedClock::new(origin);
319 self
320 }
321
322 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
323 self.env.insert(key.into(), value.into());
324 self
325 }
326
327 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
328 self.fs_reads.insert(path.into(), data.into());
329 self
330 }
331
332 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
333 self.net_gets.insert(url.into(), body.into());
334 self
335 }
336
337 pub fn random_u64(mut self, value: u64) -> Self {
338 self.random_u64.push(value);
339 self
340 }
341
342 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
348 self.stdin_lines.push(line.into());
349 self
350 }
351
352 pub fn build(self) -> Harness {
353 let clock = self.clock;
354 Harness::with_mode(
355 clock.clone() as Arc<dyn Clock>,
356 HarnessMode::Mock(Box::new(MockHarnessState {
357 calls: Mutex::new(Vec::new()),
358 clock,
359 env: self.env,
360 fs_reads: self.fs_reads,
361 net_gets: self.net_gets,
362 random_u64: Mutex::new(self.random_u64.into()),
363 stdin_lines: Mutex::new(self.stdin_lines.into()),
364 stdio: Mutex::new(String::new()),
365 stderr: Mutex::new(String::new()),
366 })),
367 )
368 }
369}
370
371#[derive(Debug, Clone)]
375pub struct Harness {
376 inner: Arc<HarnessInner>,
377}
378
379impl Harness {
380 pub fn real() -> Self {
391 Self::with_mode(
392 Arc::new(MockAwareClock::new(RealClock::new())),
393 HarnessMode::Real,
394 )
395 }
396
397 pub fn null() -> Self {
400 Self::with_mode(
401 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
402 HarnessMode::Null(NullHarnessState::default()),
403 )
404 }
405
406 pub fn mock() -> MockHarnessBuilder {
408 MockHarnessBuilder::new()
409 }
410
411 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
416 Self::with_mode(clock, HarnessMode::Real)
417 }
418
419 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
420 Self {
421 inner: Arc::new(HarnessInner { clock, mode }),
422 }
423 }
424
425 pub fn deny_events(&self) -> Vec<DenyEvent> {
426 match self.inner.mode() {
427 HarnessMode::Null(state) => state.deny_events(),
428 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
429 }
430 }
431
432 pub fn calls(&self) -> Vec<HarnessCall> {
433 match self.inner.mode() {
434 HarnessMode::Mock(state) => state.calls(),
435 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
436 }
437 }
438
439 pub fn captured_stdio(&self) -> String {
440 match self.inner.mode() {
441 HarnessMode::Mock(state) => state.stdio(),
442 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
443 }
444 }
445
446 pub fn captured_stderr(&self) -> String {
447 match self.inner.mode() {
448 HarnessMode::Mock(state) => state.stderr(),
449 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
450 }
451 }
452
453 pub fn test() -> (Self, Arc<PausedClock>) {
465 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
466 }
467
468 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
472 let paused = PausedClock::new(origin);
473 let as_dyn: Arc<dyn Clock> = paused.clone();
474 (Self::with_clock(as_dyn), paused)
475 }
476
477 pub fn stdio(&self) -> HarnessStdio {
479 HarnessStdio {
480 inner: Arc::clone(&self.inner),
481 }
482 }
483
484 pub fn clock(&self) -> HarnessClock {
486 HarnessClock {
487 inner: Arc::clone(&self.inner),
488 }
489 }
490
491 pub fn fs(&self) -> HarnessFs {
493 HarnessFs {
494 inner: Arc::clone(&self.inner),
495 }
496 }
497
498 pub fn env(&self) -> HarnessEnv {
500 HarnessEnv {
501 inner: Arc::clone(&self.inner),
502 }
503 }
504
505 pub fn random(&self) -> HarnessRandom {
507 HarnessRandom {
508 inner: Arc::clone(&self.inner),
509 }
510 }
511
512 pub fn net(&self) -> HarnessNet {
514 HarnessNet {
515 inner: Arc::clone(&self.inner),
516 }
517 }
518
519 pub fn into_vm_value(self) -> crate::value::VmValue {
521 crate::value::VmValue::Harness(VmHarness {
522 inner: self.inner,
523 kind: HarnessKind::Root,
524 })
525 }
526}
527
528fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
529 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
530 let origin =
531 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
532 PausedClock::new(origin)
533}
534
535pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
536 crate::VmValue::String(Rc::from(value.into()))
537}
538
539impl Default for Harness {
540 fn default() -> Self {
541 Self::real()
542 }
543}
544
545#[derive(Debug, Clone)]
548pub struct HarnessStdio {
549 inner: Arc<HarnessInner>,
550}
551
552#[derive(Debug, Clone)]
554pub struct HarnessClock {
555 inner: Arc<HarnessInner>,
556}
557
558impl HarnessClock {
559 pub fn clock(&self) -> &Arc<dyn Clock> {
560 self.inner.clock()
561 }
562}
563
564#[derive(Debug, Clone)]
567pub struct HarnessFs {
568 inner: Arc<HarnessInner>,
569}
570
571#[derive(Debug, Clone)]
573pub struct HarnessEnv {
574 inner: Arc<HarnessInner>,
575}
576
577#[derive(Debug, Clone)]
579pub struct HarnessRandom {
580 inner: Arc<HarnessInner>,
581}
582
583#[derive(Debug, Clone)]
585pub struct HarnessNet {
586 inner: Arc<HarnessInner>,
587}
588
589macro_rules! sub_handle_inner {
590 ($($ty:ty),* $(,)?) => {
591 $(
592 impl $ty {
593 #[allow(dead_code)]
594 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
595 &self.inner
596 }
597 }
598 )*
599 };
600}
601sub_handle_inner!(
602 HarnessStdio,
603 HarnessFs,
604 HarnessEnv,
605 HarnessRandom,
606 HarnessNet,
607);
608
609impl HarnessClock {
610 #[allow(dead_code)]
611 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
612 &self.inner
613 }
614}
615
616#[derive(Clone)]
621pub struct VmHarness {
622 inner: Arc<HarnessInner>,
623 kind: HarnessKind,
624}
625
626impl VmHarness {
627 pub fn kind(&self) -> HarnessKind {
628 self.kind
629 }
630
631 pub fn type_name(&self) -> &'static str {
632 self.kind.type_name()
633 }
634
635 pub fn inner(&self) -> &Arc<HarnessInner> {
636 &self.inner
637 }
638
639 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
643 if self.kind != HarnessKind::Root {
644 return None;
645 }
646 let kind = HarnessKind::from_field_name(field)?;
647 Some(VmHarness {
648 inner: Arc::clone(&self.inner),
649 kind,
650 })
651 }
652}
653
654impl fmt::Debug for VmHarness {
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 f.debug_struct("VmHarness")
657 .field("kind", &self.kind)
658 .finish_non_exhaustive()
659 }
660}
661
662#[derive(Debug)]
668pub struct MockAwareClock<C: Clock + 'static> {
669 inner: C,
670}
671
672impl<C: Clock + 'static> MockAwareClock<C> {
673 pub fn new(inner: C) -> Self {
674 Self { inner }
675 }
676}
677
678#[async_trait]
679impl<C: Clock + 'static> Clock for MockAwareClock<C> {
680 fn now_utc(&self) -> OffsetDateTime {
681 if let Some(mock) = crate::clock_mock::active_mock_clock() {
682 return mock.now_utc();
683 }
684 self.inner.now_utc()
685 }
686
687 fn monotonic_ms(&self) -> i64 {
688 if let Some(mock) = crate::clock_mock::active_mock_clock() {
689 return mock.monotonic_ms();
690 }
691 self.inner.monotonic_ms()
692 }
693
694 async fn sleep(&self, duration: Duration) {
695 if duration.is_zero() {
696 return;
697 }
698 if let Some(mock) = crate::clock_mock::active_mock_clock() {
699 mock.advance_std_sync(duration);
705 return;
706 }
707 self.inner.sleep(duration).await;
708 }
709
710 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
711 if let Some(mock) = crate::clock_mock::active_mock_clock() {
712 let now = mock.now_utc();
713 if deadline > now {
714 if let Ok(delta) = Duration::try_from(deadline - now) {
715 mock.advance_std_sync(delta);
716 }
717 }
718 return;
719 }
720 self.inner.sleep_until_utc(deadline).await;
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[test]
729 fn real_constructs_without_panic() {
730 let _harness = Harness::real();
731 }
732
733 #[test]
734 fn sub_handles_share_inner_state() {
735 let harness = Harness::real();
736 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
737 let clock_inner = Arc::as_ptr(harness.clock().inner());
738 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
739 }
740
741 #[test]
742 fn kinds_round_trip_through_field_names() {
743 for kind in HarnessKind::SUB_HANDLES {
744 let field = kind.field_name().unwrap();
745 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
746 }
747 assert!(HarnessKind::from_field_name("nope").is_none());
748 assert!(HarnessKind::Root.field_name().is_none());
749 }
750
751 #[test]
752 fn vm_harness_property_access_returns_sub_handle() {
753 let root = match Harness::real().into_vm_value() {
754 crate::value::VmValue::Harness(h) => h,
755 other => panic!("expected Harness variant, got {}", other.type_name()),
756 };
757 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
758 assert_eq!(stdio.kind(), HarnessKind::Stdio);
759 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
760 assert!(root.sub_handle("not_a_field").is_none());
761 }
762
763 #[test]
764 fn test_constructor_clock_advances_under_paused_clock_advance() {
765 let (harness, paused) = Harness::test();
766 let clock = harness.clock();
767 let start_wall = clock.clock().now_utc();
768 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
769 assert_eq!(clock.clock().monotonic_ms(), 0);
770
771 paused.advance(Duration::from_millis(1_500));
772 assert_eq!(clock.clock().monotonic_ms(), 1_500);
773 let after_wall = clock.clock().now_utc();
774 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
775 }
776
777 #[test]
778 fn with_paused_clock_pins_origin() {
779 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
780 let (harness, paused) = Harness::with_paused_clock(origin);
781 assert_eq!(harness.clock().clock().now_utc(), origin);
782 paused.advance(Duration::from_secs(60));
783 assert_eq!(
784 harness.clock().clock().now_utc() - origin,
785 time::Duration::seconds(60)
786 );
787 }
788
789 #[test]
790 fn null_harness_records_deny_events_for_every_sub_handle() {
791 let harness = Harness::null();
792 for source in [
793 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
794 r#"fn main(harness: Harness) { harness.clock.now_ms() }"#,
795 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
796 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
797 r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
798 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
799 ] {
800 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
801 assert!(
802 error.contains("NullHarness denied"),
803 "unexpected deny error: {error}"
804 );
805 }
806
807 let events = harness.deny_events();
808 let observed: Vec<_> = events
809 .iter()
810 .map(|event| (event.sub_handle, event.method.as_str()))
811 .collect();
812 assert_eq!(
813 observed,
814 vec![
815 (HarnessKind::Stdio, "println"),
816 (HarnessKind::Clock, "now_ms"),
817 (HarnessKind::Fs, "read_text"),
818 (HarnessKind::Env, "get"),
819 (HarnessKind::Random, "gen_u64"),
820 (HarnessKind::Net, "get"),
821 ]
822 );
823 assert_eq!(events[0].args, vec!["blocked"]);
824 assert_eq!(events[2].args, vec!["/x"]);
825 }
826
827 #[test]
828 fn mock_harness_replays_canned_responses_and_records_calls() {
829 let harness = Harness::mock()
830 .clock_at_unix_ms(1_700_000_000_000)
831 .env("KEY", "value")
832 .fs_read("/x", b"data".to_vec())
833 .random_u64(42)
834 .net_get("https://example.test", "body")
835 .build();
836
837 let output = run_harness_source(
838 r#"
839fn main(harness: Harness) {
840 harness.stdio.print("partial ")
841 harness.stdio.println("line")
842 println(harness.clock.now_ms())
843 harness.clock.sleep_ms(250)
844 println(harness.clock.now_ms())
845 println(harness.clock.monotonic_ms())
846 println(harness.env.get("KEY"))
847 println(harness.fs.read_text("/x"))
848 println(harness.fs.exists("/missing"))
849 println(harness.random.gen_u64())
850 println(harness.net.get("https://example.test"))
851}
852"#,
853 harness.clone(),
854 )
855 .expect("mock harness run succeeds");
856
857 assert_eq!(harness.captured_stdio(), "partial line\n");
858 assert_eq!(
859 output,
860 "1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\n"
861 );
862 let observed: Vec<_> = harness
863 .calls()
864 .into_iter()
865 .map(|call| (call.sub_handle, call.method))
866 .collect();
867 assert_eq!(
868 observed,
869 vec![
870 (HarnessKind::Stdio, "print".to_string()),
871 (HarnessKind::Stdio, "println".to_string()),
872 (HarnessKind::Clock, "now_ms".to_string()),
873 (HarnessKind::Clock, "sleep_ms".to_string()),
874 (HarnessKind::Clock, "now_ms".to_string()),
875 (HarnessKind::Clock, "monotonic_ms".to_string()),
876 (HarnessKind::Env, "get".to_string()),
877 (HarnessKind::Fs, "read_text".to_string()),
878 (HarnessKind::Fs, "exists".to_string()),
879 (HarnessKind::Random, "gen_u64".to_string()),
880 (HarnessKind::Net, "get".to_string()),
881 ]
882 );
883 }
884
885 #[test]
886 fn mock_harness_replays_random_values_fifo() {
887 let harness = Harness::mock()
888 .random_u64(7)
889 .random_u64(11)
890 .random_u64(u64::MAX)
891 .build();
892
893 let output = run_harness_source(
894 r#"
895fn main(harness: Harness) {
896 println(harness.random.gen_u64())
897 println(harness.random.gen_u64())
898 println(harness.random.gen_u64())
899}
900"#,
901 harness,
902 )
903 .expect("mock random succeeds");
904
905 assert_eq!(output, "7\n11\n9223372036854775807\n");
906 }
907
908 #[test]
909 fn mock_harness_reports_missing_canned_responses() {
910 let cases = [
911 (
912 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
913 "MockHarness has no fs_read response for /missing",
914 ),
915 (
916 r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
917 "MockHarness has no random_u64 response",
918 ),
919 (
920 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
921 "MockHarness has no net_get response for https://missing.test",
922 ),
923 ];
924
925 for (source, expected) in cases {
926 let error = run_harness_source(source, Harness::mock().build())
927 .expect_err("missing mock response fails");
928 assert!(
929 error.contains(expected),
930 "expected `{expected}` in `{error}`"
931 );
932 }
933 }
934
935 #[test]
936 fn mock_harness_records_failed_calls() {
937 let harness = Harness::mock().build();
938 let error = run_harness_source(
939 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
940 harness.clone(),
941 )
942 .expect_err("missing mock response fails");
943
944 assert!(error.contains("MockHarness has no net_get response"));
945 assert_eq!(
946 harness.calls(),
947 vec![HarnessCall {
948 sub_handle: HarnessKind::Net,
949 method: "get".to_string(),
950 args: vec!["https://missing.test".to_string()],
951 }]
952 );
953 }
954
955 #[test]
956 fn mock_harness_captures_stderr_separately_from_stdout() {
957 let harness = Harness::mock().build();
958 run_harness_source(
959 r#"
960fn main(harness: Harness) {
961 harness.stdio.println("stdout line")
962 harness.stdio.eprint("err ")
963 harness.stdio.eprintln("trail")
964}
965"#,
966 harness.clone(),
967 )
968 .expect("stderr capture run succeeds");
969 assert_eq!(harness.captured_stdio(), "stdout line\n");
970 assert_eq!(harness.captured_stderr(), "err trail\n");
971 }
972
973 #[test]
974 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
975 let harness = Harness::mock()
976 .stdin_line("first")
977 .stdin_line("second")
978 .build();
979 let output = run_harness_source(
980 r#"
981fn main(harness: Harness) {
982 harness.stdio.println(harness.stdio.read_line())
983 harness.stdio.println(harness.stdio.prompt("answer: "))
984 let eof = harness.stdio.read_line({trim: false})
985 harness.stdio.println(eof.status)
986}
987"#,
988 harness.clone(),
989 )
990 .expect("stdin replay succeeds");
991 assert_eq!(output, "");
993 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
994 }
995
996 #[test]
997 fn mock_harness_rejects_wrong_argument_types() {
998 let error = run_harness_source(
999 r#"fn main(harness: Harness) { harness.fs.read_text(1) }"#,
1000 Harness::mock().build(),
1001 )
1002 .expect_err("wrong argument type fails");
1003
1004 assert!(error.contains("HarnessFs.read_text expects string argument 1, got int"));
1005 }
1006
1007 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1008 let rt = tokio::runtime::Builder::new_current_thread()
1009 .enable_all()
1010 .build()
1011 .unwrap();
1012 rt.block_on(async move {
1013 let local = tokio::task::LocalSet::new();
1014 local
1015 .run_until(async move {
1016 let chunk = crate::compile_source(source)?;
1017 let mut vm = crate::Vm::new();
1018 crate::stdlib::register_vm_stdlib(&mut vm);
1019 vm.set_harness(harness);
1020 vm.execute(&chunk)
1021 .await
1022 .map_err(|error| error.to_string())?;
1023 Ok(vm.output().to_string())
1024 })
1025 .await
1026 })
1027 }
1028}