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:isolate'; |
| 14 |
| 15 import '../../../pkg/path/lib/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 |