|
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,77 @@ 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 | + |
| 224 | + test('slow first', () async { |
| 225 | + final db = SqliteDatabase.withFactory( |
| 226 | + await testUtils.testFactory(path: path), |
| 227 | + maxReaders: 1); |
| 228 | + await db.initialize(); |
| 229 | + await createTables(db); |
| 230 | + |
| 231 | + print("STAAAART"); |
| 232 | + await Future.wait([1000, 10].map((t) => db.get( |
| 233 | + 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', |
| 234 | + [t, t]))); |
| 235 | + }); |
| 236 | + |
163 | 237 | test('read-only transactions', () async {
|
164 | 238 | final db = await testUtils.setupDatabase(path: path);
|
165 | 239 | await createTables(db);
|
@@ -439,3 +513,31 @@ class _InvalidPragmaOnOpenFactory extends DefaultSqliteOpenFactory {
|
439 | 513 | ];
|
440 | 514 | }
|
441 | 515 | }
|
| 516 | + |
| 517 | +class _TestSqliteOpenFactoryWithSharedStateFile |
| 518 | + extends TestDefaultSqliteOpenFactory { |
| 519 | + final String sharedStateFilePath; |
| 520 | + |
| 521 | + _TestSqliteOpenFactoryWithSharedStateFile( |
| 522 | + {required super.path, required this.sharedStateFilePath}); |
| 523 | + |
| 524 | + @override |
| 525 | + sqlite.CommonDatabase open(SqliteOpenOptions options) { |
| 526 | + final File sharedStateFile = File(sharedStateFilePath); |
| 527 | + final String sharedState = sharedStateFile.readAsStringSync(); |
| 528 | + |
| 529 | + final db = super.open(options); |
| 530 | + |
| 531 | + // Function to return the contents of the shared state file at the time of opening |
| 532 | + // so that we know at which point the factory was called. |
| 533 | + db.createFunction( |
| 534 | + functionName: 'file_contents_on_open', |
| 535 | + argumentCount: const sqlite.AllowedArgumentCount(0), |
| 536 | + function: (args) { |
| 537 | + return sharedState; |
| 538 | + }, |
| 539 | + ); |
| 540 | + |
| 541 | + return db; |
| 542 | + } |
| 543 | +} |
0 commit comments