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, 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 { "" }; 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 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; 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 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 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(); let expected_panic_pass_count = expected_panic_passed.len();
507 let expected_trap_pass_count = expected_trap_passed.len();
508
509 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 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 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 io::stdin().read_line(&mut buf).expect("should work");
590}