1pub 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 { "" }; 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#[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; 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 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 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(); let expected_panic_pass_count = expected_panic_passed.len();
526 let expected_trap_pass_count = expected_trap_passed.len();
527
528 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 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 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 io::stdin().read_line(&mut buf).expect("should work");
609}