diff --git a/src/main/scala/eu/neverblink/jelly/cli/App.scala b/src/main/scala/eu/neverblink/jelly/cli/App.scala index 77567ad..7a19fd3 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/App.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/App.scala @@ -8,6 +8,7 @@ import org.apache.jena.sys.JenaSystem /** Main entrypoint. */ object App extends CommandsEntryPoint: + // Initialize Jena now to avoid race conditions later JenaSystem.init() @@ -18,7 +19,6 @@ object App extends CommandsEntryPoint: override def progName: String = "jelly-cli" override def commands: Seq[Command[?]] = Seq( - FoolAround, Version, RdfFromJelly, ) diff --git a/src/main/scala/eu/neverblink/jelly/cli/ErrorHandler.scala b/src/main/scala/eu/neverblink/jelly/cli/ErrorHandler.scala new file mode 100644 index 0000000..676d08b --- /dev/null +++ b/src/main/scala/eu/neverblink/jelly/cli/ErrorHandler.scala @@ -0,0 +1,26 @@ +package eu.neverblink.jelly.cli + +/** Handle exceptions. Common critical exceptions will be given custom output messages. + */ + +object ErrorHandler: + + def handle(command: JellyCommand[?], t: Throwable): Unit = + t match + case e: CriticalException => + command.printLine(f"${e.getMessage}", toStderr = true) + case e: Throwable => + command.printLine("Unknown error", toStderr = true) + printStackTrace(command, t) + command.exit(1) + + /** Print out stack trace or debugging information + * @param command + * @param t + */ + private def printStackTrace( + command: JellyCommand[?], + t: Throwable, + ): Unit = + if command.isDebugMode then t.printStackTrace(command.err) + else command.printLine("Run with --debug to see the complete stack trace.", toStderr = true) diff --git a/src/main/scala/eu/neverblink/jelly/cli/Exceptions.scala b/src/main/scala/eu/neverblink/jelly/cli/Exceptions.scala new file mode 100644 index 0000000..d93cd55 --- /dev/null +++ b/src/main/scala/eu/neverblink/jelly/cli/Exceptions.scala @@ -0,0 +1,29 @@ +package eu.neverblink.jelly.cli + +import com.google.protobuf.InvalidProtocolBufferException +import org.apache.jena.riot.RiotException + +/** Contains a set of common jelly-cli exceptions with custom output messages. + */ + +case class InputFileNotFound(file: String) + extends CriticalException(s"Input file $file does not exist.") +case class InputFileInaccessible(file: String) + extends CriticalException(s"Not enough permissions to read input file $file.") +case class OutputFileCannotBeCreated(file: String) + extends CriticalException( + s"Not enough permissions to create output file $file in this directory.", + ) +case class JellyDeserializationError(message: String) + extends CriticalException(s"Jelly deserialization error: $message") +case class JellySerializationError(message: String) + extends CriticalException(s"Jelly serialization error: $message") +case class JellyTranscodingError(message: String) + extends CriticalException(s"Jelly transcoding error: $message") +case class JenaRiotException(e: RiotException) + extends CriticalException(s"Jena RDF I/O exception: ${e.getMessage}") +case class InvalidJellyFile(e: InvalidProtocolBufferException) + extends CriticalException(s"Invalid Jelly file: ${e.getMessage}") +case class ExitException(code: Int) extends CriticalException(s"Exiting with code $code.") + +class CriticalException(message: String) extends Exception(message) diff --git a/src/main/scala/eu/neverblink/jelly/cli/ExitError.scala b/src/main/scala/eu/neverblink/jelly/cli/ExitError.scala deleted file mode 100644 index 2f33a21..0000000 --- a/src/main/scala/eu/neverblink/jelly/cli/ExitError.scala +++ /dev/null @@ -1,3 +0,0 @@ -package eu.neverblink.jelly.cli - -case class ExitError(code: Int) extends Exception diff --git a/src/main/scala/eu/neverblink/jelly/cli/JellyCommand.scala b/src/main/scala/eu/neverblink/jelly/cli/JellyCommand.scala index 69da29a..d62f021 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/JellyCommand.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/JellyCommand.scala @@ -5,10 +5,19 @@ import caseapp.* import java.io.{ByteArrayOutputStream, OutputStream, PrintStream} import scala.compiletime.uninitialized -abstract class JellyCommand[T: {Parser, Help}] extends Command[T]: +case class JellyOptions( + @HelpMessage("Add to run command in debug mode") debug: Boolean = false, +) + +trait HasJellyOptions: + @Recurse + val common: JellyOptions + +abstract class JellyCommand[T <: HasJellyOptions: {Parser, Help}] extends Command[T]: private var isTest = false - private var out = System.out - private var err = System.err + private var isDebug = false + final protected[cli] var out = System.out + final protected[cli] var err = System.err private var osOut: ByteArrayOutputStream = uninitialized private var osErr: ByteArrayOutputStream = uninitialized @@ -27,18 +36,49 @@ abstract class JellyCommand[T: {Parser, Help}] extends Command[T]: out = System.out err = System.err + /** Check and set the values of all the general options repeating for every JellyCommand + */ + private def setUpGeneralArgs(options: T, remainingArgs: RemainingArgs): Unit = + if options.common.debug then this.isDebug = true + + /** Makes sure that the repetitive options needed for every JellyCommand are set up before calling + * the doRun method, which contains Command-specific logic + */ + final override def run(options: T, remainingArgs: RemainingArgs): Unit = + setUpGeneralArgs(options, remainingArgs) + doRun(options, remainingArgs) + + /** This abstract method is the main entry point for every JellyCommand. It should be overridden + * by Command-specific implementation, including logic needed for this specific object extendind + * JellyCommand. + */ + def doRun(options: T, remainingArgs: RemainingArgs): Unit + + /** Override to have custom error handling for Jelly commands + */ + final override def main(progName: String, args: Array[String]): Unit = + try super.main(progName, args) + catch + case e: Throwable => + ErrorHandler.handle(this, e) + + /** Returns information about whether the command is in debug mode (which returns stack traces of + * every error) or not + */ + final def isDebugMode: Boolean = this.isDebug + /** Runs the command in test mode from the outside app parsing level * @param args * the command line arguments */ - def runCommand(args: List[String]): (String, String) = + final def runTestCommand(args: List[String]): (String, String) = if !isTest then testMode(true) osOut.reset() osErr.reset() App.main(args.toArray) (osOut.toString, osErr.toString) - def getOut: String = + final def getOutContent: String = if isTest then out.flush() val s = osOut.toString @@ -46,11 +86,15 @@ abstract class JellyCommand[T: {Parser, Help}] extends Command[T]: s else throw new IllegalStateException("Not in test mode") + final def getOutStream: OutputStream = + if isTest then osOut + else System.out + protected def getStdOut: OutputStream = if isTest then osOut else System.out - def getErr: String = + final def getErrContent: String = if isTest then err.flush() val s = osErr.toString @@ -58,11 +102,11 @@ abstract class JellyCommand[T: {Parser, Help}] extends Command[T]: s else throw new IllegalStateException("Not in test mode") - @throws[ExitError] - override def exit(code: Int): Nothing = - if isTest then throw ExitError(code) + @throws[ExitException] + final override def exit(code: Int): Nothing = + if isTest then throw ExitException(code) else super.exit(code) - override def printLine(line: String, toStderr: Boolean): Unit = + final override def printLine(line: String, toStderr: Boolean): Unit = if toStderr then err.println(line) else out.println(line) diff --git a/src/main/scala/eu/neverblink/jelly/cli/command/FoolAround.scala b/src/main/scala/eu/neverblink/jelly/cli/command/FoolAround.scala deleted file mode 100644 index 9f1b2f2..0000000 --- a/src/main/scala/eu/neverblink/jelly/cli/command/FoolAround.scala +++ /dev/null @@ -1,20 +0,0 @@ -package eu.neverblink.jelly.cli.command - -import caseapp.* -import eu.neverblink.jelly.cli.JellyCommand - -case class FoolAroundOptions( - @HelpMessage("What to say") - say: String = "Hello, World!", -) - -/** "foo-bar" kind of command. - */ -object FoolAround extends JellyCommand[FoolAroundOptions]: - // https://alexarchambault.github.io/case-app/commands/ - override def names: List[List[String]] = List( - List("fool-around"), - ) - - override def run(options: FoolAroundOptions, remainingArgs: RemainingArgs): Unit = - println(options.say) diff --git a/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala b/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala index 29fe4f0..d92f61d 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala @@ -3,7 +3,10 @@ package eu.neverblink.jelly.cli.command import caseapp.* import eu.neverblink.jelly.cli.* -case class VersionOptions() +case class VersionOptions( + @Recurse + common: JellyOptions = JellyOptions(), +) extends HasJellyOptions object Version extends JellyCommand[VersionOptions]: override def names: List[List[String]] = List( @@ -11,7 +14,7 @@ object Version extends JellyCommand[VersionOptions]: List("v"), ) - override def run(options: VersionOptions, remainingArgs: RemainingArgs): Unit = + override def doRun(options: VersionOptions, remainingArgs: RemainingArgs): Unit = val jenaV = BuildInfo.libraryDependencies .find(_.startsWith("org.apache.jena:jena-core:")).get.split(":")(2) val jellyV = BuildInfo.libraryDependencies diff --git a/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfFromJelly.scala b/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfFromJelly.scala index f6185ff..5b627f5 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfFromJelly.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfFromJelly.scala @@ -1,15 +1,20 @@ package eu.neverblink.jelly.cli.command.rdf import caseapp.* -import eu.neverblink.jelly.cli.JellyCommand -import eu.ostrzyciel.jelly.convert.jena.riot.{JellyLanguage, JellySubsystemLifecycle} +import com.google.protobuf.InvalidProtocolBufferException +import eu.neverblink.jelly.cli.* +import eu.neverblink.jelly.cli.util.IoUtil +import eu.ostrzyciel.jelly.convert.jena.riot.JellyLanguage +import eu.ostrzyciel.jelly.core.RdfProtoDeserializationError import org.apache.jena.riot.system.StreamRDFWriter -import org.apache.jena.riot.{RDFLanguages, RDFParser} +import org.apache.jena.riot.{RDFLanguages, RDFParser, RiotException} -import java.io.{File, FileInputStream, FileOutputStream, InputStream, OutputStream} +import java.io.{InputStream, OutputStream} case class RdfFromJellyOptions( + @Recurse + common: JellyOptions = JellyOptions(), @ExtraName("to") outputFile: Option[String] = None, -) +) extends HasJellyOptions object RdfFromJelly extends JellyCommand[RdfFromJellyOptions]: override def group = "rdf" @@ -18,26 +23,35 @@ object RdfFromJelly extends JellyCommand[RdfFromJellyOptions]: List("rdf", "from-jelly"), ) - override def run(options: RdfFromJellyOptions, remainingArgs: RemainingArgs): Unit = + override def doRun(options: RdfFromJellyOptions, remainingArgs: RemainingArgs): Unit = val inputStream = remainingArgs.remaining.headOption match { case Some(fileName: String) => - FileInputStream(File(fileName)) + IoUtil.inputStream(fileName) case _ => System.in } val outputStream = options.outputFile match { case Some(fileName: String) => - FileOutputStream(fileName) + IoUtil.outputStream(fileName) case None => getStdOut } doConversion(inputStream, outputStream) - /* - This method reads the Jelly file, rewrite it to NQuads and writes it to some output stream - * @param inputStream InputStream - * @param outputStream OutputStream - */ + /** This method reads the Jelly file, rewrites it to NQuads and writes it to some output stream + * @param inputStream + * InputStream + * @param outputStream + * OutputStream + * @throws JellyDeserializationError + * @throws ParsingError + */ private def doConversion(inputStream: InputStream, outputStream: OutputStream): Unit = - val mod = JellySubsystemLifecycle() - mod.start() - val nQuadWriter = StreamRDFWriter.getWriterStream(outputStream, RDFLanguages.NQUADS) - RDFParser.source(inputStream).lang(JellyLanguage.JELLY).parse(nQuadWriter) + try { + val nQuadWriter = StreamRDFWriter.getWriterStream(outputStream, RDFLanguages.NQUADS) + RDFParser.source(inputStream).lang(JellyLanguage.JELLY).parse(nQuadWriter) + } catch + case e: RdfProtoDeserializationError => + throw JellyDeserializationError(e.getMessage) + case e: RiotException => + throw JenaRiotException(e) + case e: InvalidProtocolBufferException => + throw InvalidJellyFile(e) diff --git a/src/main/scala/eu/neverblink/jelly/cli/util/IoUtil.scala b/src/main/scala/eu/neverblink/jelly/cli/util/IoUtil.scala new file mode 100644 index 0000000..c020620 --- /dev/null +++ b/src/main/scala/eu/neverblink/jelly/cli/util/IoUtil.scala @@ -0,0 +1,35 @@ +package eu.neverblink.jelly.cli.util + +import eu.neverblink.jelly.cli.* + +import java.io.{File, FileInputStream, FileOutputStream} + +/** Object for small, repeating I/O operations + */ +object IoUtil: + + /** Read input file and return FileInputStream + * @param fileName + * @throws InputFileNotFound + * @throws InputFileInaccessible + * @return + * FileInputStream + */ + def inputStream(fileName: String): FileInputStream = + val file = File(fileName) + if !file.exists then throw InputFileNotFound(fileName) + if !file.canRead then throw InputFileInaccessible(fileName) + FileInputStream(file) + + /** Create output stream with extra error handling. If the file exists, it will append to it. + * @param fileName + * @throws OutputFileExists + * @return + * FileOutputStream + */ + def outputStream(fileName: String): FileOutputStream = + val file = File(fileName) + val suppFile = file.getParentFile + val parentFile = if (suppFile != null) suppFile else File(".") + if !parentFile.canWrite then throw OutputFileCannotBeCreated(fileName) + FileOutputStream(file, true) diff --git a/src/test/scala/eu/neverblink/jelly/cli/command/RdfFromJellySpec.scala b/src/test/scala/eu/neverblink/jelly/cli/command/RdfFromJellySpec.scala index bda172a..5ac48e8 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/command/RdfFromJellySpec.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/command/RdfFromJellySpec.scala @@ -1,11 +1,15 @@ package eu.neverblink.jelly.cli.command +import com.google.protobuf.InvalidProtocolBufferException +import eu.neverblink.jelly.cli.* import eu.neverblink.jelly.cli.command.helpers.* import eu.neverblink.jelly.cli.command.rdf.* import org.apache.jena.riot.RDFLanguages import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import java.nio.file.attribute.PosixFilePermissions +import java.nio.file.{Files, Paths} import scala.io.Source import scala.util.Using @@ -16,7 +20,7 @@ class RdfFromJellySpec extends AnyWordSpec with Matchers with CleanUpAfterTest: val jellyFile = DataGenHelper.generateJellyFile(3) val nQuadString = DataGenHelper.generateNQuadString(3) val (out, err) = - RdfFromJelly.runCommand(List("rdf", "from-jelly", jellyFile)) + RdfFromJelly.runTestCommand(List("rdf", "from-jelly", jellyFile)) val sortedOut = out.split("\n").map(_.trim).sorted val sortedQuads = nQuadString.split("\n").map(_.trim).sorted sortedOut should contain theSameElementsAs sortedQuads @@ -25,7 +29,7 @@ class RdfFromJellySpec extends AnyWordSpec with Matchers with CleanUpAfterTest: "be able to convert a Jelly stream to NTriples output stream" in { DataGenHelper.generateJellyInputStream(3) val nQuadString = DataGenHelper.generateNQuadString(3) - val (out, err) = RdfFromJelly.runCommand(List("rdf", "from-jelly")) + val (out, err) = RdfFromJelly.runTestCommand(List("rdf", "from-jelly")) val sortedOut = out.split("\n").map(_.trim).sorted val sortedQuads = nQuadString.split("\n").map(_.trim).sorted sortedOut should contain theSameElementsAs sortedQuads @@ -35,7 +39,7 @@ class RdfFromJellySpec extends AnyWordSpec with Matchers with CleanUpAfterTest: val nQuadString = DataGenHelper.generateNQuadString(3) val outputFile = DataGenHelper.generateOutputFile(RDFLanguages.NQUADS) val (out, err) = - RdfFromJelly.runCommand( + RdfFromJelly.runTestCommand( List("rdf", "from-jelly", jellyFile, "--to", outputFile), ) val sortedOut = Using.resource(Source.fromFile(outputFile)) { content => @@ -50,7 +54,7 @@ class RdfFromJellySpec extends AnyWordSpec with Matchers with CleanUpAfterTest: val outputFile = DataGenHelper.generateOutputFile(RDFLanguages.NQUADS) val nQuadString = DataGenHelper.generateNQuadString(3) val (out, err) = - RdfFromJelly.runCommand(List("rdf", "from-jelly", "--to", outputFile)) + RdfFromJelly.runTestCommand(List("rdf", "from-jelly", "--to", outputFile)) val sortedOut = Using.resource(Source.fromFile(outputFile)) { content => content.getLines().toList.map(_.trim).sorted } @@ -58,4 +62,82 @@ class RdfFromJellySpec extends AnyWordSpec with Matchers with CleanUpAfterTest: sortedOut should contain theSameElementsAs sortedQuads out.length should be(0) } + "throw proper exception" when { + "input file is not found" in { + val nonExist = "non-existing-file" + val exception = + intercept[ExitException] { + RdfFromJelly.runTestCommand(List("rdf", "from-jelly", nonExist)) + } + val msg = InputFileNotFound(nonExist).getMessage + RdfFromJelly.getErrContent should include(msg) + exception.code should be(1) + } + "input file is not accessible" in { + val jellyFile = DataGenHelper.generateJellyFile(3) + val permissions = PosixFilePermissions.fromString("---------") + Files.setPosixFilePermissions( + Paths.get(jellyFile), + permissions, + ) + val exception = + intercept[ExitException] { + RdfFromJelly.runTestCommand(List("rdf", "from-jelly", jellyFile)) + } + val msg = InputFileInaccessible(jellyFile).getMessage + RdfFromJelly.getErrContent should include(msg) + exception.code should be(1) + } + "output file cannot be created" in { + val jellyFile = DataGenHelper.generateJellyFile(3) + val unreachableDir = DataGenHelper.makeTestDir() + Paths.get(unreachableDir).toFile.setWritable(false) + val quadFile = DataGenHelper.generateOutputFile() + val exception = + intercept[ExitException] { + RdfFromJelly.runTestCommand( + List("rdf", "from-jelly", jellyFile, "--to", quadFile), + ) + } + val msg = OutputFileCannotBeCreated(quadFile).getMessage + RdfFromJelly.getErrContent should include(msg) + exception.code should be(1) + } + "parsing error occurs" in { + val jellyFile = DataGenHelper.generateJellyFile(3) + val quadFile = DataGenHelper.generateOutputFile() + RdfFromJelly.runTestCommand( + List("rdf", "from-jelly", jellyFile, "--to", quadFile), + ) + val exception = + intercept[ExitException] { + RdfFromJelly.runTestCommand( + List("rdf", "from-jelly", quadFile), + ) + } + val msg = InvalidJellyFile(new InvalidProtocolBufferException("")).getMessage + val errContent = RdfFromJelly.getErrContent + errContent should include(msg) + errContent should include("Run with --debug to see the complete stack trace.") + exception.code should be(1) + } + "parsing error occurs with debug set" in { + val jellyFile = DataGenHelper.generateJellyFile(3) + val quadFile = DataGenHelper.generateOutputFile() + RdfFromJelly.runTestCommand( + List("rdf", "from-jelly", jellyFile, "--to", quadFile), + ) + val exception = + intercept[ExitException] { + RdfFromJelly.runTestCommand( + List("rdf", "from-jelly", quadFile, "--debug"), + ) + } + val msg = InvalidJellyFile(new InvalidProtocolBufferException("")).getMessage + val errContent = RdfFromJelly.getErrContent + errContent should include(msg) + errContent should include("eu.neverblink.jelly.cli.InvalidJellyFile") + exception.code should be(1) + } + } } diff --git a/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala b/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala index aa88d2f..212172b 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala @@ -6,7 +6,7 @@ import org.scalatest.wordspec.AnyWordSpec class VersionSpec extends AnyWordSpec, Matchers: "version command" should { "print something" in { - val (out, err) = Version.runCommand(List("version")) + val (out, err) = Version.runTestCommand(List("version")) out should startWith("jelly-cli") out should include("Jelly-JVM") out should include("Apache Jena") diff --git a/src/test/scala/eu/neverblink/jelly/cli/command/helpers/DataGenHelper.scala b/src/test/scala/eu/neverblink/jelly/cli/command/helpers/DataGenHelper.scala index d6e5f89..2d8436d 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/command/helpers/DataGenHelper.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/command/helpers/DataGenHelper.scala @@ -9,20 +9,20 @@ import java.nio.file.{Files, Paths} import scala.collection.mutable.ListBuffer import scala.util.Using -/* - * This class will be used to generate test data - */ +/** This class will be used to generate test data + */ object DataGenHelper: - + private val testDir = "test" private val testFile = "testInput.jelly" private val inputStream = System.in protected val outputFiles = ListBuffer[String]() - /* - * This method generates a triple model with nTriples - * @param nTriples number of triples to generate - * @return Model - */ + /** This method generates a triple model with nTriples + * @param nTriples + * number of triples to generate + * @return + * Model + */ def generateTripleModel(nTriples: Int): Model = val model = ModelFactory.createDefaultModel() val subStr = "http://example.org/subject" @@ -37,11 +37,14 @@ object DataGenHelper: } model - /* This method generates a Jelly file with nTriples - * @param nTriples number of triples to generate - * @param fileName name of the file to generate - * @return String - */ + /** This method generates a Jelly file with nTriples + * @param nTriples + * number of triples to generate + * @param fileName + * name of the file to generate + * @return + * String + */ def generateJellyFile(nTriples: Int): String = val model = generateTripleModel(nTriples) // TODO: Add configurable generation for different variants of Jelly (small strict etc) @@ -50,10 +53,10 @@ object DataGenHelper: } testFile - /* - * This method generates a Jelly byte input stream with nTriples - * @param nTriples number of triples to generate - */ + /** This method generates a Jelly byte input stream with nTriples + * @param nTriples + * number of triples to generate + */ def generateJellyInputStream(nTriples: Int): Unit = val model = generateTripleModel(nTriples) val outputStream = new ByteArrayOutputStream() @@ -61,29 +64,37 @@ object DataGenHelper: val jellyStream = new ByteArrayInputStream(outputStream.toByteArray) System.setIn(jellyStream) - /* - * This method generates a NQuad string with nTriples - * @param nTriples number of triples to generate - * @return String - */ + /** This method generates a NQuad string with nTriples + * @param nTriples + * number of triples to generate + * @return + * String + */ def generateNQuadString(nTriples: Int): String = val model = generateTripleModel(nTriples) val outputStream = new ByteArrayOutputStream() RDFDataMgr.write(outputStream, model, RDFLanguages.NQUADS) outputStream.toString - /* - * Generates and then cleans the file for test purposes - */ + /** Make test dir + */ + def makeTestDir(): String = + Files.createDirectories(Paths.get(testDir)) + testDir + + /** Generates and then cleans the file for test purposes + */ def generateOutputFile(format: Lang = RDFLanguages.NQUADS): String = + if !Files.exists(Paths.get(testDir)) then makeTestDir() val extension = format.getFileExtensions.get(0) - val fileName = s"testOutput${outputFiles.size}.${extension}" + val fileName = s"${testDir}/testOutput${outputFiles.size}.${extension}" outputFiles += fileName fileName def cleanUpFiles(): Unit = Files.deleteIfExists(Paths.get(testFile)) for file <- outputFiles do Files.deleteIfExists(Paths.get(file)) + Files.deleteIfExists(Paths.get(testDir)) def resetInputStream(): Unit = System.setIn(inputStream)