mirror of
https://github.com/jlengrand/atrium.git
synced 2026-03-10 08:01:19 +00:00
add convenience shortcut for Path.toBeASymbolicLink feature
For both infix and fluent API styles
This commit is contained in:
@@ -269,6 +269,21 @@ fun <T : Path> Expect<T>.isRegularFile(): Expect<T> =
|
||||
fun <T : Path> Expect<T>.isDirectory(): Expect<T> =
|
||||
_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 <T : Path> Expect<T>.toBeASymbolicLink(): Expect<T> =
|
||||
_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.
|
||||
|
||||
@@ -17,6 +17,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation
|
||||
fun0(Expect<Path>::isExecutable),
|
||||
fun0(Expect<Path>::isRegularFile),
|
||||
fun0(Expect<Path>::isDirectory),
|
||||
fun0(Expect<Path>::toBeASymbolicLink),
|
||||
fun0(Expect<Path>::isAbsolute),
|
||||
fun0(Expect<Path>::isRelative),
|
||||
Expect<Path>::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"))
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -332,6 +332,21 @@ infix fun <T : Path> Expect<T>.toBe(@Suppress("UNUSED_PARAMETER") aRegularFile:
|
||||
infix fun <T : Path> Expect<T>.toBe(@Suppress("UNUSED_PARAMETER") aDirectory: aDirectory): Expect<T> =
|
||||
_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 <T : Path> Expect<T>.toBe(@Suppress("UNUSED_PARAMETER") aSymbolicLink: aSymbolicLink): Expect<T> =
|
||||
_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.
|
||||
|
||||
@@ -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<Path>::hasDirectoryEntry),
|
||||
@@ -46,6 +47,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation
|
||||
private fun isExecutable(expect: Expect<Path>) = expect toBe executable
|
||||
private fun isRegularFile(expect: Expect<Path>) = expect toBe aRegularFile
|
||||
private fun isDirectory(expect: Expect<Path>) = expect toBe aDirectory
|
||||
private fun toBeASymbolicLink(expect: Expect<Path>) = expect toBe aSymbolicLink
|
||||
private fun isAbsolute(expect: Expect<Path>) = expect toBe absolute
|
||||
private fun isRelative(expect: Expect<Path>) = expect toBe relative
|
||||
private fun hasDirectoryEntryMultiple(expect: Expect<Path>, entry: String, vararg otherEntries: String) =
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ fun <T : Path> AssertionContainer<T>.isWritable(): Assertion = impl.isWritable(t
|
||||
fun <T : Path> AssertionContainer<T>.isExecutable(): Assertion = impl.isExecutable(this)
|
||||
fun <T : Path> AssertionContainer<T>.isRegularFile(): Assertion = impl.isRegularFile(this)
|
||||
fun <T : Path> AssertionContainer<T>.isDirectory(): Assertion = impl.isDirectory(this)
|
||||
fun <T : Path> AssertionContainer<T>.toBeASymbolicLink(): Assertion = impl.toBeASymbolicLink(this)
|
||||
fun <T : Path> AssertionContainer<T>.isAbsolute(): Assertion = impl.isAbsolute(this)
|
||||
fun <T : Path> AssertionContainer<T>.isRelative(): Assertion = impl.isRelative(this)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ interface PathAssertions {
|
||||
fun <T : Path> isExecutable(container: AssertionContainer<T>): Assertion
|
||||
fun <T : Path> isRegularFile(container: AssertionContainer<T>): Assertion
|
||||
fun <T : Path> isDirectory(container: AssertionContainer<T>): Assertion
|
||||
fun <T : Path> toBeASymbolicLink(container: AssertionContainer<T>): Assertion
|
||||
fun <T : Path> isAbsolute(container: AssertionContainer<T>): Assertion
|
||||
fun <T : Path> isRelative(container: AssertionContainer<T>): Assertion
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ class DefaultPathAssertions : PathAssertions {
|
||||
override fun <T : Path> isDirectory(container: AssertionContainer<T>): Assertion =
|
||||
fileTypeAssertion(container, A_DIRECTORY) { it.isDirectory }
|
||||
|
||||
override fun <T : Path> toBeASymbolicLink(container: AssertionContainer<T>): Assertion =
|
||||
fileTypeAssertion(container, A_SYMBOLIC_LINK, NOFOLLOW_LINKS) { it.isSymbolicLink }
|
||||
|
||||
override fun <T : Path> isAbsolute(container: AssertionContainer<T>): Assertion =
|
||||
container.createDescriptiveAssertion(DescriptionBasic.IS, ABSOLUTE_PATH) { it.isAbsolute }
|
||||
|
||||
@@ -147,8 +150,9 @@ class DefaultPathAssertions : PathAssertions {
|
||||
private inline fun <T : Path> fileTypeAssertion(
|
||||
container: AssertionContainer<T>,
|
||||
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)
|
||||
|
||||
@@ -39,6 +39,7 @@ abstract class PathExpectationsSpec(
|
||||
isExecutable: Fun0<Path>,
|
||||
isRegularFile: Fun0<Path>,
|
||||
isDirectory: Fun0<Path>,
|
||||
isSymbolicLink: Fun0<Path>,
|
||||
isAbsolute: Fun0<Path>,
|
||||
isRelative: Fun0<Path>,
|
||||
hasDirectoryEntrySingle: Fun1<Path, String>,
|
||||
@@ -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<AssertionError>().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<AssertionError>().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<AssertionError>().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()}"
|
||||
|
||||
Reference in New Issue
Block a user