OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, 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 library dev_compiler.src.testing; | 5 library dev_compiler.src.testing; |
6 | 6 |
| 7 import 'dart:collection' show Queue; |
7 import 'package:analyzer/file_system/file_system.dart'; | 8 import 'package:analyzer/file_system/file_system.dart'; |
8 import 'package:analyzer/file_system/memory_file_system.dart'; | 9 import 'package:analyzer/file_system/memory_file_system.dart'; |
9 import 'package:analyzer/src/generated/ast.dart'; | 10 import 'package:analyzer/src/generated/ast.dart'; |
10 import 'package:analyzer/src/generated/engine.dart' show AnalysisContext; | 11 import 'package:analyzer/src/generated/engine.dart' |
| 12 show AnalysisContext, AnalysisEngine; |
| 13 import 'package:analyzer/src/generated/error.dart'; |
11 import 'package:logging/logging.dart'; | 14 import 'package:logging/logging.dart'; |
12 import 'package:source_span/source_span.dart'; | 15 import 'package:source_span/source_span.dart'; |
13 import 'package:test/test.dart'; | 16 import 'package:test/test.dart'; |
14 | 17 |
15 import 'package:dev_compiler/config.dart'; | 18 import 'package:dev_compiler/strong_mode.dart'; |
16 import 'package:dev_compiler/devc.dart' show Compiler; | |
17 | 19 |
18 import 'analysis_context.dart'; | 20 import 'analysis_context.dart'; |
19 import 'dependency_graph.dart' show runtimeFilesForServerMode; | 21 import 'dependency_graph.dart' show runtimeFilesForServerMode; |
20 import 'info.dart'; | 22 import 'info.dart'; |
21 import 'options.dart'; | 23 import 'options.dart'; |
22 import 'report.dart'; | |
23 import 'utils.dart'; | 24 import 'utils.dart'; |
24 | 25 |
25 /// Run the checker on a program with files contents as indicated in | 26 /// Run the checker on a program with files contents as indicated in |
26 /// [testFiles]. | 27 /// [testFiles]. |
27 /// | 28 /// |
28 /// This function makes several assumptions to make it easier to describe error | 29 /// This function makes several assumptions to make it easier to describe error |
29 /// expectations: | 30 /// expectations: |
30 /// | 31 /// |
31 /// * a file named `/main.dart` exists in [testFiles]. | 32 /// * a file named `/main.dart` exists in [testFiles]. |
32 /// * all expected failures are listed in the source code using comments | 33 /// * all expected failures are listed in the source code using comments |
33 /// immediately in front of the AST node that should contain the error. | 34 /// immediately in front of the AST node that should contain the error. |
34 /// * errors are formatted as a token `level:Type`, where `level` is the | 35 /// * errors are formatted as a token `level:Type`, where `level` is the |
35 /// logging level were the error would be reported at, and `Type` is the | 36 /// logging level were the error would be reported at, and `Type` is the |
36 /// concrete subclass of [StaticInfo] that denotes the error. | 37 /// concrete subclass of [StaticInfo] that denotes the error. |
37 /// | 38 /// |
38 /// For example, to check that an assignment produces a warning about a boxing | 39 /// For example, to check that an assignment produces a warning about a boxing |
39 /// conversion, you can describe the test as follows: | 40 /// conversion, you can describe the test as follows: |
40 /// | 41 /// |
41 /// testChecker({ | 42 /// testChecker({ |
42 /// '/main.dart': ''' | 43 /// '/main.dart': ''' |
43 /// testMethod() { | 44 /// testMethod() { |
44 /// dynamic x = /*warning:Box*/3; | 45 /// dynamic x = /*warning:Box*/3; |
45 /// } | 46 /// } |
46 /// ''' | 47 /// ''' |
47 /// }); | 48 /// }); |
48 /// | 49 /// |
49 CheckerResults testChecker(Map<String, String> testFiles, {String sdkDir, | 50 void testChecker(Map<String, String> testFiles, {String sdkDir, |
50 customUrlMappings: const {}, | 51 customUrlMappings: const {}, relaxedCasts: true, |
51 CheckerReporter createReporter(AnalysisContext context), relaxedCasts: true, | 52 inferDownwards: StrongModeOptions.inferDownwardsDefault, |
52 inferDownwards: RulesOptions.inferDownwardsDefault, | 53 inferFromOverrides: StrongModeOptions.inferFromOverridesDefault, |
53 inferFromOverrides: ResolverOptions.inferFromOverridesDefault, | 54 inferTransitively: StrongModeOptions.inferTransitivelyDefault, |
54 inferTransitively: ResolverOptions.inferTransitivelyDefault, | 55 nonnullableTypes: StrongModeOptions.NONNULLABLE_TYPES}) { |
55 nonnullableTypes: TypeOptions.NONNULLABLE_TYPES}) { | |
56 expect(testFiles.containsKey('/main.dart'), isTrue, | 56 expect(testFiles.containsKey('/main.dart'), isTrue, |
57 reason: '`/main.dart` is missing in testFiles'); | 57 reason: '`/main.dart` is missing in testFiles'); |
58 | 58 |
59 var provider = createTestResourceProvider(testFiles); | 59 var provider = createTestResourceProvider(testFiles); |
60 var uriResolver = new TestUriResolver(provider); | 60 var uriResolver = new TestUriResolver(provider); |
| 61 var context = AnalysisEngine.instance.createAnalysisContext(); |
| 62 context.sourceFactory = createSourceFactory(new SourceResolverOptions( |
| 63 customUrlMappings: customUrlMappings, |
| 64 useMockSdk: sdkDir == null, |
| 65 dartSdkPath: sdkDir, |
| 66 entryPointFile: '/main.dart'), fileResolvers: [uriResolver]); |
61 | 67 |
62 var options = new CompilerOptions( | 68 var checker = new StrongChecker(context, new StrongModeOptions( |
63 relaxedCasts: relaxedCasts, | 69 relaxedCasts: relaxedCasts, |
64 inferDownwards: inferDownwards, | 70 inferDownwards: inferDownwards, |
65 inferFromOverrides: inferFromOverrides, | 71 inferFromOverrides: inferFromOverrides, |
66 inferTransitively: inferTransitively, | 72 inferTransitively: inferTransitively, |
67 nonnullableTypes: nonnullableTypes, | 73 nonnullableTypes: nonnullableTypes, |
68 useMockSdk: sdkDir == null, | 74 hints: true)); |
69 dartSdkPath: sdkDir, | |
70 runtimeDir: '/dev_compiler_runtime/', | |
71 entryPointFile: '/main.dart', | |
72 customUrlMappings: customUrlMappings); | |
73 | |
74 var context = createAnalysisContext(options, fileResolvers: [uriResolver]); | |
75 | 75 |
76 // Run the checker on /main.dart. | 76 // Run the checker on /main.dart. |
77 var mainFile = new Uri.file('/main.dart'); | 77 var mainSource = uriResolver.resolveAbsolute(new Uri.file('/main.dart')); |
78 TestReporter testReporter; | 78 var initialLibrary = context.resolveCompilationUnit2(mainSource, mainSource); |
79 CheckerReporter reporter; | |
80 if (createReporter == null) { | |
81 reporter = testReporter = new TestReporter(context); | |
82 } else { | |
83 reporter = createReporter(context); | |
84 } | |
85 var results = | |
86 new Compiler(options, context: context, reporter: reporter).run(); | |
87 | 79 |
88 // Extract expectations from the comments in the test files. | 80 // Extract expectations from the comments in the test files, and |
89 var expectedErrors = <AstNode, List<_ErrorExpectation>>{}; | 81 // check that all errors we emit are included in the expected map. |
90 var visitor = new _ErrorMarkerVisitor(expectedErrors); | 82 var allLibraries = reachableLibraries(initialLibrary.element.library); |
91 var initialLibrary = | 83 for (var lib in allLibraries) { |
92 context.getLibraryElement(uriResolver.resolveAbsolute(mainFile)); | |
93 for (var lib in reachableLibraries(initialLibrary)) { | |
94 for (var unit in lib.units) { | 84 for (var unit in lib.units) { |
95 unit.unit.accept(visitor); | 85 if (unit.source.uri.scheme == 'dart') continue; |
| 86 |
| 87 var errorInfo = checker.computeErrors(unit.source); |
| 88 new _ExpectedErrorVisitor(errorInfo.errors).validate(unit.unit); |
96 } | 89 } |
97 } | 90 } |
98 | |
99 if (testReporter == null) return results; | |
100 | |
101 var total = expectedErrors.values.fold(0, (p, l) => p + l.length); | |
102 // Check that all errors we emit are included in the expected map. | |
103 for (var lib in results.libraries) { | |
104 var uri = lib.library.source.uri; | |
105 testReporter.infoMap[uri].forEach((node, actual) { | |
106 var expected = expectedErrors[node]; | |
107 var expectedTotal = expected == null ? 0 : expected.length; | |
108 if (actual.length != expectedTotal) { | |
109 expect(actual.length, expectedTotal, | |
110 reason: 'The checker found ${actual.length} errors on the ' | |
111 'expression `$node`, but we expected $expectedTotal. These are the ' | |
112 'errors the checker found:\n\n ${_unexpectedErrors(node, actual)}'); | |
113 } | |
114 | |
115 for (int i = 0; i < expected.length; i++) { | |
116 expect(actual[i].level, expected[i].level, | |
117 reason: 'expected different logging level at:\n\n' | |
118 '${_messageWithSpan(actual[i])}'); | |
119 expect(actual[i].runtimeType, expected[i].type, | |
120 reason: 'expected different error type at:\n\n' | |
121 '${_messageWithSpan(actual[i])}'); | |
122 } | |
123 expectedErrors.remove(node); | |
124 }); | |
125 } | |
126 | |
127 // Check that all expected errors are accounted for. | |
128 if (!expectedErrors.isEmpty) { | |
129 var newTotal = expectedErrors.values.fold(0, (p, l) => p + l.length); | |
130 // Non empty error expectation lists remaining | |
131 if (newTotal > 0) { | |
132 fail('Not all expected errors were reported by the checker. Only' | |
133 ' ${total - newTotal} out of $total expected errors were reported.\n' | |
134 'The following errors were not reported:\n' | |
135 '${_unreportedErrors(expectedErrors)}'); | |
136 } | |
137 } | |
138 | |
139 return results; | |
140 } | 91 } |
141 | 92 |
142 /// Creates a [MemoryResourceProvider] with test data | 93 /// Creates a [MemoryResourceProvider] with test data |
143 MemoryResourceProvider createTestResourceProvider( | 94 MemoryResourceProvider createTestResourceProvider( |
144 Map<String, String> testFiles) { | 95 Map<String, String> testFiles) { |
145 var provider = new MemoryResourceProvider(); | 96 var provider = new MemoryResourceProvider(); |
146 runtimeFilesForServerMode.forEach((filepath) { | 97 runtimeFilesForServerMode.forEach((filepath) { |
147 testFiles['/dev_compiler_runtime/$filepath'] = | 98 testFiles['/dev_compiler_runtime/$filepath'] = |
148 '/* test contents of $filepath */'; | 99 '/* test contents of $filepath */'; |
149 }); | 100 }); |
(...skipping 14 matching lines...) Expand all Loading... |
164 super(provider); | 115 super(provider); |
165 resolveAbsolute(Uri uri) { | 116 resolveAbsolute(Uri uri) { |
166 if (uri.scheme == 'package') { | 117 if (uri.scheme == 'package') { |
167 return (provider.getResource('/packages/' + uri.path) as File) | 118 return (provider.getResource('/packages/' + uri.path) as File) |
168 .createSource(uri); | 119 .createSource(uri); |
169 } | 120 } |
170 return super.resolveAbsolute(uri); | 121 return super.resolveAbsolute(uri); |
171 } | 122 } |
172 } | 123 } |
173 | 124 |
174 class TestReporter extends SummaryReporter { | 125 class _ExpectedErrorVisitor extends UnifyingAstVisitor { |
175 Map<Uri, Map<AstNode, List<StaticInfo>>> infoMap = {}; | 126 final Set<AnalysisError> _actualErrors; |
176 Map<AstNode, List<StaticInfo>> _current; | 127 CompilationUnit _unit; |
| 128 String _unitSourceCode; |
177 | 129 |
178 TestReporter(AnalysisContext context) : super(context); | 130 _ExpectedErrorVisitor(List<AnalysisError> actualErrors) |
| 131 : _actualErrors = new Set.from(actualErrors); |
179 | 132 |
180 void enterLibrary(Uri uri) { | 133 validate(CompilationUnit unit) { |
181 super.enterLibrary(uri); | 134 _unit = unit; |
182 infoMap[uri] = _current = {}; | 135 // This reads the file. Only safe because tests use MemoryFileSystem. |
183 } | 136 _unitSourceCode = unit.element.source.contents.data; |
184 | 137 |
185 void log(Message info) { | 138 // Visit the compilation unit. |
186 super.log(info); | 139 unit.accept(this); |
187 if (info is StaticInfo) { | 140 |
188 _current.putIfAbsent(info.node, () => []).add(info); | 141 if (_actualErrors.isNotEmpty) { |
| 142 var actualMsgs = _actualErrors.map(_formatActualError).join('\n'); |
| 143 fail('Unexpected errors reported by checker:\n\n$actualMsgs'); |
189 } | 144 } |
190 } | 145 } |
191 } | |
192 | |
193 /// Create an error explanation for errors that were not expected, but that the | |
194 /// checker produced. | |
195 String _unexpectedErrors(AstNode node, List errors) { | |
196 final span = _spanFor(node); | |
197 return errors.map((e) { | |
198 var level = e.level.name.toLowerCase(); | |
199 return '$level: ${span.message(e.message, color: colorOf(level))}'; | |
200 }).join('\n'); | |
201 } | |
202 | |
203 String _unreportedErrors(Map<AstNode, List<_ErrorExpectation>> expected) { | |
204 var sb = new StringBuffer(); | |
205 for (var node in expected.keys) { | |
206 var span = _spanFor(node); | |
207 expected[node].forEach((e) { | |
208 var level = e.level.name.toLowerCase(); | |
209 sb.write('$level: ${span.message("${e.type}", color: colorOf(level))}\n'); | |
210 }); | |
211 } | |
212 return sb.toString(); | |
213 } | |
214 | |
215 String _messageWithSpan(StaticInfo info) { | |
216 var span = _spanFor(info.node); | |
217 var level = info.level.name.toLowerCase(); | |
218 return '$level: ${span.message(info.message, color: colorOf(level))}'; | |
219 } | |
220 | |
221 SourceSpan _spanFor(AstNode node) { | |
222 var unit = node.root as CompilationUnit; | |
223 var source = unit.element.source; | |
224 // This reads the file. Only safe in tests, because they use MemoryFileSystem. | |
225 var content = source.contents.data; | |
226 return createSpanHelper(unit, node.offset, node.end, source, content); | |
227 } | |
228 | |
229 /// Visitor that extracts expected errors from comments. | |
230 class _ErrorMarkerVisitor extends UnifyingAstVisitor { | |
231 Map<AstNode, List<_ErrorExpectation>> expectedErrors; | |
232 | |
233 _ErrorMarkerVisitor(this.expectedErrors); | |
234 | 146 |
235 visitNode(AstNode node) { | 147 visitNode(AstNode node) { |
236 var token = node.beginToken; | 148 var token = node.beginToken; |
237 var comment = token.precedingComments; | 149 var comment = token.precedingComments; |
238 // Use error marker found in an immediately preceding comment, | 150 // Use error marker found in an immediately preceding comment, |
239 // and attach it to the outermost expression that starts at that token. | 151 // and attach it to the outermost expression that starts at that token. |
240 if (comment != null) { | 152 if (comment != null) { |
241 while (comment.next != null) { | 153 while (comment.next != null) { |
242 comment = comment.next; | 154 comment = comment.next; |
243 } | 155 } |
244 if (comment.end == token.offset && node.parent.beginToken != token) { | 156 if (comment.end == token.offset && node.parent.beginToken != token) { |
245 var commentText = '$comment'; | 157 var commentText = '$comment'; |
246 var start = commentText.lastIndexOf('/*'); | 158 var start = commentText.lastIndexOf('/*'); |
247 var end = commentText.lastIndexOf('*/'); | 159 var end = commentText.lastIndexOf('*/'); |
248 if (start != -1 && end != -1) { | 160 if (start != -1 && end != -1) { |
249 expect(start, lessThan(end)); | 161 expect(start, lessThan(end)); |
250 var errors = commentText.substring(start + 2, end).split(','); | 162 var errors = commentText.substring(start + 2, end).split(','); |
251 var expectations = errors.map(_ErrorExpectation.parse); | 163 var expectations = |
252 expectedErrors[node] = expectations.where((x) => x != null).toList(); | 164 errors.map(_ErrorExpectation.parse).where((x) => x != null); |
| 165 |
| 166 for (var e in expectations) _expectError(node, e); |
253 } | 167 } |
254 } | 168 } |
255 } | 169 } |
256 return super.visitNode(node); | 170 return super.visitNode(node); |
257 } | 171 } |
| 172 |
| 173 void _expectError(AstNode node, _ErrorExpectation expected) { |
| 174 |
| 175 // See if we can find the expected error in our actual errors |
| 176 for (var actual in _actualErrors) { |
| 177 if (actual.offset == node.offset && actual.length == node.length) { |
| 178 var actualMsg = _formatActualError(actual); |
| 179 expect(_actualErrorLevel(actual), expected.level, |
| 180 reason: 'expected different error code at:\n\n$actualMsg'); |
| 181 expect(actual.errorCode.name, expected.typeName, |
| 182 reason: 'expected different error type at:\n\n$actualMsg'); |
| 183 |
| 184 // We found it. Stop the search. |
| 185 _actualErrors.remove(actual); |
| 186 return; |
| 187 } |
| 188 } |
| 189 |
| 190 var span = _createSpan(node.offset, node.length); |
| 191 var levelName = expected.level.name.toLowerCase(); |
| 192 var msg = span.message(expected.typeName, color: colorOf(levelName)); |
| 193 fail('expected error was not reported at:\n\n$levelName: $msg'); |
| 194 } |
| 195 |
| 196 Level _actualErrorLevel(AnalysisError actual) { |
| 197 return const <ErrorSeverity, Level>{ |
| 198 ErrorSeverity.ERROR: Level.SEVERE, |
| 199 ErrorSeverity.WARNING: Level.WARNING, |
| 200 ErrorSeverity.INFO: Level.INFO |
| 201 }[actual.errorCode.errorSeverity]; |
| 202 } |
| 203 |
| 204 String _formatActualError(AnalysisError actual) { |
| 205 var span = _createSpan(actual.offset, actual.length); |
| 206 var levelName = _actualErrorLevel(actual).name.toLowerCase(); |
| 207 var msg = span.message(actual.message, color: colorOf(levelName)); |
| 208 return '$levelName: $msg'; |
| 209 } |
| 210 |
| 211 SourceSpan _createSpan(int offset, int len) { |
| 212 return createSpanHelper( |
| 213 _unit, offset, offset + len, _unit.element.source, _unitSourceCode); |
| 214 } |
258 } | 215 } |
259 | 216 |
260 /// Describes an expected message that should be produced by the checker. | 217 /// Describes an expected message that should be produced by the checker. |
261 class _ErrorExpectation { | 218 class _ErrorExpectation { |
262 final Level level; | 219 final Level level; |
263 final Type type; | 220 final String typeName; |
264 _ErrorExpectation(this.level, this.type); | 221 _ErrorExpectation(this.level, this.typeName); |
265 | 222 |
266 static _ErrorExpectation _parse(String descriptor) { | 223 static _ErrorExpectation _parse(String descriptor) { |
267 var tokens = descriptor.split(':'); | 224 var tokens = descriptor.split(':'); |
268 expect(tokens.length, 2, reason: 'invalid error descriptor'); | 225 expect(tokens.length, 2, reason: 'invalid error descriptor'); |
269 var name = tokens[0].toUpperCase(); | 226 var name = tokens[0].toUpperCase(); |
270 var typeName = tokens[1].toLowerCase(); | 227 var typeName = tokens[1]; |
271 | 228 |
272 var level = | 229 var level = |
273 Level.LEVELS.firstWhere((l) => l.name == name, orElse: () => null); | 230 Level.LEVELS.firstWhere((l) => l.name == name, orElse: () => null); |
274 expect(level, isNotNull, | 231 expect(level, isNotNull, |
275 reason: 'invalid level in error descriptor: `${tokens[0]}`'); | 232 reason: 'invalid level in error descriptor: `${tokens[0]}`'); |
276 var type = infoTypes.firstWhere((t) => '$t'.toLowerCase() == typeName, | 233 expect(typeName, isNotNull, |
277 orElse: () => null); | |
278 expect(type, isNotNull, | |
279 reason: 'invalid type in error descriptor: ${tokens[1]}'); | 234 reason: 'invalid type in error descriptor: ${tokens[1]}'); |
280 return new _ErrorExpectation(level, type); | 235 return new _ErrorExpectation(level, typeName); |
281 } | 236 } |
282 | 237 |
283 static _ErrorExpectation parse(String descriptor) { | 238 static _ErrorExpectation parse(String descriptor) { |
284 descriptor = descriptor.trim(); | 239 descriptor = descriptor.trim(); |
285 var tokens = descriptor.split(' '); | 240 var tokens = descriptor.split(' '); |
286 if (tokens.length == 1) return _parse(tokens[0]); | 241 if (tokens.length == 1) return _parse(tokens[0]); |
287 expect(tokens.length, 4, reason: 'invalid error descriptor'); | 242 expect(tokens.length, 4, reason: 'invalid error descriptor'); |
288 expect(tokens[1], "should", reason: 'invalid error descriptor'); | 243 expect(tokens[1], "should", reason: 'invalid error descriptor'); |
289 expect(tokens[2], "be", reason: 'invalid error descriptor'); | 244 expect(tokens[2], "be", reason: 'invalid error descriptor'); |
290 if (tokens[0] == "pass") return null; | 245 if (tokens[0] == "pass") return null; |
291 // TODO(leafp) For now, we just use whatever the current expectation is, | 246 // TODO(leafp) For now, we just use whatever the current expectation is, |
292 // eventually we could do more automated reporting here. | 247 // eventually we could do more automated reporting here. |
293 return _parse(tokens[0]); | 248 return _parse(tokens[0]); |
294 } | 249 } |
295 | 250 |
296 String toString() => '$level $type'; | 251 String toString() => '$level $typeName'; |
297 } | 252 } |
OLD | NEW |