mod common;
mod console;
mod csv;
mod excel;
mod html;
mod json;
mod reportable_crate;
pub use console::ConsoleOutputMode;
pub use console::generate as generate_console;
pub use csv::generate as generate_csv;
pub use excel::generate as generate_xlsx;
pub use html::generate as generate_html;
pub use json::generate as generate_json;
pub use reportable_crate::ReportableCrate;
#[cfg(test)]
mod snapshot_tests {
use super::*;
use crate::expr::{Appraisal, ExpressionDisposition, ExpressionOutcome, Risk};
use crate::metrics::{Metric, MetricCategory, MetricDef, MetricValue};
use chrono::{DateTime, Local, TimeZone, Utc};
use semver::Version;
use std::io::Cursor;
use std::sync::Arc;
fn test_timestamp() -> DateTime<Local> {
Local.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap()
}
static NAME_DEF: MetricDef = MetricDef {
name: "crate.name",
description: "Name of the crate",
category: MetricCategory::Metadata,
extractor: |_| None,
default_value: || None,
};
static VERSION_DEF: MetricDef = MetricDef {
name: "crate.version",
description: "Version of the crate",
category: MetricCategory::Metadata,
extractor: |_| None,
default_value: || None,
};
static STARS_DEF: MetricDef = MetricDef {
name: "community.repo_stars",
description: "Number of stars",
category: MetricCategory::Community,
extractor: |_| None,
default_value: || None,
};
static DOWNLOADS_DEF: MetricDef = MetricDef {
name: "usage.total_downloads",
description: "Total downloads",
category: MetricCategory::Usage,
extractor: |_| None,
default_value: || None,
};
static COVERAGE_DEF: MetricDef = MetricDef {
name: "trust.code_coverage_percentage",
description: "Code coverage percentage",
category: MetricCategory::Trustworthiness,
extractor: |_| None,
default_value: || None,
};
static CREATED_AT_DEF: MetricDef = MetricDef {
name: "stability.crate_created_at",
description: "When the crate was created",
category: MetricCategory::Stability,
extractor: |_| None,
default_value: || None,
};
static HAS_CI_DEF: MetricDef = MetricDef {
name: "trust.ci_workflows",
description: "Whether CI is configured",
category: MetricCategory::Trustworthiness,
extractor: |_| None,
default_value: || None,
};
static KEYWORDS_DEF: MetricDef = MetricDef {
name: "crate.keywords",
description: "Crate keywords",
category: MetricCategory::Metadata,
extractor: |_| None,
default_value: || None,
};
fn create_test_crates() -> Vec<ReportableCrate> {
let created_at = Utc.with_ymd_and_hms(2023, 1, 15, 10, 30, 0).unwrap();
vec![
ReportableCrate::new(
"tokio".into(),
Arc::new(Version::parse("1.35.0").unwrap()),
vec![
Metric::with_value(&NAME_DEF, MetricValue::String("tokio".into())),
Metric::with_value(&VERSION_DEF, MetricValue::String("1.35.0".into())),
Metric::with_value(&STARS_DEF, MetricValue::UInt(20000)),
Metric::with_value(&DOWNLOADS_DEF, MetricValue::UInt(50_000_000)),
Metric::with_value(&COVERAGE_DEF, MetricValue::Float(85.5)),
Metric::with_value(&CREATED_AT_DEF, MetricValue::DateTime(created_at)),
Metric::with_value(&HAS_CI_DEF, MetricValue::Boolean(true)),
Metric::with_value(
&KEYWORDS_DEF,
MetricValue::List(vec![MetricValue::String("async".into()), MetricValue::String("runtime".into())]),
),
],
Some(Appraisal::new(Risk::Low, vec![ExpressionOutcome::new("high_stars".into(), "High stars and good coverage".into(), ExpressionDisposition::True)], 1, 1, 100.0)),
),
ReportableCrate::new(
"serde".into(),
Arc::new(Version::parse("1.0.195").unwrap()),
vec![
Metric::with_value(&NAME_DEF, MetricValue::String("serde".into())),
Metric::with_value(&VERSION_DEF, MetricValue::String("1.0.195".into())),
Metric::with_value(&STARS_DEF, MetricValue::UInt(8000)),
Metric::with_value(&DOWNLOADS_DEF, MetricValue::UInt(100_000_000)),
Metric::with_value(&COVERAGE_DEF, MetricValue::Float(92.3)),
Metric::with_value(&CREATED_AT_DEF, MetricValue::DateTime(created_at)),
Metric::with_value(&HAS_CI_DEF, MetricValue::Boolean(true)),
Metric::with_value(&KEYWORDS_DEF, MetricValue::List(vec![MetricValue::String("serialization".into())])),
],
Some(Appraisal::new(Risk::High, vec![ExpressionOutcome::new("low_stars".into(), "Low star count".into(), ExpressionDisposition::False)], 1, 0, 0.0)),
),
ReportableCrate::new(
"anyhow".into(),
Arc::new(Version::parse("1.0.75").unwrap()),
vec![
Metric::with_value(&NAME_DEF, MetricValue::String("anyhow".into())),
Metric::with_value(&VERSION_DEF, MetricValue::String("1.0.75".into())),
Metric::with_value(&STARS_DEF, MetricValue::UInt(4500)),
Metric::with_value(&DOWNLOADS_DEF, MetricValue::UInt(30_000_000)),
Metric::with_value(&COVERAGE_DEF, MetricValue::Float(78.9)),
Metric::with_value(&CREATED_AT_DEF, MetricValue::DateTime(created_at)),
Metric::with_value(&HAS_CI_DEF, MetricValue::Boolean(false)),
Metric::with_value(
&KEYWORDS_DEF,
MetricValue::List(vec![MetricValue::String("error".into()), MetricValue::String("handling".into())]),
),
],
None,
),
]
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_console_report_with_colors() {
let crates = create_test_crates();
let mut output = String::new();
generate_console(&crates, true, &ConsoleOutputMode::full(), &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_console_report_no_colors() {
let crates = create_test_crates();
let mut output = String::new();
generate_console(&crates, false, &ConsoleOutputMode::full(), &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_csv_report() {
let crates = create_test_crates();
let mut output = String::new();
generate_csv(&crates, &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_html_report() {
let crates = create_test_crates();
let mut output = String::new();
generate_html(&crates, test_timestamp(), &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_json_report() {
let crates = create_test_crates();
let mut output = String::new();
generate_json(&crates, &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_excel_report() {
let crates = create_test_crates();
let mut output = Cursor::new(Vec::new());
generate_xlsx(&crates, &mut output).unwrap();
let bytes = output.into_inner();
assert!(!bytes.is_empty(), "Excel output should not be empty");
assert!(bytes.len() > 1000, "Excel output should be substantial");
assert_eq!(&bytes[0..2], b"PK", "Excel file should be a valid ZIP archive");
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_empty_crates_console() {
let crates: Vec<ReportableCrate> = vec![];
let mut output = String::new();
generate_console(&crates, false, &ConsoleOutputMode::full(), &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_empty_crates_csv() {
let crates: Vec<ReportableCrate> = vec![];
let mut output = String::new();
generate_csv(&crates, &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_empty_crates_json() {
let crates: Vec<ReportableCrate> = vec![];
let mut output = String::new();
generate_json(&crates, &mut output).unwrap();
insta::assert_snapshot!(output);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetModuleFileNameW")]
fn test_single_crate_all_metrics() {
let created_at = Utc.with_ymd_and_hms(2023, 6, 1, 12, 0, 0).unwrap();
let crate_with_all_metrics = ReportableCrate::new(
"comprehensive-crate".into(),
Arc::new(Version::parse("2.0.0").unwrap()),
vec![
Metric::with_value(&NAME_DEF, MetricValue::String("comprehensive-crate".into())),
Metric::with_value(&VERSION_DEF, MetricValue::String("2.0.0".into())),
Metric::with_value(&STARS_DEF, MetricValue::UInt(12345)),
Metric::with_value(&DOWNLOADS_DEF, MetricValue::UInt(9_876_543)),
Metric::with_value(&COVERAGE_DEF, MetricValue::Float(99.99)),
Metric::with_value(&CREATED_AT_DEF, MetricValue::DateTime(created_at)),
Metric::with_value(&HAS_CI_DEF, MetricValue::Boolean(true)),
Metric::with_value(
&KEYWORDS_DEF,
MetricValue::List(vec![
MetricValue::String("test".into()),
MetricValue::String("comprehensive".into()),
MetricValue::String("snapshot".into()),
]),
),
],
Some(Appraisal::new(
Risk::Low,
vec![
ExpressionOutcome::new("coverage".into(), "Excellent coverage".into(), ExpressionDisposition::True),
ExpressionOutcome::new("active".into(), "Active development".into(), ExpressionDisposition::True),
ExpressionOutcome::new("maintained".into(), "Well maintained".into(), ExpressionDisposition::True),
],
3,
3,
100.0,
)),
);
let crates = vec![crate_with_all_metrics];
let mut console_output = String::new();
generate_console(&crates, false, &ConsoleOutputMode::full(), &mut console_output).unwrap();
insta::assert_snapshot!("single_crate_console", console_output);
let mut csv_output = String::new();
generate_csv(&crates, &mut csv_output).unwrap();
insta::assert_snapshot!("single_crate_csv", csv_output);
let mut json_output = String::new();
generate_json(&crates, &mut json_output).unwrap();
insta::assert_snapshot!("single_crate_json", json_output);
}
}