1use anyhow::{bail, Result};
2use graphix_compiler::expr::ModuleResolver;
3use graphix_rt::{GXConfig, GXEvent, GXHandle, GXRt, NoExt};
4use netidx::publisher::Value;
5use poolshark::global::GPooled;
6use tokio::sync::mpsc;
7
8pub struct TestCtx {
9 pub internal_only: netidx::InternalOnly,
10 pub rt: GXHandle<NoExt>,
11}
12
13impl TestCtx {
14 pub async fn shutdown(self) {
15 drop(self.rt);
16 self.internal_only.shutdown().await
17 }
18}
19
20pub type RegisterFn = fn(
21 &mut graphix_compiler::ExecCtx<GXRt<NoExt>, <NoExt as graphix_rt::GXExt>::UserEvent>,
22 &mut ahash::AHashMap<netidx_core::path::Path, arcstr::ArcStr>,
23 &mut graphix_package::IndexSet<arcstr::ArcStr>,
24) -> Result<()>;
25
26pub async fn init_with_resolvers(
27 sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
28 register: &[RegisterFn],
29 resolvers: Vec<ModuleResolver>,
30) -> Result<TestCtx> {
31 init_with_setup(sub, register, resolvers, |_| {}).await
32}
33
34pub async fn init(
35 sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
36 register: &[RegisterFn],
37) -> Result<TestCtx> {
38 init_with_setup(sub, register, vec![], |_| {}).await
39}
40
41pub async fn init_with_setup<F>(
42 sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
43 register: &[RegisterFn],
44 resolvers: Vec<ModuleResolver>,
45 setup: F,
46) -> Result<TestCtx>
47where
48 F: FnOnce(
49 &mut graphix_compiler::ExecCtx<
50 GXRt<NoExt>,
51 <NoExt as graphix_rt::GXExt>::UserEvent,
52 >,
53 ),
54{
55 let _ = env_logger::try_init();
56 let env = netidx::InternalOnly::new().await?;
57 let mut ctx = graphix_compiler::ExecCtx::new(GXRt::<NoExt>::new(
58 env.publisher().clone(),
59 env.subscriber().clone(),
60 ))?;
61 let mut modules = ahash::AHashMap::default();
62 let mut root_mods = graphix_package::IndexSet::new();
63 for f in register {
64 f(&mut ctx, &mut modules, &mut root_mods)?;
65 }
66 setup(&mut ctx);
67 let mut parts = Vec::new();
68 for name in &root_mods {
69 if name == "core" {
70 parts.push(format!("mod core;\nuse core"));
71 } else {
72 parts.push(format!("mod {name}"));
73 }
74 }
75 let root = arcstr::ArcStr::from(parts.join(";\n"));
76 let mut all_resolvers = vec![ModuleResolver::VFS(modules)];
77 all_resolvers.extend(resolvers);
78 Ok(TestCtx {
79 internal_only: env,
80 rt: GXConfig::builder(ctx, sub)
81 .root(root)
82 .resolvers(all_resolvers)
83 .build()?
84 .start()
85 .await?,
86 })
87}
88
89pub async fn eval(code: &str, register: &[RegisterFn]) -> Result<(Value, TestCtx)> {
95 eval_with_setup(code, register, |_| {}).await
96}
97
98pub async fn eval_with_setup<F>(
99 code: &str,
100 register: &[RegisterFn],
101 setup: F,
102) -> Result<(Value, TestCtx)>
103where
104 F: FnOnce(
105 &mut graphix_compiler::ExecCtx<
106 GXRt<NoExt>,
107 <NoExt as graphix_rt::GXExt>::UserEvent,
108 >,
109 ),
110{
111 let (tx, mut rx) = mpsc::channel(10);
112 let gx_code = format!("let result = {code}");
113 let tbl = ahash::AHashMap::from_iter([(
114 netidx_core::path::Path::from("/test.gx"),
115 arcstr::ArcStr::from(gx_code),
116 )]);
117 let resolver = ModuleResolver::VFS(tbl);
118 let ctx = init_with_setup(tx, register, vec![resolver], setup).await?;
119 let compiled = ctx.rt.compile(arcstr::literal!("{ mod test; test::result }")).await?;
120 let eid = compiled.exprs[0].id;
121 let timeout = tokio::time::sleep(std::time::Duration::from_secs(5));
122 tokio::pin!(timeout);
123 loop {
124 tokio::select! {
125 _ = &mut timeout => bail!("timeout waiting for graphix result"),
126 batch = rx.recv() => match batch {
127 None => bail!("graphix runtime died"),
128 Some(mut batch) => {
129 for e in batch.drain(..) {
130 if let GXEvent::Updated(id, v) = e {
131 if id == eid {
132 return Ok((v, ctx));
133 }
134 }
135 }
136 }
137 }
138 }
139 }
140}
141
142pub use graphix_compiler::expr::parser::GRAPHIX_ESC;
143pub use poolshark::local::LPooled;
144
145pub fn escape_path(path: std::path::Display) -> LPooled<String> {
146 use std::fmt::Write;
147 let mut buf: LPooled<String> = LPooled::take();
148 let mut res: LPooled<String> = LPooled::take();
149 write!(buf, "{path}").unwrap();
150 GRAPHIX_ESC.escape_to(&*buf, &mut res);
151 res
152}
153
154#[macro_export]
155macro_rules! run {
156 ($name:ident, $code:expr, $pred:expr) => {
157 $crate::run!(@impl $name, $pred, 30, "/test.gx" => format!("let result = {}", $code));
158 };
159 ($name:ident, $code:expr, $pred:expr, timeout: $timeout:expr) => {
160 $crate::run!(@impl $name, $pred, $timeout, "/test.gx" => format!("let result = {}", $code));
161 };
162 ($name:ident, $pred:expr, $($path:literal => $code:expr),+) => {
163 $crate::run!(@impl $name, $pred, 30, $($path => $code),+);
164 };
165 (@impl $name:ident, $pred:expr, $timeout:expr, $($path:literal => $code:expr),+) => {
166 #[tokio::test(flavor = "current_thread")]
167 async fn $name() -> ::anyhow::Result<()> {
168 let (tx, mut rx) = ::tokio::sync::mpsc::channel(10);
169 let tbl = ::ahash::AHashMap::from_iter([
170 $((::netidx_core::path::Path::from($path), ::arcstr::ArcStr::from($code))),+
171 ]);
172 let resolver = ::graphix_compiler::expr::ModuleResolver::VFS(tbl);
173 let ctx = $crate::testing::init_with_resolvers(
174 tx, &crate::TEST_REGISTER, vec![resolver],
175 ).await?;
176 let bs = &ctx.rt;
177 match bs.compile(::arcstr::literal!("{ mod test; test::result }")).await {
178 Err(e) => assert!($pred(dbg!(Err(e)))),
179 Ok(e) => {
180 dbg!("compilation succeeded");
181 let eid = e.exprs[0].id;
182 let timeout = ::tokio::time::sleep(
183 ::std::time::Duration::from_secs($timeout),
184 );
185 ::tokio::pin!(timeout);
186 loop {
187 ::tokio::select! {
188 _ = &mut timeout => ::anyhow::bail!(
189 "timeout after {}s waiting for result", $timeout,
190 ),
191 batch = rx.recv() => match batch {
192 None => ::anyhow::bail!("runtime died"),
193 Some(mut batch) => {
194 for e in batch.drain(..) {
195 match e {
196 ::graphix_rt::GXEvent::Env(_) => (),
197 ::graphix_rt::GXEvent::Updated(id, v) => {
198 eprintln!("{v}");
199 assert_eq!(id, eid);
200 assert!($pred(Ok(&v)));
201 return Ok(());
202 }
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210 }
211 ctx.shutdown().await;
212 Ok(())
213 }
214 };
215}
216
217#[macro_export]
218macro_rules! run_with_tempdir {
219 (
220 name: $test_name:ident,
221 code: $code:literal,
222 setup: |$temp_dir:ident| $setup:block,
223 expect_error
224 ) => {
225 $crate::run_with_tempdir! {
226 name: $test_name,
227 code: $code,
228 setup: |$temp_dir| $setup,
229 expect: |v: ::netidx::subscriber::Value| -> ::anyhow::Result<()> {
230 if matches!(v, ::netidx::subscriber::Value::Error(_)) {
231 Ok(())
232 } else {
233 panic!("expected Error value, got: {v:?}")
234 }
235 }
236 }
237 };
238 (
239 name: $test_name:ident,
240 code: $code:literal,
241 setup: |$temp_dir:ident| $setup:block,
242 verify: |$verify_dir:ident| $verify:block
243 ) => {
244 $crate::run_with_tempdir! {
245 name: $test_name,
246 code: $code,
247 setup: |$temp_dir| $setup,
248 expect: |v: ::netidx::subscriber::Value| -> ::anyhow::Result<()> {
249 if !matches!(v, ::netidx::subscriber::Value::Null) {
250 panic!("expected Null (success), got: {v:?}");
251 }
252 Ok(())
253 },
254 verify: |$verify_dir| $verify
255 }
256 };
257 (
258 name: $test_name:ident,
259 code: $code:literal,
260 setup: |$temp_dir:ident| $setup:block,
261 expect: $expect_payload:expr
262 $(, verify: |$verify_dir:ident| $verify:block)?
263 ) => {
264 #[tokio::test(flavor = "current_thread")]
265 async fn $test_name() -> ::anyhow::Result<()> {
266 let (tx, mut rx) = ::tokio::sync::mpsc::channel::<
267 ::poolshark::global::GPooled<Vec<::graphix_rt::GXEvent>>
268 >(10);
269 let ctx = $crate::testing::init(tx, &crate::TEST_REGISTER).await?;
270 let $temp_dir = ::tempfile::tempdir()?;
271
272 let test_file = { $setup };
273
274 let code = format!(
275 $code,
276 $crate::testing::escape_path(test_file.display())
277 );
278 let compiled = ctx.rt.compile(::arcstr::ArcStr::from(code)).await?;
279 let eid = compiled.exprs[0].id;
280
281 let timeout = ::tokio::time::sleep(::std::time::Duration::from_secs(2));
282 ::tokio::pin!(timeout);
283
284 loop {
285 ::tokio::select! {
286 _ = &mut timeout => panic!("timeout waiting for result"),
287 Some(mut batch) = rx.recv() => {
288 for event in batch.drain(..) {
289 if let ::graphix_rt::GXEvent::Updated(id, v) = event {
290 if id == eid {
291 $expect_payload(v)?;
292 $(
293 let $verify_dir = &$temp_dir;
294 $verify
295 )?
296 return Ok(());
297 }
298 }
299 }
300 }
301 }
302 }
303 }
304 };
305}