mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-28 09:47:07 +08:00
480 lines
19 KiB
Swift
480 lines
19 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2022 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import XCTest
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
import Foundation
|
|
#else
|
|
import FoundationEssentials
|
|
#endif
|
|
|
|
extension Optional {
|
|
@available(*, unavailable, message: "Use XCTUnwrap() instead")
|
|
func unwrapped(_ fn: String = #function, file: StaticString = #filePath, line: UInt = #line) throws -> Wrapped {
|
|
return try XCTUnwrap(self, file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func expectThrows<Error: Swift.Error & Equatable>(_ expectedError: Error, _ test: () throws -> Void, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
var caught = false
|
|
do {
|
|
try test()
|
|
} catch let error as Error {
|
|
caught = true
|
|
XCTAssertEqual(error, expectedError, message(), file: file, line: line)
|
|
} catch {
|
|
caught = true
|
|
XCTFail("Incorrect error thrown: \(error) -- \(message())", file: file, line: line)
|
|
}
|
|
XCTAssert(caught, "No error thrown -- \(message())", file: file, line: line)
|
|
}
|
|
|
|
func expectDoesNotThrow(_ test: () throws -> Void, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertNoThrow(try test(), message(), file: file, line: line)
|
|
}
|
|
|
|
func expectTrue(_ actual: Bool, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertTrue(actual, message(), file: file, line: line)
|
|
}
|
|
|
|
func expectFalse(_ actual: Bool, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertFalse(actual, message(), file: file, line: line)
|
|
}
|
|
|
|
public func expectEqual<T: Equatable>(_ expected: T, _ actual: T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertEqual(expected, actual, message(), file: file, line: line)
|
|
}
|
|
|
|
public func expectNotEqual<T: Equatable>(_ expected: T, _ actual: T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertNotEqual(expected, actual, message(), file: file, line: line)
|
|
}
|
|
|
|
public func expectEqual<T: FloatingPoint>(_ expected: T, _ actual: T, within: T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertEqual(expected, actual, accuracy: within, message(), file: file, line: line)
|
|
}
|
|
|
|
public func expectEqual<T: FloatingPoint>(_ expected: T?, _ actual: T, within: T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertNotNil(expected, message(), file: file, line: line)
|
|
if let expected = expected {
|
|
XCTAssertEqual(expected, actual, accuracy: within, message(), file: file, line: line)
|
|
}
|
|
}
|
|
|
|
public func expectEqual(
|
|
_ expected: Any.Type,
|
|
_ actual: Any.Type,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) {
|
|
XCTAssertTrue(expected == actual, message(), file: file, line: line)
|
|
}
|
|
|
|
public func expectEqualSequence< Expected: Sequence, Actual: Sequence>(
|
|
_ expected: Expected, _ actual: Actual,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: String = #file, line: UInt = #line,
|
|
sameValue: (Expected.Element, Expected.Element) -> Bool
|
|
) where Expected.Element == Actual.Element {
|
|
if !expected.elementsEqual(actual, by: sameValue) {
|
|
XCTFail("expected elements: \"\(expected)\"\n"
|
|
+ "actual: \"\(actual)\" (of type \(String(reflecting: type(of: actual)))), \(message())")
|
|
}
|
|
}
|
|
|
|
public func expectEqualSequence< Expected: Sequence, Actual: Sequence>(
|
|
_ expected: Expected, _ actual: Actual,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: String = #file, line: UInt = #line
|
|
) where Expected.Element == Actual.Element, Expected.Element: Equatable {
|
|
expectEqualSequence(expected, actual, message()) {
|
|
$0 == $1
|
|
}
|
|
}
|
|
|
|
func expectEqual(_ actual: Date, _ expected: Date , within: Double = 0.001, file: StaticString = #filePath, line: UInt = #line) {
|
|
let debugDescription = "\nactual: \(actual.formatted(.iso8601));\nexpected: \(expected.formatted(.iso8601))"
|
|
XCTAssertEqual(actual.timeIntervalSinceReferenceDate, expected.timeIntervalSinceReferenceDate, accuracy: within, debugDescription, file: file, line: line)
|
|
}
|
|
|
|
// Compare two date components like the original equality, but compares nanosecond within a reasonable epsilon, and optionally ignores quarter and calendar equality since they were often not supported in the original implementation
|
|
public func expectEqual(_ first: DateComponents, _ second: DateComponents, within nanosecondAccuracy: Int = 5000, expectQuarter: Bool = true, expectCalendar: Bool = true, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
|
|
XCTAssertEqual(first.era, second.era, message(), file: file, line: line)
|
|
XCTAssertEqual(first.year, second.year, message(), file: file, line: line)
|
|
XCTAssertEqual(first.month, second.month, message(), file: file, line: line)
|
|
XCTAssertEqual(first.day, second.day, message(), file: file, line: line)
|
|
XCTAssertEqual(first.dayOfYear, second.dayOfYear, message(), file: file, line: line)
|
|
XCTAssertEqual(first.hour, second.hour, message(), file: file, line: line)
|
|
XCTAssertEqual(first.minute, second.minute, message(), file: file, line: line)
|
|
XCTAssertEqual(first.second, second.second, message(), file: file, line: line)
|
|
XCTAssertEqual(first.weekday, second.weekday, message(), file: file, line: line)
|
|
XCTAssertEqual(first.weekdayOrdinal, second.weekdayOrdinal, message(), file: file, line: line)
|
|
XCTAssertEqual(first.weekOfMonth, second.weekOfMonth, message(), file: file, line: line)
|
|
XCTAssertEqual(first.weekOfYear, second.weekOfYear, message(), file: file, line: line)
|
|
XCTAssertEqual(first.yearForWeekOfYear, second.yearForWeekOfYear, message(), file: file, line: line)
|
|
if expectQuarter {
|
|
XCTAssertEqual(first.quarter, second.quarter, message(), file: file, line: line)
|
|
}
|
|
|
|
if let ns = first.nanosecond, let otherNS = second.nanosecond {
|
|
XCTAssertLessThanOrEqual(abs(ns - otherNS), nanosecondAccuracy, message(), file: file, line: line)
|
|
} else {
|
|
XCTAssertEqual(first.nanosecond, second.nanosecond, message(), file: file, line: line)
|
|
}
|
|
|
|
XCTAssertEqual(first.isLeapMonth, second.isLeapMonth, message(), file: file, line: line)
|
|
|
|
if expectCalendar {
|
|
XCTAssertEqual(first.calendar, second.calendar, message(), file: file, line: line)
|
|
}
|
|
|
|
XCTAssertEqual(first.timeZone, second.timeZone, message(), file: file, line: line)
|
|
|
|
}
|
|
|
|
func expectChanges<T: BinaryInteger>(_ check: @autoclosure () -> T, by difference: T? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, _ expression: () throws -> ()) rethrows {
|
|
let valueBefore = check()
|
|
try expression()
|
|
let valueAfter = check()
|
|
if let difference = difference {
|
|
XCTAssertEqual(valueAfter, valueBefore + difference, message(), file: file, line: line)
|
|
} else {
|
|
XCTAssertNotEqual(valueAfter, valueBefore, message(), file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func expectNoChanges<T: BinaryInteger>(_ check: @autoclosure () -> T, by difference: T? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, _ expression: () throws -> ()) rethrows {
|
|
let valueBefore = check()
|
|
try expression()
|
|
let valueAfter = check()
|
|
if let difference = difference {
|
|
XCTAssertNotEqual(valueAfter, valueBefore + difference, message(), file: file, line: line)
|
|
} else {
|
|
XCTAssertEqual(valueAfter, valueBefore, message(), file: file, line: line)
|
|
}
|
|
}
|
|
|
|
/// Test that the elements of `instances` satisfy the semantic
|
|
/// requirements of `Equatable`, using `oracle` to generate equality
|
|
/// expectations from pairs of positions in `instances`.
|
|
///
|
|
/// - Note: `oracle` is also checked for conformance to the
|
|
/// laws.
|
|
public func checkEquatable<Instances: Collection>(
|
|
_ instances: Instances,
|
|
oracle: (Instances.Index, Instances.Index) -> Bool,
|
|
allowBrokenTransitivity: Bool = false,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) where Instances.Element: Equatable {
|
|
let indices = Array(instances.indices)
|
|
_checkEquatableImpl(
|
|
Array(instances),
|
|
oracle: { oracle(indices[$0], indices[$1]) },
|
|
allowBrokenTransitivity: allowBrokenTransitivity,
|
|
message(),
|
|
file: file,
|
|
line: line)
|
|
}
|
|
|
|
private class Box<T> {
|
|
var value: T
|
|
|
|
init(_ value: T) {
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
internal func _checkEquatableImpl<Instance : Equatable>(
|
|
_ instances: [Instance],
|
|
oracle: (Int, Int) -> Bool,
|
|
allowBrokenTransitivity: Bool = false,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) {
|
|
// For each index (which corresponds to an instance being tested) track the
|
|
// set of equal instances.
|
|
var transitivityScoreboard: [Box<Set<Int>>] =
|
|
instances.indices.map { _ in Box([]) }
|
|
|
|
for i in instances.indices {
|
|
let x = instances[i]
|
|
expectTrue(oracle(i, i), "bad oracle: broken reflexivity at index \(i)")
|
|
|
|
for j in instances.indices {
|
|
let y = instances[j]
|
|
|
|
let predictedXY = oracle(i, j)
|
|
expectEqual(
|
|
predictedXY, oracle(j, i),
|
|
"bad oracle: broken symmetry between indices \(i), \(j)",
|
|
file: file,
|
|
line: line)
|
|
|
|
let isEqualXY = x == y
|
|
expectEqual(
|
|
predictedXY, isEqualXY,
|
|
"""
|
|
\((predictedXY
|
|
? "expected equal, found not equal"
|
|
: "expected not equal, found equal"))
|
|
lhs (at index \(i)): \(String(reflecting: x))
|
|
rhs (at index \(j)): \(String(reflecting: y))
|
|
""",
|
|
file: file,
|
|
line: line)
|
|
|
|
// Not-equal is an inverse of equal.
|
|
expectNotEqual(
|
|
isEqualXY, x != y,
|
|
"""
|
|
lhs (at index \(i)): \(String(reflecting: x))
|
|
rhs (at index \(j)): \(String(reflecting: y))
|
|
""",
|
|
file: file,
|
|
line: line)
|
|
|
|
if !allowBrokenTransitivity {
|
|
// Check transitivity of the predicate represented by the oracle.
|
|
// If we are adding the instance `j` into an equivalence set, check that
|
|
// it is equal to every other instance in the set.
|
|
if predictedXY && i < j && transitivityScoreboard[i].value.insert(j).inserted {
|
|
if transitivityScoreboard[i].value.count == 1 {
|
|
transitivityScoreboard[i].value.insert(i)
|
|
}
|
|
for k in transitivityScoreboard[i].value {
|
|
expectTrue(
|
|
oracle(j, k),
|
|
"bad oracle: broken transitivity at indices \(i), \(j), \(k)",
|
|
file: file,
|
|
line: line)
|
|
// No need to check equality between actual values, we will check
|
|
// them with the checks above.
|
|
}
|
|
precondition(transitivityScoreboard[j].value.isEmpty)
|
|
transitivityScoreboard[j] = transitivityScoreboard[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func hash<H: Hashable>(_ value: H, salt: Int? = nil) -> Int {
|
|
var hasher = Hasher()
|
|
if let salt = salt {
|
|
hasher.combine(salt)
|
|
}
|
|
hasher.combine(value)
|
|
return hasher.finalize()
|
|
}
|
|
|
|
public func checkHashable<Instances: Collection>(
|
|
_ instances: Instances,
|
|
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
|
|
allowIncompleteHashing: Bool = false,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: StaticString = #filePath, line: UInt = #line
|
|
) where Instances.Element: Hashable {
|
|
checkHashable(
|
|
instances,
|
|
equalityOracle: equalityOracle,
|
|
hashEqualityOracle: equalityOracle,
|
|
allowIncompleteHashing: allowIncompleteHashing,
|
|
message(),
|
|
file: file,
|
|
line: line)
|
|
}
|
|
|
|
|
|
public func checkHashable<Instances: Collection>(
|
|
_ instances: Instances,
|
|
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
|
|
hashEqualityOracle: (Instances.Index, Instances.Index) -> Bool,
|
|
allowIncompleteHashing: Bool = false,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: StaticString = #filePath, line: UInt = #line
|
|
) where Instances.Element: Hashable {
|
|
|
|
checkEquatable(
|
|
instances,
|
|
oracle: equalityOracle,
|
|
message(),
|
|
file: file,
|
|
line: line)
|
|
|
|
for i in instances.indices {
|
|
let x = instances[i]
|
|
for j in instances.indices {
|
|
let y = instances[j]
|
|
let predicted = hashEqualityOracle(i, j)
|
|
XCTAssertEqual(
|
|
predicted,
|
|
hashEqualityOracle(j, i),
|
|
"bad hash oracle: broken symmetry between indices \(i), \(j)",
|
|
file: file, line: line)
|
|
if x == y {
|
|
XCTAssertTrue(
|
|
predicted,
|
|
"""
|
|
bad hash oracle: equality must imply hash equality
|
|
lhs (at index \(i)): \(x)
|
|
rhs (at index \(j)): \(y)
|
|
""",
|
|
file: file, line: line)
|
|
}
|
|
if predicted {
|
|
XCTAssertEqual(
|
|
hash(x), hash(y),
|
|
"""
|
|
hash(into:) expected to match, found to differ
|
|
lhs (at index \(i)): \(x)
|
|
rhs (at index \(j)): \(y)
|
|
""",
|
|
file: file, line: line)
|
|
XCTAssertEqual(
|
|
x.hashValue, y.hashValue,
|
|
"""
|
|
hashValue expected to match, found to differ
|
|
lhs (at index \(i)): \(x)
|
|
rhs (at index \(j)): \(y)
|
|
""",
|
|
file: file, line: line)
|
|
XCTAssertEqual(
|
|
x._rawHashValue(seed: 0), y._rawHashValue(seed: 0),
|
|
"""
|
|
_rawHashValue(seed:) expected to match, found to differ
|
|
lhs (at index \(i)): \(x)
|
|
rhs (at index \(j)): \(y)
|
|
""",
|
|
file: file, line: line)
|
|
} else if !allowIncompleteHashing {
|
|
// Try a few different seeds; at least one of them should discriminate
|
|
// between the hashes. It is extremely unlikely this check will fail
|
|
// all ten attempts, unless the type's hash encoding is not unique,
|
|
// or unless the hash equality oracle is wrong.
|
|
XCTAssertTrue(
|
|
(0..<10).contains { hash(x, salt: $0) != hash(y, salt: $0) },
|
|
"""
|
|
hash(into:) expected to differ, found to match
|
|
lhs (at index \(i)): \(x)
|
|
rhs (at index \(j)): \(y)
|
|
""",
|
|
file: file, line: line)
|
|
XCTAssertTrue(
|
|
(0..<10).contains { i in
|
|
x._rawHashValue(seed: i) != y._rawHashValue(seed: i)
|
|
},
|
|
"""
|
|
_rawHashValue(seed:) expected to differ, found to match
|
|
lhs (at index \(i)): \(x)
|
|
rhs (at index \(j)): \(y)
|
|
""",
|
|
file: file, line: line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test that the elements of `groups` consist of instances that satisfy the
|
|
/// semantic requirements of `Hashable`, with each group defining a distinct
|
|
/// equivalence class under `==`.
|
|
public func checkHashableGroups<Groups: Collection>(
|
|
_ groups: Groups,
|
|
_ message: @autoclosure () -> String = "",
|
|
allowIncompleteHashing: Bool = false,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) where Groups.Element: Collection, Groups.Element.Element: Hashable {
|
|
let instances = groups.flatMap { $0 }
|
|
// groupIndices[i] is the index of the element in groups that contains
|
|
// instances[i].
|
|
let groupIndices =
|
|
zip(0..., groups).flatMap { i, group in group.map { _ in i } }
|
|
func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
|
|
return groupIndices[lhs] == groupIndices[rhs]
|
|
}
|
|
checkHashable(
|
|
instances,
|
|
equalityOracle: equalityOracle,
|
|
hashEqualityOracle: equalityOracle,
|
|
allowIncompleteHashing: allowIncompleteHashing,
|
|
file: file,
|
|
line: line)
|
|
}
|
|
|
|
private var shouldRunXFailTests: Bool {
|
|
// FIXME: Reenable after ProcessInfo is migrated
|
|
// return ProcessInfo.processInfo.environment["NS_FOUNDATION_ATTEMPT_XFAIL_TESTS"] == "YES"
|
|
return false
|
|
}
|
|
|
|
func shouldAttemptXFailTests(_ reason: String) -> Bool {
|
|
if shouldRunXFailTests {
|
|
return true
|
|
} else {
|
|
print("warning: Skipping test expected to fail with reason '\(reason)'\n")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func shouldAttemptWindowsXFailTests(_ reason: String) -> Bool {
|
|
#if os(Windows)
|
|
return shouldAttemptXFailTests(reason)
|
|
#else
|
|
return true
|
|
#endif
|
|
}
|
|
|
|
func shouldAttemptAndroidXFailTests(_ reason: String) -> Bool {
|
|
#if os(Android)
|
|
return shouldAttemptXFailTests(reason)
|
|
#else
|
|
return true
|
|
#endif
|
|
}
|
|
|
|
func shouldAttemptOpenBSDXFailTests(_ reason: String) -> Bool {
|
|
#if os(OpenBSD)
|
|
return shouldAttemptXFailTests(reason)
|
|
#else
|
|
return true
|
|
#endif
|
|
}
|
|
|
|
func testExpectedToFail<T>(_ test: @escaping (T) -> () throws -> Void, _ reason: String) -> (T) -> () throws -> Void {
|
|
testExpectedToFailWithCheck(check: shouldAttemptXFailTests(_:), test, reason)
|
|
}
|
|
|
|
func testExpectedToFailOnWindows<T>(_ test: @escaping (T) -> () throws -> Void, _ reason: String) -> (T) -> () throws -> Void {
|
|
testExpectedToFailWithCheck(check: shouldAttemptWindowsXFailTests(_:), test, reason)
|
|
}
|
|
|
|
func testExpectedToFailOnAndroid<T>(_ test: @escaping (T) -> () throws -> Void, _ reason: String) -> (T) -> () throws -> Void {
|
|
testExpectedToFailWithCheck(check: shouldAttemptAndroidXFailTests(_:), test, reason)
|
|
}
|
|
|
|
func testExpectedToFailOnOpenBSD<T>(_ test: @escaping (T) -> () throws -> Void, _ reason: String) -> (T) -> () throws -> Void {
|
|
testExpectedToFailWithCheck(check: shouldAttemptOpenBSDXFailTests(_:), test, reason)
|
|
}
|
|
|
|
func testExpectedToFailWithCheck<T>(check: (String) -> Bool, _ test: @escaping (T) -> () throws -> Void, _ reason: String) -> (T) -> () throws -> Void {
|
|
if check(reason) {
|
|
return test
|
|
} else {
|
|
return { _ in return { } }
|
|
}
|
|
}
|
|
|