Make Iterable.all and Iterable.containsNoDuplicates use hasNext (#939)

This commit is contained in:
Edward Hou
2021-07-06 11:37:40 -07:00
committed by GitHub
parent ce5d124e70
commit 1a8e024a7b
9 changed files with 147 additions and 157 deletions

View File

@@ -2206,8 +2206,11 @@ expect(Person("Susanne", "Whitley", 43, listOf()))
```text
expected that subject: Person(firstName=Susanne, lastName=Whitley, age=43, children=[]) (readme.examples.Person <1234789>)
children: [] (kotlin.collections.EmptyList <1234789>)
◾ ▶ has at least one element: false
is: true
has: a next element
» all entries:
» ▶age:
» ▶age:
is greater than or equal to: 18 (kotlin.Int <1234789>)
```
</ex-own-compose-4>

View File

@@ -2,12 +2,10 @@ 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.createAssertionGroupFromListOfAssertions
import ch.tutteli.atrium.logic.impl.createExplanatoryGroupForMismatches
import ch.tutteli.atrium.reporting.translating.Translatable
@@ -50,15 +48,7 @@ abstract class ContainsObjectsAssertionCreator<T : Any, TT : Any, in SC, S : Con
assertions.add(featureAssertion)
}
return if (assertions.isEmpty()) {
assertionBuilder.invisibleGroup
.withAssertion(
assertionBuilder.createDescriptive(groupDescription, searchCriterion, trueProvider)
).build()
} else assertionBuilder.list
.withDescriptionAndRepresentation(groupDescription, searchCriterion)
.withAssertions(assertions)
.build()
return createAssertionGroupFromListOfAssertions(groupDescription, searchCriterion, assertions)
}
/**

View File

@@ -3,7 +3,6 @@ package ch.tutteli.atrium.logic.creating.iterable.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.None
import ch.tutteli.atrium.core.getOrElse
import ch.tutteli.atrium.creating.AssertionContainer
@@ -16,15 +15,10 @@ import ch.tutteli.atrium.logic.creating.iterable.contains.IterableLikeContains
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.InAnyOrderSearchBehaviour
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.NotSearchBehaviour
import ch.tutteli.atrium.logic.creating.typeutils.IterableLike
import ch.tutteli.atrium.logic.hasNext
import ch.tutteli.atrium.logic.impl.allCreatedAssertionsHold
import ch.tutteli.atrium.logic.impl.createExplanatoryAssertionGroup
import ch.tutteli.atrium.logic.impl.createExplanatoryGroupForMismatches
import ch.tutteli.atrium.logic.impl.createIndexAssertions
import ch.tutteli.atrium.logic.impl.*
import ch.tutteli.atrium.reporting.translating.Translatable
import ch.tutteli.atrium.translations.DescriptionIterableAssertion
import ch.tutteli.atrium.translations.DescriptionIterableAssertion.AN_ELEMENT_WHICH
import ch.tutteli.kbox.identity
/**
* Represents a creator of a sophisticated `contains` assertions for [Iterable] where an expected entry can appear
@@ -63,20 +57,9 @@ class InAnyOrderEntriesAssertionCreator<E : Any, T : IterableLike>(
inAnyOrderAssertion: AssertionGroup,
multiConsumableContainer: AssertionContainer<List<E?>>
): AssertionGroup {
val hasNext = multiConsumableContainer.hasNext(::identity)
return if (searchBehaviour is NotSearchBehaviour && !hasNext.holds()) {
assertionBuilder.invisibleGroup
.withAssertions(
hasNext,
assertionBuilder.explanatoryGroup
.withDefaultType
.withAssertion(inAnyOrderAssertion)
.build()
)
.build()
} else {
inAnyOrderAssertion
}
return if (searchBehaviour is NotSearchBehaviour)
decorateAssertionWithHasNext(inAnyOrderAssertion, multiConsumableContainer)
else inAnyOrderAssertion
}
override fun searchAndCreateAssertion(

View File

@@ -2,8 +2,6 @@ package ch.tutteli.atrium.logic.creating.iterable.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.getOrElse
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.logic.creating.basic.contains.creators.impl.ContainsObjectsAssertionCreator
@@ -11,12 +9,10 @@ import ch.tutteli.atrium.logic.creating.iterable.contains.IterableLikeContains
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.InAnyOrderSearchBehaviour
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.NotSearchBehaviour
import ch.tutteli.atrium.logic.creating.typeutils.IterableLike
import ch.tutteli.atrium.logic.hasNext
import ch.tutteli.atrium.logic.impl.createExplanatoryGroupForMismatches
import ch.tutteli.atrium.logic.impl.createIndexAssertions
import ch.tutteli.atrium.logic.impl.decorateAssertionWithHasNext
import ch.tutteli.atrium.reporting.translating.Translatable
import ch.tutteli.atrium.translations.DescriptionIterableAssertion
import ch.tutteli.kbox.identity
/**
* Represents a creator of a sophisticated `contains` assertions for [Iterable] where an expected entry can appear
@@ -71,17 +67,8 @@ class InAnyOrderValuesAssertionCreator<SC, T : IterableLike>(
inAnyOrderAssertion: AssertionGroup,
multiConsumableContainer: AssertionContainer<List<SC>>
): AssertionGroup {
val hasNext = multiConsumableContainer.hasNext(::identity)
return if (searchBehaviour is NotSearchBehaviour && !hasNext.holds()) {
assertionBuilder.invisibleGroup.withAssertions(
hasNext,
assertionBuilder.explanatoryGroup
.withDefaultType
.withAssertion(inAnyOrderAssertion)
.build()
).build()
} else {
inAnyOrderAssertion
}
return if (searchBehaviour is NotSearchBehaviour)
decorateAssertionWithHasNext(inAnyOrderAssertion, multiConsumableContainer)
else inAnyOrderAssertion
}
}

View File

@@ -3,13 +3,13 @@ package ch.tutteli.atrium.logic.impl
import ch.tutteli.atrium.assertions.Assertion
import ch.tutteli.atrium.assertions.builders.*
import ch.tutteli.atrium.core.Option
import ch.tutteli.atrium.core.getOrElse
import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.logic.IterableLikeAssertions
import ch.tutteli.atrium.logic._logic
import ch.tutteli.atrium.logic.*
import ch.tutteli.atrium.logic.assertions.impl.LazyThreadUnsafeAssertionGroup
import ch.tutteli.atrium.logic.createDescriptiveAssertion
import ch.tutteli.atrium.logic.creating.iterable.contains.IterableLikeContains
import ch.tutteli.atrium.logic.creating.iterable.contains.creators.impl.turnSubjectToList
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.NoOpSearchBehaviour
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.NotSearchBehaviour
import ch.tutteli.atrium.logic.creating.iterable.contains.searchbehaviours.impl.NoOpSearchBehaviourImpl
@@ -19,9 +19,6 @@ import ch.tutteli.atrium.logic.creating.iterable.contains.steps.impl.EntryPointS
import ch.tutteli.atrium.logic.creating.iterable.contains.steps.notCheckerStep
import ch.tutteli.atrium.logic.creating.transformers.FeatureExtractorBuilder
import ch.tutteli.atrium.logic.creating.typeutils.IterableLike
import ch.tutteli.atrium.logic.extractFeature
import ch.tutteli.atrium.reporting.Text
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.DescriptionIterableAssertion
@@ -91,60 +88,31 @@ class DefaultIterableLikeAssertions : IterableLikeAssertions {
converter: (T) -> Iterable<E?>,
assertionCreatorOrNull: (Expect<E>.() -> Unit)?
): Assertion = LazyThreadUnsafeAssertionGroup {
val list = transformToList(container, converter)
val assertions = ArrayList<Assertion>(2)
assertions.add(createExplanatoryAssertionGroup(container, assertionCreatorOrNull))
val listAssertionContainer = turnSubjectToList(container, converter)
val list = listAssertionContainer.maybeSubject.getOrElse { emptyList() }
val explanatoryGroup = createExplanatoryAssertionGroup(container, assertionCreatorOrNull)
val assertions = mutableListOf<Assertion>(explanatoryGroup)
val mismatches = createIndexAssertions(list) { (_, element) ->
!allCreatedAssertionsHold(container, element, assertionCreatorOrNull)
}
assertions.add(createExplanatoryGroupForMismatches(mismatches))
if (mismatches.isNotEmpty()) assertions.add(createExplanatoryGroupForMismatches(mismatches))
createHasElementPlusFixedClaimGroup(
list,
DescriptionIterableAssertion.ALL,
Text.EMPTY,
mismatches.isEmpty(),
assertions
)
}
private fun <T : IterableLike, E> transformToList(
container: AssertionContainer<T>,
converter: (T) -> Iterable<E>
): List<E> =
container.maybeSubject.fold({ emptyList() }) { subject ->
val iterable = converter(subject)
when (iterable) {
is List<E> -> iterable
else -> iterable.toList()
}
}
private fun <E> createHasElementPlusFixedClaimGroup(
list: List<E>,
description: Translatable,
representation: IterableLike,
claim: Boolean,
assertions: List<Assertion>
) = assertionBuilder.invisibleGroup
.withAssertions(
createHasElementAssertion(list),
assertionBuilder.fixedClaimGroup
.withListType
.withClaim(claim)
.withDescriptionAndRepresentation(description, representation)
decorateAssertionWithHasNext(
assertionBuilder.list
.withDescriptionAndEmptyRepresentation(DescriptionIterableAssertion.ALL)
.withAssertions(assertions)
.build()
.build(),
listAssertionContainer
)
.build()
}
override fun <T : IterableLike, E> containsNoDuplicates(
container: AssertionContainer<T>,
converter: (T) -> Iterable<E>
): Assertion = LazyThreadUnsafeAssertionGroup {
val list = transformToList(container, converter)
val listAssertionContainer = turnSubjectToList(container, converter)
val list = listAssertionContainer.maybeSubject.getOrElse { emptyList() }
val lookupHashMap = HashMap<E, Int>()
val duplicateIndices = HashMap<Int, Pair<E, MutableList<Int>>>()
@@ -163,7 +131,7 @@ class DefaultIterableLikeAssertions : IterableLikeAssertions {
val duplicates = duplicateIndices
.map { (index, pair) ->
pair.let { (element, duplicateIndices) ->
val (element, indices) = pair
assertionBuilder.descriptive
.failing
.withFailureHint {
@@ -172,7 +140,7 @@ class DefaultIterableLikeAssertions : IterableLikeAssertions {
.withExplanatoryAssertion(
TranslatableWithArgs(
DescriptionIterableAssertion.DUPLICATED_BY,
duplicateIndices.joinToString(", ")
indices.joinToString(", ")
)
)
.build()
@@ -184,13 +152,14 @@ class DefaultIterableLikeAssertions : IterableLikeAssertions {
)
.build()
}
}
createHasElementPlusFixedClaimGroup(
list,
DescriptionBasic.HAS_NOT, DescriptionIterableAssertion.DUPLICATE_ELEMENTS,
duplicates.isEmpty(),
decorateAssertionWithHasNext(
createAssertionGroupFromListOfAssertions(
DescriptionBasic.HAS_NOT,
DescriptionIterableAssertion.DUPLICATE_ELEMENTS,
duplicates
),
listAssertionContainer
)
}
}

View File

@@ -3,6 +3,7 @@ package ch.tutteli.atrium.logic.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.None
import ch.tutteli.atrium.core.Some
import ch.tutteli.atrium.core.falseProvider
@@ -11,22 +12,16 @@ import ch.tutteli.atrium.creating.AssertionContainer
import ch.tutteli.atrium.creating.Expect
import ch.tutteli.atrium.logic.collectBasedOnSubject
import ch.tutteli.atrium.logic.creating.collectors.collectAssertions
import ch.tutteli.atrium.logic.hasNext
import ch.tutteli.atrium.reporting.Text
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.DescriptionIterableAssertion
import ch.tutteli.kbox.WithIndex
import ch.tutteli.kbox.identity
import ch.tutteli.kbox.mapWithIndex
internal fun createHasElementAssertion(list: List<*>): Assertion {
return assertionBuilder.feature
.withDescriptionAndRepresentation(DescriptionIterableAssertion.HAS_ELEMENT, Text(list.isNotEmpty().toString()))
.withAssertion(
assertionBuilder.createDescriptive(DescriptionBasic.IS, Text(true.toString())) { list.isNotEmpty() }
)
.build()
}
internal fun <E : Any> allCreatedAssertionsHold(
container: AssertionContainer<*>,
subject: E?,
@@ -76,7 +71,7 @@ internal fun <E> createIndexAssertions(
internal fun createExplanatoryGroupForMismatches(
mismatches: List<Assertion>
) : AssertionGroup {
): AssertionGroup {
return assertionBuilder.explanatoryGroup
.withWarningType
.withAssertion(
@@ -88,3 +83,39 @@ internal fun createExplanatoryGroupForMismatches(
.failing
.build()
}
internal fun createAssertionGroupFromListOfAssertions(
description: Translatable,
representation: Any?,
assertions: List<Assertion>
): AssertionGroup =
if (assertions.isEmpty())
assertionBuilder.invisibleGroup
.withAssertion(
assertionBuilder.createDescriptive(description, representation, trueProvider)
).build()
else assertionBuilder.list
.withDescriptionAndRepresentation(description, representation)
.withAssertions(assertions)
.build()
internal fun <E> decorateAssertionWithHasNext(
assertion: AssertionGroup,
listAssertionContainer: AssertionContainer<List<E>>
): AssertionGroup {
val hasNext = listAssertionContainer.hasNext(::identity)
return if (hasNext.holds()) {
assertion
} else {
assertionBuilder.invisibleGroup
.withAssertions(
hasNext,
assertionBuilder.explanatoryGroup
.withDefaultType
.withAssertion(assertion)
.build()
)
.build()
}
}

View File

@@ -44,6 +44,8 @@ abstract class IterableExpectationsSpec(
val nextElementDescr = DescriptionIterableAssertion.NEXT_ELEMENT.getDefault()
val duplicateElements = DescriptionIterableAssertion.DUPLICATE_ELEMENTS.getDefault()
val hasANextElement = "$hasDescr: $nextElementDescr"
describeFun(toHaveANextElement) {
val toHaveANextElementFun = toHaveANextElement.lambda
@@ -54,7 +56,7 @@ abstract class IterableExpectationsSpec(
it("throws an AssertionError if an iterable does not have next") {
expect {
expect(listOf<Int>() as Iterable<Int>).toHaveANextElementFun()
}.toThrow<AssertionError> { messageToContain("$hasDescr: $nextElementDescr") }
}.toThrow<AssertionError> { messageToContain(hasANextElement) }
}
}
@@ -132,11 +134,29 @@ abstract class IterableExpectationsSpec(
describeFun(notToContainDuplicates) {
val notToContainDuplicatesFun = notToContainDuplicates.lambda
it("list without duplicates") {
expect(listOf(1, 2) as Iterable<Int>).notToContainDuplicatesFun()
describe("empty collection") {
it("throws AssertionError as there needs to be at least one element") {
expect {
expect(listOf<Int>() as Iterable<Int>).notToContainDuplicatesFun()
}.toThrow<AssertionError> {
message {
toContain(
"$hasANextElement",
"$hasNotDescr: $duplicateElements"
)
}
}
}
}
it("list with duplicates") {
describe("list without duplicates") {
it("happy case") {
expect(listOf(1, 2) as Iterable<Int>).notToContainDuplicatesFun()
}
}
describe("list with duplicates") {
it("throws AssertionError with details of duplicate indices") {
fun index(i: Int, element: Int) = DescriptionIterableAssertion.INDEX.getDefault().format("$i: $element")
fun duplicatedBy(vararg elements: Int) =
DescriptionIterableAssertion.DUPLICATED_BY.getDefault().format(elements.joinToString(", "))
@@ -154,4 +174,5 @@ abstract class IterableExpectationsSpec(
}
}
}
}
})

View File

@@ -30,7 +30,6 @@ abstract class IterableToHaveElementsAndAllExpectationsSpec(
) {})
val toHaveElementsAndAllDescr = DescriptionIterableAssertion.ALL.getDefault()
val hasElement = DescriptionIterableAssertion.HAS_ELEMENT.getDefault()
val explanatoryPointWithIndent = "$indentRootBulletPoint$indentListBulletPoint$explanatoryBulletPoint"
@@ -47,9 +46,10 @@ abstract class IterableToHaveElementsAndAllExpectationsSpec(
expect {
expect(fluentEmpty()).toHaveElementsAndAllFun { toBeLessThan(1.0) }
}.toThrow<AssertionError> {
messageToContain(
"$rootBulletPoint$featureArrow$hasElement: false$separator" +
"$indentRootBulletPoint$indentFeatureArrow$featureBulletPoint$isDescr: true"
message.toContainRegex(
"$hasANextElement",
"$explanatoryBulletPoint$toHaveElementsAndAllDescr: ",
"$explanatoryPointWithIndent$toBeLessThanDescr: 1.0"
)
}
}

View File

@@ -40,7 +40,9 @@ buildscript {
or(
"(CharSequence|Iterable)Contains.*Spec",
"IterableAnyAssertionsSpec"
) + ".*`.*(any|contains).*`.*(throws.*AssertionError|failing cases)"
) + ".*`.*(any|contains).*`.*(throws.*AssertionError|failing cases)",
// changed reporting for Iterable.all empty collection cases with 0.17.0
"IterableAllAssertionsSpec.*" + "empty collection.*" + "throws AssertionError"
) + ".*)",
// we don't use asci bullet points in reporting since 0.17.0
// but have own tests to assure that changing bullet points work
@@ -108,7 +110,9 @@ buildscript {
or(
"(CharSequence|Iterable)Contains.*Spec",
"IterableAnyAssertionsSpec"
) + ".*`.*(any|contains).*`.*(throws.*AssertionError|failing cases)"
) + ".*`.*(any|contains).*`.*(throws.*AssertionError|failing cases)",
// changed reporting for Iterable.all empty collection cases with 0.17.0
"IterableAllAssertionsSpec.*" + "empty collection.*" + "throws AssertionError"
) + ".*)",
// we don't use asci bullet points in reporting since 0.17.0
// but have own tests to assure that changing bullet points work
@@ -177,7 +181,9 @@ buildscript {
or(
"(CharSequence|Iterable)Contains.*Spec",
"IterableAnyExpectationsSpec"
) + ".*`.*(any|contains).*`.*(throws.*AssertionError|failing cases)"
) + ".*`.*(any|contains).*`.*(throws.*AssertionError|failing cases)",
// changed reporting for Iterable.all empty collection cases with 0.17.0
"IterableAllExpectationsSpec.*" + "empty collection.*" + "throws AssertionError"
) + ".*)").let { commonPatterns ->
Pair(
// bc