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 fxhash::FxHashMap<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    mut resolvers: Vec<ModuleResolver>,
30) -> Result<TestCtx> {
31    let _ = env_logger::try_init();
32    let env = netidx::InternalOnly::new().await?;
33    let mut ctx = graphix_compiler::ExecCtx::new(GXRt::<NoExt>::new(
34        env.publisher().clone(),
35        env.subscriber().clone(),
36    ))?;
37    let mut modules = fxhash::FxHashMap::default();
38    let mut root_mods = graphix_package::IndexSet::new();
39    for f in register {
40        f(&mut ctx, &mut modules, &mut root_mods)?;
41    }
42    let mut parts = Vec::new();
43    for name in &root_mods {
44        if name == "core" {
45            parts.push(format!("mod core;\nuse core"));
46        } else {
47            parts.push(format!("mod {name}"));
48        }
49    }
50    let root = arcstr::ArcStr::from(parts.join(";\n"));
51    resolvers.insert(0, ModuleResolver::VFS(modules));
52    Ok(TestCtx {
53        internal_only: env,
54        rt: GXConfig::builder(ctx, sub)
55            .root(root)
56            .resolvers(resolvers)
57            .build()?
58            .start()
59            .await?,
60    })
61}
62
63pub async fn init(
64    sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
65    register: &[RegisterFn],
66) -> Result<TestCtx> {
67    init_with_resolvers(sub, register, vec![]).await
68}
69
70/// Evaluate a graphix expression and return its Value.
71///
72/// Compiles `code` as `let result = {code}` in a throwaway module,
73/// waits for the first update, and returns the resulting value along
74/// with the test context (caller must shut it down).
75pub async fn eval(code: &str, register: &[RegisterFn]) -> Result<(Value, TestCtx)> {
76    let (tx, mut rx) = mpsc::channel(10);
77    let gx_code = format!("let result = {code}");
78    let tbl = fxhash::FxHashMap::from_iter([(
79        netidx_core::path::Path::from("/test.gx"),
80        arcstr::ArcStr::from(gx_code),
81    )]);
82    let resolver = ModuleResolver::VFS(tbl);
83    let ctx = init_with_resolvers(tx, register, vec![resolver]).await?;
84    let compiled = ctx.rt.compile(arcstr::literal!("{ mod test; test::result }")).await?;
85    let eid = compiled.exprs[0].id;
86    let timeout = tokio::time::sleep(std::time::Duration::from_secs(5));
87    tokio::pin!(timeout);
88    loop {
89        tokio::select! {
90            _ = &mut timeout => bail!("timeout waiting for graphix result"),
91            batch = rx.recv() => match batch {
92                None => bail!("graphix runtime died"),
93                Some(mut batch) => {
94                    for e in batch.drain(..) {
95                        if let GXEvent::Updated(id, v) = e {
96                            if id == eid {
97                                return Ok((v, ctx));
98                            }
99                        }
100                    }
101                }
102            }
103        }
104    }
105}
106
107pub use graphix_compiler::expr::parser::GRAPHIX_ESC;
108pub use poolshark::local::LPooled;
109
110pub fn escape_path(path: std::path::Display) -> LPooled<String> {
111    use std::fmt::Write;
112    let mut buf: LPooled<String> = LPooled::take();
113    let mut res: LPooled<String> = LPooled::take();
114    write!(buf, "{path}").unwrap();
115    GRAPHIX_ESC.escape_to(&*buf, &mut res);
116    res
117}
118
119#[macro_export]
120macro_rules! run {
121    ($name:ident, $code:expr, $pred:expr) => {
122        $crate::run!($name, $pred, "/test.gx" => format!("let result = {}", $code));
123    };
124    ($name:ident, $pred:expr, $($path:literal => $code:expr),+) => {
125        #[tokio::test(flavor = "current_thread")]
126        async fn $name() -> ::anyhow::Result<()> {
127            let (tx, mut rx) = ::tokio::sync::mpsc::channel(10);
128            let tbl = ::fxhash::FxHashMap::from_iter([
129                $((::netidx_core::path::Path::from($path), ::arcstr::ArcStr::from($code))),+
130            ]);
131            let resolver = ::graphix_compiler::expr::ModuleResolver::VFS(tbl);
132            let ctx = $crate::testing::init_with_resolvers(
133                tx, &crate::TEST_REGISTER, vec![resolver],
134            ).await?;
135            let bs = &ctx.rt;
136            match bs.compile(::arcstr::literal!("{ mod test; test::result }")).await {
137                Err(e) => assert!($pred(dbg!(Err(e)))),
138                Ok(e) => {
139                    dbg!("compilation succeeded");
140                    let eid = e.exprs[0].id;
141                    loop {
142                        match rx.recv().await {
143                            None => ::anyhow::bail!("runtime died"),
144                            Some(mut batch) => {
145                                for e in batch.drain(..) {
146                                    match e {
147                                        ::graphix_rt::GXEvent::Env(_) => (),
148                                        ::graphix_rt::GXEvent::Updated(id, v) => {
149                                            eprintln!("{v}");
150                                            assert_eq!(id, eid);
151                                            assert!($pred(Ok(&v)));
152                                            return Ok(());
153                                        }
154                                    }
155                                }
156                            }
157                        }
158                    }
159                }
160            }
161            ctx.shutdown().await;
162            Ok(())
163        }
164    };
165}
166
167#[macro_export]
168macro_rules! run_with_tempdir {
169    (
170        name: $test_name:ident,
171        code: $code:literal,
172        setup: |$temp_dir:ident| $setup:block,
173        expect_error
174    ) => {
175        $crate::run_with_tempdir! {
176            name: $test_name,
177            code: $code,
178            setup: |$temp_dir| $setup,
179            expect: |v: ::netidx::subscriber::Value| -> ::anyhow::Result<()> {
180                if matches!(v, ::netidx::subscriber::Value::Error(_)) {
181                    Ok(())
182                } else {
183                    panic!("expected Error value, got: {v:?}")
184                }
185            }
186        }
187    };
188    (
189        name: $test_name:ident,
190        code: $code:literal,
191        setup: |$temp_dir:ident| $setup:block,
192        verify: |$verify_dir:ident| $verify:block
193    ) => {
194        $crate::run_with_tempdir! {
195            name: $test_name,
196            code: $code,
197            setup: |$temp_dir| $setup,
198            expect: |v: ::netidx::subscriber::Value| -> ::anyhow::Result<()> {
199                if !matches!(v, ::netidx::subscriber::Value::Null) {
200                    panic!("expected Null (success), got: {v:?}");
201                }
202                Ok(())
203            },
204            verify: |$verify_dir| $verify
205        }
206    };
207    (
208        name: $test_name:ident,
209        code: $code:literal,
210        setup: |$temp_dir:ident| $setup:block,
211        expect: $expect_payload:expr
212        $(, verify: |$verify_dir:ident| $verify:block)?
213    ) => {
214        #[tokio::test(flavor = "current_thread")]
215        async fn $test_name() -> ::anyhow::Result<()> {
216            let (tx, mut rx) = ::tokio::sync::mpsc::channel::<
217                ::poolshark::global::GPooled<Vec<::graphix_rt::GXEvent>>
218            >(10);
219            let ctx = $crate::testing::init(tx, &crate::TEST_REGISTER).await?;
220            let $temp_dir = ::tempfile::tempdir()?;
221
222            let test_file = { $setup };
223
224            let code = format!(
225                $code,
226                $crate::testing::escape_path(test_file.display())
227            );
228            let compiled = ctx.rt.compile(::arcstr::ArcStr::from(code)).await?;
229            let eid = compiled.exprs[0].id;
230
231            let timeout = ::tokio::time::sleep(::std::time::Duration::from_secs(2));
232            ::tokio::pin!(timeout);
233
234            loop {
235                ::tokio::select! {
236                    _ = &mut timeout => panic!("timeout waiting for result"),
237                    Some(mut batch) = rx.recv() => {
238                        for event in batch.drain(..) {
239                            if let ::graphix_rt::GXEvent::Updated(id, v) = event {
240                                if id == eid {
241                                    $expect_payload(v)?;
242                                    $(
243                                        let $verify_dir = &$temp_dir;
244                                        $verify
245                                    )?
246                                    return Ok(());
247                                }
248                            }
249                        }
250                    }
251                }
252            }
253        }
254    };
255}