graphix_package_core/
testing.rs1use anyhow::Result;
2use graphix_compiler::expr::ModuleResolver;
3use graphix_rt::{GXConfig, GXEvent, GXHandle, GXRt, NoExt};
4use poolshark::global::GPooled;
5use tokio::sync::mpsc;
6
7pub struct TestCtx {
8 pub internal_only: netidx::InternalOnly,
9 pub rt: GXHandle<NoExt>,
10}
11
12impl TestCtx {
13 pub async fn shutdown(self) {
14 drop(self.rt);
15 self.internal_only.shutdown().await
16 }
17}
18
19pub type RegisterFn = fn(
20 &mut graphix_compiler::ExecCtx<GXRt<NoExt>, <NoExt as graphix_rt::GXExt>::UserEvent>,
21 &mut fxhash::FxHashMap<netidx_core::path::Path, arcstr::ArcStr>,
22 &mut graphix_package::IndexSet<arcstr::ArcStr>,
23) -> Result<()>;
24
25pub async fn init_with_resolvers(
26 sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
27 register: &[RegisterFn],
28 mut resolvers: Vec<ModuleResolver>,
29) -> Result<TestCtx> {
30 let _ = env_logger::try_init();
31 let env = netidx::InternalOnly::new().await?;
32 let mut ctx = graphix_compiler::ExecCtx::new(GXRt::<NoExt>::new(
33 env.publisher().clone(),
34 env.subscriber().clone(),
35 ))?;
36 let mut modules = fxhash::FxHashMap::default();
37 let mut root_mods = graphix_package::IndexSet::new();
38 for f in register {
39 f(&mut ctx, &mut modules, &mut root_mods)?;
40 }
41 let mut parts = Vec::new();
42 for name in &root_mods {
43 if name == "core" {
44 parts.push(format!("mod core;\nuse core"));
45 } else {
46 parts.push(format!("mod {name}"));
47 }
48 }
49 let root = arcstr::ArcStr::from(parts.join(";\n"));
50 resolvers.insert(0, ModuleResolver::VFS(modules));
51 Ok(TestCtx {
52 internal_only: env,
53 rt: GXConfig::builder(ctx, sub)
54 .root(root)
55 .resolvers(resolvers)
56 .build()?
57 .start()
58 .await?,
59 })
60}
61
62pub async fn init(
63 sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
64 register: &[RegisterFn],
65) -> Result<TestCtx> {
66 init_with_resolvers(sub, register, vec![]).await
67}
68
69pub use graphix_compiler::expr::parser::GRAPHIX_ESC;
70pub use poolshark::local::LPooled;
71
72pub fn escape_path(path: std::path::Display) -> LPooled<String> {
73 use std::fmt::Write;
74 let mut buf: LPooled<String> = LPooled::take();
75 let mut res: LPooled<String> = LPooled::take();
76 write!(buf, "{path}").unwrap();
77 GRAPHIX_ESC.escape_to(&*buf, &mut res);
78 res
79}
80
81#[macro_export]
82macro_rules! run {
83 ($name:ident, $code:expr, $pred:expr) => {
84 $crate::run!($name, $pred, "/test.gx" => format!("let result = {}", $code));
85 };
86 ($name:ident, $pred:expr, $($path:literal => $code:expr),+) => {
87 #[tokio::test(flavor = "current_thread")]
88 async fn $name() -> ::anyhow::Result<()> {
89 let (tx, mut rx) = ::tokio::sync::mpsc::channel(10);
90 let tbl = ::fxhash::FxHashMap::from_iter([
91 $((::netidx_core::path::Path::from($path), ::arcstr::ArcStr::from($code))),+
92 ]);
93 let resolver = ::graphix_compiler::expr::ModuleResolver::VFS(tbl);
94 let ctx = $crate::testing::init_with_resolvers(
95 tx, &crate::TEST_REGISTER, vec![resolver],
96 ).await?;
97 let bs = &ctx.rt;
98 match bs.compile(::arcstr::literal!("{ mod test; test::result }")).await {
99 Err(e) => assert!($pred(dbg!(Err(e)))),
100 Ok(e) => {
101 dbg!("compilation succeeded");
102 let eid = e.exprs[0].id;
103 loop {
104 match rx.recv().await {
105 None => ::anyhow::bail!("runtime died"),
106 Some(mut batch) => {
107 for e in batch.drain(..) {
108 match e {
109 ::graphix_rt::GXEvent::Env(_) => (),
110 ::graphix_rt::GXEvent::Updated(id, v) => {
111 eprintln!("{v}");
112 assert_eq!(id, eid);
113 assert!($pred(Ok(&v)));
114 return Ok(());
115 }
116 }
117 }
118 }
119 }
120 }
121 }
122 }
123 ctx.shutdown().await;
124 Ok(())
125 }
126 };
127}
128
129#[macro_export]
130macro_rules! run_with_tempdir {
131 (
132 name: $test_name:ident,
133 code: $code:literal,
134 setup: |$temp_dir:ident| $setup:block,
135 expect_error
136 ) => {
137 $crate::run_with_tempdir! {
138 name: $test_name,
139 code: $code,
140 setup: |$temp_dir| $setup,
141 expect: |v: ::netidx::subscriber::Value| -> ::anyhow::Result<()> {
142 if matches!(v, ::netidx::subscriber::Value::Error(_)) {
143 Ok(())
144 } else {
145 panic!("expected Error value, got: {v:?}")
146 }
147 }
148 }
149 };
150 (
151 name: $test_name:ident,
152 code: $code:literal,
153 setup: |$temp_dir:ident| $setup:block,
154 verify: |$verify_dir:ident| $verify:block
155 ) => {
156 $crate::run_with_tempdir! {
157 name: $test_name,
158 code: $code,
159 setup: |$temp_dir| $setup,
160 expect: |v: ::netidx::subscriber::Value| -> ::anyhow::Result<()> {
161 if !matches!(v, ::netidx::subscriber::Value::Null) {
162 panic!("expected Null (success), got: {v:?}");
163 }
164 Ok(())
165 },
166 verify: |$verify_dir| $verify
167 }
168 };
169 (
170 name: $test_name:ident,
171 code: $code:literal,
172 setup: |$temp_dir:ident| $setup:block,
173 expect: $expect_payload:expr
174 $(, verify: |$verify_dir:ident| $verify:block)?
175 ) => {
176 #[tokio::test(flavor = "current_thread")]
177 async fn $test_name() -> ::anyhow::Result<()> {
178 let (tx, mut rx) = ::tokio::sync::mpsc::channel::<
179 ::poolshark::global::GPooled<Vec<::graphix_rt::GXEvent>>
180 >(10);
181 let ctx = $crate::testing::init(tx, &crate::TEST_REGISTER).await?;
182 let $temp_dir = ::tempfile::tempdir()?;
183
184 let test_file = { $setup };
185
186 let code = format!(
187 $code,
188 $crate::testing::escape_path(test_file.display())
189 );
190 let compiled = ctx.rt.compile(::arcstr::ArcStr::from(code)).await?;
191 let eid = compiled.exprs[0].id;
192
193 let timeout = ::tokio::time::sleep(::std::time::Duration::from_secs(2));
194 ::tokio::pin!(timeout);
195
196 loop {
197 ::tokio::select! {
198 _ = &mut timeout => panic!("timeout waiting for result"),
199 Some(mut batch) = rx.recv() => {
200 for event in batch.drain(..) {
201 if let ::graphix_rt::GXEvent::Updated(id, v) = event {
202 if id == eid {
203 $expect_payload(v)?;
204 $(
205 let $verify_dir = &$temp_dir;
206 $verify
207 )?
208 return Ok(());
209 }
210 }
211 }
212 }
213 }
214 }
215 }
216 };
217}