mirror of
https://github.com/jlengrand/atrium.git
synced 2026-03-10 08:01:19 +00:00
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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>()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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.*
|
||||
|
||||
Reference in New Issue
Block a user