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 85f9fa5e37..28cc5942d9 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 @@ -59,7 +59,7 @@ fun Expect.endsNotWith(expected: Path): Expect = * Expects that the subject of the assertion (a [Path]) exists; * meaning that there is a file system entry at the location the [Path] points to. * - * This matcher _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, + * This assertion _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, * then the search will continue at that location. * * @return An [Expect] for the current subject of the assertion. @@ -74,7 +74,7 @@ fun Expect.exists(): Expect = * Expects that the subject of the assertion (a [Path]) does not exist; * meaning that there is no file system entry at the location the [Path] points to. * - * This matcher _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, + * This assertion _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, * then the search will continue at that location. * * @return An [Expect] for the current subject of the assertion. @@ -191,7 +191,7 @@ fun Expect.resolve(other: String, assertionCreator: Expect.( * meaning that there is a file system entry at the location the [Path] points to and * that the current thread has the permission to read from it. * - * This matcher _resolves_ symbolic links. + * 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. * @@ -212,7 +212,7 @@ fun Expect.isReadable(): Expect = * meaning that there is a file system entry at the location the [Path] points to and * that the current thread has the permission to write to it. * - * This matcher _resolves_ symbolic links. + * 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. * @@ -224,11 +224,33 @@ fun Expect.isReadable(): Expect = fun Expect.isWritable(): Expect = _logicAppend { isWritable() } + +/** + * Expects that the subject of the assertion (a [Path]) is executable; + * meaning that there is a file system entry at the location the [Path] points to and + * that the current thread has the permission to execute it. + * + * The semantics of “permission to execute it” may differ when checking access to a directory. For example, on UNIX + * systems, it means that the Java virtual machine has permission to search the directory in order to access file or + * subdirectories. + * + * 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. + * + * @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.isExecutable(): Expect = + _logicAppend { isExecutable() } + /** * Expects that the subject of the assertion (a [Path]) is a file; * meaning that there is a file system entry at the location the [Path] points to and that is a regular file. * - * This matcher _resolves_ symbolic links. + * 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. * @@ -248,7 +270,7 @@ fun Expect.isRegularFile(): Expect = * Expects that the subject of the assertion (a [Path]) is a directory; * meaning that there is a file system entry at the location the [Path] points to and that is a directory. * - * This matcher _resolves_ symbolic links. + * 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. * 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 5741ede9e2..845c641257 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 @@ -17,6 +17,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe fun1(Expect::endsNotWith), fun0(Expect::isReadable), fun0(Expect::isWritable), + fun0(Expect::isExecutable), fun0(Expect::isRegularFile), fun0(Expect::isDirectory), fun1(Expect::hasSameBinaryContentAs), 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 f29c5c3e2b..beac0bad05 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 @@ -115,3 +115,9 @@ object success : Keyword * It can be used for a parameterless function so that it has one parameter and thus can be used as infix function. */ object writable : Keyword + +/** + * A helper construct to allow expressing assertions about a path being executable. + * It can be used for a parameterless function so that it has one parameter and thus can be used as infix function. + */ +object executable : Keyword 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 1b7487fac7..87f9608c62 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 @@ -5,8 +5,8 @@ package ch.tutteli.atrium.api.infix.en_GB -import ch.tutteli.atrium.api.infix.en_GB.creating.path.PathWithEncoding import ch.tutteli.atrium.api.infix.en_GB.creating.path.PathWithCreator +import ch.tutteli.atrium.api.infix.en_GB.creating.path.PathWithEncoding import ch.tutteli.atrium.creating.Expect import ch.tutteli.atrium.logic.* import java.nio.charset.Charset @@ -61,7 +61,7 @@ infix fun Expect.endsNotWith(expected: Path): Expect = * Expects that the subject of the assertion (a [Path]) exists; * meaning that there is a file system entry at the location the [Path] points to. * - * This matcher _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, + * This assertion _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, * then the search will continue at that location. * * @return An [Expect] for the current subject of the assertion. @@ -76,7 +76,7 @@ infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") existing: exis * Expects that the subject of the assertion (a [Path]) does not exist; * meaning that there is no file system entry at the location the [Path] points to. * - * This matcher _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, + * This assertion _resolves_ symbolic links. Therefore, if a symbolic link exists at the location the subject points to, * then the search will continue at that location. * * @return An [Expect] for the current subject of the assertion. @@ -202,7 +202,7 @@ fun path(path: String, assertionCreator: Expect.() -> Unit): PathWithCrea * meaning that there is a file system entry at the location the [Path] points to and * that the current thread has the permission to read from it. * - * This matcher _resolves_ symbolic links. + * 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. * @@ -223,7 +223,7 @@ infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") readable: read * meaning that there is a file system entry at the location the [Path] points to and * that the current thread has the permission to write to it. * - * This matcher _resolves_ symbolic links. + * 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. * @@ -235,11 +235,32 @@ infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") readable: read infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") writable: writable): Expect = _logicAppend { isWritable() } +/** + * Expects that the subject of the assertion (a [Path]) is executable; + * meaning that there is a file system entry at the location the [Path] points to and + * that the current thread has the permission to execute it. + * + * The semantics of “permission to execute it” may differ when checking access to a directory. For example, on UNIX + * systems, it means that the Java virtual machine has permission to search the directory in order to access file or + * subdirectories. + * + * 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. + * + * @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") executable: executable): Expect = + _logicAppend { isExecutable() } + /** * Expects that the subject of the assertion (a [Path]) is a file; * meaning that there is a file system entry at the location the [Path] points to and that is a regular file. * - * This matcher _resolves_ symbolic links. + * 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. * @@ -259,7 +280,7 @@ infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") aRegularFile: * Expects that the subject of the assertion (a [Path]) is a directory; * meaning that there is a file system entry at the location the [Path] points to and that is a directory. * - * This matcher _resolves_ symbolic links. + * 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. * 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 f36a4074bf..fdb1f810e7 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 @@ -18,6 +18,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe fun1(Expect::endsNotWith), "toBe ${readable::class.simpleName}" to Companion::isReadable, "toBe ${writable::class.simpleName}" to Companion::isWritable, + "toBe ${executable::class.simpleName}" to Companion::isExecutable, "toBe ${aRegularFile::class.simpleName}" to Companion::isRegularFile, "toBe ${aDirectory::class.simpleName}" to Companion::isDirectory, fun1(Expect::hasSameBinaryContentAs), @@ -30,6 +31,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe private fun existsNot(expect: Expect) = expect notToBe existing private fun isReadable(expect: Expect) = expect toBe readable private fun isWritable(expect: Expect) = expect toBe writable + private fun isExecutable(expect: Expect) = expect toBe executable private fun isRegularFile(expect: Expect) = expect toBe aRegularFile private fun isDirectory(expect: Expect) = expect toBe aDirectory 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 180e9fac1a..96489ef683 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 @@ -16,7 +16,6 @@ import ch.tutteli.atrium.creating.AssertionContainer import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep import java.nio.charset.Charset import java.nio.file.Path -import java.util.* fun AssertionContainer.startsWith(expected: Path): Assertion = _pathImpl.startsWith(this, expected) fun AssertionContainer.startsNotWith(expected: Path): Assertion = _pathImpl.startsNotWith(this, expected) @@ -28,6 +27,7 @@ fun AssertionContainer.existsNot(): Assertion = _pathImpl.existsNo fun AssertionContainer.isReadable(): Assertion = _pathImpl.isReadable(this) fun AssertionContainer.isWritable(): Assertion = _pathImpl.isWritable(this) +fun AssertionContainer.isExecutable(): Assertion = _pathImpl.isExecutable(this) fun AssertionContainer.isRegularFile(): Assertion = _pathImpl.isRegularFile(this) fun AssertionContainer.isDirectory(): Assertion = _pathImpl.isDirectory(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 c473e7ccaa..4637b7679d 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 @@ -10,7 +10,6 @@ import ch.tutteli.atrium.creating.AssertionContainer import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep import java.nio.charset.Charset import java.nio.file.Path -import java.util.* /** * Collection of assertion functions and builders which are applicable to subjects with a [Path] type. @@ -26,6 +25,7 @@ interface PathAssertions { fun isReadable(container: AssertionContainer): Assertion fun isWritable(container: AssertionContainer): Assertion + fun isExecutable(container: AssertionContainer): Assertion fun isRegularFile(container: AssertionContainer): Assertion fun isDirectory(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 582be6be80..640ccdc934 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 @@ -100,6 +100,9 @@ class DefaultPathAssertions : PathAssertions { override fun isWritable(container: AssertionContainer): Assertion = filePermissionAssertion(container, WRITABLE, AccessMode.WRITE) + override fun isExecutable(container: AssertionContainer): Assertion = + filePermissionAssertion(container, EXECUTABLE, AccessMode.EXECUTE) + override fun isRegularFile(container: AssertionContainer): Assertion = fileTypeAssertion(container, A_FILE) { it.isRegularFile } 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 b1f0c545d1..0d19b3915e 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 @@ -3,6 +3,7 @@ package ch.tutteli.atrium.api.fluent.en_GB.jdk8 +import ch.tutteli.atrium.api.fluent.en_GB.isExecutable import ch.tutteli.atrium.creating.Expect import ch.tutteli.atrium.specs.fun0 import ch.tutteli.atrium.specs.fun1 @@ -20,6 +21,7 @@ class PathAssertionsSpec : ch.tutteli.atrium.specs.integration.PathAssertionsSpe fun1(Expect::endsNotWith), fun0(Expect::isReadable), fun0(Expect::isWritable), + fun0(Expect::isExecutable), // checks the new function from fluent-jvm because it is not implemented in fluent-jkd8 fun0(Expect::isRegularFile), fun0(Expect::isDirectory), fun1(Expect::hasSameBinaryContentAs), 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 10b2fa309b..59528edb41 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 @@ -3,15 +3,12 @@ package ch.tutteli.atrium.specs.integration import ch.tutteli.atrium.api.fluent.en_GB.* import ch.tutteli.atrium.api.verbs.internal.expect import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.reporting.translating.Translatable import ch.tutteli.atrium.reporting.translating.TranslatableWithArgs import ch.tutteli.atrium.specs.* import ch.tutteli.atrium.translations.DescriptionBasic.* import ch.tutteli.atrium.translations.DescriptionPathAssertion.* -import ch.tutteli.niok.createSymbolicLink -import ch.tutteli.niok.getFileAttributeView -import ch.tutteli.niok.posixFilePersmissions -import ch.tutteli.niok.readAttributes -import ch.tutteli.niok.setPosixFilePermissions +import ch.tutteli.niok.* import ch.tutteli.spek.extensions.MemoizedTempFolder import ch.tutteli.spek.extensions.memoizedTempFolder import io.mockk.every @@ -27,23 +24,11 @@ import java.nio.charset.Charset import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.nio.file.attribute.AclEntry -import java.nio.file.attribute.AclEntryPermission -import java.nio.file.attribute.AclEntryPermission.READ_DATA -import java.nio.file.attribute.AclEntryPermission.WRITE_DATA -import java.nio.file.attribute.AclEntryType +import java.nio.file.attribute.* +import java.nio.file.attribute.AclEntryPermission.* import java.nio.file.attribute.AclEntryType.ALLOW import java.nio.file.attribute.AclEntryType.DENY -import java.nio.file.attribute.AclFileAttributeView -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.attribute.PosixFileAttributes -import java.nio.file.attribute.PosixFilePermission -import java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE -import java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE -import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE -import java.nio.file.attribute.PosixFilePermission.OWNER_READ -import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE -import java.nio.file.attribute.UserPrincipal +import java.nio.file.attribute.PosixFilePermission.* import java.util.regex.Pattern.quote abstract class PathAssertionsSpec( @@ -55,6 +40,7 @@ abstract class PathAssertionsSpec( endsNotWith: Fun1, isReadable: Fun0, isWritable: Fun0, + isExecutable: Fun0, isRegularFile: Fun0, isDirectory: Fun0, hasSameBinaryContentAs: Fun1, @@ -73,10 +59,11 @@ abstract class PathAssertionsSpec( endsNotWith.forSubjectLess(Paths.get("a")), isReadable.forSubjectLess(), isWritable.forSubjectLess(), + isExecutable.forSubjectLess(), isRegularFile.forSubjectLess(), isDirectory.forSubjectLess(), hasSameBinaryContentAs.forSubjectLess(Paths.get("a")), - hasSameTextualContentAs.forSubjectLess(Paths.get("a"), Charsets.ISO_8859_1, Charsets.ISO_8859_1) , + hasSameTextualContentAs.forSubjectLess(Paths.get("a"), Charsets.ISO_8859_1, Charsets.ISO_8859_1), hasSameTextualContentAsDefaultArgs.forSubjectLess(Paths.get("a")) ) {}) @@ -423,12 +410,6 @@ abstract class PathAssertionsSpec( val expectedPermissionHint = String.format(HINT_ACTUAL_POSIX_PERMISSIONS.getDefault(), "u=wx g=x o=") it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> - val expectedTypeHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_FILE.getDefault(), - READABLE.getDefault() - ) - val file = maybeLink.create(tempFolder.newFile("not-readable")) file.whileWithPermissions(OWNER_WRITE, OWNER_EXECUTE, GROUP_EXECUTE) { expect { @@ -436,7 +417,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedTypeHint, + expectedPermissionTypeHintFor(A_FILE, being = READABLE), expectedPermissionHint, expectedPosixOwnerAndGroupHintFor(file) ) @@ -446,12 +427,6 @@ abstract class PathAssertionsSpec( } it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> - val expectedTypeHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_DIRECTORY.getDefault(), - READABLE.getDefault() - ) - val folder = maybeLink.create(tempFolder.newDirectory("not-readable")) folder.whileWithPermissions(OWNER_WRITE, OWNER_EXECUTE, GROUP_EXECUTE) { expect { @@ -459,7 +434,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedTypeHint, + expectedPermissionTypeHintFor(A_DIRECTORY, being = READABLE), expectedPermissionHint, expectedPosixOwnerAndGroupHintFor(folder) ) @@ -471,12 +446,6 @@ abstract class PathAssertionsSpec( context("ACL: not readable", skip = ifAclNotSupported) { it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> - val expectedTypeHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_FILE.getDefault(), - READABLE.getDefault() - ) - val file = maybeLink.create(tempFolder.newFile("not-readable")) file.whileWithAcl(TestAcls::ownerNoRead) { expect { @@ -484,7 +453,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedTypeHint, + expectedPermissionTypeHintFor(A_FILE, being = READABLE), expectedAclOwnerHintFor(file), HINT_ACTUAL_ACL_PERMISSIONS.getDefault() ) @@ -500,12 +469,6 @@ abstract class PathAssertionsSpec( } it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> - val expectedTypeHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_DIRECTORY.getDefault(), - READABLE.getDefault() - ) - val folder = maybeLink.create(tempFolder.newDirectory("not-readable")) folder.whileWithAcl(TestAcls::ownerNoRead) { expect { @@ -513,7 +476,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedTypeHint, + expectedPermissionTypeHintFor(A_DIRECTORY, being = READABLE), expectedAclOwnerHintFor(folder), HINT_ACTUAL_ACL_PERMISSIONS.getDefault() ) @@ -569,12 +532,6 @@ abstract class PathAssertionsSpec( val expectedPermissionHint = String.format(HINT_ACTUAL_POSIX_PERMISSIONS.getDefault(), "u=rx g= o=x") it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> - val expectedTypeHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_FILE.getDefault(), - WRITABLE.getDefault() - ) - val file = maybeLink.create(tempFolder.newFile("not-writable")) file.whileWithPermissions(OWNER_READ, OWNER_EXECUTE, OTHERS_EXECUTE) { expect { @@ -582,7 +539,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedTypeHint, + expectedPermissionTypeHintFor(A_FILE, being = WRITABLE), expectedPermissionHint, expectedPosixOwnerAndGroupHintFor(file) ) @@ -592,12 +549,6 @@ abstract class PathAssertionsSpec( } it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> - val expectedHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_DIRECTORY.getDefault(), - WRITABLE.getDefault() - ) - val folder = maybeLink.create(tempFolder.newDirectory("not-writable")) folder.whileWithPermissions(OWNER_READ, OWNER_EXECUTE, OTHERS_EXECUTE) { expect { @@ -605,7 +556,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedHint, + expectedPermissionTypeHintFor(A_DIRECTORY, being = WRITABLE), expectedPermissionHint, expectedPosixOwnerAndGroupHintFor(folder) ) @@ -617,12 +568,6 @@ abstract class PathAssertionsSpec( context("ACL: not writable", skip = ifAclNotSupported) { it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> - val expectedTypeHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_FILE.getDefault(), - WRITABLE.getDefault() - ) - val file = maybeLink.create(tempFolder.newFile("not-writable")) file.whileWithAcl(TestAcls::ownerNoWrite) { expect { @@ -630,7 +575,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedTypeHint, + expectedPermissionTypeHintFor(A_FILE, being = WRITABLE), expectedAclOwnerHintFor(file), HINT_ACTUAL_ACL_PERMISSIONS.getDefault() ) @@ -644,12 +589,6 @@ abstract class PathAssertionsSpec( } it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> - val expectedHint = String.format( - FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), - A_DIRECTORY.getDefault(), - WRITABLE.getDefault() - ) - val folder = maybeLink.create(tempFolder.newDirectory("not-writable")) folder.whileWithAcl(TestAcls::ownerNoWrite) { expect { @@ -657,7 +596,7 @@ abstract class PathAssertionsSpec( }.toThrow().message { contains( expectedMessage, - expectedHint, + expectedPermissionTypeHintFor(A_DIRECTORY, being = WRITABLE), expectedAclOwnerHintFor(folder), HINT_ACTUAL_ACL_PERMISSIONS.getDefault() ) @@ -672,6 +611,145 @@ abstract class PathAssertionsSpec( } } + describeFun(isExecutable) { + val isExecutableFun = isExecutable.lambda + val expectedMessage = "$isDescr: ${EXECUTABLE.getDefault()}" + + context("not accessible") { + it("throws an AssertionError for non-existent path") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.tmpDir.resolve("nonExistent")) + expect { + expect(file).isExecutableFun() + }.toThrow().message { + contains(expectedMessage, FAILURE_DUE_TO_NO_SUCH_FILE.getDefault()) + containsExplanationFor(maybeLink) + } + } + + itPrintsFileAccessProblemDetails { testFile -> + expect(testFile).isExecutableFun() + } + } + + context("POSIX: executable", skip = ifPosixNotSupported) { + it("does not throw for file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("executable")) + file.whileWithPermissions(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE) { + expect(file).isExecutableFun() + } + } + + it("does not throw for directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("executable")) + folder.whileWithPermissions(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE) { + expect(folder).isExecutableFun() + } + } + } + + context("ACL: executable", skip = ifAclNotSupported) { + it("does not throw for file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("executable")) + file.whileWithAcl(TestAcls::all) { + expect(file).isExecutableFun() + } + } + + it("does not throw for directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("executable")) + folder.whileWithAcl(TestAcls::all) { + expect(folder).isExecutableFun() + } + } + } + + context("POSIX: not executable", skip = ifPosixNotSupported) { + val expectedPermissionHint = String.format(HINT_ACTUAL_POSIX_PERMISSIONS.getDefault(), "u=rw g=r o=") + + it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("not-executable")) + file.whileWithPermissions(OWNER_WRITE, OWNER_READ, GROUP_READ) { + expect { + expect(file).isExecutableFun() + }.toThrow().message { + contains( + expectedMessage, + expectedPermissionTypeHintFor(A_FILE, being = EXECUTABLE), + expectedPermissionHint, + expectedPosixOwnerAndGroupHintFor(file) + ) + containsExplanationFor(maybeLink) + } + } + } + + it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("not-executable")) + folder.whileWithPermissions(OWNER_WRITE, OWNER_READ, GROUP_READ) { + expect { + expect(folder).isExecutableFun() + }.toThrow().message { + contains( + expectedMessage, + expectedPermissionTypeHintFor(A_DIRECTORY, being = EXECUTABLE), + expectedPermissionHint, + expectedPosixOwnerAndGroupHintFor(folder) + ) + containsExplanationFor(maybeLink) + } + } + } + } + + context("ACL: not executable", skip = ifAclNotSupported) { + it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("not-executable")) + file.whileWithAcl(TestAcls::ownerNoExecute) { + expect { + expect(file).isExecutableFun() + }.toThrow().message { + contains( + expectedMessage, + expectedPermissionTypeHintFor(A_FILE, being = EXECUTABLE), + expectedAclOwnerHintFor(file), + HINT_ACTUAL_ACL_PERMISSIONS.getDefault() + ) + containsRegex( + file.expectedAclEntryPartFor("DENY", "EXECUTE"), + // we only check a few of the allowed options + file.expectedAclEntryPartFor("ALLOW", "WRITE_ACL"), + file.expectedAclEntryPartFor("ALLOW", "WRITE_DATA") + ) + containsExplanationFor(maybeLink) + } + } + } + + it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("not-readable")) + folder.whileWithAcl(TestAcls::ownerNoExecute) { + expect { + expect(folder).isExecutableFun() + }.toThrow().message { + contains( + expectedMessage, + expectedPermissionTypeHintFor(A_DIRECTORY, being = EXECUTABLE), + expectedAclOwnerHintFor(folder), + HINT_ACTUAL_ACL_PERMISSIONS.getDefault() + ) + containsRegex( + folder.expectedAclEntryPartFor("DENY", "EXECUTE"), + // we only check a few of the allowed options + folder.expectedAclEntryPartFor("ALLOW", "WRITE_ACL"), + folder.expectedAclEntryPartFor("ALLOW", "WRITE_DATA") + ) + containsExplanationFor(maybeLink) + } + } + } + } + } + describeFun(isRegularFile) { val isRegularFileFun = isRegularFile.lambda val expectedMessage = "$isDescr: ${A_FILE.getDefault()}" @@ -956,6 +1034,15 @@ internal object TestAcls { aclEntry(DENY, owner, WRITE_DATA), aclEntry(ALLOW, owner, *AclEntryPermission.values()) ) + + fun all(owner: UserPrincipal) = listOf( + aclEntry(ALLOW, owner, *AclEntryPermission.values()) + ) + + fun ownerNoExecute(owner: UserPrincipal) = listOf( + aclEntry(DENY, owner, EXECUTE), + aclEntry(ALLOW, owner, *AclEntryPermission.values()) + ) } private fun aclEntry(type: AclEntryType, principal: UserPrincipal, vararg permissions: AclEntryPermission) = @@ -1106,3 +1193,9 @@ internal class SimpleLink(private val tempFolderProvider: () -> MemoizedTempFold internal fun Expect.containsExplanationFor(maybeLink: MaybeLink) = maybeLink.checkAssertionErrorMessage(this) + +private fun expectedPermissionTypeHintFor(type: Translatable, being: Translatable) = String.format( + FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT.getDefault(), + type.getDefault(), + being.getDefault() +) 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 81d1c0292d..fbb5af7f34 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 @@ -25,6 +25,7 @@ enum class DescriptionPathAssertion(override val value: String) : StringBasedTra STARTS_NOT_WITH("beginnt nicht mit"), READABLE("lesbar"), WRITABLE("schreibbar"), + EXECUTABLE("ausführbar"), A_FILE("eine Datei"), A_DIRECTORY("ein Verzeichnis"), A_SYMBOLIC_LINK("eine symbolische Verknüpfung"), 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 34e318c560..3d8d56fdde 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 @@ -20,6 +20,7 @@ enum class DescriptionPathAssertion(override val value: String) : StringBasedTra STARTS_NOT_WITH("does not start with"), READABLE("readable"), WRITABLE("writable"), + EXECUTABLE("executable"), A_FILE("a file"), A_DIRECTORY("a directory"), A_SYMBOLIC_LINK("a symbolic link"),