use std::fs;
use std::{collections::HashMap, path::PathBuf};
use tempfile::TempDir;
pub use webui_protocol;
#[macro_export]
macro_rules! test_json {
($($json:tt)+) => {{
#[allow(clippy::disallowed_methods)]
let value = serde_json::json!($($json)+);
value
}};
}
#[macro_export]
macro_rules! assert_fragments {
($fragments:expr, [ $($matcher:expr),* $(,)? ]) => {{
let matchers: Vec<$crate::FragmentMatcher> = vec![$($matcher),*];
$crate::assert_fragment_list(&$fragments, &matchers);
}};
}
#[macro_export]
macro_rules! assert_stream {
($records:expr, $stream_id:expr, [ $($matcher:expr),* $(,)? ]) => {{
let stream = $records.get($stream_id)
.unwrap_or_else(|| panic!("Missing stream: {}", $stream_id));
let matchers: Vec<$crate::FragmentMatcher> = vec![$($matcher),*];
$crate::assert_fragment_list(&stream.fragments, &matchers);
}};
}
#[derive(Debug)]
pub enum FragmentMatcher {
Raw(String),
Signal {
value: String,
raw: bool,
},
Attribute(AttrMatcher),
Component(String),
ForLoop {
item: String,
collection: String,
template: String,
},
IfCond {
template: String,
},
}
#[derive(Debug, Default)]
pub struct AttrMatcher {
pub name: String,
pub value: Option<String>,
pub template: Option<String>,
pub complex: bool,
pub attr_start: bool,
pub attr_skip: bool,
pub raw_value: bool,
pub bool_signal: Option<String>,
pub bool_predicate: Option<(String, i32, String)>,
pub bool_not: Option<String>,
}
pub fn raw(value: &str) -> FragmentMatcher {
FragmentMatcher::Raw(value.to_string())
}
pub fn signal(value: &str) -> FragmentMatcher {
FragmentMatcher::Signal {
value: value.to_string(),
raw: false,
}
}
pub fn signal_raw(value: &str) -> FragmentMatcher {
FragmentMatcher::Signal {
value: value.to_string(),
raw: true,
}
}
pub fn attr(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
..Default::default()
})
}
pub fn attr_template(name: &str, template: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
template: Some(template.to_string()),
..Default::default()
})
}
pub fn attr_complex(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
complex: true,
..Default::default()
})
}
pub fn attr_complex_start(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
complex: true,
attr_start: true,
..Default::default()
})
}
pub fn bool_attr(name: &str, signal: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
bool_signal: Some(signal.to_string()),
..Default::default()
})
}
pub fn bool_attr_start(name: &str, signal: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
bool_signal: Some(signal.to_string()),
attr_start: true,
..Default::default()
})
}
pub fn bool_attr_predicate(name: &str, left: &str, op: i32, right: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
bool_predicate: Some((left.to_string(), op, right.to_string())),
..Default::default()
})
}
pub fn bool_attr_not(name: &str, inner: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
bool_not: Some(inner.to_string()),
..Default::default()
})
}
pub fn attr_raw(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
raw_value: true,
..Default::default()
})
}
pub fn attr_raw_start(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
raw_value: true,
attr_start: true,
..Default::default()
})
}
pub fn attr_skip(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
attr_skip: true,
..Default::default()
})
}
pub fn attr_start(name: &str, value: &str) -> FragmentMatcher {
FragmentMatcher::Attribute(AttrMatcher {
name: name.to_string(),
value: Some(value.to_string()),
attr_start: true,
..Default::default()
})
}
pub fn component(id: &str) -> FragmentMatcher {
FragmentMatcher::Component(id.to_string())
}
pub fn for_loop(item: &str, collection: &str, template: &str) -> FragmentMatcher {
FragmentMatcher::ForLoop {
item: item.to_string(),
collection: collection.to_string(),
template: template.to_string(),
}
}
pub fn if_cond(template: &str) -> FragmentMatcher {
FragmentMatcher::IfCond {
template: template.to_string(),
}
}
pub fn assert_fragment_list(
fragments: &[webui_protocol::WebUIFragment],
matchers: &[FragmentMatcher],
) {
use webui_protocol::web_ui_fragment::Fragment;
assert_eq!(
fragments.len(),
matchers.len(),
"Fragment count mismatch: got {} fragments, expected {}\nFragments: {:#?}",
fragments.len(),
matchers.len(),
fragments.iter().map(format_fragment).collect::<Vec<_>>()
);
for (i, (frag, matcher)) in fragments.iter().zip(matchers.iter()).enumerate() {
match (frag.fragment.as_ref(), matcher) {
(Some(Fragment::Raw(r)), FragmentMatcher::Raw(expected)) => {
assert_eq!(r.value, *expected, "Fragment[{}]: raw value mismatch", i);
}
(Some(Fragment::Signal(s)), FragmentMatcher::Signal { value, raw }) => {
assert_eq!(s.value, *value, "Fragment[{}]: signal value mismatch", i);
assert_eq!(s.raw, *raw, "Fragment[{}]: signal raw flag mismatch", i);
}
(Some(Fragment::Attribute(a)), FragmentMatcher::Attribute(m)) => {
assert_eq!(a.name, m.name, "Fragment[{}]: attr name mismatch", i);
if let Some(ref v) = m.value {
assert_eq!(a.value, *v, "Fragment[{}]: attr value mismatch", i);
}
if let Some(ref t) = m.template {
assert_eq!(a.template, *t, "Fragment[{}]: attr template mismatch", i);
}
assert_eq!(
a.complex, m.complex,
"Fragment[{}]: attr complex mismatch",
i
);
assert_eq!(
a.attr_start, m.attr_start,
"Fragment[{}]: attr_start mismatch",
i
);
assert_eq!(
a.attr_skip, m.attr_skip,
"Fragment[{}]: attr_skip mismatch",
i
);
assert_eq!(
a.raw_value, m.raw_value,
"Fragment[{}]: raw_value mismatch",
i
);
if let Some(ref sig) = m.bool_signal {
let cond = a
.condition_tree
.as_ref()
.unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
match cond.expr.as_ref() {
Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
assert_eq!(
id.value, *sig,
"Fragment[{}]: bool attr signal mismatch",
i
);
}
other => panic!(
"Fragment[{}]: expected identifier condition, got {:?}",
i, other
),
}
}
if let Some((ref left, op, ref right)) = m.bool_predicate {
let cond = a
.condition_tree
.as_ref()
.unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
match cond.expr.as_ref() {
Some(webui_protocol::condition_expr::Expr::Predicate(pred)) => {
assert_eq!(
pred.left, *left,
"Fragment[{}]: predicate left mismatch",
i
);
assert_eq!(
pred.operator, op,
"Fragment[{}]: predicate operator mismatch",
i
);
assert_eq!(
pred.right, *right,
"Fragment[{}]: predicate right mismatch",
i
);
}
other => panic!(
"Fragment[{}]: expected predicate condition, got {:?}",
i, other
),
}
}
if let Some(ref inner) = m.bool_not {
let cond = a
.condition_tree
.as_ref()
.unwrap_or_else(|| panic!("Fragment[{}]: expected condition_tree", i));
match cond.expr.as_ref() {
Some(webui_protocol::condition_expr::Expr::Not(not_cond)) => {
let inner_cond = not_cond.condition.as_ref().unwrap_or_else(|| {
panic!("Fragment[{}]: not condition missing inner", i)
});
match inner_cond.expr.as_ref() {
Some(webui_protocol::condition_expr::Expr::Identifier(id)) => {
assert_eq!(
id.value, *inner,
"Fragment[{}]: not inner identifier mismatch",
i
);
}
other => panic!(
"Fragment[{}]: expected identifier inside not, got {:?}",
i, other
),
}
}
other => panic!("Fragment[{}]: expected not condition, got {:?}", i, other),
}
}
}
(Some(Fragment::Component(c)), FragmentMatcher::Component(id)) => {
assert_eq!(c.fragment_id, *id, "Fragment[{}]: component id mismatch", i);
}
(
Some(Fragment::ForLoop(fl)),
FragmentMatcher::ForLoop {
item,
collection,
template,
},
) => {
assert_eq!(fl.item, *item, "Fragment[{}]: for item mismatch", i);
assert_eq!(
fl.collection, *collection,
"Fragment[{}]: for collection mismatch",
i
);
assert_eq!(
fl.fragment_id, *template,
"Fragment[{}]: for template mismatch",
i
);
}
(Some(Fragment::IfCond(ic)), FragmentMatcher::IfCond { template }) => {
assert_eq!(
ic.fragment_id, *template,
"Fragment[{}]: if template mismatch",
i
);
}
(_actual, expected) => {
panic!(
"Fragment[{}]: type mismatch\n expected: {:?}\n actual: {}",
i,
expected,
format_fragment(frag)
);
}
}
}
}
fn format_fragment(frag: &webui_protocol::WebUIFragment) -> String {
use webui_protocol::web_ui_fragment::Fragment;
match frag.fragment.as_ref() {
Some(Fragment::Raw(r)) => format!("raw({:?})", r.value),
Some(Fragment::Signal(s)) => format!("signal({:?}, raw={})", s.value, s.raw),
Some(Fragment::Attribute(a)) => format!(
"attr({:?}, value={:?}, template={:?}, complex={}, start={}, skip={}, raw_value={})",
a.name, a.value, a.template, a.complex, a.attr_start, a.attr_skip, a.raw_value
),
Some(Fragment::Component(c)) => format!("component({:?})", c.fragment_id),
Some(Fragment::ForLoop(f)) => format!(
"for({:?} in {:?}, template={:?})",
f.item, f.collection, f.fragment_id
),
Some(Fragment::IfCond(i)) => format!("if(template={:?})", i.fragment_id),
Some(Fragment::Plugin(p)) => format!("plugin(data={:?})", p.data),
Some(Fragment::Route(r)) => {
format!("route(path={:?}, fragment={:?})", r.path, r.fragment_id)
}
Some(Fragment::Outlet(_)) => "outlet".to_string(),
None => "None".to_string(),
}
}
pub struct TestFileSystem {
files: HashMap<String, PathBuf>,
_temp_dirs: Vec<TempDir>,
}
impl Default for TestFileSystem {
fn default() -> Self {
Self::new()
}
}
impl TestFileSystem {
pub fn new() -> Self {
Self {
files: HashMap::new(),
_temp_dirs: Vec::new(),
}
}
pub fn add_file(&mut self, path: &str, content: &str) -> PathBuf {
let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
let path_parts: Vec<&str> = path.split('/').collect();
let filename = path_parts.last().expect("Path must contain a filename");
let file_path = temp_dir.path().join(filename);
fs::write(&file_path, content).expect("Failed to write content to file");
self.files.insert(path.to_string(), file_path.clone());
self._temp_dirs.push(temp_dir);
self.files
.get(path)
.expect("File path not found in the test file system");
file_path
}
}