1use std::collections::BTreeMap;
39use std::path::{Path, PathBuf};
40
41use varpulis_core::Value;
42use varpulis_parser::parse;
43
44use crate::event_file::EventFileParser;
45use crate::Engine;
46
47#[derive(Debug)]
49pub struct VplTestFixture {
50 pub path: PathBuf,
52 pub name: String,
54 pub vpl: String,
56 pub input: String,
58 pub expect: Option<String>,
60 pub expect_count: Option<usize>,
62 pub expect_none: bool,
64 pub expect_error: Option<String>,
66 pub expect_fields: Option<String>,
68}
69
70#[derive(Debug)]
72pub struct VplTestResult {
73 pub name: String,
75 pub passed: bool,
77 pub failure: Option<String>,
79 pub output_events: Vec<OutputEvent>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub struct OutputEvent {
86 pub event_type: String,
88 pub fields: BTreeMap<String, TestValue>,
90}
91
92#[derive(Debug, Clone)]
94pub enum TestValue {
95 Int(i64),
96 Float(f64),
97 Str(String),
98 Bool(bool),
99 Null,
100}
101
102impl PartialEq for TestValue {
103 fn eq(&self, other: &Self) -> bool {
104 match (self, other) {
105 (Self::Int(a), Self::Int(b)) => a == b,
106 (Self::Float(a), Self::Float(b)) => (a - b).abs() < 1e-6,
107 (Self::Str(a), Self::Str(b)) => a == b,
108 (Self::Bool(a), Self::Bool(b)) => a == b,
109 (Self::Null, Self::Null) => true,
110 (Self::Int(a), Self::Float(b)) | (Self::Float(b), Self::Int(a)) => {
112 (*a as f64 - b).abs() < 1e-6
113 }
114 _ => false,
115 }
116 }
117}
118
119impl std::fmt::Display for TestValue {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 Self::Int(v) => write!(f, "{v}"),
123 Self::Float(v) => write!(f, "{v}"),
124 Self::Str(v) => write!(f, "\"{v}\""),
125 Self::Bool(v) => write!(f, "{v}"),
126 Self::Null => write!(f, "null"),
127 }
128 }
129}
130
131impl From<&Value> for TestValue {
132 fn from(v: &Value) -> Self {
133 match v {
134 Value::Int(i) => Self::Int(*i),
135 Value::Float(f) => Self::Float(*f),
136 Value::Str(s) => Self::Str(s.to_string()),
137 Value::Bool(b) => Self::Bool(*b),
138 Value::Null => Self::Null,
139 other => Self::Str(format!("{other}")),
140 }
141 }
142}
143
144impl std::fmt::Display for OutputEvent {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{} {{ ", self.event_type)?;
147 let fields: Vec<_> = self
148 .fields
149 .iter()
150 .map(|(k, v)| format!("{k}: {v}"))
151 .collect();
152 write!(f, "{}", fields.join(", "))?;
153 write!(f, " }}")
154 }
155}
156
157pub fn parse_fixture(path: &Path) -> Result<VplTestFixture, String> {
159 let content = std::fs::read_to_string(path)
160 .map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
161 parse_fixture_str(&content, path)
162}
163
164pub fn parse_fixture_str(content: &str, path: &Path) -> Result<VplTestFixture, String> {
166 let mut name = path
167 .file_stem()
168 .and_then(|s| s.to_str())
169 .unwrap_or("unknown")
170 .trim_end_matches(".vpl")
171 .to_string();
172
173 for line in content.lines() {
175 let trimmed = line.trim();
176 if trimmed.is_empty() {
177 continue;
178 }
179 if let Some(comment) = trimmed.strip_prefix('#') {
180 let comment = comment.trim();
181 if let Some(test_name) = comment.strip_prefix("Test:") {
182 name = test_name.trim().to_string();
183 }
184 break;
185 }
186 break;
187 }
188
189 let sections = parse_sections(content)?;
191
192 let vpl = sections
193 .get("VPL")
194 .ok_or_else(|| format!("{}: missing === VPL === section", path.display()))?
195 .clone();
196
197 let input = sections.get("INPUT").cloned().unwrap_or_default();
198
199 let expect = sections.get("EXPECT").cloned();
200 let expect_none = sections.contains_key("EXPECT_NONE");
201 let expect_error = sections.get("EXPECT_ERROR").map(|s| s.trim().to_string());
202 let expect_fields = sections.get("EXPECT_FIELDS").cloned();
203
204 let expect_count = sections.get("EXPECT_COUNT").map(|s| {
205 s.trim()
206 .parse::<usize>()
207 .map_err(|_| format!("{}: invalid EXPECT_COUNT value: {s}", path.display()))
208 });
209 let expect_count = match expect_count {
210 Some(Ok(n)) => Some(n),
211 Some(Err(e)) => return Err(e),
212 None => None,
213 };
214
215 Ok(VplTestFixture {
216 path: path.to_path_buf(),
217 name,
218 vpl,
219 input,
220 expect,
221 expect_count,
222 expect_none,
223 expect_error,
224 expect_fields,
225 })
226}
227
228fn parse_sections(content: &str) -> Result<BTreeMap<String, String>, String> {
230 let mut sections = BTreeMap::new();
231 let mut current_section: Option<String> = None;
232 let mut current_content = String::new();
233
234 for line in content.lines() {
235 let trimmed = line.trim();
236 if let Some(section_name) = parse_section_header(trimmed) {
237 if let Some(name) = current_section.take() {
239 sections.insert(name, current_content.trim().to_string());
240 }
241 current_section = Some(section_name);
242 current_content = String::new();
243 } else if current_section.is_some() {
244 current_content.push_str(line);
245 current_content.push('\n');
246 }
247 }
249
250 if let Some(name) = current_section {
252 sections.insert(name, current_content.trim().to_string());
253 }
254
255 Ok(sections)
256}
257
258fn parse_section_header(line: &str) -> Option<String> {
260 let line = line.trim();
261 if line.starts_with("===") && line.ends_with("===") {
262 let inner = line.trim_start_matches('=').trim_end_matches('=').trim();
263 if !inner.is_empty() {
264 return Some(inner.to_string());
265 }
266 }
267 None
268}
269
270fn parse_expected_events(expect_source: &str) -> Result<Vec<OutputEvent>, String> {
272 let mut events = Vec::new();
273
274 for (line_num, line) in expect_source.lines().enumerate() {
275 let trimmed = line.trim();
276 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
277 continue;
278 }
279
280 let event = parse_expected_event_line(trimmed)
282 .map_err(|e| format!("EXPECT line {}: {e}", line_num + 1))?;
283 events.push(event);
284 }
285
286 Ok(events)
287}
288
289fn parse_expected_event_line(line: &str) -> Result<OutputEvent, String> {
291 let brace_pos = line
292 .find('{')
293 .ok_or_else(|| format!("expected '{{' in event line: {line}"))?;
294
295 let event_type = line[..brace_pos].trim().to_string();
296 if event_type.is_empty() {
297 return Err(format!("empty event type in: {line}"));
298 }
299
300 let fields_str = line[brace_pos + 1..].trim().trim_end_matches('}').trim();
301
302 let mut fields = BTreeMap::new();
303 if !fields_str.is_empty() {
304 for field in split_fields(fields_str) {
306 let field = field.trim();
307 if field.is_empty() {
308 continue;
309 }
310 let colon_pos = field
311 .find(':')
312 .ok_or_else(|| format!("expected ':' in field: {field}"))?;
313
314 let key = field[..colon_pos].trim().to_string();
315 let value_str = field[colon_pos + 1..].trim();
316
317 let value = parse_test_value(value_str)?;
318 fields.insert(key, value);
319 }
320 }
321
322 Ok(OutputEvent { event_type, fields })
323}
324
325fn split_fields(s: &str) -> Vec<&str> {
327 let mut result = Vec::new();
328 let mut start = 0;
329 let mut in_quotes = false;
330
331 for (i, c) in s.char_indices() {
332 match c {
333 '"' => in_quotes = !in_quotes,
334 ',' if !in_quotes => {
335 result.push(&s[start..i]);
336 start = i + 1;
337 }
338 _ => {}
339 }
340 }
341 if start < s.len() {
342 result.push(&s[start..]);
343 }
344 result
345}
346
347fn parse_test_value(s: &str) -> Result<TestValue, String> {
349 let s = s.trim();
350 if s == "null" {
351 return Ok(TestValue::Null);
352 }
353 if s == "true" {
354 return Ok(TestValue::Bool(true));
355 }
356 if s == "false" {
357 return Ok(TestValue::Bool(false));
358 }
359 if s.starts_with('"') && s.ends_with('"') {
361 return Ok(TestValue::Str(s[1..s.len() - 1].to_string()));
362 }
363 if let Ok(i) = s.parse::<i64>() {
365 return Ok(TestValue::Int(i));
366 }
367 if let Ok(f) = s.parse::<f64>() {
369 return Ok(TestValue::Float(f));
370 }
371 Ok(TestValue::Str(s.to_string()))
373}
374
375fn event_to_output(event: &crate::Event) -> OutputEvent {
377 let mut fields = BTreeMap::new();
378 for (key, value) in &event.data {
379 fields.insert(key.to_string(), TestValue::from(value));
380 }
381 OutputEvent {
382 event_type: event.event_type.to_string(),
383 fields,
384 }
385}
386
387pub fn run_fixture(fixture: &VplTestFixture) -> VplTestResult {
389 let name = fixture.name.clone();
390
391 if let Some(ref expected_error) = fixture.expect_error {
393 return run_error_fixture(fixture, expected_error);
394 }
395
396 let program = match parse(&fixture.vpl) {
398 Ok(p) => p,
399 Err(e) => {
400 return VplTestResult {
401 name,
402 passed: false,
403 failure: Some(format!("VPL parse error: {e}")),
404 output_events: Vec::new(),
405 };
406 }
407 };
408
409 #[cfg(feature = "async-runtime")]
411 #[allow(unused_variables)]
412 let (engine, output_events) = {
413 let (tx, mut rx) = tokio::sync::mpsc::channel(10_000);
414 let mut engine = Engine::new(tx);
415 if let Err(e) = engine.load(&program) {
416 return VplTestResult {
417 name,
418 passed: false,
419 failure: Some(format!("Engine load error: {e}")),
420 output_events: Vec::new(),
421 };
422 }
423
424 let timed_events = match EventFileParser::parse(&fixture.input) {
426 Ok(events) => events,
427 Err(e) => {
428 return VplTestResult {
429 name,
430 passed: false,
431 failure: Some(format!("Input event parse error: {e}")),
432 output_events: Vec::new(),
433 };
434 }
435 };
436
437 let rt = tokio::runtime::Runtime::new().unwrap();
438 let process_result = rt.block_on(async {
439 for timed in &timed_events {
440 engine.process(timed.event.clone()).await?;
441 }
442 Ok::<(), crate::engine::error::EngineError>(())
443 });
444
445 if let Err(e) = process_result {
446 return VplTestResult {
447 name,
448 passed: false,
449 failure: Some(format!("Event processing error: {e}")),
450 output_events: Vec::new(),
451 };
452 }
453
454 let mut output_events = Vec::new();
456 while let Ok(event) = rx.try_recv() {
457 output_events.push(event_to_output(&event));
458 }
459 (engine, output_events)
460 };
461
462 #[cfg(not(feature = "async-runtime"))]
463 #[allow(unused_variables, unused_mut)]
464 let (mut engine, output_events) = {
465 let mut engine = Engine::new_benchmark();
466 if let Err(e) = engine.load(&program) {
467 return VplTestResult {
468 name,
469 passed: false,
470 failure: Some(format!("Engine load error: {e}")),
471 output_events: Vec::new(),
472 };
473 }
474
475 let timed_events = match EventFileParser::parse(&fixture.input) {
476 Ok(events) => events,
477 Err(e) => {
478 return VplTestResult {
479 name,
480 passed: false,
481 failure: Some(format!("Input event parse error: {e}")),
482 output_events: Vec::new(),
483 };
484 }
485 };
486
487 let events: Vec<crate::event::Event> = timed_events.into_iter().map(|t| t.event).collect();
488 let collected = match engine.process_batch_sync_collect(events) {
489 Ok(c) => c,
490 Err(e) => {
491 return VplTestResult {
492 name,
493 passed: false,
494 failure: Some(format!("Event processing error: {e}")),
495 output_events: Vec::new(),
496 };
497 }
498 };
499
500 let output_events: Vec<_> = collected.iter().map(event_to_output).collect();
501 (engine, output_events)
502 };
503
504 let mut failures = Vec::new();
506
507 if fixture.expect_none && !output_events.is_empty() {
509 failures.push(format!(
510 "expected no output events, got {}:\n{}",
511 output_events.len(),
512 format_events(&output_events)
513 ));
514 }
515
516 if let Some(expected_count) = fixture.expect_count {
518 if output_events.len() != expected_count {
519 failures.push(format!(
520 "expected {expected_count} output events, got {}",
521 output_events.len()
522 ));
523 }
524 }
525
526 if let Some(ref expect_source) = fixture.expect {
528 match parse_expected_events(expect_source) {
529 Ok(expected) => {
530 if output_events.len() != expected.len() {
531 failures.push(format!(
532 "expected {} output events, got {}:\n expected:\n{}\n actual:\n{}",
533 expected.len(),
534 output_events.len(),
535 format_events_indented(&expected),
536 format_events_indented(&output_events)
537 ));
538 } else {
539 for (i, (actual, expected)) in
540 output_events.iter().zip(expected.iter()).enumerate()
541 {
542 if let Some(mismatch) = compare_events(actual, expected) {
543 failures.push(format!("event[{i}]: {mismatch}"));
544 }
545 }
546 }
547 }
548 Err(e) => {
549 failures.push(format!("EXPECT parse error: {e}"));
550 }
551 }
552 }
553
554 if let Some(ref expect_fields_source) = fixture.expect_fields {
556 match parse_field_assertions(expect_fields_source) {
557 Ok(assertions) => {
558 for assertion in &assertions {
559 if let Some(failure) = check_field_assertion(assertion, &output_events) {
560 failures.push(failure);
561 }
562 }
563 }
564 Err(e) => {
565 failures.push(format!("EXPECT_FIELDS parse error: {e}"));
566 }
567 }
568 }
569
570 let passed = failures.is_empty();
571 let failure = if failures.is_empty() {
572 None
573 } else {
574 Some(failures.join("\n"))
575 };
576
577 VplTestResult {
578 name,
579 passed,
580 failure,
581 output_events,
582 }
583}
584
585fn run_error_fixture(fixture: &VplTestFixture, expected_error: &str) -> VplTestResult {
587 let name = fixture.name.clone();
588
589 let expected_lower = expected_error.to_lowercase();
590
591 let program = match parse(&fixture.vpl) {
593 Ok(p) => p,
594 Err(e) => {
595 let error_msg = format!("{e}");
596 if error_msg.to_lowercase().contains(&expected_lower) {
597 return VplTestResult {
598 name,
599 passed: true,
600 failure: None,
601 output_events: Vec::new(),
602 };
603 }
604 return VplTestResult {
605 name,
606 passed: false,
607 failure: Some(format!(
608 "expected error containing \"{expected_error}\", got: {error_msg}"
609 )),
610 output_events: Vec::new(),
611 };
612 }
613 };
614
615 #[cfg(feature = "async-runtime")]
617 let mut engine = {
618 let (tx, _rx) = tokio::sync::mpsc::channel(10_000);
619 Engine::new(tx)
620 };
621 #[cfg(not(feature = "async-runtime"))]
622 let mut engine = Engine::new_benchmark();
623 match engine.load(&program) {
624 Ok(()) => VplTestResult {
625 name,
626 passed: false,
627 failure: Some(format!(
628 "expected error containing \"{expected_error}\", but program compiled successfully"
629 )),
630 output_events: Vec::new(),
631 },
632 Err(e) => {
633 let error_msg = format!("{e}");
634 if error_msg.to_lowercase().contains(&expected_lower) {
635 VplTestResult {
636 name,
637 passed: true,
638 failure: None,
639 output_events: Vec::new(),
640 }
641 } else {
642 VplTestResult {
643 name,
644 passed: false,
645 failure: Some(format!(
646 "expected error containing \"{expected_error}\", got: {error_msg}"
647 )),
648 output_events: Vec::new(),
649 }
650 }
651 }
652 }
653}
654
655fn compare_events(actual: &OutputEvent, expected: &OutputEvent) -> Option<String> {
657 if actual.event_type != expected.event_type {
658 return Some(format!(
659 "event type mismatch: expected \"{}\", got \"{}\"",
660 expected.event_type, actual.event_type
661 ));
662 }
663
664 for (key, expected_value) in &expected.fields {
666 match actual.fields.get(key) {
667 Some(actual_value) => {
668 if actual_value != expected_value {
669 return Some(format!(
670 "field \"{key}\": expected {expected_value}, got {actual_value}"
671 ));
672 }
673 }
674 None => {
675 return Some(format!(
676 "missing expected field \"{key}\" (expected {expected_value})"
677 ));
678 }
679 }
680 }
681
682 None
683}
684
685#[derive(Debug)]
687struct FieldAssertion {
688 event_index: EventSelector,
690 field: String,
692 value: TestValue,
694}
695
696#[derive(Debug)]
697enum EventSelector {
698 Index(usize),
700 All,
702 Any,
704}
705
706fn parse_field_assertions(source: &str) -> Result<Vec<FieldAssertion>, String> {
715 let mut assertions = Vec::new();
716
717 for (line_num, line) in source.lines().enumerate() {
718 let trimmed = line.trim();
719 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
720 continue;
721 }
722
723 if !trimmed.starts_with('[') {
725 return Err(format!(
726 "line {}: expected '[index].field = value', got: {trimmed}",
727 line_num + 1
728 ));
729 }
730
731 let bracket_end = trimmed
732 .find(']')
733 .ok_or_else(|| format!("line {}: missing ']'", line_num + 1))?;
734
735 let selector_str = &trimmed[1..bracket_end];
736 let event_index = match selector_str {
737 "all" => EventSelector::All,
738 "any" => EventSelector::Any,
739 s => EventSelector::Index(
740 s.parse::<usize>()
741 .map_err(|_| format!("line {}: invalid index: {s}", line_num + 1))?,
742 ),
743 };
744
745 let rest = trimmed[bracket_end + 1..].trim();
746 let rest = rest
747 .strip_prefix('.')
748 .ok_or_else(|| format!("line {}: expected '.' after ']'", line_num + 1))?;
749
750 let eq_pos = rest
751 .find('=')
752 .ok_or_else(|| format!("line {}: expected '=' in assertion", line_num + 1))?;
753
754 let field = rest[..eq_pos].trim().to_string();
755 let value_str = rest[eq_pos + 1..].trim();
756 let value = parse_test_value(value_str)?;
757
758 assertions.push(FieldAssertion {
759 event_index,
760 field,
761 value,
762 });
763 }
764
765 Ok(assertions)
766}
767
768fn check_field_assertion(assertion: &FieldAssertion, events: &[OutputEvent]) -> Option<String> {
770 match &assertion.event_index {
771 EventSelector::Index(i) => {
772 let event = events.get(*i)?;
773 match event.fields.get(&assertion.field) {
774 Some(actual) if actual == &assertion.value => None,
775 Some(actual) => Some(format!(
776 "[{i}].{}: expected {}, got {actual}",
777 assertion.field, assertion.value
778 )),
779 None => Some(format!("[{i}].{}: field not found", assertion.field)),
780 }
781 }
782 EventSelector::All => {
783 for (i, event) in events.iter().enumerate() {
784 match event.fields.get(&assertion.field) {
785 Some(actual) if actual == &assertion.value => {}
786 Some(actual) => {
787 return Some(format!(
788 "[all].{}: event[{i}] expected {}, got {actual}",
789 assertion.field, assertion.value
790 ));
791 }
792 None => {
793 return Some(format!(
794 "[all].{}: event[{i}] field not found",
795 assertion.field
796 ));
797 }
798 }
799 }
800 None
801 }
802 EventSelector::Any => {
803 for event in events {
804 if let Some(actual) = event.fields.get(&assertion.field) {
805 if actual == &assertion.value {
806 return None;
807 }
808 }
809 }
810 Some(format!(
811 "[any].{}: no event has value {}",
812 assertion.field, assertion.value
813 ))
814 }
815 }
816}
817
818pub fn discover_fixtures(dir: &Path) -> Vec<PathBuf> {
820 let mut fixtures = Vec::new();
821 if let Ok(entries) = std::fs::read_dir(dir) {
822 for entry in entries.flatten() {
823 let path = entry.path();
824 if path.is_dir() {
825 fixtures.extend(discover_fixtures(&path));
826 } else if path
827 .file_name()
828 .is_some_and(|n| n.to_string_lossy().ends_with(".vpl.test"))
829 {
830 fixtures.push(path);
831 }
832 }
833 }
834 fixtures.sort();
835 fixtures
836}
837
838pub fn run_all(dir: &Path) -> Vec<VplTestResult> {
840 let fixtures = discover_fixtures(dir);
841 let mut results = Vec::new();
842
843 for path in fixtures {
844 match parse_fixture(&path) {
845 Ok(fixture) => results.push(run_fixture(&fixture)),
846 Err(e) => results.push(VplTestResult {
847 name: path.display().to_string(),
848 passed: false,
849 failure: Some(e),
850 output_events: Vec::new(),
851 }),
852 }
853 }
854
855 results
856}
857
858fn format_events(events: &[OutputEvent]) -> String {
859 events
860 .iter()
861 .map(|e| format!(" {e}"))
862 .collect::<Vec<_>>()
863 .join("\n")
864}
865
866fn format_events_indented(events: &[OutputEvent]) -> String {
867 events
868 .iter()
869 .map(|e| format!(" {e}"))
870 .collect::<Vec<_>>()
871 .join("\n")
872}
873
874#[cfg(test)]
875mod tests {
876 use std::path::PathBuf;
877
878 use super::*;
879
880 #[test]
881 fn test_parse_section_header() {
882 assert_eq!(parse_section_header("=== VPL ==="), Some("VPL".to_string()));
883 assert_eq!(
884 parse_section_header("=== INPUT ==="),
885 Some("INPUT".to_string())
886 );
887 assert_eq!(
888 parse_section_header("=== EXPECT ==="),
889 Some("EXPECT".to_string())
890 );
891 assert_eq!(
892 parse_section_header("=== EXPECT_COUNT ==="),
893 Some("EXPECT_COUNT".to_string())
894 );
895 assert_eq!(parse_section_header("not a section"), None);
896 assert_eq!(parse_section_header("=== ==="), None);
897 }
898
899 #[test]
900 fn test_parse_sections() {
901 let content = r"
902# A test comment
903=== VPL ===
904stream X = Y
905
906=== INPUT ===
907Y { a: 1 }
908
909=== EXPECT ===
910X { a: 1 }
911";
912 let sections = parse_sections(content).unwrap();
913 assert_eq!(sections.len(), 3);
914 assert!(sections.get("VPL").unwrap().contains("stream X = Y"));
915 assert!(sections.get("INPUT").unwrap().contains("Y { a: 1 }"));
916 assert!(sections.get("EXPECT").unwrap().contains("X { a: 1 }"));
917 }
918
919 #[test]
920 fn test_parse_expected_event_line() {
921 let event = parse_expected_event_line(r#"HighTemp { sensor: "S1", temp: 105 }"#).unwrap();
922 assert_eq!(event.event_type, "HighTemp");
923 assert_eq!(
924 event.fields.get("sensor"),
925 Some(&TestValue::Str("S1".to_string()))
926 );
927 assert_eq!(event.fields.get("temp"), Some(&TestValue::Int(105)));
928 }
929
930 #[test]
931 fn test_parse_expected_event_no_fields() {
932 let event = parse_expected_event_line("Alert {}").unwrap();
933 assert_eq!(event.event_type, "Alert");
934 assert!(event.fields.is_empty());
935 }
936
937 #[test]
938 fn test_parse_test_value() {
939 assert_eq!(parse_test_value("42").unwrap(), TestValue::Int(42));
940 assert_eq!(parse_test_value("1.23").unwrap(), TestValue::Float(1.23));
941 assert_eq!(
942 parse_test_value("\"hello\"").unwrap(),
943 TestValue::Str("hello".to_string())
944 );
945 assert_eq!(parse_test_value("true").unwrap(), TestValue::Bool(true));
946 assert_eq!(parse_test_value("null").unwrap(), TestValue::Null);
947 }
948
949 #[test]
950 fn test_parse_fixture_str() {
951 let content = r#"# Test: high temperature alert
952=== VPL ===
953stream HighTemp = SensorReading
954 .where(temperature > 100)
955 .emit(sensor: sensor_id, temp: temperature)
956
957=== INPUT ===
958SensorReading { sensor_id: "S1", temperature: 105 }
959SensorReading { sensor_id: "S2", temperature: 50 }
960
961=== EXPECT ===
962HighTemp { sensor: "S1", temp: 105 }
963"#;
964
965 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
966 assert_eq!(fixture.name, "high temperature alert");
967 assert!(fixture.vpl.contains("stream HighTemp"));
968 assert!(fixture.input.contains("SensorReading"));
969 assert!(fixture.expect.is_some());
970 }
971
972 #[test]
973 fn test_run_fixture_high_temp() {
974 let content = r#"# Test: high temperature filter
975=== VPL ===
976stream HighTemp = SensorReading
977 .where(temperature > 100)
978 .emit(sensor: sensor_id, temp: temperature)
979
980=== INPUT ===
981SensorReading { sensor_id: "S1", temperature: 105 }
982SensorReading { sensor_id: "S2", temperature: 50 }
983SensorReading { sensor_id: "S3", temperature: 200 }
984
985=== EXPECT ===
986HighTemp { sensor: "S1", temp: 105 }
987HighTemp { sensor: "S3", temp: 200 }
988"#;
989
990 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
991 let result = run_fixture(&fixture);
992 assert!(result.passed, "Test failed: {:?}", result.failure);
993 assert_eq!(result.output_events.len(), 2);
994 }
995
996 #[test]
997 fn test_run_fixture_expect_count() {
998 let content = r"
999=== VPL ===
1000stream PassThrough = Event
1001 .where(value > 0)
1002 .emit(v: value)
1003
1004=== INPUT ===
1005Event { value: 1 }
1006Event { value: -1 }
1007Event { value: 2 }
1008
1009=== EXPECT_COUNT ===
10102
1011";
1012
1013 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
1014 let result = run_fixture(&fixture);
1015 assert!(result.passed, "Test failed: {:?}", result.failure);
1016 }
1017
1018 #[test]
1019 fn test_run_fixture_expect_none() {
1020 let content = r"
1021=== VPL ===
1022stream HighTemp = SensorReading
1023 .where(temperature > 1000)
1024 .emit(temp: temperature)
1025
1026=== INPUT ===
1027SensorReading { temperature: 50 }
1028SensorReading { temperature: 100 }
1029
1030=== EXPECT_NONE ===
1031";
1032
1033 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
1034 let result = run_fixture(&fixture);
1035 assert!(result.passed, "Test failed: {:?}", result.failure);
1036 }
1037
1038 #[test]
1039 fn test_run_fixture_expect_error() {
1040 let content = r"
1041=== VPL ===
1042stream Bad = Event
1043 .where(>>>{{{invalid syntax
1044
1045=== EXPECT_ERROR ===
1046expected
1047";
1048
1049 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
1050 let result = run_fixture(&fixture);
1051 assert!(result.passed, "Test failed: {:?}", result.failure);
1052 }
1053
1054 #[test]
1055 fn test_compare_events_match() {
1056 let actual = OutputEvent {
1057 event_type: "Alert".to_string(),
1058 fields: BTreeMap::from([
1059 ("temp".to_string(), TestValue::Int(105)),
1060 ("sensor".to_string(), TestValue::Str("S1".to_string())),
1061 ]),
1062 };
1063 let expected = OutputEvent {
1064 event_type: "Alert".to_string(),
1065 fields: BTreeMap::from([
1066 ("temp".to_string(), TestValue::Int(105)),
1067 ("sensor".to_string(), TestValue::Str("S1".to_string())),
1068 ]),
1069 };
1070 assert!(compare_events(&actual, &expected).is_none());
1071 }
1072
1073 #[test]
1074 fn test_compare_events_partial_match() {
1075 let actual = OutputEvent {
1077 event_type: "Alert".to_string(),
1078 fields: BTreeMap::from([
1079 ("temp".to_string(), TestValue::Int(105)),
1080 ("sensor".to_string(), TestValue::Str("S1".to_string())),
1081 ("extra".to_string(), TestValue::Bool(true)),
1082 ]),
1083 };
1084 let expected = OutputEvent {
1085 event_type: "Alert".to_string(),
1086 fields: BTreeMap::from([("temp".to_string(), TestValue::Int(105))]),
1087 };
1088 assert!(compare_events(&actual, &expected).is_none());
1090 }
1091
1092 #[test]
1093 fn test_split_fields() {
1094 let fields = split_fields(r#"name: "hello, world", value: 42"#);
1095 assert_eq!(fields.len(), 2);
1096 assert_eq!(fields[0].trim(), r#"name: "hello, world""#);
1097 assert_eq!(fields[1].trim(), "value: 42");
1098 }
1099
1100 #[test]
1101 fn test_parse_field_assertions() {
1102 let source = r#"
1103[0].temp = 105
1104[all].event_type = "Alert"
1105[any].sensor = "S1"
1106"#;
1107 let assertions = parse_field_assertions(source).unwrap();
1108 assert_eq!(assertions.len(), 3);
1109 }
1110}