@@ -66,3 +66,193 @@ func XCTAssertEqualSequences<S1: Sequence, S2: Sequence>(
66
66
}
67
67
68
68
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