forge/compare_matrix/
report.rs1use 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}