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