graphix_package_core/
testing.rs1use 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
70pub 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}