swamp_test_runner/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use source_map_cache::SourceMapWrapper;
6use std::fmt::{Display, Formatter};
7use std::io;
8use std::io::Write;
9use std::num::ParseIntError;
10use std::path::{Path, PathBuf};
11use std::str::FromStr;
12use std::thread::sleep;
13use std::time::Duration;
14use swamp_runtime::prelude::CodeGenOptions;
15use swamp_runtime::{
16    CompileAndCodeGenOptions, CompileAndVmResult, CompileOptions, RunOptions,
17    compile_codegen_and_create_vm,
18};
19use swamp_std::print::print_fn;
20use swamp_vm::VmState;
21use swamp_vm::host::HostFunctionCallback;
22use time_dilation::ScopedTimer;
23use tracing::error;
24
25#[derive(Debug, Default, Clone, Copy)]
26struct TestContext;
27
28#[must_use]
29pub fn colorize_parts(parts: &[String]) -> String {
30    let new_parts: Vec<_> = parts
31        .iter()
32        .map(|x| format!("{}", tinter::bright_cyan(x)))
33        .collect();
34
35    new_parts.join("::")
36}
37
38#[must_use]
39pub fn colorful_module_name(parts: &[String]) -> String {
40    let x = if parts[0] == "crate" {
41        &parts[1..]
42    } else {
43        parts
44    };
45
46    colorize_parts(x)
47}
48
49#[must_use]
50pub fn pretty_module_parts(parts: &[String]) -> String {
51    let new_parts: Vec<_> = parts.iter().map(std::string::ToString::to_string).collect();
52
53    new_parts.join("::")
54}
55
56#[must_use]
57pub fn pretty_module_name(parts: &[String]) -> String {
58    let x = if parts[0] == "crate" {
59        &parts[1..]
60    } else {
61        parts
62    };
63
64    pretty_module_parts(x)
65}
66
67#[must_use]
68pub fn matches_pattern(test_name: &str, pattern: &str) -> bool {
69    if pattern.ends_with("::") {
70        test_name.starts_with(pattern)
71    } else if pattern.contains('*') {
72        let parts: Vec<&str> = pattern.split('*').collect();
73        if parts.len() > 2 {
74            return false;
75        }
76
77        let prefix = parts[0];
78        let suffix = if parts.len() == 2 { parts[1] } else { "" }; // Handle "*suffix" or "prefix*"
79
80        if !test_name.starts_with(prefix) {
81            return false;
82        }
83
84        let remaining_name = &test_name[prefix.len()..];
85        remaining_name.ends_with(suffix)
86    } else {
87        test_name == pattern
88    }
89}
90#[must_use]
91pub fn test_name_matches_filter(test_name: &str, filter_string: &str) -> bool {
92    if filter_string.trim().is_empty() {
93        return true;
94    }
95
96    let patterns: Vec<&str> = filter_string.split(',').collect();
97
98    for pattern in patterns {
99        let trimmed_pattern = pattern.trim();
100
101        if matches_pattern(test_name, trimmed_pattern) {
102            return true;
103        }
104    }
105
106    false
107}
108
109pub enum StepBehavior {
110    ResumeExecution,
111    WaitForUserInput,
112    Pause(Duration),
113}
114
115pub enum StepParseError {
116    UnknownVariant(String),
117    MissingDuration,
118    ParseInt(ParseIntError),
119}
120
121impl Display for StepParseError {
122    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
123        match self {
124            Self::UnknownVariant(_) => write!(f, "unknown"),
125            Self::MissingDuration => write!(f, "missing duration"),
126            Self::ParseInt(_) => write!(f, "parse int failed"),
127        }
128    }
129}
130
131impl FromStr for StepBehavior {
132    type Err = StepParseError;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        let lower = s.to_lowercase();
136        let mut parts = lower.splitn(2, ':');
137        let variant = parts.next().unwrap();
138
139        match variant {
140            "resume" => Ok(Self::ResumeExecution),
141            "wait" => Ok(Self::WaitForUserInput),
142            "pause" => match parts.next() {
143                Some(ms_str) => {
144                    let ms: u64 = ms_str.parse().map_err(StepParseError::ParseInt)?;
145                    Ok(Self::Pause(Duration::from_millis(ms)))
146                }
147                None => Err(StepParseError::MissingDuration),
148            },
149
150            other => Err(StepParseError::UnknownVariant(other.to_string())),
151        }
152    }
153}
154
155pub struct TestRunOptions {
156    pub should_run: bool,
157    pub iteration_count: usize,
158    pub debug_output: bool,
159    pub print_output: bool,
160    pub debug_opcodes: bool,
161    pub debug_operations: bool,
162    pub debug_stats: bool,
163    pub show_semantic: bool,
164    pub show_assembly: bool,
165    pub assembly_filter: Option<String>,
166    pub show_modules: bool,
167    pub show_types: bool,
168    pub step_behaviour: StepBehavior,
169    pub debug_memory_enabled: bool,
170}
171
172pub fn init_logger() {
173    tracing_subscriber::fmt()
174        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
175        .with_writer(std::io::stderr)
176        .init();
177}
178
179#[derive(Clone)]
180pub struct TestInfo {
181    pub name: String,
182}
183impl Display for TestInfo {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
185        write!(f, "{}", self.name)
186    }
187}
188
189pub struct TestResult {
190    pub passed_tests: Vec<TestInfo>,
191    pub failed_tests: Vec<TestInfo>,
192}
193
194impl TestResult {
195    #[must_use]
196    pub const fn succeeded(&self) -> bool {
197        self.failed_tests.is_empty()
198    }
199}
200
201pub struct TestExternals {}
202
203impl HostFunctionCallback for TestExternals {
204    fn dispatch_host_call(&mut self, args: swamp_vm::host::HostArgs) {
205        if args.function_id == 1 {
206            print_fn(args);
207        }
208    }
209}
210
211/// # Panics
212#[allow(clippy::too_many_lines)]
213pub fn run_tests(
214    test_dir: &Path,
215    options: &TestRunOptions,
216    filter: &str,
217    module_suffix: &str,
218) -> TestResult {
219    let crate_main_path = &["crate".to_string(), module_suffix.to_string()];
220    let compile_and_code_gen_options = CompileAndCodeGenOptions {
221        compile_options: CompileOptions {
222            show_semantic: options.show_semantic,
223            show_modules: options.show_modules,
224            show_types: options.show_types,
225            show_errors: true,
226        },
227        code_gen_options: CodeGenOptions {
228            show_disasm: options.show_assembly,
229            disasm_filter: options.assembly_filter.clone(),
230            show_debug: options.debug_output,
231            show_types: options.show_types,
232            ignore_host_call: true,
233        },
234        skip_codegen: false,
235    };
236    let internal_result =
237        compile_codegen_and_create_vm(test_dir, crate_main_path, &compile_and_code_gen_options)
238            .unwrap();
239
240    let CompileAndVmResult::CompileAndVm(mut result) = internal_result else {
241        panic!("didn't work to compile")
242    };
243
244    let mut passed_tests = Vec::new();
245
246    let mut panic_tests = Vec::new();
247    let mut trap_tests = Vec::new();
248    let mut failed_tests = Vec::new();
249    let mut expected_panic_passed: Vec<TestInfo> = Vec::new();
250    let mut expected_trap_passed: Vec<TestInfo> = Vec::new();
251
252    if options.should_run {
253        let should_run_in_debug_mode = true; // TODO: Until very stable, always run in debug.  options.debug_opcodes || options.debug_output;
254
255        let run_first_options = RunOptions {
256            debug_stats_enabled: options.debug_stats,
257            debug_opcodes_enabled: options.debug_opcodes,
258            debug_operations_enabled: options.debug_operations,
259            debug_memory_enabled: options.debug_memory_enabled,
260            max_count: 0,
261            use_color: true,
262            debug_info: &result.codegen.code_gen_result.debug_info,
263            source_map_wrapper: SourceMapWrapper {
264                source_map: &result.codegen.source_map,
265                current_dir: PathBuf::default(),
266            },
267        };
268
269        swamp_runtime::run_first_time(
270            &mut result.codegen.vm,
271            &result.codegen.code_gen_result.constants_in_order,
272            &mut TestExternals {},
273            &run_first_options,
274        );
275
276        {
277            let _bootstrap_timer = ScopedTimer::new("run tests a bunch of times");
278
279            for (module_name, module) in result.compile.program.modules.modules() {
280                let mut has_shown_mod_name = false;
281                for internal_fn in module.symbol_table.internal_functions() {
282                    if !internal_fn.attributes.has_attribute("test") {
283                        continue;
284                    }
285                    if options.debug_output && !has_shown_mod_name {
286                        //eprintln!(">> module {module_name:?}");
287                        has_shown_mod_name = true;
288                    }
289                    let function_to_run = result
290                        .codegen
291                        .code_gen_result
292                        .functions
293                        .get(&internal_fn.program_unique_id)
294                        .unwrap();
295
296                    let all_attributes = &function_to_run.internal_function_definition.attributes;
297
298                    let mut expected_vm_state = VmState::Normal;
299
300                    if !all_attributes.is_empty() {
301                        let code =
302                            all_attributes.get_string_from_fn_arg("should_trap", "expected", 0);
303                        if let Some(code) = code {
304                            expected_vm_state = VmState::Trap(code.parse().unwrap());
305                        } else {
306                            let panic_message = all_attributes.get_string_from_fn_arg(
307                                "should_panic",
308                                "expected",
309                                0,
310                            );
311                            if let Some(panic_message) = panic_message {
312                                expected_vm_state = VmState::Panic(panic_message.clone());
313                            }
314                        }
315                    }
316
317                    let complete_name = format!(
318                        "{}::{}",
319                        colorful_module_name(module_name),
320                        tinter::blue(&function_to_run.internal_function_definition.assigned_name)
321                    );
322                    let formal_name = format!(
323                        "{}::{}",
324                        pretty_module_name(module_name),
325                        &function_to_run.internal_function_definition.assigned_name
326                    );
327
328                    if !test_name_matches_filter(&formal_name, filter) {
329                        continue;
330                    }
331
332                    let test_info = TestInfo { name: formal_name };
333
334                    eprintln!("πŸš€starting test '{complete_name}'");
335
336                    if should_run_in_debug_mode {
337                        for _ in 0..options.iteration_count {
338                            swamp_runtime::run_function_with_debug(
339                                &mut result.codegen.vm,
340                                &function_to_run.ip_range,
341                                &mut TestExternals {},
342                                &RunOptions {
343                                    debug_stats_enabled: options.debug_stats,
344                                    debug_opcodes_enabled: options.debug_opcodes,
345                                    debug_operations_enabled: options.debug_operations,
346                                    debug_memory_enabled: options.debug_memory_enabled,
347                                    max_count: 0,
348                                    use_color: true,
349                                    debug_info: &result.codegen.code_gen_result.debug_info,
350                                    source_map_wrapper: SourceMapWrapper {
351                                        source_map: &result.codegen.source_map,
352                                        current_dir: PathBuf::default(),
353                                    },
354                                },
355                            );
356
357                            while result.codegen.vm.state == VmState::Step {
358                                handle_step(&options.step_behaviour);
359                                result.codegen.vm.state = VmState::Normal;
360                                result.codegen.vm.resume(&mut TestExternals {});
361                            }
362
363                            if result.codegen.vm.state != expected_vm_state {
364                                break;
365                            }
366                        }
367                    } else {
368                        for _ in 0..options.iteration_count {
369                            swamp_runtime::run_as_fast_as_possible(
370                                &mut result.codegen.vm,
371                                function_to_run,
372                                &mut TestExternals {},
373                                RunOptions {
374                                    debug_stats_enabled: options.debug_stats,
375                                    debug_opcodes_enabled: options.debug_opcodes,
376                                    debug_operations_enabled: options.debug_operations,
377                                    max_count: 0,
378                                    use_color: true,
379                                    debug_info: &result.codegen.code_gen_result.debug_info,
380                                    source_map_wrapper: SourceMapWrapper {
381                                        source_map: &result.codegen.source_map,
382                                        current_dir: PathBuf::default(),
383                                    },
384                                    debug_memory_enabled: options.debug_memory_enabled,
385                                },
386                            );
387                            while result.codegen.vm.state == VmState::Step {
388                                handle_step(&options.step_behaviour);
389                                result.codegen.vm.state = VmState::Normal;
390                                result.codegen.vm.resume(&mut TestExternals {});
391                            }
392                            if result.codegen.vm.state != expected_vm_state {
393                                break;
394                            }
395                        }
396                    }
397
398                    if expected_vm_state == VmState::Normal {
399                        match &result.codegen.vm.state {
400                            VmState::Panic(message) => {
401                                panic_tests.push(test_info);
402                                error!(message, "PANIC!");
403                                eprintln!("❌ Panic {complete_name} {message}");
404                            }
405                            VmState::Normal => {
406                                passed_tests.push(test_info);
407                                eprintln!("βœ… {complete_name} worked!");
408                            }
409                            VmState::Trap(trap_code) => {
410                                trap_tests.push(test_info);
411                                error!(%trap_code, "TRAP");
412                                eprintln!("❌ trap {complete_name} {trap_code}");
413                            }
414
415                            VmState::Step | VmState::Halt => {
416                                panic_tests.push(test_info);
417                                error!("Step or Halt");
418                                eprintln!("❌ trap {complete_name}");
419                            }
420                        }
421                    } else if let VmState::Trap(expected_trap_code) = expected_vm_state {
422                        match &result.codegen.vm.state {
423                            VmState::Trap(actual_trap_code) => {
424                                if actual_trap_code == &expected_trap_code {
425                                    expected_trap_passed.push(test_info.clone());
426                                    eprintln!(
427                                        "βœ… Expected Trap {complete_name} (code: {actual_trap_code})"
428                                    );
429                                } else {
430                                    failed_tests.push(test_info.clone());
431                                    error!(%expected_trap_code, %actual_trap_code, "WRONG TRAP CODE");
432                                    eprintln!(
433                                        "❌ Wrong Trap Code {complete_name} (Expected: {expected_trap_code}, Got: {actual_trap_code})"
434                                    );
435                                }
436                            }
437                            VmState::Normal => {
438                                failed_tests.push(test_info.clone());
439                                error!("Expected TRAP, got NORMAL");
440                                eprintln!("❌ Expected Trap {complete_name}, but it ran normally.");
441                            }
442                            VmState::Panic(message) => {
443                                failed_tests.push(test_info.clone());
444                                error!(message, "Expected TRAP, got PANIC");
445                                eprintln!(
446                                    "❌ Expected Trap {complete_name}, but it panicked: {message}"
447                                );
448                            }
449                            VmState::Step | VmState::Halt => {
450                                panic_tests.push(test_info);
451                                error!("Step or Halt");
452                                eprintln!("❌ trap {complete_name}");
453                            }
454                        }
455                    } else if let VmState::Panic(expected_panic_message) = expected_vm_state {
456                        match &result.codegen.vm.state {
457                            VmState::Panic(actual_panic_message) => {
458                                if actual_panic_message.contains(&expected_panic_message) {
459                                    expected_panic_passed.push(test_info.clone());
460                                    eprintln!(
461                                        "βœ… Expected Panic {complete_name} (message contains: \"{expected_panic_message}\")",
462                                    );
463                                } else {
464                                    failed_tests.push(test_info.clone());
465                                    error!(
466                                        expected_panic_message,
467                                        actual_panic_message, "WRONG PANIC MESSAGE"
468                                    );
469                                    eprintln!(
470                                        "❌ Wrong Panic Message {complete_name} (Expected contains: \"{expected_panic_message}\", Got: \"{actual_panic_message}\")"
471                                    );
472                                }
473                            }
474                            VmState::Normal => {
475                                failed_tests.push(test_info.clone());
476                                error!("Expected PANIC, got NORMAL");
477                                eprintln!(
478                                    "❌ Expected Panic {complete_name}, but it ran normally."
479                                );
480                            }
481                            VmState::Trap(trap_code) => {
482                                failed_tests.push(test_info.clone());
483                                error!(%trap_code, "Expected PANIC, got TRAP");
484                                eprintln!(
485                                    "❌ Expected Panic {complete_name}, but it trapped: {trap_code}"
486                                );
487                            }
488                            VmState::Step | VmState::Halt => {
489                                panic_tests.push(test_info);
490                                error!("Step or Halt");
491                                eprintln!("❌ trap {complete_name}");
492                            }
493                        }
494                    }
495                }
496            }
497        }
498
499        // Calculate counts for each category
500        let passed_normal_count = passed_tests.len();
501        let unexpected_panic_count = panic_tests.len();
502        let unexpected_trap_count = trap_tests.len();
503        let failed_mismatch_count = failed_tests.len(); // These are tests that failed for reasons other than an unexpected panic/trap
504
505        let expected_panic_pass_count = expected_panic_passed.len();
506        let expected_trap_pass_count = expected_trap_passed.len();
507
508        // Total counts
509        let total_passed_count =
510            passed_normal_count + expected_panic_pass_count + expected_trap_pass_count;
511        let total_failed_count =
512            unexpected_panic_count + unexpected_trap_count + failed_mismatch_count;
513        let total_tests_run = total_passed_count + total_failed_count;
514
515        // ---
516        // ## Test Run Summary
517        // ---
518        println!("\n---\nπŸš€ Test Run Complete! πŸš€\n");
519
520        println!("Results:");
521        println!("  βœ… Passed Normally: {passed_normal_count}");
522        println!("  βœ… Passed (Expected Panic): {expected_panic_pass_count}");
523        println!("  βœ… Passed (Expected Trap): {expected_trap_pass_count}");
524
525        if total_failed_count > 0 {
526            println!("  ❌ **TOTAL FAILED:** {total_failed_count}",);
527        }
528
529        println!("  Total Tests Run: {total_tests_run}",);
530
531        // ---
532        // ## Failing Test Details
533        // ---
534        if total_failed_count > 0 {
535            println!("\n--- Failing Tests Details ---");
536
537            if unexpected_panic_count > 0 {
538                println!("\n### Unexpected Panics:");
539                for test in &panic_tests {
540                    println!("- ❌ {}", test.name);
541                }
542            }
543
544            if unexpected_trap_count > 0 {
545                println!("\n### Unexpected Traps:");
546                for test in &trap_tests {
547                    println!("- ❌ {}", test.name);
548                }
549            }
550
551            if failed_mismatch_count > 0 {
552                println!("\n### Other Failures:");
553                for test in &failed_tests {
554                    println!("- ❌ {}", test.name);
555                }
556            }
557        }
558
559        eprintln!("\n\nvm stats {:?}", result.codegen.vm.debug);
560    }
561
562    let failed_tests = [trap_tests, panic_tests].concat();
563    TestResult {
564        passed_tests,
565        failed_tests,
566    }
567}
568
569fn handle_step(step_behavior: &StepBehavior) {
570    match step_behavior {
571        StepBehavior::ResumeExecution => {}
572        StepBehavior::WaitForUserInput => {
573            wait_for_user_pressing_enter();
574        }
575        StepBehavior::Pause(duration) => {
576            eprintln!("step. waiting {duration:?}");
577            sleep(*duration);
578        }
579    }
580}
581
582fn wait_for_user_pressing_enter() {
583    let mut buf = String::new();
584    print!("Step detected. press ENTER to continue");
585    io::stdout().flush().unwrap();
586
587    // Blocks until the user hits Enter
588    io::stdin().read_line(&mut buf).expect("should work");
589}