diff --git a/Tests/SwiftAlgorithmsTests/ChainTests.swift b/Tests/SwiftAlgorithmsTests/ChainTests.swift
index b04b89eb..104a6a26 100644
--- a/Tests/SwiftAlgorithmsTests/ChainTests.swift
+++ b/Tests/SwiftAlgorithmsTests/ChainTests.swift
@@ -13,14 +13,6 @@ import XCTest
@testable import Algorithms
final class ChainTests: XCTestCase {
- // intentionally does not depend on `Chain.index(_:offsetBy:)` in order to
- // avoid making assumptions about the code being tested
- func index(atOffset offset: Int, in chain: Chain2) -> Chain2.Index {
- offset < chain.base1.count
- ? .init(first: chain.base1.index(chain.base1.startIndex, offsetBy: offset))
- : .init(second: chain.base2.index(chain.base2.startIndex, offsetBy: offset - chain.base1.count))
- }
-
func testChainSequences() {
let run = chain((1...).prefix(10), 20...)
XCTAssertEqualSequences(run.prefix(20), Array(1...10) + (20..<30))
@@ -43,62 +35,17 @@ final class ChainTests: XCTestCase {
XCTAssertEqualSequences(chain(s1.reversed(), s2), "JIHGFEDCBAklmnopqrstuv")
}
- func testChainIndexOffsetBy() {
- let s1 = "abcde"
- let s2 = "VWXYZ"
- let c = chain(s1, s2)
-
- for (startOffset, endOffset) in product(0...c.count, 0...c.count) {
- let start = index(atOffset: startOffset, in: c)
- let end = index(atOffset: endOffset, in: c)
- let distance = endOffset - startOffset
- XCTAssertEqual(c.index(start, offsetBy: distance), end)
- }
- }
-
- func testChainIndexOffsetByLimitedBy() {
- let s1 = "abcd"
- let s2 = "XYZ"
- let c = chain(s1, s2)
-
- for (startOffset, limitOffset) in product(0...c.count, 0...c.count) {
- let start = index(atOffset: startOffset, in: c)
- let limit = index(atOffset: limitOffset, in: c)
-
- // verifies that the target index corresponding to each offset in `range`
- // can or cannot be reached from `start` using
- // `c.index(start, offsetBy: _, limitedBy: limit)`, depending on the
- // value of `beyondLimit`
- func checkTargetRange(_ range: ClosedRange, beyondLimit: Bool) {
- for targetOffset in range {
- let distance = targetOffset - startOffset
-
- XCTAssertEqual(
- c.index(start, offsetBy: distance, limitedBy: limit),
- beyondLimit ? nil : index(atOffset: targetOffset, in: c))
- }
- }
-
- // forward
- if limit >= start {
- // the limit has an effect
- checkTargetRange(startOffset...limitOffset, beyondLimit: false)
- checkTargetRange((limitOffset + 1)...(c.count + 1), beyondLimit: true)
- } else {
- // the limit has no effect
- checkTargetRange(startOffset...c.count, beyondLimit: false)
- }
-
- // backward
- if limit <= start {
- // the limit has an effect
- checkTargetRange(limitOffset...startOffset, beyondLimit: false)
- checkTargetRange(-1...(limitOffset - 1), beyondLimit: true)
- } else {
- // the limit has no effect
- checkTargetRange(0...startOffset, beyondLimit: false)
- }
- }
+ func testChainIndexTraversals() {
+ validateIndexTraversals(
+ chain("abcd", "XYZ"),
+ chain("abcd", ""),
+ chain("", "XYZ"),
+ chain("", ""),
+ indices: { chain in
+ chain.base1.indices.map { .init(first: $0) }
+ + chain.base2.indices.map { .init(second: $0) }
+ + [.init(second: chain.base2.endIndex)]
+ })
}
func testChainIndexOffsetAcrossBoundary() {
@@ -121,19 +68,4 @@ final class ChainTests: XCTestCase {
XCTAssertNil(j)
}
}
-
- func testChainDistanceFromTo() {
- let s1 = "abcde"
- let s2 = "VWXYZ"
- let c = chain(s1, s2)
-
- XCTAssertEqual(c.count, s1.count + s2.count)
-
- for (startOffset, endOffset) in product(0...c.count, 0...c.count) {
- let start = index(atOffset: startOffset, in: c)
- let end = index(atOffset: endOffset, in: c)
- let distance = endOffset - startOffset
- XCTAssertEqual(c.distance(from: start, to: end), distance)
- }
- }
}
diff --git a/Tests/SwiftAlgorithmsTests/ProductTests.swift b/Tests/SwiftAlgorithmsTests/ProductTests.swift
index 4fa02a51..37b434fa 100644
--- a/Tests/SwiftAlgorithmsTests/ProductTests.swift
+++ b/Tests/SwiftAlgorithmsTests/ProductTests.swift
@@ -10,7 +10,7 @@
//===----------------------------------------------------------------------===//
import XCTest
-import Algorithms
+@testable import Algorithms
final class ProductTests: XCTestCase {
func testProduct() {
@@ -37,4 +37,19 @@ final class ProductTests: XCTestCase {
let p = product([1, 2], "abc")
XCTAssertEqual(p.distance(from: p.startIndex, to: p.endIndex), 6)
}
+
+ func testProductIndexTraversals() {
+ validateIndexTraversals(
+ product([1, 2, 3, 4], "abc"),
+ product([1, 2, 3, 4], ""),
+ product([], "abc"),
+ product([], ""),
+ indices: { product in
+ product.base1.indices.flatMap { i1 in
+ product.base2.indices.map { i2 in
+ .init(i1: i1, i2: i2)
+ }
+ } + [.init(i1: product.base1.endIndex, i2: product.base2.startIndex)]
+ })
+ }
}
diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift
index 27c9c41b..5d5b452f 100644
--- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift
+++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift
@@ -66,3 +66,193 @@ func XCTAssertEqualSequences(
}
func XCTAssertLazy(_: S) {}
+
+/// Tests that all index traversal methods behave as expected.
+///
+/// Verifies the correctness of the implementations of `startIndex`, `endIndex`,
+/// `indices`, `count`, `isEmpty`, `index(before:)`, `index(after:)`,
+/// `index(_:offsetBy:)`, `index(_:offsetBy:limitedBy:)`, and
+/// `distance(from:to:)` by calling them with just about all possible input
+/// combinations. When provided, the `indices` function is used to to test the
+/// collection methods against.
+///
+/// - Parameters:
+/// - collections: The collections to be validated.
+/// - indices: A closure that returns the expected indices of the given
+/// collection, including its `endIndex`, in ascending order. Only use this
+/// parameter if you are able to compute the indices of the collection
+/// independently of the `Collection` conformance, e.g. by using the
+/// contents of the collection directly.
+///
+/// - Complexity: O(*n*^3) for each collection, where *n* is the length of the
+/// collection.
+func validateIndexTraversals(
+ _ collections: C...,
+ indices: ((C) -> [C.Index])? = nil,
+ file: StaticString = #file, line: UInt = #line
+) where C: BidirectionalCollection {
+ for c in collections {
+ let indicesIncludingEnd = indices?(c) ?? (c.indices + [c.endIndex])
+ let count = indicesIncludingEnd.count - 1
+
+ XCTAssertEqual(
+ c.count, count,
+ "Count mismatch",
+ file: file, line: line)
+ XCTAssertEqual(
+ c.isEmpty, count == 0,
+ "Emptiness mismatch",
+ file: file, line: line)
+ XCTAssertEqual(
+ c.startIndex, indicesIncludingEnd.first,
+ "`startIndex` does not equal the first index",
+ file: file, line: line)
+ XCTAssertEqual(
+ c.endIndex, indicesIncludingEnd.last,
+ "`endIndex` does not equal the last index",
+ file: file, line: line)
+
+ // `index(after:)`
+ do {
+ var index = c.startIndex
+
+ for (offset, expected) in indicesIncludingEnd.enumerated().dropFirst() {
+ c.formIndex(after: &index)
+ XCTAssertEqual(
+ index, expected,
+ """
+ `startIndex` incremented \(offset) times does not equal index at \
+ offset \(offset)
+ """,
+ file: file, line: line)
+ }
+ }
+
+ // `index(before:)`
+ do {
+ var index = c.endIndex
+
+ for (offset, expected) in indicesIncludingEnd.enumerated().dropLast().reversed() {
+ c.formIndex(before: &index)
+ XCTAssertEqual(
+ index, expected,
+ """
+ `endIndex` decremented \(count - offset) times does not equal index \
+ at offset \(offset)
+ """,
+ file: file, line: line)
+ }
+ }
+
+ // `indices`
+ XCTAssertEqual(c.indices.count, count)
+ for (offset, index) in c.indices.enumerated() {
+ XCTAssertEqual(
+ index, indicesIncludingEnd[offset],
+ "Index mismatch at offset \(offset) in `indices`",
+ file: file, line: line)
+ }
+
+ // index comparison
+ for (offsetA, a) in indicesIncludingEnd.enumerated() {
+ XCTAssertEqual(
+ a, a,
+ "Index at offset \(offsetA) does not equal itself",
+ file: file, line: line)
+ XCTAssertFalse(
+ a < a,
+ "Index at offset \(offsetA) is less than itself",
+ file: file, line: line)
+
+ for (offsetB, b) in indicesIncludingEnd[.., pastLimit: Bool) {
+ for targetOffset in range {
+ let distance = targetOffset - startOffset
+ let end = c.index(start, offsetBy: distance, limitedBy: limit)
+
+ if pastLimit {
+ XCTAssertNil(
+ end,
+ """
+ Index at offset \(startOffset) offset by \(distance) limited \
+ by index at offset \(limitOffset) does not equal `nil`
+ """,
+ file: file, line: line)
+ } else {
+ XCTAssertEqual(
+ end, indicesIncludingEnd[targetOffset],
+ """
+ Index at offset \(startOffset) offset by \(distance) limited \
+ by index at offset \(limitOffset) does not equal index at \
+ offset \(targetOffset)
+ """,
+ file: file, line: line)
+ }
+ }
+ }
+
+ // forward offsets
+ if limit >= start {
+ // the limit has an effect
+ checkTargetRange(startOffset...limitOffset, pastLimit: false)
+ checkTargetRange((limitOffset + 1)...(count + 1), pastLimit: true)
+ } else {
+ // the limit has no effect
+ checkTargetRange(startOffset...count, pastLimit: false)
+ }
+
+ // backward offsets
+ if limit <= start {
+ // the limit has an effect
+ checkTargetRange(limitOffset...startOffset, pastLimit: false)
+ checkTargetRange(-1...(limitOffset - 1), pastLimit: true)
+ } else {
+ // the limit has no effect
+ checkTargetRange(0...startOffset, pastLimit: false)
+ }
+ }
+ }
+ }
+}