This commit is contained in:
zeus
2025-01-28 12:28:03 +08:00
parent 738c373a77
commit ec96756800
2534 changed files with 486292 additions and 0 deletions
@@ -0,0 +1,306 @@
#if canImport(Combine)
import Combine
import GRDB
import XCTest
private struct Player: Codable, FetchableRecord, PersistableRecord {
var id: Int64
var name: String
var score: Int?
static func createTable(_ db: Database) throws {
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer)
}
}
}
class DatabaseReaderReadPublisherTests : XCTestCase {
// MARK: -
func testReadPublisher() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(reader: some DatabaseReader) throws {
let publisher = reader.readPublisher(value: { db in
try Player.fetchCount(db)
})
let recorder = publisher.record()
let value = try wait(for: recorder.single, timeout: 5)
XCTAssertEqual(value, 0)
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
#endif
}
// MARK: -
// TODO: fix crasher
//
// * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x20)
// frame #0: 0x00007fff7115c6e8 libobjc.A.dylib`objc_retain + 24
// frame #1: 0x00007fff71c313a1 libswiftCore.dylib`swift::metadataimpl::ValueWitnesses<swift::metadataimpl::ObjCRetainableBox>::initializeWithCopy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) + 17
// frame #2: 0x000000010d926309 GRDB`outlined init with copy of Subscribers.Completion<A.Publisher.Failure> at <compiler-generated>:0
// frame #3: 0x000000010d92474b GRDB`ReceiveValuesOnSubscription._receive(completion=failure, self=0x00000001064c13c0) at ReceiveValuesOn.swift:184:9
// frame #4: 0x000000010d92392c GRDB`closure #1 in closure #1 in closure #1 in ReceiveValuesOnSubscription.receive(self=0x00000001064c13c0, completion=failure) at ReceiveValuesOn.swift:158:26
// frame #5: 0x00007fff71d88f49 libswiftDispatch.dylib`reabstraction thunk helper from @escaping @callee_guaranteed () -> () to @escaping @callee_unowned @convention(block) () -> () + 25
// frame #6: 0x00007fff722b76c4 libdispatch.dylib`_dispatch_call_block_and_release + 12
// frame #7: 0x00007fff722b8658 libdispatch.dylib`_dispatch_client_callout + 8
// frame #8: 0x00007fff722c3cab libdispatch.dylib`_dispatch_main_queue_callback_4CF + 936
// frame #9: 0x00007fff38299e81 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
// frame #10: 0x00007fff38259c87 CoreFoundation`__CFRunLoopRun + 2028
// frame #11: 0x00007fff38258e3e CoreFoundation`CFRunLoopRunSpecific + 462
// frame #12: 0x00000001003aa82b XCTest`-[XCTWaiter waitForExpectations:timeout:enforceOrder:] + 823
// frame #13: 0x0000000100324a04 XCTest`-[XCTestCase(AsynchronousTesting) waitForExpectations:timeout:enforceOrder:] + 102
// frame #14: 0x000000010961363d GRDBOSXTests`XCTestCase.wait<R>(publisherExpectation=GRDBOSXTests.PublisherExpectations.Recording<Swift.Array<GRDB.Row>, Swift.Error> @ 0x00007ffeefbfd2f0, timeout=1, description="", self=0x0000000101d8a9c0) at PublisherExpectation.swift:97:9
// frame #15: 0x000000010915dbcf GRDBOSXTests`test #1 (reader=0x000000010672f210, self=0x0000000101d8a9c0) in DatabaseReaderReadPublisherTests.testReadPublisherError() at DatabaseReaderReadPublisherTests.swift:62:33
// frame #16: 0x000000010915dfc0 GRDBOSXTests`partial apply for test #1 (reader:) in DatabaseReaderReadPublisherTests.testReadPublisherError() at <compiler-generated>:0
// frame #17: 0x000000010915c334 GRDBOSXTests`thunk for @escaping @callee_guaranteed (@guaranteed DatabaseReader) -> (@error @owned Error) at <compiler-generated>:0
// frame #18: 0x000000010915e014 GRDBOSXTests`thunk for @escaping @callee_guaranteed (@guaranteed DatabaseReader) -> (@error @owned Error)partial apply at <compiler-generated>:0
// frame #19: 0x00000001095a85f8 GRDBOSXTests`closure #1 in Test.init(context=0x000000010672f210, _0=<unavailable>, test=0x000000010915e000 GRDBOSXTests`reabstraction thunk helper from @escaping @callee_guaranteed (@guaranteed GRDB.DatabaseReader) -> (@error @owned Swift.Error) to @escaping @callee_guaranteed (@in_guaranteed GRDB.DatabaseReader) -> (@error @owned Swift.Error)partial apply forwarder with unmangled suffix ".16" at <compiler-generated>) at Support.swift:13:41
// frame #20: 0x00000001095a86af GRDBOSXTests`partial apply for closure #1 in Test.init(repeatCount:_:) at <compiler-generated>:0
// frame #21: 0x00000001095a8a8f GRDBOSXTests`Test.run(context=0x000000010915e880 GRDBOSXTests`reabstraction thunk helper from @callee_guaranteed () -> (@owned GRDB.DatabaseReader, @error @owned Swift.Error) to @escaping @callee_guaranteed () -> (@out GRDB.DatabaseReader, @error @owned Swift.Error)partial apply forwarder with unmangled suffix ".17" at <compiler-generated>, self=(repeatCount = 1, test = 0x00000001095a8690 GRDBOSXTests`partial apply forwarder for closure #1 (A, Swift.Int) throws -> () in GRDBOSXTests.Test.init(repeatCount: Swift.Int, _: (A) throws -> ()) -> GRDBOSXTests.Test<A> at <compiler-generated>)) at Support.swift:24:17
// * frame #22: 0x000000010915d4ec GRDBOSXTests`DatabaseReaderReadPublisherTests.testReadPublisherError(self=0x0000000101d8a9c0) at DatabaseReaderReadPublisherTests.swift:71:14
// frame #23: 0x000000010915f26a GRDBOSXTests`@objc DatabaseReaderReadPublisherTests.testReadPublisherError() at <compiler-generated>:0
// frame #24: 0x00007fff3823c8ac CoreFoundation`__invoking___ + 140
// frame #25: 0x00007fff3823c751 CoreFoundation`-[NSInvocation invoke] + 303
// frame #26: 0x0000000100339d3a XCTest`__24-[XCTestCase invokeTest]_block_invoke_3 + 52
// frame #27: 0x0000000100402215 XCTest`+[XCTSwiftErrorObservation observeErrorsInBlock:] + 69
// frame #28: 0x0000000100339c3c XCTest`__24-[XCTestCase invokeTest]_block_invoke_2 + 119
// frame #29: 0x00000001003c959a XCTest`-[XCTMemoryChecker _assertInvalidObjectsDeallocatedAfterScope:] + 65
// frame #30: 0x00000001003448ea XCTest`-[XCTestCase assertInvalidObjectsDeallocatedAfterScope:] + 61
// frame #31: 0x0000000100339b82 XCTest`__24-[XCTestCase invokeTest]_block_invoke.231 + 199
// frame #32: 0x00000001003ae6d8 XCTest`-[XCTestCase(XCTIssueHandling) _caughtUnhandledDeveloperExceptionPermittingControlFlowInterruptions:caughtInterruptionException:whileExecutingBlock:] + 179
// frame #33: 0x0000000100339645 XCTest`-[XCTestCase invokeTest] + 1037
// frame #34: 0x000000010033b023 XCTest`__26-[XCTestCase performTest:]_block_invoke_2 + 43
// frame #35: 0x00000001003ae6d8 XCTest`-[XCTestCase(XCTIssueHandling) _caughtUnhandledDeveloperExceptionPermittingControlFlowInterruptions:caughtInterruptionException:whileExecutingBlock:] + 179
// frame #36: 0x000000010033af5a XCTest`__26-[XCTestCase performTest:]_block_invoke.362 + 86
// frame #37: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
// frame #38: 0x000000010033a7c7 XCTest`-[XCTestCase performTest:] + 695
// frame #39: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
// frame #40: 0x0000000100334035 XCTest`__27-[XCTestSuite performTest:]_block_invoke + 329
// frame #41: 0x0000000100333856 XCTest`__59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
// frame #42: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
// frame #43: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
// frame #44: 0x000000010033380d XCTest`-[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
// frame #45: 0x0000000100333b11 XCTest`-[XCTestSuite performTest:] + 290
// frame #46: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
// frame #47: 0x0000000100334035 XCTest`__27-[XCTestSuite performTest:]_block_invoke + 329
// frame #48: 0x0000000100333856 XCTest`__59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
// frame #49: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
// frame #50: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
// frame #51: 0x000000010033380d XCTest`-[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
// frame #52: 0x0000000100333b11 XCTest`-[XCTestSuite performTest:] + 290
// frame #53: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
// frame #54: 0x0000000100334035 XCTest`__27-[XCTestSuite performTest:]_block_invoke + 329
// frame #55: 0x0000000100333856 XCTest`__59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
// frame #56: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
// frame #57: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
// frame #58: 0x000000010033380d XCTest`-[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
// frame #59: 0x0000000100333b11 XCTest`-[XCTestSuite performTest:] + 290
// frame #60: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
// frame #61: 0x00000001003dc8b5 XCTest`__44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke_2 + 148
// frame #62: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
// frame #63: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
// frame #64: 0x00000001003dc81a XCTest`__44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke + 111
// frame #65: 0x00000001003dc99b XCTest`__44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke.95 + 96
// frame #66: 0x000000010035acb8 XCTest`-[XCTestObservationCenter _observeTestExecutionForBlock:] + 325
// frame #67: 0x00000001003dc5e0 XCTest`-[XCTTestRunSession runTestsAndReturnError:] + 615
// frame #68: 0x0000000100317a7e XCTest`-[XCTestDriver _runTests] + 466
// frame #69: 0x00000001003bbb82 XCTest`_XCTestMain + 108
// frame #70: 0x0000000100002f07 xctest`main + 210
// frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1
// frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1
func testReadPublisherError() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(reader: some DatabaseReader) throws {
let publisher = reader.readPublisher(value: { db in
try Row.fetchAll(db, sql: "THIS IS NOT SQL")
})
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() }
#endif
}
// MARK: -
func testReadPublisherIsAsynchronous() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(reader: some DatabaseReader) throws {
let expectation = self.expectation(description: "")
let semaphore = DispatchSemaphore(value: 0)
let cancellable = reader
.readPublisher(value: { db in
try Player.fetchCount(db)
})
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
semaphore.wait()
expectation.fulfill()
})
semaphore.signal()
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
#endif
}
// MARK: -
func testReadPublisherDefaultScheduler() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(reader: some DatabaseReader) {
let expectation = self.expectation(description: "")
let cancellable = reader
.readPublisher(value: { db in
try Player.fetchCount(db)
})
.sink(
receiveCompletion: { completion in
dispatchPrecondition(condition: .onQueue(.main))
expectation.fulfill()
},
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(.main))
})
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
#endif
}
// MARK: -
func testReadPublisherCustomScheduler() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(reader: some DatabaseReader) {
let queue = DispatchQueue(label: "test")
let expectation = self.expectation(description: "")
let cancellable = reader
.readPublisher(receiveOn: queue, value: { db in
try Player.fetchCount(db)
})
.sink(
receiveCompletion: { completion in
dispatchPrecondition(condition: .onQueue(queue))
expectation.fulfill()
},
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(queue))
})
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
#endif
}
// MARK: -
func testReadPublisherIsReadonly() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(reader: some DatabaseReader) throws {
let publisher = reader.readPublisher(value: { db in
try Player.createTable(db)
})
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_READONLY)
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() }
#endif
}
}
#endif
@@ -0,0 +1,116 @@
#if canImport(Combine)
import Combine
import GRDB
import XCTest
private struct Player: Codable, FetchableRecord, PersistableRecord {
var id: Int64
var name: String
var score: Int?
static func createTable(_ db: Database) throws {
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer)
}
}
}
class DatabaseRegionObservationPublisherTests : XCTestCase {
func testChangesNotifications() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = DatabaseRegionObservation(tracking: Player.all())
.publisher(in: writer)
.tryMap(Player.fetchCount)
let recorder = publisher.record()
try writer.writeWithoutTransaction { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
try db.inTransaction {
try Player(id: 2, name: "Barbara", score: 750).insert(db)
try Player(id: 3, name: "Craig", score: 500).insert(db)
return .commit
}
}
let elements = try wait(for: recorder.next(2), timeout: 1)
XCTAssertEqual(elements, [1, 3])
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// This is an usage test. Do the available APIs allow to prepend a
// database connection synchronously, with the guarantee that no race can
// have the subscriber miss an impactful change?
//
// TODO: do the same, but asynchronously. If this is too hard, update the
// public API so that users can easily do it.
func testPrependInitialDatabaseSync() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let expectation = self.expectation(description: "")
let testSubject = PassthroughSubject<Database, Error>()
let testCancellable = testSubject
.tryMap(Player.fetchCount)
.collect(3)
.sink(
receiveCompletion: { completion in
assertNoFailure(completion)
},
receiveValue: { value in
XCTAssertEqual(value, [0, 1, 3])
expectation.fulfill()
})
let observationCancellable = try writer.write { db in
DatabaseRegionObservation(tracking: Player.all())
.publisher(in: writer)
.prepend(db)
.subscribe(testSubject)
}
try writer.writeWithoutTransaction { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
try db.inTransaction {
try Player(id: 2, name: "Barbara", score: 750).insert(db)
try Player(id: 3, name: "Craig", score: 500).insert(db)
return .commit
}
}
waitForExpectations(timeout: 1, handler: nil)
testCancellable.cancel()
observationCancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
}
#endif
@@ -0,0 +1,422 @@
#if canImport(Combine)
import Combine
import GRDB
import XCTest
private struct Player: Codable, FetchableRecord, PersistableRecord {
var id: Int64
var name: String
var score: Int?
static func createTable(_ db: Database) throws {
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer)
}
}
}
class DatabaseWriterWritePublisherTests : XCTestCase {
// MARK: -
func testWritePublisher() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
try XCTAssertEqual(writer.read(Player.fetchCount), 0)
let publisher = writer.writePublisher(updates: { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
})
let recorder = publisher.record()
try wait(for: recorder.single, timeout: 5)
try XCTAssertEqual(writer.read(Player.fetchCount), 1)
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
func testWritePublisherValue() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer.writePublisher(updates: { db -> Int in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
return try Player.fetchCount(db)
})
let recorder = publisher.record()
let count = try wait(for: recorder.single, timeout: 5)
XCTAssertEqual(count, 1)
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
func testWritePublisherError() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer.writePublisher(updates: { db in
try db.execute(sql: "THIS IS NOT SQL")
})
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testWritePublisherErrorRollbacksTransaction() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer.writePublisher(updates: { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
try db.execute(sql: "THIS IS NOT SQL")
})
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
}
let count = try writer.read(Player.fetchCount)
XCTAssertEqual(count, 0)
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
func testWritePublisherIsAsynchronous() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let expectation = self.expectation(description: "")
let semaphore = DispatchSemaphore(value: 0)
let cancellable = writer
.writePublisher(updates: { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
})
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
semaphore.wait()
expectation.fulfill()
})
semaphore.signal()
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
func testWritePublisherDefaultScheduler() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test<Writer: DatabaseWriter>(writer: Writer) {
let expectation = self.expectation(description: "")
expectation.expectedFulfillmentCount = 2 // value + completion
let cancellable = writer
.writePublisher(updates: { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
})
.sink(
receiveCompletion: { completion in
dispatchPrecondition(condition: .onQueue(.main))
expectation.fulfill()
},
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(.main))
expectation.fulfill()
})
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
func testWritePublisherCustomScheduler() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test<Writer: DatabaseWriter>(writer: Writer) {
let queue = DispatchQueue(label: "test")
let expectation = self.expectation(description: "")
expectation.expectedFulfillmentCount = 2 // value + completion
let cancellable = writer
.writePublisher(receiveOn: queue, updates: { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
})
.sink(
receiveCompletion: { completion in
dispatchPrecondition(condition: .onQueue(queue))
expectation.fulfill()
},
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(queue))
expectation.fulfill()
})
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
// TODO: Fix flaky test with both pool and on-disk queue:
// - Expectation timeout
func testWriteThenReadPublisher() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer
.writePublisher(
updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) },
thenRead: { db, _ in try Player.fetchCount(db) })
let recorder = publisher.record()
let count = try wait(for: recorder.single, timeout: 5)
XCTAssertEqual(count, 1)
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
func testWriteThenReadPublisherIsReadonly() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer
.writePublisher(
updates: { _ in },
thenRead: { db, _ in try Player.createTable(db) })
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_READONLY)
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
// MARK: -
func testWriteThenReadPublisherWriteError() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer.writePublisher(
updates: { db in try db.execute(sql: "THIS IS NOT SQL") },
thenRead: { _, _ in XCTFail("Should not read") })
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer.writePublisher(
updates: { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
try db.execute(sql: "THIS IS NOT SQL")
},
thenRead: { _, _ in XCTFail("Should not read") })
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
}
let count = try writer.read(Player.fetchCount)
XCTAssertEqual(count, 0)
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: -
// TODO: Fix flaky test with both pool and on-disk queue:
// - Expectation timeout
func testWriteThenReadPublisherReadError() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let publisher = writer.writePublisher(
updates: { _ in },
thenRead: { db, _ in try Row.fetchAll(db, sql: "THIS IS NOT SQL") })
let recorder = publisher.record()
let recording = try wait(for: recorder.recording, timeout: 5)
XCTAssertTrue(recording.output.isEmpty)
assertFailure(recording.completion) { (error: DatabaseError) in
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
// MARK: - Regression tests
// Regression test against deadlocks created by concurrent completion
// and cancellations triggered by .switchToLatest().prefix(1)
func testDeadlockPrevention() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter, iteration: Int) throws {
// print(iteration)
let scoreSubject = PassthroughSubject<Int, Error>()
let publisher = scoreSubject
.map { score in
writer.writePublisher { db -> Int in
try Player(id: 1, name: "Arthur", score: score).insert(db)
return try Player.fetchCount(db)
}
}
.switchToLatest()
.prefix(1)
let recorder = publisher.record()
scoreSubject.send(0)
let count = try wait(for: recorder.single, timeout: 5)
XCTAssertEqual(count, 1)
}
try Test(repeatCount: 100, test).run { try setUp(DatabaseQueue()) }
try Test(repeatCount: 100, test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(repeatCount: 100, test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
}
#endif
@@ -0,0 +1,128 @@
#if canImport(Combine)
import Combine
import Foundation
import XCTest
final class Test<Context> {
// Raise the repeatCount in order to help spotting flaky tests.
private let repeatCount: Int
private let test: (Context, Int) throws -> ()
init(repeatCount: Int = 1, _ test: @escaping (Context) throws -> ()) {
self.repeatCount = repeatCount
self.test = { context, _ in try test(context) }
}
init(repeatCount: Int, _ test: @escaping (Context, Int) throws -> ()) {
self.repeatCount = repeatCount
self.test = test
}
@discardableResult
func run(context: () throws -> Context) throws -> Self {
for i in 1...repeatCount {
try test(context(), i)
}
return self
}
@discardableResult
func runInTemporaryDirectory(context: (_ directoryURL: URL) throws -> Context) throws -> Self {
for i in 1...repeatCount {
let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("GRDB", isDirectory: true)
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true)
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
defer {
try! FileManager.default.removeItem(at: directoryURL)
}
try test(context(directoryURL), i)
}
return self
}
@discardableResult
func runAtTemporaryDatabasePath(context: (_ path: String) throws -> Context) throws -> Self {
try runInTemporaryDirectory { url in
try context(url.appendingPathComponent("db.sqlite").path)
}
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
final class AsyncTest<Context> {
// Raise the repeatCount in order to help spotting flaky tests.
private let repeatCount: Int
private let test: (Context, Int) async throws -> ()
init(repeatCount: Int = 1, _ test: @escaping (Context) async throws -> ()) {
self.repeatCount = repeatCount
self.test = { context, _ in try await test(context) }
}
init(repeatCount: Int, _ test: @escaping (Context, Int) async throws -> ()) {
self.repeatCount = repeatCount
self.test = test
}
@discardableResult
func run(context: () async throws -> Context) async throws -> Self {
for i in 1...repeatCount {
try await test(context(), i)
}
return self
}
@discardableResult
func runInTemporaryDirectory(context: (_ directoryURL: URL) async throws -> Context) async throws -> Self {
for i in 1...repeatCount {
let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("GRDB", isDirectory: true)
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true)
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
defer {
try! FileManager.default.removeItem(at: directoryURL)
}
try await test(context(directoryURL), i)
}
return self
}
@discardableResult
func runAtTemporaryDatabasePath(context: (_ path: String) async throws -> Context) async throws -> Self {
try await runInTemporaryDirectory { url in
try await context(url.appendingPathComponent("db.sqlite").path)
}
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func assertNoFailure<Failure>(
_ completion: Subscribers.Completion<Failure>,
file: StaticString = #file,
line: UInt = #line)
{
if case let .failure(error) = completion {
XCTFail("Unexpected completion failure: \(error)", file: file, line: line)
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func assertFailure<Failure, ExpectedFailure>(
_ completion: Subscribers.Completion<Failure>,
file: StaticString = #file,
line: UInt = #line,
test: (ExpectedFailure) -> Void)
{
if case let .failure(error) = completion, let expectedError = error as? ExpectedFailure {
test(expectedError)
} else {
XCTFail("Expected \(ExpectedFailure.self), got \(completion)", file: file, line: line)
}
}
#endif
@@ -0,0 +1,465 @@
#if canImport(Combine)
import Combine
import GRDB
import XCTest
private struct Player: Codable, FetchableRecord, PersistableRecord {
var id: Int64
var name: String
var score: Int?
static func createTable(_ db: Database) throws {
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer)
}
}
}
class ValueObservationPublisherTests : XCTestCase {
// MARK: - Default Scheduler
func testDefaultSchedulerChangesNotifications() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer)
let recorder = publisher.record()
try writer.writeWithoutTransaction { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
try db.inTransaction {
try Player(id: 2, name: "Barbara", score: 750).insert(db)
try Player(id: 3, name: "Craig", score: 500).insert(db)
return .commit
}
}
let expectedElements = [0, 1, 3]
if writer is DatabaseQueue {
let elements = try wait(for: recorder.next(expectedElements.count), timeout: 1)
XCTAssertEqual(elements, expectedElements)
} else {
// TODO: prefix(until:)
let elements = try wait(for: recorder.prefix(expectedElements.count + 2).inverted, timeout: 1)
assertValueObservationRecordingMatch(recorded: elements, expected: expectedElements)
}
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let expectation = self.expectation(description: "")
let semaphore = DispatchSemaphore(value: 0)
let cancellable = ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer)
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
semaphore.wait()
expectation.fulfill()
})
semaphore.signal()
waitForExpectations(timeout: 1, handler: nil)
cancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testDefaultSchedulerError() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let publisher = ValueObservation
.trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") }
.publisher(in: writer)
let recorder = publisher.record()
let completion = try wait(for: recorder.completion, timeout: 1)
switch completion {
case let .failure(error):
XCTAssertNotNil(error as? DatabaseError)
case .finished:
XCTFail("Expected error")
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
// MARK: - Immediate Scheduler
func testImmediateSchedulerChangesNotifications() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let publisher = ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer, scheduling: .immediate)
let recorder = publisher.record()
try writer.writeWithoutTransaction { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
try db.inTransaction {
try Player(id: 2, name: "Barbara", score: 750).insert(db)
try Player(id: 3, name: "Craig", score: 500).insert(db)
return .commit
}
}
let expectedElements = [0, 1, 3]
if writer is DatabaseQueue {
let elements = try wait(for: recorder.next(expectedElements.count), timeout: 1)
XCTAssertEqual(elements, expectedElements)
} else {
// TODO: prefix(until:)
let elements = try wait(for: recorder.prefix(expectedElements.count + 2).inverted, timeout: 1)
assertValueObservationRecordingMatch(recorded: elements, expected: expectedElements)
}
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testImmediateSchedulerEmitsFirstValueSynchronously() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let semaphore = DispatchSemaphore(value: 0)
let testSubject = PassthroughSubject<Int, Error>()
let testCancellable = testSubject
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(.main))
semaphore.signal()
})
let observationCancellable = ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer, scheduling: .immediate)
.subscribe(testSubject)
semaphore.wait()
testCancellable.cancel()
observationCancellable.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testImmediateSchedulerError() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let publisher = ValueObservation
.trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") }
.publisher(in: writer, scheduling: .immediate)
let recorder = publisher.record()
let completion = try recorder.completion.get()
switch completion {
case let .failure(error):
XCTAssertNotNil(error as? DatabaseError)
case .finished:
XCTFail("Expected error")
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
// MARK: - Demand
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
private class DemandSubscriber<Input, Failure: Error>: Subscriber {
private var subscription: Subscription?
let subject = PassthroughSubject<Input, Failure>()
deinit {
subscription?.cancel()
}
func cancel() {
subscription!.cancel()
}
func request(_ demand: Subscribers.Demand) {
subscription!.request(demand)
}
func receive(subscription: Subscription) {
self.subscription = subscription
}
func receive(_ input: Input) -> Subscribers.Demand {
subject.send(input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
subject.send(completion: completion)
}
}
func testDemandNoneReceivesNoElement() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let subscriber = DemandSubscriber<Int, Error>()
let expectation = self.expectation(description: "")
expectation.isInverted = true
let testCancellable = subscriber.subject
.sink(
receiveCompletion: { _ in XCTFail("Unexpected completion") },
receiveValue: { _ in expectation.fulfill() })
ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer)
.subscribe(subscriber)
waitForExpectations(timeout: 1, handler: nil)
testCancellable.cancel()
subscriber.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testDemandOneReceivesOneElement() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let subscriber = DemandSubscriber<Int, Error>()
let expectation = self.expectation(description: "")
let testCancellable = subscriber.subject.sink(
receiveCompletion: { _ in XCTFail("Unexpected completion") },
receiveValue: { value in
XCTAssertEqual(value, 0)
expectation.fulfill()
})
ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer)
.subscribe(subscriber)
subscriber.request(.max(1))
waitForExpectations(timeout: 1, handler: nil)
testCancellable.cancel()
subscriber.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testDemandOneDoesNotReceiveTwoElements() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let subscriber = DemandSubscriber<Int, Error>()
let expectation = self.expectation(description: "")
expectation.isInverted = true
let testCancellable = subscriber.subject
.collect(2)
.sink(
receiveCompletion: { _ in XCTFail("Unexpected completion") },
receiveValue: { _ in expectation.fulfill() })
ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer, scheduling: .immediate /* make sure we get the initial db state */)
.subscribe(subscriber)
subscriber.request(.max(1))
try writer.writeWithoutTransaction { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
}
waitForExpectations(timeout: 1, handler: nil)
testCancellable.cancel()
subscriber.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
func testDemandTwoReceivesTwoElements() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
try writer.write(Player.createTable)
return writer
}
func test(writer: some DatabaseWriter) throws {
let subscriber = DemandSubscriber<Int, Error>()
let expectation = self.expectation(description: "")
let testCancellable = subscriber.subject
.collect(2)
.sink(
receiveCompletion: { _ in XCTFail("Unexpected completion") },
receiveValue: { values in
expectation.fulfill()
})
ValueObservation
.trackingConstantRegion(Player.fetchCount)
.publisher(in: writer, scheduling: .immediate /* make sure we get two db states */)
.subscribe(subscriber)
subscriber.request(.max(2))
try writer.writeWithoutTransaction { db in
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
}
waitForExpectations(timeout: 1, handler: nil)
testCancellable.cancel()
subscriber.cancel()
}
try Test(test).run { try setUp(DatabaseQueue()) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
}
// MARK: - Regression Tests
/// Regression test for https://github.com/groue/GRDB.swift/issues/1194
func testIssue1194() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
struct Record: Codable, FetchableRecord, PersistableRecord {
var id: Int64
}
var configuration = Configuration()
configuration.targetQueue = DispatchQueue(label: "crash.test", qos: .userInitiated)
let database = try DatabaseQueue(configuration: configuration)
var migrator = DatabaseMigrator()
migrator.registerMigration("v1") { (db) in
try db.create(table: Record.databaseTableName) { (t) in
t.autoIncrementedPrimaryKey("id")
}
}
try migrator.migrate(database)
let observation = ValueObservation.tracking { (db) in
try Record.fetchCount(db)
}
let exp = expectation(description: "")
let cancellable = observation.publisher(in: database, scheduling: .immediate)
.map { _ in
database.readPublisher { (db) in
try Record.fetchCount(db)
}
}
.switchToLatest()
.sink(receiveCompletion: { _ in },
receiveValue: { (value) in
exp.fulfill()
})
withExtendedLifetime(cancellable) {
waitForExpectations(timeout: 1)
}
}
}
#endif