1use super::{DownloadSource, Event, EventListener, find_max_execution_units, group_by_module};
2use crate::{CoverageMode, pretty};
3use aiken_lang::{
4 ast::OnTestFailure,
5 expr::UntypedExpr,
6 format::Formatter,
7 test_framework::{
8 AssertionStyleOptions, BenchmarkResult, PropertyTestResult, TestResult, UnitTestResult,
9 },
10};
11use numfmt::{Precision, Scales};
12use owo_colors::{OwoColorize, Stream::Stderr};
13use rgb::RGB8;
14use std::sync::LazyLock;
15use uplc::machine::cost_model::ExBudget;
16
17static BENCH_PLOT_COLOR: LazyLock<RGB8> = LazyLock::new(|| RGB8 {
18 r: 250,
19 g: 211,
20 b: 144,
21});
22
23#[derive(Debug, Default, Clone, Copy)]
24pub struct Terminal;
25
26impl EventListener for Terminal {
27 fn handle_event(&self, event: Event) {
28 match event {
29 Event::StartingCompilation {
30 name,
31 version,
32 root,
33 } => {
34 eprintln!(
35 "{} {} {} ({})",
36 " Compiling"
37 .if_supports_color(Stderr, |s| s.bold())
38 .if_supports_color(Stderr, |s| s.purple()),
39 name.if_supports_color(Stderr, |s| s.bold()),
40 version,
41 root.display()
42 .if_supports_color(Stderr, |s| s.bright_blue())
43 );
44 }
45 Event::BuildingDocumentation {
46 name,
47 version,
48 root,
49 } => {
50 eprintln!(
51 "{} {} for {} {} ({})",
52 " Generating"
53 .if_supports_color(Stderr, |s| s.bold())
54 .if_supports_color(Stderr, |s| s.purple()),
55 "documentation".if_supports_color(Stderr, |s| s.bold()),
56 name.if_supports_color(Stderr, |s| s.bold()),
57 version,
58 root.to_str()
59 .unwrap_or("")
60 .if_supports_color(Stderr, |s| s.bright_blue())
61 );
62 }
63 Event::WaitingForBuildDirLock => {
64 eprintln!(
65 "{}",
66 "Waiting for build directory lock ..."
67 .if_supports_color(Stderr, |s| s.bold())
68 .if_supports_color(Stderr, |s| s.purple())
69 );
70 }
71 Event::DumpingUPLC { path } => {
72 eprintln!(
73 "{} {} ({})",
74 " Exporting"
75 .if_supports_color(Stderr, |s| s.bold())
76 .if_supports_color(Stderr, |s| s.purple()),
77 "UPLC".if_supports_color(Stderr, |s| s.bold()),
78 path.display()
79 .if_supports_color(Stderr, |s| s.bright_blue())
80 );
81 }
82 Event::GeneratingBlueprint { path } => {
83 eprintln!(
84 "{} {} ({})",
85 " Generating"
86 .if_supports_color(Stderr, |s| s.bold())
87 .if_supports_color(Stderr, |s| s.purple()),
88 "project's blueprint".if_supports_color(Stderr, |s| s.bold()),
89 path.display()
90 .if_supports_color(Stderr, |s| s.bright_blue())
91 );
92 }
93 Event::GeneratingDocFiles { output_path } => {
94 eprintln!(
95 "{} {} to {}",
96 " Writing"
97 .if_supports_color(Stderr, |s| s.bold())
98 .if_supports_color(Stderr, |s| s.purple()),
99 "documentation files".if_supports_color(Stderr, |s| s.bold()),
100 output_path
101 .to_str()
102 .unwrap_or("")
103 .if_supports_color(Stderr, |s| s.bright_blue())
104 );
105 }
106 Event::GeneratingUPLCFor { name, path } => {
107 eprintln!(
108 "{} {} {}.{{{}}}",
109 " Generating"
110 .if_supports_color(Stderr, |s| s.bold())
111 .if_supports_color(Stderr, |s| s.purple()),
112 "UPLC for"
113 .if_supports_color(Stderr, |s| s.bold())
114 .if_supports_color(Stderr, |s| s.white()),
115 path.to_str()
116 .unwrap_or("")
117 .if_supports_color(Stderr, |s| s.blue()),
118 name.if_supports_color(Stderr, |s| s.bright_blue()),
119 );
120 }
121 Event::CollectingTests {
122 matching_module,
123 matching_names,
124 } => {
125 eprintln!(
126 "{:>13} {tests} {module}",
127 "Collecting"
128 .if_supports_color(Stderr, |s| s.bold())
129 .if_supports_color(Stderr, |s| s.purple()),
130 tests = if matching_names.is_empty() {
131 if matching_module.is_some() {
132 "all tests scenarios"
133 .if_supports_color(Stderr, |s| s.bold())
134 .to_string()
135 } else {
136 "all tests scenarios".to_string()
137 }
138 } else {
139 format!(
140 "test{} {}",
141 if matching_names.len() > 1 { "s" } else { "" },
142 matching_names
143 .iter()
144 .map(|s| format!("*{s}*"))
145 .collect::<Vec<_>>()
146 .join(", ")
147 .if_supports_color(Stderr, |s| s.bold())
148 )
149 },
150 module = match matching_module {
151 None => format!(
152 "across {}",
153 if matching_names.is_empty() {
154 "all modules".to_string()
155 } else {
156 "all modules"
157 .if_supports_color(Stderr, |s| s.bold())
158 .to_string()
159 }
160 ),
161 Some(module) => format!(
162 "within module(s): {}",
163 format!("*{module}*").if_supports_color(Stderr, |s| s.bold())
164 ),
165 }
166 );
167 }
168 Event::RunningTests => {
169 eprintln!(
170 "{} {}",
171 " Testing"
172 .if_supports_color(Stderr, |s| s.bold())
173 .if_supports_color(Stderr, |s| s.purple()),
174 "...".if_supports_color(Stderr, |s| s.bold())
175 );
176 }
177 Event::FinishedTests {
178 seed,
179 tests,
180 coverage_mode,
181 plain_numbers,
182 } => {
183 let (max_mem, max_cpu, max_iter) = find_max_execution_units(&tests);
184
185 let (mut formatter, max_mem, max_cpu) =
186 derive_execution_units_format(plain_numbers, max_mem, max_cpu);
187
188 for (module, results) in &group_by_module(&tests) {
189 let title = module
190 .if_supports_color(Stderr, |s| s.bold())
191 .if_supports_color(Stderr, |s| s.blue())
192 .to_string();
193
194 let tests = results
195 .iter()
196 .map(|r| {
197 fmt_test(
198 r,
199 max_mem,
200 max_cpu,
201 max_iter,
202 true,
203 coverage_mode,
204 &mut formatter,
205 )
206 })
207 .collect::<Vec<String>>()
208 .join("\n");
209
210 let seed_info = if results
211 .iter()
212 .any(|t| matches!(t, TestResult::PropertyTestResult { .. }))
213 {
214 format!(
215 "with {opt}={seed} → ",
216 opt = "--seed".if_supports_color(Stderr, |s| s.bold()),
217 seed = format!("{seed}").if_supports_color(Stderr, |s| s.bold())
218 )
219 } else {
220 String::new()
221 };
222
223 if !tests.is_empty() {
224 println!();
225 }
226
227 let summary = format!("{}{}", seed_info, fmt_test_summary(results, true));
228 println!(
229 "{}\n",
230 pretty::indent(
231 &pretty::open_box(&title, &tests, &summary, |border| border
232 .if_supports_color(Stderr, |s| s.bright_black())
233 .to_string()),
234 4
235 )
236 );
237 }
238
239 if !tests.is_empty() {
240 println!();
241 }
242 }
243 Event::ResolvingPackages { name } => {
244 eprintln!(
245 "{} {}",
246 " Resolving"
247 .if_supports_color(Stderr, |s| s.bold())
248 .if_supports_color(Stderr, |s| s.purple()),
249 name.if_supports_color(Stderr, |s| s.bold())
250 )
251 }
252 Event::PackageResolveFallback { name } => {
253 eprintln!(
254 "{} {}\n ↳ You're seeing this message because the package version is unpinned and the network is not accessible.",
255 " Using"
256 .if_supports_color(Stderr, |s| s.bold())
257 .if_supports_color(Stderr, |s| s.yellow()),
258 format!("uncertain local version for {name}")
259 .if_supports_color(Stderr, |s| s.yellow())
260 )
261 }
262 Event::PackagesDownloaded {
263 start,
264 count,
265 source,
266 } => {
267 let elapsed = format!("{:.2}s", start.elapsed().as_millis() as f32 / 1000.);
268
269 let msg = match count {
270 1 => format!("1 package in {elapsed}"),
271 _ => format!("{count} packages in {elapsed}"),
272 };
273
274 eprintln!(
275 "{} {} from {source}",
276 match source {
277 DownloadSource::Network => " Downloaded",
278 DownloadSource::Cache => " Fetched",
279 }
280 .if_supports_color(Stderr, |s| s.bold())
281 .if_supports_color(Stderr, |s| s.purple()),
282 msg.if_supports_color(Stderr, |s| s.bold())
283 )
284 }
285 Event::ResolvingVersions => {
286 eprintln!(
287 "{} {}",
288 " Resolving"
289 .if_supports_color(Stderr, |s| s.bold())
290 .if_supports_color(Stderr, |s| s.purple()),
291 "dependencies".if_supports_color(Stderr, |s| s.bold())
292 )
293 }
294 Event::RunningBenchmarks => {
295 eprintln!(
296 "{} {}",
297 " Benchmarking"
298 .if_supports_color(Stderr, |s| s.bold())
299 .if_supports_color(Stderr, |s| s.purple()),
300 "...".if_supports_color(Stderr, |s| s.bold())
301 );
302 }
303 Event::FinishedBenchmarks { seed, benchmarks } => {
304 let (max_mem, max_cpu, max_iter) = find_max_execution_units(&benchmarks);
305
306 for (module, results) in &group_by_module(&benchmarks) {
307 let title = module
308 .if_supports_color(Stderr, |s| s.bold())
309 .if_supports_color(Stderr, |s| s.blue())
310 .to_string();
311
312 let mut formatter = numfmt::Formatter::new();
313
314 let benchmarks = results
315 .iter()
316 .map(|r| {
317 fmt_test(
318 r,
319 max_mem,
320 max_cpu,
321 max_iter,
322 true,
323 CoverageMode::default(),
324 &mut formatter,
325 )
326 })
327 .collect::<Vec<String>>()
328 .join("\n")
329 .chars()
330 .skip(1) .collect::<String>();
332
333 let seed_info = format!(
334 "with {opt}={seed}",
335 opt = "--seed".if_supports_color(Stderr, |s| s.bold()),
336 seed = format!("{seed}").if_supports_color(Stderr, |s| s.bold())
337 );
338
339 if !benchmarks.is_empty() {
340 println!();
341 }
342
343 println!(
344 "{}\n",
345 pretty::indent(
346 &pretty::open_box(&title, &benchmarks, &seed_info, |border| border
347 .if_supports_color(Stderr, |s| s.bright_black())
348 .to_string()),
349 4
350 )
351 );
352 }
353
354 if !benchmarks.is_empty() {
355 println!();
356 }
357 }
358 }
359 }
360}
361
362fn fmt_test(
363 result: &TestResult<UntypedExpr, UntypedExpr>,
364 max_mem: usize,
365 max_cpu: usize,
366 max_iter: usize,
367 styled: bool,
368 coverage_mode: CoverageMode,
369 formatter: &mut numfmt::Formatter,
370) -> String {
371 let mut test = if matches!(result, TestResult::BenchmarkResult { .. }) {
373 format!(
374 "\n{label}{title}\n",
375 label = if result.is_success() {
376 String::new()
377 } else {
378 pretty::style_if(styled, "FAIL ".to_string(), |s| {
379 s.if_supports_color(Stderr, |s| s.bold())
380 .if_supports_color(Stderr, |s| s.red())
381 .to_string()
382 })
383 },
384 title = pretty::style_if(styled, result.title().to_string(), |s| s
385 .if_supports_color(Stderr, |s| s.bright_blue())
386 .to_string())
387 )
388 } else if result.is_success() {
389 pretty::style_if(styled, "PASS".to_string(), |s| {
390 s.if_supports_color(Stderr, |s| s.bold())
391 .if_supports_color(Stderr, |s| s.green())
392 .to_string()
393 })
394 } else {
395 pretty::style_if(styled, "FAIL".to_string(), |s| {
396 s.if_supports_color(Stderr, |s| s.bold())
397 .if_supports_color(Stderr, |s| s.red())
398 .to_string()
399 })
400 };
401
402 match result {
404 TestResult::UnitTestResult(UnitTestResult { spent_budget, .. }) => {
405 let ExBudget { mem, cpu } = spent_budget;
406
407 let mem_pad = pretty::pad_left(formatter.fmt2(*mem).to_owned(), max_mem, " ");
408 let cpu_pad = pretty::pad_left(formatter.fmt2(*cpu).to_owned(), max_cpu, " ");
409
410 test = format!(
411 "{test} [mem: {mem_unit}, cpu: {cpu_unit}]",
412 mem_unit = pretty::style_if(styled, mem_pad, |s| s
413 .if_supports_color(Stderr, |s| s.cyan())
414 .to_string()),
415 cpu_unit = pretty::style_if(styled, cpu_pad, |s| s
416 .if_supports_color(Stderr, |s| s.cyan())
417 .to_string()),
418 );
419 }
420 TestResult::PropertyTestResult(PropertyTestResult { iterations, .. }) => {
421 test = format!(
422 "{test} [after {} test{}]",
423 pretty::pad_left(
424 if *iterations == 0 {
425 "?".to_string()
426 } else {
427 iterations.to_string()
428 },
429 max_iter,
430 " "
431 ),
432 if *iterations > 1 { "s" } else { "" }
433 );
434 }
435 TestResult::BenchmarkResult(BenchmarkResult { error: Some(e), .. }) => {
436 test = format!(
437 "{test}{}",
438 e.to_string().if_supports_color(Stderr, |s| s.red())
439 );
440 }
441 TestResult::BenchmarkResult(BenchmarkResult {
442 measures,
443 error: None,
444 ..
445 }) => {
446 let max_size = measures
447 .iter()
448 .map(|(size, _)| *size)
449 .max()
450 .unwrap_or_default();
451
452 let mem_chart = format!(
453 "{title}\n{chart}",
454 title = "memory units"
455 .if_supports_color(Stderr, |s| s.yellow())
456 .if_supports_color(Stderr, |s| s.bold()),
457 chart = plot(
458 &BENCH_PLOT_COLOR,
459 measures
460 .iter()
461 .map(|(size, budget)| (*size as f32, budget.mem as f32))
462 .collect::<Vec<_>>(),
463 max_size
464 )
465 );
466
467 let cpu_chart = format!(
468 "{title}\n{chart}",
469 title = "cpu units"
470 .if_supports_color(Stderr, |s| s.yellow())
471 .if_supports_color(Stderr, |s| s.bold()),
472 chart = plot(
473 &BENCH_PLOT_COLOR,
474 measures
475 .iter()
476 .map(|(size, budget)| (*size as f32, budget.cpu as f32))
477 .collect::<Vec<_>>(),
478 max_size
479 )
480 );
481
482 let charts = mem_chart
483 .lines()
484 .zip(cpu_chart.lines())
485 .map(|(l, r)| format!(" {}{r}", pretty::pad_right(l.to_string(), 55, " ")))
486 .collect::<Vec<_>>()
487 .join("\n");
488
489 test = format!("{test}{charts}",);
490 }
491 }
492
493 test = match result {
495 TestResult::BenchmarkResult(..) => test,
496 TestResult::UnitTestResult(..) | TestResult::PropertyTestResult(..) => {
497 format!(
498 "{test} {title}",
499 title = pretty::style_if(styled, result.title().to_string(), |s| s
500 .if_supports_color(Stderr, |s| s.bright_blue())
501 .to_string())
502 )
503 }
504 };
505
506 match result {
508 TestResult::UnitTestResult(UnitTestResult {
509 assertion: Some(assertion),
510 test: unit_test,
511 ..
512 }) if !result.is_success() => {
513 test = format!(
514 "{test}\n{}",
515 assertion.to_string(
516 match unit_test.on_test_failure {
517 OnTestFailure::FailImmediately => false,
518 OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately =>
519 true,
520 },
521 &AssertionStyleOptions::new(Some(&Stderr))
522 ),
523 );
524 }
525 _ => (),
526 }
527
528 if let TestResult::PropertyTestResult(PropertyTestResult { counterexample, .. }) = result {
530 match counterexample {
531 Err(err) => {
532 test = format!(
533 "{test}\n{}\n{}",
534 "× fuzzer failed unexpectedly"
535 .if_supports_color(Stderr, |s| s.red())
536 .if_supports_color(Stderr, |s| s.bold()),
537 format!("| {err}").if_supports_color(Stderr, |s| s.red())
538 );
539 }
540
541 Ok(None) => {
542 if !result.is_success() {
543 test = format!(
544 "{test}\n{}",
545 "× no counterexample found"
546 .if_supports_color(Stderr, |s| s.red())
547 .if_supports_color(Stderr, |s| s.bold())
548 );
549 }
550 }
551
552 Ok(Some(counterexample)) => {
553 let is_expected_failure = result.is_success();
554
555 test = format!(
556 "{test}\n{}\n{}",
557 if is_expected_failure {
558 "★ counterexample"
559 .if_supports_color(Stderr, |s| s.green())
560 .if_supports_color(Stderr, |s| s.bold())
561 .to_string()
562 } else {
563 "× counterexample"
564 .if_supports_color(Stderr, |s| s.red())
565 .if_supports_color(Stderr, |s| s.bold())
566 .to_string()
567 },
568 &Formatter::new()
569 .expr(counterexample, false)
570 .to_pretty_string(60)
571 .lines()
572 .map(|line| {
573 format!(
574 "{} {}",
575 "│".if_supports_color(Stderr, |s| if is_expected_failure {
576 s.green().to_string()
577 } else {
578 s.red().to_string()
579 }),
580 line
581 )
582 })
583 .collect::<Vec<String>>()
584 .join("\n"),
585 );
586 }
587 }
588 }
589
590 if let TestResult::PropertyTestResult(PropertyTestResult {
592 labels, iterations, ..
593 }) = result
594 {
595 if !labels.is_empty() && result.is_success() {
596 test = format!(
597 "{test}\n{title}",
598 title = "· with coverage".if_supports_color(Stderr, |s| s.bold())
599 );
600
601 let mut total = 0;
602 let mut pad = 0;
603 for (k, v) in labels {
604 total += v;
605 if k.len() > pad {
606 pad = k.len();
607 }
608 }
609
610 match coverage_mode {
611 CoverageMode::RelativeToLabels => {}
612 CoverageMode::RelativeToTests => {
613 total = *iterations;
614 }
615 }
616
617 let mut labels = labels.iter().collect::<Vec<_>>();
618 labels.sort_by(|a, b| b.1.cmp(a.1));
619
620 for (k, v) in labels {
621 test = format!(
622 "{test}\n| {} {:>5.1}%",
623 pretty::pad_right(k.to_owned(), pad, " ")
624 .if_supports_color(Stderr, |s| s.bold()),
625 100.0 * (*v as f64) / (total as f64),
626 );
627 }
628 }
629 }
630
631 if !result.logs().is_empty() {
633 test = format!(
634 "{test}\n{title}\n{traces}",
635 title = "· with traces".if_supports_color(Stderr, |s| s.bold()),
636 traces = result
637 .logs()
638 .iter()
639 .map(|line| {
640 match line
641 .strip_prefix("expect")
642 .or_else(|| line.strip_prefix("<expected>"))
643 {
644 None => format!("| {line}"),
645 Some(rest) => format!(
646 "{}{rest}",
647 "x <expected>"
648 .if_supports_color(Stderr, |s| s.red().bold().to_string())
649 ),
650 }
651 })
652 .collect::<Vec<_>>()
653 .join("\n")
654 );
655 };
656
657 test
658}
659
660fn fmt_test_summary<T>(tests: &[&TestResult<T, T>], styled: bool) -> String {
661 let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| {
662 if result.is_success() {
663 (n_passed + 1, n_failed)
664 } else {
665 (n_passed, n_failed + 1)
666 }
667 });
668 format!(
669 "{} | {} | {}",
670 pretty::style_if(styled, format!("{} tests", tests.len()), |s| s
671 .if_supports_color(Stderr, |s| s.bold())
672 .to_string()),
673 pretty::style_if(styled, format!("{n_passed} passed"), |s| s
674 .if_supports_color(Stderr, |s| s.bright_green())
675 .if_supports_color(Stderr, |s| s.bold())
676 .to_string()),
677 pretty::style_if(styled, format!("{n_failed} failed"), |s| s
678 .if_supports_color(Stderr, |s| s.bright_red())
679 .if_supports_color(Stderr, |s| s.bold())
680 .to_string()),
681 )
682}
683
684fn plot(color: &RGB8, points: Vec<(f32, f32)>, max_size: usize) -> String {
685 use textplots::{Chart, ColorPlot, Shape};
686 let mut chart = Chart::new(80, 50, 1.0, max_size as f32);
687 let plot = Shape::Lines(&points);
688 let chart = chart.linecolorplot(&plot, *color);
689 chart.borders();
690 chart.axis();
691 chart.figures();
692 chart.to_string()
693}
694
695fn derive_execution_units_format(
696 plain_numbers: bool,
697 max_mem: usize,
698 max_cpu: usize,
699) -> (numfmt::Formatter, usize, usize) {
700 let update_max_size = |x: usize| {
703 if x % 3 == 0 { x + x / 3 - 1 } else { x + x / 3 }
704 };
705
706 if plain_numbers {
707 let formatter = numfmt::Formatter::new()
708 .separator('_')
709 .unwrap()
710 .precision(Precision::Decimals(0));
711 (
712 formatter,
713 update_max_size(max_mem),
714 update_max_size(max_cpu),
715 )
716 } else {
717 let formatter = numfmt::Formatter::new()
718 .scales(Scales::short())
719 .precision(Precision::Decimals(2));
720 (formatter, 8, 8)
723 }
724}