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:mirrors'; | 7 import 'dart:mirrors'; |
8 import 'package:analyzer/file_system/file_system.dart'; | 8 import 'package:analyzer/file_system/file_system.dart'; |
9 import 'package:analyzer/file_system/memory_file_system.dart'; | 9 import 'package:analyzer/file_system/memory_file_system.dart'; |
10 import 'package:analyzer/src/generated/ast.dart'; | |
11 import 'package:analyzer/src/generated/engine.dart' | 10 import 'package:analyzer/src/generated/engine.dart' |
12 show AnalysisContext, AnalysisEngine, AnalysisOptionsImpl; | 11 show AnalysisContext, AnalysisEngine, AnalysisOptionsImpl; |
13 import 'package:analyzer/src/generated/error.dart'; | |
14 import 'package:analyzer/src/generated/source.dart'; | 12 import 'package:analyzer/src/generated/source.dart'; |
15 import 'package:cli_util/cli_util.dart' show getSdkDir; | 13 import 'package:cli_util/cli_util.dart' show getSdkDir; |
16 import 'package:logging/logging.dart'; | |
17 import 'package:path/path.dart' as path; | 14 import 'package:path/path.dart' as path; |
18 import 'package:source_span/source_span.dart'; | |
19 import 'package:test/test.dart'; | |
20 | 15 |
21 import 'package:dev_compiler/strong_mode.dart'; | |
22 import 'package:dev_compiler/src/analysis_context.dart'; | 16 import 'package:dev_compiler/src/analysis_context.dart'; |
23 | 17 |
24 import 'package:dev_compiler/src/server/dependency_graph.dart' | 18 import 'package:dev_compiler/src/server/dependency_graph.dart' |
25 show runtimeFilesForServerMode; | 19 show runtimeFilesForServerMode; |
26 import 'package:dev_compiler/src/info.dart'; | |
27 import 'package:dev_compiler/src/options.dart'; | 20 import 'package:dev_compiler/src/options.dart'; |
28 import 'package:dev_compiler/src/utils.dart'; | |
29 | 21 |
30 /// Shared analysis context used for compilation. | 22 /// Shared analysis context used for compilation. |
31 final AnalysisContext realSdkContext = () { | 23 final AnalysisContext realSdkContext = () { |
32 var context = createAnalysisContextWithSources( | 24 var context = createAnalysisContextWithSources(new SourceResolverOptions( |
33 new StrongModeOptions(), | 25 dartSdkPath: getSdkDir().path, |
34 new SourceResolverOptions( | 26 customUrlMappings: { |
35 dartSdkPath: getSdkDir().path, | 27 'package:expect/expect.dart': _testCodegenPath('expect.dart'), |
36 customUrlMappings: { | 28 'package:async_helper/async_helper.dart': |
37 'package:expect/expect.dart': _testCodegenPath('expect.dart'), | 29 _testCodegenPath('async_helper.dart'), |
38 'package:async_helper/async_helper.dart': | 30 'package:unittest/unittest.dart': _testCodegenPath('unittest.dart'), |
39 _testCodegenPath('async_helper.dart'), | 31 'package:dom/dom.dart': _testCodegenPath('sunflower', 'dom.dart') |
40 'package:unittest/unittest.dart': _testCodegenPath('unittest.dart'), | 32 })); |
41 'package:dom/dom.dart': _testCodegenPath('sunflower', 'dom.dart') | |
42 })); | |
43 (context.analysisOptions as AnalysisOptionsImpl).cacheSize = 512; | 33 (context.analysisOptions as AnalysisOptionsImpl).cacheSize = 512; |
44 return context; | 34 return context; |
45 }(); | 35 }(); |
46 | 36 |
47 String _testCodegenPath(String p1, [String p2]) => | 37 String _testCodegenPath(String p1, [String p2]) => |
48 path.join(testDirectory, 'codegen', p1, p2); | 38 path.join(testDirectory, 'codegen', p1, p2); |
49 | 39 |
50 final String testDirectory = | 40 final String testDirectory = |
51 path.dirname((reflectClass(_TestUtils).owner as LibraryMirror).uri.path); | 41 path.dirname((reflectClass(_TestUtils).owner as LibraryMirror).uri.path); |
52 | 42 |
53 class _TestUtils {} | 43 class _TestUtils {} |
54 | 44 |
55 /// Run the checker on a program with files contents as indicated in | |
56 /// [testFiles]. | |
57 /// | |
58 /// This function makes several assumptions to make it easier to describe error | |
59 /// expectations: | |
60 /// | |
61 /// * a file named `/main.dart` exists in [testFiles]. | |
62 /// * all expected failures are listed in the source code using comments | |
63 /// immediately in front of the AST node that should contain the error. | |
64 /// * errors are formatted as a token `level:Type`, where `level` is the | |
65 /// logging level were the error would be reported at, and `Type` is the | |
66 /// concrete subclass of [StaticInfo] that denotes the error. | |
67 /// | |
68 /// For example, to check that an assignment produces a warning about a boxing | |
69 /// conversion, you can describe the test as follows: | |
70 /// | |
71 /// testChecker({ | |
72 /// '/main.dart': ''' | |
73 /// testMethod() { | |
74 /// dynamic x = /*warning:Box*/3; | |
75 /// } | |
76 /// ''' | |
77 /// }); | |
78 /// | |
79 void testChecker(String name, Map<String, String> testFiles, | |
80 {String sdkDir, customUrlMappings: const {}}) { | |
81 test(name, () { | |
82 expect(testFiles.containsKey('/main.dart'), isTrue, | |
83 reason: '`/main.dart` is missing in testFiles'); | |
84 | |
85 var provider = createTestResourceProvider(testFiles); | |
86 var uriResolver = new TestUriResolver(provider); | |
87 // Enable task model strong mode | |
88 AnalysisEngine.instance.useTaskModel = true; | |
89 var context = AnalysisEngine.instance.createAnalysisContext(); | |
90 context.analysisOptions.strongMode = true; | |
91 context.sourceFactory = createSourceFactory( | |
92 new SourceResolverOptions( | |
93 customUrlMappings: customUrlMappings, | |
94 useMockSdk: sdkDir == null, | |
95 dartSdkPath: sdkDir), | |
96 fileResolvers: [uriResolver]); | |
97 | |
98 var checker = | |
99 new StrongChecker(context, new StrongModeOptions(hints: true)); | |
100 | |
101 // Run the checker on /main.dart. | |
102 var mainSource = uriResolver.resolveAbsolute(new Uri.file('/main.dart')); | |
103 var initialLibrary = | |
104 context.resolveCompilationUnit2(mainSource, mainSource); | |
105 | |
106 // Extract expectations from the comments in the test files, and | |
107 // check that all errors we emit are included in the expected map. | |
108 var allLibraries = reachableLibraries(initialLibrary.element.library); | |
109 for (var lib in allLibraries) { | |
110 for (var unit in lib.units) { | |
111 if (unit.source.uri.scheme == 'dart') continue; | |
112 | |
113 var errorInfo = checker.computeErrors(unit.source); | |
114 new _ExpectedErrorVisitor(errorInfo.errors).validate(unit.unit); | |
115 } | |
116 } | |
117 }); | |
118 } | |
119 | |
120 /// Creates a [MemoryResourceProvider] with test data | 45 /// Creates a [MemoryResourceProvider] with test data |
121 MemoryResourceProvider createTestResourceProvider( | 46 MemoryResourceProvider createTestResourceProvider( |
122 Map<String, String> testFiles) { | 47 Map<String, String> testFiles) { |
123 var provider = new MemoryResourceProvider(); | 48 var provider = new MemoryResourceProvider(); |
124 runtimeFilesForServerMode.forEach((filepath) { | 49 runtimeFilesForServerMode.forEach((filepath) { |
125 testFiles['/dev_compiler_runtime/$filepath'] = | 50 testFiles['/dev_compiler_runtime/$filepath'] = |
126 '/* test contents of $filepath */'; | 51 '/* test contents of $filepath */'; |
127 }); | 52 }); |
128 testFiles.forEach((key, value) { | 53 testFiles.forEach((key, value) { |
129 var scheme = 'package:'; | 54 var scheme = 'package:'; |
(...skipping 13 matching lines...) Expand all Loading... |
143 | 68 |
144 @override | 69 @override |
145 Source resolveAbsolute(Uri uri, [Uri actualUri]) { | 70 Source resolveAbsolute(Uri uri, [Uri actualUri]) { |
146 if (uri.scheme == 'package') { | 71 if (uri.scheme == 'package') { |
147 return (provider.getResource('/packages/' + uri.path) as File) | 72 return (provider.getResource('/packages/' + uri.path) as File) |
148 .createSource(uri); | 73 .createSource(uri); |
149 } | 74 } |
150 return super.resolveAbsolute(uri, actualUri); | 75 return super.resolveAbsolute(uri, actualUri); |
151 } | 76 } |
152 } | 77 } |
153 | |
154 class _ExpectedErrorVisitor extends UnifyingAstVisitor { | |
155 final Set<AnalysisError> _actualErrors; | |
156 CompilationUnit _unit; | |
157 String _unitSourceCode; | |
158 | |
159 _ExpectedErrorVisitor(List<AnalysisError> actualErrors) | |
160 : _actualErrors = new Set.from(actualErrors); | |
161 | |
162 validate(CompilationUnit unit) { | |
163 _unit = unit; | |
164 // This reads the file. Only safe because tests use MemoryFileSystem. | |
165 _unitSourceCode = unit.element.source.contents.data; | |
166 | |
167 // Visit the compilation unit. | |
168 unit.accept(this); | |
169 | |
170 if (_actualErrors.isNotEmpty) { | |
171 var actualMsgs = _actualErrors.map(_formatActualError).join('\n'); | |
172 fail('Unexpected errors reported by checker:\n\n$actualMsgs'); | |
173 } | |
174 } | |
175 | |
176 visitNode(AstNode node) { | |
177 var token = node.beginToken; | |
178 var comment = token.precedingComments; | |
179 // Use error marker found in an immediately preceding comment, | |
180 // and attach it to the outermost expression that starts at that token. | |
181 if (comment != null) { | |
182 while (comment.next != null) { | |
183 comment = comment.next; | |
184 } | |
185 if (comment.end == token.offset && node.parent.beginToken != token) { | |
186 var commentText = '$comment'; | |
187 var start = commentText.lastIndexOf('/*'); | |
188 var end = commentText.lastIndexOf('*/'); | |
189 if (start != -1 && end != -1) { | |
190 expect(start, lessThan(end)); | |
191 var errors = commentText.substring(start + 2, end).split(','); | |
192 var expectations = | |
193 errors.map(_ErrorExpectation.parse).where((x) => x != null); | |
194 | |
195 for (var e in expectations) _expectError(node, e); | |
196 } | |
197 } | |
198 } | |
199 return super.visitNode(node); | |
200 } | |
201 | |
202 void _expectError(AstNode node, _ErrorExpectation expected) { | |
203 // See if we can find the expected error in our actual errors | |
204 for (var actual in _actualErrors) { | |
205 if (actual.offset == node.offset && actual.length == node.length) { | |
206 var actualMsg = _formatActualError(actual); | |
207 expect(_actualErrorLevel(actual), expected.level, | |
208 reason: 'expected different error code at:\n\n$actualMsg'); | |
209 expect(errorCodeName(actual.errorCode), expected.typeName, | |
210 reason: 'expected different error type at:\n\n$actualMsg'); | |
211 | |
212 // We found it. Stop the search. | |
213 _actualErrors.remove(actual); | |
214 return; | |
215 } | |
216 } | |
217 | |
218 var span = _createSpan(node.offset, node.length); | |
219 var levelName = expected.level.name.toLowerCase(); | |
220 var msg = span.message(expected.typeName, color: colorOf(levelName)); | |
221 fail('expected error was not reported at:\n\n$levelName: $msg'); | |
222 } | |
223 | |
224 Level _actualErrorLevel(AnalysisError actual) { | |
225 return const <ErrorSeverity, Level>{ | |
226 ErrorSeverity.ERROR: Level.SEVERE, | |
227 ErrorSeverity.WARNING: Level.WARNING, | |
228 ErrorSeverity.INFO: Level.INFO | |
229 }[actual.errorCode.errorSeverity]; | |
230 } | |
231 | |
232 String _formatActualError(AnalysisError actual) { | |
233 var span = _createSpan(actual.offset, actual.length); | |
234 var levelName = _actualErrorLevel(actual).name.toLowerCase(); | |
235 var msg = span.message(actual.message, color: colorOf(levelName)); | |
236 return '$levelName: [${errorCodeName(actual.errorCode)}] $msg'; | |
237 } | |
238 | |
239 SourceSpan _createSpan(int offset, int len) { | |
240 return createSpanHelper(_unit.lineInfo, offset, offset + len, | |
241 _unit.element.source, _unitSourceCode); | |
242 } | |
243 } | |
244 | |
245 /// Describes an expected message that should be produced by the checker. | |
246 class _ErrorExpectation { | |
247 final Level level; | |
248 final String typeName; | |
249 _ErrorExpectation(this.level, this.typeName); | |
250 | |
251 static _ErrorExpectation _parse(String descriptor) { | |
252 var tokens = descriptor.split(':'); | |
253 expect(tokens.length, 2, reason: 'invalid error descriptor'); | |
254 var name = tokens[0].toUpperCase(); | |
255 var typeName = tokens[1]; | |
256 | |
257 var level = | |
258 Level.LEVELS.firstWhere((l) => l.name == name, orElse: () => null); | |
259 expect(level, isNotNull, | |
260 reason: 'invalid level in error descriptor: `${tokens[0]}`'); | |
261 expect(typeName, isNotNull, | |
262 reason: 'invalid type in error descriptor: ${tokens[1]}'); | |
263 return new _ErrorExpectation(level, typeName); | |
264 } | |
265 | |
266 static _ErrorExpectation parse(String descriptor) { | |
267 descriptor = descriptor.trim(); | |
268 var tokens = descriptor.split(' '); | |
269 if (tokens.length == 1) return _parse(tokens[0]); | |
270 expect(tokens.length, 4, reason: 'invalid error descriptor'); | |
271 expect(tokens[1], "should", reason: 'invalid error descriptor'); | |
272 expect(tokens[2], "be", reason: 'invalid error descriptor'); | |
273 if (tokens[0] == "pass") return null; | |
274 // TODO(leafp) For now, we just use whatever the current expectation is, | |
275 // eventually we could do more automated reporting here. | |
276 return _parse(tokens[0]); | |
277 } | |
278 | |
279 String toString() => '$level $typeName'; | |
280 } | |
OLD | NEW |