diff --git a/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidate.scala b/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidate.scala index f46e64e..cabfdc7 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidate.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfValidate.scala @@ -14,6 +14,7 @@ import org.apache.jena.riot.RDFParser import org.apache.jena.riot.system.StreamRDFLib import org.apache.jena.sparql.core.Quad +import scala.annotation.tailrec import scala.util.Using object RdfValidatePrint extends RdfCommandPrintUtil[RdfFormat.Jena]: @@ -91,7 +92,7 @@ object RdfValidate extends JellyCommand[RdfValidateOptions]: // Step 1: Validate delimiting validateDelimiting(delimiting, delimited) // Step 2: Validate basic stream structure & the stream options - val framesSeq = frameIterator.toSeq + val framesSeq = skipEmptyFrames(frameIterator.toSeq) validateOptions(framesSeq) // Step 3: Validate the content validateContent(framesSeq, frameIndices, rdfComparison) @@ -109,10 +110,6 @@ object RdfValidate extends JellyCommand[RdfValidateOptions]: throw CriticalException("Expected undelimited input, but the file was delimited") private def validateOptions(frames: Seq[RdfStreamFrame]): Unit = - // Validate basic stream structure - if frames.isEmpty then throw CriticalException("Empty input stream") - if frames.head.rows.isEmpty then - throw CriticalException("First frame in the input stream is empty") if !frames.head.rows.head.row.isOptions then throw CriticalException("First row in the input stream does not contain stream options") val streamOptions = frames.head.rows.head.row.options @@ -192,6 +189,24 @@ object RdfValidate extends JellyCommand[RdfValidateOptions]: comparator.compare(rdfComparison, actual) } + /** Skip empty frames in the stream. If the first frame is empty, we skip it and continue with the + * next one. If the first row is empty, we throw an exception + * @param frames + * frames to check + * @return + * frames after empty frames + */ + @tailrec + private def skipEmptyFrames( + frames: Seq[RdfStreamFrame], + ): Seq[RdfStreamFrame] = + if frames.isEmpty then throw CriticalException("Empty input stream") + else if frames.head.rows.isEmpty then + // We want to accept empty frames in the stream, but not empty streams + if frames.tail.isEmpty then throw CriticalException("All frames are empty") + skipEmptyFrames(frames.tail) + else frames + /** Reads the RDF file for comparison and returns a StreamRdfCollector * @param fileName * filename to read diff --git a/src/test/resources/firstEmptyFrame.jelly b/src/test/resources/firstEmptyFrame.jelly new file mode 100644 index 0000000..af40929 Binary files /dev/null and b/src/test/resources/firstEmptyFrame.jelly differ diff --git a/src/test/resources/threeFirstEmptyFrames.jelly b/src/test/resources/threeFirstEmptyFrames.jelly new file mode 100644 index 0000000..0baf436 Binary files /dev/null and b/src/test/resources/threeFirstEmptyFrames.jelly differ 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 dea7633..47158d6 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 @@ -10,7 +10,7 @@ import org.scalatest.BeforeAndAfterAll import org.scalatest.wordspec.AnyWordSpec import java.io.FileOutputStream -import java.nio.file.{Files, Path} +import java.nio.file.{Files, Path, Paths} import java.util.UUID.randomUUID import scala.util.Using @@ -24,6 +24,7 @@ trait TestFixtureHelper extends BeforeAndAfterAll: CliRiot.initialize() } + private val specificTestDir: Path = Paths.get("src", "test", "resources") private val tmpDir: Path = Files.createTempDirectory("jelly-cli") /** The number of triples to generate for the tests @@ -85,6 +86,16 @@ trait TestFixtureHelper extends BeforeAndAfterAll: testCode(tempFile.toString) } finally { tempFile.toFile.delete() } + def withSpecificJellyFile( + testCode: (String) => Any, + fileName: String, + ): Unit = { + val filePath = specificTestDir.resolve(fileName) + if !Files.exists(filePath) then + throw new IllegalArgumentException(s"File $fileName does not exist in $specificTestDir") + else testCode(filePath.toString) + } + def withFullJellyFile(testCode: (String) => Any, frameSize: Int = 256): Unit = val extension = getFileExtension(JellyLanguage.JELLY) val tempFile = Files.createTempFile(tmpDir, randomUUID.toString, f".${extension}") 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 da10075..dfa4b16 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 @@ -23,9 +23,25 @@ class RdfValidateSpec extends AnyWordSpec, Matchers, TestFixtureHelper: RdfValidate.runTestCommand(List("rdf", "validate")) } e.cause.get shouldBe a[CriticalException] - e.cause.get.getMessage should include("First frame in the input stream is empty") + e.cause.get.getMessage should include("All frames are empty") } + "accept empty frame before stream options" in withSpecificJellyFile( + testCode = { jellyF => + val (out, err) = RdfValidate.runTestCommand(List("rdf", "validate", jellyF)) + out shouldBe empty + }, + fileName = "firstEmptyFrame.jelly", + ) + + "accept three first empty frames" in withSpecificJellyFile( + testCode = { jellyF => + val (out, err) = RdfValidate.runTestCommand(List("rdf", "validate", jellyF)) + out shouldBe empty + }, + fileName = "threeFirstEmptyFrames.jelly", + ) + "validate delimiting" when { val frame = RdfStreamFrame( Seq(