diff --git a/src/main/scala/eu/neverblink/jelly/cli/util/jena/OrderedRdfCompare.scala b/src/main/scala/eu/neverblink/jelly/cli/util/jena/OrderedRdfCompare.scala index 62a18fa..1b7c5a6 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/util/jena/OrderedRdfCompare.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/util/jena/OrderedRdfCompare.scala @@ -10,6 +10,13 @@ import scala.collection.mutable object OrderedRdfCompare extends RdfCompare: import StatementUtils.* + private val termNames = Seq( + "subject", + "predicate", + "object", + "graph", + ) + def compare( expected: StreamRdfCollector, actual: StreamRdfCollector, @@ -21,29 +28,40 @@ object OrderedRdfCompare extends RdfCompare: s"Expected ${eSeq.size} RDF elements, but got ${aSeq.size} elements.", ) val bNodeMap = mutable.Map.empty[String, String] - def tryIsomorphism(e: Seq[Node], a: Seq[Node], i: Int): Unit = - e.zip(a).foreach { (et, at) => + + def tryIsomorphism(e: Seq[Node], a: Seq[Node], location: String): Unit = + e.zip(a).zipWithIndex.foreach { (terms, termIndex) => + val (et, at) = terms if et.isBlank && at.isBlank then val eId = et.getBlankNodeLabel val aId = at.getBlankNodeLabel if bNodeMap.contains(eId) then if bNodeMap(eId) != aId then throw new CriticalException( - s"RDF element $i is different: expected $e, got $a. $eId is " + - s"already mapped to ${bNodeMap(eId)}.", + s"RDF element $location is different in ${termNames(termIndex)} term: " + + s"expected $e, got $a. $eId is already mapped to ${bNodeMap(eId)}.", ) else bNodeMap(eId) = aId + else if et.isNodeTriple && at.isNodeTriple then + // Recurse into the RDF-star quoted triple + tryIsomorphism( + iterateTerms(et.getTriple), + iterateTerms(at.getTriple), + f"${location}_${termNames(termIndex)}", + ) else if et != at then throw new CriticalException( - s"RDF element $i is different: expected $e, got $a.", + s"RDF element $location is different in ${termNames(termIndex)} term: " + + s"expected $e, got $a.", ) } + eSeq.zip(aSeq).zipWithIndex.foreach { case ((e, a), i) => (e, a) match { case (e: Triple, a: Triple) => - tryIsomorphism(iterateTerms(e), iterateTerms(a), i) + tryIsomorphism(iterateTerms(e), iterateTerms(a), i.toString) case (e: Quad, a: Quad) => - tryIsomorphism(iterateTerms(e), iterateTerms(a), i) + tryIsomorphism(iterateTerms(e), iterateTerms(a), i.toString) case (e: NamespaceDeclaration, a: NamespaceDeclaration) => if e != a then throw new CriticalException( diff --git a/src/test/scala/eu/neverblink/jelly/cli/command/helpers/TestFixtureHelper.scala b/src/test/scala/eu/neverblink/jelly/cli/command/helpers/TestFixtureHelper.scala index f17a522..dea7633 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/command/helpers/TestFixtureHelper.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/command/helpers/TestFixtureHelper.scala @@ -2,7 +2,9 @@ package eu.neverblink.jelly.cli.command.helpers import eu.neverblink.jelly.cli.util.jena.riot.CliRiot import eu.ostrzyciel.jelly.convert.jena.riot.{JellyFormatVariant, JellyLanguage} +import org.apache.jena.graph.Triple import org.apache.jena.riot.{Lang, RDFDataMgr, RDFFormat, RDFLanguages} +import org.apache.jena.sparql.graph.GraphFactory import org.apache.jena.sys.JenaSystem import org.scalatest.BeforeAndAfterAll import org.scalatest.wordspec.AnyWordSpec @@ -49,6 +51,20 @@ trait TestFixtureHelper extends BeforeAndAfterAll: testCode(tempFile.toString) } finally { tempFile.toFile.delete() } + def withJenaFileOfContent[T](content: Seq[Triple], jenaLang: Lang = RDFLanguages.NQUADS)( + testCode: String => T, + ): T = + val extension = getFileExtension(jenaLang) + val tempFile = Files.createTempFile(tmpDir, randomUUID.toString, f".${extension}") + val graph = GraphFactory.createGraphMem() + content.foreach(graph.add) + Using(new FileOutputStream(tempFile.toFile)) { fileOutputStream => + RDFDataMgr.write(fileOutputStream, graph, jenaLang) + } + try { + testCode(tempFile.toString) + } finally { tempFile.toFile.delete() } + def withEmptyJellyTextFile(testCode: (String) => Any): Unit = val tempFile = Files.createTempFile(tmpDir, randomUUID.toString, ".jelly.txt") try { diff --git a/src/test/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidateSpec.scala b/src/test/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidateSpec.scala index 8c745fc..da10075 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidateSpec.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidateSpec.scala @@ -3,9 +3,11 @@ package eu.neverblink.jelly.cli.command.rdf import eu.neverblink.jelly.cli.command.helpers.TestFixtureHelper import eu.neverblink.jelly.cli.{CriticalException, ExitException} import eu.ostrzyciel.jelly.convert.jena.JenaConverterFactory +import eu.ostrzyciel.jelly.convert.jena.riot.JellyLanguage import eu.ostrzyciel.jelly.core.proto.v1.* import eu.ostrzyciel.jelly.core.{JellyOptions, ProtoEncoder, RdfProtoDeserializationError} import org.apache.jena.graph.{NodeFactory, Triple} +import org.apache.jena.riot.Lang import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -513,6 +515,41 @@ class RdfValidateSpec extends AnyWordSpec, Matchers, TestFixtureHelper: ) } + // Regression test for https://github.com/Jelly-RDF/cli/issues/113 + "comparing RDF-star data with blank nodes and nested triples" in { + val t = Triple.create( + NodeFactory.createTripleNode( + Triple.create( + NodeFactory.createTripleNode( + Triple.create( + NodeFactory.createBlankNode(), + NodeFactory.createURI("http://example.org/predicate"), + NodeFactory.createBlankNode(), + ), + ), + NodeFactory.createURI("http://example.org/predicate"), + NodeFactory.createBlankNode(), + ), + ), + NodeFactory.createURI("http://example.org/predicate"), + NodeFactory.createBlankNode(), + ) + withJenaFileOfContent(Seq(t), Lang.NT) { ntFile => + withJenaFileOfContent(Seq(t), JellyLanguage.JELLY) { jellyFile => + val (out, err) = RdfValidate.runTestCommand( + List( + "rdf", + "validate", + "--compare-to-rdf-file=" + ntFile, + "--compare-ordered=true", + jellyFile, + ), + ) + err shouldBe empty + } + } + } + "RDF-star triples in subject and object positions (generalized=false)" in { val t = Triple.create( NodeFactory.createTripleNode( diff --git a/src/test/scala/eu/neverblink/jelly/cli/util/jena/RdfCompareSpec.scala b/src/test/scala/eu/neverblink/jelly/cli/util/jena/RdfCompareSpec.scala index 617493c..a87eb62 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/util/jena/RdfCompareSpec.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/util/jena/RdfCompareSpec.scala @@ -124,7 +124,7 @@ class RdfCompareSpec extends AnyWordSpec, Matchers: val e = intercept[CriticalException] { OrderedRdfCompare.compare(c1, c3) } - e.getMessage should include("RDF element 3 is different: expected") + e.getMessage should include("RDF element 3 is different in subject term: expected") e.getMessage should include("b1 is already mapped to b1") } @@ -137,7 +137,7 @@ class RdfCompareSpec extends AnyWordSpec, Matchers: val e = intercept[CriticalException] { OrderedRdfCompare.compare(c1, c3) } - e.getMessage should include("RDF element 1 is different: expected") + e.getMessage should include("RDF element 1 is different in subject term: expected") } }