diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathAssertions.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathAssertions.kt index 45ac998950..2e94036193 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathAssertions.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathAssertions.kt @@ -286,6 +286,26 @@ fun Expect.isRegularFile(): Expect = fun Expect.isDirectory(): Expect = _logicAppend { isDirectory() } +/** + * Expects that the subject of the assertion (a [Path]) is a relative path; + * meaning that the [Path] specified in this instance does not start at the file system root. + * + * This assertion _resolves_ symbolic links. + * Therefore, if a symbolic link exists at the location the subject points to, search will continue + * at the location the link points at. + * + * This assertion is not atomic with respect to concurrent file system operations on the paths the assertions works on. + * Its result, in particular its extended explanations, may be wrong if such concurrent file system operations + * take place. + * + * @return An [Expect] for the current subject of the assertion. + * @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct. + * + * @since 0.14.0 + */ +fun Expect.isRelative(): Expect = + _logicAppend { isRelative() } + /** * Creates an [Expect] for the property [Path.extension][ch.tutteli.niok.extension] * (provided via [niok](https://github.com/robstoll/niok)) of the subject of the assertion, diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathAssertionsSpec.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathAssertionsSpec.kt index 845c641257..dec3fb3c77 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathAssertionsSpec.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathAssertionsSpec.kt @@ -20,6 +20,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe fun0(Expect::isExecutable), fun0(Expect::isRegularFile), fun0(Expect::isDirectory), + fun0(Expect::isRelative), fun1(Expect::hasSameBinaryContentAs), fun3(Expect::hasSameTextualContentAs), fun1(Companion::hasSameTextualContentAsDefaultArgs) diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-common/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/keywords.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-common/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/keywords.kt index beac0bad05..42e36576c2 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-common/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/keywords.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-common/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/keywords.kt @@ -22,6 +22,14 @@ object aRegularFile : Keyword */ object aDirectory : Keyword +/** + * A helper construct to allow expressing assertions about a path being relative. + * It can be used for a parameterless function so that it has one parameter and thus can be used as infix function. + * + * @since 0.14.0 + */ +object relative : Keyword + /** * Represents a helper construct which allows to express blankness. * It can be used for a parameterless function so that it has one parameter and thus can be used as infix function. diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt index 993c75a9ca..6944b4f706 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt @@ -296,6 +296,27 @@ infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") aRegularFile: infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") aDirectory: aDirectory): Expect = _logicAppend { isDirectory() } +/** + * Expects that the subject of the assertion (a [Path]) is a relative path; + * meaning that the [Path] specified in this instance does not start at the file system root. + * + * This assertion _resolves_ symbolic links. + * Therefore, if a symbolic link exists at the location the subject points to, search will continue + * at the location the link points at. + * + * This assertion is not atomic with respect to concurrent file system operations on the paths the assertions works on. + * Its result, in particular its extended explanations, may be wrong if such concurrent file system operations + * take place. + * + * @return An [Expect] for the current subject of the assertion. + * @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct. + * + * @since 0.14.0 + */ +infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") relative: relative): Expect = + _logicAppend { isRelative() } + + /** * Creates an [Expect] for the property [Path.extension][ch.tutteli.niok.extension] * (provided via [niok](https://github.com/robstoll/niok)) of the subject of the assertion, diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathAssertionsSpec.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathAssertionsSpec.kt index fdb1f810e7..0becea6b4a 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathAssertionsSpec.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathAssertionsSpec.kt @@ -21,6 +21,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe "toBe ${executable::class.simpleName}" to Companion::isExecutable, "toBe ${aRegularFile::class.simpleName}" to Companion::isRegularFile, "toBe ${aDirectory::class.simpleName}" to Companion::isDirectory, + "toBe ${relative::class.simpleName}" to Companion::isRelative, fun1(Expect::hasSameBinaryContentAs), fun3(Companion::hasSameTextualContentAs), fun1(Companion::hasSameTextualContentAsDefaultArgs) @@ -34,6 +35,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe private fun isExecutable(expect: Expect) = expect toBe executable private fun isRegularFile(expect: Expect) = expect toBe aRegularFile private fun isDirectory(expect: Expect) = expect toBe aDirectory + private fun isRelative(expect: Expect) = expect toBe relative private fun hasSameTextualContentAs( expect: Expect, @@ -62,6 +64,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe a1 toBe writable a1 toBe aRegularFile a1 toBe aDirectory + a1 toBe relative a1 hasSameTextualContentAs withEncoding(Paths.get("a")) a1 hasSameTextualContentAs Paths.get("a") } diff --git a/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt b/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt index f206d28c41..f7f2623bb7 100644 --- a/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt +++ b/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt @@ -32,6 +32,7 @@ fun AssertionContainer.isWritable(): Assertion = impl.isWritable(t fun AssertionContainer.isExecutable(): Assertion = impl.isExecutable(this) fun AssertionContainer.isRegularFile(): Assertion = impl.isRegularFile(this) fun AssertionContainer.isDirectory(): Assertion = impl.isDirectory(this) +fun AssertionContainer.isRelative(): Assertion = impl.isRelative(this) fun AssertionContainer.hasSameTextualContentAs(targetPath: Path, sourceCharset: Charset, targetCharset: Charset): Assertion = impl.hasSameTextualContentAs(this, targetPath, sourceCharset, targetCharset) diff --git a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt index 2d22d1506d..39ca2ed22e 100644 --- a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt +++ b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt @@ -28,6 +28,7 @@ interface PathAssertions { fun isExecutable(container: AssertionContainer): Assertion fun isRegularFile(container: AssertionContainer): Assertion fun isDirectory(container: AssertionContainer): Assertion + fun isRelative(container: AssertionContainer): Assertion fun hasSameTextualContentAs( container: AssertionContainer, diff --git a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/creating/filesystem/hints/hints.kt b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/creating/filesystem/hints/hints.kt index 13b7b66055..d9a1342e18 100644 --- a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/creating/filesystem/hints/hints.kt +++ b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/creating/filesystem/hints/hints.kt @@ -11,6 +11,7 @@ import ch.tutteli.atrium.logic.creating.filesystem.IoResult import ch.tutteli.atrium.logic.creating.filesystem.Success import ch.tutteli.atrium.reporting.translating.Translatable import ch.tutteli.atrium.translations.DescriptionBasic +import ch.tutteli.atrium.translations.DescriptionPathAssertion import ch.tutteli.atrium.translations.DescriptionPathAssertion.* import ch.tutteli.niok.followSymbolicLink import ch.tutteli.niok.getFileAttributeView @@ -35,16 +36,30 @@ inline fun Descriptive.DescriptionOption.withIOExcept fun Descriptive.DescriptionOption.withFileAttributesFailureHint( expect: Expect> +): Descriptive.DescriptionOption = + withAttributeFailureHint(expect) { it.value.fileType } + +fun Descriptive.DescriptionOption.withPathAttributesFailureHint( + expect: Expect> +): Descriptive.DescriptionOption = + withAttributeFailureHint(expect) { it.path.type } + +private inline fun Descriptive.DescriptionOption.withAttributeFailureHint( + expect: Expect>, + crossinline actualTypeProvider: (Success) -> Translatable ): Descriptive.DescriptionOption = withFailureHintBasedOnDefinedSubject(expect) { result -> explainForResolvedLink(result.path) { realPath -> when (result) { - is Success -> describeWas(result.value.fileType) + is Success -> describeWas(actualTypeProvider(result)) is Failure -> hintForIoException(realPath, result.exception) } } } +private val Path.type: DescriptionPathAssertion + get() = if (isAbsolute) ABSOLUTE_PATH else RELATIVE_PATH + /** * Internal for testing purposes only */ diff --git a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt index 313c3378fd..534b7df78d 100644 --- a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt +++ b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt @@ -109,6 +109,9 @@ class DefaultPathAssertions : PathAssertions { override fun isDirectory(container: AssertionContainer): Assertion = fileTypeAssertion(container, A_DIRECTORY) { it.isDirectory } + override fun isRelative(container: AssertionContainer): Assertion = + pathTypeAssertion(container, RELATIVE_PATH) { !it.isAbsolute } + private fun filePermissionAssertion( container: AssertionContainer, permissionName: Translatable, @@ -147,6 +150,17 @@ class DefaultPathAssertions : PathAssertions { .build() } + private inline fun pathTypeAssertion( + container: AssertionContainer, + typeName: Translatable, + crossinline typeTest: (Path) -> Boolean + ) = changeSubjectToFileAttributes(container) { fileAttributesExpect -> + assertionBuilder.descriptive + .withTest(fileAttributesExpect) { it is Success && typeTest(it.path) } + .withPathAttributesFailureHint(fileAttributesExpect) + .withDescriptionAndRepresentation(DescriptionBasic.IS, typeName) + .build() + } override fun fileName(container: AssertionContainer): FeatureExtractorBuilder.ExecutionStep = container.manualFeature(FILE_NAME) { fileName.toString() } diff --git a/misc/deprecated/apis/fluent-en_GB-jdk8/atrium-api-fluent-en_GB-jdk8-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/jdk8/PathAssertionsSpec.kt b/misc/deprecated/apis/fluent-en_GB-jdk8/atrium-api-fluent-en_GB-jdk8-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/jdk8/PathAssertionsSpec.kt index 0d19b3915e..676278b91e 100644 --- a/misc/deprecated/apis/fluent-en_GB-jdk8/atrium-api-fluent-en_GB-jdk8-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/jdk8/PathAssertionsSpec.kt +++ b/misc/deprecated/apis/fluent-en_GB-jdk8/atrium-api-fluent-en_GB-jdk8-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/jdk8/PathAssertionsSpec.kt @@ -4,6 +4,7 @@ package ch.tutteli.atrium.api.fluent.en_GB.jdk8 import ch.tutteli.atrium.api.fluent.en_GB.isExecutable +import ch.tutteli.atrium.api.fluent.en_GB.isRelative import ch.tutteli.atrium.creating.Expect import ch.tutteli.atrium.specs.fun0 import ch.tutteli.atrium.specs.fun1 @@ -24,6 +25,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe fun0(Expect::isExecutable), // checks the new function from fluent-jvm because it is not implemented in fluent-jkd8 fun0(Expect::isRegularFile), fun0(Expect::isDirectory), + fun0(Expect::isRelative), fun1(Expect::hasSameBinaryContentAs), fun3(Expect::hasSameTextualContentAs), fun1(Companion::hasSameTextualContentAsDefaultArgs) diff --git a/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathAssertionsSpec.kt b/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathAssertionsSpec.kt index 59528edb41..3dd741dbb2 100644 --- a/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathAssertionsSpec.kt +++ b/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathAssertionsSpec.kt @@ -19,6 +19,7 @@ import org.spekframework.spek2.dsl.Skip.No import org.spekframework.spek2.dsl.Skip.Yes import org.spekframework.spek2.dsl.TestBody import org.spekframework.spek2.style.specification.Suite +import java.io.File import java.io.IOException import java.nio.charset.Charset import java.nio.file.Files @@ -43,6 +44,7 @@ abstract class PathAssertionsSpec( isExecutable: Fun0, isRegularFile: Fun0, isDirectory: Fun0, + isRelative: Fun0, hasSameBinaryContentAs: Fun1, hasSameTextualContentAs: Fun3, hasSameTextualContentAsDefaultArgs: Fun1, @@ -62,6 +64,7 @@ abstract class PathAssertionsSpec( isExecutable.forSubjectLess(), isRegularFile.forSubjectLess(), isDirectory.forSubjectLess(), + isRelative.forSubjectLess(), hasSameBinaryContentAs.forSubjectLess(Paths.get("a")), hasSameTextualContentAs.forSubjectLess(Paths.get("a"), Charsets.ISO_8859_1, Charsets.ISO_8859_1), hasSameTextualContentAsDefaultArgs.forSubjectLess(Paths.get("a")) @@ -828,6 +831,49 @@ abstract class PathAssertionsSpec( } } + describeFun(isRelative) { + val isRelativeFun = isRelative.lambda + val expectedMessage = "$isDescr: ${RELATIVE_PATH.getDefault()}" + + context("not accessible") { + it("throws an AssertionError for a non-existent path") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.tmpDir.resolve("nonExistent")) + expect { + expect(file).isRelativeFun() + }.toThrow().message { + contains(expectedMessage, FAILURE_DUE_TO_NO_SUCH_FILE.getDefault()) + containsExplanationFor(maybeLink) + } + } + + itPrintsFileAccessProblemDetails { testFile -> + expect(testFile).isRelativeFun() + } + } + + it("throws an AssertionError for absolute path") withAndWithoutSymlink { maybeLink -> + val path = maybeLink.create(tempFolder.newFile("test")) + .absolutePathAsString + .let { File(it) } + .toPath() + + expect { + expect(path).isRelativeFun() + }.toThrow().message { + contains(expectedMessage, "${WAS.getDefault()}: ${ABSOLUTE_PATH.getDefault()}") + containsExplanationFor(maybeLink) + } + } + + it("does not throw for relative path") withAndWithoutSymlink { maybeLink -> + val relativeRoot = tempFolder.newDirectory("relativeRoot") + val path = maybeLink.create(relativeRoot.newFile("file")) + .relativize(relativeRoot) + + expect(path).isRelativeFun() + } + } + describeFun(hasSameBinaryContentAs, hasSameTextualContentAs, hasSameTextualContentAsDefaultArgs) { val hasSameBinaryContentAsFun = hasSameBinaryContentAs.lambda val hasSameTextualContentAsFun = hasSameTextualContentAs.lambda diff --git a/translations/de_CH/atrium-translations-de_CH-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt b/translations/de_CH/atrium-translations-de_CH-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt index fbb5af7f34..ce16fbe25e 100644 --- a/translations/de_CH/atrium-translations-de_CH-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt +++ b/translations/de_CH/atrium-translations-de_CH-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt @@ -13,6 +13,7 @@ import java.nio.file.Path * Contains the [DescriptiveAssertion.description]s of the assertion functions which are applicable to [Path]. */ enum class DescriptionPathAssertion(override val value: String) : StringBasedTranslatable { + ABSOLUTE_PATH("ein absoluter Pfad"), DOES_NOT_HAVE_PARENT("!! hat keinen Elternpfad"), ENDS_NOT_WITH("endet nicht mit"), ENDS_WITH("endet mit"), @@ -44,5 +45,6 @@ enum class DescriptionPathAssertion(override val value: String) : StringBasedTra HINT_CLOSEST_EXISTING_PARENT_DIRECTORY("das nächste, existierende Elternverzeichnis ist %s"), HINT_FOLLOWED_SYMBOLIC_LINK("folgte der symbolischen Verknüpfung %s nach %s"), HAS_SAME_TEXTUAL_CONTENT("hat denselben textlichen Inhalt mit Kodierung %s, %s"), - HAS_SAME_BINARY_CONTENT("hat denselben binären Inhalt") + HAS_SAME_BINARY_CONTENT("hat denselben binären Inhalt"), + RELATIVE_PATH("ein relativer Pfad") } diff --git a/translations/en_GB/atrium-translations-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt b/translations/en_GB/atrium-translations-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt index 3d8d56fdde..4ae9ed9a87 100644 --- a/translations/en_GB/atrium-translations-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt +++ b/translations/en_GB/atrium-translations-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/translations/DescriptionPathAssertion.kt @@ -8,6 +8,7 @@ package ch.tutteli.atrium.translations import ch.tutteli.atrium.reporting.translating.StringBasedTranslatable enum class DescriptionPathAssertion(override val value: String) : StringBasedTranslatable { + ABSOLUTE_PATH("an absolute path"), DOES_NOT_HAVE_PARENT("!! does not have a parent"), ENDS_NOT_WITH("does not end with"), ENDS_WITH("ends with"), @@ -40,4 +41,5 @@ enum class DescriptionPathAssertion(override val value: String) : StringBasedTra HINT_FOLLOWED_SYMBOLIC_LINK("followed the symbolic link %s to %s"), HAS_SAME_TEXTUAL_CONTENT("has same textual content with encoding %s, %s"), HAS_SAME_BINARY_CONTENT("has same binary content"), + RELATIVE_PATH("a relative path"), }