244 lines
8.9 KiB
Swift
244 lines
8.9 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import GRDB
|
|
|
|
// Support for Database.logError
|
|
var lastResultCode: ResultCode? = nil
|
|
var lastMessage: String? = nil
|
|
let logErrorSetup: Void = {
|
|
let lock = NSLock()
|
|
Database.logError = { (resultCode, message) in
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
lastResultCode = resultCode
|
|
lastMessage = message
|
|
}
|
|
}()
|
|
|
|
class GRDBTestCase: XCTestCase {
|
|
// The default configuration for tests
|
|
var dbConfiguration: Configuration!
|
|
|
|
// Builds a database queue based on dbConfiguration
|
|
func makeDatabaseQueue(filename: String? = nil) throws -> DatabaseQueue {
|
|
try makeDatabaseQueue(filename: filename, configuration: dbConfiguration)
|
|
}
|
|
|
|
// Builds a database queue
|
|
func makeDatabaseQueue(filename: String? = nil, configuration: Configuration) throws -> DatabaseQueue {
|
|
try FileManager.default.createDirectory(atPath: dbDirectoryPath, withIntermediateDirectories: true, attributes: nil)
|
|
let dbPath = (dbDirectoryPath as NSString).appendingPathComponent(filename ?? ProcessInfo.processInfo.globallyUniqueString)
|
|
let dbQueue = try DatabaseQueue(path: dbPath, configuration: configuration)
|
|
try setup(dbQueue)
|
|
return dbQueue
|
|
}
|
|
|
|
// Builds a database pool based on dbConfiguration
|
|
func makeDatabasePool(filename: String? = nil) throws -> DatabasePool {
|
|
try makeDatabasePool(filename: filename, configuration: dbConfiguration)
|
|
}
|
|
|
|
// Builds a database pool
|
|
func makeDatabasePool(filename: String? = nil, configuration: Configuration) throws -> DatabasePool {
|
|
try FileManager.default.createDirectory(atPath: dbDirectoryPath, withIntermediateDirectories: true, attributes: nil)
|
|
let dbPath = (dbDirectoryPath as NSString).appendingPathComponent(filename ?? ProcessInfo.processInfo.globallyUniqueString)
|
|
let dbPool = try DatabasePool(path: dbPath, configuration: configuration)
|
|
try setup(dbPool)
|
|
return dbPool
|
|
}
|
|
|
|
// Subclasses can override
|
|
// Default implementation is empty.
|
|
func setup(_ dbWriter: some DatabaseWriter) throws {
|
|
}
|
|
|
|
// The default path for database pool directory
|
|
private var dbDirectoryPath: String!
|
|
|
|
// Populated by default configuration
|
|
@LockedBox var sqlQueries: [String] = []
|
|
|
|
// Populated by default configuration
|
|
var lastSQLQuery: String? { sqlQueries.last }
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
_ = logErrorSetup
|
|
|
|
let dbPoolDirectoryName = "GRDBTestCase-\(ProcessInfo.processInfo.globallyUniqueString)"
|
|
dbDirectoryPath = (NSTemporaryDirectory() as NSString).appendingPathComponent(dbPoolDirectoryName)
|
|
do { try FileManager.default.removeItem(atPath: dbDirectoryPath) } catch { }
|
|
|
|
dbConfiguration = Configuration()
|
|
|
|
// Test that database are deallocated in a clean state
|
|
dbConfiguration.SQLiteConnectionWillClose = { sqliteConnection in
|
|
// https://www.sqlite.org/capi3ref.html#sqlite3_close:
|
|
// > If sqlite3_close_v2() is called on a database connection that still
|
|
// > has outstanding prepared statements, BLOB handles, and/or
|
|
// > sqlite3_backup objects then it returns SQLITE_OK and the
|
|
// > deallocation of resources is deferred until all prepared
|
|
// > statements, BLOB handles, and sqlite3_backup objects are also
|
|
// > destroyed.
|
|
//
|
|
// Let's assert that there is no longer any busy update statements.
|
|
//
|
|
// SQLite would allow that. But not GRDB, since all updates happen
|
|
// in closures that retain database connections, preventing
|
|
// Database.deinit to fire.
|
|
//
|
|
// What we gain from this test is a guarantee that database
|
|
// deallocation implies that there is no pending lock in the
|
|
// database.
|
|
//
|
|
// See:
|
|
// - sqlite3_next_stmt https://www.sqlite.org/capi3ref.html#sqlite3_next_stmt
|
|
// - sqlite3_stmt_busy https://www.sqlite.org/capi3ref.html#sqlite3_stmt_busy
|
|
// - sqlite3_stmt_readonly https://www.sqlite.org/capi3ref.html#sqlite3_stmt_readonly
|
|
var stmt: SQLiteStatement? = sqlite3_next_stmt(sqliteConnection, nil)
|
|
while stmt != nil {
|
|
XCTAssertTrue(sqlite3_stmt_readonly(stmt) != 0 || sqlite3_stmt_busy(stmt) == 0)
|
|
stmt = sqlite3_next_stmt(sqliteConnection, stmt)
|
|
}
|
|
}
|
|
|
|
dbConfiguration.prepareDatabase { db in
|
|
db.trace { event in
|
|
self.sqlQueries.append(event.expandedDescription)
|
|
}
|
|
|
|
#if GRDBCIPHER_USE_ENCRYPTION
|
|
try db.usePassphrase("secret")
|
|
#endif
|
|
}
|
|
|
|
sqlQueries = []
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
do { try FileManager.default.removeItem(atPath: dbDirectoryPath) } catch { }
|
|
}
|
|
|
|
func assertNoError(file: StaticString = #file, line: UInt = #line, _ test: () throws -> Void) {
|
|
do {
|
|
try test()
|
|
} catch {
|
|
XCTFail("unexpected error: \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func assertDidExecute(sql: String, file: StaticString = #file, line: UInt = #line) {
|
|
XCTAssertTrue(sqlQueries.contains(sql), "Did not execute \(sql)", file: file, line: line)
|
|
}
|
|
|
|
func assert(_ record: some EncodableRecord, isEncodedIn row: Row, file: StaticString = #file, line: UInt = #line) throws {
|
|
let recordDict = try record.databaseDictionary
|
|
let rowDict = Dictionary(row, uniquingKeysWith: { (left, _) in left })
|
|
XCTAssertEqual(recordDict, rowDict, file: file, line: line)
|
|
}
|
|
|
|
// Compare SQL strings (ignoring leading and trailing white space and semicolons.
|
|
func assertEqualSQL(_ lhs: String, _ rhs: String, file: StaticString = #file, line: UInt = #line) {
|
|
// Trim white space and ";"
|
|
let cs = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ";"))
|
|
XCTAssertEqual(lhs.trimmingCharacters(in: cs), rhs.trimmingCharacters(in: cs), file: file, line: line)
|
|
}
|
|
|
|
// Compare SQL strings (ignoring leading and trailing white space and semicolons.
|
|
func assertEqualSQL(
|
|
_ db: Database,
|
|
_ request: some FetchRequest,
|
|
_ sql: String,
|
|
file: StaticString = #file,
|
|
line: UInt = #line)
|
|
throws
|
|
{
|
|
try request.makeStatement(db).makeCursor().next()
|
|
assertEqualSQL(lastSQLQuery!, sql, file: file, line: line)
|
|
}
|
|
|
|
// Compare SQL strings.
|
|
func assertEqualSQL(
|
|
_ db: Database,
|
|
_ expression: some SQLExpressible,
|
|
_ sql: String,
|
|
file: StaticString = #file,
|
|
line: UInt = #line)
|
|
throws
|
|
{
|
|
let request: SQLRequest<Row> = "SELECT \(expression)"
|
|
try assertEqualSQL(db, request, "SELECT \(sql)", file: file, line: line)
|
|
}
|
|
|
|
// Compare SQL strings (ignoring leading and trailing white space and semicolons.
|
|
func assertEqualSQL(
|
|
_ databaseReader: some DatabaseReader,
|
|
_ request: some FetchRequest,
|
|
_ sql: String,
|
|
file: StaticString = #file,
|
|
line: UInt = #line)
|
|
throws
|
|
{
|
|
try databaseReader.unsafeRead { db in
|
|
try assertEqualSQL(db, request, sql, file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func sql(
|
|
_ databaseReader: some DatabaseReader,
|
|
_ request: some FetchRequest)
|
|
-> String
|
|
{
|
|
try! databaseReader.unsafeRead { db in
|
|
try request.makeStatement(db).makeCursor().next()
|
|
return lastSQLQuery!
|
|
}
|
|
}
|
|
}
|
|
|
|
#if SWIFT_PACKAGE
|
|
let testBundle = Bundle.module
|
|
#else
|
|
let testBundle = Bundle(for: GRDBTestCase.self)
|
|
#endif
|
|
|
|
extension FetchRequest {
|
|
/// Turn request into a statement
|
|
func makeStatement(_ db: Database) throws -> Statement {
|
|
try makePreparedRequest(db, forSingleResult: false).statement
|
|
}
|
|
|
|
/// Turn request into SQL and arguments
|
|
func build(_ db: Database) throws -> (sql: String, arguments: StatementArguments) {
|
|
let statement = try makePreparedRequest(db, forSingleResult: false).statement
|
|
return (sql: statement.sql, arguments: statement.arguments)
|
|
}
|
|
}
|
|
|
|
/// A type-erased ValueReducer.
|
|
public struct AnyValueReducer<Fetched, Value>: ValueReducer {
|
|
private var __fetch: (Database) throws -> Fetched
|
|
private var __value: (Fetched) -> Value?
|
|
|
|
public init(
|
|
fetch: @escaping (Database) throws -> Fetched,
|
|
value: @escaping (Fetched) -> Value?)
|
|
{
|
|
self.__fetch = fetch
|
|
self.__value = value
|
|
}
|
|
|
|
public func _fetch(_ db: Database) throws -> Fetched {
|
|
try __fetch(db)
|
|
}
|
|
|
|
public func _value(_ fetched: Fetched) -> Value? {
|
|
__value(fetched)
|
|
}
|
|
}
|
|
|
|
// Assume this is correct :-/
|
|
extension XCTestExpectation: @unchecked Sendable { }
|