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 let (tx, mut rx) = tokio::sync::mpsc::channel(10_000);
411 let mut engine = Engine::new(tx);
412 if let Err(e) = engine.load(&program) {
413 return VplTestResult {
414 name,
415 passed: false,
416 failure: Some(format!("Engine load error: {e}")),
417 output_events: Vec::new(),
418 };
419 }
420
421 let timed_events = match EventFileParser::parse(&fixture.input) {
423 Ok(events) => events,
424 Err(e) => {
425 return VplTestResult {
426 name,
427 passed: false,
428 failure: Some(format!("Input event parse error: {e}")),
429 output_events: Vec::new(),
430 };
431 }
432 };
433
434 let rt = tokio::runtime::Runtime::new().unwrap();
435 let process_result = rt.block_on(async {
436 for timed in &timed_events {
437 engine.process(timed.event.clone()).await?;
438 }
439 Ok::<(), crate::engine::error::EngineError>(())
440 });
441
442 if let Err(e) = process_result {
443 return VplTestResult {
444 name,
445 passed: false,
446 failure: Some(format!("Event processing error: {e}")),
447 output_events: Vec::new(),
448 };
449 }
450
451 let mut output_events = Vec::new();
453 while let Ok(event) = rx.try_recv() {
454 output_events.push(event_to_output(&event));
455 }
456
457 let mut failures = Vec::new();
459
460 if fixture.expect_none && !output_events.is_empty() {
462 failures.push(format!(
463 "expected no output events, got {}:\n{}",
464 output_events.len(),
465 format_events(&output_events)
466 ));
467 }
468
469 if let Some(expected_count) = fixture.expect_count {
471 if output_events.len() != expected_count {
472 failures.push(format!(
473 "expected {expected_count} output events, got {}",
474 output_events.len()
475 ));
476 }
477 }
478
479 if let Some(ref expect_source) = fixture.expect {
481 match parse_expected_events(expect_source) {
482 Ok(expected) => {
483 if output_events.len() != expected.len() {
484 failures.push(format!(
485 "expected {} output events, got {}:\n expected:\n{}\n actual:\n{}",
486 expected.len(),
487 output_events.len(),
488 format_events_indented(&expected),
489 format_events_indented(&output_events)
490 ));
491 } else {
492 for (i, (actual, expected)) in
493 output_events.iter().zip(expected.iter()).enumerate()
494 {
495 if let Some(mismatch) = compare_events(actual, expected) {
496 failures.push(format!("event[{i}]: {mismatch}"));
497 }
498 }
499 }
500 }
501 Err(e) => {
502 failures.push(format!("EXPECT parse error: {e}"));
503 }
504 }
505 }
506
507 if let Some(ref expect_fields_source) = fixture.expect_fields {
509 match parse_field_assertions(expect_fields_source) {
510 Ok(assertions) => {
511 for assertion in &assertions {
512 if let Some(failure) = check_field_assertion(assertion, &output_events) {
513 failures.push(failure);
514 }
515 }
516 }
517 Err(e) => {
518 failures.push(format!("EXPECT_FIELDS parse error: {e}"));
519 }
520 }
521 }
522
523 let passed = failures.is_empty();
524 let failure = if failures.is_empty() {
525 None
526 } else {
527 Some(failures.join("\n"))
528 };
529
530 VplTestResult {
531 name,
532 passed,
533 failure,
534 output_events,
535 }
536}
537
538fn run_error_fixture(fixture: &VplTestFixture, expected_error: &str) -> VplTestResult {
540 let name = fixture.name.clone();
541
542 let expected_lower = expected_error.to_lowercase();
543
544 let program = match parse(&fixture.vpl) {
546 Ok(p) => p,
547 Err(e) => {
548 let error_msg = format!("{e}");
549 if error_msg.to_lowercase().contains(&expected_lower) {
550 return VplTestResult {
551 name,
552 passed: true,
553 failure: None,
554 output_events: Vec::new(),
555 };
556 }
557 return VplTestResult {
558 name,
559 passed: false,
560 failure: Some(format!(
561 "expected error containing \"{expected_error}\", got: {error_msg}"
562 )),
563 output_events: Vec::new(),
564 };
565 }
566 };
567
568 let (tx, _rx) = tokio::sync::mpsc::channel(10_000);
570 let mut engine = Engine::new(tx);
571 match engine.load(&program) {
572 Ok(()) => VplTestResult {
573 name,
574 passed: false,
575 failure: Some(format!(
576 "expected error containing \"{expected_error}\", but program compiled successfully"
577 )),
578 output_events: Vec::new(),
579 },
580 Err(e) => {
581 let error_msg = format!("{e}");
582 if error_msg.to_lowercase().contains(&expected_lower) {
583 VplTestResult {
584 name,
585 passed: true,
586 failure: None,
587 output_events: Vec::new(),
588 }
589 } else {
590 VplTestResult {
591 name,
592 passed: false,
593 failure: Some(format!(
594 "expected error containing \"{expected_error}\", got: {error_msg}"
595 )),
596 output_events: Vec::new(),
597 }
598 }
599 }
600 }
601}
602
603fn compare_events(actual: &OutputEvent, expected: &OutputEvent) -> Option<String> {
605 if actual.event_type != expected.event_type {
606 return Some(format!(
607 "event type mismatch: expected \"{}\", got \"{}\"",
608 expected.event_type, actual.event_type
609 ));
610 }
611
612 for (key, expected_value) in &expected.fields {
614 match actual.fields.get(key) {
615 Some(actual_value) => {
616 if actual_value != expected_value {
617 return Some(format!(
618 "field \"{key}\": expected {expected_value}, got {actual_value}"
619 ));
620 }
621 }
622 None => {
623 return Some(format!(
624 "missing expected field \"{key}\" (expected {expected_value})"
625 ));
626 }
627 }
628 }
629
630 None
631}
632
633#[derive(Debug)]
635struct FieldAssertion {
636 event_index: EventSelector,
638 field: String,
640 value: TestValue,
642}
643
644#[derive(Debug)]
645enum EventSelector {
646 Index(usize),
648 All,
650 Any,
652}
653
654fn parse_field_assertions(source: &str) -> Result<Vec<FieldAssertion>, String> {
663 let mut assertions = Vec::new();
664
665 for (line_num, line) in source.lines().enumerate() {
666 let trimmed = line.trim();
667 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
668 continue;
669 }
670
671 if !trimmed.starts_with('[') {
673 return Err(format!(
674 "line {}: expected '[index].field = value', got: {trimmed}",
675 line_num + 1
676 ));
677 }
678
679 let bracket_end = trimmed
680 .find(']')
681 .ok_or_else(|| format!("line {}: missing ']'", line_num + 1))?;
682
683 let selector_str = &trimmed[1..bracket_end];
684 let event_index = match selector_str {
685 "all" => EventSelector::All,
686 "any" => EventSelector::Any,
687 s => EventSelector::Index(
688 s.parse::<usize>()
689 .map_err(|_| format!("line {}: invalid index: {s}", line_num + 1))?,
690 ),
691 };
692
693 let rest = trimmed[bracket_end + 1..].trim();
694 let rest = rest
695 .strip_prefix('.')
696 .ok_or_else(|| format!("line {}: expected '.' after ']'", line_num + 1))?;
697
698 let eq_pos = rest
699 .find('=')
700 .ok_or_else(|| format!("line {}: expected '=' in assertion", line_num + 1))?;
701
702 let field = rest[..eq_pos].trim().to_string();
703 let value_str = rest[eq_pos + 1..].trim();
704 let value = parse_test_value(value_str)?;
705
706 assertions.push(FieldAssertion {
707 event_index,
708 field,
709 value,
710 });
711 }
712
713 Ok(assertions)
714}
715
716fn check_field_assertion(assertion: &FieldAssertion, events: &[OutputEvent]) -> Option<String> {
718 match &assertion.event_index {
719 EventSelector::Index(i) => {
720 let event = events.get(*i)?;
721 match event.fields.get(&assertion.field) {
722 Some(actual) if actual == &assertion.value => None,
723 Some(actual) => Some(format!(
724 "[{i}].{}: expected {}, got {actual}",
725 assertion.field, assertion.value
726 )),
727 None => Some(format!("[{i}].{}: field not found", assertion.field)),
728 }
729 }
730 EventSelector::All => {
731 for (i, event) in events.iter().enumerate() {
732 match event.fields.get(&assertion.field) {
733 Some(actual) if actual == &assertion.value => {}
734 Some(actual) => {
735 return Some(format!(
736 "[all].{}: event[{i}] expected {}, got {actual}",
737 assertion.field, assertion.value
738 ));
739 }
740 None => {
741 return Some(format!(
742 "[all].{}: event[{i}] field not found",
743 assertion.field
744 ));
745 }
746 }
747 }
748 None
749 }
750 EventSelector::Any => {
751 for event in events {
752 if let Some(actual) = event.fields.get(&assertion.field) {
753 if actual == &assertion.value {
754 return None;
755 }
756 }
757 }
758 Some(format!(
759 "[any].{}: no event has value {}",
760 assertion.field, assertion.value
761 ))
762 }
763 }
764}
765
766pub fn discover_fixtures(dir: &Path) -> Vec<PathBuf> {
768 let mut fixtures = Vec::new();
769 if let Ok(entries) = std::fs::read_dir(dir) {
770 for entry in entries.flatten() {
771 let path = entry.path();
772 if path.is_dir() {
773 fixtures.extend(discover_fixtures(&path));
774 } else if path
775 .file_name()
776 .is_some_and(|n| n.to_string_lossy().ends_with(".vpl.test"))
777 {
778 fixtures.push(path);
779 }
780 }
781 }
782 fixtures.sort();
783 fixtures
784}
785
786pub fn run_all(dir: &Path) -> Vec<VplTestResult> {
788 let fixtures = discover_fixtures(dir);
789 let mut results = Vec::new();
790
791 for path in fixtures {
792 match parse_fixture(&path) {
793 Ok(fixture) => results.push(run_fixture(&fixture)),
794 Err(e) => results.push(VplTestResult {
795 name: path.display().to_string(),
796 passed: false,
797 failure: Some(e),
798 output_events: Vec::new(),
799 }),
800 }
801 }
802
803 results
804}
805
806fn format_events(events: &[OutputEvent]) -> String {
807 events
808 .iter()
809 .map(|e| format!(" {e}"))
810 .collect::<Vec<_>>()
811 .join("\n")
812}
813
814fn format_events_indented(events: &[OutputEvent]) -> String {
815 events
816 .iter()
817 .map(|e| format!(" {e}"))
818 .collect::<Vec<_>>()
819 .join("\n")
820}
821
822#[cfg(test)]
823mod tests {
824 use std::path::PathBuf;
825
826 use super::*;
827
828 #[test]
829 fn test_parse_section_header() {
830 assert_eq!(parse_section_header("=== VPL ==="), Some("VPL".to_string()));
831 assert_eq!(
832 parse_section_header("=== INPUT ==="),
833 Some("INPUT".to_string())
834 );
835 assert_eq!(
836 parse_section_header("=== EXPECT ==="),
837 Some("EXPECT".to_string())
838 );
839 assert_eq!(
840 parse_section_header("=== EXPECT_COUNT ==="),
841 Some("EXPECT_COUNT".to_string())
842 );
843 assert_eq!(parse_section_header("not a section"), None);
844 assert_eq!(parse_section_header("=== ==="), None);
845 }
846
847 #[test]
848 fn test_parse_sections() {
849 let content = r"
850# A test comment
851=== VPL ===
852stream X = Y
853
854=== INPUT ===
855Y { a: 1 }
856
857=== EXPECT ===
858X { a: 1 }
859";
860 let sections = parse_sections(content).unwrap();
861 assert_eq!(sections.len(), 3);
862 assert!(sections.get("VPL").unwrap().contains("stream X = Y"));
863 assert!(sections.get("INPUT").unwrap().contains("Y { a: 1 }"));
864 assert!(sections.get("EXPECT").unwrap().contains("X { a: 1 }"));
865 }
866
867 #[test]
868 fn test_parse_expected_event_line() {
869 let event = parse_expected_event_line(r#"HighTemp { sensor: "S1", temp: 105 }"#).unwrap();
870 assert_eq!(event.event_type, "HighTemp");
871 assert_eq!(
872 event.fields.get("sensor"),
873 Some(&TestValue::Str("S1".to_string()))
874 );
875 assert_eq!(event.fields.get("temp"), Some(&TestValue::Int(105)));
876 }
877
878 #[test]
879 fn test_parse_expected_event_no_fields() {
880 let event = parse_expected_event_line("Alert {}").unwrap();
881 assert_eq!(event.event_type, "Alert");
882 assert!(event.fields.is_empty());
883 }
884
885 #[test]
886 fn test_parse_test_value() {
887 assert_eq!(parse_test_value("42").unwrap(), TestValue::Int(42));
888 assert_eq!(parse_test_value("1.23").unwrap(), TestValue::Float(1.23));
889 assert_eq!(
890 parse_test_value("\"hello\"").unwrap(),
891 TestValue::Str("hello".to_string())
892 );
893 assert_eq!(parse_test_value("true").unwrap(), TestValue::Bool(true));
894 assert_eq!(parse_test_value("null").unwrap(), TestValue::Null);
895 }
896
897 #[test]
898 fn test_parse_fixture_str() {
899 let content = r#"# Test: high temperature alert
900=== VPL ===
901stream HighTemp = SensorReading
902 .where(temperature > 100)
903 .emit(sensor: sensor_id, temp: temperature)
904
905=== INPUT ===
906SensorReading { sensor_id: "S1", temperature: 105 }
907SensorReading { sensor_id: "S2", temperature: 50 }
908
909=== EXPECT ===
910HighTemp { sensor: "S1", temp: 105 }
911"#;
912
913 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
914 assert_eq!(fixture.name, "high temperature alert");
915 assert!(fixture.vpl.contains("stream HighTemp"));
916 assert!(fixture.input.contains("SensorReading"));
917 assert!(fixture.expect.is_some());
918 }
919
920 #[test]
921 fn test_run_fixture_high_temp() {
922 let content = r#"# Test: high temperature filter
923=== VPL ===
924stream HighTemp = SensorReading
925 .where(temperature > 100)
926 .emit(sensor: sensor_id, temp: temperature)
927
928=== INPUT ===
929SensorReading { sensor_id: "S1", temperature: 105 }
930SensorReading { sensor_id: "S2", temperature: 50 }
931SensorReading { sensor_id: "S3", temperature: 200 }
932
933=== EXPECT ===
934HighTemp { sensor: "S1", temp: 105 }
935HighTemp { sensor: "S3", temp: 200 }
936"#;
937
938 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
939 let result = run_fixture(&fixture);
940 assert!(result.passed, "Test failed: {:?}", result.failure);
941 assert_eq!(result.output_events.len(), 2);
942 }
943
944 #[test]
945 fn test_run_fixture_expect_count() {
946 let content = r"
947=== VPL ===
948stream PassThrough = Event
949 .where(value > 0)
950 .emit(v: value)
951
952=== INPUT ===
953Event { value: 1 }
954Event { value: -1 }
955Event { value: 2 }
956
957=== EXPECT_COUNT ===
9582
959";
960
961 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
962 let result = run_fixture(&fixture);
963 assert!(result.passed, "Test failed: {:?}", result.failure);
964 }
965
966 #[test]
967 fn test_run_fixture_expect_none() {
968 let content = r"
969=== VPL ===
970stream HighTemp = SensorReading
971 .where(temperature > 1000)
972 .emit(temp: temperature)
973
974=== INPUT ===
975SensorReading { temperature: 50 }
976SensorReading { temperature: 100 }
977
978=== EXPECT_NONE ===
979";
980
981 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
982 let result = run_fixture(&fixture);
983 assert!(result.passed, "Test failed: {:?}", result.failure);
984 }
985
986 #[test]
987 fn test_run_fixture_expect_error() {
988 let content = r"
989=== VPL ===
990stream Bad = Event
991 .where(>>>{{{invalid syntax
992
993=== EXPECT_ERROR ===
994expected
995";
996
997 let fixture = parse_fixture_str(content, &PathBuf::from("test.vpl.test")).unwrap();
998 let result = run_fixture(&fixture);
999 assert!(result.passed, "Test failed: {:?}", result.failure);
1000 }
1001
1002 #[test]
1003 fn test_compare_events_match() {
1004 let actual = OutputEvent {
1005 event_type: "Alert".to_string(),
1006 fields: BTreeMap::from([
1007 ("temp".to_string(), TestValue::Int(105)),
1008 ("sensor".to_string(), TestValue::Str("S1".to_string())),
1009 ]),
1010 };
1011 let expected = OutputEvent {
1012 event_type: "Alert".to_string(),
1013 fields: BTreeMap::from([
1014 ("temp".to_string(), TestValue::Int(105)),
1015 ("sensor".to_string(), TestValue::Str("S1".to_string())),
1016 ]),
1017 };
1018 assert!(compare_events(&actual, &expected).is_none());
1019 }
1020
1021 #[test]
1022 fn test_compare_events_partial_match() {
1023 let actual = OutputEvent {
1025 event_type: "Alert".to_string(),
1026 fields: BTreeMap::from([
1027 ("temp".to_string(), TestValue::Int(105)),
1028 ("sensor".to_string(), TestValue::Str("S1".to_string())),
1029 ("extra".to_string(), TestValue::Bool(true)),
1030 ]),
1031 };
1032 let expected = OutputEvent {
1033 event_type: "Alert".to_string(),
1034 fields: BTreeMap::from([("temp".to_string(), TestValue::Int(105))]),
1035 };
1036 assert!(compare_events(&actual, &expected).is_none());
1038 }
1039
1040 #[test]
1041 fn test_split_fields() {
1042 let fields = split_fields(r#"name: "hello, world", value: 42"#);
1043 assert_eq!(fields.len(), 2);
1044 assert_eq!(fields[0].trim(), r#"name: "hello, world""#);
1045 assert_eq!(fields[1].trim(), "value: 42");
1046 }
1047
1048 #[test]
1049 fn test_parse_field_assertions() {
1050 let source = r#"
1051[0].temp = 105
1052[all].event_type = "Alert"
1053[any].sensor = "S1"
1054"#;
1055 let assertions = parse_field_assertions(source).unwrap();
1056 assert_eq!(assertions.len(), 3);
1057 }
1058}