Skip to main content

graphix_package_core/
testing.rs

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
89/// Evaluate a graphix expression and return its Value.
90///
91/// Compiles `code` as `let result = {code}` in a throwaway module,
92/// waits for the first update, and returns the resulting value along
93/// with the test context (caller must shut it down).
94pub 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}