// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// Tests code generation. /// /// Runs Dart Dev Compiler on all input in the `codegen` directory and checks /// that the output is what we expected. library dev_compiler.test.codegen_test; // TODO(rnystrom): This doesn't actually run any tests any more. It just // compiles stuff. This should be changed to not use unittest and just be a // regular program that outputs files. import 'dart:convert'; import 'dart:io' show Directory, File, Platform; import 'package:analyzer/analyzer.dart' show StringLiteral, parseDirectives; import 'package:analyzer/src/command_line/arguments.dart' show defineAnalysisArguments; import 'package:analyzer/src/dart/ast/ast.dart'; import 'package:analyzer/src/generated/source.dart' show Source; import 'package:args/args.dart' show ArgParser, ArgResults; import 'package:dev_compiler/src/analyzer/context.dart'; import 'package:dev_compiler/src/analyzer/module_compiler.dart' show BuildUnit, CompilerOptions, JSModuleFile, ModuleCompiler; import 'package:dev_compiler/src/compiler/module_builder.dart' show ModuleFormat, addModuleFormatOptions, parseModuleFormatOption; import 'package:path/path.dart' as path; import 'package:test/test.dart' show expect, isFalse, isTrue, test; import 'package:status_file/expectation.dart'; import 'package:test_dart/path.dart' as test_dart; import 'package:test_dart/test_suite.dart' show StandardTestSuite; import 'package:test_dart/options.dart'; import '../tool/build_sdk.dart' as build_sdk; import 'multitest.dart' show extractTestsFromMultitest, isMultiTest; import 'testing.dart' show repoDirectory, testDirectory; final ArgParser argParser = new ArgParser() ..addOption('dart-sdk', help: 'Dart SDK Path', defaultsTo: null); /// The `test/codegen` directory. final codegenDir = path.join(testDirectory, 'codegen'); /// The `test/codegen/expect` directory. final codegenExpectDir = path.join(testDirectory, 'codegen_expected'); /// The generated directory where tests, expanded multitests, and other test /// support libraries are copied to. /// /// The tests sometimes import utility libraries using a relative path. /// Likewise, the multitests do too, and one multitest even imports its own /// non-expanded form (!). To make that simpler, we copy the entire test tree /// to a generated directory and expand that multitests in there too. final codegenTestDir = path.join(repoDirectory, 'gen', 'codegen_tests'); /// The generated directory where tests and packages compiled to JS are /// output. final codegenOutputDir = path.join(repoDirectory, 'gen', 'codegen_output'); final codeCoverage = Platform.environment.containsKey('COVERALLS_TOKEN'); RegExp filePattern; main(List arguments) { if (arguments == null) arguments = []; ArgResults args = argParser.parse(arguments); filePattern = new RegExp(args.rest.length > 0 ? args.rest[0] : '.'); var sdkDir = path.join(repoDirectory, 'gen', 'patched_sdk'); var sdkSummaryFile = path.join(testDirectory, '..', 'gen', 'sdk', 'ddc_sdk.sum'); var summaryPaths = new Directory(path.join(codegenOutputDir, 'pkg')) .listSync() .map((e) => e.path) .where((p) => p.endsWith('.sum')) .toList(); var sharedCompiler = new ModuleCompiler(new AnalyzerOptions.basic( dartSdkSummaryPath: sdkSummaryFile, summaryPaths: summaryPaths)); var testDirs = ['language', 'corelib_2', 'lib']; // Copy all of the test files and expanded multitest files to // gen/codegen_tests. We'll compile from there. var testFiles = _setUpTests(testDirs); _writeRuntimeStatus(testFiles); // Our default compiler options. Individual tests can override these. var defaultOptions = ['--no-source-map', '--no-summarize']; var compileArgParser = new ArgParser(); defineAnalysisArguments(compileArgParser, ddc: true); AnalyzerOptions.addArguments(compileArgParser); CompilerOptions.addArguments(compileArgParser); addModuleFormatOptions(compileArgParser); var testFileOptionsMatcher = new RegExp(r'// (compile options: |SharedOptions=)(.*)', multiLine: true); // Ignore dart2js options that we don't support in DDC. var ignoreOptions = [ '--enable-enum', '--experimental-trust-js-interop-type-annotations', '--trust-type-annotations', '--supermixin' ]; // Compile each test file to JS and put the result in gen/codegen_output. testFiles.forEach((testFile, status) { var relativePath = path.relative(testFile, from: codegenTestDir); // Only compile the top-level files for generating coverage. bool isTopLevelTest = path.dirname(relativePath) == "."; if (codeCoverage && !isTopLevelTest) return; if (status.contains(Expectation.skip) || status.contains(Expectation.skipByDesign)) { return; } var name = path.withoutExtension(relativePath); test('dartdevc $name', () { // Check if we need to use special compile options. var contents = new File(testFile).readAsStringSync(); var match = testFileOptionsMatcher.firstMatch(contents); var args = defaultOptions.toList(); if (match != null) { var matchedArgs = match.group(2).split(' '); args.addAll(matchedArgs.where((s) => !ignoreOptions.contains(s))); } ArgResults argResults = compileArgParser.parse(args); var analyzerOptions = new AnalyzerOptions.fromArguments(argResults, dartSdkSummaryPath: sdkSummaryFile, summaryPaths: summaryPaths); var options = new CompilerOptions.fromArguments(argResults); var moduleFormat = parseModuleFormatOption(argResults).first; // Collect any other files we've imported. var files = new Set(); _collectTransitiveImports(contents, files, from: testFile); var unit = new BuildUnit( name, path.dirname(testFile), files.toList(), _moduleForLibrary); var compiler = sharedCompiler; if (analyzerOptions.declaredVariables.isNotEmpty) { compiler = new ModuleCompiler(analyzerOptions); } JSModuleFile module = null; var exception, stackTrace; try { module = compiler.compile(unit, options); } catch (e, st) { exception = e; stackTrace = st; } // This covers tests where the intent of the test is to validate that // some static error is produced. var intentionalCompileError = (contents.contains(': compile-time error') || // Use adjacent strings so test.dart doesn't match this line // as an expected compile error. contents.contains('/*@' 'compile-error=')) && !status.contains(Expectation.missingCompileTimeError); var crashing = status.contains(Expectation.crash); if (module == null) { expect(crashing, isTrue, reason: "test $name crashes during compilation.\n" "$exception\n$stackTrace"); return; } // Write out JavaScript and/or compilation errors/warnings. _writeModule( path.join(codegenOutputDir, name), isTopLevelTest ? path.join(codegenExpectDir, name) : null, moduleFormat, module); expect(crashing, isFalse, reason: "test $name no longer crashes."); var knownCompileError = status.contains(Expectation.compileTimeError) || status.contains(Expectation.fail); // TODO(jmesserly): we could also invert negative_test, however analyzer // in test.dart does not do this. // name.endsWith('negative_test') && !status.contains(Expectation.fail) if (module.isValid) { expect(knownCompileError, isFalse, reason: "test $name expected static errors, but compiled."); } else { var reason = intentionalCompileError ? "intended" : "unexpected"; expect(intentionalCompileError || knownCompileError, isTrue, reason: "test $name failed to compile due to $reason errors:" "\n\n${module.errors.join('\n')}."); } }); }); if (filePattern.hasMatch('sunflower')) { test('sunflower', () { _buildSunflower(sharedCompiler, codegenOutputDir, codegenExpectDir); }); } if (codeCoverage) { test('build_sdk code coverage', () { return build_sdk.main(['--dart-sdk', sdkDir, '-o', codegenOutputDir]); }); } } void _writeModule(String outPath, String expectPath, ModuleFormat format, JSModuleFile result) { _ensureDirectory(path.dirname(outPath)); String errors = result.errors.join('\n'); if (errors.isNotEmpty && !errors.endsWith('\n')) errors += '\n'; new File(outPath + '.txt').writeAsStringSync(errors); if (result.isValid) { result.writeCodeSync(format, outPath + '.js'); } if (result.summaryBytes != null) { new File(outPath + '.sum').writeAsBytesSync(result.summaryBytes); } // Write the expectation file if needed. // Generally speaking we try to avoid these tests, but they are occasionally // useful. if (expectPath != null) { _ensureDirectory(path.dirname(expectPath)); var expectFile = new File(expectPath + '.js'); if (result.isValid) { result.writeCodeSync(format, expectFile.path); } else { expectFile.writeAsStringSync("//FAILED TO COMPILE"); } } } void _buildSunflower( ModuleCompiler compiler, String outputDir, String expectDir) { var baseDir = path.join(codegenDir, 'sunflower'); var files = ['sunflower', 'circle', 'painter'] .map((f) => path.join(baseDir, '$f.dart')) .toList(); var input = new BuildUnit('sunflower', baseDir, files, _moduleForLibrary); var options = new CompilerOptions(summarizeApi: false); var built = compiler.compile(input, options); _writeModule(path.join(outputDir, 'sunflower', 'sunflower'), path.join(expectDir, 'sunflower', 'sunflower'), ModuleFormat.amd, built); } String _moduleForLibrary(Source source) { var scheme = source.uri.scheme; if (scheme == 'package') { return source.uri.pathSegments.first; } throw new Exception('Module not found for library "${source.fullName}"'); } void _writeRuntimeStatus(Map> testFiles) { var runtimeStatus = {}; testFiles.forEach((name, status) { name = path.withoutExtension(path.relative(name, from: codegenTestDir)); // Skip tests that we don't expect to compile. if (status.contains(Expectation.compileTimeError) || status.contains(Expectation.crash) || status.contains(Expectation.skip) || status.contains(Expectation.fail) || status.contains(Expectation.skipByDesign)) { return; } // Normalize the expectations for the Karma language_test.js runner. if (status.remove(Expectation.ok)) assert(status.isNotEmpty); if (status.remove(Expectation.missingCompileTimeError) || status.remove(Expectation.missingRuntimeError)) { status.add(Expectation.pass); } // Don't include status for passing tests, as that is the default. // TODO(jmesserly): we could record these for extra sanity checks. if (status.length == 1 && status.contains(Expectation.pass)) { return; } runtimeStatus[name] = status.map((s) => '$s').join(','); }); new File(path.join(codegenOutputDir, 'test_status.js')).writeAsStringSync(''' define([], function() { 'use strict'; return ${new JsonEncoder.withIndent(' ').convert(runtimeStatus)}; }); '''); } Map> _setUpTests(List testDirs) { var testFiles = >{}; for (var testDir in testDirs) { // TODO(rnystrom): Simplify this when the Dart 2.0 test migration is // complete (#30183). // Look for the tests in the "_strong" and "_2" directories in the SDK's // main "tests" directory. var dirParts = path.split(testDir); for (var suffix in const ["_2", "_strong"]) { var sdkTestDir = path.join( 'tests', dirParts[0] + suffix, path.joinAll(dirParts.skip(1))); var inputPath = path.join(testDirectory, '..', '..', '..', sdkTestDir); if (!new Directory(inputPath).existsSync()) continue; var browsers = Platform.environment['DDC_BROWSERS']; var runtime = browsers == 'Firefox' ? 'firefox' : 'chrome'; var config = new OptionsParser() .parse('-m release -c dartdevc --use-sdk --strong'.split(' ') ..addAll(['-r', runtime, '--suite_dir', sdkTestDir])) .single; var testSuite = new StandardTestSuite.forDirectory( config, new test_dart.Path(sdkTestDir)); var expectations = testSuite.readExpectations(); for (var file in _listFiles(inputPath, recursive: true)) { var relativePath = path.relative(file, from: inputPath); var outputPath = path.join(codegenTestDir, testDir, relativePath); _ensureDirectory(path.dirname(outputPath)); if (file.endsWith("_test.dart")) { var statusPath = path.withoutExtension(relativePath); void _writeTest(String outputPath, String contents) { if (contents.contains('package:unittest/')) { // TODO(jmesserly): we could use directive parsing, but that // feels like overkill. // Alternatively, we could detect "unittest" use at runtime. // We really need a better solution for Karma+mocha+unittest // integration. contents += '\nfinal _usesUnittestPackage = true;\n'; } new File(outputPath).writeAsStringSync(contents); } var contents = new File(file).readAsStringSync(); if (isMultiTest(contents)) { // It's a multitest, so expand it and add all of the variants. var tests = {}; extractTestsFromMultitest(file, contents, tests); var fileName = path.basenameWithoutExtension(file); var outputDir = path.dirname(outputPath); tests.forEach((name, contents) { var multiFile = path.join(outputDir, '${fileName}_${name}_multi.dart'); testFiles[multiFile] = expectations.expectations("$statusPath/$name"); _writeTest(multiFile, contents); }); } else { // It's a single test suite. testFiles[outputPath] = expectations.expectations(statusPath); } // Write the test file. // // We do this even for multitests because import_self_test // is a multitest, yet imports its own unexpanded form (!). _writeTest(outputPath, contents); } else { // Copy the non-test file over, in case it is used as an import. new File(file).copySync(outputPath); } } } } // Also include the other special files that live at the top level directory. for (var file in _listFiles(codegenDir)) { var relativePath = path.relative(file, from: codegenDir); var outputPath = path.join(codegenTestDir, relativePath); new File(file).copySync(outputPath); if (file.endsWith(".dart")) { testFiles[outputPath] = new Set()..add(Expectation.pass); } } return testFiles; } /// Recursively creates [dir] if it doesn't exist. void _ensureDirectory(String dir) { new Directory(dir).createSync(recursive: true); } /// Lists all of the files within [dir] that match [filePattern]. Iterable _listFiles(String dir, {bool recursive: false}) { return new Directory(dir) .listSync(recursive: recursive, followLinks: false) .where((e) => e is File && filePattern.hasMatch(e.path)) .map((f) => f.path); } /// Parse directives from [contents] and find the complete set of transitive /// imports, reading files as needed. /// /// This will not include dart:* libraries, as those are implicitly available. void _collectTransitiveImports(String contents, Set libraries, {String packageRoot, String from}) { var uri = from; if (packageRoot != null && path.isWithin(packageRoot, from)) { uri = 'package:${path.relative(from, from: packageRoot)}'; } if (!libraries.add(uri)) return; var unit = parseDirectives(contents, name: from, suppressErrors: true); for (var d in unit.directives) { if (d is NamespaceDirectiveImpl) { String uri = _resolveDirective(d); if (uri == null || uri.startsWith('dart:') || uri.startsWith('package:')) { continue; } var f = new File(path.join(path.dirname(from), uri)); if (f.existsSync()) { _collectTransitiveImports(f.readAsStringSync(), libraries, packageRoot: packageRoot, from: f.path); } } } } /// Simplified from ParseDartTask.resolveDirective. String _resolveDirective(NamespaceDirectiveImpl directive) { StringLiteral uriLiteral = directive.uri; String uriContent = uriLiteral.stringValue; if (uriContent != null) { uriContent = uriContent.trim(); directive.uriContent = uriContent; } return directive.validate() == null ? uriContent : null; }