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