Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(211)

Side by Side Diff: pkg/metatest/lib/metatest.dart

Issue 524153002: Sharing metatest logic between unittest and scheduled_test (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: consolidated meta test method Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 }
OLDNEW
« no previous file with comments | « pkg/metatest/LICENSE ('k') | pkg/metatest/pubspec.yaml » ('j') | pkg/metatest/pubspec.yaml » ('J')

Powered by Google App Engine
This is Rietveld 408576698