From a97149593f311212747d0d22d7606bbd19f1abed Mon Sep 17 00:00:00 2001 From: Tpt Date: Wed, 6 Jun 2018 17:13:40 +0200 Subject: [PATCH] Improves W3C tests discovery and implementation --- src/store/memory.rs | 37 +++ tests/client.rs | 52 ---- tests/rdf_test_cases.rs | 526 +++++++++++++++++++++++++------------ tests/sparql_test_cases.rs | 69 ----- 4 files changed, 391 insertions(+), 293 deletions(-) delete mode 100644 tests/client.rs delete mode 100644 tests/sparql_test_cases.rs diff --git a/src/store/memory.rs b/src/store/memory.rs index b7a987d2..a104125f 100644 --- a/src/store/memory.rs +++ b/src/store/memory.rs @@ -1,4 +1,5 @@ use model::data::*; +use model::vocab::rdf; use std::collections::HashSet; use std::fmt; use std::iter::FromIterator; @@ -84,6 +85,13 @@ impl MemoryGraph { self.subjects_for_predicate_object(predicate, object).nth(0) } + pub fn values_for_list<'a>(&'a self, root: NamedOrBlankNode) -> ListIterator<'a> { + ListIterator { + graph: self, + current_node: Some(root), + } + } + pub fn len(&self) -> usize { self.triples.len() } @@ -146,3 +154,32 @@ impl<'a> Extend<&'a Triple> for MemoryGraph { self.triples.extend(iter.into_iter().cloned()) } } + +pub struct ListIterator<'a> { + graph: &'a MemoryGraph, + current_node: Option, +} + +impl<'a> Iterator for ListIterator<'a> { + type Item = Term; + + fn next(&mut self) -> Option { + match self.current_node.clone() { + Some(current) => { + let result = self.graph + .object_for_subject_predicate(¤t, &rdf::FIRST)? + .clone(); + self.current_node = match self.graph + .object_for_subject_predicate(¤t, &rdf::REST) + { + Some(Term::NamedNode(n)) if *n == *rdf::NIL => None, + Some(Term::NamedNode(n)) => Some(n.clone().into()), + Some(Term::BlankNode(n)) => Some(n.clone().into()), + _ => None, + }; + Some(result) + } + None => None, + } + } +} diff --git a/tests/client.rs b/tests/client.rs deleted file mode 100644 index 2b8f5859..00000000 --- a/tests/client.rs +++ /dev/null @@ -1,52 +0,0 @@ -extern crate reqwest; -extern crate rudf; -extern crate url; - -use reqwest::Client; -use reqwest::Response; -use rudf::rio::ntriples::read_ntriples; -use rudf::rio::turtle::read_turtle; -use rudf::rio::RioError; -use rudf::rio::RioResult; -use rudf::sparql::ast::Query; -use rudf::sparql::parser::read_sparql_query; -use rudf::store::memory::MemoryGraph; -use std::error::Error; -use url::Url; - -pub struct RDFClient { - client: Client, -} - -impl Default for RDFClient { - fn default() -> Self { - Self { - client: Client::new(), - } - } -} - -impl RDFClient { - pub fn load_turtle(&self, url: Url) -> RioResult { - Ok(read_turtle(self.get(&url)?, Some(url))?.collect()) - } - - pub fn load_ntriples(&self, url: Url) -> RioResult { - read_ntriples(self.get(&url)?).collect() - } - - pub fn load_sparql_query(&self, url: Url) -> RioResult { - read_sparql_query(self.get(&url)?, Some(url)) - } - - fn get(&self, url: &Url) -> RioResult { - match self.client.get(url.clone()).send() { - Ok(response) => Ok(response), - Err(error) => if error.description() == "message is incomplete" { - self.get(url) - } else { - Err(RioError::new(error)) - }, - } - } -} diff --git a/tests/rdf_test_cases.rs b/tests/rdf_test_cases.rs index dafd1a0f..740f41b6 100644 --- a/tests/rdf_test_cases.rs +++ b/tests/rdf_test_cases.rs @@ -6,218 +6,400 @@ extern crate reqwest; extern crate rudf; extern crate url; -mod client; - -use client::RDFClient; +use reqwest::Client; +use reqwest::Response; use rudf::model::data::*; use rudf::model::vocab::rdf; use rudf::model::vocab::rdfs; +use rudf::rio::ntriples::read_ntriples; +use rudf::rio::turtle::read_turtle; +use rudf::rio::RioError; +use rudf::rio::RioResult; +use rudf::sparql::ast::Query; +use rudf::sparql::parser::read_sparql_query; use rudf::store::isomorphism::GraphIsomorphism; -use std::str::FromStr; +use rudf::store::memory::MemoryGraph; +use std::error::Error; +use std::fmt; use url::Url; -mod mf { - use rudf::model::data::NamedNode; - use std::str::FromStr; - - lazy_static! { - pub static ref ACTION: NamedNode = NamedNode::from_str( - "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#action" - ).unwrap(); - pub static ref RESULT: NamedNode = NamedNode::from_str( - "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#result" - ).unwrap(); - } -} - #[test] fn turtle_w3c_testsuite() { let manifest_url = Url::parse("http://www.w3.org/2013/TurtleTests/manifest.ttl").unwrap(); let client = RDFClient::default(); - let manifest = client.load_turtle(manifest_url.clone()).unwrap(); - let rdft_test_turtle_positive_syntax = Term::from( - NamedNode::from_str("http://www.w3.org/ns/rdftest#TestTurtlePositiveSyntax").unwrap(), - ); - let rdft_test_turtle_negative_syntax = Term::from( - NamedNode::from_str("http://www.w3.org/ns/rdftest#TestTurtleNegativeSyntax").unwrap(), - ); - let rdft_test_turtle_eval = - Term::from(NamedNode::from_str("http://www.w3.org/ns/rdftest#TestTurtleEval").unwrap()); - let rdft_test_turtle_negative_eval = Term::from( - NamedNode::from_str("http://www.w3.org/ns/rdftest#TestTurtleNegativeEval").unwrap(), - ); //TODO: make blacklist pass - let test_blacklist: Vec = vec![ + let test_blacklist = vec![ //UTF-8 broken surrogates in BNode ids NamedNode::new( manifest_url .join("#prefix_with_PN_CHARS_BASE_character_boundaries") .unwrap(), - ).into(), + ), NamedNode::new( manifest_url .join("#labeled_blank_node_with_PN_CHARS_BASE_character_boundaries") .unwrap(), - ).into(), + ), NamedNode::new( manifest_url .join("#localName_with_assigned_nfc_PN_CHARS_BASE_character_boundaries") .unwrap(), - ).into(), + ), NamedNode::new( manifest_url .join("#localName_with_nfc_PN_CHARS_BASE_character_boundaries") .unwrap(), - ).into(), + ), ]; - manifest - .subjects_for_predicate_object(&rdf::TYPE, &rdft_test_turtle_positive_syntax) - .for_each(|test| { - let comment = manifest - .object_for_subject_predicate(test, &rdfs::COMMENT) - .unwrap(); - if let Some(Term::NamedNode(file)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - if let Err(error) = client.load_turtle(file.url().clone()) { - assert!( - false, - "Failure on positive syntax file {} about {} with error: {}", - file, comment, error - ) - } - } - }); - manifest - .subjects_for_predicate_object(&rdf::TYPE, &rdft_test_turtle_negative_syntax) - .for_each(|test| { - let comment = manifest - .object_for_subject_predicate(test, &rdfs::COMMENT) - .unwrap(); - if let Some(Term::NamedNode(file)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - assert!( - client.load_turtle(file.url().clone()).is_err(), - "Failure on negative syntax test file {} about {}", - file, - comment - ); - } - }); - manifest - .subjects_for_predicate_object(&rdf::TYPE, &rdft_test_turtle_eval) - .for_each(|test| { - if test_blacklist.contains(test) { - return; + for test_result in TestManifest::new(&client, manifest_url) { + let test = test_result.unwrap(); + if test_blacklist.contains(&test.id) { + return; + } + if test.kind == "TestTurtlePositiveSyntax" { + if let Err(error) = client.load_turtle(test.action.clone()) { + assert!(false, "Failure on {} with error: {}", test, error) } - let comment = manifest - .object_for_subject_predicate(test, &rdfs::COMMENT) - .unwrap(); - if let Some(Term::NamedNode(input)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - if let Some(Term::NamedNode(result)) = - manifest.object_for_subject_predicate(test, &mf::RESULT) - { - match client.load_turtle(input.url().clone()) { - Ok(action_graph) => match client.load_turtle(result.url().clone()) { - Ok(result_graph) => assert!( - action_graph.is_isomorphic(&result_graph), - "Failure on positive evaluation test file {} against {} about {}. Expected file:\n{}\nParsed file:\n{}\n", - input, - result, - comment, - action_graph, - result_graph - ), - Err(error) => assert!( - false, - "Failure to parse the Turtle result file {} about {} with error: {}", - result, comment, error - ) - }, + } else if test.kind == "TestTurtleNegativeSyntax" { + assert!( + client.load_turtle(test.action.clone()).is_err(), + "Failure on {}", + test + ); + } else if test.kind == "TestTurtleEval" { + match client.load_turtle(test.action.clone()) { + Ok(action_graph) => match client.load_turtle(test.result.clone().unwrap()) { + Ok(result_graph) => assert!( + action_graph.is_isomorphic(&result_graph), + "Failure on {}. Expected file:\n{}\nParsed file:\n{}\n", + test, + action_graph, + result_graph + ), Err(error) => assert!( false, - "Failure to parse the Turtle input file {} about {} with error: {}", - input, comment, error - ) - } - } - } - }); - manifest - .subjects_for_predicate_object(&rdf::TYPE, &rdft_test_turtle_negative_eval) - .for_each(|test| { - let comment = manifest - .object_for_subject_predicate(test, &rdfs::COMMENT) - .unwrap(); - if let Some(Term::NamedNode(file)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - if let Some(Term::NamedNode(result)) = - manifest.object_for_subject_predicate(test, &mf::RESULT) - { - let action_graph = client.load_turtle(file.url().clone()); - let result_graph = client.load_turtle(result.url().clone()); - assert!( - !action_graph.unwrap().is_isomorphic(&result_graph.unwrap()), - "Failure on positive evaluation test file {} about {}", - file, - comment - ); - } + "Failure to parse the Turtle result file {} of {} with error: {}", + test.result.clone().unwrap(), + test, + error + ), + }, + Err(error) => assert!(false, "Failure to parse {} with error: {}", test, error), } - }); + } else if test.kind == "TestTurtleNegativeEval" { + let action_graph = client.load_turtle(test.action.clone()); + let result_graph = test.result + .clone() + .map(|r| client.load_turtle(r)) + .unwrap_or_else(|| Ok(MemoryGraph::default())); + assert!( + action_graph.is_err() + || !action_graph.unwrap().is_isomorphic(&result_graph.unwrap()), + "Failure on {}", + test + ); + } else { + assert!(false, "Not supported test: {}", test); + } + } } #[test] fn ntriples_w3c_testsuite() { let client = RDFClient::default(); - let manifest = client - .load_turtle(Url::parse("http://www.w3.org/2013/N-TriplesTests/manifest.ttl").unwrap()) - .unwrap(); - let rdft_test_ntriples_positive_syntax = Term::from( - NamedNode::from_str("http://www.w3.org/ns/rdftest#TestNTriplesPositiveSyntax").unwrap(), - ); - let rdft_test_ntriples_negative_syntax = Term::from( - NamedNode::from_str("http://www.w3.org/ns/rdftest#TestNTriplesNegativeSyntax").unwrap(), - ); - - manifest - .subjects_for_predicate_object(&rdf::TYPE, &rdft_test_ntriples_positive_syntax) - .for_each(|test| { - let comment = manifest - .object_for_subject_predicate(test, &rdfs::COMMENT) - .unwrap(); - if let Some(Term::NamedNode(file)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - if let Err(error) = client.load_ntriples(file.url().clone()) { - assert!( - false, - "Failure on positive syntax file {} about {} with error: {}", - file, comment, error - ) + let manifest_url = Url::parse("http://www.w3.org/2013/N-TriplesTests/manifest.ttl").unwrap(); + + for test_result in TestManifest::new(&client, manifest_url) { + let test = test_result.unwrap(); + if test.kind == "TestNTriplesPositiveSyntax" { + if let Err(error) = client.load_ntriples(test.action.clone()) { + assert!(false, "Failure on {} with error: {}", test, error) + } + } else if test.kind == "TestNTriplesNegativeSyntax" { + assert!( + client.load_ntriples(test.action.clone()).is_err(), + "Failure on {}", + test + ); + } else { + assert!(false, "Not supported test: {}", test); + } + } +} + +#[test] +fn sparql_w3c_syntax_testsuite() { + let manifest_url = Url::parse( + "http://www.w3.org/2009/sparql/docs/tests/data-sparql11/syntax-query/manifest.ttl", + ).unwrap(); + let client = RDFClient::default(); + + for test_result in TestManifest::new(&client, manifest_url) { + let test = test_result.unwrap(); + if test.kind == "PositiveSyntaxTest11" { + match client.load_sparql_query(test.action.clone()) { + Err(error) => assert!(false, "Failure on {} with error: {}", test, error), + Ok(query) => { + if let Err(error) = read_sparql_query(query.to_string().as_bytes(), None) { + assert!( + false, + "Failure tu deserialize \"{}\" of {} with error: {}", + query.to_string(), + test, + error + ) + } } } - }); - manifest - .subjects_for_predicate_object(&rdf::TYPE, &rdft_test_ntriples_negative_syntax) - .for_each(|test| { - let comment = manifest - .object_for_subject_predicate(test, &rdfs::COMMENT) - .unwrap(); - if let Some(Term::NamedNode(file)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - assert!( - client.load_ntriples(file.url().clone()).is_err(), - "Failure on negative syntax test file {} about {}", - file, - comment - ); + } else if test.kind == "NegativeSyntaxTest11" { + //TODO + /*assert!( + client.load_sparql_query(test.action.clone()).is_err(), + "Failure on {}", + test + );*/ + } else { + assert!(false, "Not supported test: {}", test); + } + } +} + +pub struct RDFClient { + client: Client, +} + +impl Default for RDFClient { + fn default() -> Self { + Self { + client: Client::new(), + } + } +} + +impl RDFClient { + pub fn load_turtle(&self, url: Url) -> RioResult { + Ok(read_turtle(self.get(&url)?, Some(url))?.collect()) + } + + pub fn load_ntriples(&self, url: Url) -> RioResult { + read_ntriples(self.get(&url)?).collect() + } + + pub fn load_sparql_query(&self, url: Url) -> RioResult { + read_sparql_query(self.get(&url)?, Some(url)) + } + + fn get(&self, url: &Url) -> RioResult { + match self.client.get(url.clone()).send() { + Ok(response) => Ok(response), + Err(error) => if error.description() == "message is incomplete" { + self.get(url) + } else { + Err(RioError::new(error)) + }, + } + } +} + +pub struct Test { + pub id: NamedNode, + pub kind: String, + pub name: Option, + pub comment: Option, + pub action: Url, + pub result: Option, +} + +impl fmt::Display for Test { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.kind)?; + for name in &self.name { + write!(f, " named \"{}\"", name)?; + } + for comment in &self.comment { + write!(f, " with comment \"{}\"", comment)?; + } + write!(f, " on file \"{}\"", self.action)?; + Ok(()) + } +} + +pub struct TestManifest<'a> { + client: &'a RDFClient, + graph: MemoryGraph, + tests_to_do: Vec, + manifests_to_do: Vec, +} + +impl<'a> TestManifest<'a> { + pub fn new(client: &'a RDFClient, url: Url) -> TestManifest<'a> { + Self { + client, + graph: MemoryGraph::default(), + tests_to_do: Vec::default(), + manifests_to_do: vec![url], + } + } +} + +pub mod mf { + use rudf::model::data::NamedNode; + use std::str::FromStr; + + lazy_static! { + pub static ref INCLUDE: NamedNode = NamedNode::from_str( + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#include" + ).unwrap(); + pub static ref ENTRIES: NamedNode = NamedNode::from_str( + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#entries" + ).unwrap(); + pub static ref ACTION: NamedNode = NamedNode::from_str( + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#action" + ).unwrap(); + pub static ref RESULT: NamedNode = NamedNode::from_str( + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#result" + ).unwrap(); + } +} + +impl<'a> Iterator for TestManifest<'a> { + type Item = Result; + + fn next(&mut self) -> Option> { + match self.tests_to_do.pop() { + Some(Term::NamedNode(test_node)) => { + let test_subject = NamedOrBlankNode::from(test_node.clone()); + let kind = match self.graph + .object_for_subject_predicate(&test_subject, &rdf::TYPE) + { + Some(Term::NamedNode(c)) => match c.value().split("#").last() { + Some(k) => k.to_string(), + None => return Some(Err(ManifestError::NoType)), + }, + _ => return Some(Err(ManifestError::NoType)), + }; + let name = match self.graph + .object_for_subject_predicate(&test_subject, &rdfs::COMMENT) + { + Some(Term::Literal(c)) => Some(c.value().to_string()), + _ => None, + }; + let comment = match self.graph + .object_for_subject_predicate(&test_subject, &rdfs::COMMENT) + { + Some(Term::Literal(c)) => Some(c.value().to_string()), + _ => None, + }; + let action = match self.graph + .object_for_subject_predicate(&test_subject, &*mf::ACTION) + { + Some(Term::NamedNode(n)) => n.url().clone(), + Some(_) => return Some(Err(ManifestError::InvalidAction)), + None => return Some(Err(ManifestError::ActionNotFound)), + }; + let result = match self.graph + .object_for_subject_predicate(&test_subject, &*mf::RESULT) + { + Some(Term::NamedNode(n)) => Some(n.url().clone()), + Some(_) => return Some(Err(ManifestError::InvalidResult)), + None => None, + }; + Some(Ok(Test { + id: test_node, + kind, + name, + comment, + action, + result, + })) + } + Some(_) => Some(Err(ManifestError::InvalidTestsList)), + None => { + match self.manifests_to_do.pop() { + Some(url) => { + let manifest = NamedOrBlankNode::from(NamedNode::new(url.clone())); + match self.client.load_turtle(url) { + Ok(g) => self.graph.extend(g.into_iter()), + Err(e) => return Some(Err(e.into())), + } + + // New manifests + match self.graph + .object_for_subject_predicate(&manifest, &*mf::INCLUDE) + { + Some(Term::BlankNode(list)) => { + self.manifests_to_do.extend( + self.graph + .values_for_list(list.clone().into()) + .flat_map(|m| match m { + Term::NamedNode(nm) => Some(nm.url().clone()), + _ => None, + }), + ); + } + Some(_) => return Some(Err(ManifestError::InvalidTestsList)), + None => (), + } + + // New tests + match self.graph + .object_for_subject_predicate(&manifest, &*mf::ENTRIES) + { + Some(Term::BlankNode(list)) => { + self.tests_to_do + .extend(self.graph.values_for_list(list.clone().into())); + } + Some(_) => return Some(Err(ManifestError::InvalidTestsList)), + None => (), + } + } + None => return None, + } + self.next() } - }); + } + } +} + +#[derive(Debug)] +pub enum ManifestError { + NoType, + ActionNotFound, + InvalidAction, + InvalidResult, + InvalidTestsList, + RioError(RioError), +} + +impl Error for ManifestError { + fn description(&self) -> &str { + match self { + ManifestError::NoType => "no type found on the test case", + ManifestError::ActionNotFound => "action not found", + ManifestError::InvalidAction => "invalid action", + ManifestError::InvalidResult => "invalid result", + ManifestError::InvalidTestsList => "invalid tests list", + ManifestError::RioError(e) => e.description(), + } + } + + fn cause(&self) -> Option<&Error> { + match self { + ManifestError::RioError(e) => Some(e), + _ => None, + } + } +} + +impl fmt::Display for ManifestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +impl From for ManifestError { + fn from(e: RioError) -> Self { + ManifestError::RioError(e) + } } diff --git a/tests/sparql_test_cases.rs b/tests/sparql_test_cases.rs deleted file mode 100644 index 43001b5b..00000000 --- a/tests/sparql_test_cases.rs +++ /dev/null @@ -1,69 +0,0 @@ -///! Integration tests based on [SPARQL 1.1 Test Cases](https://www.w3.org/2009/sparql/docs/tests/) - -#[macro_use] -extern crate lazy_static; -extern crate reqwest; -extern crate rudf; -extern crate url; - -mod client; - -use client::RDFClient; -use rudf::model::data::*; -use rudf::model::vocab::rdf; -use rudf::sparql::parser::read_sparql_query; -use url::Url; - -mod mf { - use rudf::model::data::NamedNode; - use std::str::FromStr; - - lazy_static! { - pub static ref ACTION: NamedNode = NamedNode::from_str( - "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#action" - ).unwrap(); - pub static ref RESULT: NamedNode = NamedNode::from_str( - "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#result" - ).unwrap(); - pub static ref POSITIVE_SYNTAX_TEST_11: NamedNode = NamedNode::from_str( - "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#PositiveSyntaxTest11" - ).unwrap(); - } -} - -#[test] -fn sparql_w3c_syntax_testsuite() { - let manifest_url = Url::parse( - "http://www.w3.org/2009/sparql/docs/tests/data-sparql11/syntax-query/manifest.ttl", - ).unwrap(); - let client = RDFClient::default(); - let manifest = client.load_turtle(manifest_url.clone()).unwrap(); - let mf_positive_syntax_test = Term::from(mf::POSITIVE_SYNTAX_TEST_11.clone()); - - manifest - .subjects_for_predicate_object(&rdf::TYPE, &mf_positive_syntax_test) - .for_each(|test| { - if let Some(Term::NamedNode(file)) = - manifest.object_for_subject_predicate(test, &mf::ACTION) - { - match client.load_sparql_query(file.url().clone()) { - Err(error) => assert!( - false, - "Failure on positive syntax file {} with error: {}", - file, error - ), - Ok(query) => { - if let Err(error) = read_sparql_query(query.to_string().as_bytes(), None) { - assert!( - false, - "Failure tu deserialize \"{}\" of file {} with error: {}", - query.to_string(), - file, - error - ) - } - } - } - } - }); -}