Add dart2js modular test suite under a unit test.

At this time this only compiles to .dill and then compiles from multiple modular
dill files.

Next steps: add the global split and add the modular data bits

Change-Id: I3399776dbd5187ddb0a3e2fdd307622a736570c9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102126
Commit-Queue: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/modular_test/lib/src/loader.dart b/pkg/modular_test/lib/src/loader.dart
index 8642fd8..ce00e49 100644
--- a/pkg/modular_test/lib/src/loader.dart
+++ b/pkg/modular_test/lib/src/loader.dart
@@ -111,7 +111,10 @@
   Directory folder = Directory.fromUri(root);
   int baseUriPrefixLength = folder.parent.uri.path.length;
   await for (var file in folder.list(recursive: true)) {
-    sources.add(Uri.parse(file.uri.path.substring(baseUriPrefixLength)));
+    var path = file.uri.path;
+    if (path.endsWith('.dart')) {
+      sources.add(Uri.parse(path.substring(baseUriPrefixLength)));
+    }
   }
   return sources..sort((a, b) => a.path.compareTo(b.path));
 }
diff --git a/pkg/modular_test/lib/src/suite.dart b/pkg/modular_test/lib/src/suite.dart
index e5f17a4..a96e8c8 100644
--- a/pkg/modular_test/lib/src/suite.dart
+++ b/pkg/modular_test/lib/src/suite.dart
@@ -15,6 +15,8 @@
 
   ModularTest(this.modules, this.mainModule)
       : assert(mainModule != null && modules.length > 0);
+
+  String debugString() => modules.map((m) => m.debugString()).join('\n');
 }
 
 /// A single module in a modular test.
@@ -60,6 +62,17 @@
 
   @override
   String toString() => '[module $name]';
+
+  String debugString() {
+    var buffer = new StringBuffer();
+    buffer.write('   ');
+    buffer.write(name);
+    buffer.write(': ');
+    buffer.write(isPackage ? 'package' : '(not package)');
+    buffer.write(', deps: {${dependencies.map((d) => d.name).join(", ")}}');
+    buffer.write(', sources: {${sources.map((u) => "$u").join(', ')}}');
+    return '$buffer';
+  }
 }
 
 final RegExp _validModuleName = new RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$');
diff --git a/pkg/modular_test/test/loader/valid_packages/expectation.txt b/pkg/modular_test/test/loader/valid_packages/expectation.txt
index 3c0888d..dab4163 100644
--- a/pkg/modular_test/test/loader/valid_packages/expectation.txt
+++ b/pkg/modular_test/test/loader/valid_packages/expectation.txt
@@ -12,7 +12,6 @@
   (no dependencies)
   lib/js.dart
   lib/js_util.dart
-  lib/src/
   lib/src/varargs.dart
 
 main
diff --git a/tests/compiler/dart2js/dart2js.status b/tests/compiler/dart2js/dart2js.status
index 3df3488..84af435 100644
--- a/tests/compiler/dart2js/dart2js.status
+++ b/tests/compiler/dart2js/dart2js.status
@@ -30,6 +30,7 @@
 model/native_test: Pass, Slow
 model/no_such_method_enabled_test: Pass, Slow
 model/subtype_test: Pass, Slow
