#![allow(
clippy::derive_partial_eq_without_eq,
clippy::doc_markdown,
clippy::missing_const_for_fn
)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum Grade {
#[default]
F,
D,
CMinus,
C,
CPlus,
BMinus,
B,
BPlus,
AMinus,
A,
APlus,
}
impl PartialOrd for Grade {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Grade {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.min_percentage()
.partial_cmp(&other.min_percentage())
.unwrap_or(std::cmp::Ordering::Equal)
}
}
impl Grade {
#[must_use]
pub fn from_percentage(percent: f32) -> Self {
match percent {
p if p >= 95.0 => Self::APlus,
p if p >= 90.0 => Self::A,
p if p >= 85.0 => Self::AMinus,
p if p >= 80.0 => Self::BPlus,
p if p >= 75.0 => Self::B,
p if p >= 70.0 => Self::BMinus,
p if p >= 65.0 => Self::CPlus,
p if p >= 60.0 => Self::C,
p if p >= 55.0 => Self::CMinus,
p if p >= 50.0 => Self::D,
_ => Self::F,
}
}
#[must_use]
pub const fn min_percentage(&self) -> f32 {
match self {
Self::APlus => 95.0,
Self::A => 90.0,
Self::AMinus => 85.0,
Self::BPlus => 80.0,
Self::B => 75.0,
Self::BMinus => 70.0,
Self::CPlus => 65.0,
Self::C => 60.0,
Self::CMinus => 55.0,
Self::D => 50.0,
Self::F => 0.0,
}
}
#[must_use]
pub const fn letter(&self) -> &'static str {
match self {
Self::APlus => "A+",
Self::A => "A",
Self::AMinus => "A-",
Self::BPlus => "B+",
Self::B => "B",
Self::BMinus => "B-",
Self::CPlus => "C+",
Self::C => "C",
Self::CMinus => "C-",
Self::D => "D",
Self::F => "F",
}
}
#[must_use]
pub const fn is_passing(&self) -> bool {
matches!(
self,
Self::APlus
| Self::A
| Self::AMinus
| Self::BPlus
| Self::B
| Self::BMinus
| Self::CPlus
| Self::C
)
}
#[must_use]
pub const fn is_production_ready(&self) -> bool {
matches!(self, Self::APlus | Self::A | Self::AMinus | Self::BPlus)
}
}
impl std::fmt::Display for Grade {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.letter())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Criterion {
pub name: String,
pub description: String,
pub weight: f32,
pub score: f32,
pub passed: bool,
pub feedback: Option<String>,
}
impl Criterion {
#[must_use]
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
weight: 1.0,
score: 0.0,
passed: false,
feedback: None,
}
}
#[must_use]
pub fn weight(mut self, weight: f32) -> Self {
self.weight = weight.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn score(mut self, score: f32) -> Self {
self.score = score.clamp(0.0, 100.0);
self.passed = self.score >= 60.0;
self
}
#[must_use]
pub const fn pass(mut self) -> Self {
self.score = 100.0;
self.passed = true;
self
}
#[must_use]
pub const fn fail(mut self) -> Self {
self.score = 0.0;
self.passed = false;
self
}
#[must_use]
pub fn feedback(mut self, feedback: impl Into<String>) -> Self {
self.feedback = Some(feedback.into());
self
}
#[must_use]
pub fn grade(&self) -> Grade {
Grade::from_percentage(self.score)
}
#[must_use]
pub fn weighted_score(&self) -> f32 {
self.score * self.weight
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ReportCard {
pub title: String,
pub criteria: Vec<Criterion>,
pub categories: HashMap<String, f32>,
}
impl ReportCard {
#[must_use]
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
criteria: Vec::new(),
categories: HashMap::new(),
}
}
pub fn add_criterion(&mut self, criterion: Criterion) {
self.criteria.push(criterion);
}
#[must_use]
pub fn criterion(mut self, criterion: Criterion) -> Self {
self.criteria.push(criterion);
self
}
pub fn add_category(&mut self, name: impl Into<String>, score: f32) {
self.categories.insert(name.into(), score.clamp(0.0, 100.0));
}
#[must_use]
pub fn overall_score(&self) -> f32 {
if self.criteria.is_empty() {
return 0.0;
}
let total_weight: f32 = self.criteria.iter().map(|c| c.weight).sum();
if total_weight == 0.0 {
return 0.0;
}
let weighted_sum: f32 = self.criteria.iter().map(Criterion::weighted_score).sum();
weighted_sum / total_weight
}
#[must_use]
pub fn overall_grade(&self) -> Grade {
Grade::from_percentage(self.overall_score())
}
#[must_use]
pub fn all_passed(&self) -> bool {
self.criteria.iter().all(|c| c.passed)
}
#[must_use]
pub fn passed_count(&self) -> usize {
self.criteria.iter().filter(|c| c.passed).count()
}
#[must_use]
pub fn failed_count(&self) -> usize {
self.criteria.iter().filter(|c| !c.passed).count()
}
#[must_use]
pub fn failures(&self) -> Vec<&Criterion> {
self.criteria.iter().filter(|c| !c.passed).collect()
}
#[must_use]
pub fn is_passing(&self) -> bool {
self.overall_grade().is_passing()
}
}
pub mod categories {
pub const ACCESSIBILITY: &str = "accessibility";
pub const PERFORMANCE: &str = "performance";
pub const VISUAL: &str = "visual";
pub const CODE_QUALITY: &str = "code_quality";
pub const TESTING: &str = "testing";
pub const DOCUMENTATION: &str = "documentation";
pub const SECURITY: &str = "security";
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ScoreBreakdown {
pub widget_complexity: f64,
pub layout_depth: f64,
pub component_count: f64,
pub render_time_p95: f64,
pub memory_usage: f64,
pub bundle_size: f64,
pub wcag_aa_compliance: f64,
pub keyboard_navigation: f64,
pub screen_reader: f64,
pub data_completeness: f64,
pub data_freshness: f64,
pub schema_validation: f64,
pub manifest_completeness: f64,
pub card_coverage: f64,
pub theme_adherence: f64,
pub naming_conventions: f64,
}
impl ScoreBreakdown {
#[must_use]
pub fn structural_score(&self) -> f64 {
(self.widget_complexity + self.layout_depth + self.component_count) / 3.0 * 0.25
}
#[must_use]
pub fn performance_score(&self) -> f64 {
(self.render_time_p95 + self.memory_usage + self.bundle_size) / 3.0 * 0.20
}
#[must_use]
pub fn accessibility_score(&self) -> f64 {
(self.wcag_aa_compliance + self.keyboard_navigation + self.screen_reader) / 3.0 * 0.20
}
#[must_use]
pub fn data_quality_score(&self) -> f64 {
(self.data_completeness + self.data_freshness + self.schema_validation) / 3.0 * 0.15
}
#[must_use]
pub fn documentation_score(&self) -> f64 {
(self.manifest_completeness + self.card_coverage) / 2.0 * 0.10
}
#[must_use]
pub fn consistency_score(&self) -> f64 {
(self.theme_adherence + self.naming_conventions) / 2.0 * 0.10
}
#[must_use]
pub fn total(&self) -> f64 {
self.structural_score()
+ self.performance_score()
+ self.accessibility_score()
+ self.data_quality_score()
+ self.documentation_score()
+ self.consistency_score()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AppQualityScore {
pub overall: f64,
pub grade: Grade,
pub breakdown: ScoreBreakdown,
}
impl AppQualityScore {
#[must_use]
pub fn from_breakdown(breakdown: ScoreBreakdown) -> Self {
let overall = breakdown.total();
let grade = Grade::from_percentage(overall as f32);
Self {
overall,
grade,
breakdown,
}
}
#[must_use]
pub fn meets_minimum(&self, min_grade: Grade) -> bool {
self.grade >= min_grade
}
#[must_use]
pub fn is_production_ready(&self) -> bool {
self.grade.is_production_ready()
}
}
#[derive(Debug, Clone, Default)]
pub struct QualityScoreBuilder {
breakdown: ScoreBreakdown,
}
impl QualityScoreBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn structural(mut self, complexity: f64, depth: f64, count: f64) -> Self {
self.breakdown.widget_complexity = complexity.clamp(0.0, 100.0);
self.breakdown.layout_depth = depth.clamp(0.0, 100.0);
self.breakdown.component_count = count.clamp(0.0, 100.0);
self
}
#[must_use]
pub fn performance(mut self, render_time: f64, memory: f64, bundle: f64) -> Self {
self.breakdown.render_time_p95 = render_time.clamp(0.0, 100.0);
self.breakdown.memory_usage = memory.clamp(0.0, 100.0);
self.breakdown.bundle_size = bundle.clamp(0.0, 100.0);
self
}
#[must_use]
pub fn accessibility(mut self, wcag: f64, keyboard: f64, screen_reader: f64) -> Self {
self.breakdown.wcag_aa_compliance = wcag.clamp(0.0, 100.0);
self.breakdown.keyboard_navigation = keyboard.clamp(0.0, 100.0);
self.breakdown.screen_reader = screen_reader.clamp(0.0, 100.0);
self
}
#[must_use]
pub fn data_quality(mut self, completeness: f64, freshness: f64, schema: f64) -> Self {
self.breakdown.data_completeness = completeness.clamp(0.0, 100.0);
self.breakdown.data_freshness = freshness.clamp(0.0, 100.0);
self.breakdown.schema_validation = schema.clamp(0.0, 100.0);
self
}
#[must_use]
pub fn documentation(mut self, manifest: f64, cards: f64) -> Self {
self.breakdown.manifest_completeness = manifest.clamp(0.0, 100.0);
self.breakdown.card_coverage = cards.clamp(0.0, 100.0);
self
}
#[must_use]
pub fn consistency(mut self, theme: f64, naming: f64) -> Self {
self.breakdown.theme_adherence = theme.clamp(0.0, 100.0);
self.breakdown.naming_conventions = naming.clamp(0.0, 100.0);
self
}
#[must_use]
pub fn build(self) -> AppQualityScore {
AppQualityScore::from_breakdown(self.breakdown)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QualityGates {
pub min_grade: Grade,
pub min_score: f64,
pub performance: PerformanceGates,
pub accessibility: AccessibilityGates,
pub data: DataGates,
pub documentation: DocumentationGates,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerformanceGates {
pub max_render_time_ms: u32,
pub max_bundle_size_kb: u32,
pub max_memory_mb: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AccessibilityGates {
pub wcag_level: String,
pub min_contrast_ratio: f32,
pub require_keyboard_nav: bool,
pub require_aria_labels: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataGates {
pub max_staleness_minutes: u32,
pub require_schema_validation: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DocumentationGates {
pub require_model_cards: bool,
pub require_data_cards: bool,
pub min_manifest_fields: Vec<String>,
}
impl Default for QualityGates {
fn default() -> Self {
Self {
min_grade: Grade::BPlus,
min_score: 80.0,
performance: PerformanceGates::default(),
accessibility: AccessibilityGates::default(),
data: DataGates::default(),
documentation: DocumentationGates::default(),
}
}
}
impl Default for PerformanceGates {
fn default() -> Self {
Self {
max_render_time_ms: 16,
max_bundle_size_kb: 500,
max_memory_mb: 100,
}
}
}
impl Default for AccessibilityGates {
fn default() -> Self {
Self {
wcag_level: "AA".to_string(),
min_contrast_ratio: 4.5,
require_keyboard_nav: true,
require_aria_labels: true,
}
}
}
impl Default for DataGates {
fn default() -> Self {
Self {
max_staleness_minutes: 60,
require_schema_validation: true,
}
}
}
impl Default for DocumentationGates {
fn default() -> Self {
Self {
require_model_cards: true,
require_data_cards: true,
min_manifest_fields: vec![
"name".to_string(),
"version".to_string(),
"description".to_string(),
],
}
}
}
#[derive(Debug, Clone)]
pub struct GateCheckResult {
pub passed: bool,
pub violations: Vec<GateViolation>,
}
#[derive(Debug, Clone)]
pub struct GateViolation {
pub gate: String,
pub expected: String,
pub actual: String,
pub severity: ViolationSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationSeverity {
Warning,
Error,
}
impl QualityGates {
#[must_use]
pub fn check(&self, score: &AppQualityScore) -> GateCheckResult {
let mut violations = Vec::new();
if score.grade < self.min_grade {
violations.push(GateViolation {
gate: "min_grade".to_string(),
expected: self.min_grade.letter().to_string(),
actual: score.grade.letter().to_string(),
severity: ViolationSeverity::Error,
});
}
if score.overall < self.min_score {
violations.push(GateViolation {
gate: "min_score".to_string(),
expected: format!("{:.1}", self.min_score),
actual: format!("{:.1}", score.overall),
severity: ViolationSeverity::Error,
});
}
GateCheckResult {
passed: violations.is_empty(),
violations,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EvaluationBuilder {
report: ReportCard,
}
impl EvaluationBuilder {
#[must_use]
pub fn new(title: impl Into<String>) -> Self {
Self {
report: ReportCard::new(title),
}
}
#[must_use]
pub fn accessibility(mut self, score: f32, feedback: Option<&str>) -> Self {
let mut criterion = Criterion::new(
"Accessibility",
"WCAG 2.1 AA compliance and screen reader support",
)
.weight(1.0)
.score(score);
if let Some(fb) = feedback {
criterion = criterion.feedback(fb);
}
self.report.add_criterion(criterion);
self.report
.add_category(categories::ACCESSIBILITY.to_string(), score);
self
}
#[must_use]
pub fn performance(mut self, score: f32, feedback: Option<&str>) -> Self {
let mut criterion = Criterion::new(
"Performance",
"Frame rate, memory usage, and responsiveness",
)
.weight(1.0)
.score(score);
if let Some(fb) = feedback {
criterion = criterion.feedback(fb);
}
self.report.add_criterion(criterion);
self.report
.add_category(categories::PERFORMANCE.to_string(), score);
self
}
#[must_use]
pub fn visual(mut self, score: f32, feedback: Option<&str>) -> Self {
let mut criterion =
Criterion::new("Visual Consistency", "Theme adherence and visual polish")
.weight(0.8)
.score(score);
if let Some(fb) = feedback {
criterion = criterion.feedback(fb);
}
self.report.add_criterion(criterion);
self.report
.add_category(categories::VISUAL.to_string(), score);
self
}
#[must_use]
pub fn code_quality(mut self, score: f32, feedback: Option<&str>) -> Self {
let mut criterion = Criterion::new(
"Code Quality",
"Lint compliance, documentation, and maintainability",
)
.weight(0.8)
.score(score);
if let Some(fb) = feedback {
criterion = criterion.feedback(fb);
}
self.report.add_criterion(criterion);
self.report
.add_category(categories::CODE_QUALITY.to_string(), score);
self
}
#[must_use]
pub fn testing(mut self, score: f32, feedback: Option<&str>) -> Self {
let mut criterion = Criterion::new("Testing", "Test coverage and mutation testing score")
.weight(1.0)
.score(score);
if let Some(fb) = feedback {
criterion = criterion.feedback(fb);
}
self.report.add_criterion(criterion);
self.report
.add_category(categories::TESTING.to_string(), score);
self
}
#[must_use]
pub fn custom(mut self, criterion: Criterion) -> Self {
self.report.add_criterion(criterion);
self
}
#[must_use]
pub fn build(self) -> ReportCard {
self.report
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum GateConfigError {
ParseError(String),
IoError(String),
InvalidValue(String),
}
impl std::fmt::Display for GateConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseError(msg) => write!(f, "parse error: {msg}"),
Self::IoError(msg) => write!(f, "IO error: {msg}"),
Self::InvalidValue(msg) => write!(f, "invalid value: {msg}"),
}
}
}
impl std::error::Error for GateConfigError {}
impl QualityGates {
pub const CONFIG_FILE: &'static str = ".presentar-gates.toml";
pub fn from_toml(toml_str: &str) -> Result<Self, GateConfigError> {
toml::from_str(toml_str).map_err(|e| GateConfigError::ParseError(e.to_string()))
}
#[must_use]
pub fn to_toml(&self) -> String {
toml::to_string_pretty(self).unwrap_or_default()
}
pub fn load_from_file(path: &std::path::Path) -> Result<Self, GateConfigError> {
let contents =
std::fs::read_to_string(path).map_err(|e| GateConfigError::IoError(e.to_string()))?;
Self::from_toml(&contents)
}
pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), GateConfigError> {
let contents = self.to_toml();
std::fs::write(path, contents).map_err(|e| GateConfigError::IoError(e.to_string()))
}
#[must_use]
pub fn load_default() -> Self {
let path = std::path::Path::new(Self::CONFIG_FILE);
Self::load_from_file(path).unwrap_or_default()
}
#[must_use]
pub fn check_extended(
&self,
score: &AppQualityScore,
render_time_ms: Option<u32>,
bundle_size_kb: Option<u32>,
memory_mb: Option<u32>,
) -> GateCheckResult {
let mut result = self.check(score);
if let Some(render_time) = render_time_ms {
if render_time > self.performance.max_render_time_ms {
result.violations.push(GateViolation {
gate: "max_render_time_ms".to_string(),
expected: format!("<= {}ms", self.performance.max_render_time_ms),
actual: format!("{}ms", render_time),
severity: ViolationSeverity::Error,
});
}
}
if let Some(bundle) = bundle_size_kb {
if bundle > self.performance.max_bundle_size_kb {
result.violations.push(GateViolation {
gate: "max_bundle_size_kb".to_string(),
expected: format!("<= {}KB", self.performance.max_bundle_size_kb),
actual: format!("{}KB", bundle),
severity: ViolationSeverity::Error,
});
}
}
if let Some(memory) = memory_mb {
if memory > self.performance.max_memory_mb {
result.violations.push(GateViolation {
gate: "max_memory_mb".to_string(),
expected: format!("<= {}MB", self.performance.max_memory_mb),
actual: format!("{}MB", memory),
severity: ViolationSeverity::Warning,
});
}
}
result.passed = result
.violations
.iter()
.all(|v| v.severity != ViolationSeverity::Error);
result
}
#[must_use]
pub fn sample_config() -> String {
r#"# Presentar Quality Gates Configuration
# Place this file at .presentar-gates.toml in your project root
# Minimum required grade (F, D, C, C+, B-, B, B+, A-, A, A+)
min_grade = "B+"
# Minimum required score (0-100)
min_score = 80.0
[performance]
# Maximum render time in milliseconds (60fps = 16ms)
max_render_time_ms = 16
# Maximum bundle size in KB
max_bundle_size_kb = 500
# Maximum memory usage in MB
max_memory_mb = 100
[accessibility]
# WCAG level: "A", "AA", or "AAA"
wcag_level = "AA"
# Minimum contrast ratio
min_contrast_ratio = 4.5
# Require full keyboard navigation
require_keyboard_nav = true
# Require ARIA labels
require_aria_labels = true
[data]
# Maximum data staleness in minutes
max_staleness_minutes = 60
# Require schema validation
require_schema_validation = true
[documentation]
# Require model cards for ML models
require_model_cards = true
# Require data cards for datasets
require_data_cards = true
# Minimum required manifest fields
min_manifest_fields = ["name", "version", "description"]
"#
.to_string()
}
}
impl Grade {
pub fn from_str(s: &str) -> Result<Self, GateConfigError> {
match s.trim().to_uppercase().as_str() {
"A+" => Ok(Self::APlus),
"A" => Ok(Self::A),
"A-" => Ok(Self::AMinus),
"B+" => Ok(Self::BPlus),
"B" => Ok(Self::B),
"B-" => Ok(Self::BMinus),
"C+" => Ok(Self::CPlus),
"C" => Ok(Self::C),
"C-" => Ok(Self::CMinus),
"D" => Ok(Self::D),
"F" => Ok(Self::F),
_ => Err(GateConfigError::InvalidValue(format!(
"Invalid grade: {s}. Valid values: A+, A, A-, B+, B, B-, C+, C, C-, D, F"
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grade_from_percentage() {
assert_eq!(Grade::from_percentage(100.0), Grade::APlus);
assert_eq!(Grade::from_percentage(95.0), Grade::APlus);
assert_eq!(Grade::from_percentage(92.0), Grade::A);
assert_eq!(Grade::from_percentage(90.0), Grade::A);
assert_eq!(Grade::from_percentage(87.0), Grade::AMinus);
assert_eq!(Grade::from_percentage(85.0), Grade::AMinus);
assert_eq!(Grade::from_percentage(82.0), Grade::BPlus);
assert_eq!(Grade::from_percentage(80.0), Grade::BPlus);
assert_eq!(Grade::from_percentage(77.0), Grade::B);
assert_eq!(Grade::from_percentage(75.0), Grade::B);
assert_eq!(Grade::from_percentage(72.0), Grade::BMinus);
assert_eq!(Grade::from_percentage(70.0), Grade::BMinus);
assert_eq!(Grade::from_percentage(67.0), Grade::CPlus);
assert_eq!(Grade::from_percentage(65.0), Grade::CPlus);
assert_eq!(Grade::from_percentage(62.0), Grade::C);
assert_eq!(Grade::from_percentage(60.0), Grade::C);
assert_eq!(Grade::from_percentage(57.0), Grade::CMinus);
assert_eq!(Grade::from_percentage(55.0), Grade::CMinus);
assert_eq!(Grade::from_percentage(52.0), Grade::D);
assert_eq!(Grade::from_percentage(50.0), Grade::D);
assert_eq!(Grade::from_percentage(49.0), Grade::F);
assert_eq!(Grade::from_percentage(0.0), Grade::F);
}
#[test]
fn test_grade_min_percentage() {
assert_eq!(Grade::APlus.min_percentage(), 95.0);
assert_eq!(Grade::A.min_percentage(), 90.0);
assert_eq!(Grade::AMinus.min_percentage(), 85.0);
assert_eq!(Grade::BPlus.min_percentage(), 80.0);
assert_eq!(Grade::B.min_percentage(), 75.0);
assert_eq!(Grade::BMinus.min_percentage(), 70.0);
assert_eq!(Grade::CPlus.min_percentage(), 65.0);
assert_eq!(Grade::C.min_percentage(), 60.0);
assert_eq!(Grade::CMinus.min_percentage(), 55.0);
assert_eq!(Grade::D.min_percentage(), 50.0);
assert_eq!(Grade::F.min_percentage(), 0.0);
}
#[test]
fn test_grade_letter() {
assert_eq!(Grade::APlus.letter(), "A+");
assert_eq!(Grade::A.letter(), "A");
assert_eq!(Grade::AMinus.letter(), "A-");
assert_eq!(Grade::BPlus.letter(), "B+");
assert_eq!(Grade::B.letter(), "B");
assert_eq!(Grade::BMinus.letter(), "B-");
assert_eq!(Grade::CPlus.letter(), "C+");
assert_eq!(Grade::C.letter(), "C");
assert_eq!(Grade::CMinus.letter(), "C-");
assert_eq!(Grade::D.letter(), "D");
assert_eq!(Grade::F.letter(), "F");
}
#[test]
fn test_grade_is_passing() {
assert!(Grade::APlus.is_passing());
assert!(Grade::A.is_passing());
assert!(Grade::AMinus.is_passing());
assert!(Grade::BPlus.is_passing());
assert!(Grade::B.is_passing());
assert!(Grade::BMinus.is_passing());
assert!(Grade::CPlus.is_passing());
assert!(Grade::C.is_passing());
assert!(!Grade::CMinus.is_passing());
assert!(!Grade::D.is_passing());
assert!(!Grade::F.is_passing());
}
#[test]
fn test_grade_is_production_ready() {
assert!(Grade::APlus.is_production_ready());
assert!(Grade::A.is_production_ready());
assert!(Grade::AMinus.is_production_ready());
assert!(Grade::BPlus.is_production_ready());
assert!(!Grade::B.is_production_ready());
assert!(!Grade::BMinus.is_production_ready());
assert!(!Grade::F.is_production_ready());
}
#[test]
fn test_grade_default() {
assert_eq!(Grade::default(), Grade::F);
}
#[test]
fn test_grade_display() {
assert_eq!(format!("{}", Grade::APlus), "A+");
assert_eq!(format!("{}", Grade::A), "A");
assert_eq!(format!("{}", Grade::F), "F");
}
#[test]
fn test_grade_ordering() {
assert!(Grade::APlus > Grade::A);
assert!(Grade::A > Grade::AMinus);
assert!(Grade::AMinus > Grade::BPlus);
assert!(Grade::BPlus > Grade::B);
assert!(Grade::B > Grade::BMinus);
assert!(Grade::BMinus > Grade::CPlus);
assert!(Grade::CPlus > Grade::C);
assert!(Grade::C > Grade::CMinus);
assert!(Grade::CMinus > Grade::D);
assert!(Grade::D > Grade::F);
}
#[test]
fn test_criterion_new() {
let c = Criterion::new("Test", "Description");
assert_eq!(c.name, "Test");
assert_eq!(c.description, "Description");
assert_eq!(c.weight, 1.0);
assert_eq!(c.score, 0.0);
assert!(!c.passed);
}
#[test]
fn test_criterion_weight() {
let c = Criterion::new("Test", "Desc").weight(0.5);
assert_eq!(c.weight, 0.5);
}
#[test]
fn test_criterion_weight_clamped() {
let c1 = Criterion::new("Test", "Desc").weight(2.0);
assert_eq!(c1.weight, 1.0);
let c2 = Criterion::new("Test", "Desc").weight(-1.0);
assert_eq!(c2.weight, 0.0);
}
#[test]
fn test_criterion_score() {
let c = Criterion::new("Test", "Desc").score(85.0);
assert_eq!(c.score, 85.0);
assert!(c.passed);
}
#[test]
fn test_criterion_score_failing() {
let c = Criterion::new("Test", "Desc").score(50.0);
assert_eq!(c.score, 50.0);
assert!(!c.passed);
}
#[test]
fn test_criterion_score_clamped() {
let c1 = Criterion::new("Test", "Desc").score(150.0);
assert_eq!(c1.score, 100.0);
let c2 = Criterion::new("Test", "Desc").score(-10.0);
assert_eq!(c2.score, 0.0);
}
#[test]
fn test_criterion_pass() {
let c = Criterion::new("Test", "Desc").pass();
assert_eq!(c.score, 100.0);
assert!(c.passed);
}
#[test]
fn test_criterion_fail() {
let c = Criterion::new("Test", "Desc").fail();
assert_eq!(c.score, 0.0);
assert!(!c.passed);
}
#[test]
fn test_criterion_feedback() {
let c = Criterion::new("Test", "Desc").feedback("Good work!");
assert_eq!(c.feedback, Some("Good work!".to_string()));
}
#[test]
fn test_criterion_grade() {
assert_eq!(Criterion::new("T", "D").score(95.0).grade(), Grade::APlus);
assert_eq!(Criterion::new("T", "D").score(90.0).grade(), Grade::A);
assert_eq!(Criterion::new("T", "D").score(85.0).grade(), Grade::AMinus);
assert_eq!(Criterion::new("T", "D").score(80.0).grade(), Grade::BPlus);
assert_eq!(Criterion::new("T", "D").score(75.0).grade(), Grade::B);
assert_eq!(Criterion::new("T", "D").score(70.0).grade(), Grade::BMinus);
assert_eq!(Criterion::new("T", "D").score(65.0).grade(), Grade::CPlus);
assert_eq!(Criterion::new("T", "D").score(60.0).grade(), Grade::C);
assert_eq!(Criterion::new("T", "D").score(55.0).grade(), Grade::CMinus);
assert_eq!(Criterion::new("T", "D").score(50.0).grade(), Grade::D);
assert_eq!(Criterion::new("T", "D").score(40.0).grade(), Grade::F);
}
#[test]
fn test_criterion_weighted_score() {
let c = Criterion::new("Test", "Desc").weight(0.5).score(80.0);
assert_eq!(c.weighted_score(), 40.0);
}
#[test]
fn test_report_card_new() {
let report = ReportCard::new("My Report");
assert_eq!(report.title, "My Report");
assert!(report.criteria.is_empty());
}
#[test]
fn test_report_card_add_criterion() {
let mut report = ReportCard::new("Test");
report.add_criterion(Criterion::new("C1", "D1").score(90.0));
assert_eq!(report.criteria.len(), 1);
}
#[test]
fn test_report_card_builder() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").score(90.0))
.criterion(Criterion::new("C2", "D2").score(80.0));
assert_eq!(report.criteria.len(), 2);
}
#[test]
fn test_report_card_overall_score_empty() {
let report = ReportCard::new("Test");
assert_eq!(report.overall_score(), 0.0);
}
#[test]
fn test_report_card_overall_score_equal_weights() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").weight(1.0).score(100.0))
.criterion(Criterion::new("C2", "D2").weight(1.0).score(80.0));
assert_eq!(report.overall_score(), 90.0);
}
#[test]
fn test_report_card_overall_score_different_weights() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").weight(0.75).score(100.0))
.criterion(Criterion::new("C2", "D2").weight(0.25).score(80.0));
assert_eq!(report.overall_score(), 95.0);
}
#[test]
fn test_report_card_overall_grade() {
let report = ReportCard::new("Test").criterion(Criterion::new("C1", "D1").score(90.0));
assert_eq!(report.overall_grade(), Grade::A);
}
#[test]
fn test_report_card_all_passed() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").pass())
.criterion(Criterion::new("C2", "D2").pass());
assert!(report.all_passed());
}
#[test]
fn test_report_card_not_all_passed() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").pass())
.criterion(Criterion::new("C2", "D2").fail());
assert!(!report.all_passed());
}
#[test]
fn test_report_card_passed_count() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").pass())
.criterion(Criterion::new("C2", "D2").pass())
.criterion(Criterion::new("C3", "D3").fail());
assert_eq!(report.passed_count(), 2);
assert_eq!(report.failed_count(), 1);
}
#[test]
fn test_report_card_failures() {
let report = ReportCard::new("Test")
.criterion(Criterion::new("C1", "D1").pass())
.criterion(Criterion::new("C2", "D2").fail());
let failures = report.failures();
assert_eq!(failures.len(), 1);
assert_eq!(failures[0].name, "C2");
}
#[test]
fn test_report_card_is_passing() {
let passing = ReportCard::new("Test").criterion(Criterion::new("C1", "D1").score(90.0));
assert!(passing.is_passing());
let failing = ReportCard::new("Test").criterion(Criterion::new("C1", "D1").score(50.0));
assert!(!failing.is_passing());
}
#[test]
fn test_report_card_add_category() {
let mut report = ReportCard::new("Test");
report.add_category("performance", 95.0);
assert_eq!(report.categories.get("performance"), Some(&95.0));
}
#[test]
fn test_evaluation_builder_new() {
let builder = EvaluationBuilder::new("My Eval");
let report = builder.build();
assert_eq!(report.title, "My Eval");
}
#[test]
fn test_evaluation_builder_accessibility() {
let report = EvaluationBuilder::new("Test")
.accessibility(95.0, Some("Good a11y"))
.build();
assert_eq!(report.criteria.len(), 1);
assert_eq!(report.criteria[0].name, "Accessibility");
assert_eq!(report.criteria[0].score, 95.0);
assert_eq!(
report.categories.get(categories::ACCESSIBILITY),
Some(&95.0)
);
}
#[test]
fn test_evaluation_builder_performance() {
let report = EvaluationBuilder::new("Test")
.performance(88.0, None)
.build();
assert_eq!(report.criteria[0].name, "Performance");
assert_eq!(report.criteria[0].score, 88.0);
}
#[test]
fn test_evaluation_builder_full() {
let report = EvaluationBuilder::new("Full Evaluation")
.accessibility(95.0, None)
.performance(90.0, None)
.visual(85.0, None)
.code_quality(92.0, None)
.testing(98.0, None)
.build();
assert_eq!(report.criteria.len(), 5);
assert!(report.overall_score() > 90.0);
assert_eq!(report.overall_grade(), Grade::A);
}
#[test]
fn test_evaluation_builder_custom() {
let report = EvaluationBuilder::new("Test")
.custom(Criterion::new("Custom", "My custom criterion").score(75.0))
.build();
assert_eq!(report.criteria[0].name, "Custom");
}
#[test]
fn test_score_breakdown_default() {
let breakdown = ScoreBreakdown::default();
assert_eq!(breakdown.total(), 0.0);
}
#[test]
fn test_score_breakdown_perfect() {
let breakdown = ScoreBreakdown {
widget_complexity: 100.0,
layout_depth: 100.0,
component_count: 100.0,
render_time_p95: 100.0,
memory_usage: 100.0,
bundle_size: 100.0,
wcag_aa_compliance: 100.0,
keyboard_navigation: 100.0,
screen_reader: 100.0,
data_completeness: 100.0,
data_freshness: 100.0,
schema_validation: 100.0,
manifest_completeness: 100.0,
card_coverage: 100.0,
theme_adherence: 100.0,
naming_conventions: 100.0,
};
assert!((breakdown.total() - 100.0).abs() < 0.01);
}
#[test]
fn test_score_breakdown_category_scores() {
let breakdown = ScoreBreakdown {
widget_complexity: 90.0,
layout_depth: 90.0,
component_count: 90.0,
render_time_p95: 80.0,
memory_usage: 80.0,
bundle_size: 80.0,
wcag_aa_compliance: 100.0,
keyboard_navigation: 100.0,
screen_reader: 100.0,
data_completeness: 70.0,
data_freshness: 70.0,
schema_validation: 70.0,
manifest_completeness: 60.0,
card_coverage: 60.0,
theme_adherence: 50.0,
naming_conventions: 50.0,
};
assert!((breakdown.structural_score() - 22.5).abs() < 0.01);
assert!((breakdown.performance_score() - 16.0).abs() < 0.01);
assert!((breakdown.accessibility_score() - 20.0).abs() < 0.01);
}
#[test]
fn test_app_quality_score_from_breakdown() {
let breakdown = ScoreBreakdown {
widget_complexity: 90.0,
layout_depth: 90.0,
component_count: 90.0,
render_time_p95: 90.0,
memory_usage: 90.0,
bundle_size: 90.0,
wcag_aa_compliance: 90.0,
keyboard_navigation: 90.0,
screen_reader: 90.0,
data_completeness: 90.0,
data_freshness: 90.0,
schema_validation: 90.0,
manifest_completeness: 90.0,
card_coverage: 90.0,
theme_adherence: 90.0,
naming_conventions: 90.0,
};
let score = AppQualityScore::from_breakdown(breakdown);
assert!((score.overall - 90.0).abs() < 0.01);
assert_eq!(score.grade, Grade::A);
}
#[test]
fn test_app_quality_score_meets_minimum() {
let score = QualityScoreBuilder::new()
.structural(85.0, 85.0, 85.0)
.performance(85.0, 85.0, 85.0)
.accessibility(85.0, 85.0, 85.0)
.data_quality(85.0, 85.0, 85.0)
.documentation(85.0, 85.0)
.consistency(85.0, 85.0)
.build();
assert!(score.meets_minimum(Grade::BPlus));
assert!(!score.meets_minimum(Grade::A));
}
#[test]
fn test_app_quality_score_production_ready() {
let ready = QualityScoreBuilder::new()
.structural(90.0, 90.0, 90.0)
.performance(90.0, 90.0, 90.0)
.accessibility(90.0, 90.0, 90.0)
.data_quality(90.0, 90.0, 90.0)
.documentation(90.0, 90.0)
.consistency(90.0, 90.0)
.build();
assert!(ready.is_production_ready());
let not_ready = QualityScoreBuilder::new()
.structural(70.0, 70.0, 70.0)
.performance(70.0, 70.0, 70.0)
.accessibility(70.0, 70.0, 70.0)
.data_quality(70.0, 70.0, 70.0)
.documentation(70.0, 70.0)
.consistency(70.0, 70.0)
.build();
assert!(!not_ready.is_production_ready());
}
#[test]
fn test_quality_score_builder() {
let score = QualityScoreBuilder::new()
.structural(100.0, 100.0, 100.0)
.performance(100.0, 100.0, 100.0)
.accessibility(100.0, 100.0, 100.0)
.data_quality(100.0, 100.0, 100.0)
.documentation(100.0, 100.0)
.consistency(100.0, 100.0)
.build();
assert!((score.overall - 100.0).abs() < 0.01);
assert_eq!(score.grade, Grade::APlus);
}
#[test]
fn test_quality_score_builder_clamping() {
let score = QualityScoreBuilder::new()
.structural(150.0, -10.0, 200.0)
.build();
assert_eq!(score.breakdown.widget_complexity, 100.0);
assert_eq!(score.breakdown.layout_depth, 0.0);
assert_eq!(score.breakdown.component_count, 100.0);
}
#[test]
fn test_quality_gates_default() {
let gates = QualityGates::default();
assert_eq!(gates.min_grade, Grade::BPlus);
assert_eq!(gates.min_score, 80.0);
assert_eq!(gates.performance.max_render_time_ms, 16);
assert_eq!(gates.accessibility.wcag_level, "AA");
}
#[test]
fn test_quality_gates_check_passes() {
let gates = QualityGates::default();
let score = QualityScoreBuilder::new()
.structural(90.0, 90.0, 90.0)
.performance(90.0, 90.0, 90.0)
.accessibility(90.0, 90.0, 90.0)
.data_quality(90.0, 90.0, 90.0)
.documentation(90.0, 90.0)
.consistency(90.0, 90.0)
.build();
let result = gates.check(&score);
assert!(result.passed);
assert!(result.violations.is_empty());
}
#[test]
fn test_quality_gates_check_fails_grade() {
let gates = QualityGates::default();
let score = QualityScoreBuilder::new()
.structural(60.0, 60.0, 60.0)
.performance(60.0, 60.0, 60.0)
.accessibility(60.0, 60.0, 60.0)
.data_quality(60.0, 60.0, 60.0)
.documentation(60.0, 60.0)
.consistency(60.0, 60.0)
.build();
let result = gates.check(&score);
assert!(!result.passed);
assert!(!result.violations.is_empty());
assert_eq!(result.violations[0].gate, "min_grade");
}
#[test]
fn test_quality_gates_check_fails_score() {
let gates = QualityGates {
min_grade: Grade::C, min_score: 95.0, ..QualityGates::default()
};
let score = QualityScoreBuilder::new()
.structural(85.0, 85.0, 85.0)
.performance(85.0, 85.0, 85.0)
.accessibility(85.0, 85.0, 85.0)
.data_quality(85.0, 85.0, 85.0)
.documentation(85.0, 85.0)
.consistency(85.0, 85.0)
.build();
let result = gates.check(&score);
assert!(!result.passed);
assert!(result.violations.iter().any(|v| v.gate == "min_score"));
}
#[test]
fn test_performance_gates_default() {
let gates = PerformanceGates::default();
assert_eq!(gates.max_render_time_ms, 16);
assert_eq!(gates.max_bundle_size_kb, 500);
assert_eq!(gates.max_memory_mb, 100);
}
#[test]
fn test_accessibility_gates_default() {
let gates = AccessibilityGates::default();
assert_eq!(gates.wcag_level, "AA");
assert_eq!(gates.min_contrast_ratio, 4.5);
assert!(gates.require_keyboard_nav);
assert!(gates.require_aria_labels);
}
#[test]
fn test_documentation_gates_default() {
let gates = DocumentationGates::default();
assert!(gates.require_model_cards);
assert!(gates.require_data_cards);
assert!(gates.min_manifest_fields.contains(&"name".to_string()));
assert!(gates.min_manifest_fields.contains(&"version".to_string()));
}
#[test]
fn test_violation_severity() {
let violation = GateViolation {
gate: "test".to_string(),
expected: "A".to_string(),
actual: "B".to_string(),
severity: ViolationSeverity::Error,
};
assert_eq!(violation.severity, ViolationSeverity::Error);
}
#[test]
fn test_gate_config_error_display() {
let err = GateConfigError::ParseError("invalid toml".to_string());
assert!(err.to_string().contains("parse error"));
let err = GateConfigError::IoError("file not found".to_string());
assert!(err.to_string().contains("IO error"));
let err = GateConfigError::InvalidValue("out of range".to_string());
assert!(err.to_string().contains("invalid value"));
}
#[test]
fn test_quality_gates_to_toml() {
let gates = QualityGates::default();
let toml_str = gates.to_toml();
assert!(toml_str.contains("min_score"));
assert!(toml_str.contains("[performance]"));
assert!(toml_str.contains("[accessibility]"));
assert!(toml_str.contains("max_render_time_ms"));
}
#[test]
fn test_quality_gates_from_toml() {
let toml_str = r#"
min_grade = "A"
min_score = 90.0
[performance]
max_render_time_ms = 8
max_bundle_size_kb = 300
max_memory_mb = 50
[accessibility]
wcag_level = "AAA"
min_contrast_ratio = 7.0
require_keyboard_nav = true
require_aria_labels = true
[data]
max_staleness_minutes = 30
require_schema_validation = true
[documentation]
require_model_cards = true
require_data_cards = true
min_manifest_fields = ["name", "version"]
"#;
let gates = QualityGates::from_toml(toml_str).unwrap();
assert_eq!(gates.min_score, 90.0);
assert_eq!(gates.performance.max_render_time_ms, 8);
assert_eq!(gates.performance.max_bundle_size_kb, 300);
assert_eq!(gates.accessibility.wcag_level, "AAA");
assert_eq!(gates.accessibility.min_contrast_ratio, 7.0);
assert_eq!(gates.data.max_staleness_minutes, 30);
}
#[test]
fn test_quality_gates_roundtrip() {
let original = QualityGates::default();
let toml_str = original.to_toml();
let parsed = QualityGates::from_toml(&toml_str).unwrap();
assert_eq!(parsed.min_score, original.min_score);
assert_eq!(
parsed.performance.max_render_time_ms,
original.performance.max_render_time_ms
);
assert_eq!(
parsed.accessibility.wcag_level,
original.accessibility.wcag_level
);
}
#[test]
fn test_quality_gates_from_toml_invalid() {
let result = QualityGates::from_toml("this is not valid toml {{{");
assert!(matches!(result, Err(GateConfigError::ParseError(_))));
}
#[test]
fn test_quality_gates_sample_config() {
let sample = QualityGates::sample_config();
assert!(sample.contains("min_grade"));
assert!(sample.contains("max_bundle_size_kb"));
assert!(sample.contains("wcag_level"));
assert!(sample.contains("[performance]"));
assert!(sample.contains("[accessibility]"));
assert!(sample.contains("[data]"));
assert!(sample.contains("[documentation]"));
}
#[test]
fn test_quality_gates_check_extended_passes() {
let gates = QualityGates::default();
let score = QualityScoreBuilder::new()
.structural(90.0, 90.0, 90.0)
.performance(90.0, 90.0, 90.0)
.accessibility(90.0, 90.0, 90.0)
.data_quality(90.0, 90.0, 90.0)
.documentation(90.0, 90.0)
.consistency(90.0, 90.0)
.build();
let result = gates.check_extended(&score, Some(10), Some(400), Some(50));
assert!(result.passed);
assert!(result.violations.is_empty());
}
#[test]
fn test_quality_gates_check_extended_render_time_fails() {
let gates = QualityGates::default();
let score = QualityScoreBuilder::new()
.structural(90.0, 90.0, 90.0)
.performance(90.0, 90.0, 90.0)
.accessibility(90.0, 90.0, 90.0)
.data_quality(90.0, 90.0, 90.0)
.documentation(90.0, 90.0)
.consistency(90.0, 90.0)
.build();
let result = gates.check_extended(&score, Some(25), Some(400), Some(50));
assert!(!result.passed);
assert!(result
.violations
.iter()
.any(|v| v.gate == "max_render_time_ms"));
}
#[test]
fn test_quality_gates_check_extended_bundle_size_fails() {
let gates = QualityGates::default();
let score = QualityScoreBuilder::new()
.structural(90.0, 90.0, 90.0)
.performance(90.0, 90.0, 90.0)
.accessibility(90.0, 90.0, 90.0)
.data_quality(90.0, 90.0, 90.0)
.documentation(90.0, 90.0)
.consistency(90.0, 90.0)
.build();
let result = gates.check_extended(&score, Some(10), Some(600), Some(50));
assert!(!result.passed);
assert!(result
.violations
.iter()
.any(|v| v.gate == "max_bundle_size_kb"));
}
#[test]
fn test_quality_gates_check_extended_memory_warning() {
let gates = QualityGates::default();
let score = QualityScoreBuilder::new()
.structural(90.0, 90.0, 90.0)
.performance(90.0, 90.0, 90.0)
.accessibility(90.0, 90.0, 90.0)
.data_quality(90.0, 90.0, 90.0)
.documentation(90.0, 90.0)
.consistency(90.0, 90.0)
.build();
let result = gates.check_extended(&score, Some(10), Some(400), Some(150));
assert!(result.passed); assert!(result.violations.iter().any(|v| v.gate == "max_memory_mb"));
assert!(result
.violations
.iter()
.any(|v| v.severity == ViolationSeverity::Warning));
}
#[test]
fn test_grade_from_str() {
assert_eq!(Grade::from_str("A+").unwrap(), Grade::APlus);
assert_eq!(Grade::from_str("a+").unwrap(), Grade::APlus);
assert_eq!(Grade::from_str("A").unwrap(), Grade::A);
assert_eq!(Grade::from_str("A-").unwrap(), Grade::AMinus);
assert_eq!(Grade::from_str("B+").unwrap(), Grade::BPlus);
assert_eq!(Grade::from_str("B").unwrap(), Grade::B);
assert_eq!(Grade::from_str("B-").unwrap(), Grade::BMinus);
assert_eq!(Grade::from_str("C+").unwrap(), Grade::CPlus);
assert_eq!(Grade::from_str("C").unwrap(), Grade::C);
assert_eq!(Grade::from_str("C-").unwrap(), Grade::CMinus);
assert_eq!(Grade::from_str("D").unwrap(), Grade::D);
assert_eq!(Grade::from_str("F").unwrap(), Grade::F);
}
#[test]
fn test_grade_from_str_invalid() {
assert!(matches!(
Grade::from_str("X"),
Err(GateConfigError::InvalidValue(_))
));
assert!(matches!(
Grade::from_str("E"),
Err(GateConfigError::InvalidValue(_))
));
assert!(matches!(
Grade::from_str(""),
Err(GateConfigError::InvalidValue(_))
));
}
#[test]
fn test_quality_gates_config_file_constant() {
assert_eq!(QualityGates::CONFIG_FILE, ".presentar-gates.toml");
}
}