Merge pull request #527 from robstoll/refactor-domain

Refactor domain
This commit is contained in:
Robert Stoll
2020-07-03 08:16:48 +02:00
committed by GitHub
23 changed files with 1147 additions and 48 deletions

View File

@@ -138,7 +138,8 @@ fun <T : CharSequence> Expect<T>.containsRegex(pattern: Regex, vararg otherPatte
* @return An [Expect] for the current subject of the assertion.
* @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct.
*/
fun <T : CharSequence> Expect<T>.startsWith(expected: CharSequence): Expect<T> = _logicAppend { startsWith(expected) }
fun <T : CharSequence> Expect<T>.startsWith(expected: CharSequence): Expect<T> =
_logicAppend { startsWith(expected) }
/**
* Expects that the subject of the assertion (a [CharSequence]) starts with [expected].
@@ -148,7 +149,8 @@ fun <T : CharSequence> Expect<T>.startsWith(expected: CharSequence): Expect<T> =
*
* @since 0.9.0
*/
fun <T : CharSequence> Expect<T>.startsWith(expected: Char): Expect<T> = startsWith(expected.toString())
fun <T : CharSequence> Expect<T>.startsWith(expected: Char): Expect<T> =
startsWith(expected.toString())
/**
* Expects that the subject of the assertion (a [CharSequence]) does not start with [expected].
@@ -167,7 +169,8 @@ fun <T : CharSequence> Expect<T>.startsNotWith(expected: CharSequence): Expect<T
*
* @since 0.9.0
*/
fun <T : CharSequence> Expect<T>.startsNotWith(expected: Char): Expect<T> = startsNotWith(expected.toString())
fun <T : CharSequence> Expect<T>.startsNotWith(expected: Char): Expect<T> =
startsNotWith(expected.toString())
/**
@@ -176,7 +179,8 @@ fun <T : CharSequence> Expect<T>.startsNotWith(expected: Char): Expect<T> = star
* @return An [Expect] for the current subject of the assertion.
* @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct.
*/
fun <T : CharSequence> Expect<T>.endsWith(expected: CharSequence): Expect<T> = _logicAppend { endsWith(expected) }
fun <T : CharSequence> Expect<T>.endsWith(expected: CharSequence): Expect<T> =
_logicAppend { endsWith(expected) }
/**
* Expects that the subject of the assertion (a [CharSequence]) ends with [expected].
@@ -186,7 +190,8 @@ fun <T : CharSequence> Expect<T>.endsWith(expected: CharSequence): Expect<T> = _
*
* @since 0.9.0
*/
fun <T : CharSequence> Expect<T>.endsWith(expected: Char): Expect<T> = endsWith(expected.toString())
fun <T : CharSequence> Expect<T>.endsWith(expected: Char): Expect<T> =
endsWith(expected.toString())
/**
* Expects that the subject of the assertion (a [CharSequence]) does not end with [expected].
@@ -194,7 +199,8 @@ fun <T : CharSequence> Expect<T>.endsWith(expected: Char): Expect<T> = endsWith(
* @return An [Expect] for the current subject of the assertion.
* @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct.
*/
fun <T : CharSequence> Expect<T>.endsNotWith(expected: CharSequence): Expect<T> = _logicAppend { endsNotWith(expected) }
fun <T : CharSequence> Expect<T>.endsNotWith(expected: CharSequence): Expect<T> =
_logicAppend { endsNotWith(expected) }
/**
* Expects that the subject of the assertion (a [CharSequence]) does not end with [expected].
@@ -204,7 +210,8 @@ fun <T : CharSequence> Expect<T>.endsNotWith(expected: CharSequence): Expect<T>
*
* @since 0.9.0
*/
fun <T : CharSequence> Expect<T>.endsNotWith(expected: Char): Expect<T> = endsNotWith(expected.toString())
fun <T : CharSequence> Expect<T>.endsNotWith(expected: Char): Expect<T> =
endsNotWith(expected.toString())
/**
@@ -213,7 +220,8 @@ fun <T : CharSequence> Expect<T>.endsNotWith(expected: Char): Expect<T> = endsNo
* @return An [Expect] for the current subject of the assertion.
* @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct.
*/
fun <T : CharSequence> Expect<T>.isEmpty(): Expect<T> = _logicAppend { isEmpty() }
fun <T : CharSequence> Expect<T>.isEmpty(): Expect<T> =
_logicAppend { isEmpty() }
/**
* Expects that the subject of the assertion (a [CharSequence]) [CharSequence].[kotlin.text.isNotEmpty].
@@ -221,7 +229,8 @@ fun <T : CharSequence> Expect<T>.isEmpty(): Expect<T> = _logicAppend { isEmpty()
* @return An [Expect] for the current subject of the assertion.
* @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct.
*/
fun <T : CharSequence> Expect<T>.isNotEmpty(): Expect<T> = _logicAppend { isNotEmpty() }
fun <T : CharSequence> Expect<T>.isNotEmpty(): Expect<T> =
_logicAppend { isNotEmpty() }
/**
* Expects that the subject of the assertion (a [CharSequence]) [CharSequence].[kotlin.text.isNotBlank].
@@ -229,7 +238,8 @@ fun <T : CharSequence> Expect<T>.isNotEmpty(): Expect<T> = _logicAppend { isNotE
* @return An [Expect] for the current subject of the assertion.
* @throws AssertionError Might throw an [AssertionError] if the assertion made is not correct.
*/
fun <T : CharSequence> Expect<T>.isNotBlank(): Expect<T> = _logicAppend { isNotBlank() }
fun <T : CharSequence> Expect<T>.isNotBlank(): Expect<T> =
_logicAppend { isNotBlank() }
/**
* Expects that the subject of the assertion (a [CharSequence]) matches the given [expected] [Regex].
@@ -241,7 +251,8 @@ fun <T : CharSequence> Expect<T>.isNotBlank(): Expect<T> = _logicAppend { isNotB
*
* @since 0.9.0
*/
fun <T : CharSequence> Expect<T>.matches(expected: Regex): Expect<T> = _logicAppend { matches(expected) }
fun <T : CharSequence> Expect<T>.matches(expected: Regex): Expect<T> =
_logicAppend { matches(expected) }
/**
* Expects that the subject of the assertion (a [CharSequence]) mismatches the given [expected] [Regex].
@@ -253,4 +264,5 @@ fun <T : CharSequence> Expect<T>.matches(expected: Regex): Expect<T> = _logicApp
*
* @since 0.9.0
*/
fun <T : CharSequence> Expect<T>.mismatches(expected: Regex): Expect<T> = _logicAppend { mismatches(expected) }
fun <T : CharSequence> Expect<T>.mismatches(expected: Regex): Expect<T> =
_logicAppend { mismatches(expected) }

View File

@@ -1,7 +1,6 @@
package ch.tutteli.atrium.api.fluent.en_GB
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.logic._logic
import ch.tutteli.atrium.logic.get

View File

@@ -1,7 +1,6 @@
package ch.tutteli.atrium.api.fluent.en_GB
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.logic._logic
import ch.tutteli.atrium.logic.changeSubject

View File

@@ -2,6 +2,8 @@ package ch.tutteli.atrium.api.fluent.en_GB
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.logic._logic
import ch.tutteli.atrium.logic.changeSubject
import ch.tutteli.atrium.specs.fun1
import ch.tutteli.atrium.specs.notImplemented
import ch.tutteli.atrium.specs.withNullableSuffix
@@ -79,7 +81,7 @@ class IterableAnyAssertionsSpec : Spek({
"asSequence().${Sequence<*>::asIterable.name}().${containsShortcutFun.name}" to Companion::containsInAnyOrderEntrySequence
private fun containsInAnyOrderEntrySequence(expect: Expect<Iterable<Double>>, a: Expect<Double>.() -> Unit) =
ExpectImpl.changeSubject(expect).unreported { it.asSequence() }.asIterable().contains(a)
expect._logic.changeSubject.unreported { it.asSequence() }.asIterable().contains(a)
fun getContainsNullableSequencePair() =
"asSequence().${Sequence<*>::asIterable.name}().${containsShortcutNullableFun.name}" to Companion::containsNullableEntrySequence

View File

@@ -3,7 +3,8 @@
package ch.tutteli.atrium.api.fluent.en_GB
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.logic._logic
import ch.tutteli.atrium.logic.changeSubject
import java.io.File
import java.nio.file.Path
@@ -18,7 +19,7 @@ import java.nio.file.Path
* @since 0.9.0
*/
fun <T : File> Expect<T>.asPath(): Expect<Path> =
ExpectImpl.changeSubject(this).unreported { it.toPath() }
_logic.changeSubject.unreported { it.toPath() }
/**
* Expects that the subject of the assertion holds all assertions the given [assertionCreator] creates for

View File

@@ -3,8 +3,10 @@
package ch.tutteli.atrium.api.fluent.en_GB
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.domain.builders.optional
import ch.tutteli.atrium.logic._logic
import ch.tutteli.atrium.logic._logicAppend
import ch.tutteli.atrium.logic.isEmpty
import ch.tutteli.atrium.logic.isPresent
import java.util.*
/**
@@ -18,7 +20,8 @@ import java.util.*
*
* @since 0.9.0
*/
fun <T : Optional<*>> Expect<T>.isEmpty(): Expect<T> = addAssertion(ExpectImpl.optional.isEmpty(this))
fun <T : Optional<*>> Expect<T>.isEmpty(): Expect<T> =
_logicAppend { isEmpty() }
/**
* Expects that the subject of the assertion (an [Optional]) is present
@@ -32,7 +35,8 @@ fun <T : Optional<*>> Expect<T>.isEmpty(): Expect<T> = addAssertion(ExpectImpl.o
*
* @since 0.9.0
*/
fun <E, T : Optional<E>> Expect<T>.isPresent(): Expect<E> = ExpectImpl.optional.isPresent(this).getExpectOfFeature()
fun <E, T : Optional<E>> Expect<T>.isPresent(): Expect<E> =
_logic.isPresent().getExpectOfFeature()
/**
* Expects that the subject of the assertion (an [Optional]) is present and
@@ -44,4 +48,4 @@ fun <E, T : Optional<E>> Expect<T>.isPresent(): Expect<E> = ExpectImpl.optional.
* @since 0.9.0
*/
fun <E, T : Optional<E>> Expect<T>.isPresent(assertionCreator: Expect<E>.() -> Unit): Expect<T> =
ExpectImpl.optional.isPresent(this).addToInitial(assertionCreator)
_logic.isPresent().addToInitial(assertionCreator)

View File

@@ -3,8 +3,7 @@
package ch.tutteli.atrium.api.fluent.en_GB
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.domain.builders.path
import ch.tutteli.atrium.logic.*
import java.nio.charset.Charset
import java.nio.file.Path
@@ -17,7 +16,7 @@ import java.nio.file.Path
* @since 0.9.0
*/
fun <T : Path> Expect<T>.startsWith(expected: Path): Expect<T> =
addAssertion(ExpectImpl.path.startsWith(this, expected))
_logicAppend { startsWith(expected) }
/**
* Expects that the subject of the assertion (a [Path]) does not start with the [expected] [Path].
@@ -28,7 +27,7 @@ fun <T : Path> Expect<T>.startsWith(expected: Path): Expect<T> =
* @since 0.9.0
*/
fun <T : Path> Expect<T>.startsNotWith(expected: Path): Expect<T> =
addAssertion(ExpectImpl.path.startsNotWith(this, expected))
_logicAppend { startsNotWith(expected) }
/**
* Expects that the subject of the assertion (a [Path]) ends with the expected [Path].
@@ -39,7 +38,7 @@ fun <T : Path> Expect<T>.startsNotWith(expected: Path): Expect<T> =
* @since 0.9.0
*/
fun <T : Path> Expect<T>.endsWith(expected: Path): Expect<T> =
addAssertion(ExpectImpl.path.endsWith(this, expected))
_logicAppend { endsWith(expected) }
/**
* Expects that the subject of the assertion (a [Path]) does not end with the expected [Path];
@@ -51,7 +50,7 @@ fun <T : Path> Expect<T>.endsWith(expected: Path): Expect<T> =
* @since 0.9.0
*/
fun <T : Path> Expect<T>.endsNotWith(expected: Path): Expect<T> =
addAssertion(ExpectImpl.path.endsNotWith(this, expected))
_logicAppend { endsNotWith(expected) }
/**
* Expects that the subject of the assertion (a [Path]) exists;
@@ -65,7 +64,8 @@ fun <T : Path> Expect<T>.endsNotWith(expected: Path): Expect<T> =
*
* @since 0.9.0
*/
fun <T : Path> Expect<T>.exists(): Expect<T> = addAssertion(ExpectImpl.path.exists(this))
fun <T : Path> Expect<T>.exists(): Expect<T> =
_logicAppend { exists() }
/**
* Expects that the subject of the assertion (a [Path]) does not exist;
@@ -79,7 +79,8 @@ fun <T : Path> Expect<T>.exists(): Expect<T> = addAssertion(ExpectImpl.path.exis
*
* @since 0.9.0
*/
fun <T : Path> Expect<T>.existsNot(): Expect<T> = addAssertion(ExpectImpl.path.existsNot(this))
fun <T : Path> Expect<T>.existsNot(): Expect<T> =
_logicAppend { existsNot() }
/**
* Creates an [Expect] for the property [Path.fileNameAsString][ch.tutteli.niok.fileNameAsString]
@@ -91,7 +92,7 @@ fun <T : Path> Expect<T>.existsNot(): Expect<T> = addAssertion(ExpectImpl.path.e
* @since 0.9.0
*/
val <T : Path> Expect<T>.fileName: Expect<String>
get() = ExpectImpl.path.fileName(this).getExpectOfFeature()
get() = _logic.fileName().getExpectOfFeature()
/**
* Expects that the property [Path.fileNameAsString][ch.tutteli.niok.fileNameAsString]
@@ -105,7 +106,7 @@ val <T : Path> Expect<T>.fileName: Expect<String>
* @since 0.9.0
*/
fun <T : Path> Expect<T>.fileName(assertionCreator: Expect<String>.() -> Unit): Expect<T> =
ExpectImpl.path.fileName(this).addToInitial(assertionCreator)
_logic.fileName().addToInitial(assertionCreator)
/**
* Creates an [Expect] for the property [Path.fileNameWithoutExtension][ch.tutteli.niok.fileNameWithoutExtension]
@@ -118,7 +119,7 @@ fun <T : Path> Expect<T>.fileName(assertionCreator: Expect<String>.() -> Unit):
* @since 0.9.0
*/
val <T : Path> Expect<T>.fileNameWithoutExtension: Expect<String>
get() = ExpectImpl.path.fileNameWithoutExtension(this).getExpectOfFeature()
get() = _logic.fileNameWithoutExtension().getExpectOfFeature()
/**
* Expects that the property [Path.fileNameWithoutExtension][ch.tutteli.niok.fileNameWithoutExtension]
@@ -132,7 +133,7 @@ val <T : Path> Expect<T>.fileNameWithoutExtension: Expect<String>
* @since 0.9.0
*/
fun <T : Path> Expect<T>.fileNameWithoutExtension(assertionCreator: Expect<String>.() -> Unit): Expect<T> =
ExpectImpl.path.fileNameWithoutExtension(this).addToInitial(assertionCreator)
_logic.fileNameWithoutExtension().addToInitial(assertionCreator)
/**
* Expects that this [Path] has a [parent][Path.getParent] and creates an [Expect] for it,
@@ -144,7 +145,7 @@ fun <T : Path> Expect<T>.fileNameWithoutExtension(assertionCreator: Expect<Strin
* @since 0.9.0
*/
val <T : Path> Expect<T>.parent: Expect<Path>
get() = ExpectImpl.path.parent(this).getExpectOfFeature()
get() = _logic.parent().getExpectOfFeature()
/**
* Expects that this [Path] has a [parent][Path.getParent], that the parent holds all assertions the
@@ -156,7 +157,7 @@ val <T : Path> Expect<T>.parent: Expect<Path>
* @since 0.9.0
*/
fun <T : Path> Expect<T>.parent(assertionCreator: Expect<Path>.() -> Unit): Expect<T> =
ExpectImpl.path.parent(this).addToInitial(assertionCreator)
_logic.parent().addToInitial(assertionCreator)
/**
* Expects that [other] resolves against this [Path] and creates an [Expect] for the resolved [Path]
@@ -168,7 +169,7 @@ fun <T : Path> Expect<T>.parent(assertionCreator: Expect<Path>.() -> Unit): Expe
* @since 0.10.0
*/
fun <T : Path> Expect<T>.resolve(other: String): Expect<Path> =
ExpectImpl.path.resolve(this, other).getExpectOfFeature()
_logic.resolve(other).getExpectOfFeature()
/**
* Expects that [other] resolves against this [Path], that the resolved [Path] holds all assertions the
@@ -180,7 +181,7 @@ fun <T : Path> Expect<T>.resolve(other: String): Expect<Path> =
* @since 0.10.0
*/
fun <T : Path> Expect<T>.resolve(other: String, assertionCreator: Expect<Path>.() -> Unit): Expect<T> =
ExpectImpl.path.resolve(this, other).addToInitial(assertionCreator)
_logic.resolve(other).addToInitial(assertionCreator)
/**
* Expects that the subject of the assertion (a [Path]) is readable;
@@ -200,7 +201,8 @@ fun <T : Path> Expect<T>.resolve(other: String, assertionCreator: Expect<Path>.(
*
* @since 0.9.0
*/
fun <T : Path> Expect<T>.isReadable(): Expect<T> = addAssertion(ExpectImpl.path.isReadable(this))
fun <T : Path> Expect<T>.isReadable(): Expect<T> =
_logicAppend { isReadable() }
/**
* Expects that the subject of the assertion (a [Path]) is writable;
@@ -216,7 +218,8 @@ fun <T : Path> Expect<T>.isReadable(): Expect<T> = addAssertion(ExpectImpl.path.
*
* @since 0.9.0
*/
fun <T : Path> Expect<T>.isWritable(): Expect<T> = addAssertion(ExpectImpl.path.isWritable(this))
fun <T : Path> Expect<T>.isWritable(): Expect<T> =
_logicAppend { isWritable() }
/**
* Expects that the subject of the assertion (a [Path]) is a file;
@@ -235,7 +238,8 @@ fun <T : Path> Expect<T>.isWritable(): Expect<T> = addAssertion(ExpectImpl.path.
*
* @since 0.9.0
*/
fun <T : Path> Expect<T>.isRegularFile(): Expect<T> = addAssertion(ExpectImpl.path.isRegularFile(this))
fun <T : Path> Expect<T>.isRegularFile(): Expect<T> =
_logicAppend { isRegularFile() }
/**
* Expects that the subject of the assertion (a [Path]) is a directory;
@@ -254,7 +258,8 @@ fun <T : Path> Expect<T>.isRegularFile(): Expect<T> = addAssertion(ExpectImpl.pa
*
* @since 0.9.0
*/
fun <T : Path> Expect<T>.isDirectory(): Expect<T> = addAssertion(ExpectImpl.path.isDirectory(this))
fun <T : Path> Expect<T>.isDirectory(): Expect<T> =
_logicAppend { isDirectory() }
/**
* Creates an [Expect] for the property [Path.extension][ch.tutteli.niok.extension]
@@ -266,7 +271,7 @@ fun <T : Path> Expect<T>.isDirectory(): Expect<T> = addAssertion(ExpectImpl.path
* @since 0.9.0
*/
val <T : Path> Expect<T>.extension: Expect<String>
get() = ExpectImpl.path.extension(this).getExpectOfFeature()
get() = _logic.extension().getExpectOfFeature()
/**
* Expects that the property [Path.extension][ch.tutteli.niok.extension]
@@ -280,7 +285,7 @@ val <T : Path> Expect<T>.extension: Expect<String>
* @since 0.9.0
*/
fun <T : Path> Expect<T>.extension(assertionCreator: Expect<String>.() -> Unit): Expect<T> =
ExpectImpl.path.extension(this).addToInitial(assertionCreator)
_logic.extension().addToInitial(assertionCreator)
/**
* Expects that the subject of the assertion (a [Path]) has the same textual content
@@ -294,8 +299,11 @@ fun <T : Path> Expect<T>.extension(assertionCreator: Expect<String>.() -> Unit):
*
* @since 0.12.0
*/
fun <T : Path> Expect<T>.hasSameTextualContentAs(targetPath: Path, sourceCharset: Charset = Charsets.UTF_8, targetCharset: Charset = Charsets.UTF_8): Expect<T> =
addAssertion(ExpectImpl.path.hasSameTextualContentAs(this, targetPath, sourceCharset, targetCharset))
fun <T : Path> Expect<T>.hasSameTextualContentAs(
targetPath: Path,
sourceCharset: Charset = Charsets.UTF_8,
targetCharset: Charset = Charsets.UTF_8
): Expect<T> = _logicAppend { hasSameTextualContentAs(targetPath, sourceCharset, targetCharset) }
/**
* Expects that the subject of the assertion (a [Path]) has the same binary content
@@ -307,4 +315,4 @@ fun <T : Path> Expect<T>.hasSameTextualContentAs(targetPath: Path, sourceCharset
* @since 0.12.0
*/
fun <T : Path> Expect<T>.hasSameBinaryContentAs(targetPath: Path): Expect<T> =
addAssertion(ExpectImpl.path.hasSameBinaryContentAs(this, targetPath))
_logicAppend { hasSameBinaryContentAs(targetPath) }

View File

@@ -3,8 +3,13 @@ description = 'The domain logic of Atrium for the JVM platform.'
dependencies {
api prefixedProject('domain-builders-jvm')
implementation niok()
// it is up to the consumer which atrium-translations module is used at runtime
compileOnly prefixedProject('translations-en_GB-jvm')
testImplementation prefixedProject('api-fluent-en_GB-jvm')
testImplementation prefixedProject('specs-jvm')
}
apply from: "$project.projectDir/../generateLogic.gradle"

View File

@@ -19,6 +19,8 @@ import ch.tutteli.atrium.logic.impl.DefaultChronoZonedDateTimeAssertions
import ch.tutteli.atrium.logic.impl.DefaultFloatingPointJvmAssertions
import ch.tutteli.atrium.logic.impl.DefaultLocalDateAssertions
import ch.tutteli.atrium.logic.impl.DefaultLocalDateTimeAssertions
import ch.tutteli.atrium.logic.impl.DefaultOptionalAssertions
import ch.tutteli.atrium.logic.impl.DefaultPathAssertions
import ch.tutteli.atrium.logic.impl.DefaultZonedDateTimeAssertions
@PublishedApi
@@ -49,6 +51,14 @@ internal inline val <T> AssertionContainer<T>._localDateImpl
internal inline val <T> AssertionContainer<T>._localDateTimeImpl
get() = getImpl(LocalDateTimeAssertions::class) { DefaultLocalDateTimeAssertions() }
@PublishedApi
internal inline val <T> AssertionContainer<T>._optionalImpl
get() = getImpl(OptionalAssertions::class) { DefaultOptionalAssertions() }
@PublishedApi
internal inline val <T> AssertionContainer<T>._pathImpl
get() = getImpl(PathAssertions::class) { DefaultPathAssertions() }
@PublishedApi
internal inline val <T> AssertionContainer<T>._zonedDateTimeImpl
get() = getImpl(ZonedDateTimeAssertions::class) { DefaultZonedDateTimeAssertions() }

View File

@@ -0,0 +1,15 @@
//---------------------------------------------------
// Generated content, modify:
// logic/generateLogic.gradle
// if necessary - enjoy the day 🙂
//---------------------------------------------------
package ch.tutteli.atrium.logic
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep
import java.util.*
fun <T : Optional<*>> AssertionContainer<T>.isEmpty(): Assertion = _optionalImpl.isEmpty(this)
fun <E, T : Optional<E>> AssertionContainer<T>.isPresent(): ExtractedFeaturePostStep<T, E> = _optionalImpl.isPresent(this)

View File

@@ -0,0 +1,37 @@
//---------------------------------------------------
// Generated content, modify:
// logic/generateLogic.gradle
// if necessary - enjoy the day 🙂
//---------------------------------------------------
package ch.tutteli.atrium.logic
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep
import java.nio.charset.Charset
import java.nio.file.Path
fun <T : Path> AssertionContainer<T>.startsWith(expected: Path): Assertion = _pathImpl.startsWith(this, expected)
fun <T : Path> AssertionContainer<T>.startsNotWith(expected: Path): Assertion = _pathImpl.startsNotWith(this, expected)
fun <T : Path> AssertionContainer<T>.endsWith(expected: Path): Assertion = _pathImpl.endsWith(this, expected)
fun <T : Path> AssertionContainer<T>.endsNotWith(expected: Path): Assertion = _pathImpl.endsNotWith(this, expected)
fun <T : Path> AssertionContainer<T>.exists(): Assertion = _pathImpl.exists(this)
fun <T : Path> AssertionContainer<T>.existsNot(): Assertion = _pathImpl.existsNot(this)
fun <T : Path> AssertionContainer<T>.isReadable(): Assertion = _pathImpl.isReadable(this)
fun <T : Path> AssertionContainer<T>.isWritable(): Assertion = _pathImpl.isWritable(this)
fun <T : Path> AssertionContainer<T>.isRegularFile(): Assertion = _pathImpl.isRegularFile(this)
fun <T : Path> AssertionContainer<T>.isDirectory(): Assertion = _pathImpl.isDirectory(this)
fun <T : Path> AssertionContainer<T>.hasSameTextualContentAs(targetPath: Path, sourceCharset: Charset, targetCharset: Charset): Assertion =
_pathImpl.hasSameTextualContentAs(this, targetPath, sourceCharset, targetCharset)
fun <T : Path> AssertionContainer<T>.hasSameBinaryContentAs(targetPath: Path): Assertion = _pathImpl.hasSameBinaryContentAs(this, targetPath)
fun <T : Path> AssertionContainer<T>.fileName(): ExtractedFeaturePostStep<T, String> = _pathImpl.fileName(this)
fun <T : Path> AssertionContainer<T>.extension(): ExtractedFeaturePostStep<T, String> = _pathImpl.extension(this)
fun <T : Path> AssertionContainer<T>.fileNameWithoutExtension(): ExtractedFeaturePostStep<T, String> = _pathImpl.fileNameWithoutExtension(this)
fun <T : Path> AssertionContainer<T>.parent(): ExtractedFeaturePostStep<T, Path> = _pathImpl.parent(this)
fun <T : Path> AssertionContainer<T>.resolve(other: String): ExtractedFeaturePostStep<T, Path> = _pathImpl.resolve(this, other)

View File

@@ -0,0 +1,12 @@
package ch.tutteli.atrium.logic
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep
import java.util.*
interface OptionalAssertions {
fun <T : Optional<*>> isEmpty(container: AssertionContainer<T>): Assertion
fun <E, T : Optional<E>> isPresent(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, E>
}

View File

@@ -0,0 +1,37 @@
package ch.tutteli.atrium.logic
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep
import java.nio.charset.Charset
import java.nio.file.Path
interface PathAssertions {
fun <T : Path> startsWith(container: AssertionContainer<T>, expected: Path): Assertion
fun <T : Path> startsNotWith(container: AssertionContainer<T>, expected: Path): Assertion
fun <T : Path> endsWith(container: AssertionContainer<T>, expected: Path): Assertion
fun <T : Path> endsNotWith(container: AssertionContainer<T>, expected: Path): Assertion
fun <T : Path> exists(container: AssertionContainer<T>): Assertion
fun <T : Path> existsNot(container: AssertionContainer<T>): Assertion
fun <T : Path> isReadable(container: AssertionContainer<T>): Assertion
fun <T : Path> isWritable(container: AssertionContainer<T>): Assertion
fun <T : Path> isRegularFile(container: AssertionContainer<T>): Assertion
fun <T : Path> isDirectory(container: AssertionContainer<T>): Assertion
fun <T : Path> hasSameTextualContentAs(
container: AssertionContainer<T>,
targetPath: Path,
sourceCharset: Charset,
targetCharset: Charset
): Assertion
fun <T : Path> hasSameBinaryContentAs(container: AssertionContainer<T>, targetPath: Path): Assertion
fun <T : Path> fileName(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, String>
fun <T : Path> extension(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, String>
fun <T : Path> fileNameWithoutExtension(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, String>
fun <T : Path> parent(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, Path>
fun <T : Path> resolve(container: AssertionContainer<T>, other: String): ExtractedFeaturePostStep<T, Path>
}

View File

@@ -0,0 +1,29 @@
package ch.tutteli.atrium.logic.impl
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.core.Option
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep
import ch.tutteli.atrium.logic.OptionalAssertions
import ch.tutteli.atrium.logic.createDescriptiveAssertion
import ch.tutteli.atrium.logic.extractFeature
import ch.tutteli.atrium.translations.DescriptionBasic.IS
import ch.tutteli.atrium.translations.DescriptionOptionalAssertion.*
import java.util.*
class DefaultOptionalAssertions : OptionalAssertions {
override fun <T : Optional<*>> isEmpty(container: AssertionContainer<T>): Assertion =
container.createDescriptiveAssertion(IS, EMPTY) { !it.isPresent }
override fun <E, T : Optional<E>> isPresent(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, E> =
container.extractFeature
.withDescription(GET)
.withRepresentationForFailure(IS_NOT_PRESENT)
.withFeatureExtraction {
Option.someIf(it.isPresent) { it.get() }
}
.withoutOptions()
.build()
}

View File

@@ -0,0 +1,170 @@
package ch.tutteli.atrium.logic.impl
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.assertions.builders.assertionBuilder
import ch.tutteli.atrium.core.None
import ch.tutteli.atrium.core.Some
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.creating.changers.ExtractedFeaturePostStep
import ch.tutteli.atrium.logic.*
import ch.tutteli.atrium.logic.impl.creating.filesystem.Failure
import ch.tutteli.atrium.logic.impl.creating.filesystem.IoResult
import ch.tutteli.atrium.logic.impl.creating.filesystem.Success
import ch.tutteli.atrium.logic.impl.creating.filesystem.hints.*
import ch.tutteli.atrium.logic.impl.creating.filesystem.runCatchingIo
import ch.tutteli.atrium.reporting.translating.Translatable
import ch.tutteli.atrium.reporting.translating.TranslatableWithArgs
import ch.tutteli.atrium.translations.DescriptionBasic
import ch.tutteli.atrium.translations.DescriptionPathAssertion.*
import ch.tutteli.niok.*
import java.nio.charset.Charset
import java.nio.file.AccessDeniedException
import java.nio.file.AccessMode
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.BasicFileAttributes
class DefaultPathAssertions : PathAssertions {
override fun <T : Path> startsWith(container: AssertionContainer<T>, expected: Path): Assertion =
container.createDescriptiveAssertion(STARTS_WITH, expected) { it.startsWith(expected) }
override fun <T : Path> startsNotWith(container: AssertionContainer<T>, expected: Path): Assertion =
container.createDescriptiveAssertion(STARTS_NOT_WITH, expected) { !it.startsWith(expected) }
override fun <T : Path> endsWith(container: AssertionContainer<T>, expected: Path): Assertion =
container.createDescriptiveAssertion(ENDS_WITH, expected) { it.endsWith(expected) }
override fun <T : Path> endsNotWith(container: AssertionContainer<T>, expected: Path): Assertion =
container.createDescriptiveAssertion(ENDS_NOT_WITH, expected) { !it.endsWith(expected) }
override fun <T : Path> hasSameTextualContentAs(
container: AssertionContainer<T>,
targetPath: Path,
sourceCharset: Charset,
targetCharset: Charset
): Assertion = container.createDescriptiveAssertion(
TranslatableWithArgs(HAS_SAME_TEXTUAL_CONTENT, sourceCharset, targetCharset),
targetPath
) {
it.readText(sourceCharset) == targetPath.readText(targetCharset)
}
override fun <T : Path> hasSameBinaryContentAs(container: AssertionContainer<T>, targetPath: Path): Assertion =
container.createDescriptiveAssertion(HAS_SAME_BINARY_CONTENT, targetPath) {
it.readAllBytes().contentEquals(targetPath.readAllBytes())
}
override fun <T : Path> exists(container: AssertionContainer<T>): Assertion =
changeSubjectToFileAttributes(container) { fileAttributesExpect ->
assertionBuilder.descriptive
.withTest(fileAttributesExpect) { it is Success }
.withIOExceptionFailureHint(fileAttributesExpect) { realPath, exception ->
when (exception) {
// TODO remove group once https://github.com/robstoll/atrium-roadmap/issues/1 is implemented
is NoSuchFileException -> assertionBuilder.explanatoryGroup
.withDefaultType
.withAssertion(hintForClosestExistingParent(realPath))
.build()
else -> null
}
}
.withDescriptionAndRepresentation(DescriptionBasic.TO, EXIST)
.build()
}
override fun <T : Path> existsNot(container: AssertionContainer<T>): Assertion =
changeSubjectToFileAttributes(container) { fileAttributesExpect ->
assertionBuilder.descriptive
.withTest(fileAttributesExpect) { it is Failure && it.exception is NoSuchFileException }
.withFileAttributesFailureHint(fileAttributesExpect)
.withDescriptionAndRepresentation(DescriptionBasic.NOT_TO, EXIST)
.build()
}
private inline fun <T : Path, R> changeSubjectToFileAttributes(
container: AssertionContainer<T>,
block: (Expect<IoResult<BasicFileAttributes>>) -> R
): R = container.changeSubject.unreported {
it.runCatchingIo { readAttributes<BasicFileAttributes>() }
}.let(block)
override fun <T : Path> isReadable(container: AssertionContainer<T>): Assertion =
filePermissionAssertion(container, READABLE, AccessMode.READ)
override fun <T : Path> isWritable(container: AssertionContainer<T>): Assertion =
filePermissionAssertion(container, WRITABLE, AccessMode.WRITE)
override fun <T : Path> isRegularFile(container: AssertionContainer<T>): Assertion =
fileTypeAssertion(container, A_FILE) { it.isRegularFile }
override fun <T : Path> isDirectory(container: AssertionContainer<T>): Assertion =
fileTypeAssertion(container, A_DIRECTORY) { it.isDirectory }
private fun <T : Path> filePermissionAssertion(
container: AssertionContainer<T>,
permissionName: Translatable,
accessMode: AccessMode
) = container.changeSubject.unreported {
it.runCatchingIo { fileSystem.provider().checkAccess(it, accessMode) }
}.let { checkAccessResultExpect ->
assertionBuilder.descriptive
.withTest(checkAccessResultExpect) { it is Success }
.withIOExceptionFailureHint(checkAccessResultExpect) { realPath, exception ->
when (exception) {
is AccessDeniedException -> findHintForProblemWithParent(realPath)
?: assertionBuilder.explanatoryGroup
.withDefaultType
.withAssertions(
listOf(hintForExistsButMissingPermission(realPath, permissionName))
+ hintForOwnersAndPermissions(realPath)
)
.build()
else -> null
}
}
.withDescriptionAndRepresentation(DescriptionBasic.IS, permissionName)
.build()
}
private inline fun <T : Path> fileTypeAssertion(
container: AssertionContainer<T>,
typeName: Translatable,
crossinline typeTest: (BasicFileAttributes) -> Boolean
) = changeSubjectToFileAttributes(container) { fileAttributesExpect ->
assertionBuilder.descriptive
.withTest(fileAttributesExpect) { it is Success && typeTest(it.value) }
.withFileAttributesFailureHint(fileAttributesExpect)
.withDescriptionAndRepresentation(DescriptionBasic.IS, typeName)
.build()
}
override fun <T : Path> fileName(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, String> =
container.manualFeature(FILE_NAME) { fileName.toString() }
override fun <T : Path> extension(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, String> =
container.manualFeature(EXTENSION) { extension }
override fun <T : Path> fileNameWithoutExtension(
container: AssertionContainer<T>
): ExtractedFeaturePostStep<T, String> =
container.manualFeature(FILE_NAME_WITHOUT_EXTENSION) { fileNameWithoutExtension }
override fun <T : Path> parent(container: AssertionContainer<T>): ExtractedFeaturePostStep<T, Path> =
container.extractFeature
.withDescription(PARENT)
.withRepresentationForFailure(DOES_NOT_HAVE_PARENT)
.withFeatureExtraction {
val parent: Path? = it.parent
if (parent != null) Some(parent) else None
}
.withoutOptions()
.build()
override fun <T : Path> resolve(
container: AssertionContainer<T>,
other: String
): ExtractedFeaturePostStep<T, Path> = container.f1<T, String, Path>(Path::resolve, other)
}

View File

@@ -0,0 +1,21 @@
package ch.tutteli.atrium.logic.impl.creating.filesystem
import java.io.IOException
import java.nio.file.Path
/**
* Executes the given [block] and catches [IOException]s.
*
* @return [Success] with [this] path and [block]s result if [block] executes successfully.
* [Failure] with [this] path and the thrown [IOException] if [block] throws an [IOException]
* @throws Exception any exception that is thrown by [block] if it is not an [IOException]
*/
inline fun <T> Path.runCatchingIo(block: Path.() -> T): IoResult<T> = try {
Success(this, this.block())
} catch (e: IOException) {
Failure(this, e)
}
sealed class IoResult<out T>(val path: Path)
class Success<out T>(path: Path, val value: T) : IoResult<T>(path)
class Failure(path: Path, val exception: IOException) : IoResult<Nothing>(path)

View File

@@ -0,0 +1,397 @@
package ch.tutteli.atrium.logic.impl.creating.filesystem.hints
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.assertions.AssertionGroup
import ch.tutteli.atrium.assertions.builders.*
import ch.tutteli.atrium.core.polyfills.fullName
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.logic.impl.creating.changers.ThrowableThrownFailureHandler
import ch.tutteli.atrium.logic.impl.creating.filesystem.Failure
import ch.tutteli.atrium.logic.impl.creating.filesystem.IoResult
import ch.tutteli.atrium.logic.impl.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.niok.followSymbolicLink
import ch.tutteli.niok.getFileAttributeView
import ch.tutteli.niok.readAttributes
import java.io.IOException
import java.nio.file.AccessDeniedException
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.*
import java.util.*
inline fun <T> Descriptive.DescriptionOption<Descriptive.FinalStep>.withIOExceptionFailureHint(
expect: Expect<IoResult<T>>,
crossinline f: (Path, IOException) -> Assertion?
): Descriptive.DescriptionOption<DescriptiveAssertionWithFailureHint.FinalStep> =
withFailureHintBasedOnDefinedSubject(expect) { result ->
explainForResolvedLink(result.path) { realPath ->
val exception = (result as Failure).exception
f(realPath, exception) ?: hintForIoException(realPath, exception)
}
}
fun Descriptive.DescriptionOption<Descriptive.FinalStep>.withFileAttributesFailureHint(
expect: Expect<IoResult<BasicFileAttributes>>
): Descriptive.DescriptionOption<DescriptiveAssertionWithFailureHint.FinalStep> =
withFailureHintBasedOnDefinedSubject(expect) { result ->
explainForResolvedLink(result.path) { realPath ->
when (result) {
is Success -> describeWas(result.value.fileType)
is Failure -> hintForIoException(realPath, result.exception)
}
}
}
/**
* Internal for testing purposes only
*/
fun explainForResolvedLink(
path: Path,
resolvedPathAssertionProvider: (realPath: Path) -> Assertion
): Assertion {
val hintList = LinkedList<Assertion>()
val realPath = addAllLevelResolvedSymlinkHints(path, hintList)
val resolvedPathAssertion = resolvedPathAssertionProvider(realPath)
return if (hintList.isNotEmpty()) {
when (resolvedPathAssertion) {
//TODO this should be done differently
is AssertionGroup -> hintList.addAll(resolvedPathAssertion.assertions)
else -> hintList.add(resolvedPathAssertion)
}
assertionBuilder.explanatoryGroup.withDefaultType
.withAssertions(hintList)
.build()
} else {
resolvedPathAssertion
}
}
/**
* Resolves the provided [path] and returns the resolved target (if resolving is possible).
* Adds explanatory hints for all involved symbolic links to [hintList].
*/
private fun addAllLevelResolvedSymlinkHints(path: Path, hintList: Deque<Assertion>): Path {
val absolutePath = path.toAbsolutePath().normalize()
return addAllLevelResolvedSymlinkHints(absolutePath, hintList, Stack())
}
private fun addAllLevelResolvedSymlinkHints(
absolutePath: Path,
hintList: Deque<Assertion>,
loopDetection: Stack<Path>
): Path {
var currentPath = absolutePath.root
for (part in absolutePath) {
currentPath = currentPath.resolve(part)
val loopDetectionIndex = loopDetection.indexOf(currentPath)
if (loopDetectionIndex != -1) {
// add to the list so [hintForLinkLoop] prints this duplicate twice
loopDetection.add(currentPath)
hintList.add(hintForLinkLoop(loopDetection, loopDetectionIndex))
return absolutePath
}
val nextPathAfterFollowSymbolicLink = addOneStepResolvedSymlinkHint(currentPath, hintList)
if (nextPathAfterFollowSymbolicLink != null) {
loopDetection.push(currentPath)
currentPath = addAllLevelResolvedSymlinkHints(nextPathAfterFollowSymbolicLink, hintList, loopDetection)
loopDetection.pop()
}
}
return currentPath
}
private fun hintForLinkLoop(loop: List<Path>, startIndex: Int): Assertion {
val loopRepresentation = loop.subList(startIndex, loop.size).joinToString(" -> ")
return assertionBuilder.explanatoryGroup.withWarningType
.withExplanatoryAssertion(FAILURE_DUE_TO_LINK_LOOP, loopRepresentation)
.build()
}
/**
* If [absolutePath] is surely a symlink, adds an explanatory hint to [hintList] and returns the link target.
* Return `null` and does not modify [hintList] otherwise.
*/
private fun addOneStepResolvedSymlinkHint(absolutePath: Path, hintList: Deque<Assertion>): Path? {
// we use try-catch as a control flow structure,
// where within the try we assume [absolutePath] to be a symbolic link
return try {
val nextPath = absolutePath
.resolveSibling(absolutePath.followSymbolicLink())
.normalize()
hintList.add(
assertionBuilder.explanatory
.withExplanation(HINT_FOLLOWED_SYMBOLIC_LINK, absolutePath, nextPath)
.build()
)
nextPath
} catch (e: IOException) {
// either this is not a link, or we cannot check it. The best we can do is assume it is not a link.
null
}
}
fun hintForIoException(path: Path, exception: IOException): Assertion = when (exception) {
is NoSuchFileException -> hintForFileNotFound(path)
else -> findHintForProblemWithParent(path) ?: hintForFileSpecificIoException(path, exception)
}
/**
* Searches for any problem with a parent directory that is not that the directory does not exist.
* @return an appropriate hint if a problem with a parent is found that is not that that parent does not exist.
*/
fun findHintForProblemWithParent(path: Path): Assertion? {
val absolutePath = path.toAbsolutePath()
var currentParentPart = absolutePath.root
for (part in absolutePath) {
currentParentPart = currentParentPart.resolve(part)
if (currentParentPart != path) {
try {
val attributes = currentParentPart.readAttributes<BasicFileAttributes>()
if (!attributes.isDirectory) {
return hintForParentFailure(
currentParentPart,
explanation = hintForNotDirectory(attributes.fileType)
)
}
} catch (e: AccessDeniedException) {
return hintForParentFailure(
currentParentPart.parent,
explanation = hintForAccessDenied(currentParentPart.parent)
)
} catch (e: IOException) {
return hintForParentFailure(
currentParentPart,
explanation = hintForFileSpecificIoException(currentParentPart, e)
)
}
}
}
return null
}
private val BasicFileAttributes.fileType: Translatable
get() = when {
isRegularFile -> A_FILE
isDirectory -> A_DIRECTORY
isSymbolicLink -> A_SYMBOLIC_LINK
else -> A_UNKNOWN_FILE_TYPE
}
private fun hintForParentFailure(parent: Path, explanation: Assertion) =
assertionBuilder.explanatoryGroup.withDefaultType
.withAssertions(
assertionBuilder.descriptive.failing
.withDescriptionAndRepresentation(FAILURE_DUE_TO_PARENT, parent)
.build(),
when (explanation) {
is AssertionGroup -> explanation
// TODO remove group once https://github.com/robstoll/atrium-roadmap/issues/1 is implemented
else -> assertionBuilder.explanatoryGroup.withDefaultType
.withAssertion(explanation)
.build()
}
).build()
fun hintForAccessDenied(path: Path): Assertion {
val failureDueToAccessDeniedHint = assertionBuilder.explanatory
.withExplanation(FAILURE_DUE_TO_ACCESS_DENIED)
.build()
return try {
val hints = hintForOwnersAndPermissions(path)
hints.add(0, failureDueToAccessDeniedHint)
assertionBuilder.explanatoryGroup.withDefaultType
.withAssertions(hints)
.build()
} catch (e: IOException) {
failureDueToAccessDeniedHint
}
}
fun hintForOwnersAndPermissions(path: Path): MutableList<Assertion> {
val hintList = LinkedList<Assertion>()
val aclView = path.getFileAttributeView<AclFileAttributeView>()
if (aclView != null) {
hintList.add(hintForOwner(aclView.owner.name))
hintList.addAll(hintsForActualAclPermissions(aclView.acl))
} else {
val posixView = path.getFileAttributeView<PosixFileAttributeView>()
if (posixView != null) {
val posixAttributes = posixView.readAttributes()
hintList.add(hintForOwnerAndGroup(posixAttributes.owner().name, posixAttributes.group().name))
hintList.add(hintForActualPosixPermissions(posixAttributes.permissions()))
}
}
return hintList
}
private fun hintForOwner(owner: String) =
assertionBuilder.explanatory
.withExplanation(HINT_OWNER, owner)
.build()
private fun hintForOwnerAndGroup(owner: String, group: String) =
assertionBuilder.explanatory
.withExplanation(HINT_OWNER_AND_GROUP, owner, group)
.build()
private fun hintsForActualAclPermissions(acl: List<AclEntry>) =
arrayOf(
assertionBuilder.explanatory
.withExplanation(HINT_ACTUAL_ACL_PERMISSIONS)
.build(),
assertionBuilder.explanatoryGroup.withDefaultType
.withAssertions(acl.map(::hintForAclEntry))
.build()
)
private fun hintForAclEntry(entry: AclEntry) =
assertionBuilder.explanatory
.withExplanation("${entry.type()} ${entry.principal().name}: ${entry.permissions().joinToString()}")
.build()
private fun hintForActualPosixPermissions(filePermissions: Set<PosixFilePermission>) =
assertionBuilder.explanatory
.withExplanation(HINT_ACTUAL_POSIX_PERMISSIONS, formatPosixPermissions(filePermissions))
.build()
private fun formatPosixPermissions(filePermissions: Set<PosixFilePermission>): StringBuilder {
val permissionString = StringBuilder(3 * 5 + 2)
permissionString
.append("u=")
.append(
toPermissionString(
filePermissions,
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE
)
)
.append(' ')
.append("g=")
.append(
toPermissionString(
filePermissions,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.GROUP_EXECUTE
)
)
.append(' ')
.append("o=")
.append(
toPermissionString(
filePermissions,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_WRITE,
PosixFilePermission.OTHERS_EXECUTE
)
)
return permissionString
}
private fun toPermissionString(
permissions: Set<PosixFilePermission>,
readPermission: PosixFilePermission,
writePermission: PosixFilePermission,
executePermission: PosixFilePermission
): StringBuilder {
val result = StringBuilder(3)
if (permissions.contains(readPermission)) result.append('r')
if (permissions.contains(writePermission)) result.append('w')
if (permissions.contains(executePermission)) result.append('x')
return result
}
fun <T : Path> hintForExistsButMissingPermission(subject: T, permissionName: Translatable): Assertion =
assertionBuilder.explanatory
.withExplanation(
FAILURE_DUE_TO_PERMISSION_FILE_TYPE_HINT,
subject.readAttributes<BasicFileAttributes>().fileType,
permissionName
)
.build()
private fun describeWas(actual: Translatable) =
assertionBuilder.descriptive
.failing
.withDescriptionAndRepresentation(DescriptionBasic.WAS, actual)
.build()
private fun hintForFileSpecificIoException(path: Path, exception: IOException) =
when (exception) {
is AccessDeniedException -> hintForAccessDenied(path)
else -> hintForOtherIoException(exception)
}
private fun hintForFileNotFound(path: Path) =
assertionBuilder.explanatoryGroup
.withDefaultType
.withAssertions(
hintForNoSuchFile(),
hintForClosestExistingParent(path)
)
.build()
private fun hintForNoSuchFile() =
assertionBuilder.explanatory
.withExplanation(FAILURE_DUE_TO_NO_SUCH_FILE)
.build()
/**
* Assumes that we know that [path] does not exist.
* @return The closest parent directory (including [path] itself) that exists. `null` if there is no such directory.
*/
fun hintForClosestExistingParent(path: Path): Assertion {
var testPath = path.toAbsolutePath().parent
while (testPath.nameCount > 0) {
try {
val testPathAttributes = testPath.readAttributes<BasicFileAttributes>()
return if (testPathAttributes.isDirectory) {
hintForExistingParentDirectory(testPath)
} else {
hintForParentFailure(
testPath,
explanation = hintForNotDirectory(testPathAttributes.fileType)
)
}
} catch (e: NoSuchFileException) {
/* continue searching. Any other IOException should not occur because [path] does not exist */
}
testPath = testPath.parent
}
return hintForExistingParentDirectory(null)
}
private fun hintForExistingParentDirectory(parent: Path?) =
assertionBuilder.explanatory
.withExplanation(HINT_CLOSEST_EXISTING_PARENT_DIRECTORY, parent ?: DescriptionBasic.NONE)
.build()
private fun hintForNotDirectory(actualType: Translatable) =
assertionBuilder.explanatory
.withExplanation(
FAILURE_DUE_TO_WRONG_FILE_TYPE, actualType,
A_DIRECTORY
)
.build()
private fun hintForOtherIoException(exception: IOException) =
ThrowableThrownFailureHandler.propertiesOfThrowable(
exception,
explanation = assertionBuilder.explanatory
.withExplanation(
FAILURE_DUE_TO_ACCESS_EXCEPTION,
exception::class.simpleName ?: exception::class.fullName
)
.build()
)

View File

@@ -0,0 +1,36 @@
package ch.tutteli.atrium.logic.impl.creating.filesystem
import ch.tutteli.atrium.api.fluent.en_GB.*
import ch.tutteli.atrium.api.verbs.internal.expect
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.nio.file.Paths
object IoResultSpec : Spek({
describe("runCatchingIo") {
val testPath = Paths.get("/test")
it("creates a Success if the block completes normally") {
val result = testPath.runCatchingIo { "testString" }
expect(result).isA<Success<String>> {
feature(IoResult<*>::path).toBe(testPath)
feature(Success<*>::value).toBe("testString")
}
}
it("creates a Failure if the block thrown an IOException") {
val testException = NoSuchFileException(testPath.toFile())
val result = testPath.runCatchingIo { throw testException }
expect(result).isA<Failure> {
feature(IoResult<*>::path).toBe(testPath)
feature(Failure::exception).isSameAs(testException)
}
}
it("re-throws other exceptions") {
expect {
testPath.runCatchingIo { throw IllegalStateException() }
}.toThrow<IllegalStateException>()
}
}
})

View File

@@ -0,0 +1,286 @@
package ch.tutteli.atrium.logic.impl.creating.filesystem.hints
import ch.tutteli.atrium.api.fluent.en_GB.*
import ch.tutteli.atrium.api.verbs.internal.expect
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.assertions.AssertionGroup
import ch.tutteli.atrium.assertions.ExplanatoryAssertion
import ch.tutteli.atrium.assertions.WarningAssertionGroupType
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.domain.builders.ExpectImpl
import ch.tutteli.atrium.reporting.translating.TranslatableWithArgs
import ch.tutteli.atrium.reporting.translating.Untranslatable
import ch.tutteli.atrium.specs.fileSystemSupportsCreatingSymlinks
import ch.tutteli.atrium.translations.DescriptionPathAssertion.FAILURE_DUE_TO_LINK_LOOP
import ch.tutteli.atrium.translations.DescriptionPathAssertion.HINT_FOLLOWED_SYMBOLIC_LINK
import ch.tutteli.niok.createDirectory
import ch.tutteli.niok.createFile
import ch.tutteli.niok.createSymbolicLink
import ch.tutteli.spek.extensions.memoizedTempFolder
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.spekframework.spek2.Spek
import org.spekframework.spek2.dsl.Skip
import org.spekframework.spek2.lifecycle.CachingMode.TEST
import org.spekframework.spek2.style.specification.describe
import java.nio.file.Path
import java.nio.file.Paths
object SymbolicLinkResolvingSpec : Spek({
val tempFolder by memoizedTempFolder()
// Windows with neither symlink nor admin privilege
val ifSymlinksNotSupported =
if (fileSystemSupportsCreatingSymlinks()) Skip.No else Skip.Yes("creating symbolic links is not supported on this file system")
val testAssertion = ExpectImpl.builder.createDescriptive(Untranslatable("testAssertion"), null) { true }
val resolvedPathConsumer by memoized(TEST) {
mockk<(Path) -> Assertion> {
every { this@mockk.invoke(any()) } returns testAssertion
}
}
/**
* Throughout this suite, we have to make sure that all paths we use are already completely resolved. Otherwise, we
* might get additional, unexpected messages because the path to the temporary folder contains a symlink.
*/
describe("explainForResolvedLink", skip = ifSymlinksNotSupported) {
describe("resolves correctly") {
afterEachTest {
confirmVerified(resolvedPathConsumer)
}
it("resolves an existing file to itself") {
val file = tempFolder.newFile("testFile").toRealPath()
explainForResolvedLink(file, resolvedPathConsumer)
verify { resolvedPathConsumer(file) }
}
it("resolves an existing directory to itself") {
val folder = tempFolder.newFile("testDir").toRealPath()
explainForResolvedLink(folder, resolvedPathConsumer)
verify { resolvedPathConsumer(folder) }
}
it("resolves a non-existent path to itself") {
val notExisting = tempFolder.tmpDir.toRealPath().resolve("notExisting")
explainForResolvedLink(notExisting, resolvedPathConsumer)
verify { resolvedPathConsumer(notExisting) }
}
it("resolves a relative path to its absolute target") {
val relativePath = Paths.get(".")
explainForResolvedLink(relativePath, resolvedPathConsumer)
verify { resolvedPathConsumer(relativePath.toRealPath()) }
}
it("resolves a symbolic link to its target") {
val testDir = tempFolder.newDirectory("symbolic-to-target").toRealPath()
val target = testDir.resolve("notExisting")
val link = target.createSymbolicLink(testDir.resolve("link"))
explainForResolvedLink(link, resolvedPathConsumer)
verify { resolvedPathConsumer(target) }
}
it("a relative symbolic link to its absolute target") {
val testDir = tempFolder.newDirectory("relative-symbolic-to-target").toRealPath()
val target = testDir.resolve("testFile").createFile()
val folder = testDir.resolve("testFolder").createDirectory()
val relativeLink =
Paths.get("..").resolve(target.fileName).createSymbolicLink(folder.resolve("testLink"))
explainForResolvedLink(relativeLink, resolvedPathConsumer)
verify { resolvedPathConsumer(target) }
}
it("resolves a symbolic link chain as far as possible") {
val testDir = tempFolder.newDirectory("chain").toRealPath()
val nowhere = testDir.resolve("dont-exist")
val toNowhere = nowhere.createSymbolicLink(testDir.resolve("link-to-nowhere"))
val start = tempFolder.newSymbolicLink("start", toNowhere)
explainForResolvedLink(start, resolvedPathConsumer)
verify { resolvedPathConsumer(nowhere) }
}
it("resolves multiple symbolic links to their target") {
val testDir = tempFolder.newDirectory("multi-links").toRealPath()
val target = testDir.resolve("notExisting")
val grandparent = testDir.resolve("__linksgrandparent").createDirectory()
val parent = grandparent.resolve("step").createDirectory()
val grandparentLink = grandparent.createSymbolicLink(testDir.resolve("__linkTo_grandparent"))
val innerLink = target.createSymbolicLink(parent.resolve("__linkTo_${target.fileName}"))
val innerLinkInGrandparentLink = grandparentLink.resolve(parent.fileName).resolve(innerLink.fileName)
val linkToInnerLink = tempFolder.newSymbolicLink(
"__transitive_linkTo_${target.fileName}", innerLinkInGrandparentLink
)
explainForResolvedLink(linkToInnerLink, resolvedPathConsumer)
verify { resolvedPathConsumer(target) }
}
}
describe("explains correctly") {
it("returns the original assertion if no link is involved") {
val file = tempFolder.newFile("testFile").toRealPath()
val resultAssertion = explainForResolvedLink(file, resolvedPathConsumer)
expect(resultAssertion).isSameAs(testAssertion)
}
it("adds an explanation for one symbolic link") {
val testDir = tempFolder.newDirectory("link").toRealPath()
val target = testDir.resolve("notExisting")
val link = target.createSymbolicLink(testDir.resolve("link"))
val resultAssertion = explainForResolvedLink(link, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(link, target) },
{ isSameAs(testAssertion) }
)
}
it("adds explanations for a symbolic link chain as far as possible") {
val testDir = tempFolder.newDirectory("chain").toRealPath()
val nowhere = testDir.resolve("dont-exist")
val toNowhere = nowhere.createSymbolicLink(testDir.resolve("link-to-nowhere"))
val start = toNowhere.createSymbolicLink(testDir.resolve("start"))
val resultAssertion = explainForResolvedLink(start, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(start, toNowhere) },
{ describesLink(toNowhere, nowhere) },
{ isSameAs(testAssertion) }
)
}
it("adds explanations for multiple symbolic links") {
val testDir = tempFolder.newDirectory("multi-links").toRealPath()
val target = testDir.resolve("notExisting")
val grandparent = testDir.resolve("__linksgrandparent").createDirectory()
val parent = grandparent.resolve("step").createDirectory()
val grandparentLink = grandparent.createSymbolicLink(testDir.resolve("__linkTo_grandparent"))
val innerLink = target.createSymbolicLink(parent.resolve("__linkTo_${target.fileName}"))
val innerLinkInGrandparentLink =
grandparentLink.resolve(parent.fileName).resolve(innerLink.fileName)
val linkToInnerLink = innerLinkInGrandparentLink.createSymbolicLink(
testDir.resolve("__transitive_linkTo_${target.fileName}")
)
val resultAssertion = explainForResolvedLink(linkToInnerLink, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(linkToInnerLink, innerLinkInGrandparentLink) },
{ describesLink(grandparentLink, grandparent) },
{ describesLink(innerLink, target) },
{ isSameAs(testAssertion) }
)
}
it("does not assume a link loop even if the same link appears multiple times") {
val testDir = tempFolder.newDirectory("multi-non-loop").toRealPath()
val barLink = testDir.createSymbolicLink(testDir.resolve("bar"))
val target = testDir.resolve("target").createFile()
val testLink = barLink.resolve(barLink.fileName).resolve(barLink.fileName).resolve(target.fileName)
val resultAssertion = explainForResolvedLink(testLink, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(barLink, testDir) },
{ describesLink(barLink, testDir) },
{ describesLink(barLink, testDir) },
{ isSameAs(testAssertion) }
)
}
}
it("adds an explanation for link loops") {
val testDir = tempFolder.newDirectory("link-loop").toRealPath()
val a = testDir.resolve("linkA")
val b = a.createSymbolicLink(testDir.resolve("linkB"))
b.createSymbolicLink(a)
val resultAssertion = explainForResolvedLink(a, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(a, b) },
{ describesLink(b, a) },
{ describesLinkLoop(a, b, a) },
{ isSameAs(testAssertion) }
)
}
it("adds an explanation for more subtle link loops") {
val testDir = tempFolder.newDirectory("sneaky-loop").toRealPath()
val foo = testDir.resolve("foo").createDirectory()
val fooLink = foo.createSymbolicLink(testDir.resolve("bar"))
val link = fooLink.resolve("link").createSymbolicLink(foo.resolve("link"))
val resultAssertion = explainForResolvedLink(link, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(link, fooLink.resolve("link")) },
{ describesLink(fooLink, foo) },
{ describesLinkLoop(link, link) },
{ isSameAs(testAssertion) }
)
}
it("keeps explanations for links that are not part of the loop") {
val testDir = tempFolder.newDirectory("link-loop").toRealPath()
val a = testDir.resolve("linkA")
val c = a.createSymbolicLink(testDir.resolve("linkC"))
val b = c.createSymbolicLink(testDir.resolve("linkB"))
b.createSymbolicLink(a)
val grandparent = testDir.resolve("__linksgrandparent").createDirectory()
val parent = grandparent.resolve("step").createDirectory()
val grandparentLink = grandparent.createSymbolicLink(testDir.resolve("__linkTo_grandparent"))
val innerLink = a.createSymbolicLink(parent.resolve("__linkTo_${a.fileName}"))
val innerLinkInGrandparentLink = grandparentLink.resolve(parent.fileName).resolve(innerLink.fileName)
val linkToInnerLink =
innerLinkInGrandparentLink.createSymbolicLink(testDir.resolve("__transitive_linkTo_${a.fileName}"))
val resultAssertion = explainForResolvedLink(linkToInnerLink, resolvedPathConsumer)
expect(resultAssertion).isA<AssertionGroup>()
.feature { p(it::assertions) }.containsExactly(
{ describesLink(linkToInnerLink, innerLinkInGrandparentLink) },
{ describesLink(grandparentLink, grandparent) },
{ describesLink(innerLink, a) },
{ describesLink(a, b) },
{ describesLink(b, c) },
{ describesLink(c, a) },
{ describesLinkLoop(a, b, c, a) },
{ isSameAs(testAssertion) }
)
}
}
})
fun <T : Assertion> Expect<T>.describesLink(link: Path, target: Path) {
isA<ExplanatoryAssertion> {
feature { p(it::explanation) }.toBe(
TranslatableWithArgs(HINT_FOLLOWED_SYMBOLIC_LINK, link, target)
)
}
}
fun <T : Assertion> Expect<T>.describesLinkLoop(vararg loop: Path) {
isA<AssertionGroup> {
feature { p(it::assertions) }.containsExactly {
isA<ExplanatoryAssertion> {
val expectedExplanation = TranslatableWithArgs(FAILURE_DUE_TO_LINK_LOOP, loop.joinToString(" -> "))
feature { p(it::explanation) }.toBe(expectedExplanation)
}
}
feature { p(it::type) }.toBe(WarningAssertionGroupType)
}
}

View File

@@ -1,3 +1,6 @@
//TODO remove file with 1.0.0
@file:Suppress("DEPRECATION")
package ch.tutteli.atrium.domain.robstoll.lib.creating.filesystem
import java.io.IOException
@@ -10,12 +13,16 @@ import java.nio.file.Path
* [Failure] with [this] path and the thrown [IOException] if [block] throws an [IOException]
* @throws Exception any exception that is thrown by [block] if it is not an [IOException]
*/
@Deprecated("use runCatchingIo from atrium-logic; will be removed with 1.0.0")
inline fun <T> Path.runCatchingIo(block: Path.() -> T): IoResult<T> = try {
Success(this, this.block())
} catch (e: IOException) {
Failure(this, e)
}
@Deprecated("use IoResult from atrium-logic; will be removed with 1.0.0")
sealed class IoResult<out T>(val path: Path)
@Deprecated("use Success from atrium-logic; will be removed with 1.0.0")
class Success<out T>(path: Path, val value: T) : IoResult<T>(path)
@Deprecated("use Failure from atrium-logic; will be removed with 1.0.0")
class Failure(path: Path, val exception: IOException) : IoResult<Nothing>(path)

View File

@@ -1,4 +1,8 @@
@file:Suppress("JAVA_MODULE_DOES_NOT_READ_UNNAMED_MODULE" /* TODO remove once https://youtrack.jetbrains.com/issue/KT-35343 is fixed */)
//TODO remove file with 1.0.0
@file:Suppress(
/* TODO remove once https://youtrack.jetbrains.com/issue/KT-35343 is fixed */ "JAVA_MODULE_DOES_NOT_READ_UNNAMED_MODULE",
"DEPRECATION"
)
package ch.tutteli.atrium.domain.robstoll.lib.creating.filesystem
@@ -13,6 +17,7 @@ import java.io.IOException
import java.nio.file.Path
import java.util.*
@Deprecated("use function from atrium-logic; will be removed with 1.0.0")
inline fun explainForResolvedLink(path: Path, resolvedPathAssertionProvider: (realPath: Path) -> Assertion): Assertion {
val hintList = LinkedList<Assertion>()
val realPath = addAllLevelResolvedSymlinkHints(path, hintList)
@@ -36,6 +41,7 @@ inline fun explainForResolvedLink(path: Path, resolvedPathAssertionProvider: (re
* Adds explanatory hints for all involved symbolic links to [hintList].
*/
@PublishedApi
@Deprecated("use function from atrium-logic; will be removed with 1.0.0")
internal fun addAllLevelResolvedSymlinkHints(path: Path, hintList: Deque<Assertion>): Path {
val absolutePath = path.toAbsolutePath().normalize()
return addAllLevelResolvedSymlinkHints(absolutePath, hintList, Stack())

View File

@@ -1,3 +1,6 @@
//TODO remove file with 1.0.0
@file:Suppress("DEPRECATION")
package ch.tutteli.atrium.assertions.filesystem
import ch.tutteli.atrium.api.fluent.en_GB.*

View File

@@ -1,3 +1,6 @@
//TODO remove file with 1.0.0
@file:Suppress("DEPRECATION")
package ch.tutteli.atrium.assertions.filesystem
import ch.tutteli.atrium.api.fluent.en_GB.*