Skip to content

Commit dd7367d

Browse files
committed
[test] checkHashable: Check that unequal values produce different hashes
This is safe to do with hash(into:), because random hash collisions can be eliminated with awesome certainty by trying a number of different hash seeds. (Unless there is a weakness in SipHash.) In some cases, we intentionally want hashing to produce looser equivalency classes than equality — to let those cases keep working, add an optional hashEqualityOracle parameter. Review usages of checkHashable and add hash oracles as needed.
1 parent f138396 commit dd7367d

File tree

2 files changed

+149
-19
lines changed

2 files changed

+149
-19
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift.gyb

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,17 +2095,43 @@ public func checkEquatable<T : Equatable>(
20952095
oracle: { expectedEqual || $0 == $1 }, ${trace}, showFrame: false)
20962096
}
20972097

2098+
internal func hash<H: Hashable>(_ value: H, seed: Int? = nil) -> Int {
2099+
var hasher = _Hasher()
2100+
if let seed = seed {
2101+
hasher.combine(seed)
2102+
}
2103+
hasher.combine(value)
2104+
return hasher.finalize()
2105+
}
2106+
2107+
/// Test that the elements of `instances` satisfy the semantic requirements of
2108+
/// `Hashable`, using `equalityOracle` to generate equality and hashing
2109+
/// expectations from pairs of positions in `instances`.
2110+
public func checkHashable<Instances: Collection>(
2111+
_ instances: Instances,
2112+
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
2113+
allowBrokenTransitivity: Bool = false,
2114+
${TRACE}
2115+
) where Instances.Iterator.Element: Hashable {
2116+
checkHashable(
2117+
instances,
2118+
equalityOracle: equalityOracle,
2119+
hashEqualityOracle: equalityOracle,
2120+
allowBrokenTransitivity: allowBrokenTransitivity,
2121+
${trace})
2122+
}
2123+
20982124
/// Test that the elements of `instances` satisfy the semantic
20992125
/// requirements of `Hashable`, using `equalityOracle` to generate
2100-
/// equality expectations from pairs of positions in `instances`.
2101-
public func checkHashable<Instances : Collection>(
2126+
/// equality expectations from pairs of positions in `instances`,
2127+
/// and `hashEqualityOracle` to do the same for hashing.
2128+
public func checkHashable<Instances: Collection>(
21022129
_ instances: Instances,
21032130
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
2131+
hashEqualityOracle: (Instances.Index, Instances.Index) -> Bool,
21042132
allowBrokenTransitivity: Bool = false,
21052133
${TRACE}
2106-
) where
2107-
Instances.Iterator.Element : Hashable {
2108-
2134+
) where Instances.Iterator.Element: Hashable {
21092135
checkEquatable(
21102136
instances,
21112137
oracle: equalityOracle,
@@ -2116,11 +2142,55 @@ public func checkHashable<Instances : Collection>(
21162142
let x = instances[i]
21172143
for j in instances.indices {
21182144
let y = instances[j]
2145+
let predicted = hashEqualityOracle(i, j)
2146+
expectEqual(
2147+
predicted, hashEqualityOracle(j, i),
2148+
"bad hash oracle: broken symmetry between indices \(i), \(j)",
2149+
stackTrace: ${stackTrace})
21192150
if x == y {
2151+
expectTrue(
2152+
predicted,
2153+
"""
2154+
bad hash oracle: equality must imply hash equality
2155+
lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)
2156+
""",
2157+
stackTrace: ${stackTrace})
2158+
}
2159+
if predicted {
2160+
expectEqual(
2161+
hash(x), hash(y),
2162+
"hash(into:) expected to match, found to differ\n" +
2163+
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
2164+
stackTrace: ${stackTrace})
21202165
expectEqual(
21212166
x.hashValue, y.hashValue,
2167+
"hashValue expected to match, found to differ\n" +
21222168
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
21232169
stackTrace: ${stackTrace})
2170+
expectEqual(
2171+
x._unsafeHashValue(), y._unsafeHashValue(),
2172+
"_unsafeHashValue() expected to match, found to differ\n" +
2173+
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
2174+
stackTrace: ${stackTrace})
2175+
} else {
2176+
// Try a few different seeds; one of them must discriminate
2177+
// between the hashes.
2178+
var pass = false
2179+
for s in 0..<10 {
2180+
if hash(x, seed: s) != hash(y, seed: s) {
2181+
// Triggers on first iteration in most cases.
2182+
pass = true
2183+
break
2184+
}
2185+
}
2186+
if !pass {
2187+
expectTrue(
2188+
AssertionResult(isPass: pass)
2189+
.withDescription(
2190+
"_hash(into:) expected to differ, found to match"),
2191+
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
2192+
stackTrace: ${stackTrace})
2193+
}
21242194
}
21252195
}
21262196
}

validation-test/stdlib/AnyHashable.swift.gyb

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,13 @@ AnyHashableTests.test("AnyHashable(mixed minimal hashables)/Hashable") {
175175
% end
176176
% end
177177

178-
checkHashable(xs, equalityOracle: { $0 / 2 == $1 / 2 })
178+
checkHashable(
179+
xs,
180+
equalityOracle: { $0 / 2 == $1 / 2 },
181+
// FIXME: Types that hash the same way will produce hash collisions when
182+
// converted to AnyHashable. Arguably, the type id should be used as a hash
183+
// discriminator.
184+
hashEqualityOracle: { $0 / 2 % 6 == $1 / 2 % 6 })
179185
}
180186

181187
% for (kw, name) in [
@@ -575,18 +581,27 @@ let interestingBitVectorArrays: [[UInt8]] = [
575581
AnyHashableTests.test("AnyHashable(CFBitVector)/Hashable, .base") {
576582
let bitVectors: [CFBitVector] =
577583
interestingBitVectorArrays.map(CFBitVector.makeImmutable)
584+
let hashEqualityOracle: (Int, Int) -> Bool = {
585+
// CFBitVector returns its count as the hash.
586+
interestingBitVectorArrays[$0].count == interestingBitVectorArrays[$1].count
587+
}
578588
let arrays = bitVectors.map { $0.asArray }
579589
func isEq(_ lhs: [[UInt8]], _ rhs: [[UInt8]]) -> Bool {
580590
return zip(lhs, rhs).map { $0 == $1 }.reduce(true, { $0 && $1 })
581591
}
582592
expectEqualTest(interestingBitVectorArrays, arrays, sameValue: isEq)
583-
checkHashable(bitVectors, equalityOracle: { $0 == $1 })
593+
checkHashable(
594+
bitVectors,
595+
equalityOracle: { $0 == $1 },
596+
hashEqualityOracle: hashEqualityOracle)
584597

585598
do {
586599
expectEqual(.foreignClass, SwiftRuntime.metadataKind(of: bitVectors.first!))
587600

588601
let anyHashables = bitVectors.map(AnyHashable.init)
589-
checkHashable(anyHashables, equalityOracle: { $0 == $1 })
602+
checkHashable(anyHashables,
603+
equalityOracle: { $0 == $1 },
604+
hashEqualityOracle: hashEqualityOracle)
590605

591606
let v = anyHashables.first!.base
592607
expectTrue(type(of: v) is CFBitVector.Type)
@@ -600,7 +615,9 @@ AnyHashableTests.test("AnyHashable(CFBitVector)/Hashable, .base") {
600615
SwiftRuntime.metadataKind(of: bitVectorsAsAnyObjects.first!))
601616

602617
let anyHashables = bitVectorsAsAnyObjects.map(AnyHashable.init)
603-
checkHashable(anyHashables, equalityOracle: { $0 == $1 })
618+
checkHashable(anyHashables,
619+
equalityOracle: { $0 == $1 },
620+
hashEqualityOracle: hashEqualityOracle)
604621

605622
let v = anyHashables.first!.base
606623
expectTrue(type(of: v) is CFBitVector.Type)
@@ -612,20 +629,30 @@ AnyHashableTests.test("AnyHashable(CFMutableBitVector)/Hashable, .base") {
612629
// CFBitVector.
613630
let bitVectors: [CFMutableBitVector] =
614631
interestingBitVectorArrays.map(CFMutableBitVector.makeMutable)
632+
let hashEqualityOracle: (Int, Int) -> Bool = {
633+
// CFBitVector returns its count as the hash.
634+
interestingBitVectorArrays[$0].count == interestingBitVectorArrays[$1].count
635+
}
615636
let arrays = bitVectors.map { $0.asArray }
616637
func isEq(_ lhs: [[UInt8]], _ rhs: [[UInt8]]) -> Bool {
617638
return zip(lhs, rhs).map { $0 == $1 }.reduce(true, { $0 && $1 })
618639
}
619640
expectEqualTest(interestingBitVectorArrays, arrays, sameValue: isEq)
620-
checkHashable(bitVectors, equalityOracle: { $0 == $1 })
641+
checkHashable(
642+
bitVectors,
643+
equalityOracle: { $0 == $1 },
644+
hashEqualityOracle: hashEqualityOracle)
621645

622646
do {
623647
expectEqual(
624648
.foreignClass,
625649
SwiftRuntime.metadataKind(of: bitVectors.first!))
626650

627651
let anyHashables = bitVectors.map(AnyHashable.init)
628-
checkHashable(anyHashables, equalityOracle: { $0 == $1 })
652+
checkHashable(
653+
anyHashables,
654+
equalityOracle: { $0 == $1 },
655+
hashEqualityOracle: hashEqualityOracle)
629656

630657
let v = anyHashables.first!.base
631658
expectTrue(type(of: v) is CFMutableBitVector.Type)
@@ -634,14 +661,20 @@ AnyHashableTests.test("AnyHashable(CFMutableBitVector)/Hashable, .base") {
634661
let bitVectorsAsAnyObjects: [NSObject] = bitVectors.map {
635662
($0 as AnyObject) as! NSObject
636663
}
637-
checkHashable(bitVectorsAsAnyObjects, equalityOracle: { $0 == $1 })
664+
checkHashable(
665+
bitVectorsAsAnyObjects,
666+
equalityOracle: { $0 == $1 },
667+
hashEqualityOracle: hashEqualityOracle)
638668

639669
expectEqual(
640670
.objCClassWrapper,
641671
SwiftRuntime.metadataKind(of: bitVectorsAsAnyObjects.first!))
642672

643673
let anyHashables = bitVectorsAsAnyObjects.map(AnyHashable.init)
644-
checkHashable(anyHashables, equalityOracle: { $0 == $1 })
674+
checkHashable(
675+
anyHashables,
676+
equalityOracle: { $0 == $1 },
677+
hashEqualityOracle: hashEqualityOracle)
645678

646679
let v = anyHashables.first!.base
647680
expectTrue(type(of: v) is CFMutableBitVector.Type)
@@ -717,10 +750,14 @@ AnyHashableTests.test("AnyHashable(MinimalHashableRCSwiftError)/Hashable") {
717750
.caseC(LifetimeTracked(2)), .caseC(LifetimeTracked(2)),
718751
]
719752
expectEqual(.enum, SwiftRuntime.metadataKind(of: xs.first!))
720-
checkHashable(xs, equalityOracle: { $0 / 2 == $1 / 2 })
753+
checkHashable(
754+
xs,
755+
equalityOracle: { $0 / 2 == $1 / 2 },
756+
hashEqualityOracle: { $0 / 4 == $1 / 4 })
721757
checkHashable(
722758
xs.map(AnyHashable.init),
723-
equalityOracle: { $0 / 2 == $1 / 2 })
759+
equalityOracle: { $0 / 2 == $1 / 2 },
760+
hashEqualityOracle: { $0 / 4 == $1 / 4 })
724761
}
725762

726763
AnyHashableTests.test("AnyHashable(MinimalHashableRCSwiftError).base") {
@@ -763,8 +800,16 @@ AnyHashableTests.test("AnyHashable(Wrappers)/Hashable") {
763800
return lhs == rhs
764801
}
765802

766-
checkHashable(values, equalityOracle: equalityOracle,
767-
allowBrokenTransitivity: true)
803+
func hashEqualityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
804+
// Elements in [0, 3] hash the same, as do elements in [4, 7].
805+
return lhs / 4 == rhs / 4
806+
}
807+
808+
checkHashable(
809+
values,
810+
equalityOracle: equalityOracle,
811+
hashEqualityOracle: hashEqualityOracle,
812+
allowBrokenTransitivity: true)
768813
}
769814

770815
AnyHashableTests.test("AnyHashable(Set)/Hashable") {
@@ -789,8 +834,10 @@ AnyHashableTests.test("AnyHashable(Set)/Hashable") {
789834
}
790835
}
791836

792-
checkHashable(values, equalityOracle: equalityOracle,
793-
allowBrokenTransitivity: true)
837+
checkHashable(
838+
values,
839+
equalityOracle: equalityOracle,
840+
allowBrokenTransitivity: true)
794841
}
795842

796843
AnyHashableTests.test("AnyHashable(Array)/Hashable") {
@@ -902,6 +949,12 @@ AnyHashableTests.test("AnyHashable(_SwiftNativeNSError(MinimalHashableRCSwiftErr
902949
.caseC(LifetimeTracked(1)), .caseC(LifetimeTracked(1)),
903950
.caseC(LifetimeTracked(2)), .caseC(LifetimeTracked(2)),
904951
]
952+
checkHashable(
953+
swiftErrors,
954+
equalityOracle: { $0 / 2 == $1 / 2 },
955+
hashEqualityOracle: { $0 / 4 == $1 / 4 },
956+
allowBrokenTransitivity: false)
957+
905958
let nsErrors: [NSError] = swiftErrors.flatMap {
906959
swiftError -> [NSError] in
907960
let bridgedNSError = swiftError as NSError
@@ -938,16 +991,23 @@ AnyHashableTests.test("AnyHashable(_SwiftNativeNSError(MinimalHashableRCSwiftErr
938991
return false
939992
}
940993

994+
func hashEqualityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
995+
// Every NSError that has the same domain and code hash the same way.
996+
return lhs / 8 == rhs / 8
997+
}
998+
941999
// FIXME: transitivity is broken because pure `NSError`s can compare equal to
9421000
// Swift errors with payloads just based on the domain and code, and Swift
9431001
// errors with payloads don't compare equal when payloads differ.
9441002
checkHashable(
9451003
nsErrors,
9461004
equalityOracle: equalityOracle,
1005+
hashEqualityOracle: hashEqualityOracle,
9471006
allowBrokenTransitivity: true)
9481007
checkHashable(
9491008
nsErrors.map(AnyHashable.init),
9501009
equalityOracle: equalityOracle,
1010+
hashEqualityOracle: hashEqualityOracle,
9511011
allowBrokenTransitivity: true)
9521012

9531013
// FIXME(id-as-any): run `checkHashable` on an array of mixed

0 commit comments

Comments
 (0)