Skip to content

General index consistency tests #39

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

Merged
merged 6 commits into from
Oct 30, 2020
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
90 changes: 11 additions & 79 deletions Tests/SwiftAlgorithmsTests/ChainTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<A, B>(atOffset offset: Int, in chain: Chain2<A, B>) -> Chain2<A, B>.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))
Expand All @@ -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<Int>, 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() {
Expand All @@ -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)
}
}
}
17 changes: 16 additions & 1 deletion Tests/SwiftAlgorithmsTests/ProductTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//===----------------------------------------------------------------------===//

import XCTest
import Algorithms
@testable import Algorithms

final class ProductTests: XCTestCase {
func testProduct() {
Expand All @@ -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)]
})
}
}
190 changes: 190 additions & 0 deletions Tests/SwiftAlgorithmsTests/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,193 @@ func XCTAssertEqualSequences<S1: Sequence, S2: Sequence>(
}

func XCTAssertLazy<S: LazySequenceProtocol>(_: 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<C>(
_ 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)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a check that c.count equals c.indices.count here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find!


// 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[..<offsetA].enumerated() {
XCTAssertNotEqual(
a, b,
"Index at offset \(offsetA) equals index at offset \(offsetB)",
file: file, line: line)
XCTAssertLessThan(
b, a,
"""
Index at offset \(offsetB) is not less than index at offset \(offsetA)
""",
file: file, line: line)
}
}

// `index(_:offsetBy:)` and `distance(from:to:)`
for (startOffset, start) in indicesIncludingEnd.enumerated() {
for (endOffset, end) in indicesIncludingEnd.enumerated() {
let distance = endOffset - startOffset

XCTAssertEqual(
c.index(start, offsetBy: distance), end,
"""
Index at offset \(startOffset) offset by \(distance) does not equal \
index at offset \(endOffset)
""",
file: file, line: line)
XCTAssertEqual(
c.distance(from: start, to: end), distance,
"""
Distance from index at offset \(startOffset) to index at offset \
\(endOffset) does not equal \(distance)
""",
file: file, line: line)
}
}

// `index(_:offsetBy:limitedBy:)`
for (startOffset, start) in indicesIncludingEnd.enumerated() {
for (limitOffset, limit) in indicesIncludingEnd.enumerated() {
// verifies that the target index corresponding to each offset in
// `range` can or cannot be reached from `start` using
// `chain.index(start, offsetBy: _, limitedBy: limit)`, depending on the
// value of `pastLimit`
func checkTargetRange(_ range: ClosedRange<Int>, 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)
}
}
}
}
}