mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-06 07:28:23 +00:00
330 lines
10 KiB
Rust
330 lines
10 KiB
Rust
//! Test result types
|
|
|
|
use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID};
|
|
use std::collections::BTreeMap;
|
|
use std::time::{Duration, Instant};
|
|
|
|
// ANSI color codes
|
|
const GREEN: &str = "\x1b[32m";
|
|
const RED: &str = "\x1b[31m";
|
|
const YELLOW: &str = "\x1b[33m";
|
|
const BLUE: &str = "\x1b[34m";
|
|
const CYAN: &str = "\x1b[36m";
|
|
const RESET: &str = "\x1b[0m";
|
|
const BOLD: &str = "\x1b[1m";
|
|
|
|
/// Parse line number(s) from a spec_ref string
|
|
/// Returns a vector of line numbers that this spec_ref covers
|
|
///
|
|
/// Examples:
|
|
/// - "GRASP-01:nostr-relay:7" -> [7]
|
|
/// - "GRASP-01:nostr-relay:7-9" -> [7, 8, 9]
|
|
/// - "NIP-01:basic:2" -> [] (not a GRASP-01 ref)
|
|
fn parse_spec_lines(spec_ref: &str) -> Vec<u32> {
|
|
// Only parse GRASP-01 refs
|
|
if !spec_ref.starts_with("GRASP-01:") {
|
|
return vec![];
|
|
}
|
|
|
|
// Get the last part after the last colon
|
|
let parts: Vec<&str> = spec_ref.split(':').collect();
|
|
if parts.len() < 3 {
|
|
return vec![];
|
|
}
|
|
|
|
let line_part = parts.last().unwrap();
|
|
|
|
// Handle range format like "7-9"
|
|
if line_part.contains('-') {
|
|
let range_parts: Vec<&str> = line_part.split('-').collect();
|
|
if range_parts.len() == 2 {
|
|
if let (Ok(start), Ok(end)) = (range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>()) {
|
|
return (start..=end).collect();
|
|
}
|
|
}
|
|
return vec![];
|
|
}
|
|
|
|
// Handle single line number
|
|
if let Ok(line) = line_part.parse::<u32>() {
|
|
return vec![line];
|
|
}
|
|
|
|
vec![]
|
|
}
|
|
|
|
/// Result of a single test
|
|
#[derive(Debug, Clone)]
|
|
pub struct TestResult {
|
|
pub name: String,
|
|
pub spec_ref: String,
|
|
pub requirement: String,
|
|
pub passed: bool,
|
|
pub error: Option<String>,
|
|
pub duration: Duration,
|
|
}
|
|
|
|
impl TestResult {
|
|
/// Create a new test result
|
|
pub fn new(name: &str, spec_ref: &str, requirement: &str) -> Self {
|
|
TestResult {
|
|
name: name.to_string(),
|
|
spec_ref: spec_ref.to_string(),
|
|
requirement: requirement.to_string(),
|
|
passed: false,
|
|
error: None,
|
|
duration: Duration::default(),
|
|
}
|
|
}
|
|
|
|
/// Run a test function and capture the result
|
|
pub async fn run<F, Fut>(mut self, test_fn: F) -> Self
|
|
where
|
|
F: FnOnce() -> Fut,
|
|
Fut: std::future::Future<Output = Result<(), String>>,
|
|
{
|
|
let start = Instant::now();
|
|
|
|
match test_fn().await {
|
|
Ok(()) => {
|
|
self.passed = true;
|
|
}
|
|
Err(e) => {
|
|
self.passed = false;
|
|
self.error = Some(e);
|
|
}
|
|
}
|
|
|
|
self.duration = start.elapsed();
|
|
self
|
|
}
|
|
|
|
/// Mark test as passed
|
|
pub fn pass(mut self) -> Self {
|
|
self.passed = true;
|
|
self
|
|
}
|
|
|
|
/// Mark test as failed with error
|
|
pub fn fail(mut self, error: impl Into<String>) -> Self {
|
|
self.passed = false;
|
|
self.error = Some(error.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Collection of test results for a spec
|
|
#[derive(Debug, Clone)]
|
|
pub struct AuditResult {
|
|
pub spec: String,
|
|
pub results: Vec<TestResult>,
|
|
}
|
|
|
|
impl AuditResult {
|
|
/// Create a new audit result
|
|
pub fn new(spec: impl Into<String>) -> Self {
|
|
Self {
|
|
spec: spec.into(),
|
|
results: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Add a test result
|
|
pub fn add(&mut self, result: TestResult) {
|
|
self.results.push(result);
|
|
}
|
|
|
|
/// Merge another audit result
|
|
pub fn merge(&mut self, other: AuditResult) {
|
|
self.results.extend(other.results);
|
|
}
|
|
|
|
/// Check if all tests passed
|
|
pub fn all_passed(&self) -> bool {
|
|
self.results.iter().all(|r| r.passed)
|
|
}
|
|
|
|
/// Get count of passed tests
|
|
pub fn passed_count(&self) -> usize {
|
|
self.results.iter().filter(|r| r.passed).count()
|
|
}
|
|
|
|
/// Get count of failed tests
|
|
pub fn failed_count(&self) -> usize {
|
|
self.results.iter().filter(|r| !r.passed).count()
|
|
}
|
|
|
|
/// Get total count of tests
|
|
pub fn total_count(&self) -> usize {
|
|
self.results.len()
|
|
}
|
|
|
|
/// Print a detailed report aligned to GRASP-01 specification
|
|
pub fn print_report(&self) {
|
|
println!();
|
|
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
|
println!("{}GRASP-01 Compliance Report{}", BOLD, RESET);
|
|
println!("Source: github.com/nostr-protocol/grasp (commit: {})", GRASP_COMMIT_ID);
|
|
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
|
|
|
// Build a map of spec line -> tests that cover it
|
|
let mut tests_by_line: BTreeMap<u32, Vec<&TestResult>> = BTreeMap::new();
|
|
for result in &self.results {
|
|
let lines = parse_spec_lines(&result.spec_ref);
|
|
for line in lines {
|
|
tests_by_line.entry(line).or_default().push(result);
|
|
}
|
|
}
|
|
|
|
// Track how many spec requirements have tests
|
|
let mut tested_requirements = 0;
|
|
let total_requirements = GRASP_01_REQUIREMENTS.len();
|
|
|
|
// Print results organized by section and spec line
|
|
for section in get_sections() {
|
|
println!();
|
|
println!("{}{}## {}{}", CYAN, BOLD, section, RESET);
|
|
|
|
for req in GRASP_01_REQUIREMENTS.iter().filter(|r| r.section == section) {
|
|
println!();
|
|
// Print spec requirement in blue
|
|
println!("{}📘 Line {}: {}{}", BLUE, req.line, req.text, RESET);
|
|
|
|
// Get tests for this line
|
|
if let Some(tests) = tests_by_line.get(&req.line) {
|
|
tested_requirements += 1;
|
|
for test in tests {
|
|
let (color, status) = if test.passed {
|
|
(GREEN, "✓")
|
|
} else {
|
|
(RED, "✗")
|
|
};
|
|
println!(" {}{} {}{}", color, status, test.name, RESET);
|
|
|
|
if let Some(error) = &test.error {
|
|
// Truncate long errors
|
|
let error_display = if error.len() > 100 {
|
|
format!("{}...", &error[..100])
|
|
} else {
|
|
error.clone()
|
|
};
|
|
println!(" {}Error: {}{}", RED, error_display, RESET);
|
|
}
|
|
}
|
|
} else {
|
|
println!(" {}⚠️ No Tests Implemented{}", YELLOW, RESET);
|
|
}
|
|
}
|
|
}
|
|
|
|
println!();
|
|
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
|
|
|
// Summary statistics
|
|
let passed = self.passed_count();
|
|
let total_tests = self.total_count();
|
|
|
|
let spec_coverage = if total_requirements > 0 {
|
|
(tested_requirements as f64 / total_requirements as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let pass_rate = if total_tests > 0 {
|
|
(passed as f64 / total_tests as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let summary_color = if passed == total_tests && tested_requirements == total_requirements {
|
|
GREEN
|
|
} else if passed == total_tests {
|
|
YELLOW
|
|
} else {
|
|
RED
|
|
};
|
|
|
|
println!(
|
|
"{}Spec coverage: {}/{} requirements tested ({:.1}%){}",
|
|
summary_color, tested_requirements, total_requirements, spec_coverage, RESET
|
|
);
|
|
println!(
|
|
"{}Test results: {}/{} tests passed ({:.1}%){}",
|
|
summary_color, passed, total_tests, pass_rate, RESET
|
|
);
|
|
println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
|
|
println!();
|
|
}
|
|
|
|
/// Get a summary string
|
|
pub fn summary(&self) -> String {
|
|
format!(
|
|
"{}: {}/{} passed",
|
|
self.spec,
|
|
self.passed_count(),
|
|
self.total_count()
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_result_pass() {
|
|
let result = TestResult::new("test", "SPEC:1", "Must work")
|
|
.run(|| async { Ok(()) })
|
|
.await;
|
|
|
|
assert!(result.passed);
|
|
assert!(result.error.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_result_fail() {
|
|
let result = TestResult::new("test", "SPEC:1", "Must work")
|
|
.run(|| async { Err("Failed".to_string()) })
|
|
.await;
|
|
|
|
assert!(!result.passed);
|
|
assert_eq!(result.error, Some("Failed".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_audit_result() {
|
|
let mut audit = AuditResult::new("Test Spec");
|
|
|
|
audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass());
|
|
audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error"));
|
|
|
|
assert_eq!(audit.total_count(), 2);
|
|
assert_eq!(audit.passed_count(), 1);
|
|
assert_eq!(audit.failed_count(), 1);
|
|
assert!(!audit.all_passed());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_spec_lines_single() {
|
|
assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7"), vec![7]);
|
|
assert_eq!(parse_spec_lines("GRASP-01:git-http:28"), vec![28]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_spec_lines_range() {
|
|
assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7-9"), vec![7, 8, 9]);
|
|
assert_eq!(parse_spec_lines("GRASP-01:cors:44-47"), vec![44, 45, 46, 47]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_spec_lines_non_grasp() {
|
|
assert_eq!(parse_spec_lines("NIP-01:basic:1"), Vec::<u32>::new());
|
|
assert_eq!(parse_spec_lines("OTHER:spec:5"), Vec::<u32>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_spec_lines_invalid() {
|
|
assert_eq!(parse_spec_lines("GRASP-01:invalid"), Vec::<u32>::new());
|
|
assert_eq!(parse_spec_lines("GRASP-01:test:abc"), Vec::<u32>::new());
|
|
}
|
|
} |