OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library dev_compiler.src.testing; | |
6 | |
7 import 'dart:collection' show Queue; | |
8 import 'package:analyzer/file_system/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' | |
12 show AnalysisContext, AnalysisEngine; | |
13 import 'package:analyzer/src/generated/error.dart'; | |
14 import 'package:logging/logging.dart'; | |
15 import 'package:source_span/source_span.dart'; | |
16 import 'package:test/test.dart'; | |
17 | |
18 import 'package:dev_compiler/strong_mode.dart'; | |
19 | |
20 import 'analysis_context.dart'; | |
21 import 'dependency_graph.dart' show runtimeFilesForServerMode; | |
22 import 'info.dart'; | |
23 import 'options.dart'; | |
24 import 'utils.dart'; | |
25 | |
26 /// Run the checker on a program with files contents as indicated in | |
27 /// [testFiles]. | |
28 /// | |
29 /// This function makes several assumptions to make it easier to describe error | |
30 /// expectations: | |
31 /// | |
32 /// * a file named `/main.dart` exists in [testFiles]. | |
33 /// * all expected failures are listed in the source code using comments | |
34 /// immediately in front of the AST node that should contain the error. | |
35 /// * errors are formatted as a token `level:Type`, where `level` is the | |
36 /// logging level were the error would be reported at, and `Type` is the | |
37 /// concrete subclass of [StaticInfo] that denotes the error. | |
38 /// | |
39 /// For example, to check that an assignment produces a warning about a boxing | |
40 /// conversion, you can describe the test as follows: | |
41 /// | |
42 /// testChecker({ | |
43 /// '/main.dart': ''' | |
44 /// testMethod() { | |
45 /// dynamic x = /*warning:Box*/3; | |
46 /// } | |
47 /// ''' | |
48 /// }); | |
49 /// | |
50 void testChecker(Map<String, String> testFiles, {String sdkDir, | |
51 customUrlMappings: const {}, relaxedCasts: true, | |
52 inferDownwards: StrongModeOptions.inferDownwardsDefault, | |
53 inferFromOverrides: StrongModeOptions.inferFromOverridesDefault, | |
54 inferTransitively: StrongModeOptions.inferTransitivelyDefault, | |
55 nonnullableTypes: StrongModeOptions.NONNULLABLE_TYPES}) { | |
56 expect(testFiles.containsKey('/main.dart'), isTrue, | |
57 reason: '`/main.dart` is missing in testFiles'); | |
58 | |
59 var provider = createTestResourceProvider(testFiles); | |
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]); | |
67 | |
68 var checker = new StrongChecker(context, new StrongModeOptions( | |
69 relaxedCasts: relaxedCasts, | |
70 inferDownwards: inferDownwards, | |
71 inferFromOverrides: inferFromOverrides, | |
72 inferTransitively: inferTransitively, | |
73 nonnullableTypes: nonnullableTypes, | |
74 hints: true)); | |
75 | |
76 // Run the checker on /main.dart. | |
77 var mainSource = uriResolver.resolveAbsolute(new Uri.file('/main.dart')); | |
78 var initialLibrary = context.resolveCompilationUnit2(mainSource, mainSource); | |
79 | |
80 // Extract expectations from the comments in the test files, and | |
81 // check that all errors we emit are included in the expected map. | |
82 var allLibraries = reachableLibraries(initialLibrary.element.library); | |
83 for (var lib in allLibraries) { | |
84 for (var unit in lib.units) { | |
85 if (unit.source.uri.scheme == 'dart') continue; | |
86 | |
87 var errorInfo = checker.computeErrors(unit.source); | |
88 new _ExpectedErrorVisitor(errorInfo.errors).validate(unit.unit); | |
89 } | |
90 } | |
91 } | |
92 | |
93 /// Creates a [MemoryResourceProvider] with test data | |
94 MemoryResourceProvider createTestResourceProvider( | |
95 Map<String, String> testFiles) { | |
96 var provider = new MemoryResourceProvider(); | |
97 runtimeFilesForServerMode().forEach((filepath) { | |
98 testFiles['/dev_compiler_runtime/$filepath'] = | |
99 '/* test contents of $filepath */'; | |
100 }); | |
101 testFiles.forEach((key, value) { | |
102 var scheme = 'package:'; | |
103 if (key.startsWith(scheme)) { | |
104 key = '/packages/${key.substring(scheme.length)}'; | |
105 } | |
106 provider.newFile(key, value); | |
107 }); | |
108 return provider; | |
109 } | |
110 | |
111 class TestUriResolver extends ResourceUriResolver { | |
112 final MemoryResourceProvider provider; | |
113 TestUriResolver(provider) | |
114 : provider = provider, | |
115 super(provider); | |
116 resolveAbsolute(Uri uri) { | |
117 if (uri.scheme == 'package') { | |
118 return (provider.getResource('/packages/' + uri.path) as File) | |
119 .createSource(uri); | |
120 } | |
121 return super.resolveAbsolute(uri); | |
122 } | |
123 } | |
124 | |
125 class _ExpectedErrorVisitor extends UnifyingAstVisitor { | |
126 final Set<AnalysisError> _actualErrors; | |
127 CompilationUnit _unit; | |
128 String _unitSourceCode; | |
129 | |
130 _ExpectedErrorVisitor(List<AnalysisError> actualErrors) | |
131 : _actualErrors = new Set.from(actualErrors); | |
132 | |
133 validate(CompilationUnit unit) { | |
134 _unit = unit; | |
135 // This reads the file. Only safe because tests use MemoryFileSystem. | |
136 _unitSourceCode = unit.element.source.contents.data; | |
137 | |
138 // Visit the compilation unit. | |
139 unit.accept(this); | |
140 | |
141 if (_actualErrors.isNotEmpty) { | |
142 var actualMsgs = _actualErrors.map(_formatActualError).join('\n'); | |
143 fail('Unexpected errors reported by checker:\n\n$actualMsgs'); | |
144 } | |
145 } | |
146 | |
147 visitNode(AstNode node) { | |
148 var token = node.beginToken; | |
149 var comment = token.precedingComments; | |
150 // Use error marker found in an immediately preceding comment, | |
151 // and attach it to the outermost expression that starts at that token. | |
152 if (comment != null) { | |
153 while (comment.next != null) { | |
154 comment = comment.next; | |
155 } | |
156 if (comment.end == token.offset && node.parent.beginToken != token) { | |
157 var commentText = '$comment'; | |
158 var start = commentText.lastIndexOf('/*'); | |
159 var end = commentText.lastIndexOf('*/'); | |
160 if (start != -1 && end != -1) { | |
161 expect(start, lessThan(end)); | |
162 var errors = commentText.substring(start + 2, end).split(','); | |
163 var expectations = | |
164 errors.map(_ErrorExpectation.parse).where((x) => x != null); | |
165 | |
166 for (var e in expectations) _expectError(node, e); | |
167 } | |
168 } | |
169 } | |
170 return super.visitNode(node); | |
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(errorCodeName(actual.errorCode), 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: [${errorCodeName(actual.errorCode)}] $msg'; | |
209 } | |
210 | |
211 SourceSpan _createSpan(int offset, int len) { | |
212 return createSpanHelper(_unit.lineInfo, offset, offset + len, | |
213 _unit.element.source, _unitSourceCode); | |
214 } | |
215 } | |
216 | |
217 /// Describes an expected message that should be produced by the checker. | |
218 class _ErrorExpectation { | |
219 final Level level; | |
220 final String typeName; | |
221 _ErrorExpectation(this.level, this.typeName); | |
222 | |
223 static _ErrorExpectation _parse(String descriptor) { | |
224 var tokens = descriptor.split(':'); | |
225 expect(tokens.length, 2, reason: 'invalid error descriptor'); | |
226 var name = tokens[0].toUpperCase(); | |
227 var typeName = tokens[1]; | |
228 | |
229 var level = | |
230 Level.LEVELS.firstWhere((l) => l.name == name, orElse: () => null); | |
231 expect(level, isNotNull, | |
232 reason: 'invalid level in error descriptor: `${tokens[0]}`'); | |
233 expect(typeName, isNotNull, | |
234 reason: 'invalid type in error descriptor: ${tokens[1]}'); | |
235 return new _ErrorExpectation(level, typeName); | |
236 } | |
237 | |
238 static _ErrorExpectation parse(String descriptor) { | |
239 descriptor = descriptor.trim(); | |
240 var tokens = descriptor.split(' '); | |
241 if (tokens.length == 1) return _parse(tokens[0]); | |
242 expect(tokens.length, 4, reason: 'invalid error descriptor'); | |
243 expect(tokens[1], "should", reason: 'invalid error descriptor'); | |
244 expect(tokens[2], "be", reason: 'invalid error descriptor'); | |
245 if (tokens[0] == "pass") return null; | |
246 // TODO(leafp) For now, we just use whatever the current expectation is, | |
247 // eventually we could do more automated reporting here. | |
248 return _parse(tokens[0]); | |
249 } | |
250 | |
251 String toString() => '$level $typeName'; | |
252 } | |
OLD | NEW |