mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
Swift objects can implement Equatable without implementing Hashable. Obj-C NSObject always provides both. Now that we bridge Swift Equatable to Obj-C `-isEqual:`, we have to be conservative with how we expose `-hash` for Swift types that are Equatable but not Hashable. This looks like "incomplete hashing" of such types.
353 lines
14 KiB
Swift
353 lines
14 KiB
Swift
// RUN: %empty-directory(%t)
|
|
//
|
|
// RUN: %gyb %s -o %t/BridgeIdAsAny.swift
|
|
// RUN: %target-build-swift -g -module-name a %t/BridgeIdAsAny.swift -o %t.out
|
|
// RUN: %target-codesign %t.out
|
|
// RUN: %target-run %t.out
|
|
// REQUIRES: executable_test
|
|
//
|
|
// REQUIRES: objc_interop
|
|
|
|
import StdlibUnittest
|
|
import Foundation
|
|
|
|
var BridgeAnything = TestSuite("BridgeAnything")
|
|
|
|
func wantonlyWrapInAny<T>(_ x: T) -> Any {
|
|
return x
|
|
}
|
|
|
|
// Professional runtime testers on a closed course. Do not attempt at home.
|
|
extension LifetimeTracked: Error {}
|
|
extension String: Error {}
|
|
|
|
struct KnownUnbridged: Equatable, Hashable {
|
|
var x, y: LifetimeTracked
|
|
|
|
init() {
|
|
x = LifetimeTracked(17)
|
|
y = LifetimeTracked(38)
|
|
}
|
|
|
|
public static func ==(a: KnownUnbridged, b: KnownUnbridged) -> Bool {
|
|
return a.x === b.x && a.y === b.y
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(x)
|
|
hasher.combine(y)
|
|
}
|
|
}
|
|
|
|
struct KnownUnbridgedWithDescription: CustomStringConvertible,
|
|
CustomDebugStringConvertible {
|
|
var x, y: LifetimeTracked
|
|
|
|
init() {
|
|
x = LifetimeTracked(17)
|
|
y = LifetimeTracked(38)
|
|
}
|
|
|
|
var description: String {
|
|
return "\(x)\(y), baby, hey"
|
|
}
|
|
|
|
var debugDescription: String {
|
|
return "KnownUnbridgedWithDescription(\"\(x)\(y)\" /* baby, hey */)"
|
|
}
|
|
}
|
|
|
|
func bridgedObjectPreservesIdentity(original: LifetimeTracked,
|
|
bridged: AnyObject) {
|
|
expectTrue(original === bridged)
|
|
}
|
|
|
|
func stringBridgesToEqualNSString(original: String,
|
|
bridged: AnyObject) {
|
|
expectTrue(bridged.isEqual(to: original))
|
|
}
|
|
|
|
func boxedTypeRoundTripsThroughDynamicCasting(original: KnownUnbridged,
|
|
bridged: AnyObject) {
|
|
direct: do {
|
|
guard let bridgedAndCast = bridged as? KnownUnbridged else {
|
|
expectUnreachable()
|
|
break direct
|
|
}
|
|
expectEqual(original, bridgedAndCast)
|
|
}
|
|
|
|
let bridgedAny: Any = bridged
|
|
any: do {
|
|
guard let bridgedAndCastAny = bridgedAny as? KnownUnbridged else {
|
|
expectUnreachable()
|
|
break any
|
|
}
|
|
expectEqual(original, bridgedAndCastAny)
|
|
}
|
|
|
|
anyInAny: do {
|
|
let bridgedAnyInAny = wantonlyWrapInAny(bridgedAny)
|
|
guard let bridgedAndCastAnyInAny = bridgedAnyInAny as? KnownUnbridged else {
|
|
expectUnreachable()
|
|
break anyInAny
|
|
}
|
|
expectEqual(original, bridgedAndCastAnyInAny)
|
|
}
|
|
|
|
// Failed casts should fail, and hopefully shouldn't leak or corrupt memory
|
|
// either.
|
|
expectEqual(bridged as? Int, nil)
|
|
expectEqual(bridged as? String, nil)
|
|
}
|
|
|
|
func tupleCanBeDynamicallyCast(original: (Int, String),
|
|
bridged: AnyObject) {
|
|
expectTrue(original == (bridged as! (Int, String)))
|
|
}
|
|
func metatypeCanBeDynamicallyCast(original: Int.Type,
|
|
bridged: AnyObject) {
|
|
expectTrue(original == (bridged as! Int.Type))
|
|
expectTrue(original == (bridged as! Any.Type))
|
|
}
|
|
func metatypeCanBeDynamicallyCast(original: CFString.Type,
|
|
bridged: AnyObject) {
|
|
expectTrue(original == (bridged as! CFString.Type))
|
|
expectTrue(original == (bridged as! Any.Type))
|
|
}
|
|
func metatypeCanBeDynamicallyCastGenerically<T>(original: T.Type,
|
|
bridged: AnyObject) {
|
|
expectTrue(original == (bridged as! T.Type))
|
|
expectTrue(original == (bridged as! Any.Type))
|
|
}
|
|
|
|
|
|
func guineaPigFunction() -> Int {
|
|
return 1738
|
|
}
|
|
|
|
func functionCanBeDynamicallyCast(original: () -> Int,
|
|
bridged: AnyObject) {
|
|
expectEqual(original(), (bridged as! () -> Int)())
|
|
expectEqual(original(), try! (bridged as! () throws -> Int)())
|
|
}
|
|
|
|
func classMetatypePreservesIdentity<T: AnyObject>(original: T.Type,
|
|
bridged: AnyObject) {
|
|
expectTrue(original as AnyObject === bridged)
|
|
expectTrue(original as AnyObject.Type as AnyObject === bridged)
|
|
expectTrue(original as Any.Type as AnyObject === bridged)
|
|
}
|
|
|
|
func classMetatypePreservesIdentityGenerically<T>(original: T.Type,
|
|
bridged: AnyObject) {
|
|
expectTrue(original as AnyObject === bridged)
|
|
expectTrue(original as Any.Type as AnyObject === bridged)
|
|
}
|
|
|
|
func protocolObjectPreservesIdentity(original: NSCopying.Protocol,
|
|
bridged: AnyObject) {
|
|
let proto: Protocol = original
|
|
expectTrue(proto === bridged)
|
|
}
|
|
|
|
enum MyError: Error {
|
|
case a, e, i, o, u
|
|
}
|
|
|
|
func errorValueTypeBridgesToNSError(original: MyError, bridged: AnyObject) {
|
|
let bridgedNSError = bridged as! NSError
|
|
expectEqual(bridgedNSError.domain, original._domain)
|
|
expectEqual(bridgedNSError.code, original._code)
|
|
|
|
let unbridgedError = bridged as! MyError
|
|
expectEqual(original, unbridgedError)
|
|
|
|
let unbridgedError2 = bridgedNSError as! MyError
|
|
expectEqual(original, unbridgedError2)
|
|
}
|
|
|
|
protocol P {}
|
|
|
|
// We want to exhaustively check all paths through the bridging and dynamic
|
|
// casting infrastructure, so expand out test cases that wrap the different
|
|
// interesting bridging cases in different kinds of existential container.
|
|
%{
|
|
testCases = [
|
|
# testName type valueExpr testFunc conformsToError conformsToHashable
|
|
("classes", "LifetimeTracked", "LifetimeTracked(0)", "bridgedObjectPreservesIdentity", True, True),
|
|
("strings", "String", '"vitameatavegamin"', "stringBridgesToEqualNSString", True, True),
|
|
("unbridged type", "KnownUnbridged", "KnownUnbridged()", "boxedTypeRoundTripsThroughDynamicCasting", False, True),
|
|
("tuple", "(Int, String)", '(1, "2")', "tupleCanBeDynamicallyCast", False, False),
|
|
("metatype", "Int.Type", 'Int.self', "metatypeCanBeDynamicallyCast", False, False),
|
|
("generic metatype", "Int.Type", 'Int.self', "metatypeCanBeDynamicallyCastGenerically", False, False),
|
|
("CF metatype", "CFString.Type", 'CFString.self', "metatypeCanBeDynamicallyCast", False, False),
|
|
("generic CF metatype", "CFString.Type", 'CFString.self', "metatypeCanBeDynamicallyCastGenerically", False, False),
|
|
("class metatype", "LifetimeTracked.Type", 'LifetimeTracked.self', "classMetatypePreservesIdentity", False, False),
|
|
("objc metatype", "NSObject.Type", 'NSObject.self', "classMetatypePreservesIdentity", False, False),
|
|
("protocol", "P.Protocol", 'P.self', "metatypeCanBeDynamicallyCastGenerically", False, False),
|
|
("objc protocol", "NSCopying.Protocol", 'NSCopying.self', "protocolObjectPreservesIdentity", False, False),
|
|
("objc protocol composition", "(NSCopying & NSCoding).Protocol", '(NSCopying & NSCoding).self', "metatypeCanBeDynamicallyCastGenerically", False, False),
|
|
("mixed protocol composition", "(NSCopying & P).Protocol", '(NSCopying & P).self', "metatypeCanBeDynamicallyCastGenerically", False, False),
|
|
("generic class metatype", "LifetimeTracked.Type", 'LifetimeTracked.self', "classMetatypePreservesIdentityGenerically", False, False),
|
|
("generic objc metatype", "NSObject.Type", 'NSObject.self', "classMetatypePreservesIdentityGenerically", False, False),
|
|
("function", "() -> Int", 'guineaPigFunction', "functionCanBeDynamicallyCast", False, False),
|
|
("error type", "MyError", 'MyError.e', "errorValueTypeBridgesToNSError", True, True),
|
|
]
|
|
}%
|
|
|
|
/// Whether this can be safely casted to NSObject
|
|
func isNSObject<T>(_ value: T) -> Bool {
|
|
return (value is NSObject) && !(value is LifetimeTracked)
|
|
}
|
|
|
|
% for testName, type, valueExpr, testFunc, conformsToError, conformsToHashable in testCases:
|
|
BridgeAnything.test("${testName}") {
|
|
autoreleasepool {
|
|
let x: ${type} = ${valueExpr}
|
|
${testFunc}(original: x, bridged: _bridgeAnythingToObjectiveC(x))
|
|
${testFunc}(original: x, bridged: _bridgeAnythingNonVerbatimToObjectiveC(x))
|
|
|
|
// Bridge an array containing x.
|
|
let xInArray = [x]
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInArray) as! [AnyObject])[0])
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInArray) as? [AnyObject])![0])
|
|
if isNSObject(x) {
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInArray) as! [AnyObject])[0])
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInArray) as? [AnyObject])![0])
|
|
}
|
|
|
|
// Bridge a dictionary containing x as a value.
|
|
let xInDictValue = ["key" : x]
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictValue) as! [String: AnyObject])["key"]!)
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictValue) as? [String: AnyObject])!["key"]!)
|
|
if isNSObject(x) {
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictValue) as! [String: NSObject])["key"]!)
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictValue) as? [String: NSObject])!["key"]!)
|
|
}
|
|
|
|
% if conformsToHashable:
|
|
// Bridge a dictionary containing x as a key.
|
|
let xInDictKey = [x : "value"] as [AnyHashable: String]
|
|
// FIXME: need a way to express `AnyObject & Hashable`.
|
|
// The NSObject version below can't test class LifetimeTracked.
|
|
// ${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictKey) as! [(AnyObject & Hashable): String]).keys.first!)
|
|
// ${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictKey) as? [(AnyObject & Hashable): String])!.keys.first!)
|
|
if isNSObject(x) {
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictKey) as! [NSObject: String]).keys.first!)
|
|
${testFunc}(original: x, bridged: (_bridgeAnythingToObjectiveC(xInDictKey) as? [NSObject: String])!.keys.first!)
|
|
}
|
|
|
|
% end
|
|
|
|
let xInAny: Any = x
|
|
let xInAnyBridged = _bridgeAnythingToObjectiveC(xInAny)
|
|
let xInAnyBridged2 = _bridgeAnythingNonVerbatimToObjectiveC(xInAny)
|
|
${testFunc}(original: x, bridged: xInAnyBridged)
|
|
${testFunc}(original: x, bridged: xInAnyBridged2)
|
|
|
|
let xInAnyInAny = wantonlyWrapInAny(xInAny)
|
|
let xInAnyInAnyBridged = _bridgeAnythingToObjectiveC(xInAnyInAny)
|
|
let xInAnyInAnyBridged2 = _bridgeAnythingNonVerbatimToObjectiveC(xInAnyInAny)
|
|
${testFunc}(original: x, bridged: xInAnyInAnyBridged)
|
|
${testFunc}(original: x, bridged: xInAnyInAnyBridged2)
|
|
|
|
% if conformsToError:
|
|
let xInError: Error = x
|
|
let xInErrorBridged = _bridgeAnythingToObjectiveC(xInError)
|
|
let xInErrorBridged2 = _bridgeAnythingNonVerbatimToObjectiveC(xInError)
|
|
|
|
${testFunc}(original: x, bridged: xInErrorBridged)
|
|
${testFunc}(original: x, bridged: xInErrorBridged2)
|
|
|
|
let xInErrorInAny = wantonlyWrapInAny(xInError)
|
|
let xInErrorInAnyBridged = _bridgeAnythingToObjectiveC(xInErrorInAny)
|
|
let xInErrorInAnyBridged2 = _bridgeAnythingNonVerbatimToObjectiveC(xInErrorInAny)
|
|
${testFunc}(original: x, bridged: xInErrorInAnyBridged)
|
|
${testFunc}(original: x, bridged: xInErrorInAnyBridged)
|
|
% end
|
|
|
|
% if conformsToHashable:
|
|
// Check that we get an equal value if we bridge cast back.
|
|
let xFromAny = xInAnyBridged as! ${type}
|
|
expectEqual(x, xFromAny)
|
|
let xFromAny2 = xInAnyBridged2 as! ${type}
|
|
expectEqual(x, xFromAny2)
|
|
let xInAnyBridgedInAny = wantonlyWrapInAny(xInAnyBridged)
|
|
let xFromAnyBridgedInAny = xInAnyBridgedInAny as! ${type}
|
|
expectEqual(x, xFromAnyBridgedInAny)
|
|
|
|
let xFromAnyInAny = xInAnyInAnyBridged as! ${type}
|
|
expectEqual(x, xFromAnyInAny)
|
|
let xFromAnyInAny2 = xInAnyInAnyBridged2 as! ${type}
|
|
expectEqual(x, xFromAnyInAny2)
|
|
let xInAnyInAnyBridgedInAny = wantonlyWrapInAny(xInAnyInAnyBridged)
|
|
let xFromAnyInAnyBridgedInAny = xInAnyInAnyBridgedInAny as! ${type}
|
|
expectEqual(x, xFromAnyInAnyBridgedInAny)
|
|
|
|
% if conformsToError:
|
|
let xFromError = xInErrorBridged as! ${type}
|
|
expectEqual(x, xFromError)
|
|
let xFromError2 = xInErrorBridged2 as! ${type}
|
|
expectEqual(x, xFromError2)
|
|
let xInErrorBridgedInAny = wantonlyWrapInAny(xInErrorBridged)
|
|
let xFromErrorBridgedInAny = xInErrorBridgedInAny as! ${type}
|
|
expectEqual(x, xFromErrorBridgedInAny)
|
|
|
|
let xFromErrorInAny = xInErrorInAnyBridged as! ${type}
|
|
expectEqual(x, xFromErrorInAny)
|
|
let xFromErrorInAny2 = xInErrorInAnyBridged2 as! ${type}
|
|
expectEqual(x, xFromErrorInAny2)
|
|
let xInErrorInAnyBridgedInAny = wantonlyWrapInAny(xInErrorInAnyBridged)
|
|
let xFromErrorInAnyBridgedInAny = xInErrorInAnyBridgedInAny as! ${type}
|
|
expectEqual(x, xFromErrorInAnyBridgedInAny)
|
|
% end
|
|
% end
|
|
}
|
|
}
|
|
% end
|
|
|
|
BridgeAnything.test("description of boxed values") {
|
|
for x in [KnownUnbridged(), KnownUnbridgedWithDescription()] as [Any] {
|
|
let summary = String(reflecting: x)
|
|
expectEqual(summary, _bridgeAnythingToObjectiveC(x).description)
|
|
expectEqual(summary, _bridgeAnythingToObjectiveC(x).debugDescription)
|
|
}
|
|
}
|
|
|
|
BridgeAnything.test("SwiftValue(mixed values)/Hashable") {
|
|
var boxedXs: [NSObject] = []
|
|
% for wrapped in ['OpaqueValue', 'MinimalHashableValue']:
|
|
do {
|
|
let xs = (0..<5).flatMap {
|
|
[ ${wrapped}($0, identity: 0),
|
|
${wrapped}($0, identity: 1) ]
|
|
}
|
|
boxedXs.append(
|
|
contentsOf: xs.map {
|
|
_bridgeAnythingToObjectiveC($0) as! NSObject
|
|
})
|
|
}
|
|
% end
|
|
|
|
boxedXs.append(
|
|
contentsOf: [0, 0, 1, 1, 2, 2, 3, 3, 4, 4].map {
|
|
_bridgeAnythingToObjectiveC($0) as! NSObject
|
|
})
|
|
|
|
boxedXs.append(
|
|
contentsOf: ["a", "a", "b", "b", "c", "c", "d", "d", "e", "e"].map {
|
|
_bridgeAnythingToObjectiveC($0) as! NSObject
|
|
})
|
|
|
|
func equalityOracle(_ lhs: Int, rhs: Int) -> Bool {
|
|
if (0..<10).contains(lhs) || (0..<10).contains(rhs) {
|
|
return lhs == rhs
|
|
}
|
|
return lhs / 2 == rhs / 2
|
|
}
|
|
checkHashable(boxedXs, equalityOracle: equalityOracle, allowIncompleteHashing: true)
|
|
}
|
|
|
|
runAllTests()
|