blob: 4c2a4130254d434b33ff0c35814ab5733aa66e93 [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 ddc.
///
/// This is a shell that runs multiple tests, one per folder under `data/`.
import 'dart:io';
import 'package:modular_test/src/io_pipeline.dart';
import 'package:modular_test/src/pipeline.dart';
import 'package:modular_test/src/suite.dart';
import 'package:modular_test/src/runner.dart';
// TODO(vsm): Hack until we have either:
// (1) an NNBD version of the below
// (2) the ability to compile the primary version with NNBD
List<String> _nnbdOptOut = ['sdk'];
String _test_package = 'ddc_modular_test';
Uri sdkRoot = Platform.script.resolve("../../../");
Options _options;
String _dartdevcScript;
String _buildSdkScript;
String _patchSdkScript;
String _sdkDevRuntime;
String _sdkDevRuntimeNnbd;
main(List<String> args) async {
_options = Options.parse(args);
await _resolveScripts();
await runSuite(
sdkRoot.resolve('tests/modular/'),
'tests/modular',
_options,
IOPipeline([
DDCStep(),
RunD8(),
], cacheSharedModules: true));
// DDC only test suite.
await runSuite(
sdkRoot.resolve('tests/compiler/dartdevc/modular/'),
'tests/compiler/dartdevc/modular',
_options,
IOPipeline([
DDCStep(),
RunD8(),
], cacheSharedModules: true));
}
const sumId = DataId("sum");
const jsId = DataId("js");
const txtId = DataId("txt");
class DDCStep implements IOModularStep {
@override
List<DataId> get resultData => const [sumId, jsId];
@override
bool get needsSources => true;
@override
List<DataId> get dependencyDataNeeded => const [sumId];
@override
List<DataId> get moduleDataNeeded => const [];
@override
bool get onlyOnMain => false;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: ddc on $module");
Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
await _createPackagesFile(module, root, transitiveDependencies);
ProcessResult result;
bool nnbd = flags.contains('non-nullable');
bool allowErrors = nnbd && _nnbdOptOut.contains(module.name);
if (module.isSdk) {
assert(transitiveDependencies.isEmpty);
// Apply patches.
result = await _runProcess(
Platform.resolvedExecutable,
[
_patchSdkScript,
sdkRoot.toFilePath(),
if (nnbd) _sdkDevRuntimeNnbd else _sdkDevRuntime,
'patched_sdk',
if (nnbd) 'sdk_nnbd'
],
root.toFilePath());
_checkExitCode(result, this, module);
// Build the SDK.
result = await _runProcess(
Platform.resolvedExecutable,
[
_buildSdkScript,
'--dart-sdk',
'patched_sdk',
'--dart-sdk-summary=build',
'--summary-out',
'${toUri(module, sumId)}',
'--modules=es6',
if (allowErrors) '--unsafe-force-compile',
for (String flag in flags) '--enable-experiment=$flag',
'-o',
'${toUri(module, jsId)}',
],
root.toFilePath());
_checkExitCode(result, this, module);
} else {
Module sdkModule = module.dependencies.firstWhere((m) => m.isSdk);
List<String> sources = module.sources
.map((relativeUri) => _sourceToImportUri(module, relativeUri))
.toList();
Map<String, String> _urlMappings = {};
if (!module.isPackage) {
for (var source in module.sources) {
var importUri = _sourceToImportUri(module, source);
_urlMappings[importUri] = '$source';
}
}
for (var dependency in transitiveDependencies) {
if (!dependency.isPackage && !dependency.isSdk) {
for (var source in dependency.sources) {
var importUri = _sourceToImportUri(dependency, source);
_urlMappings[importUri] = '$source';
}
}
}
var extraArgs = [
'--dart-sdk-summary',
'${toUri(sdkModule, sumId)}',
'--packages',
'.packages',
];
Uri output = toUri(module, jsId);
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/.packages',
_dartdevcScript,
'--modules=es6',
'--summarize',
'--no-source-map',
...sources,
if (_urlMappings.isNotEmpty)
'--url-mapping=${_urlMappings.entries.map((entry) => '${entry.key},${entry.value}').join(',')}',
...extraArgs,
for (String flag in flags) '--enable-experiment=$flag',
...(transitiveDependencies
.where((m) => !m.isSdk)
.expand((m) => ['-s', '${toUri(m, sumId)}'])),
'-o',
'$output',
];
result = await _runProcess(
Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("\ncached step: ddc on $module");
}
}
class RunD8 implements IOModularStep {
@override
List<DataId> get resultData => const [txtId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [jsId];
@override
List<DataId> get moduleDataNeeded => const [jsId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: d8 on $module");
// Rename sdk.js to dart_sdk.js (the alternative, but more hermetic solution
// would be to rename the import on all other .js files, but seems
// overkill/unnecessary.
if (await File.fromUri(root.resolve('dart_sdk.js')).exists()) {
throw 'error: dart_sdk.js already exists.';
}
await File.fromUri(root.resolve('sdk.js'))
.copy(root.resolve('dart_sdk.js').toFilePath());
var runjs = '''
import { dart, _isolate_helper } from 'dart_sdk.js';
import { main } from 'main.js';
_isolate_helper.startRootIsolate(() => {}, []);
main.main();
''';
var wrapper =
root.resolveUri(toUri(module, jsId)).toFilePath() + ".wrapper.js";
await File(wrapper).writeAsString(runjs);
List<String> d8Args = ['--module', wrapper];
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 as String);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("\ncached 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) {
throw "${step.runtimeType} failed on $module:\n\n"
"stdout:\n${result.stdout}\n\n"
"stderr:\n${result.stderr}";
}
}
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 UnsupportedError('Unsupported platform.');
}
Future<void> _createPackagesFile(
Module module, Uri root, Set<Module> transitiveDependencies) async {
// 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 = StringBuffer();
if (module.isPackage) {
packagesContents.write('${module.name}:${module.packageBase}\n');
}
for (Module dependency in transitiveDependencies) {
if (dependency.isPackage) {
packagesContents.write('${dependency.name}:unused\n');
}
}
await File.fromUri(root.resolve('.packages'))
.writeAsString('$packagesContents');
}
String _sourceToImportUri(Module module, 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 'package:${_test_package}/$relativeUri';
}
}
Future<void> _resolveScripts() async {
Future<String> resolve(String sdkSourcePath,
[String relativeSnapshotPath]) async {
String result = sdkRoot.resolve(sdkSourcePath).toFilePath();
if (_options.useSdk && relativeSnapshotPath != null) {
String snapshot = Uri.file(Platform.resolvedExecutable)
.resolve(relativeSnapshotPath)
.toFilePath();
if (await File(snapshot).exists()) {
return snapshot;
}
}
return result;
}
_dartdevcScript = await resolve(
'pkg/dev_compiler/bin/dartdevc.dart', 'snapshots/dartdevc.dart.snapshot');
_buildSdkScript = await resolve('pkg/dev_compiler/tool/build_sdk.dart');
_patchSdkScript = await resolve('pkg/dev_compiler/tool/patch_sdk.dart');
_sdkDevRuntime = await resolve('sdk/lib/_internal/js_dev_runtime');
_sdkDevRuntimeNnbd = await resolve('sdk_nnbd/lib/_internal/js_dev_runtime');
}