upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/result.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-02 20:54:15 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-02 21:03:24 +0000
commit5c10ca008413744b09136618eaa85275c997704c (patch)
treeaf24387d8916bdec26315a31f67bd99c39544544 /grasp-audit/src/result.rs
parentc07954f44f4c03cc17d4a83b144667cbcbb226cf (diff)
grasp-audit: show tests under GRASP-01 line
Diffstat (limited to 'grasp-audit/src/result.rs')
-rw-r--r--grasp-audit/src/result.rs188
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
3use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID};
3use std::collections::BTreeMap; 4use std::collections::BTreeMap;
4use std::time::{Duration, Instant}; 5use std::time::{Duration, Instant};
5 6
6// ANSI color codes 7// ANSI color codes
7const GREEN: &str = "\x1b[32m"; 8const GREEN: &str = "\x1b[32m";
8const RED: &str = "\x1b[31m"; 9const RED: &str = "\x1b[31m";
10const YELLOW: &str = "\x1b[33m";
11const BLUE: &str = "\x1b[34m";
12const CYAN: &str = "\x1b[36m";
9const RESET: &str = "\x1b[0m"; 13const RESET: &str = "\x1b[0m";
10const BOLD: &str = "\x1b[1m"; 14const 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///
15fn 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)
23fn 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