OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /// A test library for testing test libraries? We must go deeper. | |
6 /// | |
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 | |
9 /// isolate, then reporting the results back to the parent isolate. | |
10 library metatest; | |
11 | |
12 import 'dart:async'; | |
13 import 'dart:io'; | |
14 import 'dart:isolate'; | |
15 | |
16 import 'package:path/path.dart' as path; | |
17 import 'package:unittest/unittest.dart'; | |
18 import 'package:scheduled_test/scheduled_test.dart' as scheduled_test; | |
19 | |
20 import 'utils.dart'; | |
21 | |
22 /// 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 | |
24 /// default, this expects that all tests defined in [body] pass, but if | |
25 /// [passing] is passed, only tests listed there are expected to pass. | |
26 void expectTestsPass(String description, void body(), {List<String> passing}) { | |
27 _setUpTest(description, body, (results) { | |
28 if (_hasError(results)) { | |
29 throw 'Expected all tests to pass, but got error(s):\n' | |
30 '${_summarizeTests(results)}'; | |
31 } else if (passing == null) { | |
32 if (results['failed'] != 0) { | |
33 throw 'Expected all tests to pass, but some failed:\n' | |
34 '${_summarizeTests(results)}'; | |
35 } | |
36 } else { | |
37 var shouldPass = new Set.from(passing); | |
38 var didPass = new Set.from(results['results'] | |
39 .where((t) => t['result'] == 'pass') | |
40 .map((t) => t['description'])); | |
41 | |
42 if (!shouldPass.containsAll(didPass) || | |
43 !didPass.containsAll(shouldPass)) { | |
44 String stringify(Set<String> tests) => | |
45 '{${tests.map((t) => '"$t"').join(', ')}}'; | |
46 | |
47 fail('Expected exactly ${stringify(shouldPass)} to pass, but ' | |
48 '${stringify(didPass)} passed.\n' | |
49 '${_summarizeTests(results)}'); | |
50 } | |
51 } | |
52 }); | |
53 } | |
54 | |
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 | |
88 /// to the `main` method of a test file, and will be run in an isolate. Expects | |
89 /// all tests defined by [body] to fail. | |
90 void expectTestsFail(String description, void body()) { | |
91 _setUpTest(description, body, (results) { | |
92 if (_hasError(results)) { | |
93 throw 'Expected all tests to fail, but got error(s):\n' | |
94 '${_summarizeTests(results)}'; | |
95 } else if (results['passed'] != 0) { | |
96 throw 'Expected all tests to fail, but some passed:\n' | |
97 '${_summarizeTests(results)}'; | |
98 } | |
99 }); | |
100 } | |
101 | |
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, | |
109 /// calls [validate] with the result map. | |
110 void _setUpTest(String description, void body(), void validate(Map)) { | |
111 if (_inChildIsolate) { | |
112 _ensureInitialized(); | |
113 if (_testToRun == description) body(); | |
114 } else { | |
115 test(description, () { | |
116 expect(_runInIsolate(description).then(validate), completes); | |
117 }); | |
118 } | |
119 } | |
120 | |
121 /// The description of the test to run in the child isolate. | |
122 /// | |
123 /// `null` in the parent isolate. | |
124 String _testToRun; | |
125 | |
126 /// The port with which the child isolate should communicate with the parent | |
127 /// isolate. | |
128 /// | |
129 /// `null` in the parent isolate. | |
130 SendPort _replyTo; | |
131 | |
132 /// Whether or not we're running in a child isolate that's supposed to run a | |
133 /// test. | |
134 bool _inChildIsolate; | |
135 | |
136 /// Initialize metatest. | |
137 /// | |
138 /// [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. | |
140 void initMetatest(message) { | |
141 if (message == null) { | |
142 _inChildIsolate = false; | |
143 } else { | |
144 _testToRun = message['testToRun']; | |
145 _replyTo = message['replyTo']; | |
146 _inChildIsolate = true; | |
147 } | |
148 } | |
149 | |
150 /// Runs the test described by [description] in its own isolate. Returns a map | |
151 /// describing the results of that test run. | |
152 Future<Map> _runInIsolate(String description) { | |
153 var replyPort = new ReceivePort(); | |
154 // TODO(nweiz): Don't use path here once issue 8440 is fixed. | |
155 var uri = path.toUri(path.absolute(path.fromUri(Platform.script))); | |
156 return Isolate.spawnUri(uri, [], { | |
157 'testToRun': description, | |
158 'replyTo': replyPort.sendPort | |
159 }).then((_) { | |
160 // TODO(nweiz): Remove this timeout once issue 8875 is fixed and we can | |
161 // capture top-level exceptions. | |
162 return timeout(replyPort.first, 30 * 1000, () { | |
163 throw 'Timed out waiting for test to complete.'; | |
164 }); | |
165 }); | |
166 } | |
167 | |
168 /// Returns whether [results] (a test result map) describes a test run in which | |
169 /// an error occurred. | |
170 bool _hasError(Map results) { | |
171 return results['errors'] > 0 || results['uncaughtError'] != null || | |
172 (results['passed'] == 0 && results['failed'] == 0); | |
173 } | |
174 | |
175 /// Returns a string description of the test run descibed by [results]. | |
176 String _summarizeTests(Map results) { | |
177 var buffer = new StringBuffer(); | |
178 for (var t in results["results"]) { | |
179 buffer.writeln("${t['result'].toUpperCase()}: ${t['description']}"); | |
180 if (t['message'] != '') buffer.writeln("${_indent(t['message'])}"); | |
181 if (t['stackTrace'] != null && t['stackTrace'] != '') { | |
182 buffer.writeln("${_indent(t['stackTrace'])}"); | |
183 } | |
184 } | |
185 | |
186 buffer.writeln(); | |
187 | |
188 var success = false; | |
189 if (results['passed'] == 0 && results['failed'] == 0 && | |
190 results['errors'] == 0 && results['uncaughtError'] == null) { | |
191 buffer.write('No tests found.'); | |
192 // This is considered a failure too. | |
193 } else if (results['failed'] == 0 && results['errors'] == 0 && | |
194 results['uncaughtError'] == null) { | |
195 buffer.write('All ${results['passed']} tests passed.'); | |
196 success = true; | |
197 } else { | |
198 if (results['uncaughtError'] != null) { | |
199 buffer.write('Top-level uncaught error: ${results['uncaughtError']}'); | |
200 } | |
201 buffer.write('${results['passed']} PASSED, ${results['failed']} FAILED, ' | |
202 '${results['errors']} ERRORS'); | |
203 } | |
204 return prefixLines(buffer.toString()); | |
205 } | |
206 | |
207 /// Indents each line of [str] by two spaces. | |
208 String _indent(String str) { | |
209 return str.replaceAll(new RegExp("^", multiLine: true), " "); | |
210 } | |
211 | |
212 /// Ensure that the metatest configuration is loaded. | |
213 void _ensureInitialized() { | |
214 unittestConfiguration = _singleton; | |
215 } | |
216 | |
217 final _singleton = new _MetaConfiguration(); | |
218 | |
219 /// Special test configuration for use within the child isolates. This hides all | |
220 /// output and reports data back to the parent isolate. | |
221 class _MetaConfiguration extends Configuration { | |
222 | |
223 _MetaConfiguration() : super.blank(); | |
224 | |
225 void onSummary(int passed, int failed, int errors, List<TestCase> results, | |
226 String uncaughtError) { | |
227 _replyTo.send({ | |
228 "passed": passed, | |
229 "failed": failed, | |
230 "errors": errors, | |
231 "uncaughtError": uncaughtError, | |
232 "results": results.map((testCase) => { | |
233 "description": testCase.description, | |
234 "message": testCase.message, | |
235 "result": testCase.result, | |
236 "stackTrace": testCase.stackTrace.toString() | |
237 }).toList() | |
238 }); | |
239 } | |
240 } | |
OLD | NEW |