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 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 { "" }; 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#[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 };
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.is_sort_of_equal(&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}