From 2d3b1c898a56c8cb1cea8eec8429bcc0e568f646 Mon Sep 17 00:00:00 2001 From: jaime Date: Sun, 14 Feb 2021 11:53:03 +0100 Subject: [PATCH] add convenience shortcut for `Path.toBeASymbolicLink` feature For both infix and fluent API styles --- .../atrium/api/fluent/en_GB/pathAssertions.kt | 15 ++++ .../api/fluent/en_GB/PathExpectationsSpec.kt | 2 + .../en_GB/samples/PathAssertionSamples.kt | 31 +++++++ .../atrium/api/infix/en_GB/keywords.kt | 6 ++ .../atrium/api/infix/en_GB/pathAssertions.kt | 15 ++++ .../api/infix/en_GB/PathExpectationsSpec.kt | 2 + .../en_GB/samples/PathAssertionSamples.kt | 32 +++++++ .../kotlin/ch/tutteli/atrium/logic/path.kt | 1 + .../ch/tutteli/atrium/logic/PathAssertions.kt | 1 + .../logic/impl/DefaultPathAssertions.kt | 6 +- .../specs/integration/PathExpectationsSpec.kt | 84 +++++++++++++++++-- 11 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathAssertionSamples.kt create mode 100644 apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathAssertionSamples.kt 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 19d1a45a5..f14835691 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 @@ -269,6 +269,21 @@ fun Expect.isRegularFile(): Expect = fun Expect.isDirectory(): Expect = _logicAppend { isDirectory() } +/** + * Expects that the subject of `this` expectation (a [Path]) is a symbolic link; + * meaning that there is a file system entry at the location the [Path] points to and that is a symbolic link. + * + * This assertion is not atomic with respect to concurrent file system operations on the paths the assertion 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 subject of `this` expectation. + * + * @since 0.16.0 + */ +fun Expect.toBeASymbolicLink(): Expect = + _logicAppend { toBeASymbolicLink() } + /** * Expects that the subject of `this` expectation (a [Path]) is an absolute path; * meaning that the [Path] specified in this instance starts at the file system root. diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt index d6bd93671..90907bcc2 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt @@ -17,6 +17,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation fun0(Expect::isExecutable), fun0(Expect::isRegularFile), fun0(Expect::isDirectory), + fun0(Expect::toBeASymbolicLink), fun0(Expect::isAbsolute), fun0(Expect::isRelative), Expect::hasDirectoryEntry.name to Companion::hasDirectoryEntrySingle, @@ -57,6 +58,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation a1.isWritable() a1.isRegularFile() a1.isDirectory() + a1.toBeASymbolicLink() a1.hasSameBinaryContentAs(Paths.get("a")) a1.hasSameTextualContentAs(Paths.get("a")) diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathAssertionSamples.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathAssertionSamples.kt new file mode 100644 index 000000000..ac1bd3c7b --- /dev/null +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathAssertionSamples.kt @@ -0,0 +1,31 @@ +package ch.tutteli.atrium.api.fluent.en_GB.samples + +import ch.tutteli.atrium.api.fluent.en_GB.toBeASymbolicLink +import ch.tutteli.atrium.api.verbs.internal.expect +import ch.tutteli.niok.newFile +import java.nio.file.Files +import kotlin.test.Test + +class PathAssertionSamples { + + private val tempDir = Files.createTempDirectory("PathAssertionSamples") + + @Test + fun isASymbolicLink() { + val target = tempDir.newFile("target") + val link = Files.createSymbolicLink(tempDir.resolve("link"), target) + + // Passes, as subject `link` is a symbolic link + expect(link).toBeASymbolicLink() + } + + @Test + fun isNotASymbolicLink() { + val path = tempDir.newFile("somePath") + + // Fails, as subject `path` is a not a symbolic link + fails { + expect(path).toBeASymbolicLink() + } + } +} 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 97ba3cf5c..92ac172c1 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,12 @@ object aRegularFile : Keyword */ object aDirectory : Keyword +/** + * A helper construct to allow expressing assertions about a path being a symbolic link. + * It can be used for a parameterless function so that it has one parameter and thus can be used as infix function. + */ +object aSymbolicLink : Keyword + /** * A helper construct to allow expressing assertions about a path being absolute. * 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 64e4e42aa..dc25c741d 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 @@ -332,6 +332,21 @@ 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 `this` expectation (a [Path]) is a symbolic link; + * meaning that there is a file system entry at the location the [Path] points to and that is a symbolic link. + * + * This assertion is not atomic with respect to concurrent file system operations on the paths the assertion 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 subject of `this` expectation. + * + * @since 0.16.0 + */ +infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") aSymbolicLink: aSymbolicLink): Expect = + _logicAppend { toBeASymbolicLink() } + /** * Expects that the subject of `this` expectation (a [Path]) is an absolute path; * meaning that the [Path] specified in this instance starts at the file system root. diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt index 9e07596e2..5b7742f76 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt @@ -19,6 +19,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation "toBe ${executable::class.simpleName}" to Companion::isExecutable, "toBe ${aRegularFile::class.simpleName}" to Companion::isRegularFile, "toBe ${aDirectory::class.simpleName}" to Companion::isDirectory, + "toBe ${aSymbolicLink::class.simpleName}" to Companion::toBeASymbolicLink, "toBe ${relative::class.simpleName}" to Companion::isAbsolute, "toBe ${relative::class.simpleName}" to Companion::isRelative, fun1(Expect::hasDirectoryEntry), @@ -46,6 +47,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation 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 toBeASymbolicLink(expect: Expect) = expect toBe aSymbolicLink private fun isAbsolute(expect: Expect) = expect toBe absolute private fun isRelative(expect: Expect) = expect toBe relative private fun hasDirectoryEntryMultiple(expect: Expect, entry: String, vararg otherEntries: String) = diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathAssertionSamples.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathAssertionSamples.kt new file mode 100644 index 000000000..a354c1924 --- /dev/null +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathAssertionSamples.kt @@ -0,0 +1,32 @@ +package ch.tutteli.atrium.api.infix.en_GB.samples + +import ch.tutteli.atrium.api.infix.en_GB.aSymbolicLink +import ch.tutteli.atrium.api.infix.en_GB.toBe +import ch.tutteli.atrium.api.verbs.internal.expect +import ch.tutteli.niok.newFile +import java.nio.file.Files +import kotlin.test.Test + +class PathAssertionSamples { + + private val tempDir = Files.createTempDirectory("PathAssertionSamples") + + @Test + fun isASymbolicLink() { + val target = tempDir.newFile("target") + val link = Files.createSymbolicLink(tempDir.resolve("link"), target) + + // Passes, as subject `link` is a symbolic link + expect(link) toBe aSymbolicLink + } + + @Test + fun isNotASymbolicLink() { + val path = tempDir.newFile("somePath") + + // Fails, as subject `path` is a not a symbolic link + fails { + expect(path) toBe aSymbolicLink + } + } +} 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 32d8c7d90..eca4ad3f9 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 @@ -34,6 +34,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.toBeASymbolicLink(): Assertion = impl.toBeASymbolicLink(this) fun AssertionContainer.isAbsolute(): Assertion = impl.isAbsolute(this) fun AssertionContainer.isRelative(): Assertion = impl.isRelative(this) 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 1e5e77675..b316a6c4f 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 @@ -29,6 +29,7 @@ interface PathAssertions { fun isExecutable(container: AssertionContainer): Assertion fun isRegularFile(container: AssertionContainer): Assertion fun isDirectory(container: AssertionContainer): Assertion + fun toBeASymbolicLink(container: AssertionContainer): Assertion fun isAbsolute(container: AssertionContainer): Assertion fun isRelative(container: AssertionContainer): Assertion 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 10137ccc3..6b33ce23e 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 @@ -112,6 +112,9 @@ class DefaultPathAssertions : PathAssertions { override fun isDirectory(container: AssertionContainer): Assertion = fileTypeAssertion(container, A_DIRECTORY) { it.isDirectory } + override fun toBeASymbolicLink(container: AssertionContainer): Assertion = + fileTypeAssertion(container, A_SYMBOLIC_LINK, NOFOLLOW_LINKS) { it.isSymbolicLink } + override fun isAbsolute(container: AssertionContainer): Assertion = container.createDescriptiveAssertion(DescriptionBasic.IS, ABSOLUTE_PATH) { it.isAbsolute } @@ -147,8 +150,9 @@ class DefaultPathAssertions : PathAssertions { private inline fun fileTypeAssertion( container: AssertionContainer, typeName: Translatable, + linkOption: LinkOption? = null, crossinline typeTest: (BasicFileAttributes) -> Boolean - ) = changeSubjectToFileAttributes(container) { fileAttributesExpect -> + ) = changeSubjectToFileAttributes(container, linkOption) { fileAttributesExpect -> assertionBuilder.descriptive .withTest(fileAttributesExpect) { it is Success && typeTest(it.value) } .withFileAttributesFailureHint(fileAttributesExpect) diff --git a/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt b/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt index 68ce49a41..6eaf9c2f9 100644 --- a/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt +++ b/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt @@ -39,6 +39,7 @@ abstract class PathExpectationsSpec( isExecutable: Fun0, isRegularFile: Fun0, isDirectory: Fun0, + isSymbolicLink: Fun0, isAbsolute: Fun0, isRelative: Fun0, hasDirectoryEntrySingle: Fun1, @@ -72,6 +73,7 @@ abstract class PathExpectationsSpec( isExecutable.forSubjectLess(), isRegularFile.forSubjectLess(), isDirectory.forSubjectLess(), + isSymbolicLink.forSubjectLess(), isAbsolute.forSubjectLess(), isRelative.forSubjectLess(), hasDirectoryEntrySingle.forSubjectLess("a"), @@ -112,19 +114,27 @@ abstract class PathExpectationsSpec( val fileNameDescr = FILE_NAME.getDefault() val fileNameWithoutExtensionDescr = FILE_NAME_WITHOUT_EXTENSION.getDefault() - fun Suite.it(description: String, skip: Skip = No, timeout: Long = delegate.defaultTimeout) = - SymlinkTestBuilder({ tempFolder }, skipWithLink = ifSymlinksNotSupported) { prefix, innerSkip, body -> + fun Suite.it( + description: String, + skip: Skip = No, + forceNoLink: Skip = No, + timeout: Long = delegate.defaultTimeout + ): SymlinkTestBuilder { + val skipWithLink = if (forceNoLink == No) ifSymlinksNotSupported else forceNoLink + return SymlinkTestBuilder({ tempFolder }, skipWithLink) { prefix, innerSkip, body -> val skipToUse = if (skip == No) innerSkip else skip it(prefix + description, skipToUse, timeout, body) } + } - fun Suite.itPrintsParentAccessDeniedDetails(block: (Path) -> Unit) { + fun Suite.itPrintsParentAccessDeniedDetails(forceNoLinks: Skip = No, block: (Path) -> Unit) { // this test case makes only sense on POSIX systems, where a missing execute permission on the parent means // that children cannot be accessed. On Windows, for example, one can still access children whithout any // permissions on the parent. it( "POSIX: prints parent permission error details", - skip = ifPosixNotSupported + skip = ifPosixNotSupported, + forceNoLink = forceNoLinks ) withAndWithoutSymlink { maybeLink -> val start = tempFolder.newDirectory("startDir") val doesNotExist = maybeLink.create(start.resolve("i").resolve("dont").resolve("exist")) @@ -166,8 +176,8 @@ abstract class PathExpectationsSpec( } } - fun Suite.itPrintsFileAccessProblemDetails(block: (Path) -> Unit) { - it("prints the closest existing parent if it is a directory") withAndWithoutSymlink { maybeLink -> + fun Suite.itPrintsFileAccessProblemDetails(forceNoLinks: Skip = No, block: (Path) -> Unit) { + it("prints the closest existing parent if it is a directory", forceNoLink = forceNoLinks) withAndWithoutSymlink { maybeLink -> val start = tempFolder.newDirectory("startDir").toRealPath() val doesNotExist = maybeLink.create(start.resolve("i").resolve("dont").resolve("exist")) val existingParentHintMessage = @@ -180,7 +190,7 @@ abstract class PathExpectationsSpec( } } - it("explains if a parent is a file") withAndWithoutSymlink { maybeLink -> + it("explains if a parent is a file", forceNoLink = forceNoLinks) withAndWithoutSymlink { maybeLink -> val start = tempFolder.newFile("startFile") val doesNotExist = maybeLink.create(start.resolve("i").resolve("dont").resolve("exist")) val parentErrorMessage = String.format(FAILURE_DUE_TO_PARENT.getDefault(), start) @@ -198,7 +208,10 @@ abstract class PathExpectationsSpec( } } - it("prints an explanation for link loops", skip = ifSymlinksNotSupported) { + it( + "prints an explanation for link loops", + skip = if (forceNoLinks == No) ifSymlinksNotSupported else forceNoLinks + ) { val testDir = tempFolder.newDirectory("loop").toRealPath() val a = testDir.resolve("a") val b = a.createSymbolicLink(testDir.resolve("b")) @@ -211,7 +224,7 @@ abstract class PathExpectationsSpec( } } - itPrintsParentAccessDeniedDetails(block) + itPrintsParentAccessDeniedDetails(forceNoLinks, block) itPrintsFileAccessExceptionDetails(block) } @@ -810,6 +823,59 @@ abstract class PathExpectationsSpec( } } + describeFun(isSymbolicLink) { + val isSymbolicLinkFun = isSymbolicLink.lambda + val expectedMessage = "$isDescr: ${A_SYMBOLIC_LINK.getDefault()}" + + context("not accessible") { + it("throws an AssertionError for a non-existent path") { + val path = tempFolder.resolve("nonExistent") + expect { + expect(path).toBeASymbolicLink() + }.toThrow().message { + contains( + expectedMessage, + FAILURE_DUE_TO_NO_SUCH_FILE.getDefault() + ) + } + } + + itPrintsFileAccessProblemDetails(Yes("link resolution will not be triggered")) { testFile -> + expect(testFile).isSymbolicLinkFun() + } + } + + it("throws an AssertionError for a file") { + val file = tempFolder.newFile("test") + expect { + expect(file).isSymbolicLinkFun() + }.toThrow().message { + contains( + expectedMessage, + "${WAS.getDefault()}: ${A_FILE.getDefault()}" + ) + } + } + + it("throws an AssertionError for a directory") { + val folder = tempFolder.newDirectory("test") + expect { + expect(folder).isSymbolicLinkFun() + }.toThrow().message { + contains( + expectedMessage, + "${WAS.getDefault()}: ${A_DIRECTORY.getDefault()}" + ) + } + } + + it("does not throw for a symbolic link") { + val target = tempFolder.resolve("target") + val link = tempFolder.newSymbolicLink("link", target) + expect(link).isSymbolicLinkFun() + } + } + describeFun(isDirectory) { val isDirectoryFun = isDirectory.lambda val expectedMessage = "$isDescr: ${A_DIRECTORY.getDefault()}"