Introduce pkg/modular_test: a package to specify modular tests
This initial commit includes: the definitions of a modular test, a module, a
pipeline, and an in-memory implementation of such pipeline, and an IO implementation.
Change-Id: I69056342da8ba126459064d7751d5dbe75ebcfca
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/100627
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/.packages b/.packages
index c5944a1..db0e7d7 100644
--- a/.packages
+++ b/.packages
@@ -59,6 +59,7 @@
meta:pkg/meta/lib
mime:third_party/pkg/mime/lib
mockito:third_party/pkg/mockito/lib
+modular_test:pkg/modular_test/lib
mustache:third_party/pkg/mustache/lib
oauth2:third_party/pkg/oauth2/lib
observatory:runtime/observatory/lib
diff --git a/pkg/modular_test/lib/src/io_pipeline.dart b/pkg/modular_test/lib/src/io_pipeline.dart
new file mode 100644
index 0000000..a92c6c7
--- /dev/null
+++ b/pkg/modular_test/lib/src/io_pipeline.dart
@@ -0,0 +1,91 @@
+// 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.
+
+/// An implementation of [Pipeline] that runs using IO.
+///
+/// To define a step, implement [IOModularStep].
+import 'dart:io';
+
+import 'pipeline.dart';
+import 'suite.dart';
+
+/// Indicates where to read and write data produced by the pipeline.
+typedef ModuleDataToRelativeUri = Uri Function(Module, DataId);
+
+abstract class IOModularStep extends ModularStep {
+ /// Execute the step under [root].
+ ///
+ /// The [root] folder will hold all inputs and will be used to emit the output
+ /// of this step.
+ ///
+ /// Assets created on previous steps of the pipeline should be available under
+ /// `root.resolveUri(toUri(module, dataId))` and the output of this step
+ /// should be stored under `root.resolveUri(toUri(module, resultKind))`.
+ Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri);
+}
+
+class IOPipeline extends Pipeline<IOModularStep> {
+ /// A folder per step. The key is the data id produced by a specific step.
+ ///
+ /// This contains internal state used during the run of the pipeline, but is
+ /// expected to be null before and after the pipeline is executed.
+ Map<DataId, Uri> _tmpFolders;
+ Map<DataId, Uri> get tmpFoldersForTesting => _tmpFolders;
+ bool saveFoldersForTesting;
+
+ IOPipeline(List<ModularStep> steps, {this.saveFoldersForTesting: false})
+ : super(steps);
+
+ @override
+ Future<void> run(ModularTest test) async {
+ assert(_tmpFolders == null);
+ _tmpFolders = {};
+ await super.run(test);
+ if (!saveFoldersForTesting) {
+ for (var folder in _tmpFolders.values) {
+ await Directory.fromUri(folder).delete(recursive: true);
+ }
+ _tmpFolders = null;
+ }
+ }
+
+ @override
+ Future<void> runStep(IOModularStep step, Module module,
+ Map<Module, Set<DataId>> visibleData) async {
+ var folder = await Directory.systemTemp
+ .createTemp('modular_test_${step.resultKind}-');
+ _tmpFolders[step.resultKind] ??= (await Directory.systemTemp
+ .createTemp('modular_test_${step.resultKind}_res-'))
+ .uri;
+ for (var module in visibleData.keys) {
+ for (var dataId in visibleData[module]) {
+ var filename = "${module.name}.${dataId.name}";
+ var assetUri = _tmpFolders[dataId].resolve(filename);
+ await File.fromUri(assetUri)
+ .copy(folder.uri.resolve(filename).toFilePath());
+ }
+ }
+ if (step.needsSources) {
+ for (var uri in module.sources) {
+ var originalUri = module.rootUri.resolveUri(uri);
+ await File.fromUri(originalUri)
+ .copy(folder.uri.resolveUri(uri).toFilePath());
+ }
+ }
+
+ await step.execute(module, folder.uri,
+ (Module m, DataId id) => Uri.parse("${m.name}.${id.name}"));
+
+ var outputFile = File.fromUri(
+ folder.uri.resolve("${module.name}.${step.resultKind.name}"));
+ if (!await outputFile.exists()) {
+ throw StateError(
+ "Step '${step.runtimeType}' didn't produce an output file");
+ }
+ await outputFile.copy(_tmpFolders[step.resultKind]
+ .resolve("${module.name}.${step.resultKind.name}")
+ .toFilePath());
+ await folder.delete(recursive: true);
+ }
+}
diff --git a/pkg/modular_test/lib/src/memory_pipeline.dart b/pkg/modular_test/lib/src/memory_pipeline.dart
new file mode 100644
index 0000000..4254bbc
--- /dev/null
+++ b/pkg/modular_test/lib/src/memory_pipeline.dart
@@ -0,0 +1,62 @@
+// 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.
+
+/// An implementation of [Pipeline] that runs in-memory.
+///
+/// To define a step, implement [MemoryModularStep].
+import 'pipeline.dart';
+import 'suite.dart';
+
+/// A hook to fetch data previously computed for a dependency.
+typedef ModuleDataProvider = Object Function(Module, DataId);
+typedef SourceProvider = String Function(Uri);
+
+abstract class MemoryModularStep extends ModularStep {
+ Future<Object> execute(Module module, SourceProvider sourceProvider,
+ ModuleDataProvider dataProvider);
+}
+
+class MemoryPipeline extends Pipeline<MemoryModularStep> {
+ final Map<Uri, String> _sources;
+
+ /// Internal state to hold the current results as they are computed by the
+ /// pipeline. Expected to be null before and after the pipeline runs.
+ Map<Module, Map<DataId, Object>> _results;
+
+ /// A copy of [_result] at the time the pipeline last finished running.
+ Map<Module, Map<DataId, Object>> resultsForTesting;
+
+ MemoryPipeline(this._sources, List<ModularStep> steps) : super(steps);
+
+ @override
+ Future<void> run(ModularTest test) async {
+ assert(_results == null);
+ _results = {};
+ await super.run(test);
+ resultsForTesting = _results;
+ _results = null;
+ }
+
+ @override
+ Future<void> runStep(MemoryModularStep step, Module module,
+ Map<Module, Set<DataId>> visibleData) async {
+ Map<Module, Map<DataId, Object>> inputData = {};
+ visibleData.forEach((module, dataIdSet) {
+ inputData[module] = {};
+ for (var dataId in dataIdSet) {
+ inputData[module][dataId] = _results[module][dataId];
+ }
+ });
+ Map<Uri, String> inputSources = {};
+ if (step.needsSources) {
+ module.sources.forEach((relativeUri) {
+ var uri = module.rootUri.resolveUri(relativeUri);
+ inputSources[uri] = _sources[uri];
+ });
+ }
+ Object result = await step.execute(module, (Uri uri) => inputSources[uri],
+ (Module m, DataId id) => inputData[m][id]);
+ (_results[module] ??= {})[step.resultKind] = result;
+ }
+}
diff --git a/pkg/modular_test/lib/src/pipeline.dart b/pkg/modular_test/lib/src/pipeline.dart
new file mode 100644
index 0000000..1323459
--- /dev/null
+++ b/pkg/modular_test/lib/src/pipeline.dart
@@ -0,0 +1,153 @@
+// 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.
+
+/// Abstraction for a compilation pipeline.
+///
+/// A pipeline defines how modular steps are excuted and ensures that a step
+/// only has access to the data it declares.
+///
+/// The abstract implementation validates how the data is declared, and the
+/// underlying implementations enforce the access to data in different ways.
+///
+/// The IO-based implementation ensures hermeticity by copying data to different
+/// directories. The memory-based implementation ensures hemeticity by filtering
+/// out the data before invoking the next step.
+import 'suite.dart';
+
+/// Describes a step in a modular compilation pipeline.
+class ModularStep {
+ /// Whether this step needs to read the source files in the module.
+ final bool needsSources;
+
+ /// Data that this step needs to read about dependencies.
+ ///
+ /// This can be data produced on a previous stage of the pipeline
+ /// or produced by this same step when it was run on a dependency.
+ ///
+ /// If this list includes [resultKind], then the modular-step has to be run on
+ /// dependencies before it is run on a module. Otherwise, it could be run in
+ /// parallel.
+ final List<DataId> dependencyDataNeeded;
+
+ /// Data that this step needs to read about the module itself.
+ ///
+ /// This is meant to be data produced in earlier stages of the modular
+ /// pipeline. It is an error to include [resultKind] in this list.
+ final List<DataId> moduleDataNeeded;
+
+ /// Data that this step produces.
+ final DataId resultKind;
+
+ ModularStep(
+ {this.needsSources: true,
+ this.dependencyDataNeeded: const [],
+ this.moduleDataNeeded: const [],
+ this.resultKind});
+}
+
+/// An object to uniquely identify modular data produced by a modular step.
+///
+/// Two modular steps on the same pipeline cannot emit the same data.
+class DataId {
+ final String name;
+
+ const DataId(this.name);
+
+ @override
+ String toString() => name;
+}
+
+abstract class Pipeline<S extends ModularStep> {
+ final List<S> steps;
+
+ Pipeline(this.steps) {
+ _validate();
+ }
+
+ void _validate() {
+ // Ensure that steps consume only data that was produced by previous steps
+ // or by the same step on a dependency.
+ Map<DataId, S> previousKinds = {};
+ for (var step in steps) {
+ var resultKind = step.resultKind;
+ if (previousKinds.containsKey(resultKind)) {
+ _validationError("Cannot produce the same data on two modular steps."
+ " '$resultKind' was previously produced by "
+ "'${previousKinds[resultKind].runtimeType}' but "
+ "'${step.runtimeType}' also produces the same data.");
+ }
+ previousKinds[resultKind] = step;
+ for (var dataId in step.dependencyDataNeeded) {
+ if (!previousKinds.containsKey(dataId)) {
+ _validationError(
+ "Step '${step.runtimeType}' needs data '${dataId}', but the data"
+ " is not produced by this or a preceding step.");
+ }
+ }
+ for (var dataId in step.moduleDataNeeded) {
+ if (!previousKinds.containsKey(dataId)) {
+ _validationError(
+ "Step '${step.runtimeType}' needs data '${dataId}', but the data"
+ " is not produced by a preceding step.");
+ }
+ if (dataId == resultKind) {
+ _validationError(
+ "Circular dependency on '$dataId' in step '${step.runtimeType}'");
+ }
+ }
+ }
+ }
+
+ void _validationError(String s) => throw InvalidPipelineError(s);
+
+ Future<void> run(ModularTest test) async {
+ // TODO(sigmund): validate that [ModularTest] has no cycles.
+ Map<Module, Set<DataId>> computedData = {};
+ for (var step in steps) {
+ await _recursiveRun(step, test.mainModule, computedData, {});
+ }
+ }
+
+ Future<void> _recursiveRun(S step, Module module,
+ Map<Module, Set<DataId>> computedData, Set<Module> seen) async {
+ if (!seen.add(module)) return;
+ for (var dependency in module.dependencies) {
+ await _recursiveRun(step, dependency, computedData, seen);
+ }
+ // Include only requested data from transitive dependencies.
+ Map<Module, Set<DataId>> visibleData = {};
+
+ // TODO(sigmund): consider excluding parent modules here. In particular,
+ // [seen] not only contains transitive dependencies, but also this module
+ // and parent modules. Technically we haven't computed any data for those,
+ // so we shouldn't be including any entries for parent modules in
+ // [visibleData].
+ seen.forEach((dep) {
+ if (dep == module) return;
+ visibleData[dep] = {};
+ for (var dataId in step.dependencyDataNeeded) {
+ if (computedData[dep].contains(dataId)) {
+ visibleData[dep].add(dataId);
+ }
+ }
+ });
+ visibleData[module] = {};
+ for (var dataId in step.moduleDataNeeded) {
+ if (computedData[module].contains(dataId)) {
+ visibleData[module].add(dataId);
+ }
+ }
+ await runStep(step, module, visibleData);
+ (computedData[module] ??= {}).add(step.resultKind);
+ }
+
+ Future<void> runStep(
+ S step, Module module, Map<Module, Set<DataId>> visibleData);
+}
+
+class InvalidPipelineError extends Error {
+ final String message;
+ InvalidPipelineError(this.message);
+ String toString() => "Invalid pipeline: $message";
+}
diff --git a/pkg/modular_test/lib/src/suite.dart b/pkg/modular_test/lib/src/suite.dart
new file mode 100644
index 0000000..bba158e
--- /dev/null
+++ b/pkg/modular_test/lib/src/suite.dart
@@ -0,0 +1,51 @@
+// 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.
+
+/// Model for a modular test.
+
+/// A modular test declares the structure of the test code: what files are
+/// grouped as a module and how modules depend on one another.
+class ModularTest {
+ /// Modules that will be compiled by for modular test
+ final List<Module> modules;
+
+ /// The module containing the main entry method.
+ final Module mainModule;
+
+ ModularTest(this.modules, this.mainModule)
+ : assert(mainModule != null && modules.length > 0);
+}
+
+/// A single module in a modular test.
+class Module {
+ /// A short name to identify this module.
+ final String name;
+
+ /// Other modules that need to be compiled first and whose result may be
+ /// necessary in order to compile this module.
+ final List<Module> dependencies;
+
+ /// Root under which all sources in the module can be found.
+ final Uri rootUri;
+
+ /// Source files that are part of this module only. Stored as a relative [Uri]
+ /// from [rootUri].
+ final List<Uri> sources;
+
+ /// The file containing the main entry method, if any. Stored as a relative
+ /// [Uri] from [rootUri].
+ final Uri mainSource;
+
+ Module(this.name, this.dependencies, this.rootUri, this.sources,
+ this.mainSource) {
+ if (!_validModuleName.hasMatch(name)) {
+ throw "invalid module name: $name";
+ }
+ }
+
+ @override
+ String toString() => '[module $name]';
+}
+
+final RegExp _validModuleName = new RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$');
diff --git a/pkg/modular_test/pubspec.yaml b/pkg/modular_test/pubspec.yaml
new file mode 100644
index 0000000..94f26d8
--- /dev/null
+++ b/pkg/modular_test/pubspec.yaml
@@ -0,0 +1,14 @@
+name: modular_test
+publish_to: none
+description: >
+ Small framework to test modular pipelines.
+ This is used within the Dart SDK to define and validate modular tests, and to
+ execute them using the modular pipeline of different SDK tools.
+environment:
+ sdk: ">=2.2.1 <3.0.0"
+
+dependencies:
+ yaml: ^2.1.15
+
+dev_dependencies:
+ test: any
diff --git a/pkg/modular_test/test/io_pipeline_test.dart b/pkg/modular_test/test/io_pipeline_test.dart
new file mode 100644
index 0000000..a30207c
--- /dev/null
+++ b/pkg/modular_test/test/io_pipeline_test.dart
@@ -0,0 +1,180 @@
+// 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.
+
+/// Unit test for in-memory pipelines.
+import 'dart:io';
+
+import 'package:modular_test/src/io_pipeline.dart';
+
+import 'pipeline_common.dart';
+
+main() async {
+ var uri = Directory.systemTemp.uri.resolve("io_modular_test_root/");
+ int i = 0;
+ while (await Directory.fromUri(uri).exists()) {
+ uri = Directory.systemTemp.uri.resolve("io_modular_test_root$i/");
+ i++;
+ }
+ runPipelineTest(new IOPipelineTestStrategy(uri));
+}
+
+/// The strategy implementation to exercise the pipeline test on a
+/// [IOPipeline].
+class IOPipelineTestStrategy implements PipelineTestStrategy<IOModularStep> {
+ @override
+ final Uri testRootUri;
+
+ IOPipelineTestStrategy(this.testRootUri);
+
+ @override
+ Future<Pipeline<IOModularStep>> createPipeline(
+ Map<Uri, String> sources, List<IOModularStep> steps) async {
+ await Directory.fromUri(testRootUri).create();
+ for (var uri in sources.keys) {
+ var file = new File.fromUri(uri);
+ await file.create(recursive: true);
+ await file.writeAsStringSync(sources[uri]);
+ }
+ return new IOPipeline(steps, saveFoldersForTesting: true);
+ }
+
+ @override
+ IOModularStep createConcatStep({bool requestSources: true}) =>
+ ConcatStep(requestSources);
+
+ @override
+ IOModularStep createLowerCaseStep({bool requestModuleData: true}) =>
+ LowerCaseStep(requestModuleData);
+
+ @override
+ IOModularStep createReplaceAndJoinStep(
+ {bool requestDependenciesData: true}) =>
+ ReplaceAndJoinStep(requestDependenciesData);
+
+ @override
+ IOModularStep createReplaceAndJoinStep2(
+ {bool requestDependenciesData: true}) =>
+ ReplaceAndJoinStep2(requestDependenciesData);
+
+ @override
+ String getResult(covariant IOPipeline pipeline, Module m, DataId dataId) {
+ var folderUri = pipeline.tmpFoldersForTesting[dataId];
+ return File.fromUri(folderUri.resolve("${m.name}.${dataId.name}"))
+ .readAsStringSync();
+ }
+
+ @override
+ Future<void> cleanup(Pipeline<IOModularStep> pipeline) async {
+ var folders = (pipeline as IOPipeline).tmpFoldersForTesting.values;
+ for (var folder in folders) {
+ await Directory.fromUri(folder).delete(recursive: true);
+ }
+ await Directory.fromUri(testRootUri).delete(recursive: true);
+ }
+}
+
+class ConcatStep implements IOModularStep {
+ final bool needsSources;
+ List<DataId> get dependencyDataNeeded => const [];
+ List<DataId> get moduleDataNeeded => const [];
+ DataId get resultKind => const DataId("concat");
+
+ ConcatStep(this.needsSources);
+
+ @override
+ Future<void> execute(
+ Module module, Uri root, ModuleDataToRelativeUri toUri) async {
+ var buffer = new StringBuffer();
+ for (var uri in module.sources) {
+ var file = File.fromUri(root.resolveUri(uri));
+ String data = await file.exists() ? await file.readAsString() : null;
+ buffer.write("$uri: ${data}\n");
+ }
+ await File.fromUri(root.resolveUri(toUri(module, resultKind)))
+ .writeAsString('$buffer');
+ }
+}
+
+Future<String> _readHelper(Module module, Uri root, DataId dataId,
+ ModuleDataToRelativeUri toUri) async {
+ var file = File.fromUri(root.resolveUri(toUri(module, dataId)));
+ if (await file.exists()) {
+ return await file.readAsString();
+ }
+ return null;
+}
+
+class LowerCaseStep implements IOModularStep {
+ bool get needsSources => false;
+ List<DataId> get dependencyDataNeeded => const [];
+ final List<DataId> moduleDataNeeded;
+ DataId get resultKind => const DataId("lowercase");
+
+ LowerCaseStep(bool requestConcat)
+ : moduleDataNeeded = requestConcat ? const [DataId("concat")] : const [];
+
+ @override
+ Future<void> execute(
+ Module module, Uri root, ModuleDataToRelativeUri toUri) async {
+ var concatData =
+ await _readHelper(module, root, const DataId("concat"), toUri);
+ if (concatData == null) concatData = "data for $module was null";
+ await File.fromUri(root.resolveUri(toUri(module, resultKind)))
+ .writeAsString(concatData.toLowerCase());
+ }
+}
+
+class ReplaceAndJoinStep implements IOModularStep {
+ bool get needsSources => false;
+ final List<DataId> dependencyDataNeeded;
+ List<DataId> get moduleDataNeeded => const [DataId("lowercase")];
+ DataId get resultKind => const DataId("join");
+
+ ReplaceAndJoinStep(bool requestDependencies)
+ : dependencyDataNeeded =
+ requestDependencies ? const [DataId("join")] : [];
+
+ @override
+ Future<void> execute(
+ Module module, Uri root, ModuleDataToRelativeUri toUri) async {
+ var buffer = new StringBuffer();
+ for (var dependency in module.dependencies) {
+ var depData =
+ await _readHelper(dependency, root, const DataId("join"), toUri);
+ buffer.write("$depData\n");
+ }
+ var moduleData =
+ await _readHelper(module, root, const DataId("lowercase"), toUri);
+ buffer.write(moduleData.replaceAll(".dart:", ""));
+ await File.fromUri(root.resolveUri(toUri(module, resultKind)))
+ .writeAsString('$buffer');
+ }
+}
+
+class ReplaceAndJoinStep2 implements IOModularStep {
+ bool get needsSources => false;
+ final List<DataId> dependencyDataNeeded;
+ List<DataId> get moduleDataNeeded => const [DataId("lowercase")];
+ DataId get resultKind => const DataId("join");
+
+ ReplaceAndJoinStep2(bool requestDependencies)
+ : dependencyDataNeeded =
+ requestDependencies ? const [DataId("lowercase")] : [];
+
+ @override
+ Future<void> execute(
+ Module module, Uri root, ModuleDataToRelativeUri toUri) async {
+ var buffer = new StringBuffer();
+ for (var dependency in module.dependencies) {
+ var depData =
+ await _readHelper(dependency, root, const DataId("lowercase"), toUri);
+ buffer.write("$depData\n");
+ }
+ var moduleData =
+ await _readHelper(module, root, const DataId("lowercase"), toUri);
+ buffer.write(moduleData.replaceAll(".dart:", ""));
+ await File.fromUri(root.resolveUri(toUri(module, resultKind)))
+ .writeAsString('$buffer');
+ }
+}
diff --git a/pkg/modular_test/test/memory_pipeline_test.dart b/pkg/modular_test/test/memory_pipeline_test.dart
new file mode 100644
index 0000000..29c407f
--- /dev/null
+++ b/pkg/modular_test/test/memory_pipeline_test.dart
@@ -0,0 +1,132 @@
+// 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.
+
+/// Unit test for in-memory pipelines.
+import 'dart:async';
+
+import 'package:modular_test/src/memory_pipeline.dart';
+
+import 'pipeline_common.dart';
+
+main() {
+ runPipelineTest(new MemoryPipelineTestStrategy());
+}
+
+/// The strategy implementation to exercise the pipeline test on a
+/// [MemoryPipeline].
+class MemoryPipelineTestStrategy
+ implements PipelineTestStrategy<MemoryModularStep> {
+ @override
+ Uri get testRootUri => Uri.parse('/');
+
+ @override
+ FutureOr<Pipeline<MemoryModularStep>> createPipeline(
+ Map<Uri, String> sources, List<MemoryModularStep> steps) {
+ return new MemoryPipeline(sources, steps);
+ }
+
+ @override
+ MemoryModularStep createConcatStep({bool requestSources: true}) =>
+ ConcatStep(requestSources);
+
+ @override
+ MemoryModularStep createLowerCaseStep({bool requestModuleData: true}) =>
+ LowerCaseStep(requestModuleData);
+
+ @override
+ MemoryModularStep createReplaceAndJoinStep(
+ {bool requestDependenciesData: true}) =>
+ ReplaceAndJoinStep(requestDependenciesData);
+
+ @override
+ MemoryModularStep createReplaceAndJoinStep2(
+ {bool requestDependenciesData: true}) =>
+ ReplaceAndJoinStep2(requestDependenciesData);
+
+ @override
+ String getResult(covariant MemoryPipeline pipeline, Module m, DataId dataId) {
+ return pipeline.resultsForTesting[m][dataId];
+ }
+
+ FutureOr<void> cleanup(Pipeline<MemoryModularStep> pipeline) => null;
+}
+
+class ConcatStep implements MemoryModularStep {
+ final bool needsSources;
+ List<DataId> get dependencyDataNeeded => const [];
+ List<DataId> get moduleDataNeeded => const [];
+ DataId get resultKind => const DataId("concat");
+
+ ConcatStep(this.needsSources);
+
+ Future<Object> execute(Module module, SourceProvider sourceProvider,
+ ModuleDataProvider dataProvider) {
+ var buffer = new StringBuffer();
+ for (var uri in module.sources) {
+ buffer.write("$uri: ${sourceProvider(module.rootUri.resolveUri(uri))}\n");
+ }
+ return Future.value('$buffer');
+ }
+}
+
+class LowerCaseStep implements MemoryModularStep {
+ bool get needsSources => false;
+ List<DataId> get dependencyDataNeeded => const [];
+ final List<DataId> moduleDataNeeded;
+ DataId get resultKind => const DataId("lowercase");
+
+ LowerCaseStep(bool requestConcat)
+ : moduleDataNeeded = requestConcat ? const [DataId("concat")] : const [];
+
+ Future<Object> execute(Module module, SourceProvider sourceProvider,
+ ModuleDataProvider dataProvider) {
+ var concatData = dataProvider(module, const DataId("concat")) as String;
+ if (concatData == null) return Future.value("data for $module was null");
+ return Future.value(concatData.toLowerCase());
+ }
+}
+
+class ReplaceAndJoinStep implements MemoryModularStep {
+ bool get needsSources => false;
+ final List<DataId> dependencyDataNeeded;
+ List<DataId> get moduleDataNeeded => const [DataId("lowercase")];
+ DataId get resultKind => const DataId("join");
+
+ ReplaceAndJoinStep(bool requestDependencies)
+ : dependencyDataNeeded =
+ requestDependencies ? const [DataId("join")] : [];
+
+ Future<Object> execute(Module module, SourceProvider sourceProvider,
+ ModuleDataProvider dataProvider) {
+ var buffer = new StringBuffer();
+ for (var dependency in module.dependencies) {
+ buffer.write("${dataProvider(dependency, const DataId("join"))}\n");
+ }
+ var moduleData = dataProvider(module, const DataId("lowercase")) as String;
+ buffer.write(moduleData.replaceAll(".dart:", ""));
+ return Future.value('$buffer');
+ }
+}
+
+class ReplaceAndJoinStep2 implements MemoryModularStep {
+ bool get needsSources => false;
+ final List<DataId> dependencyDataNeeded;
+ List<DataId> get moduleDataNeeded => const [DataId("lowercase")];
+ DataId get resultKind => const DataId("join");
+
+ ReplaceAndJoinStep2(bool requestDependencies)
+ : dependencyDataNeeded =
+ requestDependencies ? const [DataId("lowercase")] : [];
+
+ Future<Object> execute(Module module, SourceProvider sourceProvider,
+ ModuleDataProvider dataProvider) {
+ var buffer = new StringBuffer();
+ for (var dependency in module.dependencies) {
+ buffer.write("${dataProvider(dependency, const DataId("lowercase"))}\n");
+ }
+ var moduleData = dataProvider(module, const DataId("lowercase")) as String;
+ buffer.write(moduleData.replaceAll(".dart:", ""));
+ return Future.value('$buffer');
+ }
+}
diff --git a/pkg/modular_test/test/pipeline_common.dart b/pkg/modular_test/test/pipeline_common.dart
new file mode 100644
index 0000000..159e464
--- /dev/null
+++ b/pkg/modular_test/test/pipeline_common.dart
@@ -0,0 +1,192 @@
+// 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.
+
+/// Defines the abstract skeleton of the memory and io pipeline tests.
+///
+/// The idea is to ensure that pipelines are evaluated in the expected order
+/// and that steps are hermetic in that they are only provided the data they
+/// request.
+///
+/// We place most of the logic here to guarantee that the two different pipeline
+/// implementations are consistent with each other.
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:modular_test/src/suite.dart';
+import 'package:modular_test/src/pipeline.dart';
+
+export 'package:modular_test/src/suite.dart';
+export 'package:modular_test/src/pipeline.dart';
+
+/// A strategy to create the steps and pipelines used by the pipeline test. This
+/// is implemented in `memory_pipeline_test.dart` and `io_pipeline_test.dart`.
+abstract class PipelineTestStrategy<S extends ModularStep> {
+ /// Root URI where test sources are found.
+ Uri get testRootUri;
+
+ /// Creates a pipeline with the given sources and steps. Steps will be created
+ /// by other methods in this strategy to ensure they are compatible with to
+ /// the pipeline created here.
+ FutureOr<Pipeline<S>> createPipeline(Map<Uri, String> sources, List<S> steps);
+
+ /// Create a step that concatenates all contents of the sources in a module.
+ S createConcatStep({bool requestSources: true});
+
+ /// Create a step that consumes the concat step result and converts the
+ /// contents to lower-case.
+ S createLowerCaseStep({bool requestModuleData: true});
+
+ /// Create a step that consumes the concat and lower-case steps and does a
+ /// replace and join operation as expected in the tests below.
+ ///
+ /// This step consumes it's own data from dependencies.
+ S createReplaceAndJoinStep({bool requestDependenciesData: true});
+
+ /// Create a step that consumes the concat and lower-case steps and does a
+ /// replace and join operation as expected in the tests below.
+ ///
+ /// This step consumes the lower-case step data from dependencies.
+ S createReplaceAndJoinStep2({bool requestDependenciesData: true});
+
+ /// Return the result data produced by a modular step.
+ String getResult(Pipeline<S> pipeline, Module m, DataId dataId);
+
+ /// Do any cleanup work needed after pipeline is completed. Needed because
+ /// some implementations retain data around to be able to answer [getResult]
+ /// queries.
+ FutureOr<void> cleanup(Pipeline<S> pipeline);
+}
+
+runPipelineTest<S extends ModularStep>(PipelineTestStrategy<S> testStrategy) {
+ var sources = {
+ testStrategy.testRootUri.resolve("a1.dart"): 'A1',
+ testStrategy.testRootUri.resolve("a2.dart"): 'A2',
+ testStrategy.testRootUri.resolve("b/b1.dart"): 'B1',
+ testStrategy.testRootUri.resolve("b/b2.dart"): 'B2',
+ };
+
+ var m1 = Module("a", const [], testStrategy.testRootUri,
+ [Uri.parse("a1.dart"), Uri.parse("a2.dart")], null);
+ var m2 = Module("b", [m1], testStrategy.testRootUri.resolve('b/'),
+ [Uri.parse("b1.dart"), Uri.parse("b2.dart")], null);
+
+ var singleModuleInput = ModularTest([m1], m1);
+ var multipleModulesInput = ModularTest([m1, m2], m2);
+
+ test('can read source data if requested', () async {
+ var concatStep = testStrategy.createConcatStep();
+ var pipeline = await testStrategy.createPipeline(sources, <S>[concatStep]);
+ await pipeline.run(singleModuleInput);
+ expect(testStrategy.getResult(pipeline, m1, concatStep.resultKind),
+ "a1.dart: A1\na2.dart: A2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('cannot read source data if not requested', () async {
+ var concatStep = testStrategy.createConcatStep(requestSources: false);
+ var pipeline = await testStrategy.createPipeline(sources, <S>[concatStep]);
+ await pipeline.run(singleModuleInput);
+ expect(testStrategy.getResult(pipeline, m1, concatStep.resultKind),
+ "a1.dart: null\na2.dart: null\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('step is applied to all modules', () async {
+ var concatStep = testStrategy.createConcatStep();
+ var pipeline = await testStrategy.createPipeline(sources, <S>[concatStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, concatStep.resultKind),
+ "a1.dart: A1\na2.dart: A2\n");
+ expect(testStrategy.getResult(pipeline, m2, concatStep.resultKind),
+ "b1.dart: B1\nb2.dart: B2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('can read previous step results if requested', () async {
+ var concatStep = testStrategy.createConcatStep();
+ var lowercaseStep = testStrategy.createLowerCaseStep();
+ var pipeline = await testStrategy
+ .createPipeline(sources, <S>[concatStep, lowercaseStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, lowercaseStep.resultKind),
+ "a1.dart: a1\na2.dart: a2\n");
+ expect(testStrategy.getResult(pipeline, m2, lowercaseStep.resultKind),
+ "b1.dart: b1\nb2.dart: b2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('cannot read previous step results if not requested', () async {
+ var concatStep = testStrategy.createConcatStep();
+ var lowercaseStep =
+ testStrategy.createLowerCaseStep(requestModuleData: false);
+ var pipeline = await testStrategy
+ .createPipeline(sources, <S>[concatStep, lowercaseStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, lowercaseStep.resultKind),
+ "data for [module a] was null");
+ expect(testStrategy.getResult(pipeline, m2, lowercaseStep.resultKind),
+ "data for [module b] was null");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('can read same-step results of dependencies if requested', () async {
+ var concatStep = testStrategy.createConcatStep();
+ var lowercaseStep = testStrategy.createLowerCaseStep();
+ var replaceJoinStep = testStrategy.createReplaceAndJoinStep();
+ var pipeline = await testStrategy.createPipeline(
+ sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, replaceJoinStep.resultKind),
+ "a1 a1\na2 a2\n");
+ expect(testStrategy.getResult(pipeline, m2, replaceJoinStep.resultKind),
+ "a1 a1\na2 a2\n\nb1 b1\nb2 b2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('cannot read same-step results of dependencies if not requested',
+ () async {
+ var concatStep = testStrategy.createConcatStep();
+ var lowercaseStep = testStrategy.createLowerCaseStep();
+ var replaceJoinStep =
+ testStrategy.createReplaceAndJoinStep(requestDependenciesData: false);
+ var pipeline = await testStrategy.createPipeline(
+ sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, replaceJoinStep.resultKind),
+ "a1 a1\na2 a2\n");
+ expect(testStrategy.getResult(pipeline, m2, replaceJoinStep.resultKind),
+ "null\nb1 b1\nb2 b2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('can read prior step results of dependencies if requested', () async {
+ var concatStep = testStrategy.createConcatStep();
+ var lowercaseStep = testStrategy.createLowerCaseStep();
+ var replaceJoinStep = testStrategy.createReplaceAndJoinStep2();
+ var pipeline = await testStrategy.createPipeline(
+ sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, replaceJoinStep.resultKind),
+ "a1 a1\na2 a2\n");
+ expect(testStrategy.getResult(pipeline, m2, replaceJoinStep.resultKind),
+ "a1.dart: a1\na2.dart: a2\n\nb1 b1\nb2 b2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+
+ test('cannot read prior step results of dependencies if not requested',
+ () async {
+ var concatStep = testStrategy.createConcatStep();
+ var lowercaseStep = testStrategy.createLowerCaseStep();
+ var replaceJoinStep =
+ testStrategy.createReplaceAndJoinStep2(requestDependenciesData: false);
+ var pipeline = await testStrategy.createPipeline(
+ sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
+ await pipeline.run(multipleModulesInput);
+ expect(testStrategy.getResult(pipeline, m1, replaceJoinStep.resultKind),
+ "a1 a1\na2 a2\n");
+ expect(testStrategy.getResult(pipeline, m2, replaceJoinStep.resultKind),
+ "null\nb1 b1\nb2 b2\n");
+ await testStrategy.cleanup(pipeline);
+ });
+}
diff --git a/pkg/modular_test/test/validate_test.dart b/pkg/modular_test/test/validate_test.dart
new file mode 100644
index 0000000..6858544
--- /dev/null
+++ b/pkg/modular_test/test/validate_test.dart
@@ -0,0 +1,80 @@
+// 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.
+
+/// Unit test for validation of modular steps in a pipeline.
+import 'package:test/test.dart';
+import 'package:modular_test/src/suite.dart';
+import 'package:modular_test/src/pipeline.dart';
+
+main() {
+ test('no steps is OK', () {
+ validateSteps([]);
+ });
+
+ test('no errors', () {
+ var id1 = DataId("data_a");
+ var id2 = DataId("data_b");
+ var id3 = DataId("data_c");
+ validateSteps([
+ ModularStep(
+ needsSources: true, dependencyDataNeeded: [id1], resultKind: id1),
+ ModularStep(moduleDataNeeded: [id1], resultKind: id2),
+ ModularStep(
+ moduleDataNeeded: [id2],
+ dependencyDataNeeded: [id1, id3],
+ resultKind: id3),
+ ]);
+ });
+
+ test('circular dependency is not allowed', () {
+ var id1 = DataId("data_a");
+ expect(
+ () => validateSteps([
+ ModularStep(moduleDataNeeded: [id1], resultKind: id1),
+ ]),
+ throwsA(TypeMatcher<InvalidPipelineError>()));
+ });
+
+ test('out of order dependencies are not allowed', () {
+ var id1 = DataId("data_a");
+ var id2 = DataId("data_b");
+ validateSteps([
+ ModularStep(
+ resultKind: id1), // id1 must be produced before it is consumed.
+ ModularStep(dependencyDataNeeded: [id1], resultKind: id2),
+ ]);
+
+ expect(
+ () => validateSteps([
+ ModularStep(dependencyDataNeeded: [id1], resultKind: id2),
+ ModularStep(resultKind: id1),
+ ]),
+ throwsA(TypeMatcher<InvalidPipelineError>()));
+ });
+
+ test('same data cannot be produced by two steps', () {
+ var id1 = DataId("data_a");
+ expect(
+ () => validateSteps([
+ ModularStep(resultKind: id1),
+ ModularStep(resultKind: id1),
+ ]),
+ throwsA(TypeMatcher<InvalidPipelineError>()));
+ });
+}
+
+validateSteps(List<ModularStep> steps) {
+ new _NoopPipeline(steps);
+}
+
+/// An implementation of [Pipeline] that simply validates the steps, but doesn't
+/// do anything else.
+class _NoopPipeline extends Pipeline {
+ _NoopPipeline(List<ModularStep> steps) : super(steps);
+
+ @override
+ Future<void> runStep(ModularStep step, Module module,
+ Map<Module, Set<DataId>> visibleData) =>
+ null;
+}
diff --git a/pkg/pkg.status b/pkg/pkg.status
index d26e5ee..5ad6e12 100644
--- a/pkg/pkg.status
+++ b/pkg/pkg.status
@@ -130,6 +130,7 @@
[ $runtime != vm ]
dev_compiler/test/options/*: SkipByDesign
front_end/test/hot_reload_e2e_test: Skip
+modular_test/test/io_pipeline_test: SkipByDesign
vm/test/*: SkipByDesign # Only meant to run on vm
[ $system == linux ]