Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 171 additions & 17 deletions crates/coverage-report/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,28 @@ pub fn format_cell(pair_result: &PairResult) -> String {
format!("{} {}/{}", emoji, pair_result.passed, total)
}

/// Returns (table_markdown, stats, failures, limitations, missing_fixtures)
pub fn generate_table(
results: &HashMap<(usize, usize), PairResult>,
adapters: &[Box<dyn ProviderAdapter>],
title: &str,
) -> (String, TableStats, Vec<(String, String, String)>) {
) -> (
String,
TableStats,
Vec<(String, String, String)>,
Vec<(String, String, String)>,
Vec<(String, String, String)>,
) {
let mut table = String::new();
let mut stats = TableStats {
passed: 0,
failed: 0,
limitations: 0,
missing_fixtures: 0,
};
let mut all_failures: Vec<(String, String, String)> = Vec::new();
let mut all_limitations: Vec<(String, String, String)> = Vec::new();
let mut all_missing_fixtures: Vec<(String, String, String)> = Vec::new();

table.push_str(&format!("### {}\n\n", title));
table.push_str("| Source ↓ / Target → |");
Expand All @@ -57,6 +68,8 @@ pub fn generate_table(

stats.passed += pair_result.passed;
stats.failed += pair_result.failed;
stats.limitations += pair_result.limitations;
stats.missing_fixtures += pair_result.missing_fixtures;

for (test_case, error) in &pair_result.failures {
all_failures.push((
Expand All @@ -65,12 +78,28 @@ pub fn generate_table(
error.clone(),
));
}

for (test_case, error) in &pair_result.limitation_details {
all_limitations.push((
format!("{} → {}", source.display_name(), target.display_name()),
test_case.clone(),
error.clone(),
));
}

for (test_case, error) in &pair_result.missing_fixture_details {
all_missing_fixtures.push((
format!("{} → {}", source.display_name(), target.display_name()),
test_case.clone(),
error.clone(),
));
}
}
}
table.push('\n');
}

(table, stats, all_failures)
(table, stats, all_failures, all_limitations, all_missing_fixtures)
}

