add iOS
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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 { }
|
||||
Reference in New Issue
Block a user