1use super::{RdfFormat, RdfParser};
14use anyhow::Result;
15use serde::{Deserialize, Serialize};
16use std::collections::{HashMap, HashSet};
17use std::fs;
18use std::path::PathBuf;
19use tokio::time::{timeout, Duration};
20use url::Url;
21
22#[derive(Debug)]
24pub struct W3cRdfTestSuiteRunner {
25 #[allow(dead_code)]
27 base_url: Url,
28
29 config: W3cRdfTestConfig,
31
32 manifests: HashMap<RdfFormat, Vec<RdfTestManifest>>,
34
35 results: HashMap<String, RdfTestResult>,
37
38 stats: RdfComplianceStats,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct W3cRdfTestConfig {
45 pub test_suite_location: String,
47
48 pub enabled_formats: HashSet<RdfFormat>,
50
51 pub enabled_test_types: HashSet<RdfTestType>,
53
54 pub test_timeout_seconds: u64,
56
57 pub max_parallel_tests: usize,
59
60 pub verbose_logging: bool,
62
63 pub output_directory: Option<PathBuf>,
65
66 pub test_filters: Vec<RdfTestFilter>,
68
69 pub skip_known_failures: bool,
71}
72
73impl Default for W3cRdfTestConfig {
74 fn default() -> Self {
75 let mut enabled_formats = HashSet::new();
76 enabled_formats.insert(RdfFormat::Turtle);
77 enabled_formats.insert(RdfFormat::NTriples);
78 enabled_formats.insert(RdfFormat::NQuads);
79 enabled_formats.insert(RdfFormat::TriG);
80
81 let mut enabled_test_types = HashSet::new();
82 enabled_test_types.insert(RdfTestType::PositiveParser);
83 enabled_test_types.insert(RdfTestType::NegativeParser);
84 enabled_test_types.insert(RdfTestType::PositiveSyntax);
85 enabled_test_types.insert(RdfTestType::NegativeSyntax);
86
87 Self {
88 test_suite_location: "https://w3c.github.io/rdf-tests/".to_string(),
89 enabled_formats,
90 enabled_test_types,
91 test_timeout_seconds: 30,
92 max_parallel_tests: 8,
93 verbose_logging: false,
94 output_directory: None,
95 test_filters: Vec::new(),
96 skip_known_failures: true,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
103pub enum RdfTestType {
104 PositiveParser,
106 NegativeParser,
108 PositiveSyntax,
110 NegativeSyntax,
112 Evaluation,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct RdfTestFilter {
119 pub name_pattern: Option<String>,
121 pub test_type: Option<RdfTestType>,
123 pub format: Option<RdfFormat>,
125 pub approved_only: bool,
127}
128
129#[derive(Debug, Clone, Deserialize, Serialize)]
131pub struct RdfTestManifest {
132 #[serde(rename = "@id")]
133 pub id: String,
134 #[serde(rename = "@type")]
135 pub test_type: Vec<String>,
136 pub name: String,
137 pub comment: Option<String>,
138 pub action: RdfTestAction,
139 pub result: Option<String>,
140 #[serde(default)]
141 pub approval: String,
142 #[serde(default)]
143 pub format: String,
144}
145
146#[derive(Debug, Clone, Deserialize, Serialize)]
148#[serde(untagged)]
149pub enum RdfTestAction {
150 FilePath(String),
152 Complex {
154 input: String,
155 expected: Option<String>,
156 #[serde(default)]
157 base: Option<String>,
158 },
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct RdfTestResult {
164 pub test_id: String,
165 pub test_name: String,
166 pub test_type: RdfTestType,
167 pub format: RdfFormat,
168 pub status: RdfTestStatus,
169 pub execution_time_ms: u64,
170 pub error_message: Option<String>,
171 pub expected_quads: Option<usize>,
172 pub actual_quads: Option<usize>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub enum RdfTestStatus {
178 Passed,
180 Failed,
182 Skipped,
184 Timeout,
186 Error,
188 KnownFailure,
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct RdfComplianceStats {
195 pub total_tests: usize,
196 pub passed: usize,
197 pub failed: usize,
198 pub skipped: usize,
199 pub timeout: usize,
200 pub error: usize,
201 pub known_failures: usize,
202 pub format_stats: HashMap<RdfFormat, FormatStats>,
203}
204
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
207pub struct FormatStats {
208 pub total: usize,
209 pub passed: usize,
210 pub failed: usize,
211 pub compliance_percentage: f64,
212}
213
214impl W3cRdfTestSuiteRunner {
215 pub fn new(config: W3cRdfTestConfig) -> Result<Self> {
217 let base_url = Url::parse(&config.test_suite_location)?;
218
219 Ok(Self {
220 base_url,
221 config,
222 manifests: HashMap::new(),
223 results: HashMap::new(),
224 stats: RdfComplianceStats::default(),
225 })
226 }
227
228 pub async fn load_manifests(&mut self) -> Result<()> {
230 for format in &self.config.enabled_formats {
231 let manifest_path = self.get_manifest_path(format);
232
233 if let Ok(manifests) = self
234 .load_format_manifest(&manifest_path, format.clone())
235 .await
236 {
237 self.manifests.insert(format.clone(), manifests);
238
239 if self.config.verbose_logging {
240 println!(
241 "Loaded {} tests for {}",
242 self.manifests
243 .get(format)
244 .expect("format just inserted into manifests")
245 .len(),
246 format
247 );
248 }
249 }
250 }
251
252 Ok(())
253 }
254
255 pub async fn run_tests(&mut self) -> Result<RdfComplianceStats> {
257 let mut total_tests = 0;
258
259 for (format, manifests) in &self.manifests {
260 let mut format_stats = FormatStats::default();
261
262 for manifest in manifests {
263 if !self.should_run_test(manifest) {
264 continue;
265 }
266
267 total_tests += 1;
268 format_stats.total += 1;
269
270 let test_result = self.run_single_test(manifest, format).await;
271
272 match test_result.status {
273 RdfTestStatus::Passed => {
274 self.stats.passed += 1;
275 format_stats.passed += 1;
276 }
277 RdfTestStatus::Failed => {
278 self.stats.failed += 1;
279 format_stats.failed += 1;
280 }
281 RdfTestStatus::Skipped => {
282 self.stats.skipped += 1;
283 }
284 RdfTestStatus::Timeout => {
285 self.stats.timeout += 1;
286 }
287 RdfTestStatus::Error => {
288 self.stats.error += 1;
289 }
290 RdfTestStatus::KnownFailure => {
291 self.stats.known_failures += 1;
292 }
293 }
294
295 self.results
296 .insert(test_result.test_id.clone(), test_result);
297 }
298
299 format_stats.compliance_percentage = if format_stats.total > 0 {
300 (format_stats.passed as f64 / format_stats.total as f64) * 100.0
301 } else {
302 0.0
303 };
304
305 self.stats.format_stats.insert(format.clone(), format_stats);
306 }
307
308 self.stats.total_tests = total_tests;
309 Ok(self.stats.clone())
310 }
311
312 async fn run_single_test(
314 &self,
315 manifest: &RdfTestManifest,
316 format: &RdfFormat,
317 ) -> RdfTestResult {
318 let start_time = std::time::Instant::now();
319 let test_type = self.determine_test_type(&manifest.test_type);
320
321 let test_future = self.execute_test(manifest, format, &test_type);
323 let timeout_duration = Duration::from_secs(self.config.test_timeout_seconds);
324
325 let (status, error_message, actual_quads) =
326 match timeout(timeout_duration, test_future).await {
327 Ok(result) => result,
328 Err(_) => (
329 RdfTestStatus::Timeout,
330 Some("Test execution timed out".to_string()),
331 None,
332 ),
333 };
334
335 let execution_time = start_time.elapsed().as_millis() as u64;
336
337 RdfTestResult {
338 test_id: manifest.id.clone(),
339 test_name: manifest.name.clone(),
340 test_type,
341 format: format.clone(),
342 status,
343 execution_time_ms: execution_time,
344 error_message,
345 expected_quads: None, actual_quads,
347 }
348 }
349
350 async fn execute_test(
352 &self,
353 manifest: &RdfTestManifest,
354 format: &RdfFormat,
355 test_type: &RdfTestType,
356 ) -> (RdfTestStatus, Option<String>, Option<usize>) {
357 match test_type {
358 RdfTestType::PositiveParser | RdfTestType::PositiveSyntax => {
359 self.run_positive_test(manifest, format).await
360 }
361 RdfTestType::NegativeParser | RdfTestType::NegativeSyntax => {
362 self.run_negative_test(manifest, format).await
363 }
364 RdfTestType::Evaluation => self.run_evaluation_test(manifest, format).await,
365 }
366 }
367
368 async fn run_positive_test(
370 &self,
371 manifest: &RdfTestManifest,
372 format: &RdfFormat,
373 ) -> (RdfTestStatus, Option<String>, Option<usize>) {
374 let input_data = match self.load_test_data(manifest).await {
375 Ok(data) => data,
376 Err(e) => return (RdfTestStatus::Error, Some(e.to_string()), None),
377 };
378
379 let parser = RdfParser::new(format.clone());
380 let mut quad_count = 0;
381
382 for quad_result in parser.for_slice(input_data.as_bytes()) {
383 match quad_result {
384 Ok(_) => quad_count += 1,
385 Err(e) => {
386 return (
387 RdfTestStatus::Failed,
388 Some(format!("Parsing failed: {e}")),
389 Some(quad_count),
390 );
391 }
392 }
393 }
394
395 (RdfTestStatus::Passed, None, Some(quad_count))
396 }
397
398 async fn run_negative_test(
400 &self,
401 manifest: &RdfTestManifest,
402 format: &RdfFormat,
403 ) -> (RdfTestStatus, Option<String>, Option<usize>) {
404 let input_data = match self.load_test_data(manifest).await {
405 Ok(data) => data,
406 Err(e) => return (RdfTestStatus::Error, Some(e.to_string()), None),
407 };
408
409 let parser = RdfParser::new(format.clone());
410 let mut had_error = false;
411
412 for quad_result in parser.for_slice(input_data.as_bytes()) {
413 if quad_result.is_err() {
414 had_error = true;
415 break;
416 }
417 }
418
419 if had_error {
420 (RdfTestStatus::Passed, None, None)
421 } else {
422 (
423 RdfTestStatus::Failed,
424 Some("Expected parsing to fail but it succeeded".to_string()),
425 None,
426 )
427 }
428 }
429
430 async fn run_evaluation_test(
432 &self,
433 manifest: &RdfTestManifest,
434 format: &RdfFormat,
435 ) -> (RdfTestStatus, Option<String>, Option<usize>) {
436 let input_data = match self.load_test_data(manifest).await {
437 Ok(data) => data,
438 Err(e) => return (RdfTestStatus::Error, Some(e.to_string()), None),
439 };
440
441 let parser = RdfParser::new(format.clone());
443 let mut parsed_quads = Vec::new();
444
445 for quad_result in parser.for_slice(input_data.as_bytes()) {
446 match quad_result {
447 Ok(quad) => parsed_quads.push(quad),
448 Err(e) => {
449 return (
450 RdfTestStatus::Failed,
451 Some(format!("Parsing failed: {e}")),
452 Some(parsed_quads.len()),
453 );
454 }
455 }
456 }
457
458 if parsed_quads.is_empty() && !input_data.trim().is_empty() {
461 (
462 RdfTestStatus::Failed,
463 Some("No quads parsed from non-empty input".to_string()),
464 Some(0),
465 )
466 } else {
467 (RdfTestStatus::Passed, None, Some(parsed_quads.len()))
468 }
469 }
470
471 async fn load_test_data(&self, manifest: &RdfTestManifest) -> Result<String> {
473 let file_path = match &manifest.action {
474 RdfTestAction::FilePath(path) => path.clone(),
475 RdfTestAction::Complex { input, .. } => input.clone(),
476 };
477
478 let local_paths = vec![
480 file_path.clone(),
481 format!("tests/w3c/{}", file_path),
482 format!("tests/data/{}", file_path),
483 ];
484
485 for path in local_paths {
486 if let Ok(content) = fs::read_to_string(&path) {
487 return Ok(content);
488 }
489 }
490
491 self.generate_sample_test_data(manifest)
493 }
494
495 fn generate_sample_test_data(&self, manifest: &RdfTestManifest) -> Result<String> {
497 let test_type = self.determine_test_type(&manifest.test_type);
498
499 match test_type {
500 RdfTestType::PositiveParser | RdfTestType::PositiveSyntax => {
501 self.generate_positive_test_data(&manifest.format)
502 }
503 RdfTestType::NegativeParser | RdfTestType::NegativeSyntax => {
504 self.generate_negative_test_data(&manifest.format)
505 }
506 RdfTestType::Evaluation => self.generate_evaluation_test_data(&manifest.format),
507 }
508 }
509
510 fn generate_positive_test_data(&self, format: &str) -> Result<String> {
512 match format.to_lowercase().as_str() {
513 "turtle" | "ttl" => Ok(r#"
514@prefix ex: <http://example.org/> .
515@prefix foaf: <http://xmlns.com/foaf/0.1/> .
516
517ex:alice a foaf:Person ;
518 foaf:name "Alice Smith" ;
519 foaf:age 30 ;
520 foaf:knows ex:bob .
521
522ex:bob a foaf:Person ;
523 foaf:name "Bob Jones" ;
524 foaf:age 25 .
525"#.to_string()),
526 "ntriples" | "nt" => Ok(r#"
527<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
528<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
529<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
530<http://example.org/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> .
531<http://example.org/bob> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
532<http://example.org/bob> <http://xmlns.com/foaf/0.1/name> "Bob Jones" .
533<http://example.org/bob> <http://xmlns.com/foaf/0.1/age> "25"^^<http://www.w3.org/2001/XMLSchema#integer> .
534"#.to_string()),
535 "nquads" | "nq" => Ok(r#"
536<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> <http://example.org/graph1> .
537<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" <http://example.org/graph1> .
538<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> <http://example.org/graph1> .
539<http://example.org/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> <http://example.org/graph1> .
540<http://example.org/bob> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> <http://example.org/graph2> .
541<http://example.org/bob> <http://xmlns.com/foaf/0.1/name> "Bob Jones" <http://example.org/graph2> .
542<http://example.org/bob> <http://xmlns.com/foaf/0.1/age> "25"^^<http://www.w3.org/2001/XMLSchema#integer> <http://example.org/graph2> .
543"#.to_string()),
544 "trig" => Ok(r#"
545@prefix ex: <http://example.org/> .
546@prefix foaf: <http://xmlns.com/foaf/0.1/> .
547
548ex:graph1 {
549 ex:alice a foaf:Person ;
550 foaf:name "Alice Smith" ;
551 foaf:age 30 ;
552 foaf:knows ex:bob .
553}
554
555ex:graph2 {
556 ex:bob a foaf:Person ;
557 foaf:name "Bob Jones" ;
558 foaf:age 25 .
559}
560"#.to_string()),
561 "jsonld" => Ok(r#"
562{
563 "@context": {
564 "foaf": "http://xmlns.com/foaf/0.1/",
565 "ex": "http://example.org/",
566 "name": "foaf:name",
567 "age": "foaf:age",
568 "knows": "foaf:knows"
569 },
570 "@graph": [
571 {
572 "@id": "ex:alice",
573 "@type": "foaf:Person",
574 "name": "Alice Smith",
575 "age": 30,
576 "knows": {"@id": "ex:bob"}
577 },
578 {
579 "@id": "ex:bob",
580 "@type": "foaf:Person",
581 "name": "Bob Jones",
582 "age": 25
583 }
584 ]
585}
586"#.to_string()),
587 _ => Ok("<http://example.org/s> <http://example.org/p> <http://example.org/o> .".to_string()),
588 }
589 }
590
591 fn generate_negative_test_data(&self, format: &str) -> Result<String> {
593 match format.to_lowercase().as_str() {
594 "turtle" | "ttl" => Ok(r#"
595@prefix ex: <http://example.org/> .
596@prefix : <invalid-uri> . # Invalid prefix URI
597
598ex:alice a foaf:Person ; # Missing prefix declaration for foaf
599 foaf:name "Alice Smith"
600 # Missing semicolon and period
601"#.to_string()),
602 "ntriples" | "nt" => Ok(r#"
603<http://example.org/alice> <http://example.org/predicate> "literal with unescaped quote" .
604<invalid-uri> <http://example.org/predicate> <http://example.org/object> .
605<http://example.org/subject> <http://example.org/predicate>
606"#.to_string()),
607 "nquads" | "nq" => Ok(r#"
608<http://example.org/alice> <http://example.org/predicate> "literal" <invalid-graph-uri> .
609<http://example.org/subject> <http://example.org/predicate> <http://example.org/object> missing-graph .
610"#.to_string()),
611 "trig" => Ok(r#"
612@prefix ex: <http://example.org/> .
613
614invalid-graph-name {
615 ex:alice ex:predicate "value"
616 # Missing period and closing brace
617"#.to_string()),
618 "jsonld" => Ok(r#"
619{
620 "@context": "invalid-context-url",
621 "@id": "ex:alice",
622 "@type": "foaf:Person",
623 "foaf:name": "Alice Smith"
624 # Missing comma and incomplete JSON
625"#.to_string()),
626 _ => Ok("invalid RDF content with syntax errors".to_string()),
627 }
628 }
629
630 fn generate_evaluation_test_data(&self, format: &str) -> Result<String> {
632 match format.to_lowercase().as_str() {
634 "turtle" | "ttl" => Ok(r#"
635@prefix ex: <http://example.org/> .
636@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
637@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
638@prefix foaf: <http://xmlns.com/foaf/0.1/> .
639@prefix dc: <http://purl.org/dc/elements/1.1/> .
640
641ex:dataset a <http://www.w3.org/ns/dcat#Dataset> ;
642 dc:title "Sample Dataset"@en, "Exemple de jeu de données"@fr ;
643 dc:description """This is a multi-line description
644 with special characters: ñ, ü, €, 中文""" ;
645 dc:created "2023-01-01T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> ;
646 ex:hasContact [
647 a foaf:Person ;
648 foaf:name "Data Manager" ;
649 foaf:mbox <mailto:manager@example.org>
650 ] ;
651 ex:topics ( ex:science ex:technology ex:research ) .
652
653ex:science rdfs:label "Science" .
654ex:technology rdfs:label "Technology" .
655ex:research rdfs:label "Research" .
656"#
657 .to_string()),
658 _ => self.generate_positive_test_data(format),
659 }
660 }
661
662 fn determine_test_type(&self, type_strings: &[String]) -> RdfTestType {
664 for type_str in type_strings {
665 if type_str.contains("PositiveParserTest") {
666 return RdfTestType::PositiveParser;
667 } else if type_str.contains("NegativeParserTest") {
668 return RdfTestType::NegativeParser;
669 } else if type_str.contains("PositiveSyntaxTest") {
670 return RdfTestType::PositiveSyntax;
671 } else if type_str.contains("NegativeSyntaxTest") {
672 return RdfTestType::NegativeSyntax;
673 } else if type_str.contains("EvaluationTest") {
674 return RdfTestType::Evaluation;
675 }
676 }
677 RdfTestType::PositiveParser }
679
680 fn should_run_test(&self, manifest: &RdfTestManifest) -> bool {
682 for filter in &self.config.test_filters {
684 if let Some(name_pattern) = &filter.name_pattern {
685 if !manifest.name.contains(name_pattern) {
686 return false;
687 }
688 }
689
690 if filter.approved_only && manifest.approval.is_empty() {
691 return false;
692 }
693 }
694
695 let test_type = self.determine_test_type(&manifest.test_type);
697 self.config.enabled_test_types.contains(&test_type)
698 }
699
700 fn get_manifest_path(&self, format: &RdfFormat) -> String {
702 match format {
703 RdfFormat::Turtle => "turtle/manifest.ttl".to_string(),
704 RdfFormat::NTriples => "ntriples/manifest.ttl".to_string(),
705 RdfFormat::NQuads => "nquads/manifest.ttl".to_string(),
706 RdfFormat::TriG => "trig/manifest.ttl".to_string(),
707 RdfFormat::RdfXml => "rdf-xml/manifest.ttl".to_string(),
708 _ => "manifest.ttl".to_string(),
709 }
710 }
711
712 async fn load_format_manifest(
714 &self,
715 manifest_path: &str,
716 format: RdfFormat,
717 ) -> Result<Vec<RdfTestManifest>> {
718 let local_path = format!("tests/w3c/{manifest_path}");
720
721 if let Ok(content) = fs::read_to_string(&local_path) {
722 return self.parse_manifest_content(&content, format).await;
723 }
724
725 let sample_manifests = self.generate_sample_manifests(format.clone());
728
729 if self.config.verbose_logging {
730 println!(
731 "Generated {} sample test manifests for {format:?}",
732 sample_manifests.len()
733 );
734 }
735
736 Ok(sample_manifests)
737 }
738
739 async fn parse_manifest_content(
741 &self,
742 content: &str,
743 format: RdfFormat,
744 ) -> Result<Vec<RdfTestManifest>> {
745 let mut manifests = Vec::new();
748
749 for (i, line) in content.lines().enumerate() {
751 if line.trim().starts_with(":test") {
752 let test_id = format!(
753 "test_{}_{}_{}",
754 format.to_string().to_lowercase(),
755 i,
756 std::time::SystemTime::now()
757 .duration_since(std::time::UNIX_EPOCH)
758 .unwrap_or_default()
759 .as_secs()
760 % 1000
761 );
762
763 let manifest = RdfTestManifest {
764 id: test_id.clone(),
765 test_type: vec!["http://www.w3.org/ns/rdftest#PositiveParserTest".to_string()],
766 name: format!("W3C {format:?} Test {i}"),
767 comment: Some(format!("Test case for {format:?} format parsing")),
768 action: RdfTestAction::FilePath(format!(
769 "test_data_{i}.{}",
770 self.get_file_extension(&format)
771 )),
772 result: None,
773 approval: "Approved".to_string(),
774 format: format.to_string(),
775 };
776
777 manifests.push(manifest);
778 }
779 }
780
781 Ok(manifests)
782 }
783
784 fn generate_sample_manifests(&self, format: RdfFormat) -> Vec<RdfTestManifest> {
786 let mut manifests = Vec::new();
787 let extension = self.get_file_extension(&format);
788
789 for i in 0..5 {
791 manifests.push(RdfTestManifest {
792 id: format!(
793 "positive_parser_test_{}_{}",
794 format.to_string().to_lowercase(),
795 i
796 ),
797 test_type: vec!["http://www.w3.org/ns/rdftest#PositiveParserTest".to_string()],
798 name: format!("{format:?} Positive Parser Test {i}"),
799 comment: Some(format!("Positive parsing test for {format:?} format")),
800 action: RdfTestAction::FilePath(format!("positive_test_{i}.{extension}")),
801 result: Some(format!("positive_result_{i}.nq")),
802 approval: "Approved".to_string(),
803 format: format.to_string(),
804 });
805 }
806
807 for i in 0..3 {
809 manifests.push(RdfTestManifest {
810 id: format!(
811 "negative_parser_test_{}_{}",
812 format.to_string().to_lowercase(),
813 i
814 ),
815 test_type: vec!["http://www.w3.org/ns/rdftest#NegativeParserTest".to_string()],
816 name: format!("{format:?} Negative Parser Test {i}"),
817 comment: Some(format!(
818 "Negative parsing test for {format:?} format - should fail"
819 )),
820 action: RdfTestAction::FilePath(format!("negative_test_{i}.{extension}")),
821 result: None,
822 approval: "Approved".to_string(),
823 format: format.to_string(),
824 });
825 }
826
827 for i in 0..3 {
829 manifests.push(RdfTestManifest {
830 id: format!("syntax_test_{}_{}", format.to_string().to_lowercase(), i),
831 test_type: vec!["http://www.w3.org/ns/rdftest#PositiveSyntaxTest".to_string()],
832 name: format!("{format:?} Syntax Test {i}"),
833 comment: Some(format!("Syntax validation test for {format:?} format")),
834 action: RdfTestAction::FilePath(format!("syntax_test_{i}.{extension}")),
835 result: None,
836 approval: "Approved".to_string(),
837 format: format.to_string(),
838 });
839 }
840
841 manifests
842 }
843
844 fn get_file_extension(&self, format: &RdfFormat) -> String {
846 match format {
847 RdfFormat::Turtle => "ttl".to_string(),
848 RdfFormat::NTriples => "nt".to_string(),
849 RdfFormat::NQuads => "nq".to_string(),
850 RdfFormat::TriG => "trig".to_string(),
851 RdfFormat::RdfXml => "rdf".to_string(),
852 RdfFormat::JsonLd { .. } => "jsonld".to_string(),
853 _ => "rdf".to_string(),
854 }
855 }
856
857 pub fn generate_report(&self) -> String {
859 let mut report = String::new();
860
861 report.push_str("# W3C RDF Format Compliance Report\n\n");
862 report.push_str(&format!("Total tests: {}\n", self.stats.total_tests));
863 report.push_str(&format!("Passed: {}\n", self.stats.passed));
864 report.push_str(&format!("Failed: {}\n", self.stats.failed));
865 report.push_str(&format!("Skipped: {}\n", self.stats.skipped));
866 report.push_str(&format!("Timeout: {}\n", self.stats.timeout));
867 report.push_str(&format!("Error: {}\n", self.stats.error));
868 report.push_str(&format!("Known failures: {}\n", self.stats.known_failures));
869
870 let overall_compliance = if self.stats.total_tests > 0 {
871 (self.stats.passed as f64 / self.stats.total_tests as f64) * 100.0
872 } else {
873 0.0
874 };
875 report.push_str(&format!("Overall compliance: {overall_compliance:.2}%\n\n"));
876
877 report.push_str("## Format-specific results:\n\n");
878 for (format, stats) in &self.stats.format_stats {
879 report.push_str(&format!("### {format:?}\n"));
880 report.push_str(&format!("- Total: {}\n", stats.total));
881 report.push_str(&format!("- Passed: {}\n", stats.passed));
882 report.push_str(&format!("- Failed: {}\n", stats.failed));
883 report.push_str(&format!(
884 "- Compliance: {:.2}%\n\n",
885 stats.compliance_percentage
886 ));
887 }
888
889 report
890 }
891
892 pub fn get_stats(&self) -> &RdfComplianceStats {
894 &self.stats
895 }
896
897 pub fn get_results(&self) -> &HashMap<String, RdfTestResult> {
899 &self.results
900 }
901}
902
903pub async fn run_w3c_compliance_tests(
905 config: Option<W3cRdfTestConfig>,
906) -> Result<RdfComplianceStats> {
907 let config = config.unwrap_or_default();
908 let mut runner = W3cRdfTestSuiteRunner::new(config)?;
909
910 runner.load_manifests().await?;
911 let stats = runner.run_tests().await?;
912
913 println!("{}", runner.generate_report());
914 Ok(stats)
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920
921 #[test]
922 fn test_config_creation() {
923 let config = W3cRdfTestConfig::default();
924 assert!(config.enabled_formats.contains(&RdfFormat::Turtle));
925 assert!(config
926 .enabled_test_types
927 .contains(&RdfTestType::PositiveParser));
928 assert_eq!(config.test_timeout_seconds, 30);
929 }
930
931 #[test]
932 fn test_test_type_determination() {
933 let config = W3cRdfTestConfig::default();
934 let runner = W3cRdfTestSuiteRunner::new(config).expect("construction should succeed");
935
936 let test_types = vec!["http://www.w3.org/ns/rdftest#PositiveParserTest".to_string()];
937 assert_eq!(
938 runner.determine_test_type(&test_types),
939 RdfTestType::PositiveParser
940 );
941
942 let test_types = vec!["http://www.w3.org/ns/rdftest#NegativeSyntaxTest".to_string()];
943 assert_eq!(
944 runner.determine_test_type(&test_types),
945 RdfTestType::NegativeSyntax
946 );
947 }
948
949 #[tokio::test]
950 async fn test_runner_creation() {
951 let config = W3cRdfTestConfig::default();
952 let runner = W3cRdfTestSuiteRunner::new(config);
953 assert!(runner.is_ok());
954 }
955
956 #[test]
957 fn test_compliance_stats() {
958 let stats = RdfComplianceStats {
959 total_tests: 100,
960 passed: 85,
961 failed: 15,
962 ..Default::default()
963 };
964
965 assert_eq!(stats.total_tests, 100);
966 assert_eq!(stats.passed, 85);
967 assert_eq!(stats.failed, 15);
968 }
969}