Index: pkg/observe/test/observe_test.dart |
diff --git a/pkg/observe/test/observe_test.dart b/pkg/observe/test/observe_test.dart |
index e60f740027b3a95363782cbeb69106c1ebefcca3..76c25410346e530685471a164b37eaf53635250c 100644 |
--- a/pkg/observe/test/observe_test.dart |
+++ b/pkg/observe/test/observe_test.dart |
@@ -3,128 +3,205 @@ |
// BSD-style license that can be found in the LICENSE file. |
import 'dart:async'; |
+import 'package:logging/logging.dart'; |
import 'package:observe/observe.dart'; |
+import 'package:observe/src/watcher.dart' as watcher; |
import 'package:unittest/unittest.dart'; |
-import 'utils.dart'; |
+import 'observe_test_utils.dart'; |
+ |
+const _VALUE = const Symbol('value'); |
main() { |
// Note: to test the basic Observable system, we use ObservableBox due to its |
- // simplicity. |
+ // simplicity. We also test a variant that is based on dirty-checking. |
- const _VALUE = const Symbol('value'); |
+ observeTest('no observers at the start', () { |
+ expect(watcher.allObservablesCount, 0); |
+ }); |
- group('ObservableBox', () { |
- test('no observers', () { |
- var t = new ObservableBox<int>(123); |
- expect(t.value, 123); |
- t.value = 42; |
- expect(t.value, 42); |
- expect(t.hasObservers, false); |
- }); |
+ group('WatcherModel', () { observeTests(watch: true); }); |
- test('listen adds an observer', () { |
- var t = new ObservableBox<int>(123); |
- expect(t.hasObservers, false); |
+ group('ObservableBox', () { observeTests(); }); |
- t.changes.listen((n) {}); |
- expect(t.hasObservers, true); |
+ group('dirtyCheck loops can be debugged', () { |
+ var messages; |
+ var subscription; |
+ setUp(() { |
+ messages = []; |
+ subscription = Logger.root.onRecord.listen((record) { |
+ messages.add(record.message); |
+ }); |
}); |
- test('changes delived async', () { |
- var t = new ObservableBox<int>(123); |
- int called = 0; |
- |
- t.changes.listen(expectAsync1((records) { |
- called++; |
- expectChanges(records, [_record(_VALUE), _record(_VALUE)]); |
- })); |
- t.value = 41; |
- t.value = 42; |
- expect(called, 0); |
+ tearDown(() { |
+ subscription.cancel(); |
}); |
- test('cause changes in handler', () { |
- var t = new ObservableBox<int>(123); |
- int called = 0; |
+ test('logs debug information', () { |
+ var maxNumIterations = watcher.MAX_DIRTY_CHECK_CYCLES; |
+ |
+ var x = new WatcherModel(0); |
+ var sub = x.changes.listen(expectAsync1((_) { x.value++; }, |
+ count: maxNumIterations)); |
+ x.value = 1; |
+ Observable.dirtyCheck(); |
+ expect(x.value, maxNumIterations + 1); |
+ expect(messages.length, 2); |
- t.changes.listen(expectAsync1((records) { |
- called++; |
- expectChanges(records, [_record(_VALUE)]); |
- if (called == 1) { |
- // Cause another change |
- t.value = 777; |
- } |
- }, count: 2)); |
+ expect(messages[0], contains('Possible loop')); |
+ expect(messages[1], contains('index 0')); |
+ expect(messages[1], contains('object: $x')); |
- t.value = 42; |
+ sub.cancel(); |
}); |
+ }); |
+} |
- test('multiple observers', () { |
- var t = new ObservableBox<int>(123); |
+observeTests({bool watch: false}) { |
- verifyRecords(records) { |
- expectChanges(records, [_record(_VALUE), _record(_VALUE)]); |
- }; |
+ final createModel = watch ? (x) => new WatcherModel(x) |
+ : (x) => new ObservableBox(x); |
- t.changes.listen(expectAsync1(verifyRecords)); |
- t.changes.listen(expectAsync1(verifyRecords)); |
+ // Track the subscriptions so we can clean them up in tearDown. |
+ List subs; |
- t.value = 41; |
- t.value = 42; |
- }); |
+ int initialObservers; |
+ setUp(() { |
+ initialObservers = watcher.allObservablesCount; |
+ subs = []; |
- test('deliverChangeRecords', () { |
- var t = new ObservableBox<int>(123); |
- var records = []; |
- t.changes.listen((r) { records.addAll(r); }); |
- t.value = 41; |
- t.value = 42; |
- expectChanges(records, [], reason: 'changes delived async'); |
+ if (watch) runAsync(Observable.dirtyCheck); |
+ }); |
- deliverChangeRecords(); |
- expectChanges(records, |
- [_record(_VALUE), _record(_VALUE)]); |
- records.clear(); |
+ tearDown(() { |
+ for (var sub in subs) sub.cancel(); |
+ performMicrotaskCheckpoint(); |
- t.value = 777; |
- expectChanges(records, [], reason: 'changes delived async'); |
+ expect(watcher.allObservablesCount, initialObservers, |
+ reason: 'Observable object leaked'); |
+ }); |
- deliverChangeRecords(); |
- expectChanges(records, [_record(_VALUE)]); |
+ observeTest('no observers', () { |
+ var t = createModel(123); |
+ expect(t.value, 123); |
+ t.value = 42; |
+ expect(t.value, 42); |
+ expect(t.hasObservers, false); |
+ }); |
- // Has no effect if there are no changes |
- deliverChangeRecords(); |
- expectChanges(records, [_record(_VALUE)]); |
- }); |
+ observeTest('listen adds an observer', () { |
+ var t = createModel(123); |
+ expect(t.hasObservers, false); |
+ |
+ subs.add(t.changes.listen((n) {})); |
+ expect(t.hasObservers, true); |
+ }); |
+ |
+ observeTest('changes delived async', () { |
+ var t = createModel(123); |
+ int called = 0; |
+ |
+ subs.add(t.changes.listen(expectAsync1((records) { |
+ called++; |
+ expectChanges(records, _changedValue(watch ? 1 : 2)); |
+ }))); |
- test('cancel listening', () { |
- var t = new ObservableBox<int>(123); |
- var sub; |
- sub = t.changes.listen(expectAsync1((records) { |
- expectChanges(records, [_record(_VALUE)]); |
- sub.cancel(); |
+ t.value = 41; |
+ t.value = 42; |
+ expect(called, 0); |
+ }); |
+ |
+ observeTest('cause changes in handler', () { |
+ var t = createModel(123); |
+ int called = 0; |
+ |
+ subs.add(t.changes.listen(expectAsync1((records) { |
+ called++; |
+ expectChanges(records, _changedValue(1)); |
+ if (called == 1) { |
+ // Cause another change |
t.value = 777; |
- })); |
- t.value = 42; |
- }); |
+ } |
+ }, count: 2))); |
- test('cancel and reobserve', () { |
- var t = new ObservableBox<int>(123); |
- var sub; |
- sub = t.changes.listen(expectAsync1((records) { |
- expectChanges(records, [_record(_VALUE)]); |
- sub.cancel(); |
- |
- runAsync(expectAsync0(() { |
- sub = t.changes.listen(expectAsync1((records) { |
- expectChanges(records, [_record(_VALUE)]); |
- })); |
- t.value = 777; |
- })); |
+ t.value = 42; |
+ }); |
+ |
+ observeTest('multiple observers', () { |
+ var t = createModel(123); |
+ |
+ verifyRecords(records) { |
+ expectChanges(records, _changedValue(watch ? 1 : 2)); |
+ }; |
+ |
+ subs.add(t.changes.listen(expectAsync1(verifyRecords))); |
+ subs.add(t.changes.listen(expectAsync1(verifyRecords))); |
+ |
+ t.value = 41; |
+ t.value = 42; |
+ }); |
+ |
+ observeTest('performMicrotaskCheckpoint', () { |
+ var t = createModel(123); |
+ var records = []; |
+ subs.add(t.changes.listen((r) { records.addAll(r); })); |
+ t.value = 41; |
+ t.value = 42; |
+ expectChanges(records, [], reason: 'changes delived async'); |
+ |
+ performMicrotaskCheckpoint(); |
+ expectChanges(records, _changedValue(watch ? 1 : 2)); |
+ records.clear(); |
+ |
+ t.value = 777; |
+ expectChanges(records, [], reason: 'changes delived async'); |
+ |
+ performMicrotaskCheckpoint(); |
+ expectChanges(records, _changedValue(1)); |
+ |
+ // Has no effect if there are no changes |
+ performMicrotaskCheckpoint(); |
+ expectChanges(records, _changedValue(1)); |
+ }); |
+ |
+ observeTest('cancel listening', () { |
+ var t = createModel(123); |
+ var sub; |
+ sub = t.changes.listen(expectAsync1((records) { |
+ expectChanges(records, _changedValue(1)); |
+ sub.cancel(); |
+ t.value = 777; |
+ runAsync(Observable.dirtyCheck); |
+ })); |
+ t.value = 42; |
+ }); |
+ |
+ observeTest('cancel and reobserve', () { |
+ var t = createModel(123); |
+ var sub; |
+ sub = t.changes.listen(expectAsync1((records) { |
+ expectChanges(records, _changedValue(1)); |
+ sub.cancel(); |
+ |
+ runAsync(expectAsync0(() { |
+ subs.add(t.changes.listen(expectAsync1((records) { |
+ expectChanges(records, _changedValue(1)); |
+ }))); |
+ t.value = 777; |
+ runAsync(Observable.dirtyCheck); |
})); |
- t.value = 42; |
- }); |
+ })); |
+ t.value = 42; |
}); |
} |
-_record(key) => new PropertyChangeRecord(key); |
+_changedValue(len) => new List.filled(len, new PropertyChangeRecord(_VALUE)); |
+ |
+// A test model based on dirty checking. |
+class WatcherModel<T> extends ObservableBase { |
+ @observable T value; |
+ |
+ WatcherModel([T initialValue]) : value = initialValue; |
+ |
+ String toString() => '#<$runtimeType value: $value>'; |
+} |