diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-02 20:54:15 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-02 21:03:24 +0000 |
| commit | 5c10ca008413744b09136618eaa85275c997704c (patch) | |
| tree | af24387d8916bdec26315a31f67bd99c39544544 /grasp-audit/src/result.rs | |
| parent | c07954f44f4c03cc17d4a83b144667cbcbb226cf (diff) | |
grasp-audit: show tests under GRASP-01 line
Diffstat (limited to 'grasp-audit/src/result.rs')
| -rw-r--r-- | grasp-audit/src/result.rs | 188 |
1 files changed, 140 insertions, 48 deletions
diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs index bc0008a..0de16ae 100644 --- a/grasp-audit/src/result.rs +++ b/grasp-audit/src/result.rs | |||
| @@ -1,35 +1,56 @@ | |||
| 1 | //! Test result types | 1 | //! Test result types |
| 2 | 2 | ||
| 3 | use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; | ||
| 3 | use std::collections::BTreeMap; | 4 | use std::collections::BTreeMap; |
| 4 | use std::time::{Duration, Instant}; | 5 | use std::time::{Duration, Instant}; |
| 5 | 6 | ||
| 6 | // ANSI color codes | 7 | // ANSI color codes |
| 7 | const GREEN: &str = "\x1b[32m"; | 8 | const GREEN: &str = "\x1b[32m"; |
| 8 | const RED: &str = "\x1b[31m"; | 9 | const RED: &str = "\x1b[31m"; |
| 10 | const YELLOW: &str = "\x1b[33m"; | ||
| 11 | const BLUE: &str = "\x1b[34m"; | ||
| 12 | const CYAN: &str = "\x1b[36m"; | ||
| 9 | const RESET: &str = "\x1b[0m"; | 13 | const RESET: &str = "\x1b[0m"; |
| 10 | const BOLD: &str = "\x1b[1m"; | 14 | const BOLD: &str = "\x1b[1m"; |
| 11 | 15 | ||
| 12 | /// Extract spec category from a spec_ref by removing trailing test number | 16 | /// Parse line number(s) from a spec_ref string |
| 13 | /// e.g., "GRASP-01:event-acceptance:1.1" -> "GRASP-01:event-acceptance" | 17 | /// Returns a vector of line numbers that this spec_ref covers |
| 14 | /// e.g., "NIP-01:basic:2" -> "NIP-01:basic" | 18 | /// |
| 15 | fn extract_spec_category(spec_ref: &str) -> String { | 19 | /// Examples: |
| 20 | /// - "GRASP-01:nostr-relay:7" -> [7] | ||
| 21 | /// - "GRASP-01:nostr-relay:7-9" -> [7, 8, 9] | ||
| 22 | /// - "NIP-01:basic:2" -> [] (not a GRASP-01 ref) | ||
| 23 | fn parse_spec_lines(spec_ref: &str) -> Vec<u32> { | ||
| 24 | // Only parse GRASP-01 refs | ||
| 25 | if !spec_ref.starts_with("GRASP-01:") { | ||
| 26 | return vec![]; | ||
| 27 | } | ||
| 28 | |||
| 29 | // Get the last part after the last colon | ||
| 16 | let parts: Vec<&str> = spec_ref.split(':').collect(); | 30 | let parts: Vec<&str> = spec_ref.split(':').collect(); |
| 17 | if parts.len() >= 2 { | 31 | if parts.len() < 3 { |
| 18 | // Check if the last part looks like a test number (starts with digit) | 32 | return vec![]; |
| 19 | if let Some(last) = parts.last() { | 33 | } |
| 20 | if last | 34 | |
| 21 | .chars() | 35 | let line_part = parts.last().unwrap(); |
| 22 | .next() | 36 | |
| 23 | .map(|c| c.is_ascii_digit()) | 37 | // Handle range format like "7-9" |
| 24 | .unwrap_or(false) | 38 | if line_part.contains('-') { |
| 25 | { | 39 | let range_parts: Vec<&str> = line_part.split('-').collect(); |
| 26 | // Remove the trailing number part | 40 | if range_parts.len() == 2 { |
| 27 | return parts[..parts.len() - 1].join(":"); | 41 | if let (Ok(start), Ok(end)) = (range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>()) { |
| 42 | return (start..=end).collect(); | ||
| 28 | } | 43 | } |
| 29 | } | 44 | } |
| 45 | return vec![]; | ||
| 30 | } | 46 | } |
| 31 | // Return as-is if no trailing number found | 47 | |
| 32 | spec_ref.to_string() | 48 | // Handle single line number |
| 49 | if let Ok(line) = line_part.parse::<u32>() { | ||
| 50 | return vec![line]; | ||
| 51 | } | ||
| 52 | |||
| 53 | vec![] | ||
| 33 | } | 54 | } |
| 34 | 55 | ||
| 35 | /// Result of a single test | 56 | /// Result of a single test |
| @@ -138,53 +159,100 @@ impl AuditResult { | |||
| 138 | self.results.len() | 159 | self.results.len() |
| 139 | } | 160 | } |
| 140 | 161 | ||
| 141 | /// Print a detailed report with tests grouped by spec_ref | 162 | /// Print a detailed report aligned to GRASP-01 specification |
| 142 | pub fn print_report(&self) { | 163 | pub fn print_report(&self) { |
| 143 | println!("\n{}{}{}", BOLD, self.spec, RESET); | 164 | println!(); |
| 144 | println!("{}", "═".repeat(60)); | 165 | println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); |
| 145 | 166 | println!("{}GRASP-01 Compliance Report{}", BOLD, RESET); | |
| 146 | let passed = self.passed_count(); | 167 | println!("Source: github.com/nostr-protocol/grasp (commit: {})", GRASP_COMMIT_ID); |
| 147 | let total = self.total_count(); | 168 | println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); |
| 148 | 169 | ||
| 149 | // Group results by spec category (strip trailing test number like ":1.1") | 170 | // Build a map of spec line -> tests that cover it |
| 150 | let mut grouped: BTreeMap<String, Vec<&TestResult>> = BTreeMap::new(); | 171 | let mut tests_by_line: BTreeMap<u32, Vec<&TestResult>> = BTreeMap::new(); |
| 151 | for result in &self.results { | 172 | for result in &self.results { |
| 152 | // Extract category from spec_ref (e.g., "GRASP-01:event-acceptance:1.1" -> "GRASP-01:event-acceptance") | 173 | let lines = parse_spec_lines(&result.spec_ref); |
| 153 | let category = extract_spec_category(&result.spec_ref); | 174 | for line in lines { |
| 154 | grouped.entry(category).or_default().push(result); | 175 | tests_by_line.entry(line).or_default().push(result); |
| 176 | } | ||
| 155 | } | 177 | } |
| 156 | 178 | ||
| 157 | // Print grouped results | 179 | // Track how many spec requirements have tests |
| 158 | for (category, results) in &grouped { | 180 | let mut tested_requirements = 0; |
| 159 | println!("\n{}[{}]{}", BOLD, category, RESET); | 181 | let total_requirements = GRASP_01_REQUIREMENTS.len(); |
| 160 | 182 | ||
| 161 | for result in results { | 183 | // Print results organized by section and spec line |
| 162 | let (color, status) = if result.passed { | 184 | for section in get_sections() { |
| 163 | (GREEN, "✓") | 185 | println!(); |
| 186 | println!("{}{}## {}{}", CYAN, BOLD, section, RESET); | ||
| 187 | |||
| 188 | for req in GRASP_01_REQUIREMENTS.iter().filter(|r| r.section == section) { | ||
| 189 | println!(); | ||
| 190 | // Print spec requirement in blue | ||
| 191 | println!("{}📘 Line {}: {}{}", BLUE, req.line, req.text, RESET); | ||
| 192 | |||
| 193 | // Get tests for this line | ||
| 194 | if let Some(tests) = tests_by_line.get(&req.line) { | ||
| 195 | tested_requirements += 1; | ||
| 196 | for test in tests { | ||
| 197 | let (color, status) = if test.passed { | ||
| 198 | (GREEN, "✓") | ||
| 199 | } else { | ||
| 200 | (RED, "✗") | ||
| 201 | }; | ||
| 202 | println!(" {}{} {}{}", color, status, test.name, RESET); | ||
| 203 | |||
| 204 | if let Some(error) = &test.error { | ||
| 205 | // Truncate long errors | ||
| 206 | let error_display = if error.len() > 100 { | ||
| 207 | format!("{}...", &error[..100]) | ||
| 208 | } else { | ||
| 209 | error.clone() | ||
| 210 | }; | ||
| 211 | println!(" {}Error: {}{}", RED, error_display, RESET); | ||
| 212 | } | ||
| 213 | } | ||
| 164 | } else { | 214 | } else { |
| 165 | (RED, "✗") | 215 | println!(" {}⚠️ No Tests Implemented{}", YELLOW, RESET); |
| 166 | }; | ||
| 167 | |||
| 168 | println!(" {}{} {}{}", color, status, result.name, RESET); | ||
| 169 | |||
| 170 | if let Some(error) = &result.error { | ||
| 171 | println!(" {}Error: {}{}", RED, error, RESET); | ||
| 172 | } | 216 | } |
| 173 | } | 217 | } |
| 174 | } | 218 | } |
| 175 | 219 | ||
| 176 | println!(); | 220 | println!(); |
| 177 | let pass_rate = if total > 0 { | 221 | println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); |
| 178 | (passed as f64 / total as f64) * 100.0 | 222 | |
| 223 | // Summary statistics | ||
| 224 | let passed = self.passed_count(); | ||
| 225 | let total_tests = self.total_count(); | ||
| 226 | |||
| 227 | let spec_coverage = if total_requirements > 0 { | ||
| 228 | (tested_requirements as f64 / total_requirements as f64) * 100.0 | ||
| 229 | } else { | ||
| 230 | 0.0 | ||
| 231 | }; | ||
| 232 | |||
| 233 | let pass_rate = if total_tests > 0 { | ||
| 234 | (passed as f64 / total_tests as f64) * 100.0 | ||
| 179 | } else { | 235 | } else { |
| 180 | 0.0 | 236 | 0.0 |
| 181 | }; | 237 | }; |
| 182 | 238 | ||
| 183 | let summary_color = if passed == total { GREEN } else { RED }; | 239 | let summary_color = if passed == total_tests && tested_requirements == total_requirements { |
| 240 | GREEN | ||
| 241 | } else if passed == total_tests { | ||
| 242 | YELLOW | ||
| 243 | } else { | ||
| 244 | RED | ||
| 245 | }; | ||
| 246 | |||
| 184 | println!( | 247 | println!( |
| 185 | "{}Results: {}/{} passed ({:.1}%){}", | 248 | "{}Spec coverage: {}/{} requirements tested ({:.1}%){}", |
| 186 | summary_color, passed, total, pass_rate, RESET | 249 | summary_color, tested_requirements, total_requirements, spec_coverage, RESET |
| 187 | ); | 250 | ); |
| 251 | println!( | ||
| 252 | "{}Test results: {}/{} tests passed ({:.1}%){}", | ||
| 253 | summary_color, passed, total_tests, pass_rate, RESET | ||
| 254 | ); | ||
| 255 | println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET); | ||
| 188 | println!(); | 256 | println!(); |
| 189 | } | 257 | } |
| 190 | 258 | ||
| @@ -235,4 +303,28 @@ mod tests { | |||
| 235 | assert_eq!(audit.failed_count(), 1); | 303 | assert_eq!(audit.failed_count(), 1); |
| 236 | assert!(!audit.all_passed()); | 304 | assert!(!audit.all_passed()); |
| 237 | } | 305 | } |
| 238 | } | 306 | |
| 307 | #[test] | ||
| 308 | fn test_parse_spec_lines_single() { | ||
| 309 | assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7"), vec![7]); | ||
| 310 | assert_eq!(parse_spec_lines("GRASP-01:git-http:28"), vec![28]); | ||
| 311 | } | ||
| 312 | |||
| 313 | #[test] | ||
| 314 | fn test_parse_spec_lines_range() { | ||
| 315 | assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7-9"), vec![7, 8, 9]); | ||
| 316 | assert_eq!(parse_spec_lines("GRASP-01:cors:44-47"), vec![44, 45, 46, 47]); | ||
| 317 | } | ||
| 318 | |||
| 319 | #[test] | ||
| 320 | fn test_parse_spec_lines_non_grasp() { | ||
| 321 | assert_eq!(parse_spec_lines("NIP-01:basic:1"), Vec::<u32>::new()); | ||
| 322 | assert_eq!(parse_spec_lines("OTHER:spec:5"), Vec::<u32>::new()); | ||
| 323 | } | ||
| 324 | |||
| 325 | #[test] | ||
| 326 | fn test_parse_spec_lines_invalid() { | ||
| 327 | assert_eq!(parse_spec_lines("GRASP-01:invalid"), Vec::<u32>::new()); | ||
| 328 | assert_eq!(parse_spec_lines("GRASP-01:test:abc"), Vec::<u32>::new()); | ||
| 329 | } | ||
| 330 | } \ No newline at end of file | ||