// ============================================================================
Expand Down Expand Up @@ -259,25 +288,30 @@ pub fn generate_report(

report.push_str("## Cross-Provider Transformation Coverage\n\n");

let (req_table, req_stats, req_failures) =
let (req_table, req_stats, req_failures, req_limitations, req_missing) =
generate_table(request_results, adapters, "Request Transformations");
report.push_str(&req_table);

report.push('\n');
let (resp_table, resp_stats, resp_failures) =
let (resp_table, resp_stats, resp_failures, resp_limitations, resp_missing) =
generate_table(response_results, adapters, "Response Transformations");
report.push_str(&resp_table);

report.push('\n');
let (stream_table, stream_stats, stream_failures) = generate_table(
streaming_results,
adapters,
"Streaming Response Transformations",
);
let (stream_table, stream_stats, stream_failures, stream_limitations, stream_missing) =
generate_table(
streaming_results,
adapters,
"Streaming Response Transformations",
);
report.push_str(&stream_table);

let total_passed = req_stats.passed + resp_stats.passed + stream_stats.passed;
let total_failed = req_stats.failed + resp_stats.failed + stream_stats.failed;
let total_limitations =
req_stats.limitations + resp_stats.limitations + stream_stats.limitations;
let total_missing =
req_stats.missing_fixtures + resp_stats.missing_fixtures + stream_stats.missing_fixtures;
let total = total_passed + total_failed;

let pass_percentage = if total > 0 {
Expand All @@ -288,25 +322,37 @@ pub fn generate_report(

report.push_str("\n### Summary\n\n");
report.push_str(&format!(
"**{}/{} ({:.1}%)** - {} failed\n",
total_passed, total, pass_percentage, total_failed
"**{}/{} ({:.1}%)** - {} failed, {} limitations, {} missing fixtures\n",
total_passed, total, pass_percentage, total_failed, total_limitations, total_missing
));

let req_total = req_stats.passed + req_stats.failed;
let resp_total = resp_stats.passed + resp_stats.failed;
let stream_total = stream_stats.passed + stream_stats.failed;

report.push_str(&format!(
"\n**Requests:** {}/{} passed, {} failed\n",
req_stats.passed, req_total, req_stats.failed
"\n**Requests:** {}/{} passed, {} failed, {} limitations, {} missing\n",
req_stats.passed,
req_total,
req_stats.failed,
req_stats.limitations,
req_stats.missing_fixtures
));
report.push_str(&format!(
"**Responses:** {}/{} passed, {} failed\n",
resp_stats.passed, resp_total, resp_stats.failed
"**Responses:** {}/{} passed, {} failed, {} limitations, {} missing\n",
resp_stats.passed,
resp_total,
resp_stats.failed,
resp_stats.limitations,
resp_stats.missing_fixtures
));
report.push_str(&format!(
"**Streaming:** {}/{} passed, {} failed\n",
stream_stats.passed, stream_total, stream_stats.failed
"**Streaming:** {}/{} passed, {} failed, {} limitations, {} missing\n",
stream_stats.passed,
stream_total,
stream_stats.failed,
stream_stats.limitations,
stream_stats.missing_fixtures
));

// Organize issues by source provider → request/response/streaming → target
Expand Down Expand Up @@ -509,6 +555,114 @@ pub fn generate_report(
}
}

// Add provider limitations section
let all_limitations: Vec<_> = req_limitations
.into_iter()
.chain(resp_limitations)
.chain(stream_limitations)
.collect();

if !all_limitations.is_empty() {
report.push_str("\n### Provider Limitations\n\n");
report.push_str("These are provider-specific features that cannot be transformed:\n\n");

// Group by source provider
let mut by_source: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
for (direction, test_case, error) in all_limitations {
let source = direction
.split(" → ")
.next()
.unwrap_or(&direction)
.to_string();
by_source
.entry(source)
.or_default()
.push((direction, test_case, error));
}

let mut sources: Vec<_> = by_source.into_iter().collect();
sources.sort_by(|a, b| b.1.len().cmp(&a.1.len()));

for (source, limitations) in sources {
report.push_str("<details>\n");
report.push_str(&format!(
"<summary>⚠️ {} ({} limitations)</summary>\n\n",
source,
limitations.len()
));

// Group by target
let mut by_target: HashMap<String, Vec<(String, String)>> = HashMap::new();
for (direction, test_case, error) in limitations {
let target = direction
.split(" → ")
.nth(1)
.unwrap_or("Unknown")
.to_string();
by_target
.entry(target)
.or_default()
.push((test_case, error));
}

let mut targets: Vec<_> = by_target.into_iter().collect();
targets.sort_by(|a, b| b.1.len().cmp(&a.1.len()));

for (target, target_limitations) in targets {
report.push_str(&format!("**→ {}:**\n", target));
for (test_case, error) in target_limitations {
report.push_str(&format!(" - `{}` - {}\n", test_case, error));
}
report.push('\n');
}

report.push_str("</details>\n\n");
}
}

// Add missing fixtures section (collapsed by default)
let all_missing: Vec<_> = req_missing
.into_iter()
.chain(resp_missing)
.chain(stream_missing)
.collect();

if !all_missing.is_empty() {
report.push_str("\n### Missing Test Fixtures\n\n");
report.push_str("<details>\n");
report.push_str(&format!(
"<summary>📁 {} missing fixtures (expand to see details)</summary>\n\n",
all_missing.len()
));

// Group by source provider
let mut by_source: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
for (direction, test_case, error) in all_missing {
let source = direction
.split(" → ")
.next()
.unwrap_or(&direction)
.to_string();
by_source
.entry(source)
.or_default()
.push((direction, test_case, error));
}

let mut sources: Vec<_> = by_source.into_iter().collect();
sources.sort_by(|a, b| b.1.len().cmp(&a.1.len()));

for (source, missing) in sources {
report.push_str(&format!("**{}** ({} missing):\n", source, missing.len()));
for (_, test_case, _) in missing {
report.push_str(&format!(" - `{}`\n", test_case));
}
report.push('\n');
}

report.push_str("</details>\n");
}

// Add roundtrip section
report.push('\n');
report.push_str(&generate_roundtrip_section(roundtrip_results, adapters));
Expand Down
Loading