|
2 | 2 | library; |
3 | 3 |
|
4 | 4 | import 'dart:async'; |
| 5 | +import 'dart:io'; |
5 | 6 | import 'dart:math'; |
6 | 7 |
|
7 | 8 | import 'package:collection/collection.dart'; |
| 9 | +import 'package:path/path.dart' show join; |
8 | 10 | import 'package:sqlite3/common.dart' as sqlite; |
9 | 11 | import 'package:sqlite3/sqlite3.dart' show Row; |
10 | 12 | import 'package:sqlite_async/sqlite_async.dart'; |
11 | 13 | import 'package:test/test.dart'; |
12 | 14 |
|
| 15 | +import '../utils/abstract_test_utils.dart'; |
13 | 16 | import '../utils/test_utils_impl.dart'; |
14 | 17 |
|
15 | 18 | final testUtils = TestUtils(); |
@@ -126,7 +129,7 @@ void main() { |
126 | 129 |
|
127 | 130 | print("${DateTime.now()} start"); |
128 | 131 | await db.withAllConnections((writer, readers) async { |
129 | | - assert(readers.length == 3); |
| 132 | + expect(readers.length, 3); |
130 | 133 |
|
131 | 134 | // Run some reads during the block that they should run after the block finishes and releases |
132 | 135 | // all locks |
@@ -160,6 +163,64 @@ void main() { |
160 | 163 | await readsCalledWhileWithAllConnsRunning; |
161 | 164 | }); |
162 | 165 |
|
| 166 | + test('prevent opening new readers while in withAllConnections', () async { |
| 167 | + final sharedStateDir = Directory.systemTemp.createTempSync(); |
| 168 | + addTearDown(() => sharedStateDir.deleteSync(recursive: true)); |
| 169 | + |
| 170 | + final File sharedStateFile = |
| 171 | + File(join(sharedStateDir.path, 'shared-state.txt')); |
| 172 | + |
| 173 | + sharedStateFile.writeAsStringSync('initial'); |
| 174 | + |
| 175 | + final db = SqliteDatabase.withFactory( |
| 176 | + _TestSqliteOpenFactoryWithSharedStateFile( |
| 177 | + path: path, sharedStateFilePath: sharedStateFile.path), |
| 178 | + maxReaders: 3); |
| 179 | + await db.initialize(); |
| 180 | + await createTables(db); |
| 181 | + |
| 182 | + // The writer saw 'initial' in the file when opening the connection |
| 183 | + expect( |
| 184 | + await db |
| 185 | + .writeLock((c) => c.get('SELECT file_contents_on_open() AS state')), |
| 186 | + {'state': 'initial'}, |
| 187 | + ); |
| 188 | + |
| 189 | + final withAllConnectionsCompleter = Completer<void>(); |
| 190 | + |
| 191 | + final withAllConnsFut = db.withAllConnections((writer, readers) async { |
| 192 | + expect(readers.length, 0); // No readers yet |
| 193 | + |
| 194 | + // Simulate some work until the file is updated |
| 195 | + await Future.delayed(const Duration(milliseconds: 200)); |
| 196 | + sharedStateFile.writeAsStringSync('updated'); |
| 197 | + |
| 198 | + await withAllConnectionsCompleter.future; |
| 199 | + }); |
| 200 | + |
| 201 | + // Start a reader that gets the contents of the shared file |
| 202 | + bool readFinished = false; |
| 203 | + final someReadFut = |
| 204 | + db.get('SELECT file_contents_on_open() AS state', []).then((r) { |
| 205 | + readFinished = true; |
| 206 | + return r; |
| 207 | + }); |
| 208 | + |
| 209 | + // The withAllConnections should prevent the reader from opening |
| 210 | + await Future.delayed(const Duration(milliseconds: 100)); |
| 211 | + expect(readFinished, isFalse); |
| 212 | + |
| 213 | + // Free all the locks |
| 214 | + withAllConnectionsCompleter.complete(); |
| 215 | + await withAllConnsFut; |
| 216 | + |
| 217 | + final readerInfo = await someReadFut; |
| 218 | + expect(readFinished, isTrue); |
| 219 | + // The read should see the updated value in the file. This checks |
| 220 | + // that a reader doesn't spawn while running withAllConnections |
| 221 | + expect(readerInfo, {'state': 'updated'}); |
| 222 | + }); |
| 223 | + |
163 | 224 | test('read-only transactions', () async { |
164 | 225 | final db = await testUtils.setupDatabase(path: path); |
165 | 226 | await createTables(db); |
@@ -439,3 +500,31 @@ class _InvalidPragmaOnOpenFactory extends DefaultSqliteOpenFactory { |
439 | 500 | ]; |
440 | 501 | } |
441 | 502 | } |
| 503 | + |
| 504 | +class _TestSqliteOpenFactoryWithSharedStateFile |
| 505 | + extends TestDefaultSqliteOpenFactory { |
| 506 | + final String sharedStateFilePath; |
| 507 | + |
| 508 | + _TestSqliteOpenFactoryWithSharedStateFile( |
| 509 | + {required super.path, required this.sharedStateFilePath}); |
| 510 | + |
| 511 | + @override |
| 512 | + sqlite.CommonDatabase open(SqliteOpenOptions options) { |
| 513 | + final File sharedStateFile = File(sharedStateFilePath); |
| 514 | + final String sharedState = sharedStateFile.readAsStringSync(); |
| 515 | + |
| 516 | + final db = super.open(options); |
| 517 | + |
| 518 | + // Function to return the contents of the shared state file at the time of opening |
| 519 | + // so that we know at which point the factory was called. |
| 520 | + db.createFunction( |
| 521 | + functionName: 'file_contents_on_open', |
| 522 | + argumentCount: const sqlite.AllowedArgumentCount(0), |
| 523 | + function: (args) { |
| 524 | + return sharedState; |
| 525 | + }, |
| 526 | + ); |
| 527 | + |
| 528 | + return db; |
| 529 | + } |
| 530 | +} |
0 commit comments