aiken_project/telemetry/
terminal.rs

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) // Remove extra first newline
331                        .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    // Status
372    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    // Execution units / iteration steps
403    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    // Title
494    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    // Annotations
507    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    // CounterExamples
529    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    // Labels
591    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    // Traces
632    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    // Update max size of the execution units to account for underscores
701    // after three decimal place e.g. 1_000_000
702    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        // For units denoted in scales, max unit value
721        // does not give max unit size (e.g. 123.4 K vs 12.3 M )
722        (formatter, 8, 8)
723    }
724}