OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 import 'dart:async'; | 5 import 'dart:async'; |
| 6 import 'package:logging/logging.dart'; |
6 import 'package:observe/observe.dart'; | 7 import 'package:observe/observe.dart'; |
| 8 import 'package:observe/src/watcher.dart' as watcher; |
7 import 'package:unittest/unittest.dart'; | 9 import 'package:unittest/unittest.dart'; |
8 import 'utils.dart'; | 10 import 'observe_test_utils.dart'; |
| 11 |
| 12 const _VALUE = const Symbol('value'); |
9 | 13 |
10 main() { | 14 main() { |
11 // Note: to test the basic Observable system, we use ObservableBox due to its | 15 // Note: to test the basic Observable system, we use ObservableBox due to its |
12 // simplicity. | 16 // simplicity. We also test a variant that is based on dirty-checking. |
13 | 17 |
14 const _VALUE = const Symbol('value'); | 18 observeTest('no observers at the start', () { |
15 | 19 expect(watcher.allObservablesCount, 0); |
16 group('ObservableBox', () { | 20 }); |
17 test('no observers', () { | 21 |
18 var t = new ObservableBox<int>(123); | 22 group('WatcherModel', () { observeTests(watch: true); }); |
19 expect(t.value, 123); | 23 |
20 t.value = 42; | 24 group('ObservableBox', () { observeTests(); }); |
21 expect(t.value, 42); | 25 |
22 expect(t.hasObservers, false); | 26 group('dirtyCheck loops can be debugged', () { |
| 27 var messages; |
| 28 var subscription; |
| 29 setUp(() { |
| 30 messages = []; |
| 31 subscription = Logger.root.onRecord.listen((record) { |
| 32 messages.add(record.message); |
| 33 }); |
23 }); | 34 }); |
24 | 35 |
25 test('listen adds an observer', () { | 36 tearDown(() { |
26 var t = new ObservableBox<int>(123); | 37 subscription.cancel(); |
27 expect(t.hasObservers, false); | |
28 | |
29 t.changes.listen((n) {}); | |
30 expect(t.hasObservers, true); | |
31 }); | 38 }); |
32 | 39 |
33 test('changes delived async', () { | 40 test('logs debug information', () { |
34 var t = new ObservableBox<int>(123); | 41 var maxNumIterations = watcher.MAX_DIRTY_CHECK_CYCLES; |
35 int called = 0; | 42 |
36 | 43 var x = new WatcherModel(0); |
37 t.changes.listen(expectAsync1((records) { | 44 var sub = x.changes.listen(expectAsync1((_) { x.value++; }, |
38 called++; | 45 count: maxNumIterations)); |
39 expectChanges(records, [_record(_VALUE), _record(_VALUE)]); | 46 x.value = 1; |
| 47 Observable.dirtyCheck(); |
| 48 expect(x.value, maxNumIterations + 1); |
| 49 expect(messages.length, 2); |
| 50 |
| 51 expect(messages[0], contains('Possible loop')); |
| 52 expect(messages[1], contains('index 0')); |
| 53 expect(messages[1], contains('object: $x')); |
| 54 |
| 55 sub.cancel(); |
| 56 }); |
| 57 }); |
| 58 } |
| 59 |
| 60 observeTests({bool watch: false}) { |
| 61 |
| 62 final createModel = watch ? (x) => new WatcherModel(x) |
| 63 : (x) => new ObservableBox(x); |
| 64 |
| 65 // Track the subscriptions so we can clean them up in tearDown. |
| 66 List subs; |
| 67 |
| 68 int initialObservers; |
| 69 setUp(() { |
| 70 initialObservers = watcher.allObservablesCount; |
| 71 subs = []; |
| 72 |
| 73 if (watch) runAsync(Observable.dirtyCheck); |
| 74 }); |
| 75 |
| 76 tearDown(() { |
| 77 for (var sub in subs) sub.cancel(); |
| 78 performMicrotaskCheckpoint(); |
| 79 |
| 80 expect(watcher.allObservablesCount, initialObservers, |
| 81 reason: 'Observable object leaked'); |
| 82 }); |
| 83 |
| 84 observeTest('no observers', () { |
| 85 var t = createModel(123); |
| 86 expect(t.value, 123); |
| 87 t.value = 42; |
| 88 expect(t.value, 42); |
| 89 expect(t.hasObservers, false); |
| 90 }); |
| 91 |
| 92 observeTest('listen adds an observer', () { |
| 93 var t = createModel(123); |
| 94 expect(t.hasObservers, false); |
| 95 |
| 96 subs.add(t.changes.listen((n) {})); |
| 97 expect(t.hasObservers, true); |
| 98 }); |
| 99 |
| 100 observeTest('changes delived async', () { |
| 101 var t = createModel(123); |
| 102 int called = 0; |
| 103 |
| 104 subs.add(t.changes.listen(expectAsync1((records) { |
| 105 called++; |
| 106 expectChanges(records, _changedValue(watch ? 1 : 2)); |
| 107 }))); |
| 108 |
| 109 t.value = 41; |
| 110 t.value = 42; |
| 111 expect(called, 0); |
| 112 }); |
| 113 |
| 114 observeTest('cause changes in handler', () { |
| 115 var t = createModel(123); |
| 116 int called = 0; |
| 117 |
| 118 subs.add(t.changes.listen(expectAsync1((records) { |
| 119 called++; |
| 120 expectChanges(records, _changedValue(1)); |
| 121 if (called == 1) { |
| 122 // Cause another change |
| 123 t.value = 777; |
| 124 } |
| 125 }, count: 2))); |
| 126 |
| 127 t.value = 42; |
| 128 }); |
| 129 |
| 130 observeTest('multiple observers', () { |
| 131 var t = createModel(123); |
| 132 |
| 133 verifyRecords(records) { |
| 134 expectChanges(records, _changedValue(watch ? 1 : 2)); |
| 135 }; |
| 136 |
| 137 subs.add(t.changes.listen(expectAsync1(verifyRecords))); |
| 138 subs.add(t.changes.listen(expectAsync1(verifyRecords))); |
| 139 |
| 140 t.value = 41; |
| 141 t.value = 42; |
| 142 }); |
| 143 |
| 144 observeTest('performMicrotaskCheckpoint', () { |
| 145 var t = createModel(123); |
| 146 var records = []; |
| 147 subs.add(t.changes.listen((r) { records.addAll(r); })); |
| 148 t.value = 41; |
| 149 t.value = 42; |
| 150 expectChanges(records, [], reason: 'changes delived async'); |
| 151 |
| 152 performMicrotaskCheckpoint(); |
| 153 expectChanges(records, _changedValue(watch ? 1 : 2)); |
| 154 records.clear(); |
| 155 |
| 156 t.value = 777; |
| 157 expectChanges(records, [], reason: 'changes delived async'); |
| 158 |
| 159 performMicrotaskCheckpoint(); |
| 160 expectChanges(records, _changedValue(1)); |
| 161 |
| 162 // Has no effect if there are no changes |
| 163 performMicrotaskCheckpoint(); |
| 164 expectChanges(records, _changedValue(1)); |
| 165 }); |
| 166 |
| 167 observeTest('cancel listening', () { |
| 168 var t = createModel(123); |
| 169 var sub; |
| 170 sub = t.changes.listen(expectAsync1((records) { |
| 171 expectChanges(records, _changedValue(1)); |
| 172 sub.cancel(); |
| 173 t.value = 777; |
| 174 runAsync(Observable.dirtyCheck); |
| 175 })); |
| 176 t.value = 42; |
| 177 }); |
| 178 |
| 179 observeTest('cancel and reobserve', () { |
| 180 var t = createModel(123); |
| 181 var sub; |
| 182 sub = t.changes.listen(expectAsync1((records) { |
| 183 expectChanges(records, _changedValue(1)); |
| 184 sub.cancel(); |
| 185 |
| 186 runAsync(expectAsync0(() { |
| 187 subs.add(t.changes.listen(expectAsync1((records) { |
| 188 expectChanges(records, _changedValue(1)); |
| 189 }))); |
| 190 t.value = 777; |
| 191 runAsync(Observable.dirtyCheck); |
40 })); | 192 })); |
41 t.value = 41; | 193 })); |
42 t.value = 42; | 194 t.value = 42; |
43 expect(called, 0); | |
44 }); | |
45 | |
46 test('cause changes in handler', () { | |
47 var t = new ObservableBox<int>(123); | |
48 int called = 0; | |
49 | |
50 t.changes.listen(expectAsync1((records) { | |
51 called++; | |
52 expectChanges(records, [_record(_VALUE)]); | |
53 if (called == 1) { | |
54 // Cause another change | |
55 t.value = 777; | |
56 } | |
57 }, count: 2)); | |
58 | |
59 t.value = 42; | |
60 }); | |
61 | |
62 test('multiple observers', () { | |
63 var t = new ObservableBox<int>(123); | |
64 | |
65 verifyRecords(records) { | |
66 expectChanges(records, [_record(_VALUE), _record(_VALUE)]); | |
67 }; | |
68 | |
69 t.changes.listen(expectAsync1(verifyRecords)); | |
70 t.changes.listen(expectAsync1(verifyRecords)); | |
71 | |
72 t.value = 41; | |
73 t.value = 42; | |
74 }); | |
75 | |
76 test('deliverChangeRecords', () { | |
77 var t = new ObservableBox<int>(123); | |
78 var records = []; | |
79 t.changes.listen((r) { records.addAll(r); }); | |
80 t.value = 41; | |
81 t.value = 42; | |
82 expectChanges(records, [], reason: 'changes delived async'); | |
83 | |
84 deliverChangeRecords(); | |
85 expectChanges(records, | |
86 [_record(_VALUE), _record(_VALUE)]); | |
87 records.clear(); | |
88 | |
89 t.value = 777; | |
90 expectChanges(records, [], reason: 'changes delived async'); | |
91 | |
92 deliverChangeRecords(); | |
93 expectChanges(records, [_record(_VALUE)]); | |
94 | |
95 // Has no effect if there are no changes | |
96 deliverChangeRecords(); | |
97 expectChanges(records, [_record(_VALUE)]); | |
98 }); | |
99 | |
100 test('cancel listening', () { | |
101 var t = new ObservableBox<int>(123); | |
102 var sub; | |
103 sub = t.changes.listen(expectAsync1((records) { | |
104 expectChanges(records, [_record(_VALUE)]); | |
105 sub.cancel(); | |
106 t.value = 777; | |
107 })); | |
108 t.value = 42; | |
109 }); | |
110 | |
111 test('cancel and reobserve', () { | |
112 var t = new ObservableBox<int>(123); | |
113 var sub; | |
114 sub = t.changes.listen(expectAsync1((records) { | |
115 expectChanges(records, [_record(_VALUE)]); | |
116 sub.cancel(); | |
117 | |
118 runAsync(expectAsync0(() { | |
119 sub = t.changes.listen(expectAsync1((records) { | |
120 expectChanges(records, [_record(_VALUE)]); | |
121 })); | |
122 t.value = 777; | |
123 })); | |
124 })); | |
125 t.value = 42; | |
126 }); | |
127 }); | 195 }); |
128 } | 196 } |
129 | 197 |
130 _record(key) => new PropertyChangeRecord(key); | 198 _changedValue(len) => new List.filled(len, new PropertyChangeRecord(_VALUE)); |
| 199 |
| 200 // A test model based on dirty checking. |
| 201 class WatcherModel<T> extends ObservableBase { |
| 202 @observable T value; |
| 203 |
| 204 WatcherModel([T initialValue]) : value = initialValue; |
| 205 |
| 206 String toString() => '#<$runtimeType value: $value>'; |
| 207 } |
OLD | NEW |