From 86ab0b929c66ab37f2f6d475d3ceba0a8cef777a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:03:20 +0000 Subject: [PATCH 1/2] test: add deterministic broadcast batching/ordering tests via fakeAsync Loon schedules broadcasts on a zero-duration timer so writes in one event-loop task are delivered as a single update. Testing that with real time is inherently racy (it depends on a real timer firing before the test's wait), which is a known source of flakiness in the suite. These tests run under fakeAsync: virtual time is advanced explicitly so the broadcast timer and its microtask stream delivery fire deterministi- cally before each assertion, independent of wall-clock scheduling or CPU load. They cover broadcast batching (many writes -> one emission), create+update coalescing, per-task broadcast separation, and that a no-op update does not rebroadcast. A dedicated file runs in its own test isolate, so the global store starts clean and these virtual-time tests are isolated from the rest of the suite. fake_async is pulled in via flutter_test. --- test/core/broadcast_timing_test.dart | 125 +++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 test/core/broadcast_timing_test.dart diff --git a/test/core/broadcast_timing_test.dart b/test/core/broadcast_timing_test.dart new file mode 100644 index 0000000..4a9cb38 --- /dev/null +++ b/test/core/broadcast_timing_test.dart @@ -0,0 +1,125 @@ +// ignore_for_file: depend_on_referenced_packages +import 'package:fake_async/fake_async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:loon/loon.dart'; + +/// Deterministic tests for broadcast batching, coalescing, and ordering. +/// +/// Loon schedules a broadcast on a zero-duration timer so that all writes in a +/// single event-loop task are delivered to observers as one update. Testing +/// that behaviour with real time is inherently racy (it depends on a real timer +/// firing before the test's wait), which is a known source of flakiness in the +/// suite. These tests run under [fakeAsync] instead: virtual time is advanced +/// explicitly, so the broadcast timer and its microtask stream delivery fire +/// deterministically before each assertion, with no dependence on wall-clock +/// scheduling or CPU load. +/// +/// (A dedicated file runs in its own test isolate, so the global store starts +/// clean and these virtual-time tests are isolated from the rest of the suite.) + +/// Advances virtual time past the broadcast's zero-duration timer and drains +/// the microtasks that deliver stream events. +void _flush(FakeAsync async) { + async.elapse(const Duration(milliseconds: 1)); +} + +void _reset(FakeAsync async) { + Loon.unsubscribe(); + Loon.clearAll(broadcast: false); + async.flushMicrotasks(); +} + +void main() { + group('Broadcast batching and coalescing', () { + test('Multiple writes in one task produce a single broadcast', () { + fakeAsync((async) { + _reset(async); + final col = Loon.collection('items'); + + final emissions = >[]; + final sub = col + .stream() + .listen((snaps) => emissions.add([for (final s in snaps) s.data])); + _flush(async); // initial emission + + col.doc('1').create(1); + col.doc('2').create(2); + col.doc('3').create(3); + _flush(async); + + // One emission for the initial value and exactly one for the batch. + expect(emissions.length, 2); + expect(emissions.last..sort(), [1, 2, 3]); + + sub.cancel(); + async.flushMicrotasks(); + }); + }); + + test('Create then update in one task coalesces to the final value', () { + fakeAsync((async) { + _reset(async); + final doc = Loon.collection('items').doc('1'); + + final emissions = []; + final sub = doc.stream().listen((snap) => emissions.add(snap?.data)); + _flush(async); // initial null + + doc.create(1); + doc.update(2); + _flush(async); + + // The create and update collapse into a single emission of the final value. + expect(emissions, [null, 2]); + + sub.cancel(); + async.flushMicrotasks(); + }); + }); + + test('Writes in separate tasks produce separate broadcasts', () { + fakeAsync((async) { + _reset(async); + final doc = Loon.collection('items').doc('1'); + + final emissions = []; + final sub = doc.stream().listen((snap) => emissions.add(snap?.data)); + _flush(async); // initial null + + doc.create(1); + _flush(async); + doc.update(2); + _flush(async); + doc.update(3); + _flush(async); + + expect(emissions, [null, 1, 2, 3]); + + sub.cancel(); + async.flushMicrotasks(); + }); + }); + + test('An unchanged update does not rebroadcast', () { + fakeAsync((async) { + _reset(async); + final doc = Loon.collection('items').doc('1'); + + final emissions = []; + final sub = doc.stream().listen((snap) => emissions.add(snap?.data)); + _flush(async); // initial null + + doc.create(1); + _flush(async); + doc.update(1); // same value + _flush(async); + + // No emission for the no-op update. + expect(emissions, [null, 1]); + + sub.cancel(); + async.flushMicrotasks(); + }); + }); + }); +} From 370e9a236c6887d481d3ff5f8070ac27de791cdd Mon Sep 17 00:00:00 2001 From: Dan Reynolds Date: Sun, 31 May 2026 09:39:17 -0400 Subject: [PATCH 2/2] Declare fake_async test dependency --- pubspec.yaml | 2 +- test/core/broadcast_timing_test.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 39fb931..49761fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,10 +19,10 @@ dependencies: sqflite_common_ffi: ^2.3.4 dev_dependencies: + fake_async: ^1.3.3 flutter_test: sdk: flutter flutter_lints: ^6.0.0 flutter: - \ No newline at end of file diff --git a/test/core/broadcast_timing_test.dart b/test/core/broadcast_timing_test.dart index 4a9cb38..9b3e6b6 100644 --- a/test/core/broadcast_timing_test.dart +++ b/test/core/broadcast_timing_test.dart @@ -1,4 +1,3 @@ -// ignore_for_file: depend_on_referenced_packages import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:loon/loon.dart';