Skip to main content

forge/compare_matrix/
report.rs

1// #t(file: rust_println) JSON and TSV are intentional CLI output formats.
2
3use anyhow::Result;
4#[cfg(test)]
5use googletest::{Result as GtestResult, prelude::*};
6use tabled::Tabled;
7use wowlab_common::output;
8
9use super::types::{ComparisonMatrix, ComparisonRow, ComparisonStatus, MatrixFormat};
10
11const ERROR_DISPLAY_CHARS: usize = 100;
12const CLOSE_PARITY_PERCENT: f64 = 10.0;
13
14#[derive(Debug, Tabled)]
15struct MatrixRow {
16    #[tabled(rename = "Spec")]
17    spec: String,
18    #[tabled(rename = "Status")]
19    status: String,
20    #[tabled(rename = "WoW Lab")]
21    wowlab_dps: String,
22    #[tabled(rename = "SimC")]
23    simc_dps: String,
24    #[tabled(rename = "Delta")]
25    delta_dps: String,
26    #[tabled(rename = "Delta %")]
27    delta_pct: String,
28}
29
30pub(super) fn print(matrix: &ComparisonMatrix, format: MatrixFormat) -> Result<()> {
31    match format {
32        MatrixFormat::Table => print_table(matrix),
33        MatrixFormat::Json => println!("{}", serde_json::to_string_pretty(matrix)?),
34        MatrixFormat::Tsv => print_tsv(matrix),
35    }
36
37    Ok(())
38}
39
40fn print_table(matrix: &ComparisonMatrix) {
41    output::blank();
42    output::subheader("Spec parity (failures first, then largest absolute gap)");
43    output::table(matrix.rows.iter().map(table_row));
44
45    let succeeded = matrix
46        .rows
47        .iter()
48        .filter(|row| row.status == ComparisonStatus::Ok)
49        .count();
50    let failed = matrix.rows.len() - succeeded;
51    let within_ten = matrix
52        .rows
53        .iter()
54        .filter(|row| {
55            row.absolute_delta_pct()
56                .is_some_and(|delta| delta <= CLOSE_PARITY_PERCENT)
57        })
58        .count();
59
60    output::detail(&format!(
61        "{succeeded} succeeded, {failed} failed, {within_ten} within ±10%",
62    ));
63
64    for row in matrix
65        .rows
66        .iter()
67        .filter(|row| row.status == ComparisonStatus::Failed)
68    {
69        let error = row.error.as_deref().unwrap_or("unknown error");
70        let display: String = error.chars().take(ERROR_DISPLAY_CHARS).collect();
71
72        eprintln!("  {}: {display}", row.spec);
73    }
74}
75
76fn table_row(row: &ComparisonRow) -> MatrixRow {
77    MatrixRow {
78        spec: row.spec.clone(),
79        status: match row.status {
80            ComparisonStatus::Ok => "ok".to_string(),
81            ComparisonStatus::Failed => "failed".to_string(),
82        },
83        wowlab_dps: decimal(row.wowlab_dps),
84        simc_dps: decimal(row.simc_dps),
85        delta_dps: signed(row.delta_dps),
86        delta_pct: row
87            .delta_pct
88            .map_or_else(|| "--".to_string(), |value| format!("{value:+.1}%")),
89    }
90}
91
92fn decimal(value: Option<f64>) -> String {
93    value.map_or_else(|| "--".to_string(), |value| format!("{value:.1}"))
94}
95
96fn signed(value: Option<f64>) -> String {
97    value.map_or_else(|| "--".to_string(), |value| format!("{value:+.1}"))
98}
99
100fn print_tsv(matrix: &ComparisonMatrix) {
101    println!("spec\tstatus\twowlab_dps\tsimc_dps\tdelta_dps\tdelta_pct\terror");
102
103    for row in &matrix.rows {
104        let error = row
105            .error
106            .as_deref()
107            .unwrap_or_default()
108            .replace(['\t', '\n', '\r'], " ");
109
110        println!(
111            "{}\t{}\t{}\t{}\t{}\t{}\t{}",
112            row.spec,
113            match row.status {
114                ComparisonStatus::Ok => "ok",
115                ComparisonStatus::Failed => "failed",
116            },
117            optional_number(row.wowlab_dps),
118            optional_number(row.simc_dps),
119            optional_number(row.delta_dps),
120            optional_number(row.delta_pct),
121            error,
122        );
123    }
124}
125
126fn optional_number(value: Option<f64>) -> String {
127    value.map_or_else(String::new, |value| value.to_string())
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[gtest]
135    fn table_rows_render_success_and_failure_cells() -> GtestResult<()> {
136        let success = table_row(&ComparisonRow::success(
137            "fire_mage".to_string(),
138            80.0,
139            100.0,
140        ));
141        let failed = table_row(&ComparisonRow::failed(
142            "subtlety_rogue".to_string(),
143            "compile failed".to_string(),
144        ));
145
146        verify_that!(
147            success,
148            matches_pattern!(MatrixRow {
149                wowlab_dps: eq("80.0"),
150                delta_dps: eq("-20.0"),
151                delta_pct: eq("-20.0%"),
152                ..
153            })
154        )?;
155        verify_that!(
156            failed,
157            matches_pattern!(MatrixRow {
158                status: eq("failed"),
159                wowlab_dps: eq("--"),
160                delta_pct: eq("--"),
161                ..
162            })
163        )?;
164
165        Ok(())
166    }
167}