Skip to content

Commit 1f31aa8

Browse files
committed
feat(xtask): add automatic panic registry update tool
1 parent f73d37e commit 1f31aa8

File tree

3 files changed

+532
-26
lines changed

3 files changed

+532
-26
lines changed

xtask/Cargo.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ wrt-integration = ["wrt"]
1212
xshell = "0.2.5"
1313

1414
# Additions for symbols task
15-
anyhow = "1.0"
16-
clap = { version = "4.5", features = ["derive", "env"] }
15+
anyhow = "1.0.75"
16+
clap = { version = "4.4.3", features = ["derive"] }
1717
rustc-demangle = "0.1"
18-
serde = { version = "1.0", features = ["derive"] }
18+
serde = { version = "1.0.188", features = ["derive"] }
1919
serde_json = "1.0"
2020
tera = "1"
2121

@@ -28,4 +28,8 @@ wrt = { path = "../wrt", optional = true }
2828
# For documentation HTTP server
2929
tiny_http = "0.12"
3030

31-
# Check for the latest compatible version
31+
# Check for the latest compatible version
32+
toml = "0.7.6"
33+
syn = { version = "2.0.34", features = ["parsing", "full", "extra-traits"] }
34+
regex = "1.9.5"
35+
chrono = "0.4.24"

xtask/src/main.rs

Lines changed: 260 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod check_panics;
1515
mod docs;
1616
mod fs_ops;
1717
mod qualification;
18+
mod update_panic_registry;
1819
mod wasm_ops;
1920
mod wast_tests;
2021

@@ -84,6 +85,16 @@ enum Command {
8485
#[command(subcommand)]
8586
command: DocsCommands,
8687
},
88+
/// Update the panic registry CSV file
89+
UpdatePanicRegistry {
90+
/// Output CSV file path (relative to workspace root)
91+
#[arg(long, default_value = "docs/source/development/panic_registry.csv")]
92+
output: String,
93+
94+
/// Whether to print verbose output
95+
#[arg(short, long)]
96+
verbose: bool,
97+
},
8798
}
8899

89100
#[derive(Subcommand, Debug)]
@@ -147,7 +158,47 @@ pub struct BuildOpts {
147158

148159
#[derive(Args, Debug)]
149160
pub struct CoverageOpts {
150-
// Add options for coverage if needed
161+
/// Mode of operation: single (run llvm-cov), individual (per-crate coverage), or combined (merge with grcov)
162+
#[clap(long, value_enum, default_value = "single")]
163+
mode: CoverageMode,
164+
165+
/// Format for coverage output
166+
#[clap(long, value_enum, default_value = "lcov")]
167+
format: CoverageFormat,
168+
169+
/// Crates to include, if not specified all will be included
170+
#[clap(long, use_value_delimiter = true, value_delimiter = ',')]
171+
crates: Vec<String>,
172+
173+
/// Exclude specific crates from coverage
174+
#[clap(long, use_value_delimiter = true, value_delimiter = ',')]
175+
exclude: Vec<String>,
176+
177+
/// Directory to store coverage artifacts
178+
#[clap(long, default_value = "target/coverage")]
179+
output_dir: PathBuf,
180+
}
181+
182+
#[derive(ValueEnum, Clone, Debug, PartialEq)]
183+
enum CoverageMode {
184+
/// Run coverage for all crates combined (default)
185+
Single,
186+
/// Run coverage for each crate individually
187+
Individual,
188+
/// Combine previously generated coverage reports with grcov
189+
Combined,
190+
}
191+
192+
#[derive(ValueEnum, Clone, Debug, PartialEq)]
193+
enum CoverageFormat {
194+
/// LCOV format (default)
195+
Lcov,
196+
/// HTML report
197+
Html,
198+
/// Cobertura XML format
199+
Cobertura,
200+
/// All formats
201+
All,
151202
}
152203

153204
#[derive(Args, Debug)]
@@ -375,6 +426,10 @@ fn main() -> Result<()> {
375426
DocsCommands::SwitcherJson { local } => docs::generate_switcher_json(local),
376427
DocsCommands::Serve => docs::serve_docs(),
377428
},
429+
Command::UpdatePanicRegistry { output, verbose } => {
430+
update_panic_registry::run(&sh, &output, verbose)?;
431+
Ok(())
432+
}
378433
}
379434
}
380435

@@ -445,45 +500,228 @@ fn run_build(sh: &Shell, opts: BuildOpts) -> Result<()> {
445500
Ok(())
446501
}
447502

