Skip to content

Commit ff0459d

Browse files
authored
Continued swift testing adoption (#1350)
* Convert buffer view tests to swift-testing * Convert BuiltInUnicodeScalarSet tests * Convert ErrorTests * Convert LockedStateTests * Move aside old XCTest-based equatable/hashable utilities and define new swift-testing utilities * Convert UUID tests * Convert DateInterval tests * Convert date tests * Convert SortComparator tests * Convert decimal tests * Convert IndexPath tests * Fix build failures
1 parent 0a30fcd commit ff0459d

15 files changed

+1544
-1464
lines changed

Sources/TestSupport/Utilities.swift

Lines changed: 265 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func expectNoChanges<T: BinaryInteger>(_ check: @autoclosure () -> T, by differe
170170
///
171171
/// - Note: `oracle` is also checked for conformance to the
172172
/// laws.
173-
public func checkEquatable<Instances: Collection>(
173+
public func XCTCheckEquatable<Instances: Collection>(
174174
_ instances: Instances,
175175
oracle: (Instances.Index, Instances.Index) -> Bool,
176176
allowBrokenTransitivity: Bool = false,
@@ -179,7 +179,7 @@ public func checkEquatable<Instances: Collection>(
179179
line: UInt = #line
180180
) where Instances.Element: Equatable {
181181
let indices = Array(instances.indices)
182-
_checkEquatableImpl(
182+
_XCTCheckEquatableImpl(
183183
Array(instances),
184184
oracle: { oracle(indices[$0], indices[$1]) },
185185
allowBrokenTransitivity: allowBrokenTransitivity,
@@ -188,15 +188,7 @@ public func checkEquatable<Instances: Collection>(
188188
line: line)
189189
}
190190

191-
private class Box<T> {
192-
var value: T
193-
194-
init(_ value: T) {
195-
self.value = value
196-
}
197-
}
198-
199-
internal func _checkEquatableImpl<Instance : Equatable>(
191+
internal func _XCTCheckEquatableImpl<Instance : Equatable>(
200192
_ instances: [Instance],
201193
oracle: (Int, Int) -> Bool,
202194
allowBrokenTransitivity: Bool = false,
@@ -271,23 +263,14 @@ internal func _checkEquatableImpl<Instance : Equatable>(
271263
}
272264
}
273265

274-
func hash<H: Hashable>(_ value: H, salt: Int? = nil) -> Int {
275-
var hasher = Hasher()
276-
if let salt = salt {
277-
hasher.combine(salt)
278-
}
279-
hasher.combine(value)
280-
return hasher.finalize()
281-
}
282-
283-
public func checkHashable<Instances: Collection>(
266+
public func XCTCheckHashable<Instances: Collection>(
284267
_ instances: Instances,
285268
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
286269
allowIncompleteHashing: Bool = false,
287270
_ message: @autoclosure () -> String = "",
288271
file: StaticString = #filePath, line: UInt = #line
289272
) where Instances.Element: Hashable {
290-
checkHashable(
273+
XCTCheckHashable(
291274
instances,
292275
equalityOracle: equalityOracle,
293276
hashEqualityOracle: equalityOracle,
@@ -298,7 +281,7 @@ public func checkHashable<Instances: Collection>(
298281
}
299282

300283

301-
public func checkHashable<Instances: Collection>(
284+
public func XCTCheckHashable<Instances: Collection>(
302285
_ instances: Instances,
303286
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
304287
hashEqualityOracle: (Instances.Index, Instances.Index) -> Bool,
@@ -307,7 +290,7 @@ public func checkHashable<Instances: Collection>(
307290
file: StaticString = #filePath, line: UInt = #line
308291
) where Instances.Element: Hashable {
309292

310-
checkEquatable(
293+
XCTCheckEquatable(
311294
instances,
312295
oracle: equalityOracle,
313296
message(),
@@ -390,7 +373,7 @@ public func checkHashable<Instances: Collection>(
390373
/// Test that the elements of `groups` consist of instances that satisfy the
391374
/// semantic requirements of `Hashable`, with each group defining a distinct
392375
/// equivalence class under `==`.
393-
public func checkHashableGroups<Groups: Collection>(
376+
public func XCTCheckHashableGroups<Groups: Collection>(
394377
_ groups: Groups,
395378
_ message: @autoclosure () -> String = "",
396379
allowIncompleteHashing: Bool = false,
@@ -405,7 +388,7 @@ public func checkHashableGroups<Groups: Collection>(
405388
func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
406389
return groupIndices[lhs] == groupIndices[rhs]
407390
}
408-
checkHashable(
391+
XCTCheckHashable(
409392
instances,
410393
equalityOracle: equalityOracle,
411394
hashEqualityOracle: equalityOracle,
@@ -477,3 +460,259 @@ func testExpectedToFailWithCheck<T>(check: (String) -> Bool, _ test: @escaping
477460
}
478461
}
479462

463+
// MARK: - swift-testing Helpers
464+
465+
import Testing
466+
467+
/// Test that the elements of `instances` satisfy the semantic
468+
/// requirements of `Equatable`, using `oracle` to generate equality
469+
/// expectations from pairs of positions in `instances`.
470+
///
471+
/// - Note: `oracle` is also checked for conformance to the
472+
/// laws.
473+
func checkEquatable<Instances : Collection>(
474+
_ instances: Instances,
475+
oracle: (Instances.Index, Instances.Index) -> Bool,
476+
allowBrokenTransitivity: Bool = false,
477+
_ message: @autoclosure () -> String = "",
478+
sourceLocation: SourceLocation = #_sourceLocation
479+
) where Instances.Element: Equatable {
480+
let indices = Array(instances.indices)
481+
_checkEquatable(
482+
instances,
483+
oracle: { oracle(indices[$0], indices[$1]) },
484+
allowBrokenTransitivity: allowBrokenTransitivity,
485+
message(),
486+
sourceLocation: sourceLocation
487+
)
488+
}
489+
490+
func _checkEquatable<Instances : Collection>(
491+
_ _instances: Instances,
492+
oracle: (Int, Int) -> Bool,
493+
allowBrokenTransitivity: Bool = false,
494+
_ message: @autoclosure () -> String = "",
495+
sourceLocation: SourceLocation = #_sourceLocation
496+
) where Instances.Element: Equatable {
497+
let instances = Array(_instances)
498+
499+
// For each index (which corresponds to an instance being tested) track the
500+
// set of equal instances.
501+
var transitivityScoreboard: [Box<Set<Int>>] =
502+
instances.indices.map { _ in Box([]) }
503+
504+
for i in instances.indices {
505+
let x = instances[i]
506+
#expect(oracle(i, i), "bad oracle: broken reflexivity at index \(i)")
507+
508+
for j in instances.indices {
509+
let y = instances[j]
510+
511+
let predictedXY = oracle(i, j)
512+
#expect(
513+
predictedXY == oracle(j, i),
514+
"bad oracle: broken symmetry between indices \(i), \(j)",
515+
sourceLocation: sourceLocation
516+
)
517+
518+
let isEqualXY = x == y
519+
#expect(
520+
predictedXY == isEqualXY,
521+
"""
522+
\((predictedXY
523+
? "expected equal, found not equal"
524+
: "expected not equal, found equal"))
525+
lhs (at index \(i)): \(String(reflecting: x))
526+
rhs (at index \(j)): \(String(reflecting: y))
527+
""",
528+
sourceLocation: sourceLocation
529+
)
530+
531+
// Not-equal is an inverse of equal.
532+
#expect(
533+
isEqualXY != (x != y),
534+
"""
535+
lhs (at index \(i)): \(String(reflecting: x))
536+
rhs (at index \(j)): \(String(reflecting: y))
537+
""",
538+
sourceLocation: sourceLocation
539+
)
540+
541+
if !allowBrokenTransitivity {
542+
// Check transitivity of the predicate represented by the oracle.
543+
// If we are adding the instance `j` into an equivalence set, check that
544+
// it is equal to every other instance in the set.
545+
if predictedXY && i < j && transitivityScoreboard[i].value.insert(j).inserted {
546+
if transitivityScoreboard[i].value.count == 1 {
547+
transitivityScoreboard[i].value.insert(i)
548+
}
549+
for k in transitivityScoreboard[i].value {
550+
#expect(
551+
oracle(j, k),
552+
"bad oracle: broken transitivity at indices \(i), \(j), \(k)",
553+
sourceLocation: sourceLocation
554+
)
555+
// No need to check equality between actual values, we will check
556+
// them with the checks above.
557+
}
558+
precondition(transitivityScoreboard[j].value.isEmpty)
559+
transitivityScoreboard[j] = transitivityScoreboard[i]
560+
}
561+
}
562+
}
563+
}
564+
}
565+
566+
public func checkHashable<Instances: Collection>(
567+
_ instances: Instances,
568+
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
569+
allowIncompleteHashing: Bool = false,
570+
_ message: @autoclosure () -> String = "",
571+
sourceLocation: SourceLocation = #_sourceLocation
572+
) where Instances.Element: Hashable {
573+
checkHashable(
574+
instances,
575+
equalityOracle: equalityOracle,
576+
hashEqualityOracle: equalityOracle,
577+
allowIncompleteHashing: allowIncompleteHashing,
578+
message(),
579+
sourceLocation: sourceLocation)
580+
}
581+
582+
func checkHashable<Instances: Collection>(
583+
_ instances: Instances,
584+
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
585+
hashEqualityOracle: (Instances.Index, Instances.Index) -> Bool,
586+
allowIncompleteHashing: Bool = false,
587+
_ message: @autoclosure () -> String = "",
588+
sourceLocation: SourceLocation = #_sourceLocation
589+
) where Instances.Element: Hashable {
590+
checkEquatable(
591+
instances,
592+
oracle: equalityOracle,
593+
message(),
594+
sourceLocation: sourceLocation
595+
)
596+
597+
for i in instances.indices {
598+
let x = instances[i]
599+
for j in instances.indices {
600+
let y = instances[j]
601+
let predicted = hashEqualityOracle(i, j)
602+
#expect(
603+
predicted == hashEqualityOracle(j, i),
604+
"bad hash oracle: broken symmetry between indices \(i), \(j)",
605+
sourceLocation: sourceLocation
606+
)
607+
if x == y {
608+
#expect(
609+
predicted,
610+
"""
611+
bad hash oracle: equality must imply hash equality
612+
lhs (at index \(i)): \(x)
613+
rhs (at index \(j)): \(y)
614+
""",
615+
sourceLocation: sourceLocation
616+
)
617+
}
618+
if predicted {
619+
#expect(
620+
hash(x) == hash(y),
621+
"""
622+
hash(into:) expected to match, found to differ
623+
lhs (at index \(i)): \(x)
624+
rhs (at index \(j)): \(y)
625+
""",
626+
sourceLocation: sourceLocation
627+
)
628+
#expect(
629+
x.hashValue == y.hashValue,
630+
"""
631+
hashValue expected to match, found to differ
632+
lhs (at index \(i)): \(x)
633+
rhs (at index \(j)): \(y)
634+
""",
635+
sourceLocation: sourceLocation
636+
)
637+
#expect(
638+
x._rawHashValue(seed: 0) == y._rawHashValue(seed: 0),
639+
"""
640+
_rawHashValue(seed:) expected to match, found to differ
641+
lhs (at index \(i)): \(x)
642+
rhs (at index \(j)): \(y)
643+
""",
644+
sourceLocation: sourceLocation
645+
)
646+
} else if !allowIncompleteHashing {
647+
// Try a few different seeds; at least one of them should discriminate
648+
// between the hashes. It is extremely unlikely this check will fail
649+
// all ten attempts, unless the type's hash encoding is not unique,
650+
// or unless the hash equality oracle is wrong.
651+
#expect(
652+
(0..<10).contains { hash(x, salt: $0) != hash(y, salt: $0) },
653+
"""
654+
hash(into:) expected to differ, found to match
655+
lhs (at index \(i)): \(x)
656+
rhs (at index \(j)): \(y)
657+
""",
658+
sourceLocation: sourceLocation
659+
)
660+
#expect(
661+
(0..<10).contains { i in
662+
x._rawHashValue(seed: i) != y._rawHashValue(seed: i)
663+
},
664+
"""
665+
_rawHashValue(seed:) expected to differ, found to match
666+
lhs (at index \(i)): \(x)
667+
rhs (at index \(j)): \(y)
668+
""",
669+
sourceLocation: sourceLocation
670+
)
671+
}
672+
}
673+
}
674+
}
675+
676+
/// Test that the elements of `groups` consist of instances that satisfy the
677+
/// semantic requirements of `Hashable`, with each group defining a distinct
678+
/// equivalence class under `==`.
679+
public func checkHashableGroups<Groups: Collection>(
680+
_ groups: Groups,
681+
_ message: @autoclosure () -> String = "",
682+
allowIncompleteHashing: Bool = false,
683+
sourceLocation: SourceLocation = #_sourceLocation
684+
) where Groups.Element: Collection, Groups.Element.Element: Hashable {
685+
let instances = groups.flatMap { $0 }
686+
// groupIndices[i] is the index of the element in groups that contains
687+
// instances[i].
688+
let groupIndices =
689+
zip(0..., groups).flatMap { i, group in group.map { _ in i } }
690+
func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
691+
return groupIndices[lhs] == groupIndices[rhs]
692+
}
693+
checkHashable(
694+
instances,
695+
equalityOracle: equalityOracle,
696+
hashEqualityOracle: equalityOracle,
697+
allowIncompleteHashing: allowIncompleteHashing,
698+
sourceLocation: sourceLocation)
699+
}
700+
701+
// MARK: - Private Types
702+
703+
private class Box<T> {
704+
var value: T
705+
706+
init(_ value: T) {
707+
self.value = value
708+
}
709+
}
710+
711+
private func hash<H: Hashable>(_ value: H, salt: Int? = nil) -> Int {
712+
var hasher = Hasher()
713+
if let salt = salt {
714+
hasher.combine(salt)
715+
}
716+
hasher.combine(value)
717+
return hasher.finalize()
718+
}

0 commit comments

Comments
 (0)