blob: 2e37025d545721c7561647814d220aa43b502b67 [file] [log] [blame]
// Copyright (c) 2019, 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.
/// Test the modular compilation pipeline of dart2js.
///
/// This is a shell that runs multiple tests, one per folder under `data/`.
import 'dart:io';
import 'package:args/args.dart';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:front_end/src/compute_platform_binaries_location.dart'
show computePlatformBinariesLocation;
import 'package:modular_test/src/io_pipeline.dart';
import 'package:modular_test/src/loader.dart';
import 'package:modular_test/src/pipeline.dart';
import 'package:modular_test/src/suite.dart';
_Options _options;
main(List<String> args) {
_options = _Options.parse(args);
asyncTest(() async {
var baseUri = Platform.script.resolve('data/');
var baseDir = Directory.fromUri(baseUri);
var pipeline = new IOPipeline([
SourceToDillStep(),
GlobalAnalysisStep(),
Dart2jsBackendStep(),
RunD8(),
], cacheSharedModules: true);
await for (var entry in baseDir.list(recursive: false)) {
if (entry is Directory) {
await _runTest(pipeline, entry.uri, baseUri);
}
}
await pipeline.cleanup();
});
}
Future<void> _runTest(IOPipeline pipeline, Uri uri, Uri baseDir) async {
var dirName = uri.path.substring(baseDir.path.length);
if (_options.filter != null && !dirName.contains(_options.filter)) {
if (_options.showSkipped) print("skipped: $dirName");
return;
}
print("testing: $dirName");
ModularTest test = await loadTest(uri);
if (_options.verbose) print(test.debugString());
await pipeline.run(test);
}
const dillId = const DataId("dill");
const updatedDillId = const DataId("udill");
const globalDataId = const DataId("gdata");
const jsId = const DataId("js");
const txtId = const DataId("txt");
// Step that compiles sources in a module to a .dill file.
class SourceToDillStep implements IOModularStep {
@override
List<DataId> get resultData => const [dillId];
@override
bool get needsSources => true;
@override
List<DataId> get dependencyDataNeeded => const [dillId];
@override
List<DataId> get moduleDataNeeded => const [];
@override
bool get onlyOnMain => false;
@override
Future<void> execute(
Module module, Uri root, ModuleDataToRelativeUri toUri) async {
if (_options.verbose) print("step: source-to-dill on $module");
// We use non file-URI schemes for representeing source locations in a
// root-agnostic way. This allows us to refer to file across modules and
// across steps without exposing the underlying temporary folders that are
// created by the framework. In build systems like bazel this is especially
// important because each step may be run on a different machine.
//
// Files in packages are defined in terms of `package:` URIs, while
// non-package URIs are defined using the `dart-dev-app` scheme.
String rootScheme = 'dev-dart-app';
String sourceToImportUri(Uri relativeUri) {
if (module.isPackage) {
var basePath = module.packageBase.path;
var packageRelativePath = basePath == "./"
? relativeUri.path
: relativeUri.path.substring(basePath.length);
return 'package:${module.name}/$packageRelativePath';
} else {
return '$rootScheme:/$relativeUri';
}
}
// We create a .packages file which defines the location of this module if
// it is a package. The CFE requires that if a `package:` URI of a
// dependency is used in an import, then we need that package entry in the
// .packages file. However, after it checks that the definition exists, the
// CFE will not actually use the resolved URI if a library for the import
// URI is already found in one of the provided .dill files of the
// dependencies. For that reason, and to ensure that a step only has access
// to the files provided in a module, we generate a .packages with invalid
// folders for other packages.
// TODO(sigmund): follow up with the CFE to see if we can remove the need
// for the .packages entry altogether if they won't need to read the
// sources.
var packagesContents = new StringBuffer();
if (module.isPackage) {
packagesContents.write('${module.name}:${module.packageBase}\n');
}
Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
for (Module dependency in transitiveDependencies) {
if (dependency.isPackage) {
packagesContents.write('${dependency.name}:unused\n');
}
}
await File.fromUri(root.resolve('.packages'))
.writeAsString('$packagesContents');
var sdkRoot = Platform.script.resolve("../../../../");
var platform =
computePlatformBinariesLocation().resolve("dart2js_platform.dill");
List<String> workerArgs = [
sdkRoot.resolve("utils/bazel/kernel_worker.dart").toFilePath(),
'--no-summary-only',
'--target',
'dart2js',
'--multi-root',
'$root',
'--multi-root-scheme',
rootScheme,
'--dart-sdk-summary',
'${platform}',
'--output',
'${toUri(module, dillId)}',
'--packages-file',
'$rootScheme:/.packages',
...(transitiveDependencies
.expand((m) => ['--input-linked', '${toUri(m, dillId)}'])),
...(module.sources.expand((uri) => ['--source', sourceToImportUri(uri)])),
];
var result = await _runProcess(
Platform.resolvedExecutable, workerArgs, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("cached step: source-to-dill on $module");
}
}
// Step that invokes the dart2js global analysis on the main module by providing
// the .dill files of all transitive modules as inputs.
class GlobalAnalysisStep implements IOModularStep {
@override
List<DataId> get resultData => const [globalDataId, updatedDillId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [dillId];
@override
List<DataId> get moduleDataNeeded => const [dillId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(
Module module, Uri root, ModuleDataToRelativeUri toUri) async {
if (_options.verbose) print("step: dart2js global analysis on $module");
Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
Iterable<String> dillDependencies =
transitiveDependencies.map((m) => '${toUri(m, dillId)}');
var sdkRoot = Platform.script.resolve("../../../../");
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/.packages',
'package:compiler/src/dart2js.dart',
'${toUri(module, dillId)}',
'--dill-dependencies=${dillDependencies.join(',')}',
'--write-data=${toUri(module, globalDataId)}',
'--out=${toUri(module, updatedDillId)}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose)
print("cached step: dart2js global analysis on $module");
}
}
// Step that invokes the dart2js backend on the main module given the results of
// the global analysis step.
class Dart2jsBackendStep implements IOModularStep {
@override
List<DataId> get resultData => const [jsId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [];
@override
List<DataId> get moduleDataNeeded => const [updatedDillId, globalDataId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(
Module module, Uri root, ModuleDataToRelativeUri toUri) async {
if (_options.verbose) print("step: dart2js backend on $module");
var sdkRoot = Platform.script.resolve("../../../../");
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/.packages',
'package:compiler/src/dart2js.dart',
'${toUri(module, updatedDillId)}',
'--read-data=${toUri(module, globalDataId)}',
'--out=${toUri(module, jsId)}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("cached step: dart2js backend on $module");
}
}
/// Step that runs the output of dart2js in d8 and saves the output.
class RunD8 implements IOModularStep {
@override
List<DataId> get resultData => const [txtId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [];
@override
List<DataId> get moduleDataNeeded => const [jsId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(
Module module, Uri root, ModuleDataToRelativeUri toUri) async {
if (_options.verbose) print("step: d8 on $module");
var sdkRoot = Platform.script.resolve("../../../../");
List<String> d8Args = [
sdkRoot
.resolve('sdk/lib/_internal/js_runtime/lib/preambles/d8.js')
.toFilePath(),
root.resolveUri(toUri(module, jsId)).toFilePath(),
];
var result = await _runProcess(
sdkRoot.resolve(_d8executable).toFilePath(), d8Args, root.toFilePath());
_checkExitCode(result, this, module);
await File.fromUri(root.resolveUri(toUri(module, txtId)))
.writeAsString(result.stdout);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("cached step: d8 on $module");
}
}
void _checkExitCode(ProcessResult result, IOModularStep step, Module module) {
if (result.exitCode != 0 || _options.verbose) {
stdout.write(result.stdout);
stderr.write(result.stderr);
}
if (result.exitCode != 0) {
exitCode = result.exitCode;
Expect.fail("${step.runtimeType} failed on $module");
}
}
Future<ProcessResult> _runProcess(
String command, List<String> arguments, String workingDirectory) {
if (_options.verbose) {
print('command:\n$command ${arguments.join(' ')} from $workingDirectory');
}
return Process.run(command, arguments, workingDirectory: workingDirectory);
}
String get _d8executable {
if (Platform.isWindows) {
return 'third_party/d8/windows/d8.exe';
} else if (Platform.isLinux) {
return 'third_party/d8/linux/d8';
} else if (Platform.isMacOS) {
return 'third_party/d8/macos/d8';
}
throw new UnsupportedError('Unsupported platform.');
}
class _Options {
bool showSkipped = false;
bool verbose = false;
String filter = null;
static _Options parse(List<String> args) {
var parser = new ArgParser()
..addFlag('verbose',
abbr: 'v',
defaultsTo: false,
help: "print detailed information about the test and modular steps")
..addFlag('show-skipped',
defaultsTo: false,
help: "print the name of the tests skipped by the filtering option")
..addOption('filter',
help: "only run tests containing this filter as a substring");
ArgResults argResults = parser.parse(args);
return _Options()
..showSkipped = argResults['show-skipped']
..verbose = argResults['verbose']
..filter = argResults['filter'];
}
}