All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.kotest.matchers.collections.CollectionMatchers.kt Maven / Gradle / Ivy

package io.kotest.matchers.collections

import io.kotest.assertions.ErrorCollectionMode
import io.kotest.assertions.errorCollector
import io.kotest.assertions.print.print
import io.kotest.assertions.runWithMode
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.neverNullMatcher

fun  existInOrder(vararg ps: (T) -> Boolean): Matcher?> = existInOrder(ps.asList())

/**
 * Assert that a collections contains a subsequence that matches the given subsequence of predicates, possibly with
 * values in between.
 */
fun  existInOrder(predicates: List<(T) -> Boolean>): Matcher?> = neverNullMatcher { actual ->
   require(predicates.isNotEmpty()) { "predicates must not be empty" }

   var subsequenceIndex = 0
   val actualIterator = actual.iterator()

   while (actualIterator.hasNext() && subsequenceIndex < predicates.size) {
      if (predicates[subsequenceIndex](actualIterator.next())) subsequenceIndex += 1
   }

   MatcherResult(
      subsequenceIndex == predicates.size,
      { "${actual.print().value} did not match the predicates ${predicates.print().value} in order" },
      { "${actual.print().value} should not match the predicates ${predicates.print().value} in order" }
   )
}

fun  haveSize(size: Int): Matcher> = haveSizeMatcher(size)

fun  singleElement(t: T): Matcher> = object : Matcher> {
   override fun test(value: Collection) = MatcherResult(
      value.size == 1 && value.first() == t,
      { "Collection should be a single element of $t but has ${value.size} elements: ${value.print().value}" },
      { "Collection should not be a single element of $t" }
   )
}

fun  singleElement(p: (T) -> Boolean): Matcher> = object : Matcher> {
   override fun test(value: Collection): MatcherResult {
      val filteredValue: List = value.filter(p)
      return MatcherResult(
         filteredValue.size == 1,
         { "Collection should have a single element by a given predicate but has ${filteredValue.size} elements: ${value.print().value}" },
         { "Collection should not have a single element by a given predicate" }
      )
   }
}

fun > beSorted(): Matcher> = sorted()
fun > sorted(): Matcher> = sortedBy { it }

fun > beSortedBy(transform: (T) -> E): Matcher> = sortedBy(transform)
fun > sortedBy(transform: (T) -> E): Matcher> = object : Matcher> {
   override fun test(value: List): MatcherResult {
      val failure = value.withIndex().firstOrNull { (i, it) -> i != value.lastIndex && transform(it) > transform(value[i + 1]) }
      val elementMessage = when (failure) {
         null -> ""
         else -> ". Element ${failure.value} at index ${failure.index} was greater than element ${value[failure.index + 1]}"
      }
      return MatcherResult(
         failure == null,
         { "List ${value.print().value} should be sorted$elementMessage" },
         { "List ${value.print().value} should not be sorted" }
      )
   }
}

fun  matchEach(vararg fns: (T) -> Unit): Matcher?> = matchEach(fns.asList())
fun  matchInOrder(vararg fns: (T) -> Unit): Matcher?> = matchInOrder(fns.asList(), allowGaps = false)
fun  matchInOrderSubset(vararg fns: (T) -> Unit): Matcher?> = matchInOrder(fns.asList(), allowGaps = true)

/**
 * Assert that a [Collection] contains a subsequence that matches the given assertions. Failing elements may occur
 * between passing ones, if [allowGaps] is set to true
 */
fun  matchInOrder(assertions: List<(T) -> Unit>, allowGaps: Boolean): Matcher?> = neverNullMatcher { actual ->
   val originalMode = errorCollector.getCollectionMode()
   try {
      data class MatchInOrderSubsetProblem(
         val atIndex: Int,
         val problem: String,
      )

      data class MatchInOrderSubsetResult(
         val startIndex: Int,
         val elementsPassed: Int,
         val problems: List
      )

      val actualAsList = actual.toList()

      var allPassed = false
      var bestResult: MatchInOrderSubsetResult? = null

      for (startIndex in 0..(actual.size - assertions.size)) {
         var elementsPassed = 0
         var elementsTested = 0
         val currentProblems = ArrayList()

         while (startIndex + elementsTested < actual.size) {
            if (bestResult == null || elementsPassed > bestResult.elementsPassed) {
               bestResult = MatchInOrderSubsetResult(startIndex, elementsPassed, currentProblems)
            }

            if (!allowGaps && elementsTested > elementsPassed) break

            val elementResult = runCatching {
               assertions[elementsPassed](actualAsList[startIndex + elementsTested])
            }

            if (elementResult.isSuccess) {
               elementsPassed++
               currentProblems.clear()
               if (elementsPassed == assertions.size) {
                  allPassed = true
                  break
               }
            } else {
               currentProblems.add(
                  MatchInOrderSubsetProblem(
                     startIndex + elementsTested,
                     elementResult.exceptionOrNull()!!.message!!
                  )
               )
            }

            elementsTested++
         }

         if (allPassed) break
      }

      MatcherResult(
         allPassed,
         {
            """
            |Expected a sequence of elements to pass the assertions, ${if (allowGaps) "possibly with gaps between " else ""}but failed to match all assertions
            |
            |Best result when comparing from index [${bestResult?.startIndex}], where ${bestResult?.elementsPassed} elements passed, but the following elements failed:
            |
            ${
               bestResult?.problems?.joinToString("\n") { problem ->
                  "|${problem.atIndex} => ${problem.problem}"
               }
            }
            """.trimMargin()
         },
         { "Expected some assertion to fail but all passed" }
      )
   } finally {
      errorCollector.setCollectionMode(originalMode)
   }
}

/**
 * Asserts that each element in the collection matches its corresponding matcher in [assertions].
 * Elements will be compared sequentially in the order given by the iterators of the collections.
 */
fun  matchEach(assertions: List<(T) -> Unit>): Matcher?> = neverNullMatcher { actual ->
   data class ElementPass(val atIndex: Int)
   data class MatchEachProblem(val atIndex: Int, val problem: String?)

   val problems = errorCollector.runWithMode(ErrorCollectionMode.Hard) {
      actual.mapIndexedNotNull { index, element ->
         if (index !in assertions.indices) {
            MatchEachProblem(index, "Element has no corresponding assertion. Only ${assertions.size} assertions provided")
         } else {
            runCatching {
               assertions[index](element)
            }.exceptionOrNull()?.let { exception ->
               MatchEachProblem(index, exception.message)
            }
         }
      }
   } + (actual.size..assertions.size - 1).map {
      MatchEachProblem(
         it,
         "No actual element for assertion at index $it"
      )
   }

   MatcherResult(
      problems.isEmpty(),
      {
         "Expected each element to pass its assertion, but found issues at indexes: [${problems.joinToString { it.atIndex.toString() }}]\n\n" +
            "${problems.joinToString(separator = "\n") { "${it.atIndex} => ${it.problem}" }}"
      },
      { "Expected some element to fail its assertion, but all passed." },
   )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy