Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More consise reporting for contains.atLeast(1) #934

Merged
merged 12 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -999,8 +999,7 @@ expect(listOf(1, 2, 2, 4)).toContain(2, 3)
expected that subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ contains, in any order:
⚬ an element which equals: 3 (kotlin.Int <1234789>)
⚬ ▶ number of such entries: 0
◾ is at least: 1
» but no such element was found
```
</ex-collection-short-1>

Expand Down Expand Up @@ -1039,13 +1038,11 @@ expected that subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>
◆ contains, in any order:
⚬ an element which:
» is less than: 0 (kotlin.Int <1234789>)
⚬ ▶ number of such entries: 0
◾ is at least: 1
» but no such element was found
⚬ an element which:
» is greater than: 2 (kotlin.Int <1234789>)
» is less than: 4 (kotlin.Int <1234789>)
⚬ ▶ number of such entries: 0
◾ is at least: 1
» but no such element was found
```
</ex-collection-short-2>

Expand Down Expand Up @@ -1085,8 +1082,7 @@ expected that subject: [1, 2, 3, 4] (java.util.Arrays.ArrayList <1234789>
◆ contains, in any order:
⚬ an element which:
» is less than: 0 (kotlin.Int <1234789>)
⚬ ▶ number of such entries: 0
◾ is at least: 1
» but no such element was found
```
</ex-collection-any>
<hr/>
Expand Down Expand Up @@ -1739,8 +1735,7 @@ expected that subject: "calling myNullableFun with ..." <1234789>
◆ ▶ myNullableFun(-2147483648): null
» contains:
⚬ value: "min" <1234789>
⚬ ▶ number of matches:
◾ is at least: 1
» but no match was found
◆ ▶ myNullableFun(2147483647): "2147483647" <1234789>
◾ equals: "max" <1234789>
```
Expand Down Expand Up @@ -1888,8 +1883,7 @@ expected that subject: () -> kotlin.Nothing (readme.examples.MostExamples
◾ is instance of type: String (kotlin.String) -- Class: java.lang.String
◾ contains:
⚬ value: "no no no" <1234789>
⚬ ▶ number of matches:
◾ is at least: 1
» but no match was found
ℹ Properties of the unexpected IllegalArgumentException
» message: "no no no..." <1234789>
» stacktrace:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package ch.tutteli.atrium.logic.creating.basic.contains.creators.impl

import ch.tutteli.atrium.assertions.AssertionGroup
import ch.tutteli.atrium.assertions.builders.assertionBuilder
import ch.tutteli.atrium.assertions.builders.withExplanatoryAssertion
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.logic.creating.basic.contains.Contains
import ch.tutteli.atrium.logic.assertions.impl.LazyThreadUnsafeAssertionGroup
import ch.tutteli.atrium.logic.creating.basic.contains.checkers.AtLeastChecker
import ch.tutteli.atrium.reporting.Text
import ch.tutteli.atrium.reporting.translating.Translatable
import ch.tutteli.atrium.reporting.translating.TranslatableWithArgs

/**
* Represents the base class for [Contains.Creator]s, providing a template to fulfill its job.
Expand All @@ -31,6 +34,17 @@ abstract class ContainsAssertionCreator<T : Any, TT : Any, in SC, C : Contains.C
*/
protected abstract val descriptionContains: Translatable

/**
* Provides the translation for when an item is not found in a `contains.atLeast(1)` check.
*/
protected abstract val descriptionNotFound: Translatable

/**
* Provides the translation for `and N such elements were found` when an item is not found in a
* `contains.atLeast(1)` check.
*/
protected abstract val descriptionNumberOfElementsFound: Translatable

final override fun createAssertionGroup(
container: AssertionContainer<T>,
searchCriteria: List<SC>
Expand Down Expand Up @@ -83,9 +97,27 @@ abstract class ContainsAssertionCreator<T : Any, TT : Any, in SC, C : Contains.C

private fun featureFactory(count: Int, numberOfOccurrences: Translatable): AssertionGroup {
val assertions = checkers.map { it.createAssertion(count) }
return assertionBuilder.feature
.withDescriptionAndRepresentation(numberOfOccurrences, Text(count.toString()))
.withAssertions(assertions)
.build()
val checker = checkers.firstOrNull()
return if (checkers.size == 1 && checker is AtLeastChecker && checker.times == 1) {
if (checker.createAssertion(count).holds()) {
assertionBuilder.explanatoryGroup
.withDefaultType
.withExplanatoryAssertion(
TranslatableWithArgs(descriptionNumberOfElementsFound, count.toString())
)
.build()
} else {
assertionBuilder.explanatoryGroup
.withDefaultType
.withExplanatoryAssertion(descriptionNotFound)
.failing
.build()
}
} else {
assertionBuilder.feature
.withDescriptionAndRepresentation(numberOfOccurrences, Text(count.toString()))
.withAssertions(assertions)
.build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package ch.tutteli.atrium.logic.creating.basic.contains.creators.impl
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.assertions.AssertionGroup
import ch.tutteli.atrium.assertions.builders.assertionBuilder
import ch.tutteli.atrium.assertions.builders.invisibleGroup
import ch.tutteli.atrium.core.trueProvider
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.logic.creating.basic.contains.Contains
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.NotSearchBehaviour
import ch.tutteli.atrium.logic.impl.createExplanatoryGroupForMismatches
import ch.tutteli.atrium.reporting.translating.Translatable

/**
Expand Down Expand Up @@ -36,12 +40,24 @@ abstract class ContainsObjectsAssertionCreator<T : Any, TT : Any, in SC, S : Con
searchCriterion: SC,
featureFactory: (Int, Translatable) -> AssertionGroup
): AssertionGroup {
val count = search(multiConsumableContainer, searchCriterion)
val featureAssertion = featureFactory(count, descriptionNumberOfOccurrences)
val assertions = mutableListOf<Assertion>()
if (searchBehaviour is NotSearchBehaviour) {
val mismatches = mismatchesForNotSearchBehaviour(multiConsumableContainer, searchCriterion)
if (mismatches.isNotEmpty()) assertions.add(createExplanatoryGroupForMismatches(mismatches))
} else {
val count = search(multiConsumableContainer, searchCriterion)
val featureAssertion = featureFactory(count, descriptionNumberOfOccurrences)
assertions.add(featureAssertion)
}

return assertionBuilder.list
return if (assertions.isEmpty()) {
assertionBuilder.invisibleGroup
.withAssertion(
assertionBuilder.createDescriptive(groupDescription, searchCriterion, trueProvider)
).build()
} else assertionBuilder.list
.withDescriptionAndRepresentation(groupDescription, searchCriterion)
.withAssertion(featureAssertion)
.withAssertions(assertions)
.build()
}

Expand All @@ -67,4 +83,19 @@ abstract class ContainsObjectsAssertionCreator<T : Any, TT : Any, in SC, S : Con
* @return The number of times the [searchCriterion] matched in the subject of this expectation.
*/
protected abstract fun search(multiConsumableContainer: AssertionContainer<TT>, searchCriterion: SC): Int

/**
* Finds the mismatched indices and values when the [searchBehaviour] is `NotSearchBehaviour` in the subject of the
* given [multiConsumableContainer] and creates a list of assertions about the mismatched indexed values
*
* @param multiConsumableContainer The provider of the subject of this expectation in which we shall look for something
* not matching the given [searchCriterion].
* @param searchCriterion The search criterion used to determine whether something matches or not.
*
* @return A list of [Assertion]s that describe the indexed values that did not match the [searchCriterion]
*/
protected open fun mismatchesForNotSearchBehaviour(
multiConsumableContainer: AssertionContainer<TT>,
searchCriterion: SC
): List<Assertion> = emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class CharSequenceContainsAssertionCreator<T : CharSequence, in SC : Any, S : Se

override val descriptionContains: Translatable = DescriptionCharSequenceAssertion.CONTAINS
override val descriptionNumberOfOccurrences: Translatable = DescriptionCharSequenceAssertion.NUMBER_OF_OCCURRENCES
override val descriptionNotFound: Translatable = DescriptionCharSequenceAssertion.NOT_FOUND
override val descriptionNumberOfElementsFound: Translatable = DescriptionCharSequenceAssertion.NUMBER_OF_MATCHES_FOUND

override fun makeSubjectMultipleTimesConsumable(container: AssertionContainer<T>): AssertionContainer<String> =
container.changeSubject.unreported { it.toString() }.toAssertionContainer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class InAnyOrderEntriesAssertionCreator<E : Any, T : IterableLike>(
IterableLikeContains.Creator<T, (Expect<E>.() -> Unit)?> {

override val descriptionContains: Translatable = DescriptionIterableAssertion.CONTAINS
override val descriptionNotFound: Translatable = DescriptionIterableAssertion.ELEMENT_NOT_FOUND
override val descriptionNumberOfElementsFound: Translatable = DescriptionIterableAssertion.NUMBER_OF_ELEMENTS_FOUND

override fun makeSubjectMultipleTimesConsumable(container: AssertionContainer<T>): AssertionContainer<List<E?>> =
turnSubjectToList(container, converter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,25 @@ class InAnyOrderValuesAssertionCreator<SC, T : IterableLike>(
override val descriptionContains: Translatable = DescriptionIterableAssertion.CONTAINS
override val descriptionNumberOfOccurrences: Translatable = DescriptionIterableAssertion.NUMBER_OF_OCCURRENCES
override val groupDescription: Translatable = DescriptionIterableAssertion.AN_ELEMENT_WHICH_EQUALS
override val descriptionNotFound: Translatable = DescriptionIterableAssertion.ELEMENT_NOT_FOUND
override val descriptionNumberOfElementsFound: Translatable = DescriptionIterableAssertion.NUMBER_OF_ELEMENTS_FOUND

override fun makeSubjectMultipleTimesConsumable(container: AssertionContainer<T>): AssertionContainer<List<SC>> =
turnSubjectToList(container, converter)

override fun search(multiConsumableContainer: AssertionContainer<List<SC>>, searchCriterion: SC): Int =
multiConsumableContainer.maybeSubject.fold({ -1 }) { subject -> subject.filter { it == searchCriterion }.size }

override fun searchAndCreateAssertion(
/**
* Override in any subclass that wants to report mismatched elements individually when the [searchBehaviour]
* is [NotSearchBehaviour]
*/
override fun mismatchesForNotSearchBehaviour(
multiConsumableContainer: AssertionContainer<List<SC>>,
searchCriterion: SC,
featureFactory: (Int, Translatable) -> AssertionGroup
): AssertionGroup {
return if (searchBehaviour is NotSearchBehaviour) {
val list = multiConsumableContainer.maybeSubject.getOrElse { emptyList() }
val mismatches = createIndexAssertions(list) { (_, element) -> element == searchCriterion }
val assertions = mutableListOf<Assertion>()
if (mismatches.isNotEmpty()) assertions.add(createExplanatoryGroupForMismatches(mismatches))
assertionBuilder.list
.withDescriptionAndRepresentation(groupDescription, searchCriterion)
.withAssertions(assertions)
.build()
} else {
super.searchAndCreateAssertion(multiConsumableContainer, searchCriterion, featureFactory)
}
searchCriterion: SC
): List<Assertion> {
val list = multiConsumableContainer.maybeSubject.getOrElse { emptyList() }
return createIndexAssertions(list) { (_, element) -> element == searchCriterion }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ abstract class CharSequenceToContainAtLeastExpectationsSpec(
it("${toContainAtLeastPair.first("'h'", "once")} throws AssertionError") {
expect {
fluentHelloWorld.toContainAtLeastFun(1, 'h')
}.toThrow<AssertionError> { messageToContain("$atLeast: 1", "$valueWithIndent: 'h'") }
}.toThrow<AssertionError> { messageToContain(noMatchFoundDescr, "$valueWithIndent: 'h'") }
}
it("${toContainAtLeastIgnoringCasePair.first("'h'", "once")} does not throw") {
fluentHelloWorld.toContainAtLeastIgnoringCaseFun(1, 'h')
Expand All @@ -151,7 +151,7 @@ abstract class CharSequenceToContainAtLeastExpectationsSpec(
it("${toContainAtLeastPair.first("'H', 'E'", "once")} throws AssertionError") {
expect {
fluentHelloWorld.toContainAtLeastFun(1, 'H', 'E')
}.toThrow<AssertionError> { messageToContain(atLeast, 'E') }
}.toThrow<AssertionError> { messageToContain(noMatchFoundDescr, 'E') }
}
it("${toContainAtLeastIgnoringCasePair.first("'H', 'E'", "once")} does not throw") {
fluentHelloWorld.toContainAtLeastIgnoringCaseFun(1, 'H', 'E')
Expand All @@ -162,7 +162,7 @@ abstract class CharSequenceToContainAtLeastExpectationsSpec(
fluentHelloWorld.toContainAtLeastFun(1, 'E', 'H')
}.toThrow<AssertionError> {
message {
toContain("$atLeast: 1", "$valueWithIndent: 'E'")
toContain(noMatchFoundDescr, "$valueWithIndent: 'E'")
notToContain("$valueWithIndent: 'H'")
}
}
Expand All @@ -181,7 +181,7 @@ abstract class CharSequenceToContainAtLeastExpectationsSpec(
fluentHelloWorld.toContainAtLeastFun(1, 'H', 'E', 'w', 'r')
}.toThrow<AssertionError> {
message {
toContain("$atLeast: 1", "$valueWithIndent: 'E'", "$valueWithIndent: 'w'")
toContain(noMatchFoundDescr, "$valueWithIndent: 'E'", "$valueWithIndent: 'w'")
notToContain("$valueWithIndent: 'H'", "$valueWithIndent: 'r'")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ abstract class CharSequenceToContainNotToContainExpectationsSpec(
messageToContain(
"$rootBulletPoint$toContainDescr: $separator" +
"$valueWithIndent: \"Hello\"",
"$numberOfOccurrences: 0",
"$atLeast: 1"
noMatchFoundDescr
)
}
}
Expand Down Expand Up @@ -103,9 +102,8 @@ abstract class CharSequenceToContainNotToContainExpectationsSpec(
fluent.toContainFun("hello", "robert")
}.toThrow<AssertionError> {
message {
this.toContain.exactly(2).values(
"$numberOfOccurrences: 0",
"$atLeast: 1"
this.toContain.exactly(2).value(
noMatchFoundDescr
)
this.toContain.exactly(1).values(
"$rootBulletPoint$toContainDescr: $separator",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,7 @@ abstract class CharSequenceToContainRegexExpectationsSpec(
toContain(
"$rootBulletPoint$toContainDescr: $separator" +
"$regexWithIndent: ${roberto.toLowerCase()}",
"$numberOfOccurrences: 0",
"$atLeast: 1"
noMatchFoundDescr
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ abstract class CharSequenceToContainSpecBase(spec: Root.() -> Unit) : Spek(spec)
val numberOfOccurrences = DescriptionCharSequenceAssertion.NUMBER_OF_OCCURRENCES.getDefault()
val value = DescriptionCharSequenceAssertion.VALUE.getDefault()
val stringMatchingRegex = DescriptionCharSequenceAssertion.STRING_MATCHING_REGEX.getDefault()
val noMatchFoundDescr = DescriptionCharSequenceAssertion.NOT_FOUND.getDefault()

val atLeast = DescriptionCharSequenceAssertion.AT_LEAST.getDefault()
val atMost = DescriptionCharSequenceAssertion.AT_MOST.getDefault()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ abstract class IterableToContainEntriesSpecBase(
val indexDescr = String.format(DescriptionIterableAssertion.INDEX.getDefault(), index)
return "$indexDescr: ${value.toString()}"
}
val noSuchEntryDescr = DescriptionIterableAssertion.ELEMENT_NOT_FOUND.getDefault()

fun index(index: Int) = String.format(DescriptionIterableAssertion.INDEX.getDefault(), index)

Expand Down
Loading