Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(209)

Side by Side Diff: lib/src/testing.dart

Issue 1174643003: expose strong checker API, for use by analyzer_cli (Closed) Base URL: git@github.com:dart-lang/dev_compiler.git@master
Patch Set: Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/src/summary.dart ('k') | lib/src/utils.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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 }
OLDNEW
« no previous file with comments | « lib/src/summary.dart ('k') | lib/src/utils.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698