1use std::cell::RefCell;
21use std::collections::HashMap;
22use std::fmt;
23use std::marker::PhantomData;
24use std::path::{Path, PathBuf};
25use std::sync::{mpsc, Arc, Mutex, RwLock};
26use std::time::{Duration, Instant};
27
28use indexmap::IndexMap;
29use serde::de::DeserializeOwned;
30use serde::Serialize;
31
32use crate::error::Severity;
33use crate::eval::budget::Budget;
34use crate::eval::methods::MethodRegistry;
35use crate::eval::value::Value;
36use crate::eval::{DenyFs, EnvCap, FileAccess, FileError, Interpreter, MemFs, Options, RealFs};
37use crate::facade::{to_report, Report};
38use crate::host::{HostCall, HostFn, HostKind, HostOutcome, HostTable};
39use crate::source::SourceCache;
40use crate::span::Span;
41use crate::vfs::loader::Loader;
42use crate::vfs::lockfile::Lockfile;
43use crate::vfs::{FileResolver, ImportSpec, LocalFsResolver, MemoryResolver, ModuleId};
44
45#[derive(Debug, Clone)]
53pub struct Error {
54 reports: Vec<Report>,
55}
56
57impl Error {
58 pub fn code(&self) -> &'static str {
60 self.reports.first().map_or("E0001", |r| r.code)
61 }
62
63 pub fn reports(&self) -> &[Report] {
65 &self.reports
66 }
67
68 fn host(code: &'static str, message: impl Into<String>) -> Self {
69 Error {
70 reports: vec![Report {
71 code,
72 severity: Severity::Error,
73 message: message.into(),
74 file: String::new(),
75 line: 0,
76 column: 0,
77 help: None,
78 }],
79 }
80 }
81}
82
83impl From<Vec<Report>> for Error {
84 fn from(reports: Vec<Report>) -> Self {
85 Error { reports }
86 }
87}
88
89impl fmt::Display for Error {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 for (i, r) in self.reports.iter().enumerate() {
92 if i > 0 {
93 writeln!(f)?;
94 }
95 write!(f, "{r}")?;
96 }
97 Ok(())
98 }
99}
100
101impl std::error::Error for Error {}
102
103#[derive(Default)]
110pub struct ScriptBuilder {
111 sources: Vec<(String, String)>,
112 entry: Option<String>,
113 file: Option<PathBuf>,
114 env: Option<Vec<String>>,
115 env_values: HashMap<String, String>,
116 read: Vec<PathBuf>,
117 strict: bool,
118 budget: Budget,
119 host: Vec<HostFn>,
120 host_for_imports: Vec<String>,
121 host_call_steps: Option<u64>,
122 watch: Option<(Duration, OnReload)>,
123 settle: Option<Duration>,
124}
125
126impl ScriptBuilder {
127 pub fn source(mut self, name: impl Into<String>, text: impl Into<String>) -> Self {
129 self.sources.push((name.into(), text.into()));
130 self
131 }
132
133 pub fn entry(mut self, name: impl Into<String>) -> Self {
135 self.entry = Some(name.into());
136 self
137 }
138
139 pub fn file(mut self, path: impl AsRef<Path>) -> Self {
141 self.file = Some(path.as_ref().to_path_buf());
142 self
143 }
144
145 pub fn allow_env<I, S>(mut self, names: I) -> Self
147 where
148 I: IntoIterator<Item = S>,
149 S: Into<String>,
150 {
151 self.env
152 .get_or_insert_with(Vec::new)
153 .extend(names.into_iter().map(Into::into));
154 self
155 }
156
157 pub fn env_values<I, K, V>(mut self, values: I) -> Self
162 where
163 I: IntoIterator<Item = (K, V)>,
164 K: Into<String>,
165 V: Into<String>,
166 {
167 for (k, v) in values {
168 let k = k.into();
169 self.env.get_or_insert_with(Vec::new).push(k.clone());
170 self.env_values.insert(k, v.into());
171 }
172 self
173 }
174
175 pub fn allow_read(mut self, dir: impl AsRef<Path>) -> Self {
177 self.read.push(dir.as_ref().to_path_buf());
178 self
179 }
180
181 pub fn strict(mut self) -> Self {
183 self.strict = true;
184 self
185 }
186
187 pub fn budget(mut self, budget: Budget) -> Self {
189 self.budget = budget;
190 self
191 }
192
193 pub fn pure<Args, F: PureFn<Args>>(mut self, name: impl Into<String>, f: F) -> Self {
199 self.host.push(HostFn {
200 name: name.into(),
201 kind: HostKind::Pure,
202 arity: F::ARITY,
203 for_imports: false,
204 call: f.erase(),
205 });
206 self
207 }
208
209 pub fn effect<Args, F: EffectFn<Args>>(mut self, name: impl Into<String>, f: F) -> Self {
213 self.host.push(HostFn {
214 name: name.into(),
215 kind: HostKind::Effect,
216 arity: F::ARITY,
217 for_imports: false,
218 call: f.erase(),
219 });
220 self
221 }
222
223 pub fn grant_to_imports(mut self, name: impl Into<String>) -> Self {
227 self.host_for_imports.push(name.into());
228 self
229 }
230
231 pub fn host_call_steps(mut self, steps: u64) -> Self {
235 self.host_call_steps = Some(steps);
236 self
237 }
238
239 pub fn watch(
248 mut self,
249 interval: Duration,
250 on_reload: impl Fn(Result<ScriptVersion, Error>) + Send + 'static,
251 ) -> Self {
252 self.watch = Some((interval, Box::new(on_reload)));
253 self
254 }
255
256 pub fn settle(mut self, settle: Duration) -> Self {
262 self.settle = Some(settle);
263 self
264 }
265
266 pub fn load(self) -> Result<Script, Error> {
267 Script::build(self)
268 }
269}
270
271struct State<'a> {
279 exports: IndexMap<String, Value<'a>>,
280 registry: Arc<MethodRegistry<'a>>,
281 fs: Arc<dyn FileAccess>,
282 env_cap: EnvCap,
283 env_values: Arc<HashMap<String, String>>,
284 host_call_steps: u64,
285 strict: bool,
286 _closures: Interpreter<'a>,
287}
288
289self_cell::self_cell!(
290 struct Loaded {
291 owner: SourceCache,
292 #[not_covariant]
293 dependent: State,
294 }
295);
296
297struct Config {
300 sources: Vec<(String, String)>,
301 entry: Option<String>,
302 file: Option<PathBuf>,
303 read: Vec<PathBuf>,
304 env_cap: EnvCap,
305 env_values: Arc<HashMap<String, String>>,
306 granted: bool,
307 strict: bool,
308 budget: Budget,
309 host: Arc<HostTable>,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct ScriptVersion {
316 pub generation: u64,
317 pub hash: String,
318}
319
320struct Version {
322 loaded: Loaded,
323 id: ScriptVersion,
324 files: Vec<(PathBuf, String)>,
326}
327
328type Contract = Vec<(String, Option<usize>)>;
331
332struct Shared {
334 config: Config,
335 lock: Lockfile,
338 contract: Contract,
340 current: RwLock<Arc<Version>>,
341 installing: Mutex<()>,
343}
344
345struct Attempt {
349 result: Result<(Loaded, Lockfile), Error>,
350 files: Vec<(PathBuf, String)>,
351 base: u64,
353}
354
355pub struct Script {
358 shared: Arc<Shared>,
359 watcher: Option<Watcher>,
360}
361
362impl Script {
363 pub fn load(path: impl AsRef<Path>) -> Result<Script, Error> {
366 Self::builder().file(path).load()
367 }
368
369 pub fn builder() -> ScriptBuilder {
370 ScriptBuilder::default()
371 }
372
373 fn build(mut b: ScriptBuilder) -> Result<Script, Error> {
374 let host = Arc::new(host_table(&mut b)?);
375 let granted = b.env.is_some() || !b.read.is_empty() || host.has_effects();
377 let env_cap = match &b.env {
378 None => EnvCap::Deny,
379 Some(names) => EnvCap::Allow(names.clone()),
380 };
381 let watch = b.watch.take();
382 if let (Some((interval, _)), Some(settle)) = (&watch, b.settle) {
383 if settle < *interval {
384 return Err(Error::host(
385 "E0001",
386 format!(
387 "settle ({settle:?}) is shorter than the watch interval ({interval:?}): \
388 the files would be read before anyone looked at them twice"
389 ),
390 ));
391 }
392 }
393 if watch.is_some() && b.file.is_none() {
394 return Err(Error::host(
395 "E0001",
396 "only a script loaded from a file can be watched; an in-memory source has no file",
397 ));
398 }
399 let config = Config {
400 sources: std::mem::take(&mut b.sources),
401 entry: b.entry.take(),
402 file: b.file.take(),
403 read: std::mem::take(&mut b.read),
404 env_cap,
405 env_values: Arc::new(std::mem::take(&mut b.env_values)),
406 granted,
407 strict: b.strict,
408 budget: b.budget,
409 host,
410 };
411
412 let lock = match &config.file {
413 Some(path) => read_lock(path)?,
414 None => Lockfile::default(),
415 };
416 let attempt = load_version(&config, lock, false, 0);
417 let (loaded, lock) = attempt.result?;
418 let contract = contract_of(&loaded);
419 let version = Version {
420 loaded,
421 id: ScriptVersion {
422 generation: 1,
423 hash: hash_files(&attempt.files),
424 },
425 files: attempt.files,
426 };
427 let shared = Arc::new(Shared {
428 config,
429 lock,
430 contract,
431 current: RwLock::new(Arc::new(version)),
432 installing: Mutex::new(()),
433 });
434 let settle = b.settle;
435 let watcher = watch.map(|(interval, on_reload)| {
436 let settle = settle.unwrap_or_else(|| default_settle(interval));
437 Watcher::start(&shared, interval, settle, on_reload)
438 });
439 Ok(Script { shared, watcher })
440 }
441
442 pub fn version(&self) -> ScriptVersion {
444 self.shared.current().id.clone()
445 }
446
447 pub fn reload(&self) -> Result<ScriptVersion, Error> {
454 if self.shared.config.file.is_none() {
455 return Err(Error::host(
456 "E0001",
457 "a script built from in-memory sources has no files to reload; build a new Script",
458 ));
459 }
460 let attempt = self.shared.attempt();
461 Ok(self
462 .shared
463 .install(attempt)?
464 .unwrap_or_else(|| self.version()))
465 }
466
467 pub fn output<T: DeserializeOwned>(&self) -> Result<T, Error> {
470 refuse_reentry()?;
471 let current = self.shared.current();
472 let json = current.loaded.with_dependent(|cache, state| {
473 let module = Value::Object(Arc::new(state.exports.clone()));
474 crate::serialize::to_json(&module).map_err(|d| Error::from(vec![to_report(&d, cache)]))
475 })?;
476 from_json(json)
477 }
478
479 pub fn get<T: DeserializeOwned>(&self, name: &str) -> Result<T, Error> {
481 refuse_reentry()?;
482 let current = self.shared.current();
483 let json = current.loaded.with_dependent(|cache, state| {
484 let v = state.exports.get(name).ok_or_else(|| no_export(name))?;
485 crate::serialize::to_json(v).map_err(|d| Error::from(vec![to_report(&d, cache)]))
486 })?;
487 from_json(json)
488 }
489
490 pub fn call<A: Args, R: DeserializeOwned>(&self, name: &str, args: A) -> Result<R, Error> {
493 self.function::<A, R>(name)?.call(args)
494 }
495
496 pub fn function<A: Args, R: DeserializeOwned>(
500 &self,
501 name: &str,
502 ) -> Result<Function<'_, A, R>, Error> {
503 self.shared.current().loaded.with_dependent(|_, state| {
504 let value = state.exports.get(name).ok_or_else(|| no_export(name))?;
505 let Value::Function(def) = value else {
506 return Err(Error::host(
507 "E0003",
508 format!("'{name}' is a {}, not a function", value.type_name()),
509 ));
510 };
511 if def.params.len() != A::ARITY {
512 return Err(Error::host(
513 "E0004",
514 format!(
515 "'{name}' takes {} argument{}, but the host passes {}",
516 def.params.len(),
517 if def.params.len() == 1 { "" } else { "s" },
518 A::ARITY
519 ),
520 ));
521 }
522 Ok(())
523 })?;
524 Ok(Function {
525 script: self,
526 name: name.to_string(),
527 _types: PhantomData,
528 })
529 }
530
531 fn invoke<A: Args>(&self, name: &str, args: A) -> Result<serde_json::Value, Error> {
534 refuse_reentry()?;
535 let budget = self.shared.config.budget;
536 let current = self.shared.current();
537 current.loaded.with_dependent(|cache, state| {
538 let f = state.exports.get(name).ok_or_else(|| no_export(name))?;
541 let mut it = Interpreter::with_registry(
542 state.registry.clone(),
543 Options {
544 strict: state.strict,
545 dry_run: false,
546 },
547 );
548 it.fs = Box::new(SharedFs(state.fs.clone()));
549 it.env_cap = state.env_cap.clone();
550 it.env_overrides = (*state.env_values).clone();
551 it.host_call_steps = state.host_call_steps;
552 let _meter = budget.begin(&mut it);
553
554 let args = args.to_values().map_err(|e| Error::host("E0005", e.0))?;
555 let span = Span::new(0, 0, 0);
556 let result = it
557 .call_value(f, &args, span)
558 .map_err(|d| Error::from(vec![to_report(&d, cache)]))?;
559 crate::serialize::to_json(&result).map_err(|d| {
562 let mut e = Error::from(vec![to_report(&d, cache)]);
563 e.reports[0].code = "E0006";
564 e
565 })
566 })
567 }
568}
569
570impl Drop for Script {
571 fn drop(&mut self) {
572 if let Some(w) = self.watcher.take() {
573 w.stop();
574 }
575 }
576}
577
578impl Shared {
579 fn current(&self) -> Arc<Version> {
580 self.current
583 .read()
584 .unwrap_or_else(|e| e.into_inner())
585 .clone()
586 }
587
588 fn attempt(&self) -> Attempt {
590 let base = self.current().id.generation;
591 load_version(&self.config, self.lock.clone(), true, base)
592 }
593
594 fn install(&self, attempt: Attempt) -> Result<Option<ScriptVersion>, Error> {
597 let (loaded, _) = attempt.result?;
598 let _one = self.installing.lock().unwrap_or_else(|e| e.into_inner());
599 let current = self.current();
600 let hash = hash_files(&attempt.files);
601 if hash == current.id.hash || current.id.generation != attempt.base {
602 return Ok(None);
603 }
604 check_contract(&self.contract, &contract_of(&loaded))?;
605 let id = ScriptVersion {
606 generation: current.id.generation + 1,
607 hash,
608 };
609 let version = Arc::new(Version {
610 loaded,
611 id: id.clone(),
612 files: attempt.files,
613 });
614 *self.current.write().unwrap_or_else(|e| e.into_inner()) = version;
615 Ok(Some(id))
616 }
617}
618
619fn read_lock(path: &Path) -> Result<Lockfile, Error> {
621 let entry = std::fs::canonicalize(path)
622 .map_err(|e| Error::host("E0001", format!("cannot open {}: {e}", path.display())))?;
623 let root = entry.parent().unwrap_or(Path::new("."));
624 match std::fs::read_to_string(root.join("aura.lock")) {
625 Ok(text) => Lockfile::parse(&text)
626 .map_err(|e| Error::host("E0001", format!("invalid aura.lock: {e}"))),
627 Err(_) => Ok(Lockfile::default()),
628 }
629}
630
631fn load_version(config: &Config, lock: Lockfile, frozen: bool, base: u64) -> Attempt {
635 let mut files = Vec::new();
636 let mut lock_after = None;
637 let result = Loaded::try_new(SourceCache::new(), |cache| -> Result<State<'_>, Error> {
638 let (resolver, fs, entry): (Box<dyn FileResolver>, Arc<dyn FileAccess>, String) =
640 match &config.file {
641 Some(path) => {
642 let entry = std::fs::canonicalize(path).map_err(|e| {
643 Error::host("E0001", format!("cannot open {}: {e}", path.display()))
644 })?;
645 let root = entry.parent().unwrap_or(Path::new(".")).to_path_buf();
646 let registry_dir = std::env::var_os("USERPROFILE")
647 .or_else(|| std::env::var_os("HOME"))
648 .map(PathBuf::from)
649 .unwrap_or_default()
650 .join(".aura")
651 .join("registry");
652 let fs: Arc<dyn FileAccess> = if config.read.is_empty() {
653 Arc::new(DenyFs)
654 } else {
655 Arc::new(RealFs {
656 allowed: config.read.clone(),
657 })
658 };
659 let name = entry
660 .file_name()
661 .unwrap_or_default()
662 .to_string_lossy()
663 .to_string();
664 (Box::new(LocalFsResolver { root, registry_dir }), fs, name)
665 }
666 None => {
667 let entry = config
668 .entry
669 .clone()
670 .or_else(|| config.sources.first().map(|(n, _)| n.clone()))
671 .ok_or_else(|| Error::host("E0001", "no source was given"))?;
672 let files: HashMap<String, String> = config.sources.iter().cloned().collect();
673 let fs: Arc<dyn FileAccess> = if config.read.is_empty() {
674 Arc::new(DenyFs)
675 } else {
676 Arc::new(MemFs(files.clone()))
677 };
678 (Box::new(MemoryResolver { files }), fs, entry)
679 }
680 };
681 let recording = Recording {
682 inner: resolver.as_ref(),
683 seen: RefCell::new(Vec::new()),
684 };
685
686 let mut loader = Loader::new(cache, &recording);
687 loader.hermetic = !config.granted;
689 loader.lock = lock;
690 loader.frozen = frozen;
691 loader.host = config.host.clone();
692
693 let registry = Arc::new(MethodRegistry::builtin());
694 let mut interp = Interpreter::with_registry(
695 registry.clone(),
696 Options {
697 strict: config.strict,
698 dry_run: false,
699 },
700 );
701 interp.fs = Box::new(SharedFs(fs.clone()));
702 interp.env_cap = config.env_cap.clone();
703 interp.env_overrides = (*config.env_values).clone();
704 interp.host_call_steps = config.host.call_steps;
705 let _meter = config.budget.begin(&mut interp);
709
710 let value = loader.eval_entry(&mut interp, &ImportSpec::File(&entry));
711 files = recording.seen.take();
712 let mut reports: Vec<Report> = loader.diags.iter().map(|d| to_report(d, cache)).collect();
713 let value = match value {
714 Ok(v) => v,
715 Err(d) => {
716 reports.insert(0, to_report(&d, cache));
717 return Err(reports.into());
718 }
719 };
720 if crate::analysis::has_blocking(&loader.diags, config.strict) {
721 return Err(reports.into());
722 }
723 let Value::Object(exports) = value else {
724 return Err(Error::host("E0001", "a module evaluates to an object"));
725 };
726 lock_after = Some(std::mem::take(&mut loader.lock));
727
728 Ok(State {
729 exports: (*exports).clone(),
730 registry,
731 fs,
732 env_cap: config.env_cap.clone(),
733 env_values: config.env_values.clone(),
734 host_call_steps: config.host.call_steps,
735 strict: config.strict,
736 _closures: interp,
737 })
738 });
739 Attempt {
740 result: result.map(|loaded| (loaded, lock_after.unwrap_or_default())),
741 files,
742 base,
743 }
744}
745
746struct Recording<'r> {
749 inner: &'r dyn FileResolver,
750 seen: RefCell<Vec<(PathBuf, String)>>,
751}
752
753impl FileResolver for Recording<'_> {
754 fn resolve(
755 &self,
756 spec: &ImportSpec<'_>,
757 importer: Option<&ModuleId>,
758 ) -> Result<ModuleId, String> {
759 self.inner.resolve(spec, importer)
760 }
761
762 fn load(&self, id: &ModuleId) -> Result<String, String> {
763 let text = self.inner.load(id)?;
764 if let ModuleId::Local(path) = id {
765 self.seen.borrow_mut().push((path.clone(), text.clone()));
766 }
767 Ok(text)
768 }
769
770 fn dir_of(&self, id: &ModuleId) -> Option<PathBuf> {
771 self.inner.dir_of(id)
772 }
773}
774
775fn hash_files(files: &[(PathBuf, String)]) -> String {
777 use sha2::{Digest, Sha256};
778 let mut sorted: Vec<_> = files.iter().collect();
779 sorted.sort_by(|a, b| a.0.cmp(&b.0));
780 let mut h = Sha256::new();
781 for (path, text) in sorted {
782 h.update(path.to_string_lossy().as_bytes());
783 h.update([0]);
784 h.update(text.as_bytes());
785 h.update([0]);
786 }
787 h.finalize().iter().map(|b| format!("{b:02x}")).collect()
788}
789
790fn contract_of(loaded: &Loaded) -> Contract {
791 loaded.with_dependent(|_, s| {
792 s.exports
793 .iter()
794 .map(|(k, v)| {
795 let arity = match v {
796 Value::Function(d) => Some(d.params.len()),
797 _ => None,
798 };
799 (k.clone(), arity)
800 })
801 .collect()
802 })
803}
804
805fn check_contract(fixed: &Contract, new: &Contract) -> Result<(), Error> {
808 let describe = |k: &Option<usize>| match k {
809 Some(n) => format!(
810 "a function of {n} argument{}",
811 if *n == 1 { "" } else { "s" }
812 ),
813 None => "a value".to_string(),
814 };
815 for (name, kind) in fixed {
816 let msg = match new.iter().find(|(n, _)| n == name) {
817 None => format!("the new version no longer exports '{name}'"),
818 Some((_, k)) if k != kind => format!(
819 "'{name}' was {}, and the new version makes it {}",
820 describe(kind),
821 describe(k)
822 ),
823 Some(_) => continue,
824 };
825 let mut e = Error::host("E0011", msg);
826 e.reports[0].help = Some(
827 "the host may have bound it, so the reload was refused and the old version keeps \
828 serving; restart the application to change what the script exports"
829 .to_string(),
830 );
831 return Err(e);
832 }
833 Ok(())
834}
835
836struct Watcher {
839 stop: mpsc::Sender<()>,
840 thread: std::thread::JoinHandle<()>,
841}
842
843type OnReload = Box<dyn Fn(Result<ScriptVersion, Error>) + Send>;
845
846impl Watcher {
847 fn start(
848 shared: &Arc<Shared>,
849 interval: Duration,
850 settle: Duration,
851 on_reload: OnReload,
852 ) -> Watcher {
853 let (stop, stopped) = mpsc::channel();
854 let weak = Arc::downgrade(shared);
855 let thread = std::thread::Builder::new()
856 .name("aura-watch".into())
857 .spawn(move || watch_loop(weak, interval, settle, stopped, on_reload))
858 .expect("spawn the watcher thread");
859 Watcher { stop, thread }
860 }
861
862 fn stop(self) {
863 drop(self.stop);
864 if self.thread.thread().id() != std::thread::current().id() {
867 let _ = self.thread.join();
868 }
869 }
870}
871
872fn tick(stopped: &mpsc::Receiver<()>, interval: Duration) -> bool {
874 matches!(
875 stopped.recv_timeout(interval),
876 Err(mpsc::RecvTimeoutError::Timeout)
877 )
878}
879
880type Stats = Vec<Option<(u64, Option<std::time::SystemTime>)>>;
883
884fn stats(files: &[(PathBuf, String)]) -> Stats {
885 files
886 .iter()
887 .map(|(p, _)| {
888 std::fs::metadata(p)
889 .ok()
890 .map(|m| (m.len(), m.modified().ok()))
891 })
892 .collect()
893}
894
895fn read_still(files: &[(PathBuf, String)]) -> Option<(Stats, Vec<(PathBuf, String)>)> {
899 let before = stats(files);
900 let read = reread(files)?;
901 (stats(files) == before).then_some((before, read))
902}
903
904fn reread(files: &[(PathBuf, String)]) -> Option<Vec<(PathBuf, String)>> {
906 files
907 .iter()
908 .map(|(p, _)| std::fs::read_to_string(p).ok().map(|t| (p.clone(), t)))
909 .collect()
910}
911
912#[derive(Debug)]
923struct Settle {
924 settle: Duration,
925 seen: Option<(Stats, Instant)>,
927}
928
929#[derive(Debug, PartialEq, Eq)]
930enum Step {
931 Wait,
933 Look,
935}
936
937impl Settle {
938 fn new(settle: Duration) -> Self {
939 Settle { settle, seen: None }
940 }
941
942 fn observe(&mut self, now: Instant, stats: &Stats) -> Step {
943 match &self.seen {
944 Some((s, since)) if s == stats => {
945 if now.saturating_duration_since(*since) >= self.settle {
946 Step::Look
947 } else {
948 Step::Wait
949 }
950 }
951 _ => {
952 self.seen = Some((stats.clone(), now));
953 Step::Wait
954 }
955 }
956 }
957
958 fn reset(&mut self) {
960 self.seen = None;
961 }
962}
963
964fn default_settle(interval: Duration) -> Duration {
968 (interval * 2).max(Duration::from_secs(1))
969}
970
971fn watch_loop(
972 shared: std::sync::Weak<Shared>,
973 interval: Duration,
974 settle: Duration,
975 stopped: mpsc::Receiver<()>,
976 on_reload: OnReload,
977) {
978 let mut gate = Settle::new(settle);
979 let mut quiet: Option<Stats> = None;
982 let mut failed: Option<Stats> = None;
984 let mut tracked: Option<Vec<(PathBuf, String)>> = None;
987 loop {
988 if !tick(&stopped, interval) {
989 return;
990 }
991 let Some(script) = shared.upgrade() else {
992 return;
993 };
994 let current = script.current();
995 let files = tracked.clone().unwrap_or_else(|| current.files.clone());
996 let now = stats(&files);
997 if quiet.as_ref() == Some(&now) || failed.as_ref() == Some(&now) {
998 gate.reset();
999 continue;
1000 }
1001 if gate.observe(Instant::now(), &now) == Step::Wait {
1002 continue;
1003 }
1004 let Some((read_stats, read)) = read_still(&files) else {
1007 gate.reset();
1008 continue;
1009 };
1010 if read_stats != now {
1011 gate.reset();
1012 continue;
1013 }
1014 if tracked.is_none() && hash_files(&read) == current.id.hash {
1015 quiet = Some(now);
1016 gate.reset();
1017 continue;
1018 }
1019 let attempt = script.attempt();
1020 drop(current);
1021
1022 let same_set = attempt.files.len() == files.len()
1026 && attempt.files.iter().zip(&files).all(|(a, b)| a.0 == b.0);
1027 if !same_set {
1028 tracked = Some(attempt.files.clone());
1029 gate.reset();
1030 continue;
1031 }
1032 if attempt.files != read || stats(&attempt.files) != now {
1034 gate.reset();
1035 continue;
1036 }
1037 tracked = None;
1038 match script.install(attempt) {
1039 Ok(Some(v)) => {
1040 failed = None;
1041 quiet = None;
1042 on_reload(Ok(v));
1043 }
1044 Ok(None) => quiet = Some(now),
1045 Err(e) => {
1046 failed = Some(now);
1047 on_reload(Err(e));
1048 }
1049 }
1050 gate.reset();
1051 }
1052}
1053
1054fn refuse_reentry() -> Result<(), Error> {
1058 if crate::host::inside_host() {
1059 return Err(Error::host(
1060 "E0010",
1061 "a script was called from inside one of its host functions",
1062 ));
1063 }
1064 Ok(())
1065}
1066
1067fn host_table(b: &mut ScriptBuilder) -> Result<HostTable, Error> {
1071 let registry = MethodRegistry::builtin();
1072 let mut table = HostTable::new();
1073 if let Some(steps) = b.host_call_steps {
1074 table.call_steps = steps;
1075 }
1076 let ident = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
1077 for mut f in std::mem::take(&mut b.host) {
1078 let Some((module, func)) = f.name.split_once('.').filter(|(m, n)| ident(m) && ident(n))
1079 else {
1080 return Err(Error::host(
1081 "E0001",
1082 format!(
1083 "host function '{}' must be named module.function, like \"geo.region\"",
1084 f.name
1085 ),
1086 ));
1087 };
1088 if table.get(module, func).is_some() {
1089 return Err(Error::host(
1090 "E0001",
1091 format!("host function '{}' is provided twice", f.name),
1092 ));
1093 }
1094 if registry
1097 .get(crate::eval::value::TypeTag::Object, func)
1098 .is_some()
1099 {
1100 return Err(Error::host(
1101 "E0001",
1102 format!(
1103 "host function '{}' is named like the built-in object method '{func}'",
1104 f.name
1105 ),
1106 ));
1107 }
1108 f.for_imports = b.host_for_imports.contains(&f.name);
1109 table.insert(f);
1110 }
1111 for n in &b.host_for_imports {
1112 if !table.fns().any(|f| f.name == *n) {
1113 return Err(Error::host(
1114 "E0001",
1115 format!("grant_to_imports names '{n}', which is not provided"),
1116 ));
1117 }
1118 }
1119 Ok(table)
1120}
1121
1122pub trait PureFn<Args>: Send + Sync + 'static {
1125 const ARITY: usize;
1126 #[doc(hidden)]
1127 fn erase(self) -> Arc<HostCall>;
1128}
1129
1130pub trait EffectFn<Args>: Send + Sync + 'static {
1133 const ARITY: usize;
1134 #[doc(hidden)]
1135 fn erase(self) -> Arc<HostCall>;
1136}
1137
1138fn host_result<R: Serialize>(r: R) -> HostOutcome {
1140 match r.serialize(ToValue(PhantomData)) {
1141 Ok(v) => HostOutcome::Value(v),
1142 Err(e) => HostOutcome::BadArgument(format!("its result: {e}")),
1143 }
1144}
1145
1146macro_rules! host_fn {
1147 ($n:expr $(, $t:ident $v:ident $i:tt)*) => {
1148 impl<F, R, $($t),*> PureFn<($($t,)*)> for F
1149 where
1150 F: Fn($($t),*) -> R + Send + Sync + 'static,
1151 R: Serialize,
1152 $($t: DeserializeOwned,)*
1153 {
1154 const ARITY: usize = $n;
1155 fn erase(self) -> Arc<HostCall> {
1156 Arc::new(move |args: &[serde_json::Value]| {
1157 $(
1158 let $v: $t = match serde_json::from_value(args[$i].clone()) {
1159 Ok(v) => v,
1160 Err(e) => {
1161 return HostOutcome::BadArgument(format!("argument {}: {e}", $i + 1))
1162 }
1163 };
1164 )*
1165 let _ = args;
1166 host_result(self($($v),*))
1167 })
1168 }
1169 }
1170
1171 impl<F, R, Er, $($t),*> EffectFn<($($t,)*)> for F
1172 where
1173 F: Fn($($t),*) -> Result<R, Er> + Send + Sync + 'static,
1174 R: Serialize,
1175 Er: fmt::Display,
1176 $($t: DeserializeOwned,)*
1177 {
1178 const ARITY: usize = $n;
1179 fn erase(self) -> Arc<HostCall> {
1180 Arc::new(move |args: &[serde_json::Value]| {
1181 $(
1182 let $v: $t = match serde_json::from_value(args[$i].clone()) {
1183 Ok(v) => v,
1184 Err(e) => {
1185 return HostOutcome::BadArgument(format!("argument {}: {e}", $i + 1))
1186 }
1187 };
1188 )*
1189 let _ = args;
1190 match self($($v),*) {
1191 Ok(r) => host_result(r),
1192 Err(e) => HostOutcome::Failed(e.to_string()),
1193 }
1194 })
1195 }
1196 }
1197 };
1198}
1199
1200host_fn!(0);
1201host_fn!(1, A a 0);
1202host_fn!(2, A a 0, B b 1);
1203host_fn!(3, A a 0, B b 1, C c 2);
1204host_fn!(4, A a 0, B b 1, C c 2, D d 3);
1205host_fn!(5, A a 0, B b 1, C c 2, D d 3, E e 4);
1206host_fn!(6, A a 0, B b 1, C c 2, D d 3, E e 4, G g 5);
1207
1208fn no_export(name: &str) -> Error {
1209 Error::host(
1210 "E0002",
1211 format!("the script exports nothing named '{name}'"),
1212 )
1213}
1214
1215fn from_json<T: DeserializeOwned>(json: serde_json::Value) -> Result<T, Error> {
1216 serde_path_to_error::deserialize(json).map_err(|e| {
1217 let path = e.path().to_string();
1218 Error::host(
1219 "E0006",
1220 format!(
1221 "the result does not match the host's type at {path}: {}",
1222 e.inner()
1223 ),
1224 )
1225 })
1226}
1227
1228pub struct Function<'s, A, R> {
1231 script: &'s Script,
1232 name: String,
1233 _types: PhantomData<fn(A) -> R>,
1234}
1235
1236impl<A: Args, R: DeserializeOwned> Function<'_, A, R> {
1237 pub fn call(&self, args: A) -> Result<R, Error> {
1238 from_json(self.script.invoke(&self.name, args)?)
1239 }
1240}
1241
1242impl<A, R> fmt::Debug for Function<'_, A, R> {
1243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1244 f.debug_struct("Function")
1245 .field("name", &self.name)
1246 .finish()
1247 }
1248}
1249
1250impl fmt::Debug for Script {
1251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1252 let current = self.shared.current();
1253 let exports: Vec<String> = current
1254 .loaded
1255 .with_dependent(|_, s| s.exports.keys().cloned().collect());
1256 f.debug_struct("Script")
1257 .field("exports", &exports)
1258 .field("version", ¤t.id)
1259 .field("budget", &self.shared.config.budget)
1260 .finish()
1261 }
1262}
1263
1264struct SharedFs(Arc<dyn FileAccess>);
1266
1267impl FileAccess for SharedFs {
1268 fn read(&self, path: &str) -> Result<String, FileError> {
1269 self.0.read(path)
1270 }
1271}
1272
1273pub trait Args {
1280 const ARITY: usize;
1281 #[doc(hidden)]
1282 fn to_values<'a>(&self) -> Result<Vec<Value<'a>>, BoundaryError>;
1283}
1284
1285macro_rules! args_tuple {
1286 ($n:expr $(, $t:ident $i:tt)*) => {
1287 impl<$($t: Serialize),*> Args for ($($t,)*) {
1288 const ARITY: usize = $n;
1289 #[allow(unused_variables)]
1290 fn to_values<'a>(&self) -> Result<Vec<Value<'a>>, BoundaryError> {
1291 Ok(vec![$(self.$i.serialize(ToValue(PhantomData))?),*])
1292 }
1293 }
1294 };
1295}
1296
1297args_tuple!(0);
1298args_tuple!(1, A 0);
1299args_tuple!(2, A 0, B 1);
1300args_tuple!(3, A 0, B 1, C 2);
1301args_tuple!(4, A 0, B 1, C 2, D 3);
1302args_tuple!(5, A 0, B 1, C 2, D 3, E 4);
1303args_tuple!(6, A 0, B 1, C 2, D 3, E 4, F 5);
1304
1305#[derive(Debug)]
1309pub struct BoundaryError(String);
1310
1311impl fmt::Display for BoundaryError {
1312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1313 f.write_str(&self.0)
1314 }
1315}
1316
1317impl std::error::Error for BoundaryError {}
1318
1319impl serde::ser::Error for BoundaryError {
1320 fn custom<T: fmt::Display>(msg: T) -> Self {
1321 BoundaryError(msg.to_string())
1322 }
1323}
1324
1325struct ToValue<'a>(PhantomData<Value<'a>>);
1328
1329fn refuse<T>(what: impl fmt::Display) -> Result<T, BoundaryError> {
1330 Err(BoundaryError(format!("{what} has no exact Aura value")))
1331}
1332
1333impl<'a> serde::Serializer for ToValue<'a> {
1334 type Ok = Value<'a>;
1335 type Error = BoundaryError;
1336 type SerializeSeq = SeqToValue<'a>;
1337 type SerializeTuple = SeqToValue<'a>;
1338 type SerializeTupleStruct = SeqToValue<'a>;
1339 type SerializeTupleVariant = VariantToValue<SeqToValue<'a>>;
1340 type SerializeMap = MapToValue<'a>;
1341 type SerializeStruct = MapToValue<'a>;
1342 type SerializeStructVariant = VariantToValue<MapToValue<'a>>;
1343
1344 fn serialize_bool(self, v: bool) -> Result<Value<'a>, BoundaryError> {
1345 Ok(Value::Bool(v))
1346 }
1347 fn serialize_i8(self, v: i8) -> Result<Value<'a>, BoundaryError> {
1348 Ok(Value::Int(v.into()))
1349 }
1350 fn serialize_i16(self, v: i16) -> Result<Value<'a>, BoundaryError> {
1351 Ok(Value::Int(v.into()))
1352 }
1353 fn serialize_i32(self, v: i32) -> Result<Value<'a>, BoundaryError> {
1354 Ok(Value::Int(v.into()))
1355 }
1356 fn serialize_i64(self, v: i64) -> Result<Value<'a>, BoundaryError> {
1357 Ok(Value::Int(v))
1358 }
1359 fn serialize_i128(self, v: i128) -> Result<Value<'a>, BoundaryError> {
1360 i64::try_from(v).map(Value::Int).or_else(|_| refuse(v))
1361 }
1362 fn serialize_u8(self, v: u8) -> Result<Value<'a>, BoundaryError> {
1363 Ok(Value::Int(v.into()))
1364 }
1365 fn serialize_u16(self, v: u16) -> Result<Value<'a>, BoundaryError> {
1366 Ok(Value::Int(v.into()))
1367 }
1368 fn serialize_u32(self, v: u32) -> Result<Value<'a>, BoundaryError> {
1369 Ok(Value::Int(v.into()))
1370 }
1371 fn serialize_u64(self, v: u64) -> Result<Value<'a>, BoundaryError> {
1372 i64::try_from(v).map(Value::Int).or_else(|_| refuse(v))
1373 }
1374 fn serialize_u128(self, v: u128) -> Result<Value<'a>, BoundaryError> {
1375 i64::try_from(v).map(Value::Int).or_else(|_| refuse(v))
1376 }
1377 fn serialize_f32(self, v: f32) -> Result<Value<'a>, BoundaryError> {
1378 self.serialize_f64(v.into())
1379 }
1380 fn serialize_f64(self, v: f64) -> Result<Value<'a>, BoundaryError> {
1381 if v.is_finite() {
1382 Ok(Value::Float(v))
1383 } else {
1384 refuse(v)
1385 }
1386 }
1387 fn serialize_char(self, v: char) -> Result<Value<'a>, BoundaryError> {
1388 Ok(Value::str(v.encode_utf8(&mut [0; 4])))
1389 }
1390 fn serialize_str(self, v: &str) -> Result<Value<'a>, BoundaryError> {
1391 Ok(Value::str(v))
1392 }
1393 fn serialize_bytes(self, _: &[u8]) -> Result<Value<'a>, BoundaryError> {
1394 refuse("a byte string")
1395 }
1396 fn serialize_none(self) -> Result<Value<'a>, BoundaryError> {
1397 Ok(Value::Null)
1398 }
1399 fn serialize_some<T: ?Sized + Serialize>(self, v: &T) -> Result<Value<'a>, BoundaryError> {
1400 v.serialize(self)
1401 }
1402 fn serialize_unit(self) -> Result<Value<'a>, BoundaryError> {
1403 Ok(Value::Null)
1404 }
1405 fn serialize_unit_struct(self, _: &'static str) -> Result<Value<'a>, BoundaryError> {
1406 Ok(Value::Null)
1407 }
1408 fn serialize_unit_variant(
1409 self,
1410 _: &'static str,
1411 _: u32,
1412 variant: &'static str,
1413 ) -> Result<Value<'a>, BoundaryError> {
1414 Ok(Value::str(variant))
1415 }
1416 fn serialize_newtype_struct<T: ?Sized + Serialize>(
1417 self,
1418 _: &'static str,
1419 v: &T,
1420 ) -> Result<Value<'a>, BoundaryError> {
1421 v.serialize(self)
1422 }
1423 fn serialize_newtype_variant<T: ?Sized + Serialize>(
1424 self,
1425 _: &'static str,
1426 _: u32,
1427 variant: &'static str,
1428 v: &T,
1429 ) -> Result<Value<'a>, BoundaryError> {
1430 let inner = v.serialize(ToValue(PhantomData))?;
1431 Ok(Value::object(IndexMap::from([(
1432 variant.to_string(),
1433 inner,
1434 )])))
1435 }
1436 fn serialize_seq(self, len: Option<usize>) -> Result<SeqToValue<'a>, BoundaryError> {
1437 Ok(SeqToValue(Vec::with_capacity(len.unwrap_or(0))))
1438 }
1439 fn serialize_tuple(self, len: usize) -> Result<SeqToValue<'a>, BoundaryError> {
1440 self.serialize_seq(Some(len))
1441 }
1442 fn serialize_tuple_struct(
1443 self,
1444 _: &'static str,
1445 len: usize,
1446 ) -> Result<SeqToValue<'a>, BoundaryError> {
1447 self.serialize_seq(Some(len))
1448 }
1449 fn serialize_tuple_variant(
1450 self,
1451 _: &'static str,
1452 _: u32,
1453 variant: &'static str,
1454 len: usize,
1455 ) -> Result<Self::SerializeTupleVariant, BoundaryError> {
1456 Ok(VariantToValue(variant, self.serialize_seq(Some(len))?))
1457 }
1458 fn serialize_map(self, len: Option<usize>) -> Result<MapToValue<'a>, BoundaryError> {
1459 Ok(MapToValue {
1460 map: IndexMap::with_capacity(len.unwrap_or(0)),
1461 key: None,
1462 })
1463 }
1464 fn serialize_struct(
1465 self,
1466 _: &'static str,
1467 len: usize,
1468 ) -> Result<MapToValue<'a>, BoundaryError> {
1469 self.serialize_map(Some(len))
1470 }
1471 fn serialize_struct_variant(
1472 self,
1473 _: &'static str,
1474 _: u32,
1475 variant: &'static str,
1476 len: usize,
1477 ) -> Result<Self::SerializeStructVariant, BoundaryError> {
1478 Ok(VariantToValue(variant, self.serialize_map(Some(len))?))
1479 }
1480}
1481
1482struct SeqToValue<'a>(Vec<Value<'a>>);
1483
1484impl<'a> SeqToValue<'a> {
1485 fn push<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1486 self.0.push(v.serialize(ToValue(PhantomData))?);
1487 Ok(())
1488 }
1489}
1490
1491impl<'a> serde::ser::SerializeSeq for SeqToValue<'a> {
1492 type Ok = Value<'a>;
1493 type Error = BoundaryError;
1494 fn serialize_element<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1495 self.push(v)
1496 }
1497 fn end(self) -> Result<Value<'a>, BoundaryError> {
1498 Ok(Value::list(self.0))
1499 }
1500}
1501
1502impl<'a> serde::ser::SerializeTuple for SeqToValue<'a> {
1503 type Ok = Value<'a>;
1504 type Error = BoundaryError;
1505 fn serialize_element<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1506 self.push(v)
1507 }
1508 fn end(self) -> Result<Value<'a>, BoundaryError> {
1509 Ok(Value::list(self.0))
1510 }
1511}
1512
1513impl<'a> serde::ser::SerializeTupleStruct for SeqToValue<'a> {
1514 type Ok = Value<'a>;
1515 type Error = BoundaryError;
1516 fn serialize_field<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1517 self.push(v)
1518 }
1519 fn end(self) -> Result<Value<'a>, BoundaryError> {
1520 Ok(Value::list(self.0))
1521 }
1522}
1523
1524struct MapToValue<'a> {
1525 map: IndexMap<String, Value<'a>>,
1526 key: Option<String>,
1527}
1528
1529impl<'a> MapToValue<'a> {
1530 fn finish(self) -> Value<'a> {
1531 Value::object(self.map)
1532 }
1533}
1534
1535impl<'a> serde::ser::SerializeMap for MapToValue<'a> {
1536 type Ok = Value<'a>;
1537 type Error = BoundaryError;
1538 fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), BoundaryError> {
1539 match key.serialize(ToValue(PhantomData))? {
1540 Value::Str(s) => {
1541 self.key = Some(s.to_string());
1542 Ok(())
1543 }
1544 other => refuse(format!("a map key of type {}", other.type_name())),
1545 }
1546 }
1547 fn serialize_value<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1548 let key = self.key.take().expect("serde calls serialize_key first");
1549 self.map.insert(key, v.serialize(ToValue(PhantomData))?);
1550 Ok(())
1551 }
1552 fn end(self) -> Result<Value<'a>, BoundaryError> {
1553 Ok(self.finish())
1554 }
1555}
1556
1557impl<'a> serde::ser::SerializeStruct for MapToValue<'a> {
1558 type Ok = Value<'a>;
1559 type Error = BoundaryError;
1560 fn serialize_field<T: ?Sized + Serialize>(
1561 &mut self,
1562 key: &'static str,
1563 v: &T,
1564 ) -> Result<(), BoundaryError> {
1565 self.map
1566 .insert(key.to_string(), v.serialize(ToValue(PhantomData))?);
1567 Ok(())
1568 }
1569 fn end(self) -> Result<Value<'a>, BoundaryError> {
1570 Ok(self.finish())
1571 }
1572}
1573
1574struct VariantToValue<S>(&'static str, S);
1577
1578impl<'a> serde::ser::SerializeTupleVariant for VariantToValue<SeqToValue<'a>> {
1579 type Ok = Value<'a>;
1580 type Error = BoundaryError;
1581 fn serialize_field<T: ?Sized + Serialize>(&mut self, v: &T) -> Result<(), BoundaryError> {
1582 self.1.push(v)
1583 }
1584 fn end(self) -> Result<Value<'a>, BoundaryError> {
1585 Ok(Value::object(IndexMap::from([(
1586 self.0.to_string(),
1587 Value::list(self.1 .0),
1588 )])))
1589 }
1590}
1591
1592impl<'a> serde::ser::SerializeStructVariant for VariantToValue<MapToValue<'a>> {
1593 type Ok = Value<'a>;
1594 type Error = BoundaryError;
1595 fn serialize_field<T: ?Sized + Serialize>(
1596 &mut self,
1597 key: &'static str,
1598 v: &T,
1599 ) -> Result<(), BoundaryError> {
1600 serde::ser::SerializeStruct::serialize_field(&mut self.1, key, v)
1601 }
1602 fn end(self) -> Result<Value<'a>, BoundaryError> {
1603 Ok(Value::object(IndexMap::from([(
1604 self.0.to_string(),
1605 self.1.finish(),
1606 )])))
1607 }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612 mod settle {
1615 use super::super::{Settle, Stats, Step};
1616 use std::time::{Duration, Instant, SystemTime};
1617
1618 fn file(len: u64, mtime_ms: u64) -> Stats {
1619 vec![Some((
1620 len,
1621 Some(SystemTime::UNIX_EPOCH + Duration::from_millis(mtime_ms)),
1622 ))]
1623 }
1624
1625 fn run(settle_ms: u64, story: &[(u64, Stats)]) -> Vec<Step> {
1627 let t0 = Instant::now();
1628 let mut g = Settle::new(Duration::from_millis(settle_ms));
1629 story
1630 .iter()
1631 .map(|(ms, s)| g.observe(t0 + Duration::from_millis(*ms), s))
1632 .collect()
1633 }
1634
1635 #[test]
1636 fn files_left_alone_for_the_settle_period_are_read() {
1637 let s = file(100, 5);
1638 let steps = run(1000, &[(0, s.clone()), (500, s.clone()), (1000, s)]);
1639 assert_eq!(steps, [Step::Wait, Step::Wait, Step::Look]);
1640 }
1641
1642 #[test]
1646 fn a_writer_stalled_mid_write_is_not_read() {
1647 let empty = file(0, 40);
1648 let full = file(100, 90);
1649 let steps = run(
1650 1000,
1651 &[
1652 (40, empty.clone()),
1653 (60, empty.clone()),
1654 (80, empty),
1655 (100, full),
1656 ],
1657 );
1658 assert!(!steps.contains(&Step::Look), "{steps:?}");
1659 }
1660
1661 #[test]
1664 fn a_valid_prefix_that_does_not_last_is_not_read() {
1665 let prefix = file(60, 10);
1666 let whole = file(100, 610);
1667 let mut story: Vec<(u64, Stats)> =
1668 (0..=6).map(|i| (10 + i * 100, prefix.clone())).collect();
1669 story.push((610, whole.clone()));
1670 story.push((1600, whole.clone()));
1671 story.push((1610, whole));
1672 let steps = run(1000, &story);
1673 let looks: Vec<_> = steps
1674 .iter()
1675 .enumerate()
1676 .filter(|(_, s)| **s == Step::Look)
1677 .collect();
1678 assert_eq!(looks.len(), 1, "{steps:?}");
1679 assert_eq!(
1680 looks[0].0,
1681 story.len() - 1,
1682 "read before the whole file settled"
1683 );
1684 }
1685
1686 #[test]
1688 fn every_change_restarts_the_count() {
1689 let steps = run(
1690 1000,
1691 &[
1692 (0, file(10, 0)),
1693 (900, file(10, 900)), (1800, file(11, 900)), (2700, file(11, 900)),
1696 (2800, file(11, 900)),
1697 ],
1698 );
1699 assert_eq!(
1700 steps,
1701 [Step::Wait, Step::Wait, Step::Wait, Step::Wait, Step::Look]
1702 );
1703 }
1704
1705 #[test]
1706 fn reset_starts_over() {
1707 let s = file(1, 1);
1708 let t0 = Instant::now();
1709 let mut g = Settle::new(Duration::from_millis(100));
1710 g.observe(t0, &s);
1711 g.reset();
1712 assert_eq!(g.observe(t0 + Duration::from_millis(200), &s), Step::Wait);
1713 assert_eq!(g.observe(t0 + Duration::from_millis(300), &s), Step::Look);
1714 }
1715
1716 #[test]
1717 fn the_default_is_two_intervals_and_at_least_a_second() {
1718 use super::super::default_settle;
1719 assert_eq!(
1720 default_settle(Duration::from_millis(20)),
1721 Duration::from_secs(1)
1722 );
1723 assert_eq!(
1724 default_settle(Duration::from_secs(2)),
1725 Duration::from_secs(4)
1726 );
1727 }
1728 }
1729
1730 use super::*;
1731
1732 #[test]
1733 fn a_script_is_send_and_sync() {
1734 fn check<T: Send + Sync>() {}
1735 check::<Script>();
1736 }
1737
1738 #[test]
1739 fn diagnostics_render_as_the_cli_does() {
1740 let e = Error::host("E0002", "the script exports nothing named 'g'");
1741 assert_eq!(e.code(), "E0002");
1742 assert!(e.to_string().contains("E0002"));
1743 }
1744}