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 /// A test library for testing test libraries? We must go deeper. | 5 /// A test library for testing test libraries? We must go deeper. |
6 /// | 6 /// |
7 /// Since unit testing code tends to use a lot of global state, it can be tough | 7 /// Since unit testing code tends to use a lot of global state, it can be tough |
8 /// to test. This library manages it by running each test case in a child | 8 /// to test. This library manages it by running each test case in a child |
9 /// isolate, then reporting the results back to the parent isolate. | 9 /// isolate, then reporting the results back to the parent isolate. |
10 library metatest; | 10 library metatest; |
11 | 11 |
12 import 'dart:async'; | 12 import 'dart:async'; |
13 import 'dart:io'; | |
14 import 'dart:isolate'; | 13 import 'dart:isolate'; |
15 | 14 |
16 import 'package:path/path.dart' as path; | |
17 import 'package:unittest/unittest.dart'; | 15 import 'package:unittest/unittest.dart'; |
18 import 'package:scheduled_test/scheduled_test.dart' as scheduled_test; | |
19 | 16 |
20 import 'utils.dart'; | 17 Future defer(void fn()) { |
nweiz
2014/09/08 22:44:40
What is this for? Where is it used? Why not just c
kevmoo
2014/09/10 03:17:28
Done.
| |
18 return new Future.sync(fn); | |
19 } | |
20 | |
21 /// Runs [setUpFn] before every metatest. Note that [setUpFn] will be | |
22 /// overwritten if the test itself calls [setUp]. | |
nweiz
2014/09/08 22:44:41
While you're in here, can you fix the comments to
kevmoo
2014/09/10 03:17:28
Done.
| |
23 void metaSetUp(void setUpFn()) { | |
24 if (_inChildIsolate) setUp(setUpFn); | |
25 } | |
26 | |
27 void expectTestResults(String description, void body(), | |
nweiz
2014/09/08 22:44:40
Document public functions.
kevmoo
2014/09/10 03:17:28
Done.
| |
28 List<Map> expectedResults) { | |
29 _setUpTest(description, body, (resultsMap) { | |
30 var list = resultsMap['results'] as List; | |
nweiz
2014/09/08 22:44:40
Don't type annotate local variables. "as" counts.
kevmoo
2014/09/10 03:17:28
Done.
| |
31 expect(list, hasLength(expectedResults.length)); | |
nweiz
2014/09/08 22:44:41
Why don't these [expect] calls have reasons anymor
kevmoo
2014/09/10 03:17:28
Done.
| |
32 | |
33 for (var i = 0; i < list.length; i++) { | |
34 var expectedMap = expectedResults[i]; | |
35 var map = list[i] as Map; | |
36 | |
37 expectedMap.forEach((key, value) { | |
38 expect(map, containsPair(key, value)); | |
39 }); | |
40 } | |
41 }); | |
42 } | |
43 | |
44 void expectSingleTest(String description, String result, String message, void | |
45 body()) { | |
nweiz
2014/09/08 22:44:40
Nit: don't split the type and variable name across
kevmoo
2014/09/10 03:17:28
Done.
| |
46 _setUpTest(description, body, (results) { | |
47 var testResults = results['results'] as List; | |
48 if (testResults.length != 1) { | |
49 throw 'Expected only one test to run'; | |
nweiz
2014/09/08 22:44:40
Don't throw strings.
kevmoo
2014/09/10 03:17:28
Done.
| |
50 } | |
51 var testResult = testResults.single as Map; | |
52 expect(testResult['result'], result); | |
53 expect(testResult['message'], message); | |
54 }); | |
55 } | |
21 | 56 |
22 /// Declares a test with the given [description] and [body]. [body] corresponds | 57 /// Declares a test with the given [description] and [body]. [body] corresponds |
23 /// to the `main` method of a test file, and will be run in an isolate. By | 58 /// to the `main` method of a test file, and will be run in an isolate. By |
24 /// default, this expects that all tests defined in [body] pass, but if | 59 /// default, this expects that all tests defined in [body] pass, but if |
25 /// [passing] is passed, only tests listed there are expected to pass. | 60 /// [passing] is passed, only tests listed there are expected to pass. |
26 void expectTestsPass(String description, void body(), {List<String> passing}) { | 61 void expectTestsPass(String description, void body(), {List<String> passing}) { |
27 _setUpTest(description, body, (results) { | 62 _setUpTest(description, body, (results) { |
28 if (_hasError(results)) { | 63 if (_hasError(results)) { |
29 throw 'Expected all tests to pass, but got error(s):\n' | 64 throw 'Expected all tests to pass, but got error(s):\n' |
30 '${_summarizeTests(results)}'; | 65 '${_summarizeTests(results)}'; |
31 } else if (passing == null) { | 66 } else if (passing == null) { |
32 if (results['failed'] != 0) { | 67 if (results['failed'] != 0) { |
33 throw 'Expected all tests to pass, but some failed:\n' | 68 throw 'Expected all tests to pass, but some failed:\n' |
34 '${_summarizeTests(results)}'; | 69 '${_summarizeTests(results)}'; |
35 } | 70 } |
36 } else { | 71 } else { |
37 var shouldPass = new Set.from(passing); | 72 var shouldPass = new Set.from(passing); |
38 var didPass = new Set.from(results['results'] | 73 var didPass = |
39 .where((t) => t['result'] == 'pass') | 74 new Set.from( |
40 .map((t) => t['description'])); | 75 results['results'].where( |
76 (t) => t['result'] == 'pass').map((t) => t['description'])); | |
nweiz
2014/09/08 22:44:41
Why did you change the formattin ghere? This looks
kevmoo
2014/09/10 03:17:28
Done.
| |
41 | 77 |
42 if (!shouldPass.containsAll(didPass) || | 78 if (!shouldPass.containsAll(didPass) || |
43 !didPass.containsAll(shouldPass)) { | 79 !didPass.containsAll(shouldPass)) { |
44 String stringify(Set<String> tests) => | 80 String stringify(Set<String> tests) => |
45 '{${tests.map((t) => '"$t"').join(', ')}}'; | 81 '{${tests.map((t) => '"$t"').join(', ')}}'; |
46 | 82 |
47 fail('Expected exactly ${stringify(shouldPass)} to pass, but ' | 83 fail( |
48 '${stringify(didPass)} passed.\n' | 84 'Expected exactly ${stringify(shouldPass)} to pass, but ' |
nweiz
2014/09/08 22:44:40
This also doesn't seem like a good reformatting at
kevmoo
2014/09/10 03:17:28
Done.
| |
49 '${_summarizeTests(results)}'); | 85 '${stringify(didPass)} passed.\n' '${_summarizeTests(results)}') ; |
50 } | 86 } |
51 } | 87 } |
52 }); | 88 }); |
53 } | 89 } |
54 | 90 |
55 /// Declares a test with the given [description] and [body]. | |
56 /// | |
57 /// [body] corresponds | |
58 /// to the `main` method of a test file, and will be run in an isolate. | |
59 /// | |
60 /// All tests must have an expected result in [expectedResults]. | |
61 void expectTests(String description, void body(), | |
62 Map<String, String> expectedResults) { | |
63 _setUpTest(description, body, (results) { | |
64 expectedResults = new Map.from(expectedResults); | |
65 | |
66 for (var testResult in results['results']) { | |
67 var description = testResult['description']; | |
68 | |
69 expect(expectedResults, contains(description), | |
70 reason: '"$description" did not have an expected result set.\n' | |
71 '${_summarizeTests(results)}'); | |
72 | |
73 var result = testResult['result']; | |
74 | |
75 expect(expectedResults, containsPair(description, result), | |
76 reason: 'The test "$description" not not have the expected result.\n' | |
77 '${_summarizeTests(results)}'); | |
78 | |
79 expectedResults.remove(description); | |
80 } | |
81 expect(expectedResults, isEmpty, | |
82 reason: 'Unexpected additional test results\n' | |
83 '${_summarizeTests(results)}'); | |
84 }); | |
85 } | |
86 | |
87 /// Declares a test with the given [description] and [body]. [body] corresponds | 91 /// Declares a test with the given [description] and [body]. [body] corresponds |
88 /// to the `main` method of a test file, and will be run in an isolate. Expects | 92 /// to the `main` method of a test file, and will be run in an isolate. Expects |
89 /// all tests defined by [body] to fail. | 93 /// all tests defined by [body] to fail. |
90 void expectTestsFail(String description, void body()) { | 94 void expectTestsFail(String description, void body()) { |
91 _setUpTest(description, body, (results) { | 95 _setUpTest(description, body, (results) { |
92 if (_hasError(results)) { | 96 if (_hasError(results)) { |
93 throw 'Expected all tests to fail, but got error(s):\n' | 97 throw 'Expected all tests to fail, but got error(s):\n' |
94 '${_summarizeTests(results)}'; | 98 '${_summarizeTests(results)}'; |
95 } else if (results['passed'] != 0) { | 99 } else if (results['passed'] != 0) { |
96 throw 'Expected all tests to fail, but some passed:\n' | 100 throw 'Expected all tests to fail, but some passed:\n' |
97 '${_summarizeTests(results)}'; | 101 '${_summarizeTests(results)}'; |
98 } | 102 } |
99 }); | 103 }); |
100 } | 104 } |
101 | 105 |
102 /// Runs [setUpFn] before every metatest. Note that [setUpFn] will be | |
103 /// overwritten if the test itself calls [setUp]. | |
104 void metaSetUp(void setUpFn()) { | |
105 if (_inChildIsolate) scheduled_test.setUp(setUpFn); | |
106 } | |
107 | |
108 /// Sets up a test with the given [description] and [body]. After the test runs, | 106 /// Sets up a test with the given [description] and [body]. After the test runs, |
109 /// calls [validate] with the result map. | 107 /// calls [validate] with the result map. |
110 void _setUpTest(String description, void body(), void validate(Map)) { | 108 void _setUpTest(String description, void body(), void validate(Map map)) { |
111 if (_inChildIsolate) { | 109 if (_inChildIsolate) { |
112 _ensureInitialized(); | 110 _ensureInitialized(); |
113 if (_testToRun == description) body(); | 111 if (_testToRun == description) body(); |
114 } else { | 112 } else { |
115 test(description, () { | 113 test(description, () { |
116 expect(_runInIsolate(description).then(validate), completes); | 114 return _runInIsolate(description).then(validate); |
117 }); | 115 }); |
118 } | 116 } |
119 } | 117 } |
120 | 118 |
121 /// The description of the test to run in the child isolate. | 119 /// The description of the test to run in the child isolate. |
122 /// | 120 /// |
123 /// `null` in the parent isolate. | 121 /// `null` in the parent isolate. |
124 String _testToRun; | 122 String _testToRun; |
125 | 123 |
126 /// The port with which the child isolate should communicate with the parent | 124 /// The port with which the child isolate should communicate with the parent |
127 /// isolate. | 125 /// isolate. |
128 /// | 126 /// |
129 /// `null` in the parent isolate. | 127 /// `null` in the parent isolate. |
130 SendPort _replyTo; | 128 SendPort _replyTo; |
131 | 129 |
132 /// Whether or not we're running in a child isolate that's supposed to run a | 130 /// Whether or not we're running in a child isolate that's supposed to run a |
133 /// test. | 131 /// test. |
134 bool _inChildIsolate; | 132 bool _inChildIsolate; |
135 | 133 |
134 Function _testBody; | |
nweiz
2014/09/08 22:44:40
Document these fields.
kevmoo
2014/09/10 03:17:28
Done.
| |
135 | |
136 Duration _timeoutOverride; | |
137 | |
136 /// Initialize metatest. | 138 /// Initialize metatest. |
137 /// | 139 /// |
138 /// [message] should be the second argument to [main]. It's used to determine | 140 /// [message] should be the second argument to [main]. It's used to determine |
139 /// whether this test is in the parent isolate or a child isolate. | 141 /// whether this test is in the parent isolate or a child isolate. |
nweiz
2014/09/08 22:44:40
Document [timeout].
kevmoo
2014/09/10 03:17:28
Done.
| |
140 void initMetatest(message) { | 142 void initMetatest(Map message, {Duration timeout}) { |
nweiz
2014/09/08 22:44:40
I don't like type-annotating [message] here. The f
kevmoo
2014/09/10 03:17:28
Done.
| |
143 _timeoutOverride = timeout; | |
141 if (message == null) { | 144 if (message == null) { |
142 _inChildIsolate = false; | 145 _inChildIsolate = false; |
143 } else { | 146 } else { |
144 _testToRun = message['testToRun']; | 147 _testToRun = message['testToRun']; |
145 _replyTo = message['replyTo']; | 148 _replyTo = message['replyTo']; |
146 _inChildIsolate = true; | 149 _inChildIsolate = true; |
147 } | 150 } |
148 } | 151 } |
149 | 152 |
153 // TODO(kevmoo) We need to capture the main method to allow running in an | |
154 // isolate. There is no mechanism to capture the current executing URI between | |
155 // browser and vm. Issue 1145 and/or Issue 8440 | |
156 void initTests(void testBody(message)) { | |
nweiz
2014/09/08 22:44:40
Why does the user have to call both [initTests] an
kevmoo
2014/09/10 03:17:28
I wish I could figure out how to do this. _body is
nweiz
2014/09/10 21:04:41
Oh, I see, it's the top-level-function-only thing.
| |
157 _testBody = testBody; | |
158 _testBody(null); | |
159 } | |
160 | |
150 /// Runs the test described by [description] in its own isolate. Returns a map | 161 /// Runs the test described by [description] in its own isolate. Returns a map |
151 /// describing the results of that test run. | 162 /// describing the results of that test run. |
152 Future<Map> _runInIsolate(String description) { | 163 Future<Map> _runInIsolate(String description) { |
164 if (_testBody == null) { | |
165 throw new StateError('initTests was not called.'); | |
nweiz
2014/09/08 22:44:40
Explain how to use initTests here.
kevmoo
2014/09/10 03:17:28
Done.
| |
166 } | |
167 | |
153 var replyPort = new ReceivePort(); | 168 var replyPort = new ReceivePort(); |
154 // TODO(nweiz): Don't use path here once issue 8440 is fixed. | 169 return Isolate.spawn(_testBody, { |
155 var uri = path.toUri(path.absolute(path.fromUri(Platform.script))); | |
156 return Isolate.spawnUri(uri, [], { | |
157 'testToRun': description, | 170 'testToRun': description, |
158 'replyTo': replyPort.sendPort | 171 'replyTo': replyPort.sendPort |
159 }).then((_) { | 172 }).then((_) { |
160 // TODO(nweiz): Remove this timeout once issue 8875 is fixed and we can | 173 return replyPort.first; |
nweiz
2014/09/08 22:44:41
Nit: Use "=>" here
kevmoo
2014/09/10 03:17:28
Done.
| |
161 // capture top-level exceptions. | |
162 return timeout(replyPort.first, 30 * 1000, () { | |
163 throw 'Timed out waiting for test to complete.'; | |
164 }); | |
165 }); | 174 }); |
166 } | 175 } |
167 | 176 |
168 /// Returns whether [results] (a test result map) describes a test run in which | 177 /// Returns whether [results] (a test result map) describes a test run in which |
169 /// an error occurred. | 178 /// an error occurred. |
170 bool _hasError(Map results) { | 179 bool _hasError(Map results) { |
171 return results['errors'] > 0 || results['uncaughtError'] != null || | 180 return results['errors'] > 0 || |
181 results['uncaughtError'] != null || | |
nweiz
2014/09/08 22:44:40
Fix all these gratuitous formatting changes.
kevmoo
2014/09/10 03:17:28
Done.
| |
172 (results['passed'] == 0 && results['failed'] == 0); | 182 (results['passed'] == 0 && results['failed'] == 0); |
173 } | 183 } |
174 | 184 |
175 /// Returns a string description of the test run descibed by [results]. | 185 /// Returns a string description of the test run descibed by [results]. |
176 String _summarizeTests(Map results) { | 186 String _summarizeTests(Map results) { |
177 var buffer = new StringBuffer(); | 187 var buffer = new StringBuffer(); |
178 for (var t in results["results"]) { | 188 for (var t in results["results"]) { |
179 buffer.writeln("${t['result'].toUpperCase()}: ${t['description']}"); | 189 buffer.writeln("${t['result'].toUpperCase()}: ${t['description']}"); |
180 if (t['message'] != '') buffer.writeln("${_indent(t['message'])}"); | 190 if (t['message'] != '') buffer.writeln("${_indent(t['message'])}"); |
181 if (t['stackTrace'] != null && t['stackTrace'] != '') { | 191 if (t['stackTrace'] != null && t['stackTrace'] != '') { |
182 buffer.writeln("${_indent(t['stackTrace'])}"); | 192 buffer.writeln("${_indent(t['stackTrace'])}"); |
183 } | 193 } |
184 } | 194 } |
185 | 195 |
186 buffer.writeln(); | 196 buffer.writeln(); |
187 | 197 |
188 var success = false; | 198 var success = false; |
189 if (results['passed'] == 0 && results['failed'] == 0 && | 199 if (results['passed'] == 0 && |
190 results['errors'] == 0 && results['uncaughtError'] == null) { | 200 results['failed'] == 0 && |
201 results['errors'] == 0 && | |
202 results['uncaughtError'] == null) { | |
191 buffer.write('No tests found.'); | 203 buffer.write('No tests found.'); |
192 // This is considered a failure too. | 204 // This is considered a failure too. |
193 } else if (results['failed'] == 0 && results['errors'] == 0 && | 205 } else if (results['failed'] == 0 && |
206 results['errors'] == 0 && | |
194 results['uncaughtError'] == null) { | 207 results['uncaughtError'] == null) { |
195 buffer.write('All ${results['passed']} tests passed.'); | 208 buffer.write('All ${results['passed']} tests passed.'); |
196 success = true; | 209 success = true; |
197 } else { | 210 } else { |
198 if (results['uncaughtError'] != null) { | 211 if (results['uncaughtError'] != null) { |
199 buffer.write('Top-level uncaught error: ${results['uncaughtError']}'); | 212 buffer.write('Top-level uncaught error: ${results['uncaughtError']}'); |
200 } | 213 } |
201 buffer.write('${results['passed']} PASSED, ${results['failed']} FAILED, ' | 214 buffer.write( |
202 '${results['errors']} ERRORS'); | 215 '${results['passed']} PASSED, ${results['failed']} FAILED, ' |
216 '${results['errors']} ERRORS'); | |
203 } | 217 } |
204 return prefixLines(buffer.toString()); | 218 return _prefixLines(buffer.toString()); |
205 } | 219 } |
206 | 220 |
207 /// Indents each line of [str] by two spaces. | 221 /// Indents each line of [str] by two spaces. |
208 String _indent(String str) { | 222 String _indent(String str) { |
209 return str.replaceAll(new RegExp("^", multiLine: true), " "); | 223 return str.replaceAll(new RegExp("^", multiLine: true), " "); |
210 } | 224 } |
211 | 225 |
212 /// Ensure that the metatest configuration is loaded. | 226 /// Ensure that the metatest configuration is loaded. |
213 void _ensureInitialized() { | 227 void _ensureInitialized() { |
214 unittestConfiguration = _singleton; | 228 unittestConfiguration = _singleton; |
229 if (_timeoutOverride != null) { | |
230 unittestConfiguration.timeout = _timeoutOverride; | |
231 } | |
215 } | 232 } |
216 | 233 |
217 final _singleton = new _MetaConfiguration(); | 234 final _singleton = new _MetaConfiguration(); |
218 | 235 |
219 /// Special test configuration for use within the child isolates. This hides all | 236 /// Special test configuration for use within the child isolates. This hides all |
220 /// output and reports data back to the parent isolate. | 237 /// output and reports data back to the parent isolate. |
221 class _MetaConfiguration extends Configuration { | 238 class _MetaConfiguration extends Configuration { |
222 | 239 |
223 _MetaConfiguration() : super.blank(); | 240 _MetaConfiguration() : super.blank(); |
224 | 241 |
225 void onSummary(int passed, int failed, int errors, List<TestCase> results, | 242 void onSummary(int passed, int failed, int errors, List<TestCase> results, |
226 String uncaughtError) { | 243 String uncaughtError) { |
227 _replyTo.send({ | 244 _replyTo.send({ |
228 "passed": passed, | 245 "passed": passed, |
229 "failed": failed, | 246 "failed": failed, |
230 "errors": errors, | 247 "errors": errors, |
231 "uncaughtError": uncaughtError, | 248 "uncaughtError": uncaughtError, |
232 "results": results.map((testCase) => { | 249 "results": results.map((testCase) => { |
233 "description": testCase.description, | 250 "description": testCase.description, |
234 "message": testCase.message, | 251 "message": testCase.message, |
235 "result": testCase.result, | 252 "result": testCase.result, |
236 "stackTrace": testCase.stackTrace.toString() | 253 "stackTrace": testCase.stackTrace.toString() |
237 }).toList() | 254 }).toList() |
238 }); | 255 }); |
239 } | 256 } |
240 } | 257 } |
258 | |
259 /// Prepends each line in [text] with [prefix]. If [firstPrefix] is passed, the | |
260 /// first line is prefixed with that instead. | |
261 String _prefixLines(String text, {String prefix: '| ', String firstPrefix}) { | |
nweiz
2014/09/08 22:44:40
This should go in a utils file.
kevmoo
2014/09/10 03:17:28
Done.
| |
262 var lines = text.split('\n'); | |
263 if (firstPrefix == null) { | |
264 return lines.map((line) => '$prefix$line').join('\n'); | |
265 } | |
266 | |
267 var firstLine = "$firstPrefix${lines.first}"; | |
268 lines = lines.skip(1).map((line) => '$prefix$line').toList(); | |
269 lines.insert(0, firstLine); | |
270 return lines.join('\n'); | |
271 } | |
OLD | NEW |