OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2016, 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 /// Tests code generation. | 5 /// Tests code generation. |
6 /// Runs Dart Dev Compiler on all input in the `codegen` directory and checks | 6 /// Runs Dart Dev Compiler on all input in the `codegen` directory and checks |
7 /// that the output is what we expected. | 7 /// that the output is what we expected. |
8 library dev_compiler.test.codegen_test; | 8 library dev_compiler.test.codegen_test; |
9 | 9 |
10 import 'dart:io'; | 10 import 'dart:convert' show JSON; |
11 import 'package:analyzer/src/generated/engine.dart' | 11 import 'dart:io' show Directory, File, Platform; |
12 show AnalysisContext, AnalysisEngine, Logger; | 12 import 'package:args/args.dart' show ArgParser, ArgResults; |
13 import 'package:analyzer/src/generated/java_engine.dart' show CaughtException; | |
14 import 'package:analyzer/src/generated/source_io.dart'; | |
15 import 'package:args/args.dart'; | |
16 import 'package:cli_util/cli_util.dart' show getSdkDir; | |
17 import 'package:logging/logging.dart' show Level; | |
18 import 'package:path/path.dart' as path; | 13 import 'package:path/path.dart' as path; |
19 import 'package:test/test.dart'; | 14 import 'package:test/test.dart' show group, test; |
20 | 15 |
21 import 'package:dev_compiler/devc.dart'; | 16 import 'package:analyzer/analyzer.dart' |
22 import 'package:dev_compiler/src/analysis_context.dart'; | 17 show |
23 import 'package:dev_compiler/src/compiler.dart' show defaultRuntimeFiles; | 18 ExportDirective, |
24 import 'package:dev_compiler/src/options.dart'; | 19 ImportDirective, |
25 import 'package:dev_compiler/src/report.dart' show LogReporter; | 20 StringLiteral, |
26 | 21 UriBasedDirective, |
| 22 parseDirectives; |
| 23 import 'package:analyzer/src/generated/source.dart' show Source; |
| 24 import 'package:dev_compiler/src/analyzer/context.dart' show AnalyzerOptions; |
| 25 import 'package:dev_compiler/src/compiler/compiler.dart' |
| 26 show BuildUnit, CompilerOptions, ModuleCompiler; |
27 import 'testing.dart' show testDirectory; | 27 import 'testing.dart' show testDirectory; |
28 import 'multitest.dart'; | 28 import 'multitest.dart' show extractTestsFromMultitest, isMultiTest; |
| 29 import '../tool/build_sdk.dart' as build_sdk; |
| 30 import 'package:dev_compiler/src/compiler/compiler.dart'; |
29 | 31 |
30 final ArgParser argParser = new ArgParser() | 32 final ArgParser argParser = new ArgParser() |
31 ..addOption('dart-sdk', help: 'Dart SDK Path', defaultsTo: null); | 33 ..addOption('dart-sdk', help: 'Dart SDK Path', defaultsTo: null); |
32 | 34 |
33 final inputDir = path.join(testDirectory, 'codegen'); | |
34 | |
35 Iterable<String> _findTests(String dir, RegExp filePattern) { | |
36 var files = new Directory(dir) | |
37 .listSync() | |
38 .where((f) => f is File) | |
39 .map((f) => f.path) | |
40 .where((p) => p.endsWith('.dart') && filePattern.hasMatch(p)); | |
41 if (dir != inputDir) { | |
42 files = files | |
43 .where((p) => p.endsWith('_test.dart') || p.endsWith('_multi.dart')); | |
44 } | |
45 return files; | |
46 } | |
47 | |
48 main(arguments) { | 35 main(arguments) { |
49 if (arguments == null) arguments = []; | 36 if (arguments == null) arguments = []; |
50 ArgResults args = argParser.parse(arguments); | 37 ArgResults args = argParser.parse(arguments); |
51 var filePattern = new RegExp(args.rest.length > 0 ? args.rest[0] : '.'); | 38 var filePattern = new RegExp(args.rest.length > 0 ? args.rest[0] : '.'); |
52 var compilerMessages = new StringBuffer(); | |
53 var loggerSub; | |
54 | 39 |
55 bool codeCoverage = Platform.environment.containsKey('COVERALLS_TOKEN'); | 40 var expectDir = path.join(inputDir, 'expect'); |
| 41 var testDirs = ['language', path.join('lib', 'typed_data')]; |
56 | 42 |
57 setUp(() { | 43 var multitests = expandMultiTests(testDirs, filePattern); |
58 compilerMessages.clear(); | 44 |
59 loggerSub = setupLogger(Level.CONFIG, compilerMessages.writeln); | 45 // Build packages tests depend on |
| 46 var compiler = new ModuleCompiler( |
| 47 new AnalyzerOptions(customUrlMappings: packageUrlMappings)); |
| 48 |
| 49 group('dartdevc package', () { |
| 50 _buildPackages(compiler, expectDir); |
| 51 |
| 52 test('matcher', () { |
| 53 _buildMatcher(compiler, expectDir); |
| 54 }); |
60 }); | 55 }); |
61 | 56 |
62 tearDown(() { | 57 test('dartdevc sunflower', () { |
63 if (loggerSub != null) { | 58 _buildSunflower(compiler, expectDir); |
64 loggerSub.cancel(); | |
65 loggerSub = null; | |
66 } | |
67 }); | 59 }); |
68 | 60 |
69 var expectDir = path.join(inputDir, 'expect'); | 61 // Our default compiler options. Individual tests can override these. |
70 | 62 var defaultOptions = ['--no-source-map', '--no-summarize']; |
71 BatchCompiler createCompiler(DartUriResolver sdkResolver, | 63 var compilerArgParser = CompilerOptions.addArguments(new ArgParser()); |
72 {bool checkSdk: false, | |
73 bool sourceMaps: false, | |
74 bool destructureNamedParams: false, | |
75 bool closure: false, | |
76 ModuleFormat moduleFormat: ModuleFormat.legacy}) { | |
77 String _testCodegenPath(String p1, [String p2]) => | |
78 path.join(testDirectory, 'codegen', p1, p2); | |
79 | |
80 var context = createAnalysisContextWithSources( | |
81 new SourceResolverOptions(customUrlMappings: { | |
82 'package:expect/expect.dart': _testCodegenPath('expect.dart'), | |
83 'package:async_helper/async_helper.dart': | |
84 _testCodegenPath('async_helper.dart'), | |
85 'package:unittest/unittest.dart': _testCodegenPath('unittest.dart'), | |
86 'package:dom/dom.dart': _testCodegenPath('sunflower', 'dom.dart') | |
87 }), | |
88 sdkResolver: sdkResolver); | |
89 | |
90 // TODO(jmesserly): add a way to specify flags in the test file, so | |
91 // they're more self-contained. | |
92 var runtimeDir = path.join(path.dirname(testDirectory), 'lib', 'runtime'); | |
93 var options = new CompilerOptions( | |
94 codegenOptions: new CodegenOptions( | |
95 outputDir: expectDir, | |
96 emitSourceMaps: sourceMaps, | |
97 closure: closure, | |
98 destructureNamedParams: destructureNamedParams, | |
99 forceCompile: checkSdk, | |
100 moduleFormat: moduleFormat), | |
101 useColors: false, | |
102 checkSdk: checkSdk, | |
103 runtimeDir: runtimeDir, | |
104 inputBaseDir: inputDir); | |
105 var reporter = new LogReporter(context); | |
106 return new BatchCompiler(context, options, reporter: reporter); | |
107 } | |
108 | |
109 bool compile(BatchCompiler compiler, String filePath) { | |
110 compiler.compileFromUriString(filePath, (String url) { | |
111 // Write compiler messages to disk. | |
112 var messagePath = '${path.withoutExtension(url)}.txt'; | |
113 var file = new File(messagePath); | |
114 var message = ''' | |
115 // Messages from compiling ${path.basenameWithoutExtension(url)}.dart | |
116 $compilerMessages'''; | |
117 var dir = file.parent; | |
118 if (!dir.existsSync()) dir.createSync(recursive: true); | |
119 file.writeAsStringSync(message); | |
120 compilerMessages.clear(); | |
121 }); | |
122 return !compiler.failure; | |
123 } | |
124 | |
125 var testDirs = <String>['language', path.join('lib', 'typed_data')]; | |
126 | |
127 var multitests = new Set<String>(); | |
128 { | |
129 // Expand wacky multitests into a bunch of test files. | |
130 // We'll compile each one as if it was an input. | |
131 for (var testDir in testDirs) { | |
132 var fullDir = path.join(inputDir, testDir); | |
133 var testFiles = _findTests(fullDir, filePattern); | |
134 | |
135 for (var filePath in testFiles) { | |
136 if (filePath.endsWith('_multi.dart')) continue; | |
137 | |
138 var contents = new File(filePath).readAsStringSync(); | |
139 if (isMultiTest(contents)) { | |
140 multitests.add(filePath); | |
141 | |
142 var tests = new Map<String, String>(); | |
143 var outcomes = new Map<String, Set<String>>(); | |
144 extractTestsFromMultitest(filePath, contents, tests, outcomes); | |
145 | |
146 var filename = path.basenameWithoutExtension(filePath); | |
147 tests.forEach((name, contents) { | |
148 new File(path.join(fullDir, '${filename}_${name}_multi.dart')) | |
149 .writeAsStringSync(contents); | |
150 }); | |
151 } | |
152 } | |
153 } | |
154 } | |
155 | |
156 var realSdkResolver = createSdkPathResolver(getSdkDir().path); | |
157 var batchCompiler = createCompiler(realSdkResolver); | |
158 | 64 |
159 var allDirs = [null]; | 65 var allDirs = [null]; |
160 allDirs.addAll(testDirs); | 66 allDirs.addAll(testDirs); |
161 for (var dir in allDirs) { | 67 for (var dir in allDirs) { |
162 if (codeCoverage && dir != null) continue; | 68 if (codeCoverage && dir != null) continue; |
163 | 69 |
164 group('dartdevc ' + path.join('test', 'codegen', dir), () { | 70 group('dartdevc ' + path.join('test', 'codegen', dir), () { |
165 var outDir = new Directory(path.join(expectDir, dir)); | 71 var outDir = new Directory(path.join(expectDir, dir)); |
166 if (!outDir.existsSync()) outDir.createSync(recursive: true); | 72 if (!outDir.existsSync()) outDir.createSync(recursive: true); |
167 | 73 |
168 var testFiles = _findTests(path.join(inputDir, dir), filePattern); | 74 var testFiles = _findTests(path.join(inputDir, dir), filePattern); |
169 for (var filePath in testFiles) { | 75 for (var filePath in testFiles) { |
170 if (multitests.contains(filePath)) continue; | 76 if (multitests.contains(filePath)) continue; |
171 | 77 |
172 var filename = path.basenameWithoutExtension(filePath); | 78 var filename = path.basenameWithoutExtension(filePath); |
173 | 79 |
174 test('$filename.dart', () { | 80 test('$filename.dart', () { |
175 // TODO(jmesserly): this was added to get some coverage of source maps | 81 // Check if we need to use special compile options. |
176 // and closure annotations. | 82 var contents = new File(filePath).readAsStringSync(); |
177 // We need a more comprehensive strategy to test them. | 83 var match = |
178 var sourceMaps = filename == 'map_keys'; | 84 new RegExp(r'// compile options: (.*)').matchAsPrefix(contents); |
179 var closure = filename == 'closure'; | 85 |
180 var destructureNamedParams = filename == 'destructuring' || closure; | 86 var args = new List.from(defaultOptions); |
181 var moduleFormat = filename == 'es6_modules' || closure | 87 if (match != null) { |
182 ? ModuleFormat.es6 | 88 args.addAll(match.group(1).split(' ')); |
183 : filename == 'node_modules' | 89 } |
184 ? ModuleFormat.node | 90 var options = |
185 : ModuleFormat.legacy; | 91 new CompilerOptions.fromArguments(compilerArgParser.parse(args)); |
186 var success; | 92 |
187 // TODO(vsm): Is it okay to reuse the same context here? If there is | 93 // Collect any other files we've imported. |
188 // overlap between test files, we may need separate ones for each | 94 var files = new Set<String>(); |
189 // compiler. | 95 _collectTransitiveImports(contents, files, from: filePath); |
190 var compiler = (sourceMaps || | 96 |
191 closure || | 97 var unit = new BuildUnit(filename, files.toList(), _moduleForLibrary); |
192 destructureNamedParams || | 98 var module = compiler.compile(unit, options); |
193 moduleFormat != ModuleFormat.legacy) | 99 _writeModule(path.join(outDir.path, filename), module); |
194 ? createCompiler(realSdkResolver, | |
195 sourceMaps: sourceMaps, | |
196 destructureNamedParams: destructureNamedParams, | |
197 closure: closure, | |
198 moduleFormat: moduleFormat) | |
199 : batchCompiler; | |
200 success = compile(compiler, filePath); | |
201 | |
202 var outFile = new File(path.join(outDir.path, '$filename.js')); | |
203 expect(!success || outFile.existsSync(), true, | |
204 reason: '${outFile.path} was created if compilation succeeds'); | |
205 }); | 100 }); |
206 } | 101 } |
207 }); | 102 }); |
208 } | 103 } |
209 | 104 |
210 if (codeCoverage) { | 105 if (codeCoverage) { |
211 group('sdk', () { | 106 test('build_sdk code coverage', () { |
212 // The analyzer does not bubble exception messages for certain internal | 107 var generatedSdkDir = |
213 // dart:* library failures, such as failing to find | 108 path.join(testDirectory, '..', 'tool', 'generated_sdk'); |
214 // "_internal/libraries.dart". Instead it produces an opaque "failed to | 109 return build_sdk.main(['--dart-sdk', generatedSdkDir, '-o', expectDir]); |
215 // instantiate dart:core" message. To remedy this we hook up an analysis | |
216 // logger that prints these messages. | |
217 var savedLogger; | |
218 setUp(() { | |
219 savedLogger = AnalysisEngine.instance.logger; | |
220 AnalysisEngine.instance.logger = new PrintLogger(); | |
221 }); | |
222 tearDown(() { | |
223 AnalysisEngine.instance.logger = savedLogger; | |
224 }); | |
225 | |
226 test('devc dart:core', () { | |
227 var testSdkResolver = createSdkPathResolver( | |
228 path.join(testDirectory, '..', 'tool', 'generated_sdk')); | |
229 | |
230 // Get the test SDK. We use a checked in copy so test expectations can | |
231 // be generated against a specific SDK version. | |
232 var compiler = createCompiler(testSdkResolver, checkSdk: true); | |
233 compile(compiler, 'dart:core'); | |
234 var outFile = new File(path.join(expectDir, 'dart/core.js')); | |
235 expect(outFile.existsSync(), true, | |
236 reason: '${outFile.path} was created for dart:core'); | |
237 }); | |
238 }); | 110 }); |
239 } | 111 } |
240 | 112 } |
241 var expectedRuntime = | 113 |
242 defaultRuntimeFiles.map((f) => 'dev_compiler/runtime/$f'); | 114 void _writeModule(String outPath, JSModuleFile result) { |
243 | 115 new Directory(path.dirname(outPath)).createSync(recursive: true); |
244 test('devc jscodegen sunflower.html', () { | 116 |
245 var filePath = path.join(inputDir, 'sunflower', 'sunflower.html'); | 117 result.errors.add(''); // for trailing newline |
246 var success = compile(batchCompiler, filePath); | 118 new File(outPath + '.txt').writeAsStringSync(result.errors.join('\n')); |
247 | 119 |
248 var expectedFiles = ['sunflower.html', 'sunflower.js',]; | 120 if (result.isValid) { |
249 | 121 new File(outPath + '.js').writeAsStringSync(result.code); |
250 for (var filepath in expectedFiles) { | 122 if (result.sourceMap != null) { |
251 var outFile = new File(path.join(expectDir, 'sunflower', filepath)); | 123 var mapPath = outPath + '.js.map'; |
252 expect(outFile.existsSync(), success, | 124 new File(mapPath) |
253 reason: '${outFile.path} was created iff compilation succeeds'); | 125 .writeAsStringSync(JSON.encode(result.placeSourceMap(mapPath))); |
254 } | 126 } |
255 }); | 127 } |
256 | 128 } |
257 test('devc jscodegen html_input.html', () { | 129 |
258 var filePath = path.join(inputDir, 'html_input.html'); | 130 void _buildSunflower(ModuleCompiler compiler, String expectDir) { |
259 var success = compile(batchCompiler, filePath); | 131 var files = ['sunflower', 'circle', 'painter'] |
260 | 132 .map((f) => path.join(inputDir, 'sunflower', '$f.dart')) |
261 var expectedFiles = [ | 133 .toList(); |
262 'html_input.html', | 134 var input = new BuildUnit('sunflower', files, _moduleForLibrary); |
263 'dir/html_input_a.js', | 135 var options = new CompilerOptions(summarizeApi: false); |
264 'dir/html_input_b.js', | 136 |
265 'dir/html_input_c.js', | 137 var built = compiler.compile(input, options); |
266 'dir/html_input_d.js', | 138 _writeModule(path.join(expectDir, 'sunflower', 'sunflower'), built); |
267 'dir/html_input_e.js' | 139 } |
268 ]..addAll(expectedRuntime); | 140 |
269 | 141 void _buildPackages(ModuleCompiler compiler, String expectDir) { |
270 for (var filepath in expectedFiles) { | 142 // Note: we don't summarize these, as we're going to rely on our in-memory |
271 var outFile = new File(path.join(expectDir, filepath)); | 143 // shared analysis context for caching, and `_moduleForLibrary` below |
272 expect(outFile.existsSync(), success, | 144 // understands these are from other modules. |
273 reason: '${outFile.path} was created iff compilation succeeds'); | 145 var options = new CompilerOptions(sourceMap: false, summarizeApi: false); |
| 146 |
| 147 for (var uri in packageUrlMappings.keys) { |
| 148 assert(uri.startsWith('package:')); |
| 149 var uriPath = uri.substring('package:'.length); |
| 150 var name = path.basenameWithoutExtension(uriPath); |
| 151 test(name, () { |
| 152 var input = new BuildUnit(name, [uri], _moduleForLibrary); |
| 153 var built = compiler.compile(input, options); |
| 154 |
| 155 var outPath = path.join(expectDir, path.withoutExtension(uriPath)); |
| 156 _writeModule(outPath, built); |
| 157 }); |
| 158 } |
| 159 } |
| 160 |
| 161 void _buildMatcher(ModuleCompiler compiler, String expectDir) { |
| 162 var options = new CompilerOptions(sourceMap: false, summarizeApi: false); |
| 163 |
| 164 var filePath = path.join(inputDir, 'packages', 'matcher', 'matcher.dart'); |
| 165 var contents = new File(filePath).readAsStringSync(); |
| 166 |
| 167 // Collect any other files we've imported. |
| 168 var files = new Set<String>(); |
| 169 _collectTransitiveImports(contents, files, from: filePath); |
| 170 |
| 171 var unit = new BuildUnit('matcher', files.toList(), _moduleForLibrary); |
| 172 var module = compiler.compile(unit, options); |
| 173 |
| 174 var outPath = path.join(expectDir, 'matcher', 'matcher'); |
| 175 _writeModule(outPath, module); |
| 176 } |
| 177 |
| 178 String _moduleForLibrary(Source source) { |
| 179 var scheme = source.uri.scheme; |
| 180 if (scheme == 'package') { |
| 181 return source.uri.pathSegments.first; |
| 182 } |
| 183 throw new Exception('Module not found for library "${source.fullName}"'); |
| 184 } |
| 185 |
| 186 /// Expands wacky multitests into a bunch of test files. |
| 187 /// |
| 188 /// We'll compile each one as if it was an input. |
| 189 /// NOTE: this will write the individual test files to disk. |
| 190 Set<String> expandMultiTests(List testDirs, RegExp filePattern) { |
| 191 var multitests = new Set<String>(); |
| 192 |
| 193 for (var testDir in testDirs) { |
| 194 var fullDir = path.join(inputDir, testDir); |
| 195 var testFiles = _findTests(fullDir, filePattern); |
| 196 |
| 197 for (var filePath in testFiles) { |
| 198 if (filePath.endsWith('_multi.dart')) continue; |
| 199 |
| 200 var contents = new File(filePath).readAsStringSync(); |
| 201 if (isMultiTest(contents)) { |
| 202 multitests.add(filePath); |
| 203 |
| 204 var tests = new Map<String, String>(); |
| 205 var outcomes = new Map<String, Set<String>>(); |
| 206 extractTestsFromMultitest(filePath, contents, tests, outcomes); |
| 207 |
| 208 var filename = path.basenameWithoutExtension(filePath); |
| 209 tests.forEach((name, contents) { |
| 210 new File(path.join(fullDir, '${filename}_${name}_multi.dart')) |
| 211 .writeAsStringSync(contents); |
| 212 }); |
| 213 } |
274 } | 214 } |
275 }); | 215 } |
276 } | 216 return multitests; |
277 | 217 } |
278 /// An implementation of analysis engine's [Logger] that prints. | 218 |
279 class PrintLogger implements Logger { | 219 // TODO(jmesserly): switch this to a .packages file. |
280 @override | 220 final packageUrlMappings = { |
281 void logError(String message, [CaughtException exception]) { | 221 'package:expect/expect.dart': path.join(inputDir, 'expect.dart'), |
282 print('[AnalysisEngine] error $message $exception'); | 222 'package:async_helper/async_helper.dart': |
283 } | 223 path.join(inputDir, 'async_helper.dart'), |
284 | 224 'package:unittest/unittest.dart': path.join(inputDir, 'unittest.dart'), |
285 void logInformation(String message, [CaughtException exception]) {} | 225 'package:js/js.dart': path.join(inputDir, 'packages', 'js', 'js.dart') |
286 void logInformation2(String message, Object exception) {} | 226 }; |
287 } | 227 |
| 228 final codeCoverage = Platform.environment.containsKey('COVERALLS_TOKEN'); |
| 229 |
| 230 final inputDir = path.join(testDirectory, 'codegen'); |
| 231 |
| 232 Iterable<String> _findTests(String dir, RegExp filePattern) { |
| 233 var files = new Directory(dir) |
| 234 .listSync() |
| 235 .where((f) => f is File) |
| 236 .map((f) => f.path) |
| 237 .where((p) => p.endsWith('.dart') && filePattern.hasMatch(p)); |
| 238 if (dir != inputDir) { |
| 239 files = files |
| 240 .where((p) => p.endsWith('_test.dart') || p.endsWith('_multi.dart')); |
| 241 } |
| 242 return files; |
| 243 } |
| 244 |
| 245 /// Parse directives from [contents] and find the complete set of transitive |
| 246 /// imports, reading files as needed. |
| 247 /// |
| 248 /// This will not include dart:* libraries, as those are implicitly available. |
| 249 void _collectTransitiveImports(String contents, Set<String> libraries, |
| 250 {String from}) { |
| 251 if (!libraries.add(from)) return; |
| 252 |
| 253 var unit = parseDirectives(contents, name: from, suppressErrors: true); |
| 254 for (var d in unit.directives) { |
| 255 if (d is ImportDirective || d is ExportDirective) { |
| 256 String uri = _resolveDirective(d); |
| 257 if (uri == null || |
| 258 uri.startsWith('dart:') || |
| 259 uri.startsWith('package:')) { |
| 260 continue; |
| 261 } |
| 262 |
| 263 var f = new File(path.join(path.dirname(from), uri)); |
| 264 if (f.existsSync()) { |
| 265 _collectTransitiveImports(f.readAsStringSync(), libraries, |
| 266 from: f.path); |
| 267 } |
| 268 } |
| 269 } |
| 270 } |
| 271 |
| 272 /// Simplified from ParseDartTask.resolveDirective. |
| 273 String _resolveDirective(UriBasedDirective directive) { |
| 274 StringLiteral uriLiteral = directive.uri; |
| 275 String uriContent = uriLiteral.stringValue; |
| 276 if (uriContent != null) { |
| 277 uriContent = uriContent.trim(); |
| 278 directive.uriContent = uriContent; |
| 279 } |
| 280 return directive.validate() == null ? uriContent : null; |
| 281 } |
OLD | NEW |