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