use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, ValueEnum};
use crate::build::platforms::tests::TestsBuilder;
use crate::build::{BuildContext, BuildOptions, PlatformBuilder};
use crate::commands::build::{BuildTarget, LinkType, WindowsToolchain};
use crate::config::CcgoConfig;
use crate::testing::coverage::{CoverageCollector, CoverageConfig, CoverageFormat};
use crate::testing::discovery::TestDiscovery;
use crate::testing::results::TestResultAggregator;
use crate::testing::ci::{CiFormat, CiReporter};
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum CoverageOutputFormat {
#[default]
Html,
Lcov,
Json,
Cobertura,
Summary,
}
impl From<CoverageOutputFormat> for CoverageFormat {
fn from(f: CoverageOutputFormat) -> Self {
match f {
CoverageOutputFormat::Html => CoverageFormat::Html,
CoverageOutputFormat::Lcov => CoverageFormat::Lcov,
CoverageOutputFormat::Json => CoverageFormat::Json,
CoverageOutputFormat::Cobertura => CoverageFormat::Cobertura,
CoverageOutputFormat::Summary => CoverageFormat::Summary,
}
}
}
#[derive(Args, Debug)]
pub struct TestCommand {
#[arg(long)]
pub filter: Option<String>,
#[arg(long)]
pub ide_project: bool,
#[arg(long)]
pub build_only: bool,
#[arg(long)]
pub run_only: bool,
#[arg(short, long)]
pub jobs: Option<usize>,
#[arg(long)]
pub release: bool,
#[arg(long)]
pub list: bool,
#[arg(long)]
pub coverage: bool,
#[arg(long, value_enum, default_value = "html")]
pub coverage_format: CoverageOutputFormat,
#[arg(long, default_value = "coverage")]
pub coverage_dir: PathBuf,
#[arg(long)]
pub coverage_threshold: Option<f64>,
#[arg(long)]
pub fail_under_coverage: bool,
#[arg(long)]
pub aggregate: bool,
#[arg(long)]
pub ci_format: Option<String>,
#[arg(long)]
pub junit_xml: Option<PathBuf>,
}
impl TestCommand {
pub fn execute(self, verbose: bool) -> Result<()> {
let config = CcgoConfig::load()?;
let project_root = std::env::current_dir()?;
let options = BuildOptions {
target: BuildTarget::Linux, architectures: vec![],
link_type: LinkType::Both,
use_docker: false,
auto_docker: false,
jobs: self.jobs,
ide_project: self.ide_project,
release: self.release,
native_only: false,
toolchain: WindowsToolchain::Auto,
verbose,
dev: false,
features: vec![],
use_default_features: true,
all_features: false,
cache: Some("auto".to_string()),
analytics: false,
};
let ctx = BuildContext::new(project_root.clone(), config, options);
let builder = TestsBuilder::new();
let release_subdir = if self.release { "release" } else { "debug" };
let build_dir = project_root
.join("cmake_build")
.join(release_subdir)
.join("tests");
if self.list {
return self.list_tests(&build_dir, verbose);
}
if self.ide_project {
return builder.generate_ide_project(&ctx);
}
if !self.run_only {
if verbose {
eprintln!("Building tests...");
}
if self.coverage {
eprintln!("📊 Coverage collection enabled");
}
builder.build(&ctx)?;
}
if !self.build_only {
if verbose {
eprintln!("Running tests...");
}
builder.run_tests(&ctx, self.filter.as_deref())?;
}
if self.aggregate {
self.aggregate_results(&build_dir, verbose)?;
}
if self.ci_format.is_some() || self.junit_xml.is_some() {
self.report_to_ci(&build_dir, verbose)?;
}
if self.coverage && !self.build_only {
self.collect_coverage(&project_root, &build_dir, verbose)?;
}
Ok(())
}
fn list_tests(&self, build_dir: &PathBuf, verbose: bool) -> Result<()> {
let discovery = TestDiscovery::new(build_dir.clone(), verbose);
println!("Discovering tests...");
let tests = if let Some(ref pattern) = self.filter {
discovery.filter(pattern)?
} else {
discovery.discover_all()?
};
discovery.print_tests(&tests);
println!("\nTests by Suite:");
let by_suite = discovery.list_by_suite()?;
for (suite, suite_tests) in by_suite {
println!(" {} ({} tests)", suite, suite_tests.len());
}
Ok(())
}
fn aggregate_results(&self, build_dir: &PathBuf, verbose: bool) -> Result<()> {
let mut aggregator = TestResultAggregator::new(verbose);
aggregator.find_results(build_dir)?;
let summary = aggregator.aggregate()?;
summary.print_summary();
if let Some(ref junit_path) = self.junit_xml {
std::fs::write(junit_path, summary.to_junit_xml())?;
eprintln!("JUnit XML written to: {}", junit_path.display());
}
if !summary.all_passed() {
anyhow::bail!("{} test(s) failed", summary.failed + summary.errors);
}
Ok(())
}
fn report_to_ci(&self, build_dir: &PathBuf, verbose: bool) -> Result<()> {
let mut aggregator = TestResultAggregator::new(verbose);
aggregator.find_results(build_dir)?;
let summary = aggregator.aggregate()?;
let format = if let Some(ref fmt) = self.ci_format {
fmt.parse::<CiFormat>()?
} else {
CiFormat::detect()
};
let reporter = CiReporter::new(format);
reporter.report_tests(&summary);
if let Some(ref junit_path) = self.junit_xml {
reporter.write_junit_xml(&summary, junit_path)?;
eprintln!("JUnit XML written to: {}", junit_path.display());
}
Ok(())
}
fn collect_coverage(
&self,
project_root: &PathBuf,
build_dir: &PathBuf,
verbose: bool,
) -> Result<()> {
eprintln!("\n📊 Collecting code coverage...");
let config = CoverageConfig {
source_dir: project_root.clone(),
build_dir: build_dir.clone(),
output_dir: self.coverage_dir.clone(),
format: self.coverage_format.into(),
threshold: self.coverage_threshold,
fail_under: self.fail_under_coverage,
..Default::default()
};
let collector = CoverageCollector::new(config, verbose);
match collector.collect() {
Ok(report) => {
report.print_summary();
if matches!(self.coverage_format, CoverageOutputFormat::Html) {
match collector.generate_html(&report) {
Ok(html_path) => {
eprintln!("\n📄 HTML report: {}", html_path.display());
#[cfg(target_os = "macos")]
{
let _ = std::process::Command::new("open")
.arg(&html_path)
.status();
}
}
Err(e) => {
eprintln!("Warning: Could not generate HTML report: {}", e);
}
}
}
if let Some(threshold) = self.coverage_threshold {
if !report.meets_threshold(threshold) {
let msg = format!(
"Coverage {:.1}% is below threshold {:.1}%",
report.line_coverage_percent(),
threshold
);
if self.fail_under_coverage {
anyhow::bail!(msg);
} else {
eprintln!("⚠️ {}", msg);
}
}
}
if self.ci_format.is_some() {
let format = if let Some(ref fmt) = self.ci_format {
fmt.parse::<CiFormat>()?
} else {
CiFormat::detect()
};
let reporter = CiReporter::new(format);
reporter.report_coverage(&report);
}
Ok(())
}
Err(e) => {
eprintln!("⚠️ Coverage collection failed: {}", e);
eprintln!(" Make sure tests were built with coverage flags:");
eprintln!(" -DCMAKE_CXX_FLAGS=\"--coverage\" -DCMAKE_C_FLAGS=\"--coverage\"");
Ok(()) }
}
}
}