| 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 |