1use 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 { "" }; 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#[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; 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 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 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(); let expected_panic_pass_count = expected_panic_passed.len();
506 let expected_trap_pass_count = expected_trap_passed.len();
507
508 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 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 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 io::stdin().read_line(&mut buf).expect("should work");
589}