Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, 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 import 'src/util.dart'; |
|
nweiz
2014/09/10 21:04:42
Nit: We use "utils", not "util".
kevmoo
2014/09/17 21:16:29
Done.
| |
| 18 | |
| 19 /// Whether or not we're running in a child isolate that's supposed to run a | |
| 20 /// test. | |
| 21 bool _inChildIsolate; | |
| 22 | |
| 23 /// The port with which the child isolate should communicate with the parent | |
| 24 /// isolate. | |
| 25 /// | |
| 26 /// `null` in the parent isolate. | |
| 27 SendPort _replyTo; | |
| 28 | |
| 29 /// The only value of the configuration used in metatest. | |
| 30 final _singleton = new _MetaConfiguration(); | |
|
nweiz
2014/09/10 21:04:42
"singleton" isn't a good name for this: it only co
kevmoo
2014/09/17 21:16:28
Done.
| |
| 31 | |
| 32 /// The function holding the tests to be run. | |
| 33 Function _testBody; | |
| 34 | |
| 35 /// The description of the test to run in the child isolate. | |
| 36 /// | |
| 37 /// `null` in the parent isolate. | |
| 38 String _testToRun; | |
| 39 | |
| 40 /// Stores the optional timeout used to override the default unittest timeout. | |
| 41 Duration _timeoutOverride; | |
| 42 | |
| 43 /// Runs [setUpFn] before every metatest. | |
| 44 /// | |
| 45 /// Note that [setUpFn] will be overwritten if the test itself calls [setUp]. | |
| 46 void metaSetUp(void setUpFn()) { | |
| 47 if (_inChildIsolate) setUp(setUpFn); | |
| 48 } | |
| 49 | |
| 50 /// Runs a set of tests defined in `body` and checks the result by comparing | |
| 51 /// with values in `expectedResults`. | |
|
nweiz
2014/09/10 21:04:42
This still doesn't explain what the format of [exp
kevmoo
2014/09/17 21:16:28
Done.
| |
| 52 void expectTestResults(String description, void body(), | |
| 53 List<Map> expectedResults) { | |
| 54 _setUpTest(description, body, (resultsMap) { | |
| 55 var list = resultsMap['results'] as List; | |
|
nweiz
2014/09/10 21:04:41
This still uses "as".
kevmoo
2014/09/17 21:16:29
Done.
| |
| 56 expect(list, hasLength(expectedResults.length)); | |
|
nweiz
2014/09/10 21:04:42
These expect calls still don't have descriptions.
kevmoo
2014/09/17 21:16:28
Done.
| |
| 57 | |
| 58 for (var i = 0; i < list.length; i++) { | |
| 59 var expectedMap = expectedResults[i]; | |
| 60 var map = list[i]; | |
| 61 | |
| 62 expectedMap.forEach((key, value) { | |
| 63 expect(map, containsPair(key, value)); | |
| 64 }); | |
|
nweiz
2014/09/10 21:04:42
Can't you just do expect(map, equals(expectedMap))
kevmoo
2014/09/17 21:16:29
I want to match a subset. There may be more in 'ma
| |
| 65 } | |
| 66 }); | |
| 67 } | |
| 68 | |
| 69 /// Runs a test defined in `body` and validates that `result` and `message` | |
| 70 /// match the values generated by the finished test. | |
|
nweiz
2014/09/10 21:04:42
What are the semantics of "result" and "message"?
kevmoo
2014/09/17 21:16:29
Done.
| |
| 71 void expectSingleTest(String description, String result, String message, | |
| 72 void body()) { | |
| 73 _setUpTest(description, body, (results) { | |
| 74 var testResults = results['results']; | |
| 75 expect(testResults, hasLength(1), reason: 'Only one test should be run.'); | |
| 76 | |
| 77 var testResult = testResults.single; | |
| 78 expect(testResult['result'], result, | |
| 79 reason: 'The test result did not meet expectations.'); | |
|
nweiz
2014/09/10 21:04:42
"Unexpected test result."
kevmoo
2014/09/17 21:16:29
Done.
| |
| 80 expect(testResult['message'], message, | |
| 81 reason: 'The test message did not meet expectations.'); | |
|
nweiz
2014/09/10 21:04:42
"Unexpected test message."
kevmoo
2014/09/17 21:16:29
Done.
| |
| 82 }); | |
| 83 } | |
| 21 | 84 |
| 22 /// Declares a test with the given [description] and [body]. [body] corresponds | 85 /// 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 | 86 /// 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 | 87 /// default, this expects that all tests defined in [body] pass, but if |
| 25 /// [passing] is passed, only tests listed there are expected to pass. | 88 /// [passing] is passed, only tests listed there are expected to pass. |
| 26 void expectTestsPass(String description, void body(), {List<String> passing}) { | 89 void expectTestsPass(String description, void body(), {List<String> passing}) { |
| 27 _setUpTest(description, body, (results) { | 90 _setUpTest(description, body, (results) { |
| 28 if (_hasError(results)) { | 91 if (_hasError(results)) { |
| 29 throw 'Expected all tests to pass, but got error(s):\n' | 92 fail('Expected all tests to pass, but got error(s):\n' |
| 30 '${_summarizeTests(results)}'; | 93 '${_summarizeTests(results)}'); |
| 31 } else if (passing == null) { | 94 } else if (passing == null) { |
| 32 if (results['failed'] != 0) { | 95 if (results['failed'] != 0) { |
| 33 throw 'Expected all tests to pass, but some failed:\n' | 96 fail('Expected all tests to pass, but some failed:\n' |
| 34 '${_summarizeTests(results)}'; | 97 '${_summarizeTests(results)}'); |
| 35 } | 98 } |
| 36 } else { | 99 } else { |
| 37 var shouldPass = new Set.from(passing); | 100 var shouldPass = new Set.from(passing); |
| 38 var didPass = new Set.from(results['results'] | 101 var didPass = new Set.from(results['results'] |
| 39 .where((t) => t['result'] == 'pass') | 102 .where((t) => t['result'] == 'pass') |
| 40 .map((t) => t['description'])); | 103 .map((t) => t['description'])); |
| 41 | 104 |
| 42 if (!shouldPass.containsAll(didPass) || | 105 if (!shouldPass.containsAll(didPass) || |
| 43 !didPass.containsAll(shouldPass)) { | 106 !didPass.containsAll(shouldPass)) { |
| 44 String stringify(Set<String> tests) => | 107 String stringify(Set<String> tests) => |
| 45 '{${tests.map((t) => '"$t"').join(', ')}}'; | 108 '{${tests.map((t) => '"$t"').join(', ')}}'; |
| 46 | 109 |
| 47 fail('Expected exactly ${stringify(shouldPass)} to pass, but ' | 110 fail('Expected exactly ${stringify(shouldPass)} to pass, but ' |
| 48 '${stringify(didPass)} passed.\n' | 111 '${stringify(didPass)} passed.\n' '${_summarizeTests(results)}'); |
|
nweiz
2014/09/10 21:04:41
There's still gratuitous reformatting here.
kevmoo
2014/09/17 21:16:29
Done, I think...
| |
| 49 '${_summarizeTests(results)}'); | |
| 50 } | 112 } |
| 51 } | 113 } |
| 52 }); | 114 }); |
| 53 } | 115 } |
| 54 | 116 |
| 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 | 117 /// 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 | 118 /// to the `main` method of a test file, and will be run in an isolate. Expects |
| 89 /// all tests defined by [body] to fail. | 119 /// all tests defined by [body] to fail. |
| 90 void expectTestsFail(String description, void body()) { | 120 void expectTestsFail(String description, void body()) { |
| 91 _setUpTest(description, body, (results) { | 121 _setUpTest(description, body, (results) { |
| 92 if (_hasError(results)) { | 122 if (_hasError(results)) { |
| 93 throw 'Expected all tests to fail, but got error(s):\n' | 123 throw 'Expected all tests to fail, but got error(s):\n' |
| 94 '${_summarizeTests(results)}'; | 124 '${_summarizeTests(results)}'; |
| 95 } else if (results['passed'] != 0) { | 125 } else if (results['passed'] != 0) { |
| 96 throw 'Expected all tests to fail, but some passed:\n' | 126 throw 'Expected all tests to fail, but some passed:\n' |
| 97 '${_summarizeTests(results)}'; | 127 '${_summarizeTests(results)}'; |
| 98 } | 128 } |
| 99 }); | 129 }); |
| 100 } | 130 } |
| 101 | 131 |
| 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, | 132 /// Sets up a test with the given [description] and [body]. After the test runs, |
| 109 /// calls [validate] with the result map. | 133 /// calls [validate] with the result map. |
| 110 void _setUpTest(String description, void body(), void validate(Map)) { | 134 void _setUpTest(String description, void body(), void validate(Map map)) { |
| 111 if (_inChildIsolate) { | 135 if (_inChildIsolate) { |
| 112 _ensureInitialized(); | 136 _ensureInitialized(); |
| 113 if (_testToRun == description) body(); | 137 if (_testToRun == description) body(); |
| 114 } else { | 138 } else { |
| 115 test(description, () { | 139 test(description, () { |
| 116 expect(_runInIsolate(description).then(validate), completes); | 140 return _runInIsolate(description).then(validate); |
| 117 }); | 141 }); |
| 118 } | 142 } |
| 119 } | 143 } |
| 120 | 144 |
| 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. | 145 /// Initialize metatest. |
| 137 /// | 146 /// |
| 138 /// [message] should be the second argument to [main]. It's used to determine | 147 /// [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. | 148 /// whether this test is in the parent isolate or a child isolate. |
| 140 void initMetatest(message) { | 149 /// |
| 150 /// [timeout], when specified, overrides the default timeout for unittest. | |
| 151 void initMetatest(message, {Duration timeout}) { | |
| 152 _timeoutOverride = timeout; | |
| 141 if (message == null) { | 153 if (message == null) { |
| 142 _inChildIsolate = false; | 154 _inChildIsolate = false; |
| 143 } else { | 155 } else { |
| 144 _testToRun = message['testToRun']; | 156 _testToRun = message['testToRun']; |
| 145 _replyTo = message['replyTo']; | 157 _replyTo = message['replyTo']; |
| 146 _inChildIsolate = true; | 158 _inChildIsolate = true; |
| 147 } | 159 } |
| 148 } | 160 } |
| 149 | 161 |
| 150 /// Runs the test described by [description] in its own isolate. Returns a map | 162 // TODO(kevmoo) We need to capture the main method to allow running in an |
| 151 /// describing the results of that test run. | 163 // isolate. There is no mechanism to capture the current executing URI between |
| 164 // browser and vm. Issue 1145 and/or Issue 8440 | |
| 165 void initTests(void testBody(message)) { | |
| 166 _testBody = testBody; | |
| 167 _testBody(null); | |
| 168 } | |
| 169 | |
| 170 /// Runs the test described by [description] in its own isolate. | |
| 171 /// | |
| 172 /// Returns a map describing the results of that test run. | |
| 152 Future<Map> _runInIsolate(String description) { | 173 Future<Map> _runInIsolate(String description) { |
| 174 if (_testBody == null) { | |
| 175 throw new StateError('initTests was not called.'); | |
| 176 } | |
| 177 | |
| 153 var replyPort = new ReceivePort(); | 178 var replyPort = new ReceivePort(); |
| 154 // TODO(nweiz): Don't use path here once issue 8440 is fixed. | 179 return Isolate.spawn(_testBody, { |
| 155 var uri = path.toUri(path.absolute(path.fromUri(Platform.script))); | |
| 156 return Isolate.spawnUri(uri, [], { | |
| 157 'testToRun': description, | 180 'testToRun': description, |
| 158 'replyTo': replyPort.sendPort | 181 'replyTo': replyPort.sendPort |
| 159 }).then((_) { | 182 }).then((_) => replyPort.first); |
| 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 } | 183 } |
| 167 | 184 |
| 168 /// Returns whether [results] (a test result map) describes a test run in which | 185 /// Returns whether [results] (a test result map) describes a test run in which |
| 169 /// an error occurred. | 186 /// an error occurred. |
| 170 bool _hasError(Map results) { | 187 bool _hasError(Map results) { |
| 171 return results['errors'] > 0 || results['uncaughtError'] != null || | 188 return results['errors'] > 0 || results['uncaughtError'] != null || |
| 172 (results['passed'] == 0 && results['failed'] == 0); | 189 (results['passed'] == 0 && results['failed'] == 0); |
| 173 } | 190 } |
| 174 | 191 |
| 175 /// Returns a string description of the test run descibed by [results]. | 192 /// Returns a string description of the test run descibed by [results]. |
| 176 String _summarizeTests(Map results) { | 193 String _summarizeTests(Map results) { |
| 177 var buffer = new StringBuffer(); | 194 var buffer = new StringBuffer(); |
| 178 for (var t in results["results"]) { | 195 for (var t in results["results"]) { |
| 179 buffer.writeln("${t['result'].toUpperCase()}: ${t['description']}"); | 196 buffer.writeln("${t['result'].toUpperCase()}: ${t['description']}"); |
| 180 if (t['message'] != '') buffer.writeln("${_indent(t['message'])}"); | 197 if (t['message'] != '') buffer.writeln("${_indent(t['message'])}"); |
| 181 if (t['stackTrace'] != null && t['stackTrace'] != '') { | 198 if (t['stackTrace'] != null && t['stackTrace'] != '') { |
| 182 buffer.writeln("${_indent(t['stackTrace'])}"); | 199 buffer.writeln("${_indent(t['stackTrace'])}"); |
| 183 } | 200 } |
| 184 } | 201 } |
| 185 | 202 |
| 186 buffer.writeln(); | 203 buffer.writeln(); |
| 187 | 204 |
| 188 var success = false; | 205 var success = false; |
| 189 if (results['passed'] == 0 && results['failed'] == 0 && | 206 if (results['passed'] == 0 && |
| 190 results['errors'] == 0 && results['uncaughtError'] == null) { | 207 results['failed'] == 0 && |
| 208 results['errors'] == 0 && | |
| 209 results['uncaughtError'] == null) { | |
| 191 buffer.write('No tests found.'); | 210 buffer.write('No tests found.'); |
| 192 // This is considered a failure too. | 211 // This is considered a failure too. |
| 193 } else if (results['failed'] == 0 && results['errors'] == 0 && | 212 } else if (results['failed'] == 0 && |
| 213 results['errors'] == 0 && | |
| 194 results['uncaughtError'] == null) { | 214 results['uncaughtError'] == null) { |
| 195 buffer.write('All ${results['passed']} tests passed.'); | 215 buffer.write('All ${results['passed']} tests passed.'); |
| 196 success = true; | 216 success = true; |
| 197 } else { | 217 } else { |
| 198 if (results['uncaughtError'] != null) { | 218 if (results['uncaughtError'] != null) { |
| 199 buffer.write('Top-level uncaught error: ${results['uncaughtError']}'); | 219 buffer.write('Top-level uncaught error: ${results['uncaughtError']}'); |
| 200 } | 220 } |
| 201 buffer.write('${results['passed']} PASSED, ${results['failed']} FAILED, ' | 221 buffer.write( |
| 202 '${results['errors']} ERRORS'); | 222 '${results['passed']} PASSED, ${results['failed']} FAILED, ' |
| 223 '${results['errors']} ERRORS'); | |
|
nweiz
2014/09/10 21:04:42
There's still reformatting all through this method
kevmoo
2014/09/17 21:16:28
Done.
| |
| 203 } | 224 } |
| 204 return prefixLines(buffer.toString()); | 225 return prefixLines(buffer.toString()); |
| 205 } | 226 } |
| 206 | 227 |
| 207 /// Indents each line of [str] by two spaces. | 228 /// Indents each line of [str] by two spaces. |
| 208 String _indent(String str) { | 229 String _indent(String str) { |
| 209 return str.replaceAll(new RegExp("^", multiLine: true), " "); | 230 return str.replaceAll(new RegExp("^", multiLine: true), " "); |
| 210 } | 231 } |
| 211 | 232 |
| 212 /// Ensure that the metatest configuration is loaded. | 233 /// Ensure that the metatest configuration is loaded. |
| 213 void _ensureInitialized() { | 234 void _ensureInitialized() { |
| 214 unittestConfiguration = _singleton; | 235 unittestConfiguration = _singleton; |
| 236 if (_timeoutOverride != null) { | |
| 237 unittestConfiguration.timeout = _timeoutOverride; | |
| 238 } | |
| 215 } | 239 } |
| 216 | 240 |
| 217 final _singleton = new _MetaConfiguration(); | |
| 218 | |
| 219 /// Special test configuration for use within the child isolates. This hides all | 241 /// Special test configuration for use within the child isolates. This hides all |
| 220 /// output and reports data back to the parent isolate. | 242 /// output and reports data back to the parent isolate. |
| 221 class _MetaConfiguration extends Configuration { | 243 class _MetaConfiguration extends Configuration { |
| 222 | 244 |
| 223 _MetaConfiguration() : super.blank(); | 245 _MetaConfiguration() : super.blank(); |
| 224 | 246 |
| 225 void onSummary(int passed, int failed, int errors, List<TestCase> results, | 247 void onSummary(int passed, int failed, int errors, List<TestCase> results, |
| 226 String uncaughtError) { | 248 String uncaughtError) { |
| 227 _replyTo.send({ | 249 _replyTo.send({ |
| 228 "passed": passed, | 250 "passed": passed, |
| 229 "failed": failed, | 251 "failed": failed, |
| 230 "errors": errors, | 252 "errors": errors, |
| 231 "uncaughtError": uncaughtError, | 253 "uncaughtError": uncaughtError, |
| 232 "results": results.map((testCase) => { | 254 "results": results.map((testCase) => { |
| 233 "description": testCase.description, | 255 "description": testCase.description, |
| 234 "message": testCase.message, | 256 "message": testCase.message, |
| 235 "result": testCase.result, | 257 "result": testCase.result, |
| 236 "stackTrace": testCase.stackTrace.toString() | 258 "stackTrace": testCase.stackTrace.toString() |
| 237 }).toList() | 259 }).toList() |
| 238 }); | 260 }); |
| 239 } | 261 } |
| 240 } | 262 } |
| OLD | NEW |