From 7e2ae22a1a4ee4371fe570b04b9e196a5392639c Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Fri, 16 Oct 2020 13:14:22 +0200 Subject: [PATCH 1/6] Add the `validateIndexTraversals` function --- .../SwiftAlgorithmsTests/TestUtilities.swift | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index 27c9c41b..a97f1ff6 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -66,3 +66,189 @@ 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. The return values are validated using the given `indices` +/// function, which is the source of truth. +/// +/// - 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. +/// +/// - Complexity: O(*n*^3) for each collection, where *n* is the length of the +/// collection. +func validateIndexTraversals( + _ collections: C..., + indices: (C) -> [C.Index], + file: StaticString = #file, line: UInt = #line +) where C: BidirectionalCollection { + for c in collections { + let indices = indices(c) + let count = indices.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, indices.first, + "`startIndex` does not equal the first index", + file: file, line: line) + XCTAssertEqual( + c.endIndex, indices.last, + "`endIndex` does not equal the last index", + file: file, line: line) + + // `index(after:)` + do { + var index = c.startIndex + + for (offset, expected) in indices.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 indices.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` + for (offset, index) in c.indices.enumerated() { + XCTAssertEqual( + index, indices[offset], + "Index mismatch at offset \(offset) in `indices`", + file: file, line: line) + } + + // index comparison + for (offsetA, a) in indices.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 indices[.., 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, indices[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) + } + } + } + } +} From a750d42b5ff0d75e8a6e14b51ad7070ac1cc5eb3 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Tue, 27 Oct 2020 11:50:03 +0100 Subject: [PATCH 2/6] Add chain index traversal test --- Tests/SwiftAlgorithmsTests/ChainTests.swift | 90 +++------------------ 1 file changed, 11 insertions(+), 79 deletions(-) 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) - } - } } From fa56d7c604f5980b8d598c13e8b46a5b1d4cd0cf Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Tue, 27 Oct 2020 12:07:06 +0100 Subject: [PATCH 3/6] Add product index traversal test --- Tests/SwiftAlgorithmsTests/ProductTests.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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)] + }) + } } From 4107190e1058f8e24cf3754ee495f1010d4d853a Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Thu, 29 Oct 2020 13:47:19 +0100 Subject: [PATCH 4/6] Make the `indices` parameter optional, default to `c.indices + [c.endIndex]` --- Tests/SwiftAlgorithmsTests/TestUtilities.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index a97f1ff6..b3142d80 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -73,23 +73,26 @@ func XCTAssertLazy(_: S) {} /// `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. The return values are validated using the given `indices` -/// function, which is the source of truth. +/// 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. +/// 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], + indices: ((C) -> [C.Index])? = nil, file: StaticString = #file, line: UInt = #line ) where C: BidirectionalCollection { for c in collections { - let indices = indices(c) + let indices = indices?(c) ?? (c.indices + [c.endIndex]) let count = indices.count - 1 XCTAssertEqual( From d7e43f9508f292e65e0f96fd29ea828eae762d48 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Thu, 29 Oct 2020 19:26:38 +0100 Subject: [PATCH 5/6] Rename variable --- .../SwiftAlgorithmsTests/TestUtilities.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index b3142d80..a084b6a4 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -92,8 +92,8 @@ func validateIndexTraversals( file: StaticString = #file, line: UInt = #line ) where C: BidirectionalCollection { for c in collections { - let indices = indices?(c) ?? (c.indices + [c.endIndex]) - let count = indices.count - 1 + let indicesIncludingEnd = indices?(c) ?? (c.indices + [c.endIndex]) + let count = indicesIncludingEnd.count - 1 XCTAssertEqual( c.count, count, @@ -104,11 +104,11 @@ func validateIndexTraversals( "Emptiness mismatch", file: file, line: line) XCTAssertEqual( - c.startIndex, indices.first, + c.startIndex, indicesIncludingEnd.first, "`startIndex` does not equal the first index", file: file, line: line) XCTAssertEqual( - c.endIndex, indices.last, + c.endIndex, indicesIncludingEnd.last, "`endIndex` does not equal the last index", file: file, line: line) @@ -116,7 +116,7 @@ func validateIndexTraversals( do { var index = c.startIndex - for (offset, expected) in indices.enumerated().dropFirst() { + for (offset, expected) in indicesIncludingEnd.enumerated().dropFirst() { c.formIndex(after: &index) XCTAssertEqual( index, expected, @@ -132,7 +132,7 @@ func validateIndexTraversals( do { var index = c.endIndex - for (offset, expected) in indices.enumerated().dropLast().reversed() { + for (offset, expected) in indicesIncludingEnd.enumerated().dropLast().reversed() { c.formIndex(before: &index) XCTAssertEqual( index, expected, @@ -147,13 +147,13 @@ func validateIndexTraversals( // `indices` for (offset, index) in c.indices.enumerated() { XCTAssertEqual( - index, indices[offset], + index, indicesIncludingEnd[offset], "Index mismatch at offset \(offset) in `indices`", file: file, line: line) } // index comparison - for (offsetA, a) in indices.enumerated() { + for (offsetA, a) in indicesIncludingEnd.enumerated() { XCTAssertEqual( a, a, "Index at offset \(offsetA) does not equal itself", @@ -163,7 +163,7 @@ func validateIndexTraversals( "Index at offset \(offsetA) is less than itself", file: file, line: line) - for (offsetB, b) in indices[..( } // `index(_:offsetBy:)` and `distance(from:to:)` - for (startOffset, start) in indices.enumerated() { - for (endOffset, end) in indices.enumerated() { + for (startOffset, start) in indicesIncludingEnd.enumerated() { + for (endOffset, end) in indicesIncludingEnd.enumerated() { let distance = endOffset - startOffset XCTAssertEqual( @@ -200,8 +200,8 @@ func validateIndexTraversals( } // `index(_:offsetBy:limitedBy:)` - for (startOffset, start) in indices.enumerated() { - for (limitOffset, limit) in indices.enumerated() { + 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 @@ -221,7 +221,7 @@ func validateIndexTraversals( file: file, line: line) } else { XCTAssertEqual( - end, indices[targetOffset], + end, indicesIncludingEnd[targetOffset], """ Index at offset \(startOffset) offset by \(distance) limited \ by index at offset \(limitOffset) does not equal index at \ From 4dcba178a74503431a7c640c037b6efaaeef3a9f Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Thu, 29 Oct 2020 19:30:22 +0100 Subject: [PATCH 6/6] Assert that `indices` has the correct length --- Tests/SwiftAlgorithmsTests/TestUtilities.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index a084b6a4..5d5b452f 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -145,6 +145,7 @@ func validateIndexTraversals( } // `indices` + XCTAssertEqual(c.indices.count, count) for (offset, index) in c.indices.enumerated() { XCTAssertEqual( index, indicesIncludingEnd[offset],