add iOS
This commit is contained in:
+355
@@ -0,0 +1,355 @@
|
||||
import XCTest
|
||||
@testable import GRDB
|
||||
|
||||
class DatabasePoolReleaseMemoryTests: GRDBTestCase {
|
||||
|
||||
func testDatabasePoolDeinitClosesAllConnections() throws {
|
||||
let countQueue = DispatchQueue(label: "GRDB")
|
||||
var openConnectionCount = 0
|
||||
var totalOpenConnectionCount = 0
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidOpen = {
|
||||
countQueue.sync {
|
||||
totalOpenConnectionCount += 1
|
||||
openConnectionCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidClose = {
|
||||
countQueue.sync {
|
||||
openConnectionCount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// write & read
|
||||
|
||||
do {
|
||||
// Create and release DatabasePool
|
||||
let dbPool = try makeDatabasePool()
|
||||
// Writer connection
|
||||
try dbPool.write { db in
|
||||
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
|
||||
}
|
||||
// Reader connection
|
||||
try dbPool.read { _ in }
|
||||
}
|
||||
|
||||
// One reader, one writer
|
||||
XCTAssertEqual(totalOpenConnectionCount, 2)
|
||||
|
||||
// All connections are closed
|
||||
XCTAssertEqual(openConnectionCount, 0)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
func testDatabasePoolReleasesMemoryOnPressureEvent() throws {
|
||||
// Create a database pool, and expect a reader connection to be closed
|
||||
let expectation = self.expectation(description: "Reader connection closed")
|
||||
|
||||
var configuration = Configuration()
|
||||
configuration.SQLiteConnectionWillClose = { conn in
|
||||
if sqlite3_db_readonly(conn, nil) != 0 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
let dbPool = try makeDatabasePool(configuration: configuration)
|
||||
|
||||
// Precondition: there is one reader.
|
||||
try dbPool.read { _ in }
|
||||
|
||||
// Simulate memory warning.
|
||||
NotificationCenter.default.post(
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil)
|
||||
|
||||
// Postcondition: reader connection was closed
|
||||
withExtendedLifetime(dbPool) { _ in
|
||||
waitForExpectations(timeout: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
func testDatabasePoolDoesNotReleaseMemoryOnPressureEventIfDisabled() throws {
|
||||
// Create a database pool, and do not expect any reader connection to be closed
|
||||
let expectation = self.expectation(description: "Reader connection closed")
|
||||
expectation.isInverted = true
|
||||
|
||||
var configuration = Configuration()
|
||||
configuration.automaticMemoryManagement = false
|
||||
configuration.SQLiteConnectionWillClose = { conn in
|
||||
if sqlite3_db_readonly(conn, nil) != 0 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
let dbPool = try makeDatabasePool(configuration: configuration)
|
||||
|
||||
// Precondition: there is one reader.
|
||||
try dbPool.read { _ in }
|
||||
|
||||
// Simulate memory warning.
|
||||
NotificationCenter.default.post(
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil)
|
||||
|
||||
// Postcondition: no reader connection was closed
|
||||
withExtendedLifetime(dbPool) { _ in
|
||||
waitForExpectations(timeout: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for <https://github.com/groue/GRDB.swift/pull/1253#issuecomment-1177166630>
|
||||
func testDatabasePoolDoesNotPreventConcurrentReadsOnPressureEvent() throws {
|
||||
let dbPool = try makeDatabasePool()
|
||||
|
||||
// Start a read that blocks
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
dbPool.asyncRead { _ in
|
||||
semaphore.wait()
|
||||
}
|
||||
|
||||
// Simulate memory warning.
|
||||
NotificationCenter.default.post(
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil)
|
||||
|
||||
// Make sure we can read
|
||||
try dbPool.read { _ in }
|
||||
|
||||
// Cleanup
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
func test_DatabasePool_releaseMemory_closes_reader_connections() throws {
|
||||
// A complicated test setup that opens multiple reader connections.
|
||||
let countQueue = DispatchQueue(label: "GRDB")
|
||||
var openConnectionCount = 0
|
||||
var totalOpenConnectionCount = 0
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidOpen = {
|
||||
countQueue.sync {
|
||||
totalOpenConnectionCount += 1
|
||||
openConnectionCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidClose = {
|
||||
countQueue.sync {
|
||||
openConnectionCount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
let dbPool = try makeDatabasePool()
|
||||
try dbPool.write { db in
|
||||
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
|
||||
for _ in 0..<2 {
|
||||
try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
|
||||
}
|
||||
}
|
||||
|
||||
// Block 1 Block 2 Block3
|
||||
// SELECT * FROM items
|
||||
// step
|
||||
// >
|
||||
let s1 = DispatchSemaphore(value: 0)
|
||||
// SELECT * FROM items
|
||||
// step
|
||||
// >
|
||||
let s2 = DispatchSemaphore(value: 0)
|
||||
// step step
|
||||
// >
|
||||
let s3 = DispatchSemaphore(value: 0)
|
||||
// end end releaseMemory
|
||||
|
||||
let block1 = { () in
|
||||
try! dbPool.read { db in
|
||||
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
|
||||
XCTAssertTrue(try cursor.next() != nil)
|
||||
s1.signal()
|
||||
_ = s2.wait(timeout: .distantFuture)
|
||||
XCTAssertTrue(try cursor.next() != nil)
|
||||
s3.signal()
|
||||
XCTAssertTrue(try cursor.next() == nil)
|
||||
}
|
||||
}
|
||||
let block2 = { () in
|
||||
_ = s1.wait(timeout: .distantFuture)
|
||||
try! dbPool.read { db in
|
||||
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
|
||||
XCTAssertTrue(try cursor.next() != nil)
|
||||
s2.signal()
|
||||
XCTAssertTrue(try cursor.next() != nil)
|
||||
XCTAssertTrue(try cursor.next() == nil)
|
||||
}
|
||||
}
|
||||
let block3 = { () in
|
||||
_ = s3.wait(timeout: .distantFuture)
|
||||
dbPool.releaseMemory()
|
||||
}
|
||||
let blocks = [block1, block2, block3]
|
||||
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in // FIXME: this crashes sometimes
|
||||
blocks[index]()
|
||||
}
|
||||
|
||||
// Two readers, one writer
|
||||
XCTAssertEqual(totalOpenConnectionCount, 3)
|
||||
|
||||
// Writer is still open
|
||||
XCTAssertEqual(openConnectionCount, 1)
|
||||
}
|
||||
|
||||
func test_DatabasePool_releaseMemory_closes_reader_connections_when_persistentReadOnlyConnections_is_false() throws {
|
||||
var persistentConnectionCount = 0
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidOpen = {
|
||||
persistentConnectionCount += 1
|
||||
}
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidClose = {
|
||||
persistentConnectionCount -= 1
|
||||
}
|
||||
|
||||
dbConfiguration.persistentReadOnlyConnections = false
|
||||
|
||||
let dbPool = try makeDatabasePool()
|
||||
XCTAssertEqual(persistentConnectionCount, 1) // writer
|
||||
|
||||
try dbPool.read { _ in }
|
||||
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
|
||||
|
||||
dbPool.releaseMemory()
|
||||
XCTAssertEqual(persistentConnectionCount, 1) // writer
|
||||
}
|
||||
|
||||
func test_DatabasePool_releaseMemory_does_not_close_reader_connections_when_persistentReadOnlyConnections_is_true() throws {
|
||||
var persistentConnectionCount = 0
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidOpen = {
|
||||
persistentConnectionCount += 1
|
||||
}
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidClose = {
|
||||
persistentConnectionCount -= 1
|
||||
}
|
||||
|
||||
dbConfiguration.persistentReadOnlyConnections = true
|
||||
|
||||
let dbPool = try makeDatabasePool()
|
||||
XCTAssertEqual(persistentConnectionCount, 1) // writer
|
||||
|
||||
try dbPool.read { _ in }
|
||||
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
|
||||
|
||||
dbPool.releaseMemory()
|
||||
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
|
||||
}
|
||||
|
||||
func testBlocksRetainConnection() throws {
|
||||
let countQueue = DispatchQueue(label: "GRDB")
|
||||
var openConnectionCount = 0
|
||||
var totalOpenConnectionCount = 0
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidOpen = {
|
||||
countQueue.sync {
|
||||
totalOpenConnectionCount += 1
|
||||
openConnectionCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
dbConfiguration.SQLiteConnectionDidClose = {
|
||||
countQueue.sync {
|
||||
openConnectionCount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// Block 1 Block 2
|
||||
// read {
|
||||
// >
|
||||
let s1 = DispatchSemaphore(value: 0)
|
||||
// dbPool = nil
|
||||
// >
|
||||
let s2 = DispatchSemaphore(value: 0)
|
||||
// use database
|
||||
// }
|
||||
|
||||
let (block1, block2) = { () -> (() -> (), () -> ()) in
|
||||
var dbPool: DatabasePool? = try! self.makeDatabasePool()
|
||||
try! dbPool!.write { db in
|
||||
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
|
||||
}
|
||||
|
||||
let block1 = { () in
|
||||
_ = s1.wait(timeout: .distantFuture)
|
||||
dbPool = nil
|
||||
s2.signal()
|
||||
}
|
||||
let block2 = { [weak dbPool] () in
|
||||
if let dbPool {
|
||||
try! dbPool.read { db in
|
||||
s1.signal()
|
||||
_ = s2.wait(timeout: .distantFuture)
|
||||
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items"), 0)
|
||||
}
|
||||
} else {
|
||||
XCTFail("expect non nil dbPool")
|
||||
}
|
||||
}
|
||||
return (block1, block2)
|
||||
}()
|
||||
let blocks = [block1, block2]
|
||||
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
||||
blocks[index]()
|
||||
}
|
||||
|
||||
// one writer, one reader
|
||||
XCTAssertEqual(totalOpenConnectionCount, 2)
|
||||
|
||||
// All connections are closed
|
||||
XCTAssertEqual(openConnectionCount, 0)
|
||||
}
|
||||
|
||||
func testStatementDoNotRetainDatabaseConnection() throws {
|
||||
// Block 1 Block 2
|
||||
// create statement INSERT
|
||||
// >
|
||||
let s1 = DispatchSemaphore(value: 0)
|
||||
// dbPool = nil
|
||||
// >
|
||||
let s2 = DispatchSemaphore(value: 0)
|
||||
// dbPool is nil
|
||||
|
||||
let (block1, block2) = { () -> (() -> (), () -> ()) in
|
||||
var dbPool: DatabasePool? = try! self.makeDatabasePool()
|
||||
let block1 = { () in
|
||||
_ = s1.wait(timeout: .distantFuture)
|
||||
dbPool = nil
|
||||
s2.signal()
|
||||
}
|
||||
let block2 = { [weak dbPool] () in
|
||||
var statement: Statement? = nil
|
||||
do {
|
||||
if let dbPool {
|
||||
do {
|
||||
try dbPool.write { db in
|
||||
statement = try db.makeStatement(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
|
||||
s1.signal()
|
||||
}
|
||||
} catch {
|
||||
XCTFail("error: \(error)")
|
||||
}
|
||||
} else {
|
||||
XCTFail("expect non nil dbPool")
|
||||
}
|
||||
}
|
||||
_ = s2.wait(timeout: .distantFuture)
|
||||
XCTAssertTrue(statement != nil)
|
||||
XCTAssertTrue(dbPool == nil)
|
||||
}
|
||||
return (block1, block2)
|
||||
}()
|
||||
let blocks = [block1, block2]
|
||||
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
||||
blocks[index]()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user