add swiftUI code
This commit is contained in:
@@ -0,0 +1,829 @@
|
||||
// Inspired by https://github.com/groue/CombineExpectations
|
||||
import XCTest
|
||||
@testable import GRDB
|
||||
|
||||
// MARK: - ValueObservationRecorder
|
||||
|
||||
public class ValueObservationRecorder<Value> {
|
||||
private struct RecorderExpectation {
|
||||
var expectation: XCTestExpectation
|
||||
var remainingCount: Int? // nil for error expectation
|
||||
var isIncluded: ((Value) -> Bool)? // nil for error expectation
|
||||
}
|
||||
|
||||
/// The recorder state
|
||||
private struct State {
|
||||
var values: [Value]
|
||||
var error: Error?
|
||||
var recorderExpectation: RecorderExpectation?
|
||||
var cancellable: AnyDatabaseCancellable?
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var state = State(values: [], recorderExpectation: nil, cancellable: nil)
|
||||
private var consumedCount = 0
|
||||
|
||||
/// Internal for testability. Use ValueObservation.record(in:) instead.
|
||||
init() { }
|
||||
|
||||
private func synchronized<T>(_ execute: () throws -> T) rethrows -> T {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return try execute()
|
||||
}
|
||||
|
||||
// MARK: ValueObservation API
|
||||
|
||||
// Internal for testability.
|
||||
func onChange(_ value: Value) {
|
||||
return synchronized {
|
||||
if state.error != nil {
|
||||
// This is possible with ValueObservation, but not supported by ValueObservationRecorder
|
||||
XCTFail("ValueObservationRecorder got unexpected value after error: \(String(reflecting: value))")
|
||||
}
|
||||
|
||||
state.values.append(value)
|
||||
|
||||
if let exp = state.recorderExpectation,
|
||||
let remainingCount = exp.remainingCount,
|
||||
let isIncluded = exp.isIncluded
|
||||
{
|
||||
assert(remainingCount > 0)
|
||||
if isIncluded(value) {
|
||||
exp.expectation.fulfill()
|
||||
if remainingCount > 1 {
|
||||
state.recorderExpectation = RecorderExpectation(
|
||||
expectation: exp.expectation,
|
||||
remainingCount: remainingCount - 1,
|
||||
isIncluded: isIncluded)
|
||||
} else {
|
||||
state.recorderExpectation = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for testability.
|
||||
func onError(_ error: Error) {
|
||||
return synchronized {
|
||||
if state.error != nil {
|
||||
// This is possible with ValueObservation, but not supported by ValueObservationRecorder
|
||||
XCTFail("f got unexpected error after error: \(String(describing: error))")
|
||||
}
|
||||
|
||||
if let exp = state.recorderExpectation {
|
||||
exp.expectation.fulfill(count: exp.remainingCount ?? 1)
|
||||
state.recorderExpectation = nil
|
||||
}
|
||||
state.error = error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ValueObservationExpectation API
|
||||
|
||||
func fulfillOnValue(_ expectation: XCTestExpectation, includingConsumed: Bool, isIncluded: @escaping (Value) -> Bool) {
|
||||
synchronized {
|
||||
preconditionCanFulfillExpectation()
|
||||
|
||||
let expectedFulfillmentCount = expectation.expectedFulfillmentCount
|
||||
|
||||
if state.error != nil {
|
||||
expectation.fulfill(count: expectedFulfillmentCount)
|
||||
return
|
||||
}
|
||||
|
||||
let values = state.values.filter(isIncluded)
|
||||
let consumedValues = state.values[0..<consumedCount].filter(isIncluded)
|
||||
let maxFulfillmentCount = includingConsumed
|
||||
? values.count
|
||||
: values.count - consumedValues.count
|
||||
let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount)
|
||||
expectation.fulfill(count: fulfillmentCount)
|
||||
|
||||
let remainingCount = expectedFulfillmentCount - fulfillmentCount
|
||||
if remainingCount > 0 {
|
||||
state.recorderExpectation = RecorderExpectation(
|
||||
expectation: expectation,
|
||||
remainingCount: remainingCount,
|
||||
isIncluded: isIncluded)
|
||||
} else {
|
||||
state.recorderExpectation = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fulfillOnError(_ expectation: XCTestExpectation) {
|
||||
synchronized {
|
||||
preconditionCanFulfillExpectation()
|
||||
|
||||
if state.error != nil {
|
||||
expectation.fulfill()
|
||||
return
|
||||
}
|
||||
|
||||
state.recorderExpectation = RecorderExpectation(
|
||||
expectation: expectation,
|
||||
remainingCount: nil,
|
||||
isIncluded: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a value based on the recorded state.
|
||||
///
|
||||
/// - parameter value: A function which returns the value, given the
|
||||
/// recorded state.
|
||||
/// - parameter values: All recorded values.
|
||||
/// - parameter remainingValues: The values that were not consumed yet.
|
||||
/// - parameter consume: A function which consumes values.
|
||||
/// - parameter count: The number of consumed values.
|
||||
/// - returns: The value
|
||||
func value<T>(_ value: (
|
||||
_ values: [Value],
|
||||
_ error: Error?,
|
||||
_ remainingValues: ArraySlice<Value>,
|
||||
_ consume: (_ count: Int) -> ()) throws -> T)
|
||||
rethrows -> T
|
||||
{
|
||||
try synchronized {
|
||||
let values = state.values
|
||||
let remainingValues = values[consumedCount...]
|
||||
return try value(values, state.error, remainingValues, { count in
|
||||
precondition(count >= 0)
|
||||
precondition(count <= remainingValues.count)
|
||||
consumedCount += count
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that recorder can fulfill an expectation.
|
||||
///
|
||||
/// The reason this method exists is that a recorder can fulfill a single
|
||||
/// expectation at a given time. It is a programmer error to wait for two
|
||||
/// expectations concurrently.
|
||||
///
|
||||
/// This method MUST be called within a synchronized block.
|
||||
private func preconditionCanFulfillExpectation() {
|
||||
if let exp = state.recorderExpectation {
|
||||
// We are already waiting for an expectation! Is it a programmer
|
||||
// error? Recorder drops references to non-inverted expectations
|
||||
// when they are fulfilled. But inverted expectations are not
|
||||
// fulfilled, and thus not dropped. We can't quite know if an
|
||||
// inverted expectations has expired yet, so just let it go.
|
||||
precondition(exp.expectation.isInverted, "Already waiting for an expectation")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func receive(_ cancellable: AnyDatabaseCancellable) {
|
||||
synchronized {
|
||||
if state.cancellable != nil {
|
||||
XCTFail("ValueObservationRecorder is already observing")
|
||||
}
|
||||
state.cancellable = cancellable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ValueObservationRecorder + Expectations
|
||||
|
||||
extension ValueObservationRecorder {
|
||||
public func failure() -> ValueObservationExpectations.Failure<Value> {
|
||||
ValueObservationExpectations.Failure(recorder: self)
|
||||
}
|
||||
|
||||
public func next() -> ValueObservationExpectations.NextOne<Value> {
|
||||
ValueObservationExpectations.NextOne(recorder: self)
|
||||
}
|
||||
|
||||
public func next(_ count: Int) -> ValueObservationExpectations.Next<Value> {
|
||||
ValueObservationExpectations.Next(recorder: self, count: count)
|
||||
}
|
||||
|
||||
public func prefix(_ maxLength: Int) -> ValueObservationExpectations.Prefix<Value> {
|
||||
ValueObservationExpectations.Prefix(
|
||||
recorder: self,
|
||||
expectedFulfillmentCount: maxLength,
|
||||
isIncluded: { _ in true })
|
||||
}
|
||||
|
||||
public func prefix(until predicate: @escaping (Value) -> Bool) -> ValueObservationExpectations.Prefix<Value> {
|
||||
ValueObservationExpectations.Prefix(
|
||||
recorder: self,
|
||||
expectedFulfillmentCount: 1,
|
||||
isIncluded: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ValueObservation + ValueObservationRecorder
|
||||
|
||||
extension ValueObservation {
|
||||
public func record(
|
||||
in reader: some DatabaseReader,
|
||||
scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main),
|
||||
onError: ((Error) -> Void)? = nil,
|
||||
onChange: ((Reducer.Value) -> Void)? = nil)
|
||||
-> ValueObservationRecorder<Reducer.Value>
|
||||
where Reducer: ValueReducer
|
||||
{
|
||||
let recorder = ValueObservationRecorder<Reducer.Value>()
|
||||
let cancellable = start(
|
||||
in: reader,
|
||||
scheduling: scheduler,
|
||||
onError: { [weak recorder] in
|
||||
onError?($0)
|
||||
recorder?.onError($0)
|
||||
},
|
||||
onChange: { [weak recorder] in
|
||||
onChange?($0)
|
||||
recorder?.onChange($0)
|
||||
})
|
||||
recorder.receive(cancellable)
|
||||
return recorder
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ValueObservationExpectation
|
||||
|
||||
public enum ValueRecordingError: Error {
|
||||
case notEnoughValues
|
||||
case notFailed
|
||||
}
|
||||
|
||||
extension ValueRecordingError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .notEnoughValues:
|
||||
return "ValueRecordingError.notEnoughValues"
|
||||
case .notFailed:
|
||||
return "ValueRecordingError.notFailed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol _ValueObservationExpectationBase {
|
||||
func _setup(_ expectation: XCTestExpectation)
|
||||
}
|
||||
|
||||
public protocol ValueObservationExpectation: _ValueObservationExpectationBase {
|
||||
associatedtype Output
|
||||
func get() throws -> Output
|
||||
}
|
||||
|
||||
// MARK: - XCTestCase + ValueObservationExpectation
|
||||
|
||||
extension XCTestCase {
|
||||
public func wait<E: ValueObservationExpectation>(
|
||||
for valueObservationExpectation: E,
|
||||
timeout: TimeInterval,
|
||||
description: String = "")
|
||||
throws -> E.Output
|
||||
{
|
||||
let expectation = self.expectation(description: description)
|
||||
valueObservationExpectation._setup(expectation)
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
return try valueObservationExpectation.get()
|
||||
}
|
||||
|
||||
/// This test checks the fundamental promise of ValueObservation by
|
||||
/// comparing recorded values with expected values.
|
||||
///
|
||||
/// Recorded values match the expected values if and only if:
|
||||
///
|
||||
/// - The last recorded value is the last expected value
|
||||
/// - Recorded values are in the same order as expected values
|
||||
///
|
||||
/// However, both missing and repeated values are allowed - with the only
|
||||
/// exception of the last expected value which can not be missed.
|
||||
///
|
||||
/// For example, if the expected values are [0, 1], then the following
|
||||
/// recorded values match:
|
||||
///
|
||||
/// - `[0, 1]` (identical values)
|
||||
/// - `[1]` (missing value but the last one)
|
||||
/// - `[0, 0, 1, 1]` (repeated value)
|
||||
///
|
||||
/// However the following recorded values don't match, and fail the test:
|
||||
///
|
||||
/// - `[1, 0]` (wrong order)
|
||||
/// - `[0]` (missing last value)
|
||||
/// - `[]` (missing last value)
|
||||
/// - `[0, 1, 2]` (unexpected value)
|
||||
/// - `[1, 0, 1]` (unexpected value)
|
||||
func assertValueObservationRecordingMatch<R, E>(
|
||||
recorded: R,
|
||||
expected: E,
|
||||
_ message: @autoclosure () -> String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
where
|
||||
R: Collection,
|
||||
E: Collection,
|
||||
R.Element == E.Element,
|
||||
R.Element: Equatable
|
||||
{
|
||||
XCTAssertTrue(
|
||||
valueObservationRecordingMatch(recorded: recorded, expected: expected),
|
||||
"Unexpected recording \(Array(recorded)) - \(message())",
|
||||
file: file, line: line)
|
||||
}
|
||||
|
||||
func valueObservationRecordingMatch<R, E>(
|
||||
recorded: R,
|
||||
expected: E)
|
||||
-> Bool
|
||||
where R: Collection,
|
||||
E: Collection,
|
||||
R.Element == E.Element,
|
||||
R.Element: Equatable
|
||||
{
|
||||
guard let first = recorded.first else {
|
||||
return expected.isEmpty
|
||||
}
|
||||
|
||||
return expected.indices.lazy
|
||||
.filter { expected[$0] == first }
|
||||
.contains {
|
||||
valueObservationRecordingMatch(
|
||||
recorded: recorded.drop(while: { $0 == first }),
|
||||
expected: expected[$0...].dropFirst())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GRDBTestCase + ValueObservationExpectation
|
||||
|
||||
extension GRDBTestCase {
|
||||
func assertValueObservation<Reducer: ValueReducer>(
|
||||
_ observation: ValueObservation<Reducer>,
|
||||
records expectedValues: [Reducer.Value],
|
||||
setup: (Database) throws -> Void,
|
||||
recordedUpdates: @escaping (Database) throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
throws
|
||||
where Reducer.Value: Equatable
|
||||
{
|
||||
func test(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
testValueDispatching: @escaping () -> Void) throws
|
||||
{
|
||||
func testRecordingEqualWhenWriteAfterStart(writer: some DatabaseWriter) throws {
|
||||
try writer.write(setup)
|
||||
|
||||
var value: Reducer.Value?
|
||||
let recorder = observation.record(
|
||||
in: writer,
|
||||
scheduling: scheduler,
|
||||
onChange: {
|
||||
testValueDispatching()
|
||||
value = $0
|
||||
})
|
||||
|
||||
// Test that initial value is set when scheduler is immediate
|
||||
if scheduler.immediateInitialValue() {
|
||||
XCTAssertNotNil(value)
|
||||
}
|
||||
|
||||
// Perform writes after start
|
||||
try writer.writeWithoutTransaction(recordedUpdates)
|
||||
|
||||
let expectation = recorder.next(expectedValues.count)
|
||||
let values = try wait(for: expectation, timeout: 5)
|
||||
XCTAssertEqual(
|
||||
values, expectedValues,
|
||||
"\(#function), \(writer), \(scheduler)", file: file, line: line)
|
||||
}
|
||||
|
||||
func testRecordingEqualWhenWriteAfterFirstValue(writer: some DatabaseWriter) throws {
|
||||
try writer.write(setup)
|
||||
|
||||
var valueCount = 0
|
||||
var value: Reducer.Value?
|
||||
let recorder = observation.record(
|
||||
in: writer,
|
||||
scheduling: scheduler,
|
||||
onChange: { [unowned writer] in
|
||||
testValueDispatching()
|
||||
valueCount += 1
|
||||
if valueCount == 1 {
|
||||
// Perform writes after initial value
|
||||
try! writer.writeWithoutTransaction(recordedUpdates)
|
||||
}
|
||||
value = $0
|
||||
})
|
||||
|
||||
// Test that initial value is set when scheduler is immediate
|
||||
if scheduler.immediateInitialValue() {
|
||||
XCTAssertNotNil(value)
|
||||
}
|
||||
|
||||
let expectation = recorder.next(expectedValues.count)
|
||||
let values = try wait(for: expectation, timeout: 5)
|
||||
XCTAssertEqual(
|
||||
values, expectedValues,
|
||||
"\(#function), \(writer), \(scheduler)", file: file, line: line)
|
||||
}
|
||||
|
||||
func testRecordingMatchWhenWriteAfterStart(writer: some DatabaseWriter) throws {
|
||||
try writer.write(setup)
|
||||
|
||||
var value: Reducer.Value?
|
||||
let recorder = observation.record(
|
||||
in: writer,
|
||||
scheduling: scheduler,
|
||||
onChange: {
|
||||
testValueDispatching()
|
||||
value = $0
|
||||
})
|
||||
|
||||
// Test that initial value is set when scheduler is immediate
|
||||
if scheduler.immediateInitialValue() {
|
||||
XCTAssertNotNil(value)
|
||||
}
|
||||
|
||||
try writer.writeWithoutTransaction(recordedUpdates)
|
||||
|
||||
let recordedValues: [Reducer.Value]
|
||||
let lastExpectedValue = expectedValues.last!
|
||||
let waitForLast = expectedValues.firstIndex(of: lastExpectedValue) == expectedValues.count - 1
|
||||
if waitForLast {
|
||||
// Optimization!
|
||||
let expectation = recorder.prefix(until: { $0 == lastExpectedValue } )
|
||||
recordedValues = try wait(for: expectation, timeout: 5)
|
||||
} else {
|
||||
// Slow!
|
||||
assertionFailure("Please rewrite your test, because it is too slow: make sure the last expected value is unique.")
|
||||
let expectation = recorder
|
||||
.prefix(expectedValues.count + 2 /* pool may perform double initial fetch */)
|
||||
.inverted
|
||||
recordedValues = try wait(for: expectation, timeout: 5)
|
||||
}
|
||||
|
||||
if scheduler.immediateInitialValue() {
|
||||
XCTAssertEqual(recordedValues.first, expectedValues.first)
|
||||
}
|
||||
|
||||
assertValueObservationRecordingMatch(
|
||||
recorded: recordedValues,
|
||||
expected: expectedValues,
|
||||
"\(#function), \(writer), \(scheduler)", file: file, line: line)
|
||||
}
|
||||
|
||||
func testRecordingMatchWhenWriteAfterFirstValue(writer: some DatabaseWriter) throws {
|
||||
try writer.write(setup)
|
||||
|
||||
var valueCount = 0
|
||||
var value: Reducer.Value?
|
||||
let recorder = observation.record(
|
||||
in: writer,
|
||||
scheduling: scheduler,
|
||||
onChange: { [unowned writer] in
|
||||
testValueDispatching()
|
||||
valueCount += 1
|
||||
if valueCount == 1 {
|
||||
// Perform writes after initial value
|
||||
try! writer.writeWithoutTransaction(recordedUpdates)
|
||||
}
|
||||
value = $0
|
||||
})
|
||||
|
||||
// Test that initial value is set when scheduler is immediate
|
||||
if scheduler.immediateInitialValue() {
|
||||
XCTAssertNotNil(value)
|
||||
}
|
||||
|
||||
let recordedValues: [Reducer.Value]
|
||||
let lastExpectedValue = expectedValues.last!
|
||||
let waitForLast = expectedValues.firstIndex(of: lastExpectedValue) == expectedValues.count - 1
|
||||
if waitForLast {
|
||||
// Optimization!
|
||||
let expectation = recorder.prefix(until: { $0 == lastExpectedValue } )
|
||||
recordedValues = try wait(for: expectation, timeout: 5)
|
||||
} else {
|
||||
// Slow!
|
||||
assertionFailure("Please rewrite your test, because it is too slow: make sure the last expected value is unique.")
|
||||
let expectation = recorder
|
||||
.prefix(expectedValues.count + 2 /* pool may perform double initial fetch */)
|
||||
.inverted
|
||||
recordedValues = try wait(for: expectation, timeout: 5)
|
||||
}
|
||||
|
||||
XCTAssertEqual(recordedValues.first, expectedValues.first)
|
||||
|
||||
assertValueObservationRecordingMatch(
|
||||
recorded: recordedValues,
|
||||
expected: expectedValues,
|
||||
"\(#function), \(writer), \(scheduler)", file: file, line: line)
|
||||
}
|
||||
|
||||
try testRecordingEqualWhenWriteAfterStart(writer: DatabaseQueue())
|
||||
try testRecordingEqualWhenWriteAfterFirstValue(writer: DatabaseQueue())
|
||||
|
||||
try testRecordingEqualWhenWriteAfterStart(writer: makeDatabaseQueue())
|
||||
try testRecordingEqualWhenWriteAfterFirstValue(writer: makeDatabaseQueue())
|
||||
|
||||
if observation.requiresWriteAccess {
|
||||
try testRecordingEqualWhenWriteAfterStart(writer: makeDatabasePool())
|
||||
try testRecordingEqualWhenWriteAfterFirstValue(writer: makeDatabasePool())
|
||||
} else {
|
||||
// DatabasePool may miss some changes
|
||||
try testRecordingMatchWhenWriteAfterStart(writer: makeDatabasePool())
|
||||
try testRecordingMatchWhenWriteAfterFirstValue(writer: makeDatabasePool())
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let key = DispatchSpecificKey<()>()
|
||||
DispatchQueue.main.setSpecific(key: key, value: ())
|
||||
|
||||
try test(
|
||||
observation: observation,
|
||||
scheduling: .immediate,
|
||||
testValueDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) })
|
||||
}
|
||||
|
||||
do {
|
||||
let key = DispatchSpecificKey<()>()
|
||||
DispatchQueue.main.setSpecific(key: key, value: ())
|
||||
|
||||
try test(
|
||||
observation: observation,
|
||||
scheduling: .async(onQueue: .main),
|
||||
testValueDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) })
|
||||
}
|
||||
|
||||
do {
|
||||
let queue = DispatchQueue(label: "custom")
|
||||
let key = DispatchSpecificKey<()>()
|
||||
queue.setSpecific(key: key, value: ())
|
||||
|
||||
try test(
|
||||
observation: observation,
|
||||
scheduling: .async(onQueue: queue),
|
||||
testValueDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) })
|
||||
}
|
||||
}
|
||||
|
||||
func assertValueObservation<Reducer: ValueReducer, Failure: Error>(
|
||||
_ observation: ValueObservation<Reducer>,
|
||||
fails testFailure: (Failure, any DatabaseWriter) throws -> Void,
|
||||
setup: (Database) throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
throws
|
||||
{
|
||||
func test(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
testErrorDispatching: @escaping () -> Void) throws
|
||||
{
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
try writer.write(setup)
|
||||
|
||||
let recorder = observation.record(
|
||||
in: writer,
|
||||
scheduling: scheduler,
|
||||
onError: { _ in testErrorDispatching() })
|
||||
|
||||
let (_, error) = try wait(for: recorder.failure(), timeout: 5)
|
||||
if let error = error as? Failure {
|
||||
try testFailure(error, writer)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
try test(writer: DatabaseQueue())
|
||||
try test(writer: makeDatabaseQueue())
|
||||
try test(writer: makeDatabasePool())
|
||||
}
|
||||
|
||||
do {
|
||||
let key = DispatchSpecificKey<()>()
|
||||
DispatchQueue.main.setSpecific(key: key, value: ())
|
||||
|
||||
try test(
|
||||
observation: observation,
|
||||
scheduling: .immediate,
|
||||
testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) })
|
||||
}
|
||||
|
||||
do {
|
||||
let key = DispatchSpecificKey<()>()
|
||||
DispatchQueue.main.setSpecific(key: key, value: ())
|
||||
|
||||
try test(
|
||||
observation: observation,
|
||||
scheduling: .async(onQueue: .main),
|
||||
testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) })
|
||||
}
|
||||
|
||||
do {
|
||||
let queue = DispatchQueue(label: "custom")
|
||||
let key = DispatchSpecificKey<()>()
|
||||
queue.setSpecific(key: key, value: ())
|
||||
|
||||
try test(
|
||||
observation: observation,
|
||||
scheduling: .async(onQueue: queue),
|
||||
testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ValueObservationExpectations
|
||||
|
||||
public enum ValueObservationExpectations { }
|
||||
|
||||
extension ValueObservationExpectations {
|
||||
|
||||
// MARK: Inverted
|
||||
|
||||
public struct Inverted<Base: ValueObservationExpectation>: ValueObservationExpectation {
|
||||
let base: Base
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
base._setup(expectation)
|
||||
expectation.isInverted.toggle()
|
||||
}
|
||||
|
||||
public func get() throws -> Base.Output {
|
||||
try base.get()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NextOne
|
||||
|
||||
public struct NextOne<Value>: ValueObservationExpectation {
|
||||
let recorder: ValueObservationRecorder<Value>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnValue(expectation, includingConsumed: false, isIncluded: { _ in true })
|
||||
}
|
||||
|
||||
public func get() throws -> Value {
|
||||
try recorder.value { (_, error, remainingValues, consume) in
|
||||
if let next = remainingValues.first {
|
||||
consume(1)
|
||||
return next
|
||||
}
|
||||
if let error {
|
||||
throw error
|
||||
} else {
|
||||
throw ValueRecordingError.notEnoughValues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var inverted: NextOneInverted<Value> {
|
||||
return NextOneInverted(recorder: recorder)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NextOneInverted
|
||||
|
||||
public struct NextOneInverted<Value>: ValueObservationExpectation {
|
||||
let recorder: ValueObservationRecorder<Value>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
expectation.isInverted = true
|
||||
recorder.fulfillOnValue(expectation, includingConsumed: false, isIncluded: { _ in true })
|
||||
}
|
||||
|
||||
public func get() throws {
|
||||
try recorder.value { (_, error, remainingValues, consume) in
|
||||
if remainingValues.isEmpty == false {
|
||||
return
|
||||
}
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Next
|
||||
|
||||
public struct Next<Value>: ValueObservationExpectation {
|
||||
let recorder: ValueObservationRecorder<Value>
|
||||
let count: Int
|
||||
|
||||
init(recorder: ValueObservationRecorder<Value>, count: Int) {
|
||||
precondition(count >= 0, "Invalid negative count")
|
||||
self.recorder = recorder
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
if count == 0 {
|
||||
// Such an expectation is immediately fulfilled, by essence.
|
||||
expectation.expectedFulfillmentCount = 1
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
expectation.expectedFulfillmentCount = count
|
||||
recorder.fulfillOnValue(expectation, includingConsumed: false, isIncluded: { _ in true })
|
||||
}
|
||||
}
|
||||
|
||||
public func get() throws -> [Value] {
|
||||
try recorder.value { (_, error, remainingValues, consume) in
|
||||
if remainingValues.count >= count {
|
||||
consume(count)
|
||||
return Array(remainingValues.prefix(count))
|
||||
}
|
||||
if let error {
|
||||
throw error
|
||||
} else {
|
||||
throw ValueRecordingError.notEnoughValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Prefix
|
||||
|
||||
public struct Prefix<Value>: ValueObservationExpectation {
|
||||
let recorder: ValueObservationRecorder<Value>
|
||||
let expectedFulfillmentCount: Int
|
||||
let isIncluded: (Value) -> Bool
|
||||
|
||||
init(recorder: ValueObservationRecorder<Value>, expectedFulfillmentCount: Int, isIncluded: @escaping (Value) -> Bool) {
|
||||
precondition(expectedFulfillmentCount >= 0, "Invalid negative count")
|
||||
self.recorder = recorder
|
||||
self.expectedFulfillmentCount = expectedFulfillmentCount
|
||||
self.isIncluded = isIncluded
|
||||
}
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
if expectedFulfillmentCount == 0 {
|
||||
// Such an expectation is immediately fulfilled, by essence.
|
||||
expectation.expectedFulfillmentCount = 1
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
expectation.expectedFulfillmentCount = expectedFulfillmentCount
|
||||
recorder.fulfillOnValue(expectation, includingConsumed: true, isIncluded: isIncluded)
|
||||
}
|
||||
}
|
||||
|
||||
public func get() throws -> [Value] {
|
||||
if expectedFulfillmentCount == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
return try recorder.value { (values, error, remainingValues, consume) in
|
||||
let includedValues = values.filter(isIncluded)
|
||||
if includedValues.count >= expectedFulfillmentCount {
|
||||
let matchedCount = values
|
||||
.indices
|
||||
.filter { isIncluded(values[$0]) }
|
||||
.prefix(expectedFulfillmentCount)
|
||||
.last! + 1
|
||||
let extraCount = max(matchedCount + remainingValues.count - values.count, 0)
|
||||
consume(extraCount)
|
||||
return Array(values.prefix(matchedCount))
|
||||
}
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
consume(remainingValues.count)
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
public var inverted: Inverted<Self> {
|
||||
return Inverted(base: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Failure
|
||||
|
||||
public struct Failure<Value>: ValueObservationExpectation {
|
||||
let recorder: ValueObservationRecorder<Value>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnError(expectation)
|
||||
}
|
||||
|
||||
public func get() throws -> (values: [Value], error: Error) {
|
||||
try recorder.value { (values, error, remainingValues, consume) in
|
||||
if let error {
|
||||
consume(remainingValues.count)
|
||||
return (values: values, error: error)
|
||||
} else {
|
||||
throw ValueRecordingError.notFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
extension XCTestExpectation {
|
||||
fileprivate func fulfill(count: Int) {
|
||||
for _ in 0..<count {
|
||||
fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user