Skip to content

Commit 34c0a0b

Browse files
authored
General index consistency tests (#39)
* Add the `validateIndexTraversals` function
1 parent 66ec968 commit 34c0a0b

File tree

3 files changed

+217
-80
lines changed

3 files changed

+217
-80
lines changed

Tests/SwiftAlgorithmsTests/ChainTests.swift

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ import XCTest
1313
@testable import Algorithms
1414

1515
final class ChainTests: XCTestCase {
16-
// intentionally does not depend on `Chain.index(_:offsetBy:)` in order to
17-
// avoid making assumptions about the code being tested
18-
func index<A, B>(atOffset offset: Int, in chain: Chain2<A, B>) -> Chain2<A, B>.Index {
19-
offset < chain.base1.count
20-
? .init(first: chain.base1.index(chain.base1.startIndex, offsetBy: offset))
21-
: .init(second: chain.base2.index(chain.base2.startIndex, offsetBy: offset - chain.base1.count))
22-
}
23-
2416
func testChainSequences() {
2517
let run = chain((1...).prefix(10), 20...)
2618
XCTAssertEqualSequences(run.prefix(20), Array(1...10) + (20..<30))
@@ -43,62 +35,17 @@ final class ChainTests: XCTestCase {
4335
XCTAssertEqualSequences(chain(s1.reversed(), s2), "JIHGFEDCBAklmnopqrstuv")
4436
}
4537

46-
func testChainIndexOffsetBy() {
47-
let s1 = "abcde"
48-
let s2 = "VWXYZ"
49-
let c = chain(s1, s2)
50-
51-
for (startOffset, endOffset) in product(0...c.count, 0...c.count) {
52-
let start = index(atOffset: startOffset, in: c)
53-
let end = index(atOffset: endOffset, in: c)
54-
let distance = endOffset - startOffset
55-
XCTAssertEqual(c.index(start, offsetBy: distance), end)
56-
}
57-
}
58-
59-
func testChainIndexOffsetByLimitedBy() {
60-
let s1 = "abcd"
61-
let s2 = "XYZ"
62-
let c = chain(s1, s2)
63-
64-
for (startOffset, limitOffset) in product(0...c.count, 0...c.count) {
65-
let start = index(atOffset: startOffset, in: c)
66-
let limit = index(atOffset: limitOffset, in: c)
67-
68-
// verifies that the target index corresponding to each offset in `range`
69-
// can or cannot be reached from `start` using
70-
// `c.index(start, offsetBy: _, limitedBy: limit)`, depending on the
71-
// value of `beyondLimit`
72-
func checkTargetRange(_ range: ClosedRange<Int>, beyondLimit: Bool) {
73-
for targetOffset in range {
74-
let distance = targetOffset - startOffset
75-
76-
XCTAssertEqual(
77-
c.index(start, offsetBy: distance, limitedBy: limit),
78-
beyondLimit ? nil : index(atOffset: targetOffset, in: c))
79-
}
80-
}
81-
82-
// forward
83-
if limit >= start {
84-
// the limit has an effect
85-
checkTargetRange(startOffset...limitOffset, beyondLimit: false)
86-
checkTargetRange((limitOffset + 1)...(c.count + 1), beyondLimit: true)
87-
} else {
88-
// the limit has no effect
89-
checkTargetRange(startOffset...c.count, beyondLimit: false)
90-
}
91-
92-
// backward
93-
if limit <= start {
94-
// the limit has an effect
95-
checkTargetRange(limitOffset...startOffset, beyondLimit: false)
96-
checkTargetRange(-1...(limitOffset - 1), beyondLimit: true)
97-
} else {
98-
// the limit has no effect
99-
checkTargetRange(0...startOffset, beyondLimit: false)
100-
}
101-
}
38+
func testChainIndexTraversals() {
39+
validateIndexTraversals(
40+
chain("abcd", "XYZ"),
41+
chain("abcd", ""),
42+
chain("", "XYZ"),
43+
chain("", ""),
44+
indices: { chain in
45+
chain.base1.indices.map { .init(first: $0) }
46+
+ chain.base2.indices.map { .init(second: $0) }
47+
+ [.init(second: chain.base2.endIndex)]
48+
})
10249
}
10350

10451
func testChainIndexOffsetAcrossBoundary() {
@@ -121,19 +68,4 @@ final class ChainTests: XCTestCase {
12168
XCTAssertNil(j)
12269
}
12370
}
124-
125-
func testChainDistanceFromTo() {
126-
let s1 = "abcde"
127-
let s2 = "VWXYZ"
128-
let c = chain(s1, s2)
129-
130-
XCTAssertEqual(c.count, s1.count + s2.count)
131-
132-
for (startOffset, endOffset) in product(0...c.count, 0...c.count) {
133-
let start = index(atOffset: startOffset, in: c)
134-
let end = index(atOffset: endOffset, in: c)
135-
let distance = endOffset - startOffset
136-
XCTAssertEqual(c.distance(from: start, to: end), distance)
137-
}
138-
}
13971
}

Tests/SwiftAlgorithmsTests/ProductTests.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
import XCTest
13-
import Algorithms
13+
@testable import Algorithms
1414

1515
final class ProductTests: XCTestCase {
1616
func testProduct() {
@@ -37,4 +37,19 @@ final class ProductTests: XCTestCase {
3737
let p = product([1, 2], "abc")
3838
XCTAssertEqual(p.distance(from: p.startIndex, to: p.endIndex), 6)
3939
}
40+
41+
func testProductIndexTraversals() {
42+
validateIndexTraversals(
43+
product([1, 2, 3, 4], "abc"),
44+
product([1, 2, 3, 4], ""),
45+
product([], "abc"),
46+
product([], ""),
47+
indices: { product in
48+
product.base1.indices.flatMap { i1 in
49+
product.base2.indices.map { i2 in
50+
.init(i1: i1, i2: i2)
51+
}
52+
} + [.init(i1: product.base1.endIndex, i2: product.base2.startIndex)]
53+
})
54+
}
4055
}

Tests/SwiftAlgorithmsTests/TestUtilities.swift

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,193 @@ func XCTAssertEqualSequences<S1: Sequence, S2: Sequence>(
6666
}
6767

6868
func XCTAssertLazy<S: LazySequenceProtocol>(_: S) {}
69+
70+
/// Tests that all index traversal methods behave as expected.
71+
///
72+
/// Verifies the correctness of the implementations of `startIndex`, `endIndex`,
73+
/// `indices`, `count`, `isEmpty`, `index(before:)`, `index(after:)`,
74+
/// `index(_:offsetBy:)`, `index(_:offsetBy:limitedBy:)`, and
75+
/// `distance(from:to:)` by calling them with just about all possible input
76+
/// combinations. When provided, the `indices` function is used to to test the
77+
/// collection methods against.
78+
///
79+
/// - Parameters:
80+
/// - collections: The collections to be validated.
81+
/// - indices: A closure that returns the expected indices of the given
82+
/// collection, including its `endIndex`, in ascending order. Only use this
83+
/// parameter if you are able to compute the indices of the collection
84+
/// independently of the `Collection` conformance, e.g. by using the
85+
/// contents of the collection directly.
86+
///
87+
/// - Complexity: O(*n*^3) for each collection, where *n* is the length of the
88+
/// collection.
89+
func validateIndexTraversals<C>(
90+
_ collections: C...,
91+
indices: ((C) -> [C.Index])? = nil,
92+
file: StaticString = #file, line: UInt = #line
93+
) where C: BidirectionalCollection {
94+
for c in collections {
95+
let indicesIncludingEnd = indices?(c) ?? (c.indices + [c.endIndex])
96+
let count = indicesIncludingEnd.count - 1
97+
98+
XCTAssertEqual(
99+
c.count, count,
100+
"Count mismatch",
101+
file: file, line: line)
102+
XCTAssertEqual(
103+
c.isEmpty, count == 0,
104+
"Emptiness mismatch",
105+
file: file, line: line)
106+
XCTAssertEqual(
107+
c.startIndex, indicesIncludingEnd.first,
108+
"`startIndex` does not equal the first index",
109+
file: file, line: line)
110+
XCTAssertEqual(
111+
c.endIndex, indicesIncludingEnd.last,
112+
"`endIndex` does not equal the last index",
113+
file: file, line: line)
114+
115+
// `index(after:)`
116+
do {
117+
var index = c.startIndex
118+
119+
for (offset, expected) in indicesIncludingEnd.enumerated().dropFirst() {
120+
c.formIndex(after: &index)
121+
XCTAssertEqual(
122+
index, expected,
123+
"""
124+
`startIndex` incremented \(offset) times does not equal index at \
125+
offset \(offset)
126+
""",
127+
file: file, line: line)
128+
}
129+
}
130+
131+
// `index(before:)`
132+
do {
133+
var index = c.endIndex
134+
135+
for (offset, expected) in indicesIncludingEnd.enumerated().dropLast().reversed() {
136+
c.formIndex(before: &index)
137+
XCTAssertEqual(
138+
index, expected,
139+
"""
140+
`endIndex` decremented \(count - offset) times does not equal index \
141+
at offset \(offset)
142+
""",
143+
file: file, line: line)
144+
}
145+
}
146+
147+
// `indices`
148+
XCTAssertEqual(c.indices.count, count)
149+
for (offset, index) in c.indices.enumerated() {
150+
XCTAssertEqual(
151+
index, indicesIncludingEnd[offset],
152+
"Index mismatch at offset \(offset) in `indices`",
153+
file: file, line: line)
154+
}
155+
156+
// index comparison
157+
for (offsetA, a) in indicesIncludingEnd.enumerated() {
158+
XCTAssertEqual(
159+
a, a,
160+
"Index at offset \(offsetA) does not equal itself",
161+
file: file, line: line)
162+
XCTAssertFalse(
163+
a < a,
164+
"Index at offset \(offsetA) is less than itself",
165+
file: file, line: line)
166+
167+
for (offsetB, b) in indicesIncludingEnd[..<offsetA].enumerated() {
168+
XCTAssertNotEqual(
169+
a, b,
170+
"Index at offset \(offsetA) equals index at offset \(offsetB)",
171+
file: file, line: line)
172+
XCTAssertLessThan(
173+
b, a,
174+
"""
175+
Index at offset \(offsetB) is not less than index at offset \(offsetA)
176+
""",
177+
file: file, line: line)
178+
}
179+
}
180+
181+
// `index(_:offsetBy:)` and `distance(from:to:)`
182+
for (startOffset, start) in indicesIncludingEnd.enumerated() {
183+
for (endOffset, end) in indicesIncludingEnd.enumerated() {
184+
let distance = endOffset - startOffset
185+
186+
XCTAssertEqual(
187+
c.index(start, offsetBy: distance), end,
188+
"""
189+
Index at offset \(startOffset) offset by \(distance) does not equal \
190+
index at offset \(endOffset)
191+
""",
192+
file: file, line: line)
193+
XCTAssertEqual(
194+
c.distance(from: start, to: end), distance,
195+
"""
196+
Distance from index at offset \(startOffset) to index at offset \
197+
\(endOffset) does not equal \(distance)
198+
""",
199+
file: file, line: line)
200+
}
201+
}
202+
203+
// `index(_:offsetBy:limitedBy:)`
204+
for (startOffset, start) in indicesIncludingEnd.enumerated() {
205+
for (limitOffset, limit) in indicesIncludingEnd.enumerated() {
206+
// verifies that the target index corresponding to each offset in
207+
// `range` can or cannot be reached from `start` using
208+
// `chain.index(start, offsetBy: _, limitedBy: limit)`, depending on the
209+
// value of `pastLimit`
210+
func checkTargetRange(_ range: ClosedRange<Int>, pastLimit: Bool) {
211+
for targetOffset in range {
212+
let distance = targetOffset - startOffset
213+
let end = c.index(start, offsetBy: distance, limitedBy: limit)
214+
215+
if pastLimit {
216+
XCTAssertNil(
217+
end,
218+
"""
219+
Index at offset \(startOffset) offset by \(distance) limited \
220+
by index at offset \(limitOffset) does not equal `nil`
221+
""",
222+
file: file, line: line)
223+
} else {
224+
XCTAssertEqual(
225+
end, indicesIncludingEnd[targetOffset],
226+
"""
227+
Index at offset \(startOffset) offset by \(distance) limited \
228+
by index at offset \(limitOffset) does not equal index at \
229+
offset \(targetOffset)
230+
""",
231+
file: file, line: line)
232+
}
233+
}
234+
}
235+
236+
// forward offsets
237+
if limit >= start {
238+
// the limit has an effect
239+
checkTargetRange(startOffset...limitOffset, pastLimit: false)
240+
checkTargetRange((limitOffset + 1)...(count + 1), pastLimit: true)
241+
} else {
242+
// the limit has no effect
243+
checkTargetRange(startOffset...count, pastLimit: false)
244+
}
245+
246+
// backward offsets
247+
if limit <= start {
248+
// the limit has an effect
249+
checkTargetRange(limitOffset...startOffset, pastLimit: false)
250+
checkTargetRange(-1...(limitOffset - 1), pastLimit: true)
251+
} else {
252+
// the limit has no effect
253+
checkTargetRange(0...startOffset, pastLimit: false)
254+
}
255+
}
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)