From 04685499c43503eb4e13413bce019fc8d787e201 Mon Sep 17 00:00:00 2001 From: morazow Date: Tue, 12 Oct 2021 23:37:19 +0200 Subject: [PATCH 1/5] Added file location checker. Fixes #20. --- .../{ => common}/avro/AvroConverter.scala | 0 .../exasol/{ => common}/avro/AvroRow.scala | 0 .../{ => common}/avro/AvroRowIterator.scala | 0 .../com/exasol/common/{ => data}/Row.scala | 0 .../common/file/BucketFSFileChecker.scala | 10 +++ .../com/exasol/common/file/FileChecker.scala | 61 +++++++++++++++++++ .../exasol/{ => common}/json/JsonMapper.scala | 0 .../avro/AvroComplexTypesTest.scala | 0 .../avro/AvroLogicalTypesTest.scala | 0 .../avro/AvroPrimitiveReaderTest.scala | 0 .../avro/AvroRowIteratorTest.scala | 0 .../{ => common}/avro/AvroRowTest.scala | 0 .../avro/AvroStringReaderTest.scala | 0 .../exasol/common/{ => data}/RowTest.scala | 0 .../exasol/common/file/FileCheckerTest.scala | 36 +++++++++++ .../{ => common}/json/JsonMapperTest.scala | 0 16 files changed, 107 insertions(+) rename src/main/scala/com/exasol/{ => common}/avro/AvroConverter.scala (100%) rename src/main/scala/com/exasol/{ => common}/avro/AvroRow.scala (100%) rename src/main/scala/com/exasol/{ => common}/avro/AvroRowIterator.scala (100%) rename src/main/scala/com/exasol/common/{ => data}/Row.scala (100%) create mode 100644 src/main/scala/com/exasol/common/file/BucketFSFileChecker.scala create mode 100644 src/main/scala/com/exasol/common/file/FileChecker.scala rename src/main/scala/com/exasol/{ => common}/json/JsonMapper.scala (100%) rename src/test/scala/com/exasol/{ => common}/avro/AvroComplexTypesTest.scala (100%) rename src/test/scala/com/exasol/{ => common}/avro/AvroLogicalTypesTest.scala (100%) rename src/test/scala/com/exasol/{ => common}/avro/AvroPrimitiveReaderTest.scala (100%) rename src/test/scala/com/exasol/{ => common}/avro/AvroRowIteratorTest.scala (100%) rename src/test/scala/com/exasol/{ => common}/avro/AvroRowTest.scala (100%) rename src/test/scala/com/exasol/{ => common}/avro/AvroStringReaderTest.scala (100%) rename src/test/scala/com/exasol/common/{ => data}/RowTest.scala (100%) create mode 100644 src/test/scala/com/exasol/common/file/FileCheckerTest.scala rename src/test/scala/com/exasol/{ => common}/json/JsonMapperTest.scala (100%) diff --git a/src/main/scala/com/exasol/avro/AvroConverter.scala b/src/main/scala/com/exasol/common/avro/AvroConverter.scala similarity index 100% rename from src/main/scala/com/exasol/avro/AvroConverter.scala rename to src/main/scala/com/exasol/common/avro/AvroConverter.scala diff --git a/src/main/scala/com/exasol/avro/AvroRow.scala b/src/main/scala/com/exasol/common/avro/AvroRow.scala similarity index 100% rename from src/main/scala/com/exasol/avro/AvroRow.scala rename to src/main/scala/com/exasol/common/avro/AvroRow.scala diff --git a/src/main/scala/com/exasol/avro/AvroRowIterator.scala b/src/main/scala/com/exasol/common/avro/AvroRowIterator.scala similarity index 100% rename from src/main/scala/com/exasol/avro/AvroRowIterator.scala rename to src/main/scala/com/exasol/common/avro/AvroRowIterator.scala diff --git a/src/main/scala/com/exasol/common/Row.scala b/src/main/scala/com/exasol/common/data/Row.scala similarity index 100% rename from src/main/scala/com/exasol/common/Row.scala rename to src/main/scala/com/exasol/common/data/Row.scala diff --git a/src/main/scala/com/exasol/common/file/BucketFSFileChecker.scala b/src/main/scala/com/exasol/common/file/BucketFSFileChecker.scala new file mode 100644 index 0000000..a3b9662 --- /dev/null +++ b/src/main/scala/com/exasol/common/file/BucketFSFileChecker.scala @@ -0,0 +1,10 @@ +package com.exasol.common.file + +/** + * A BucketFS file location checker implementation. + */ +class BucketFSFileChecker extends FileChecker { + + override final def getLocationPrefix(): String = "/buckets" + +} diff --git a/src/main/scala/com/exasol/common/file/FileChecker.scala b/src/main/scala/com/exasol/common/file/FileChecker.scala new file mode 100644 index 0000000..d1922d0 --- /dev/null +++ b/src/main/scala/com/exasol/common/file/FileChecker.scala @@ -0,0 +1,61 @@ +package com.exasol.common.file + +import java.io.IOException +import java.io.UncheckedIOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +import com.exasol.errorreporting.ExaError + +/** + * An abstract class for file checker implementations. + */ +abstract class FileChecker { + + /** + * Returns an expected prefix that file paths starts with for this filesystem. + * + * @return a location prefix + */ + def getLocationPrefix(): String + + /** + * Cheks if path is regular file. + * + * @param path a path to file location + * @return {@code true} if regular file, otherwise {@code false} + */ + final def isRegularFile(filePath: String): Boolean = { + val path = Paths.get(filePath) + assertBucketFsPath(path) + Files.isRegularFile(path) + } + + private[this] def assertBucketFsPath(path: Path): Unit = { + val file = path.toFile() + try { + val absolutePath = file.getCanonicalPath() + if (!absolutePath.startsWith(getLocationPrefix())) { + throw new IllegalArgumentException( + ExaError + .messageBuilder("E-IEUCS-12") + .message("Provided path {{PATH}} is not a BucketFS file location.", absolutePath) + .mitigation("Please make sure that file path start with '/buckets'.") + .toString() + ) + } + } catch { + case exception: IOException => + throw new UncheckedIOException( + ExaError + .messageBuilder("E-IEUCS-13") + .message("Failed to open BucketFS path {{PATH}}.", path) + .mitigation("Please make sure that file exists in BucketFS location.") + .toString(), + exception + ) + } + } + +} diff --git a/src/main/scala/com/exasol/json/JsonMapper.scala b/src/main/scala/com/exasol/common/json/JsonMapper.scala similarity index 100% rename from src/main/scala/com/exasol/json/JsonMapper.scala rename to src/main/scala/com/exasol/common/json/JsonMapper.scala diff --git a/src/test/scala/com/exasol/avro/AvroComplexTypesTest.scala b/src/test/scala/com/exasol/common/avro/AvroComplexTypesTest.scala similarity index 100% rename from src/test/scala/com/exasol/avro/AvroComplexTypesTest.scala rename to src/test/scala/com/exasol/common/avro/AvroComplexTypesTest.scala diff --git a/src/test/scala/com/exasol/avro/AvroLogicalTypesTest.scala b/src/test/scala/com/exasol/common/avro/AvroLogicalTypesTest.scala similarity index 100% rename from src/test/scala/com/exasol/avro/AvroLogicalTypesTest.scala rename to src/test/scala/com/exasol/common/avro/AvroLogicalTypesTest.scala diff --git a/src/test/scala/com/exasol/avro/AvroPrimitiveReaderTest.scala b/src/test/scala/com/exasol/common/avro/AvroPrimitiveReaderTest.scala similarity index 100% rename from src/test/scala/com/exasol/avro/AvroPrimitiveReaderTest.scala rename to src/test/scala/com/exasol/common/avro/AvroPrimitiveReaderTest.scala diff --git a/src/test/scala/com/exasol/avro/AvroRowIteratorTest.scala b/src/test/scala/com/exasol/common/avro/AvroRowIteratorTest.scala similarity index 100% rename from src/test/scala/com/exasol/avro/AvroRowIteratorTest.scala rename to src/test/scala/com/exasol/common/avro/AvroRowIteratorTest.scala diff --git a/src/test/scala/com/exasol/avro/AvroRowTest.scala b/src/test/scala/com/exasol/common/avro/AvroRowTest.scala similarity index 100% rename from src/test/scala/com/exasol/avro/AvroRowTest.scala rename to src/test/scala/com/exasol/common/avro/AvroRowTest.scala diff --git a/src/test/scala/com/exasol/avro/AvroStringReaderTest.scala b/src/test/scala/com/exasol/common/avro/AvroStringReaderTest.scala similarity index 100% rename from src/test/scala/com/exasol/avro/AvroStringReaderTest.scala rename to src/test/scala/com/exasol/common/avro/AvroStringReaderTest.scala diff --git a/src/test/scala/com/exasol/common/RowTest.scala b/src/test/scala/com/exasol/common/data/RowTest.scala similarity index 100% rename from src/test/scala/com/exasol/common/RowTest.scala rename to src/test/scala/com/exasol/common/data/RowTest.scala diff --git a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala new file mode 100644 index 0000000..a9089ed --- /dev/null +++ b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala @@ -0,0 +1,36 @@ +package com.exasol.common.file + +import java.io.File +import java.nio.file.Files + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class FileCheckerTest extends AnyFunSuite with Matchers { + + test("temporary file checker checks correct path") { + val fileParent = "/tmp/bfsdefault/bucket1" + val file = new File(fileParent, "/file.txt") + Files.createDirectories(new File(fileParent).toPath()) + Files.createFile(file.toPath()) + assert(new TemporaryFileChecker().isRegularFile("/tmp/bfsdefault/bucket1/file.txt") === true) + file.delete() + } + + test("bucketfs file checker throws when path is missing") { + assert(new BucketFSFileChecker().isRegularFile("/buckets/non-existing/default/file.txt") === false) + } + + test("bucketfs file checker throws when path does not start with expected prefix") { + val thrown = intercept[IllegalArgumentException] { + new BucketFSFileChecker().isRegularFile("/var/log/bucket1/file.txt") + } + val message = thrown.getMessage() + assert(message.startsWith("E-IEUCS-12: Provided path '/var/log/bucket1/file.txt' is not a BucketFS file location.")) + } + + class TemporaryFileChecker extends FileChecker { + override final def getLocationPrefix(): String = "/tmp" + } + +} diff --git a/src/test/scala/com/exasol/json/JsonMapperTest.scala b/src/test/scala/com/exasol/common/json/JsonMapperTest.scala similarity index 100% rename from src/test/scala/com/exasol/json/JsonMapperTest.scala rename to src/test/scala/com/exasol/common/json/JsonMapperTest.scala From 2932669cd456149b5a833a6e22bafe139fcabd69 Mon Sep 17 00:00:00 2001 From: morazow Date: Tue, 12 Oct 2021 23:56:15 +0200 Subject: [PATCH 2/5] Updated changes file. --- README.md | 2 +- doc/changes/changes_0.3.0.md | 7 ++++--- project/Dependencies.scala | 2 +- project/plugins.sbt | 4 ++-- scripts/ci.sh | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3904dc7..255189e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://github.com/exasol/import-export-udf-common-scala/actions/workflows/ci-build.yml/badge.svg)](https://github.com/exasol/import-export-udf-common-scala/actions/workflows/ci-build.yml) [![Coveralls](https://img.shields.io/coveralls/exasol/import-export-udf-common-scala.svg)](https://coveralls.io/github/exasol/import-export-udf-common-scala) -[![Maven Central](https://img.shields.io/maven-central/v/com.exasol/import-export-udf-common-scala)](https://search.maven.org/artifact/com.exasol/import-export-udf-common-scala) +[![Maven Central](https://img.shields.io/maven-central/v/com.exasol/import-export-udf-common-scala)](https://search.maven.org/artifact/com.exasol/import-export-udf-common-scala_2.13) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Aimport-export-udf-common-scala&metric=alert_status)](https://sonarcloud.io/dashboard?id=com.exasol%3Aimport-export-udf-common-scala) diff --git a/doc/changes/changes_0.3.0.md b/doc/changes/changes_0.3.0.md index 772443e..7933eae 100644 --- a/doc/changes/changes_0.3.0.md +++ b/doc/changes/changes_0.3.0.md @@ -14,6 +14,7 @@ In this release, we added custom user defined property separators. We also migra * #15: Migrated to Github actions * #18: Added unified error codes +* #20: Added file location checker ## Dependency Updates @@ -28,7 +29,7 @@ In this release, we added custom user defined property separators. We also migra ### Test Dependency Updates -* Updated `org.mockito:mockito-core:test:3.6.0` to `3.12.4` +* Updated `org.mockito:mockito-core:test:3.6.0` to `4.0.0` * Updated `org.scalatest:scalatest:test:3.2.2` to `3.2.10` ### Plugin Updates @@ -36,9 +37,9 @@ In this release, we added custom user defined property separators. We also migra * Added `org.scalameta:sbt-scalafmt:2.4.3` * Updated `com.jsuereth:sbt-pgp:2.0.1` to `2.1.1` * Updated `com.timushev.sbt:sbt-updates:0.5.1` to `0.6.0` -* Updated `com.typesafe.sbt:sbt-git:1.0.0` to `1.0.1` +* Updated `com.typesafe.sbt:sbt-git:1.0.0` to `1.0.2` * Updated `org.scoverage:sbt-coveralls:1.2.7` to `1.3.1` -* Updated `org.scoverage:sbt-scoverage:1.6.1` to `1.9.0` +* Updated `org.scoverage:sbt-scoverage:1.6.1` to `1.9.1` * Updated `org.wartremover:sbt-wartremover:2.4.12` to `2.4.16` * Updated `org.wartremover:sbt-wartremover-contib:1.3.10` to `1.3.12` * Updated `org.xerial.sbt:sbt-sonatype:3.9.4` to `3.9.10` diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e508864..f5b113e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { // Test dependencies versions private val ScalaTestVersion = "3.2.10" private val ScalaTestPlusVersion = "1.0.0-M2" - private val MockitoCoreVersion = "3.12.4" + private val MockitoCoreVersion = "4.0.0" val ExasolResolvers: Seq[Resolver] = Seq( "Exasol Releases" at "https://maven.exasol.com/artifactory/exasol-releases" diff --git a/project/plugins.sbt b/project/plugins.sbt index 0bbdac8..5a62e8a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -16,7 +16,7 @@ addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.0") // Adds Scala Code Coverage (Scoverage) used during unit tests // http://github.com/scoverage/sbt-scoverage -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.1") // Adds SBT Coveralls plugin for uploading Scala code coverage to // https://coveralls.io @@ -46,4 +46,4 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.1.1") // Adds a `git` plugin // https://github.com/sbt/sbt-git -addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.2") diff --git a/scripts/ci.sh b/scripts/ci.sh index 5ae88b2..5fff77f 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -6,7 +6,7 @@ set -o errtrace -o nounset -o pipefail -o errexit BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )" cd "$BASE_DIR" -DEFAULT_SCALA_VERSION=2.12.12 +DEFAULT_SCALA_VERSION=2.13.6 if [[ -z "${SCALA_VERSION:-}" ]]; then echo "Environment variable SCALA_VERSION is not set" From 795d7d2f8364186404bb4f449d003d87573d2774 Mon Sep 17 00:00:00 2001 From: morazow Date: Wed, 13 Oct 2021 09:33:25 +0200 Subject: [PATCH 3/5] Refactored --- .../scala/com/exasol/common/file/FileChecker.scala | 12 ++++++------ .../com/exasol/common/file/FileCheckerTest.scala | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/scala/com/exasol/common/file/FileChecker.scala b/src/main/scala/com/exasol/common/file/FileChecker.scala index d1922d0..54b0188 100644 --- a/src/main/scala/com/exasol/common/file/FileChecker.scala +++ b/src/main/scala/com/exasol/common/file/FileChecker.scala @@ -28,11 +28,11 @@ abstract class FileChecker { */ final def isRegularFile(filePath: String): Boolean = { val path = Paths.get(filePath) - assertBucketFsPath(path) + checkStartsWithPath(path) Files.isRegularFile(path) } - private[this] def assertBucketFsPath(path: Path): Unit = { + private[this] def checkStartsWithPath(path: Path): Unit = { val file = path.toFile() try { val absolutePath = file.getCanonicalPath() @@ -40,8 +40,8 @@ abstract class FileChecker { throw new IllegalArgumentException( ExaError .messageBuilder("E-IEUCS-12") - .message("Provided path {{PATH}} is not a BucketFS file location.", absolutePath) - .mitigation("Please make sure that file path start with '/buckets'.") + .message("Provided path {{PATH}} does not start with expected location prefix.", absolutePath) + .mitigation("Please make sure that file path start with {{PREFIX}}.", getLocationPrefix()) .toString() ) } @@ -50,8 +50,8 @@ abstract class FileChecker { throw new UncheckedIOException( ExaError .messageBuilder("E-IEUCS-13") - .message("Failed to open BucketFS path {{PATH}}.", path) - .mitigation("Please make sure that file exists in BucketFS location.") + .message("Failed to open path {{PATH}}.", path) + .mitigation("Please make sure that file exists and starts with location {{PREFIX}}", getLocationPrefix()) .toString(), exception ) diff --git a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala index a9089ed..3708ca1 100644 --- a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala +++ b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala @@ -26,7 +26,9 @@ class FileCheckerTest extends AnyFunSuite with Matchers { new BucketFSFileChecker().isRegularFile("/var/log/bucket1/file.txt") } val message = thrown.getMessage() - assert(message.startsWith("E-IEUCS-12: Provided path '/var/log/bucket1/file.txt' is not a BucketFS file location.")) + assert(message.startsWith("E-IEUCS-12")) + assert(message.contains("Provided path '/var/log/bucket1/file.txt' does not start with expected")) + assert(message.contains("Please make sure that file path start with '/buckets'.")) } class TemporaryFileChecker extends FileChecker { From 85afa74306d66cd6fb2ef4729b3f946ddeaa086a Mon Sep 17 00:00:00 2001 From: morazow Date: Wed, 13 Oct 2021 09:58:33 +0200 Subject: [PATCH 4/5] Updated tests. --- .../com/exasol/common/file/FileChecker.scala | 9 ++++----- .../com/exasol/common/file/FileCheckerTest.scala | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/scala/com/exasol/common/file/FileChecker.scala b/src/main/scala/com/exasol/common/file/FileChecker.scala index 54b0188..46b9e26 100644 --- a/src/main/scala/com/exasol/common/file/FileChecker.scala +++ b/src/main/scala/com/exasol/common/file/FileChecker.scala @@ -1,5 +1,6 @@ package com.exasol.common.file +import java.io.File import java.io.IOException import java.io.UncheckedIOException import java.nio.file.Files @@ -28,12 +29,11 @@ abstract class FileChecker { */ final def isRegularFile(filePath: String): Boolean = { val path = Paths.get(filePath) - checkStartsWithPath(path) + checkStartsWithPath(path.toFile()) Files.isRegularFile(path) } - private[this] def checkStartsWithPath(path: Path): Unit = { - val file = path.toFile() + protected[file] final def checkStartsWithPath(file: File): Unit = try { val absolutePath = file.getCanonicalPath() if (!absolutePath.startsWith(getLocationPrefix())) { @@ -50,12 +50,11 @@ abstract class FileChecker { throw new UncheckedIOException( ExaError .messageBuilder("E-IEUCS-13") - .message("Failed to open path {{PATH}}.", path) + .message("Failed to open path {{PATH}}.", file.getAbsolutePath()) .mitigation("Please make sure that file exists and starts with location {{PREFIX}}", getLocationPrefix()) .toString(), exception ) } - } } diff --git a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala index 3708ca1..c088f97 100644 --- a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala +++ b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala @@ -1,12 +1,16 @@ package com.exasol.common.file import java.io.File +import java.io.IOException +import java.io.UncheckedIOException import java.nio.file.Files +import org.mockito.Mockito.when import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import org.scalatestplus.mockito.MockitoSugar -class FileCheckerTest extends AnyFunSuite with Matchers { +class FileCheckerTest extends AnyFunSuite with Matchers with MockitoSugar { test("temporary file checker checks correct path") { val fileParent = "/tmp/bfsdefault/bucket1" @@ -31,6 +35,16 @@ class FileCheckerTest extends AnyFunSuite with Matchers { assert(message.contains("Please make sure that file path start with '/buckets'.")) } + test("file checker throw ioexception") { + val file = mock[File] + when(file.getAbsolutePath()).thenReturn("test/path") + when(file.getCanonicalPath()).thenThrow(new IOException()) + val thrown = intercept[UncheckedIOException] { + new BucketFSFileChecker().checkStartsWithPath(file) + } + assert(thrown.getMessage().startsWith("E-IEUCS-13: Failed to open path 'test/path'.")) + } + class TemporaryFileChecker extends FileChecker { override final def getLocationPrefix(): String = "/tmp" } From 0b7778fba46e8d93cbb0027f805da2b2e16e16f5 Mon Sep 17 00:00:00 2001 From: morazow Date: Wed, 13 Oct 2021 10:03:33 +0200 Subject: [PATCH 5/5] Fixed CI findings. --- src/main/scala/com/exasol/common/file/FileChecker.scala | 1 - src/test/scala/com/exasol/common/file/FileCheckerTest.scala | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/scala/com/exasol/common/file/FileChecker.scala b/src/main/scala/com/exasol/common/file/FileChecker.scala index 46b9e26..f3d5b00 100644 --- a/src/main/scala/com/exasol/common/file/FileChecker.scala +++ b/src/main/scala/com/exasol/common/file/FileChecker.scala @@ -4,7 +4,6 @@ import java.io.File import java.io.IOException import java.io.UncheckedIOException import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths import com.exasol.errorreporting.ExaError diff --git a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala index c088f97..475921b 100644 --- a/src/test/scala/com/exasol/common/file/FileCheckerTest.scala +++ b/src/test/scala/com/exasol/common/file/FileCheckerTest.scala @@ -35,7 +35,7 @@ class FileCheckerTest extends AnyFunSuite with Matchers with MockitoSugar { assert(message.contains("Please make sure that file path start with '/buckets'.")) } - test("file checker throw ioexception") { + test("bucketfs file checker throws ioexception") { val file = mock[File] when(file.getAbsolutePath()).thenReturn("test/path") when(file.getCanonicalPath()).thenThrow(new IOException())