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 have to go deeper. | |
Bob Nystrom
2013/02/08 16:15:37
"have to" -> "must"
nweiz
2013/02/08 22:14:38
Done.
| |
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:isolate'; | |
14 | |
15 import 'package:pathos/path.dart' as path; | |
16 import 'package:scheduled_test/src/utils.dart'; | |
17 import 'package:unittest/unittest.dart'; | |
18 | |
19 /// Declares a test with the given [description] and [body]. [body] corresponds | |
20 /// to the `main` method of a test file, and will be run in an isolate. By | |
21 /// default, this expects that all tests defined in [body] pass, but if | |
22 /// [passing] is passed, only tests listed there are expected to pass. | |
23 void expectTestsPass(String description, void body(), {List<String> passing}) { | |
24 _setUpTest(description, body, (results) { | |
25 if (_hasError(results)) { | |
26 throw 'Expected all tests to pass, but got error(s):\n' | |
27 '${_summarizeTests(results)}'; | |
28 } else if (passing == null) { | |
29 if (results['failed'] != 0) { | |
30 throw 'Expected all tests to pass, but some failed:\n' | |
31 '${_summarizeTests(results)}'; | |
32 } | |
33 } else { | |
34 var shouldPass = new Set.from(passing); | |
35 var didPass = new Set.from(results['results'] | |
36 .where((t) => t['result'] == 'pass') | |
37 .map((t) => t['description'])); | |
38 | |
39 if (!shouldPass.containsAll(didPass) || | |
40 !didPass.containsAll(shouldPass)) { | |
41 String stringify(Set<String> tests) => | |
42 '{${tests.map((t) => '"$t"').join(', ')}}'; | |
43 | |
44 fail('Expected exactly ${stringify(shouldPass)} to pass, but ' | |
45 '${stringify(didPass)} passed.\n' | |
46 '${_summarizeTests(results)}'); | |
47 } | |
48 } | |
49 }); | |
50 } | |
51 | |
52 /// Declares a test with the given [description] and [body]. [body] corresponds | |
53 /// to the `main` method of a test file, and will be run in an isolate. Expects | |
54 /// all tests defined by [body] to fail. | |
55 void expectTestsFail(String description, void body()) { | |
56 _setUpTest(description, body, (results) { | |
57 if (_hasError(results)) { | |
58 throw 'Expected all tests to fail, but got error(s):\n' | |
59 '${_summarizeTests(results)}'; | |
60 } else if (results['passed'] != 0) { | |
61 throw 'Expected all tests to fail, but some passed:\n' | |
62 '${_summarizeTests(results)}'; | |
63 } | |
64 }); | |
65 } | |
66 | |
67 /// Sets up a test with the given [description] and [body]. After the test runs, | |
68 /// calls [validate] with the result map. | |
69 void _setUpTest(String description, void body(), void validate(Map)) { | |
70 _inChildIsolate.then((inIsolate) { | |
71 if (inIsolate) { | |
72 _ensureInitialized(); | |
73 if (_testToRun == description) body(); | |
74 } else { | |
75 test(description, () { | |
76 expect(_runInIsolate(description).then(validate), completes); | |
77 }); | |
78 } | |
79 }); | |
80 } | |
81 | |
82 /// The description of the test to run in the child isolate. `null` in the | |
83 /// parent isolate. Not set until [_inChildIsolate] completes. | |
84 String _testToRun; | |
85 | |
86 /// The port with which the child isolate should communicate with the parent | |
87 /// isolate. `null` in the parent isolate. Not set until [_inChildIsolate] | |
88 /// completes. | |
89 SendPort _replyTo; | |
90 | |
91 /// The cached [Future] for [_inChildIsolate]. | |
92 Future<bool> _inChildIsolateFuture; | |
93 | |
94 /// Returns whether or not we're running in a child isolate that's supposed to | |
95 /// run a test. | |
96 Future<bool> get _inChildIsolate { | |
97 if (_inChildIsolateFuture != null) return _inChildIsolateFuture; | |
98 | |
99 var completer = new Completer(); | |
100 port.receive((message, replyTo) { | |
101 _testToRun = message; | |
102 _replyTo = replyTo; | |
103 port.close(); | |
104 completer.complete(true); | |
105 }); | |
106 | |
107 // TODO(nweiz): don't use a timeout here once issue 8416 is fixed. | |
108 _inChildIsolateFuture = timeout(completer.future, 500, () { | |
109 port.close(); | |
110 return false; | |
111 }); | |
112 return _inChildIsolateFuture; | |
113 } | |
114 | |
115 /// Runs the test described by [description] in its own isolate. Returns a map | |
116 /// describing the results of that test run. | |
117 Future<Map> _runInIsolate(String description) { | |
118 var future = spawnUri(path.join(path.current, new Options().script)) | |
119 .call(description); | |
120 // TODO(nweiz): Remove this timeout once issue 8417 is fixed and we can | |
121 // capture top-level exceptions. | |
122 return timeout(future, 30 * 1000, () { | |
123 throw 'Timed out waiting for test to complete.'; | |
124 }); | |
125 } | |
126 | |
127 /// Returns whether [results] (a test result map) describes a test run in which | |
128 /// an error occurred. | |
129 bool _hasError(Map results) { | |
130 return results['errors'] > 0 || results['uncaughtError'] != null || | |
131 (results['passed'] == 0 && results['failed'] == 0); | |
132 } | |
133 | |
134 /// Returns a string description of the test run descibed by [results]. | |
135 String _summarizeTests(Map results) { | |
136 var buffer = new StringBuffer(); | |
137 for (var t in results["results"]) { | |
138 buffer.add("${t['result'].toUpperCase()}: ${t['description']}\n"); | |
139 if (t['message'] != '') buffer.add("${_indent(t['message'])}\n"); | |
140 if (t['stackTrace'] != null && t['stackTrace'] != '') { | |
141 buffer.add("${_indent(t['stackTrace'])}\n"); | |
142 } | |
143 } | |
144 | |
145 buffer.add("\n"); | |
146 | |
147 var success = false; | |
148 if (results['passed'] == 0 && results['failed'] == 0 && | |
149 results['errors'] == 0 && results['uncaughtError'] == null) { | |
150 buffer.add('No tests found.'); | |
151 // This is considered a failure too. | |
152 } else if (results['failed'] == 0 && results['errors'] == 0 && | |
153 results['uncaughtError'] == null) { | |
154 buffer.add('All ${results['passed']} tests passed.'); | |
155 success = true; | |
156 } else { | |
157 if (results['uncaughtError'] != null) { | |
158 buffer.add('Top-level uncaught error: ${results['uncaughtError']}'); | |
159 } | |
160 buffer.add('${results['passed']} PASSED, ${results['failed']} FAILED, ' | |
161 '${results['errors']} ERRORS'); | |
162 } | |
163 return prefixLines(buffer.toString()); | |
164 } | |
165 | |
166 /// Indents each line of [str] by two spaces. | |
167 String _indent(String str) { | |
168 // TODO(nweiz): Use this simpler code once issue 2980 is fixed. | |
169 // return str.replaceAll(new RegExp("^", multiLine: true), " "); | |
170 | |
171 return Strings.join(str.split("\n").map((line) => " $line"), "\n"); | |
172 } | |
173 | |
174 /// Ensure that the metatest configuration is loaded. | |
175 void _ensureInitialized() { | |
176 if (config is! _MetaConfiguration) configure(new _MetaConfiguration()); | |
177 } | |
178 | |
179 /// Special test configuration for use within the child isolates. This hides all | |
180 /// output and reports data back to the parent isolate. | |
181 class _MetaConfiguration extends Configuration { | |
182 final name = "MetaConfiguration"; | |
183 | |
184 void logTestCaseMesssage(TestCase testCase, String message) {} | |
185 | |
186 void onSummary(int passed, int failed, int errors, List<TestCase> results, | |
187 String uncaughtError) { | |
188 _replyTo.send({ | |
189 "passed": passed, | |
190 "failed": failed, | |
191 "errors": errors, | |
192 "uncaughtError": uncaughtError, | |
193 "results": results.map((testCase) => { | |
194 "description": testCase.description, | |
195 "message": testCase.message, | |
196 "result": testCase.result, | |
197 "stackTrace": testCase.stackTrace | |
198 }).toList() | |
199 }); | |
200 } | |
201 | |
202 void onInit() {} | |
203 void onDone(bool success) {} | |
204 } | |
OLD | NEW |