448-
fn run_coverage(sh: &Shell, _opts: CoverageOpts) -> Result<()> {
449-
let lcov_path = PathBuf::from("coverage.lcov");
450-
let html_output_dir = PathBuf::from("target/llvm-cov/html");
503+
fn run_coverage(sh: &Shell, opts: CoverageOpts) -> Result<()> {
504+
// Create output directory if it doesn't exist
505+
fs_ops::mkdirp(&opts.output_dir)?;
506+
507+
match opts.mode {
508+
CoverageMode::Single => run_single_coverage(sh, &opts),
509+
CoverageMode::Individual => run_individual_coverage(sh, &opts),
510+
CoverageMode::Combined => run_combined_coverage(sh, &opts),
511+
}
512+
}
513+
514+
fn run_single_coverage(sh: &Shell, opts: &CoverageOpts) -> Result<()> {
515+
let lcov_path = opts.output_dir.join("coverage.lcov");
516+
let html_output_dir = opts.output_dir.join("html");
451517
let summary_rst_path = PathBuf::from("docs/source/_generated_coverage_summary.rst");
452518

453519
// 1. Generate LCOV data
454-
println!("Generating LCOV coverage data...");
455-
sh.cmd("cargo")
520+
println!("Generating LCOV coverage data for all crates...");
521+
let mut cmd = sh.cmd("cargo");
522+
cmd = cmd
456523
.arg("llvm-cov")
457524
.arg("test") // Run tests to generate coverage
458525
.arg("--all-features")
459-
.arg("--lcov")
460-
.arg("--output-path")
461-
.arg(&lcov_path)
462-
.run()?;
463-
println!("LCOV data generated at {}", lcov_path.display());
526+
.arg("--workspace");
464527

528+
// Add exclusions if specified
529+
for excl in &opts.exclude {
530+
cmd = cmd.arg("--exclude").arg(excl);
531+
}
532+
533+
cmd = cmd.arg("--lcov").arg("--output-path").arg(&lcov_path);
534+
535+
// Run the command but don't fail if it returns an error
536+
match cmd.run() {
537+
Ok(_) => println!("LCOV data generated at {}", lcov_path.display()),
538+
Err(e) => {
539+
println!("Warning: Failed to generate complete LCOV data: {}", e);
540+
println!("Continuing with partial coverage data if available");
541+
}
542+
}
543+
544+
// Only proceed if the LCOV file was generated
465545
if !lcov_path.exists() {
466546
anyhow::bail!("LCOV file was not generated: {}", lcov_path.display());
467547
}
468548

469-
// 2. Generate HTML report from LCOV data
549+
// 2. Generate HTML report from LCOV data if requested
550+
if opts.format == CoverageFormat::Html || opts.format == CoverageFormat::All {
551+
generate_html_report(sh, &lcov_path, &html_output_dir)?;
552+
}
553+
554+
// 3. Generate summary for documentation
555+
generate_coverage_summary(&lcov_path, &summary_rst_path)?;
556+
557+
Ok(())
558+
}
559+
560+
fn run_individual_coverage(sh: &Shell, opts: &CoverageOpts) -> Result<()> {
561+
println!("Running individual coverage for each crate...");
562+
563+
// Get list of crates in workspace
564+
let mut crates = if opts.crates.is_empty() {
565+
// Get all crates in workspace
566+
let output = sh
567+
.cmd("cargo")
568+
.arg("metadata")
569+
.arg("--format-version=1")
570+
.read()?;
571+
let metadata: serde_json::Value = serde_json::from_str(&output)?;
572+
573+
metadata["packages"]
574+
.as_array()
575+
.map(|packages| {
576+
packages
577+
.iter()
578+
.filter_map(|pkg| pkg["name"].as_str().map(String::from))
579+
.collect::<Vec<_>>()
580+
})
581+
.unwrap_or_default()
582+
} else {
583+
opts.crates.clone()
584+
};
585+
586+
// Filter out excluded crates
587+
crates.retain(|c| !opts.exclude.contains(c));
588+
589+
// Create directory for individual reports
590+
let individual_dir = opts.output_dir.join("individual");
591+
fs_ops::mkdirp(&individual_dir)?;
592+
593+
// Run coverage for each crate
594+
for crate_name in &crates {
595+
println!("Generating coverage for crate: {}", crate_name);
596+
let crate_lcov_path = individual_dir.join(format!("{}.lcov", crate_name));
597+
598+
// Run coverage for this crate
599+
let result = sh
600+
.cmd("cargo")
601+
.arg("llvm-cov")
602+
.arg("test")
603+
.arg("--all-features")
604+
.arg("--package")
605+
.arg(crate_name)
606+
.arg("--lcov")
607+
.arg("--output-path")
608+
.arg(&crate_lcov_path)
609+
.run();
610+
611+
match result {
612+
Ok(_) => println!(" ✓ Coverage generated for {}", crate_name),
613+
Err(e) => println!(" ✗ Failed to generate coverage for {}: {}", crate_name, e),
614+
}
615+
}
616+
617+
println!(
618+
"Individual coverage reports generated in {}",
619+
individual_dir.display()
620+
);
621+
Ok(())
622+
}
623+
624+
fn run_combined_coverage(sh: &Shell, opts: &CoverageOpts) -> Result<()> {
625+
println!("Combining coverage reports with grcov...");
626+
627+
// Check if grcov is installed
628+
if sh.cmd("which").arg("grcov").read().is_err() {
629+
return Err(anyhow::anyhow!(
630+
"grcov is not installed. Install with 'cargo install grcov'"
631+
));
632+
}
633+
634+
// Find all LCOV files
635+
let individual_dir = opts.output_dir.join("individual");
636+
if !individual_dir.exists() {
637+
return Err(anyhow::anyhow!(
638+
"No individual coverage reports found in {}",
639+
individual_dir.display()
640+
));
641+
}
642+
643+
// Output paths
644+
let combined_lcov = opts.output_dir.join("combined.lcov");
645+
let html_output_dir = opts.output_dir.join("html");
646+
647+
// Run grcov to combine reports
648+
println!("Merging LCOV files with grcov...");
649+
650+
// Use grcov to process the individual directory with all LCOV files
651+
let mut cmd = sh.cmd("grcov");
652+
cmd = cmd.arg(&individual_dir);
653+
654+
// Add proper output format
655+
if opts.format == CoverageFormat::Lcov || opts.format == CoverageFormat::All {
656+
cmd = cmd.arg("-t").arg("lcov").arg("-o").arg(&combined_lcov);
657+
}
658+
659+
if opts.format == CoverageFormat::Html || opts.format == CoverageFormat::All {
660+
fs_ops::mkdirp(&html_output_dir)?;
661+
cmd = cmd.arg("-t").arg("html").arg("--branch");
662+
663+
// Add exclusion pattern for assertions and derives
664+
cmd = cmd
665+
.arg("--excl-br-line")
666+
.arg("^\\s*((debug_)?assert(_eq|_ne)?!|#\\[derive\\()");
667+
668+
cmd = cmd.arg("--ignore-not-existing");
669+
cmd = cmd.arg("-o").arg(&html_output_dir);
670+
}
671+
672+
if opts.format == CoverageFormat::Cobertura || opts.format == CoverageFormat::All {
673+
let cobertura_path = opts.output_dir.join("cobertura.xml");
674+
cmd = cmd.arg("-t").arg("cobertura").arg("-o").arg(cobertura_path);
675+
}
676+
677+
// Run grcov (don't fail if it returns an error)
678+
match cmd.run() {
679+
Ok(_) => println!("Combined coverage reports successfully"),
680+
Err(e) => println!("Warning: grcov had issues combining reports: {}", e),
681+
}
682+
683+
// Generate summary for documentation if LCOV was generated
684+
if combined_lcov.exists()
685+
&& (opts.format == CoverageFormat::Lcov || opts.format == CoverageFormat::All)
686+
{
687+
let summary_rst_path = PathBuf::from("docs/source/_generated_coverage_summary.rst");
688+
generate_coverage_summary(&combined_lcov, &summary_rst_path)?;
689+
}
690+
691+
println!(
692+
"Combined coverage report generated in {}",
693+
opts.output_dir.display()
694+
);
695+
Ok(())
696+
}
697+
698+
fn generate_html_report(sh: &Shell, lcov_path: &PathBuf, html_output_dir: &PathBuf) -> Result<()> {
470699
println!("Generating HTML coverage report from LCOV data...");
471700
// Ensure the target directory exists
472-
fs_ops::mkdirp(html_output_dir.parent().unwrap())?; // Create target/llvm-cov if needed
473-
sh.cmd("cargo")
701+
fs_ops::mkdirp(html_output_dir)?;
702+
703+
let result = sh
704+
.cmd("cargo")
474705
.arg("llvm-cov")
475706
.arg("report") // Use report subcommand
476707
.arg("--lcov")
477-
.arg(&lcov_path) // Input LCOV
708+
.arg(lcov_path) // Input LCOV
478709
.arg("--html")
479710
.arg("--output-dir") // Specify output directory
480-
.arg(&html_output_dir)
481-
.run()?; // removed .arg("test") - we are reporting, not testing again
482-
println!("HTML report generated in {}", html_output_dir.display());
711+
.arg(html_output_dir)
712+
.run();
713+
714+
match result {
715+
Ok(_) => println!("HTML report generated in {}", html_output_dir.display()),
716+
Err(e) => println!("Warning: Failed to generate HTML report: {}", e),
717+
}
718+
719+
Ok(())
720+
}
483721

484-
// 3. Parse LCOV data for summary
722+
fn generate_coverage_summary(lcov_path: &PathBuf, summary_rst_path: &PathBuf) -> Result<()> {
485723
println!("Parsing LCOV data for summary...");
486-
let file = File::open(&lcov_path)
724+
let file = File::open(lcov_path)
487725
.with_context(|| format!("Failed to open LCOV file: {}", lcov_path.display()))?;
488726
let reader = BufReader::new(file);
489727

@@ -516,7 +754,7 @@ fn run_coverage(sh: &Shell, _opts: CoverageOpts) -> Result<()> {
516754
covered_lines, total_lines, percentage
517755
);
518756

519-
// 4. Generate RST summary file
757+
// Generate RST summary file
520758
println!(
521759
"Generating RST summary file: {}",
522760
summary_rst_path.display()
@@ -540,7 +778,7 @@ fn run_coverage(sh: &Shell, _opts: CoverageOpts) -> Result<()> {
540778
)
541779
})?;
542780
}
543-
fs::write(&summary_rst_path, rst_content).with_context(|| {
781+
fs::write(summary_rst_path, rst_content).with_context(|| {
544782
format!(
545783
"Failed to write RST summary file: {}",
546784
summary_rst_path.display()

0 commit comments

Comments
 (0)