diff --git a/testsuite/rdf-tests b/testsuite/rdf-tests index 22cfdfda..7f556ae7 160000 --- a/testsuite/rdf-tests +++ b/testsuite/rdf-tests @@ -1 +1 @@ -Subproject commit 22cfdfda79153b6dba91844e69043c019c89fd84 +Subproject commit 7f556ae7c7d94ef63997e13a173603fba80db5d7 diff --git a/testsuite/src/files.rs b/testsuite/src/files.rs index 4d8b4809..657fcc9a 100644 --- a/testsuite/src/files.rs +++ b/testsuite/src/files.rs @@ -1,15 +1,16 @@ use anyhow::{anyhow, bail, Result}; use oxigraph::io::{DatasetFormat, DatasetParser, GraphFormat, GraphParser}; -use oxigraph::model::{Dataset, Graph, GraphNameRef}; -use oxigraph::store::Store; +use oxigraph::model::{Dataset, Graph}; use std::fs::File; use std::io::{BufRead, BufReader, Read}; use std::path::PathBuf; pub fn read_file(url: &str) -> Result { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push(if url.starts_with("http://w3c.github.io/rdf-tests/") { - url.replace("http://w3c.github.io/rdf-tests/", "rdf-tests/") + path.push(if url.starts_with("http://w3c.github.io/") { + url.replace("http://w3c.github.io/", "") + } else if url.starts_with("https://w3c.github.io/") { + url.replace("https://w3c.github.io/", "") } else if url.starts_with("http://www.w3.org/2013/RDFXMLTests/") { url.replace("http://www.w3.org/2013/RDFXMLTests/", "rdf-tests/rdf-xml/") } else if url.starts_with("http://www.w3.org/2001/sw/DataAccess/tests/data-r2/") { @@ -22,8 +23,6 @@ pub fn read_file(url: &str) -> Result { "http://www.w3.org/2009/sparql/docs/tests/", "rdf-tests/sparql11/", ) - } else if url.starts_with("https://w3c.github.io/rdf-star/") { - url.replace("https://w3c.github.io/", "") } else if url.starts_with("https://github.com/oxigraph/oxigraph/tests/") { url.replace( "https://github.com/oxigraph/oxigraph/tests/", @@ -41,47 +40,7 @@ pub fn read_file_to_string(url: &str) -> Result { Ok(buf) } -pub fn load_to_store<'a>( - url: &str, - store: &Store, - to_graph_name: impl Into>, -) -> Result<()> { - if url.ends_with(".nt") { - store.load_graph( - read_file(url)?, - GraphFormat::NTriples, - to_graph_name, - Some(url), - )? - } else if url.ends_with(".ttl") { - store.load_graph( - read_file(url)?, - GraphFormat::Turtle, - to_graph_name, - Some(url), - )? - } else if url.ends_with(".rdf") { - store.load_graph( - read_file(url)?, - GraphFormat::RdfXml, - to_graph_name, - Some(url), - )? - } else if url.ends_with(".nq") { - store.load_dataset(read_file(url)?, DatasetFormat::NQuads, Some(url))? - } else if url.ends_with(".trig") { - store.load_dataset(read_file(url)?, DatasetFormat::TriG, Some(url))? - } else { - bail!("Serialization type not found for {url}"); - } - Ok(()) -} - -pub fn load_to_graph(url: &str, graph: &mut Graph) -> Result<()> { - let format = url - .rsplit_once('.') - .and_then(|(_, extension)| GraphFormat::from_extension(extension)) - .ok_or_else(|| anyhow!("Serialization type not found for {url}"))?; +pub fn load_to_graph(url: &str, graph: &mut Graph, format: GraphFormat) -> Result<()> { let parser = GraphParser::from_format(format).with_base_iri(url)?; for t in parser.read_triples(read_file(url)?)? { graph.insert(&t?); @@ -89,37 +48,34 @@ pub fn load_to_graph(url: &str, graph: &mut Graph) -> Result<()> { Ok(()) } -pub fn load_graph(url: &str) -> Result { +pub fn load_graph(url: &str, format: GraphFormat) -> Result { let mut graph = Graph::new(); - load_to_graph(url, &mut graph)?; + load_to_graph(url, &mut graph, format)?; Ok(graph) } -pub fn load_to_dataset<'a>( - url: &str, - dataset: &mut Dataset, - to_graph_name: impl Into>, -) -> Result<()> { - let to_graph_name = to_graph_name.into(); - let extension = url.rsplit_once('.').map(|(_, ext)| ext); - if let Some(format) = extension.and_then(GraphFormat::from_extension) { - let parser = GraphParser::from_format(format).with_base_iri(url)?; - for t in parser.read_triples(read_file(url)?)? { - dataset.insert(&t?.in_graph(to_graph_name)); - } - } else if let Some(format) = extension.and_then(DatasetFormat::from_extension) { - let parser = DatasetParser::from_format(format).with_base_iri(url)?; - for q in parser.read_quads(read_file(url)?)? { - dataset.insert(&q?); - } - } else { - bail!("Serialization type not found for {url}") +pub fn guess_graph_format(url: &str) -> Result { + url.rsplit_once('.') + .and_then(|(_, extension)| GraphFormat::from_extension(extension)) + .ok_or_else(|| anyhow!("Serialization type not found for {url}")) +} + +pub fn load_to_dataset(url: &str, dataset: &mut Dataset, format: DatasetFormat) -> Result<()> { + let parser = DatasetParser::from_format(format).with_base_iri(url)?; + for q in parser.read_quads(read_file(url)?)? { + dataset.insert(&q?); } Ok(()) } -pub fn load_dataset(url: &str) -> Result { +pub fn load_dataset(url: &str, format: DatasetFormat) -> Result { let mut dataset = Dataset::new(); - load_to_dataset(url, &mut dataset, GraphNameRef::DefaultGraph)?; + load_to_dataset(url, &mut dataset, format)?; Ok(dataset) } + +pub fn guess_dataset_format(url: &str) -> Result { + url.rsplit_once('.') + .and_then(|(_, extension)| DatasetFormat::from_extension(extension)) + .ok_or_else(|| anyhow!("Serialization type not found for {url}")) +} diff --git a/testsuite/src/manifest.rs b/testsuite/src/manifest.rs index e5c77e70..d354cbca 100644 --- a/testsuite/src/manifest.rs +++ b/testsuite/src/manifest.rs @@ -1,4 +1,4 @@ -use crate::files::load_to_graph; +use crate::files::{guess_graph_format, load_to_graph}; use crate::vocab::*; use anyhow::{bail, Result}; use oxigraph::model::vocab::*; @@ -281,7 +281,7 @@ impl TestManifest { return Ok(None); }; self.graph.clear(); - load_to_graph(&url, &mut self.graph)?; + load_to_graph(&url, &mut self.graph, guess_graph_format(&url)?)?; let manifests = self .graph diff --git a/testsuite/src/parser_evaluator.rs b/testsuite/src/parser_evaluator.rs index 50ac25b5..3ea3f91b 100644 --- a/testsuite/src/parser_evaluator.rs +++ b/testsuite/src/parser_evaluator.rs @@ -1,109 +1,147 @@ use crate::evaluator::TestEvaluator; -use crate::files::load_dataset; +use crate::files::{guess_dataset_format, guess_graph_format, load_dataset, load_graph}; use crate::manifest::Test; -use crate::report::dataset_diff; +use crate::report::{dataset_diff, graph_diff}; use anyhow::{anyhow, bail, Result}; +use oxigraph::io::{DatasetFormat, GraphFormat}; pub fn register_parser_tests(evaluator: &mut TestEvaluator) { evaluator.register( "http://www.w3.org/ns/rdftest#TestNTriplesPositiveSyntax", - evaluate_positive_syntax_test, + |t| evaluate_positive_graph_syntax_test(t, GraphFormat::NTriples), ); evaluator.register( "http://www.w3.org/ns/rdftest#TestNQuadsPositiveSyntax", - evaluate_positive_syntax_test, + |t| evaluate_positive_dataset_syntax_test(t, DatasetFormat::NQuads), ); evaluator.register( "http://www.w3.org/ns/rdftest#TestTurtlePositiveSyntax", - evaluate_positive_syntax_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestTrigPositiveSyntax", - evaluate_positive_syntax_test, + |t| evaluate_positive_graph_syntax_test(t, GraphFormat::Turtle), ); + evaluator.register("http://www.w3.org/ns/rdftest#TestTrigPositiveSyntax", |t| { + evaluate_positive_dataset_syntax_test(t, DatasetFormat::TriG) + }); evaluator.register( "http://www.w3.org/ns/rdftest#TestNTriplesNegativeSyntax", - evaluate_negative_syntax_test, + |t| evaluate_negative_graph_syntax_test(t, GraphFormat::NTriples), ); evaluator.register( "http://www.w3.org/ns/rdftest#TestNQuadsNegativeSyntax", - evaluate_negative_syntax_test, + |t| evaluate_negative_dataset_syntax_test(t, DatasetFormat::NQuads), ); evaluator.register( "http://www.w3.org/ns/rdftest#TestTurtleNegativeSyntax", - evaluate_negative_syntax_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestTurtleNegativeEval", - evaluate_negative_syntax_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestTrigNegativeSyntax", - evaluate_negative_syntax_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestTrigNegativeEval", - evaluate_negative_syntax_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestXMLNegativeSyntax", - evaluate_negative_syntax_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestTurtleEval", - evaluate_eval_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestTrigEval", - evaluate_eval_test, - ); - evaluator.register( - "http://www.w3.org/ns/rdftest#TestXMLEval", - evaluate_eval_test, + |t| evaluate_negative_graph_syntax_test(t, GraphFormat::Turtle), ); + evaluator.register("http://www.w3.org/ns/rdftest#TestTrigNegativeSyntax", |t| { + evaluate_negative_dataset_syntax_test(t, DatasetFormat::TriG) + }); + evaluator.register("http://www.w3.org/ns/rdftest#TestXMLNegativeSyntax", |t| { + evaluate_negative_graph_syntax_test(t, GraphFormat::RdfXml) + }); + evaluator.register("http://www.w3.org/ns/rdftest#TestTurtleEval", |t| { + evaluate_graph_eval_test(t, GraphFormat::Turtle) + }); + evaluator.register("http://www.w3.org/ns/rdftest#TestTrigEval", |t| { + evaluate_dataset_eval_test(t, DatasetFormat::TriG) + }); + evaluator.register("http://www.w3.org/ns/rdftest#TestXMLEval", |t| { + evaluate_graph_eval_test(t, GraphFormat::RdfXml) + }); + evaluator.register("http://www.w3.org/ns/rdftest#TestTurtleNegativeEval", |t| { + evaluate_negative_graph_syntax_test(t, GraphFormat::Turtle) + }); + evaluator.register("http://www.w3.org/ns/rdftest#TestTrigNegativeEval", |t| { + evaluate_negative_dataset_syntax_test(t, DatasetFormat::TriG) + }); } -fn evaluate_positive_syntax_test(test: &Test) -> Result<()> { +fn evaluate_positive_graph_syntax_test(test: &Test, format: GraphFormat) -> Result<()> { let action = test .action .as_deref() .ok_or_else(|| anyhow!("No action found for test {test}"))?; - load_dataset(action).map_err(|e| anyhow!("Parse error: {e}"))?; + load_graph(action, format).map_err(|e| anyhow!("Parse error: {e}"))?; Ok(()) } -fn evaluate_negative_syntax_test(test: &Test) -> Result<()> { +fn evaluate_positive_dataset_syntax_test(test: &Test, format: DatasetFormat) -> Result<()> { let action = test .action .as_deref() .ok_or_else(|| anyhow!("No action found for test {test}"))?; - match load_dataset(action) { - Ok(_) => bail!("File parsed with an error even if it should not"), + load_dataset(action, format).map_err(|e| anyhow!("Parse error: {e}"))?; + Ok(()) +} + +fn evaluate_negative_graph_syntax_test(test: &Test, format: GraphFormat) -> Result<()> { + let action = test + .action + .as_deref() + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + match load_graph(action, format) { + Ok(_) => bail!("File parsed without errors even if it should not"), Err(_) => Ok(()), } } -fn evaluate_eval_test(test: &Test) -> Result<()> { +fn evaluate_negative_dataset_syntax_test(test: &Test, format: DatasetFormat) -> Result<()> { + let action = test + .action + .as_deref() + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + match load_dataset(action, format) { + Ok(_) => bail!("File parsed without errors even if it should not"), + Err(_) => Ok(()), + } +} + +fn evaluate_graph_eval_test(test: &Test, format: GraphFormat) -> Result<()> { let action = test .action .as_deref() .ok_or_else(|| anyhow!("No action found for test {test}"))?; let mut actual_graph = - load_dataset(action).map_err(|e| anyhow!("Parse error on file {action}: {e}"))?; + load_graph(action, format).map_err(|e| anyhow!("Parse error on file {action}: {e}"))?; actual_graph.canonicalize(); - if let Some(result) = &test.result { - let mut expected_graph = - load_dataset(result).map_err(|e| anyhow!("Parse error on file {action}: {e}"))?; - expected_graph.canonicalize(); - if expected_graph == actual_graph { - Ok(()) - } else { - bail!( - "The two files are not isomorphic. Diff:\n{}", - dataset_diff(&expected_graph, &actual_graph) - ) - } + let results = test + .result + .as_ref() + .ok_or_else(|| anyhow!("No tests result found"))?; + let mut expected_graph = load_graph(results, guess_graph_format(results)?) + .map_err(|e| anyhow!("Parse error on file {results}: {e}"))?; + expected_graph.canonicalize(); + if expected_graph == actual_graph { + Ok(()) + } else { + bail!( + "The two files are not isomorphic. Diff:\n{}", + graph_diff(&expected_graph, &actual_graph) + ) + } +} + +fn evaluate_dataset_eval_test(test: &Test, format: DatasetFormat) -> Result<()> { + let action = test + .action + .as_deref() + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + let mut actual_dataset = + load_dataset(action, format).map_err(|e| anyhow!("Parse error on file {action}: {e}"))?; + actual_dataset.canonicalize(); + let results = test + .result + .as_ref() + .ok_or_else(|| anyhow!("No tests result found"))?; + let mut expected_dataset = load_dataset(results, guess_dataset_format(results)?) + .map_err(|e| anyhow!("Parse error on file {results}: {e}"))?; + expected_dataset.canonicalize(); + if expected_dataset == actual_dataset { + Ok(()) } else { - bail!("No tests result found") + bail!( + "The two files are not isomorphic. Diff:\n{}", + dataset_diff(&expected_dataset, &actual_dataset) + ) } } diff --git a/testsuite/src/report.rs b/testsuite/src/report.rs index f1fc35e8..ba24ee2c 100644 --- a/testsuite/src/report.rs +++ b/testsuite/src/report.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use oxigraph::model::{Dataset, NamedNode}; +use oxigraph::model::{Dataset, Graph, NamedNode}; use std::fmt::Write; use text_diff::{diff, Difference}; use time::format_description::well_known::Rfc3339; @@ -12,7 +12,7 @@ pub struct TestResult { pub date: OffsetDateTime, } -pub fn dataset_diff(expected: &Dataset, actual: &Dataset) -> String { +pub(super) fn dataset_diff(expected: &Dataset, actual: &Dataset) -> String { format_diff( &normalize_dataset_text(expected), &normalize_dataset_text(actual), @@ -26,6 +26,20 @@ fn normalize_dataset_text(store: &Dataset) -> String { quads.join("\n") } +pub(super) fn graph_diff(expected: &Graph, actual: &Graph) -> String { + format_diff( + &normalize_graph_text(expected), + &normalize_graph_text(actual), + "triples", + ) +} + +fn normalize_graph_text(store: &Graph) -> String { + let mut triples: Vec<_> = store.iter().map(|q| q.to_string()).collect(); + triples.sort(); + triples.join("\n") +} + pub(super) fn format_diff(expected: &str, actual: &str, kind: &str) -> String { let (_, changeset) = diff(expected, actual, "\n"); let mut ret = String::new(); diff --git a/testsuite/src/sparql_evaluator.rs b/testsuite/src/sparql_evaluator.rs index 8713be24..96501c14 100644 --- a/testsuite/src/sparql_evaluator.rs +++ b/testsuite/src/sparql_evaluator.rs @@ -150,10 +150,10 @@ fn result_syntax_check(test: &Test, format: QueryResultsFormat) -> Result<()> { fn evaluate_evaluation_test(test: &Test) -> Result<()> { let store = Store::new()?; if let Some(data) = &test.data { - load_to_store(data, &store, GraphNameRef::DefaultGraph)?; + load_dataset_to_store(data, &store)?; } for (name, value) in &test.graph_data { - load_to_store(value, &store, name)?; + load_graph_to_store(value, &store, name)?; } let query_file = test .query @@ -172,14 +172,14 @@ fn evaluate_evaluation_test(test: &Test) -> Result<()> { if !query.dataset().is_default_dataset() { for graph_name in query.dataset().default_graph_graphs().unwrap_or(&[]) { if let GraphName::NamedNode(graph_name) = graph_name { - load_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; + load_graph_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; } else { bail!("Invalid FROM in query {query} for test {test}"); } } for graph_name in query.dataset().available_named_graphs().unwrap_or(&[]) { if let NamedOrBlankNode::NamedNode(graph_name) = graph_name { - load_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; + load_graph_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; } else { bail!("Invalid FROM NAMED in query {query} for test {test}"); } @@ -243,18 +243,18 @@ fn evaluate_negative_update_syntax_test(test: &Test) -> Result<()> { fn evaluate_update_evaluation_test(test: &Test) -> Result<()> { let store = Store::new()?; if let Some(data) = &test.data { - load_to_store(data, &store, GraphNameRef::DefaultGraph)?; + load_dataset_to_store(data, &store)?; } for (name, value) in &test.graph_data { - load_to_store(value, &store, name)?; + load_graph_to_store(value, &store, name)?; } let result_store = Store::new()?; if let Some(data) = &test.result { - load_to_store(data, &result_store, GraphNameRef::DefaultGraph)?; + load_dataset_to_store(data, &result_store)?; } for (name, value) in &test.result_graph_data { - load_to_store(value, &result_store, name)?; + load_graph_to_store(value, &result_store, name)?; } let update_file = test @@ -303,7 +303,7 @@ fn load_sparql_query_result(url: &str) -> Result { false, ) } else { - StaticQueryResults::from_graph(load_graph(url)?) + StaticQueryResults::from_graph(load_graph(url, guess_graph_format(url)?)?) } } @@ -321,7 +321,7 @@ impl StaticServiceHandler { .map(|(name, data)| { let name = NamedNode::new(name)?; let store = Store::new()?; - load_to_store(data, &store, GraphNameRef::DefaultGraph)?; + load_dataset_to_store(data, &store)?; Ok((name, store)) }) .collect::>()?, @@ -700,3 +700,31 @@ fn solutions_to_string(solutions: Vec>, ordered: bool) -> } lines.join("\n") } + +fn load_graph_to_store<'a>( + url: &str, + store: &Store, + to_graph_name: impl Into>, +) -> Result<()> { + store.load_graph( + read_file(url)?, + guess_graph_format(url)?, + to_graph_name, + Some(url), + )?; + Ok(()) +} + +fn load_dataset_to_store<'a>(url: &str, store: &Store) -> Result<()> { + if let Ok(format) = guess_dataset_format(url) { + store.load_dataset(read_file(url)?, format, Some(url)) + } else { + store.load_graph( + read_file(url)?, + guess_graph_format(url)?, + GraphNameRef::DefaultGraph, + Some(url), + ) + }?; + Ok(()) +}