+modular/*: Slow, Pass
 packages/*: Skip # Skip packages folder
 rti/rti_emission_test: Pass, Slow
 rti/rti_need0_test: Pass, Slow
diff --git a/tests/compiler/dart2js/modular/data/int_js_number/def.dart b/tests/compiler/dart2js/modular/data/int_js_number/def.dart
new file mode 100644
index 0000000..0fd7377
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/int_js_number/def.dart
@@ -0,0 +1,4 @@
+// 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.
+const int foo = 331;
diff --git a/tests/compiler/dart2js/modular/data/int_js_number/main.dart b/tests/compiler/dart2js/modular/data/int_js_number/main.dart
new file mode 100644
index 0000000..6352c3d
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/int_js_number/main.dart
@@ -0,0 +1,8 @@
+// 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.
+import 'def.dart';
+
+main() {
+  print(const <int>[foo]);
+}
diff --git a/tests/compiler/dart2js/modular/data/int_js_number/modules.yaml b/tests/compiler/dart2js/modular/data/int_js_number/modules.yaml
new file mode 100644
index 0000000..a02b69a
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/int_js_number/modules.yaml
@@ -0,0 +1,8 @@
+# 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.
+#
+# Regression test: integral numbers should be treated as int and not double
+# after serialization across modules.
+dependencies:
+  main: def
diff --git a/tests/compiler/dart2js/modular/data/package_imports/.packages b/tests/compiler/dart2js/modular/data/package_imports/.packages
new file mode 100644
index 0000000..3da0bf2
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/.packages
@@ -0,0 +1,6 @@
+# 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.
+f0:.
+f1:.
+a:a
diff --git a/tests/compiler/dart2js/modular/data/package_imports/a/a.dart b/tests/compiler/dart2js/modular/data/package_imports/a/a.dart
new file mode 100644
index 0000000..3a62c13
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/a/a.dart
@@ -0,0 +1,6 @@
+// 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.
+import 'package:f1/f1.dart';
+
+int z = y + 100;
diff --git a/tests/compiler/dart2js/modular/data/package_imports/f0.dart b/tests/compiler/dart2js/modular/data/package_imports/f0.dart
new file mode 100644
index 0000000..27b7873
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/f0.dart
@@ -0,0 +1,4 @@
+// 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.
+int x = 1;
diff --git a/tests/compiler/dart2js/modular/data/package_imports/f1.dart b/tests/compiler/dart2js/modular/data/package_imports/f1.dart
new file mode 100644
index 0000000..0eeb9f8
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/f1.dart
@@ -0,0 +1,6 @@
+// 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.
+import 'package:f0/f0.dart';
+
+int y = x + 10;
diff --git a/tests/compiler/dart2js/modular/data/package_imports/f3.dart b/tests/compiler/dart2js/modular/data/package_imports/f3.dart
new file mode 100644
index 0000000..72965f8
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/f3.dart
@@ -0,0 +1,6 @@
+// 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.
+import 'package:a/a.dart';
+
+int w = z + 1000;
diff --git a/tests/compiler/dart2js/modular/data/package_imports/main.dart b/tests/compiler/dart2js/modular/data/package_imports/main.dart
new file mode 100644
index 0000000..7dcee7a
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/main.dart
@@ -0,0 +1,10 @@
+// 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.
+import 'f3.dart';
+import 'package:expect/expect.dart';
+
+main() {
+  print(w);
+  Expect.isTrue(w == 1111);
+}
diff --git a/tests/compiler/dart2js/modular/data/package_imports/modules.yaml b/tests/compiler/dart2js/modular/data/package_imports/modules.yaml
new file mode 100644
index 0000000..a8731e9
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/package_imports/modules.yaml
@@ -0,0 +1,13 @@
+# 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 ensuring that the modular compiler works properly with `package:`
+# imports. This test also ensures that the dart2js implementation of the modular
+# test pipeline works as intended. The test is not designed to cover any
+# compiler or language feature explicitly.
+dependencies:
+  main: [f3, expect]
+  f3: a
+  a: f1
+  f1: f0
diff --git a/tests/compiler/dart2js/modular/data/subclass/a/a.dart b/tests/compiler/dart2js/modular/data/subclass/a/a.dart
new file mode 100644
index 0000000..e393550
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/subclass/a/a.dart
@@ -0,0 +1,4 @@
+// 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.
+const x = 1;
diff --git a/tests/compiler/dart2js/modular/data/subclass/f0.dart b/tests/compiler/dart2js/modular/data/subclass/f0.dart
new file mode 100644
index 0000000..113a6ff
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/subclass/f0.dart
@@ -0,0 +1,6 @@
+// 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.
+class A0 {
+  StringBuffer buffer = new StringBuffer()..write("hello ");
+}
diff --git a/tests/compiler/dart2js/modular/data/subclass/f1.dart b/tests/compiler/dart2js/modular/data/subclass/f1.dart
new file mode 100644
index 0000000..00f7709
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/subclass/f1.dart
@@ -0,0 +1,10 @@
+// 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.
+import 'f0.dart';
+
+class B1 extends A0 {
+  A0 get foo => null;
+}
+
+A0 createA0() => new A0();
diff --git a/tests/compiler/dart2js/modular/data/subclass/main.dart b/tests/compiler/dart2js/modular/data/subclass/main.dart
new file mode 100644
index 0000000..090de94
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/subclass/main.dart
@@ -0,0 +1,16 @@
+// 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.
+import 'f1.dart';
+import 'a/a.dart';
+
+class C2 extends B1 {
+  final foo = createA0();
+}
+
+main() {
+  var buffer = new C2().foo.buffer;
+
+  buffer.write('world! $x');
+  print(buffer.toString());
+}
diff --git a/tests/compiler/dart2js/modular/data/subclass/modules.yaml b/tests/compiler/dart2js/modular/data/subclass/modules.yaml
new file mode 100644
index 0000000..70606e9
--- /dev/null
+++ b/tests/compiler/dart2js/modular/data/subclass/modules.yaml
@@ -0,0 +1,9 @@
+# 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 illustrating how the modular pipeline works. This test is not designed to
+# stress any specific feature of the compiler.
+dependencies:
+  f1: f0
+  main: [f1, a]
diff --git a/tests/compiler/dart2js/modular/modular_test.dart b/tests/compiler/dart2js/modular/modular_test.dart
new file mode 100644
index 0000000..b28edbf
--- /dev/null
+++ b/tests/compiler/dart2js/modular/modular_test.dart
@@ -0,0 +1,295 @@
+// 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);
+    await for (var entry in baseDir.list(recursive: false)) {
+      if (entry is Directory) {
+        await _runTest(entry.uri, baseUri);
+      }
+    }
+  });
+}
+
+Future<void> _runTest(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());
+  var pipeline = new IOPipeline([
+    SourceToDillStep(),
+    CompileFromDillStep(),
+    RunD8(),
+  ]);
+
+  await pipeline.run(test);
+}
+
+const dillId = const DataId("dill");
+const jsId = const DataId("js");
+
+// Step that compiles sources in a module to a .dill file.
+class SourceToDillStep implements IOModularStep {
+  @override
+  DataId get resultId => 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);
+  }
+}
+
+// Step that invokes dart2js on the main module by providing the .dill files of
+// all transitive modules as inputs.
+class CompileFromDillStep implements IOModularStep {
+  @override
+  DataId get resultId => jsId;
+
+  @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 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(',')}',
+      '--out=${toUri(module, resultId)}',
+    ];
+    var result =
+        await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
+
+    _checkExitCode(result, this, module);
+  }
+}
+
+/// Step that runs the output of dart2js in d8 and saves the output.
+class RunD8 implements IOModularStep {
+  @override
+  DataId get resultId => const DataId("txt");
+
+  @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, resultId)))
+        .writeAsString(result.stdout);
+  }
+}
+
+/// Helper to compute trnsitive dependencies from [module].
+Set<Module> _computeTransitiveDependencies(Module module) {
+  Set<Module> deps = {};
+  helper(Module m) {
+    if (deps.add(m)) m.dependencies.forEach(helper);
+  }
+
+  module.dependencies.forEach(helper);
+  return deps;
+}
+
+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'];
+  }
+}