Add support for caching results of shared modules.

This is important as we will soon add support for compiling the sdk as a
module and we would like to only compile it once when running a suite of
tests.

+ also enable caching in the dart2js pipeline test.

Change-Id: Ic9043f868123164f3ab425ba73f7428416b05fc0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/103485
Commit-Queue: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
Sigmund Cherem
2019-05-24 01:27:13 +00:00
committed by commit-bot@chromium.org
parent 8208e872f8
commit edadc9c881
22 changed files with 578 additions and 123 deletions
+98 -32
View File
@@ -26,49 +26,103 @@ abstract class IOModularStep extends ModularStep {
}
class IOPipeline extends Pipeline<IOModularStep> {
/// A folder per step. The key is the data id produced by a specific step.
/// Folder that holds the results of each step during the run of the pipeline.
///
/// 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;
/// This value is usually null before and after the pipeline runs, but will be
/// non-null in two cases:
///
/// * for testing purposes when using [saveIntermediateResultsForTesting].
///
/// * to share results across pipeline runs when using [cacheSharedModules].
///
/// When using [cacheSharedModules] the pipeline will only reuse data for
/// modules that are known to be shared (e.g. shared packages and sdk
/// libraries), and not modules that are test specific. File names will be
/// specific enough so that we can keep separate the artifacts created from
/// running tools under different configurations (with different flags).
Uri _resultsFolderUri;
Uri get resultFolderUriForTesting => _resultsFolderUri;
IOPipeline(List<IOModularStep> steps, {this.saveFoldersForTesting: false})
: super(steps);
/// A unique number to denote the current modular test configuration.
///
/// When using [cacheSharedModules], a test can resuse the output of a
/// previous run of this pipeline if that output was generated with the same
/// configuration.
int _currentConfiguration;
final ConfigurationRegistry _registry;
/// Whether to keep alive the temporary folder used to store intermediate
/// results in order to inspect it later in test.
final bool saveIntermediateResultsForTesting;
IOPipeline(List<IOModularStep> steps,
{this.saveIntermediateResultsForTesting: false,
bool cacheSharedModules: false})
: _registry = cacheSharedModules ? new ConfigurationRegistry() : null,
super(steps, cacheSharedModules);
@override
Future<void> run(ModularTest test) async {
assert(_tmpFolders == null);
_tmpFolders = {};
var resultsDir = null;
if (_resultsFolderUri == null) {
resultsDir = await Directory.systemTemp.createTemp('modular_test_res-');
_resultsFolderUri = resultsDir.uri;
}
if (cacheSharedModules) {
_currentConfiguration = _registry.computeConfigurationId(test);
}
await super.run(test);
if (!saveFoldersForTesting) {
for (var folder in _tmpFolders.values) {
await Directory.fromUri(folder).delete(recursive: true);
}
_tmpFolders = null;
if (resultsDir != null &&
!saveIntermediateResultsForTesting &&
!cacheSharedModules) {
await resultsDir.delete(recursive: true);
_resultsFolderUri = null;
}
if (!saveIntermediateResultsForTesting) {
_currentConfiguration = null;
}
}
/// Delete folders that were kept around either because of
/// [saveIntermediateResultsForTesting] or because of [cacheSharedModules].
Future<void> cleanup() async {
if (saveIntermediateResultsForTesting || cacheSharedModules) {
await Directory.fromUri(_resultsFolderUri).delete(recursive: true);
}
}
@override
Future<void> runStep(IOModularStep step, Module module,
Map<Module, Set<DataId>> visibleData) async {
// Since data ids are unique throughout the pipeline, we use the first
// result data id as a hint for the name of the temporary folder of a step.
var stepFolder;
for (var dataId in step.resultData) {
stepFolder ??=
await Directory.systemTemp.createTemp('modular_test_${dataId}-');
_tmpFolders[dataId] ??=
(await Directory.systemTemp.createTemp('modular_test_${dataId}_res-'))
.uri;
if (cacheSharedModules && module.isShared) {
// If all expected outputs are already available, skip the step.
bool allCachedResultsFound = true;
for (var dataId in step.resultData) {
var cachedFile = File.fromUri(_resultsFolderUri
.resolve(_toFileName(module, dataId, configSpecific: true)));
if (!await cachedFile.exists()) {
allCachedResultsFound = false;
break;
}
}
if (allCachedResultsFound) {
step.notifyCached(module);
return;
}
}
// Each step is executed in a separate folder. To make it easier to debug
// issues, we include one of the step data ids in the name of the folder.
var stepId = step.resultData.first;
var stepFolder =
await Directory.systemTemp.createTemp('modular_test_${stepId}-');
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(stepFolder.uri.resolve(filename).toFilePath());
var assetUri = _resultsFolderUri
.resolve(_toFileName(module, dataId, configSpecific: true));
await File.fromUri(assetUri).copy(
stepFolder.uri.resolve(_toFileName(module, dataId)).toFilePath());
}
}
if (step.needsSources) {
@@ -81,19 +135,31 @@ class IOPipeline extends Pipeline<IOModularStep> {
}
await step.execute(module, stepFolder.uri,
(Module m, DataId id) => Uri.parse("${m.name}.${id.name}"));
(Module m, DataId id) => Uri.parse(_toFileName(m, id)));
for (var dataId in step.resultData) {
var outputFile =
File.fromUri(stepFolder.uri.resolve("${module.name}.${dataId.name}"));
File.fromUri(stepFolder.uri.resolve(_toFileName(module, dataId)));
if (!await outputFile.exists()) {
throw StateError(
"Step '${step.runtimeType}' didn't produce an output file");
}
await outputFile.copy(_tmpFolders[dataId]
.resolve("${module.name}.${dataId.name}")
await outputFile.copy(_resultsFolderUri
.resolve(_toFileName(module, dataId, configSpecific: true))
.toFilePath());
}
await stepFolder.delete(recursive: true);
}
String _toFileName(Module module, DataId dataId,
{bool configSpecific: false}) {
var prefix =
cacheSharedModules && configSpecific && _currentConfiguration != null
? _currentConfiguration
: '';
return "$prefix${module.name}.${dataId.name}";
}
String configSpecificResultFileNameForTesting(Module module, DataId dataId) =>
_toFileName(module, dataId, configSpecific: true);
}
+12 -9
View File
@@ -17,11 +17,11 @@
/// [defaultPackagesInput]. The list of packages provided is expected to
/// be disjoint with those in [defaultPackagesInput].
/// * a modules.yaml file: a specification of dependencies between modules.
/// The format is described in `dependencies_parser.dart`.
/// The format is described in `test_specification_parser.dart`.
import 'dart:io';
import 'dart:convert';
import 'suite.dart';
import 'dependency_parser.dart';
import 'test_specification_parser.dart';
import 'find_sdk_root.dart';
import 'package:package_config/packages_file.dart' as package_config;
@@ -40,7 +40,7 @@ Future<ModularTest> loadTest(Uri uri) async {
Map<String, Uri> defaultPackages =
package_config.parse(_defaultPackagesInput, root);
Map<String, Module> modules = {};
String spec;
String specString;
Module mainModule;
Map<String, Uri> packages = {};
await for (var entry in folder.list(recursive: false)) {
@@ -69,7 +69,7 @@ Future<ModularTest> loadTest(Uri uri) async {
List<int> packagesBytes = await entry.readAsBytes();
packages = package_config.parse(packagesBytes, entryUri);
} else if (fileName == 'modules.yaml') {
spec = await entry.readAsString();
specString = await entry.readAsString();
}
} else {
assert(entry is Directory);
@@ -88,7 +88,7 @@ Future<ModularTest> loadTest(Uri uri) async {
packageBase: Uri.parse('$moduleName/'));
}
}
if (spec == null) {
if (specString == null) {
return _invalidTest("modules.yaml file is missing");
}
if (mainModule == null) {
@@ -97,12 +97,15 @@ Future<ModularTest> loadTest(Uri uri) async {
_addDefaultPackageEntries(packages, defaultPackages);
await _addModulePerPackage(packages, modules);
_attachDependencies(parseDependencyMap(spec), modules);
_attachDependencies(parseDependencyMap(_defaultPackagesSpec), modules);
TestSpecification spec = parseTestSpecification(specString);
_attachDependencies(spec.dependencies, modules);
_attachDependencies(
parseTestSpecification(_defaultPackagesSpec).dependencies, modules);
_detectCyclesAndRemoveUnreachable(modules, mainModule);
var sortedModules = modules.values.toList()
..sort((a, b) => a.name.compareTo(b.name));
return new ModularTest(sortedModules, mainModule);
var sortedFlags = spec.flags.toList()..sort();
return new ModularTest(sortedModules, mainModule, sortedFlags);
}
/// Returns all source files recursively found in a folder as relative URIs.
@@ -169,7 +172,7 @@ Future<void> _addModulePerPackage(
// module that is part of the test (package name and module name should
// match).
modules[packageName] = Module(packageName, [], rootUri, sources,
isPackage: true, packageBase: Uri.parse('lib/'));
isPackage: true, packageBase: Uri.parse('lib/'), isShared: true);
}
}
}
+42 -2
View File
@@ -27,20 +27,60 @@ class MemoryPipeline extends Pipeline<MemoryModularStep> {
/// A copy of [_result] at the time the pipeline last finished running.
Map<Module, Map<DataId, Object>> resultsForTesting;
MemoryPipeline(this._sources, List<MemoryModularStep> steps) : super(steps);
final ConfigurationRegistry _registry;
/// Cache of results when [cacheSharedModules] is true
final List<Map<Module, Map<DataId, Object>>> _resultCache;
MemoryPipeline(this._sources, List<MemoryModularStep> steps,
{bool cacheSharedModules: false})
: _registry = cacheSharedModules ? new ConfigurationRegistry() : null,
_resultCache = cacheSharedModules ? [] : null,
super(steps, cacheSharedModules);
@override
Future<void> run(ModularTest test) async {
assert(_results == null);
_results = {};
Map<Module, Map<DataId, Object>> cache = null;
if (cacheSharedModules) {
int id = _registry.computeConfigurationId(test);
if (id < _resultCache.length) {
cache = _resultCache[id];
} else {
assert(id == _resultCache.length);
_resultCache.add(cache = {});
}
_results.addAll(cache);
}
await super.run(test);
resultsForTesting = _results;
if (cacheSharedModules) {
for (var module in _results.keys) {
if (module.isShared) {
cache[module] = _results[module];
}
}
}
_results = null;
}
@override
Future<void> runStep(MemoryModularStep step, Module module,
Map<Module, Set<DataId>> visibleData) async {
if (cacheSharedModules && module.isShared) {
bool allCachedResultsFound = true;
for (var dataId in step.resultData) {
if (_results[module] == null || _results[module][dataId] == null) {
allCachedResultsFound = false;
break;
}
}
if (allCachedResultsFound) {
step.notifyCached(module);
return;
}
}
Map<Module, Map<DataId, Object>> inputData = {};
visibleData.forEach((module, dataIdSet) {
inputData[module] = {};
+8 -1
View File
@@ -48,6 +48,9 @@ class ModularStep {
this.moduleDataNeeded: const [],
this.resultData,
this.onlyOnMain: false});
/// Notifies that the step was not executed, but cached instead.
void notifyCached(Module module) {}
}
/// An object to uniquely identify modular data produced by a modular step.
@@ -63,9 +66,13 @@ class DataId {
}
abstract class Pipeline<S extends ModularStep> {
/// Whether to cache the result of shared modules (e.g. shard packages and sdk
/// libraries) when multiple tests are run by this pipeline.
final bool cacheSharedModules;
final List<S> steps;
Pipeline(this.steps) {
Pipeline(this.steps, this.cacheSharedModules) {
_validate();
}
+63 -3
View File
@@ -13,8 +13,23 @@ class ModularTest {
/// The module containing the main entry method.
final Module mainModule;
ModularTest(this.modules, this.mainModule)
: assert(mainModule != null && modules.length > 0);
/// Flags provided to tools that compile and execute the test.
final List<String> flags;
ModularTest(this.modules, this.mainModule, this.flags) {
if (mainModule == null) {
throw ArgumentError("main module was null");
}
if (flags == null) {
throw ArgumentError("flags was null");
}
if (modules == null || modules.length == 0) {
throw ArgumentError("modules cannot be null or empty");
}
for (var module in modules) {
module._validateDependencies();
}
}
String debugString() => modules.map((m) => m.debugString()).join('\n');
}
@@ -50,16 +65,36 @@ class Module {
/// Whether this is the main entry module of a test.
bool isMain;
/// Whether this module is test specific or shared across tests. Usually this
/// will be true only for the SDK and shared packages like `package:expect`.
bool isShared;
Module(this.name, this.dependencies, this.rootUri, this.sources,
{this.mainSource,
this.isPackage: false,
this.isMain: false,
this.packageBase}) {
this.packageBase,
this.isShared: false}) {
if (!_validModuleName.hasMatch(name)) {
throw ArgumentError("invalid module name: $name");
}
}
void _validateDependencies() {
if (!isPackage && !isShared) return;
for (var dependency in dependencies) {
if (isPackage && !dependency.isPackage) {
throw InvalidModularTestError("invalid dependency: $name is a package "
"but it depends on ${dependency.name}, which is not.");
}
if (isShared && !dependency.isShared) {
throw InvalidModularTestError(
"invalid dependency: $name is a shared module "
"but it depends on ${dependency.name}, which is not.");
}
}
}
@override
String toString() => '[module $name]';
@@ -87,3 +122,28 @@ Set<Module> computeTransitiveDependencies(Module module) {
module.dependencies.forEach(helper);
return deps;
}
/// A registry that can map a test configuration to a simple id.
///
/// This is used to help determine whether two tests are run with the same set
/// of flags (the same configuration), and thus pipelines could reuse the
/// results of shared modules from the first test when running the second test.
class ConfigurationRegistry {
Map<String, int> _configurationId = {};
/// Compute an id to identify the configuration of a modular test.
///
/// A configuration is defined in terms of the set of flags provided to a
/// test. If two test provided to this registry share the same set of flags,
/// the resulting ids are the same. Similarly, if the flags are different,
/// their ids will be different as well.
int computeConfigurationId(ModularTest test) {
return _configurationId[test.flags.join(' ')] ??= _configurationId.length;
}
}
class InvalidModularTestError extends Error {
final String message;
InvalidModularTestError(this.message);
String toString() => "Invalid modular test: $message";
}
@@ -2,14 +2,19 @@
// 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.
/// This library defines how to read module dependencies from a Yaml
/// specification. We expect to find specifications written in this format:
/// This library defines how to read a test specification from a Yaml
/// file. We expect specifications written in this format:
///
/// dependencies:
/// b: a
/// main: [b, expect]
/// flags:
/// - "--enable-experiment=constant-update-2018"
///
/// Where:
/// Where the dependencies section describe how modules depend on one another,
/// and the flags section show what flags are needed to run that specific test.
///
/// When defining dependencies:
/// - Each name corresponds to a module.
/// - Module names correlate to either a file, a folder, or a package.
/// - A map entry contains all the dependencies of a module, if any.
@@ -22,12 +27,8 @@
import 'package:yaml/yaml.dart';
/// Parses [contents] containing a module dependencies specification written in
/// yaml, and returns a normalized dependency map.
///
/// Note: some values in the map may not have a corresponding key. That may be
/// the case for modules that have no dependencies and modules that are not
/// specified in [contents] (e.g. modules that are supported by default).
Map<String, List<String>> parseDependencyMap(String contents) {
/// yaml, and returns a [TestSpecification].
TestSpecification parseTestSpecification(String contents) {
var spec = loadYaml(contents);
if (spec is! YamlMap) {
return _invalidSpecification("spec is not a map");
@@ -60,7 +61,38 @@ Map<String, List<String>> parseDependencyMap(String contents) {
"entry: '$value' is not a string or a list of strings");
}
});
return normalizedMap;
List<String> normalizedFlags = [];
dynamic flags = spec['flags'];
if (flags is String) {
normalizedFlags.add(flags);
} else if (flags is List) {
normalizedFlags.addAll(flags.cast<String>());
} else if (flags != null) {
_invalidSpecification(
"flags: '$flags' expected to be string or list of strings");
}
return new TestSpecification(normalizedFlags, normalizedMap);
}
/// Data specifying details about a modular test including dependencies and
/// flags that are necessary in order to properly run a test.
///
class TestSpecification {
/// Set of flags necessary to properly run a test.
///
/// Usually this contains flags enabling language experiments.
final List<String> flags;
/// Dependencies of the modules that are expected to exist on the test.
///
/// Note: some values in the map may not have a corresponding key. That may be
/// the case for modules that have no dependencies and modules that are not
/// specified explicitly because they are added automatically by the framework
/// (for instance, the module of `package:expect` or the sdk itself).
final Map<String, List<String>> dependencies;
TestSpecification(this.flags, this.dependencies);
}
_invalidSpecification(String message) {
+25 -9
View File
@@ -29,14 +29,17 @@ class IOPipelineTestStrategy implements PipelineTestStrategy<IOModularStep> {
@override
Future<Pipeline<IOModularStep>> createPipeline(
Map<Uri, String> sources, List<IOModularStep> steps) async {
Map<Uri, String> sources, List<IOModularStep> steps,
{bool cacheSharedModules: false}) 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);
return new IOPipeline(steps,
saveIntermediateResultsForTesting: true,
cacheSharedModules: cacheSharedModules);
}
@override
@@ -83,17 +86,15 @@ class IOPipelineTestStrategy implements PipelineTestStrategy<IOModularStep> {
@override
String getResult(covariant IOPipeline pipeline, Module m, DataId dataId) {
var folderUri = pipeline.tmpFoldersForTesting[dataId];
var file = File.fromUri(folderUri.resolve("${m.name}.${dataId.name}"));
var folderUri = pipeline.resultFolderUriForTesting;
var file = File.fromUri(folderUri
.resolve(pipeline.configSpecificResultFileNameForTesting(m, dataId)));
return file.existsSync() ? file.readAsStringSync() : null;
}
@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);
}
Future<void> cleanup(covariant IOPipeline pipeline) async {
pipeline.cleanup();
await Directory.fromUri(testRootUri).delete(recursive: true);
}
}
@@ -122,6 +123,9 @@ class SourceOnlyStep implements IOModularStep {
await File.fromUri(root.resolveUri(toUri(module, resultId)))
.writeAsString(action(sources));
}
@override
void notifyCached(Module module) {}
}
class ModuleDataStep implements IOModularStep {
@@ -146,6 +150,9 @@ class ModuleDataStep implements IOModularStep {
await File.fromUri(root.resolveUri(toUri(module, resultId)))
.writeAsString(result);
}
@override
void notifyCached(Module module) {}
}
class TwoOutputStep implements IOModularStep {
@@ -176,6 +183,9 @@ class TwoOutputStep implements IOModularStep {
await File.fromUri(root.resolveUri(toUri(module, result2Id)))
.writeAsString(result2);
}
@override
void notifyCached(Module module) {}
}
class LinkStep implements IOModularStep {
@@ -205,6 +215,9 @@ class LinkStep implements IOModularStep {
await File.fromUri(root.resolveUri(toUri(module, resultId)))
.writeAsString(action(inputData, depsData));
}
@override
void notifyCached(Module module) {}
}
class MainOnlyStep implements IOModularStep {
@@ -234,6 +247,9 @@ class MainOnlyStep implements IOModularStep {
await File.fromUri(root.resolveUri(toUri(module, resultId)))
.writeAsString(action(inputData, depsData));
}
@override
void notifyCached(Module module) {}
}
Future<String> _readHelper(Module module, Uri root, DataId dataId,
@@ -2,21 +2,25 @@
a
is package? no
is shared? no
dependencies: b, c
a.dart
b
is package? no
is shared? no
dependencies: d
b.dart
c
is package? no
is shared? no
(no dependencies)
c.dart
d
is package? no
is shared? no
(no dependencies)
d/d.dart
d/e.dart
@@ -24,5 +28,6 @@ d
main
**main module**
is package? no
is shared? no
dependencies: a, b
main.dart
@@ -1,2 +1,4 @@
a:a/
b:b/
d:d/
c:c/
@@ -2,21 +2,25 @@
a
is package? yes
is shared? no
dependencies: b, c
a.dart
b
is package? no
is package? yes
is shared? no
dependencies: d
b.dart
c
is package? yes
is shared? no
(no dependencies)
c.dart
d
is package? no
is package? yes
is shared? no
(no dependencies)
d/d.dart
d/e.dart
@@ -24,5 +28,6 @@ d
main
**main module**
is package? no
is shared? no
dependencies: a, b
main.dart
@@ -2,6 +2,7 @@
expect
is package? yes
is shared? yes
dependencies: meta
lib/expect.dart
lib/matchers_lite.dart
@@ -10,11 +11,13 @@ expect
main
**main module**
is package? no
is shared? no
dependencies: expect
main.dart
meta
is package? yes
is shared? yes
(no dependencies)
lib/dart2js.dart
lib/meta.dart
+17 -25
View File
@@ -4,34 +4,27 @@
/// Tests that the logic to load, parse, and validate modular tests.
import 'dart:io';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:test/test.dart';
import 'package:modular_test/src/loader.dart';
import 'package:modular_test/src/suite.dart';
import 'package:args/args.dart';
main(List<String> args) {
main(List<String> args) async {
var options = _Options.parse(args);
asyncTest(() async {
var baseUri = Platform.script.resolve('./');
var baseDir = Directory.fromUri(baseUri);
await for (var entry in baseDir.list(recursive: false)) {
if (entry is Directory) {
await _runTest(entry.uri, baseUri, options);
}
var baseUri = Platform.script.resolve('./');
var baseDir = Directory.fromUri(baseUri);
await for (var entry in baseDir.list(recursive: false)) {
if (entry is Directory) {
var dirName = entry.uri.path.substring(baseDir.path.length);
test(dirName, () => _runTest(entry.uri, dirName, options),
skip: options.filter != null && !dirName.contains(options.filter));
}
});
}
}
Future<void> _runTest(Uri uri, Uri baseDir, _Options options) 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");
Future<void> _runTest(Uri uri, String dirName, _Options options) async {
String result;
String header =
"# This expectation file is generated by loader_test.dart\n\n";
@@ -44,7 +37,8 @@ Future<void> _runTest(Uri uri, Uri baseDir, _Options options) async {
var file = File.fromUri(uri.resolve('expectation.txt'));
if (!options.updateExpectations) {
Expect.isTrue(await file.exists(), "expectation.txt file is missing");
expect(await file.exists(), isTrue,
reason: "expectation.txt file is missing");
var expectation = await file.readAsString();
if (expectation != result) {
print("expectation.txt doesn't match the result of the test. "
@@ -52,8 +46,7 @@ Future<void> _runTest(Uri uri, Uri baseDir, _Options options) async {
" ${Platform.executable} ${Platform.script} "
"--update --show-update --filter $dirName");
}
Expect.equals(expectation, result);
print(" expectation matches result.");
expect(expectation, result);
} else if (await file.exists() && (await file.readAsString() == result)) {
print(" expectation matches result and was up to date.");
} else {
@@ -76,10 +69,11 @@ String _dumpAsText(ModularTest test) {
}
buffer.write(module.name);
if (module.isMain) {
Expect.equals(test.mainModule, module);
expect(test.mainModule, module);
buffer.write('\n **main module**');
}
buffer.write('\n is package? ${module.isPackage ? 'yes' : 'no'}');
buffer.write('\n is shared? ${module.isShared ? 'yes' : 'no'}');
if (module.dependencies.isEmpty) {
buffer.write('\n (no dependencies)');
} else {
@@ -103,7 +97,6 @@ String _dumpAsText(ModularTest test) {
class _Options {
bool updateExpectations = false;
bool showResultOnUpdate = false;
bool showSkipped = false;
String filter = null;
static _Options parse(List<String> args) {
@@ -124,7 +117,6 @@ class _Options {
return _Options()
..updateExpectations = argResults['update']
..showResultOnUpdate = argResults['show-update']
..showSkipped = argResults['show-skipped']
..filter = argResults['filter'];
}
}
@@ -3,5 +3,6 @@
main
**main module**
is package? no
is shared? no
(no dependencies)
main.dart
@@ -2,21 +2,25 @@
a
is package? no
is shared? no
dependencies: b, c
a.dart
b
is package? no
is shared? no
dependencies: d
b.dart
c
is package? no
is shared? no
(no dependencies)
c.dart
d
is package? no
is shared? no
(no dependencies)
d/d.dart
d/e.dart
@@ -24,5 +28,6 @@ d
main
**main module**
is package? no
is shared? no
dependencies: a
main.dart
@@ -2,6 +2,7 @@
expect
is package? yes
is shared? yes
dependencies: meta
lib/expect.dart
lib/matchers_lite.dart
@@ -9,6 +10,7 @@ expect
js
is package? yes
is shared? yes
(no dependencies)
lib/js.dart
lib/js_util.dart
@@ -17,11 +19,13 @@ js
main
**main module**
is package? no
is shared? no
dependencies: js, expect
main.dart
meta
is package? yes
is shared? yes
(no dependencies)
lib/dart2js.dart
lib/meta.dart
@@ -22,8 +22,10 @@ class MemoryPipelineTestStrategy
@override
FutureOr<Pipeline<MemoryModularStep>> createPipeline(
Map<Uri, String> sources, List<MemoryModularStep> steps) {
return new MemoryPipeline(sources, steps);
Map<Uri, String> sources, List<MemoryModularStep> steps,
{bool cacheSharedModules: false}) {
return new MemoryPipeline(sources, steps,
cacheSharedModules: cacheSharedModules);
}
@override
@@ -95,6 +97,9 @@ class SourceOnlyStep implements MemoryModularStep {
}
return Future.value({resultId: action(sources)});
}
@override
void notifyCached(Module module) {}
}
class ModuleDataStep implements MemoryModularStep {
@@ -117,6 +122,9 @@ class ModuleDataStep implements MemoryModularStep {
return Future.value({resultId: "data for $module was null"});
return Future.value({resultId: action(inputData)});
}
@override
void notifyCached(Module module) {}
}
class TwoOutputStep implements MemoryModularStep {
@@ -145,6 +153,9 @@ class TwoOutputStep implements MemoryModularStep {
return Future.value(
{result1Id: action1(inputData), result2Id: action2(inputData)});
}
@override
void notifyCached(Module module) {}
}
class LinkStep implements MemoryModularStep {
@@ -170,6 +181,9 @@ class LinkStep implements MemoryModularStep {
var inputData = dataProvider(module, inputId) as String;
return Future.value({resultId: action(inputData, depsData)});
}
@override
void notifyCached(Module module) {}
}
class MainOnlyStep implements MemoryModularStep {
@@ -195,4 +209,7 @@ class MainOnlyStep implements MemoryModularStep {
var inputData = dataProvider(module, inputId) as String;
return Future.value({resultId: action(inputData, depsData)});
}
@override
void notifyCached(Module module) {}
}
+107 -5
View File
@@ -28,7 +28,8 @@ abstract class PipelineTestStrategy<S extends ModularStep> {
/// 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);
FutureOr<Pipeline<S>> createPipeline(Map<Uri, String> sources, List<S> steps,
{bool cacheSharedModules: false});
/// Create a step that applies [action] on all input files of the module, and
/// emits a result with the given [id]
@@ -98,15 +99,16 @@ runPipelineTest<S extends ModularStep>(PipelineTestStrategy<S> testStrategy) {
};
var m1 = Module("a", const [], testStrategy.testRootUri,
[Uri.parse("a1.dart"), Uri.parse("a2.dart")]);
[Uri.parse("a1.dart"), Uri.parse("a2.dart")],
isShared: true);
var m2 = Module("b", [m1], testStrategy.testRootUri,
[Uri.parse("b/b1.dart"), Uri.parse("b/b2.dart")]);
var m3 = Module("c", [m2], testStrategy.testRootUri, [Uri.parse("c.dart")],
isMain: true);
var singleModuleInput = ModularTest([m1], m1);
var twoModuleInput = ModularTest([m1, m2], m2);
var threeModuleInput = ModularTest([m1, m2, m3], m3);
var singleModuleInput = ModularTest([m1], m1, []);
var twoModuleInput = ModularTest([m1, m2], m2, []);
var threeModuleInput = ModularTest([m1, m2, m3], m3, []);
test('can read source data if requested', () async {
var concatStep =
@@ -310,6 +312,106 @@ runPipelineTest<S extends ModularStep>(PipelineTestStrategy<S> testStrategy) {
expect(testStrategy.getResult(pipeline, m3, _joinId), "null\nnull\nc c0\n");
await testStrategy.cleanup(pipeline);
});
test('no reuse of existing results if not caching', () async {
int i = 1;
const counterId = const DataId("counter");
const linkId = const DataId("link");
// This step is not idempotent, we do this purposely to test whether caching
// is taking place.
var counterStep = testStrategy.createSourceOnlyStep(
action: (_) => '${i++}', resultId: counterId);
var linkStep = testStrategy.createLinkStep(
action: (String m, List<String> deps) => "${deps.join(',')},$m",
inputId: counterId,
depId: counterId,
resultId: linkId,
requestDependenciesData: true);
var pipeline = await testStrategy.createPipeline(
sources, <S>[counterStep, linkStep],
cacheSharedModules: false);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, counterId), "1");
expect(testStrategy.getResult(pipeline, m2, counterId), "2");
expect(testStrategy.getResult(pipeline, m2, linkId), "1,2");
await pipeline.run(threeModuleInput);
expect(testStrategy.getResult(pipeline, m1, counterId), "3");
expect(testStrategy.getResult(pipeline, m2, counterId), "4");
expect(testStrategy.getResult(pipeline, m2, linkId), "3,4");
expect(testStrategy.getResult(pipeline, m3, counterId), "5");
expect(testStrategy.getResult(pipeline, m3, linkId), "4,5");
await testStrategy.cleanup(pipeline);
});
test('caching reuses existing results for the same configuration', () async {
int i = 1;
const counterId = const DataId("counter");
const linkId = const DataId("link");
var counterStep = testStrategy.createSourceOnlyStep(
action: (_) => '${i++}', resultId: counterId);
var linkStep = testStrategy.createLinkStep(
action: (String m, List<String> deps) => "${deps.join(',')},$m",
inputId: counterId,
depId: counterId,
resultId: linkId,
requestDependenciesData: true);
var pipeline = await testStrategy.createPipeline(
sources, <S>[counterStep, linkStep],
cacheSharedModules: true);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, counterId), "1");
expect(testStrategy.getResult(pipeline, m2, counterId), "2");
expect(testStrategy.getResult(pipeline, m2, linkId), "1,2");
await pipeline.run(threeModuleInput);
expect(testStrategy.getResult(pipeline, m1, counterId), "1"); // cached!
expect(testStrategy.getResult(pipeline, m2, counterId), "3");
expect(testStrategy.getResult(pipeline, m2, linkId), "1,3");
expect(testStrategy.getResult(pipeline, m3, counterId), "4");
expect(testStrategy.getResult(pipeline, m3, linkId), "3,4");
await testStrategy.cleanup(pipeline);
});
test('no reuse of existing results on different configurations', () async {
int i = 1;
const counterId = const DataId("counter");
const linkId = const DataId("link");
// This step is not idempotent, we do this purposely to test whether caching
// is taking place.
var counterStep = testStrategy.createSourceOnlyStep(
action: (_) => '${i++}', resultId: counterId);
var linkStep = testStrategy.createLinkStep(
action: (String m, List<String> deps) => "${deps.join(',')},$m",
inputId: counterId,
depId: counterId,
resultId: linkId,
requestDependenciesData: true);
var pipeline = await testStrategy.createPipeline(
sources, <S>[counterStep, linkStep],
cacheSharedModules: true);
var input1 = ModularTest([m1, m2], m2, []);
var input2 = ModularTest([m1, m2], m2, ['--foo']);
var input3 = ModularTest([m1, m2], m2, ['--foo']);
await pipeline.run(input1);
expect(testStrategy.getResult(pipeline, m1, counterId), "1");
expect(testStrategy.getResult(pipeline, m2, counterId), "2");
expect(testStrategy.getResult(pipeline, m2, linkId), "1,2");
await pipeline.run(input2);
expect(testStrategy.getResult(pipeline, m1, counterId), "3"); // no cache!
expect(testStrategy.getResult(pipeline, m2, counterId), "4");
expect(testStrategy.getResult(pipeline, m2, linkId), "3,4");
await pipeline.run(input3);
expect(testStrategy.getResult(pipeline, m1, counterId), "3"); // same config
expect(testStrategy.getResult(pipeline, m2, counterId), "5");
expect(testStrategy.getResult(pipeline, m2, linkId), "3,5");
await testStrategy.cleanup(pipeline);
});
}
DataId _concatId = const DataId("concat");
@@ -3,46 +3,46 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:test/test.dart';
import 'package:modular_test/src/dependency_parser.dart';
import 'package:modular_test/src/test_specification_parser.dart';
main() {
test('require dependencies section', () {
expect(() => parseDependencyMap(""),
expect(() => parseTestSpecification(""),
throwsA(TypeMatcher<InvalidSpecificationError>()));
});
test('dependencies is a map', () {
expect(() => parseDependencyMap("dependencies: []"),
expect(() => parseTestSpecification("dependencies: []"),
throwsA(TypeMatcher<InvalidSpecificationError>()));
});
test('dependencies can be a string or list of strings', () {
parseDependencyMap('''
parseTestSpecification('''
dependencies:
a: b
''');
parseDependencyMap('''
parseTestSpecification('''
dependencies:
a: [b, c]
''');
expect(() => parseDependencyMap('''
expect(() => parseTestSpecification('''
dependencies:
a: 1
'''), throwsA(TypeMatcher<InvalidSpecificationError>()));
expect(() => parseDependencyMap('''
expect(() => parseTestSpecification('''
dependencies:
a: true
'''), throwsA(TypeMatcher<InvalidSpecificationError>()));
expect(() => parseDependencyMap('''
expect(() => parseTestSpecification('''
dependencies:
a: [false]
'''), throwsA(TypeMatcher<InvalidSpecificationError>()));
expect(() => parseDependencyMap('''
expect(() => parseTestSpecification('''
dependencies:
a:
c: d
@@ -51,14 +51,26 @@ main() {
test('result map is normalized', () {
expect(
parseDependencyMap('''
parseTestSpecification('''
dependencies:
a: [b, c]
b: d
'''),
''').dependencies,
equals({
'a': ['b', 'c'],
'b': ['d'],
}));
});
test('flags are normalized', () {
expect(parseTestSpecification('''
dependencies: {}
flags: "a"
''').flags, equals(["a"]));
expect(parseTestSpecification('''
dependencies: {}
flags: ["a"]
''').flags, equals(["a"]));
});
}
@@ -27,7 +27,7 @@ main() {
]);
});
test('circular dependency is not allowed', () {
test('circular step dependency is not allowed', () {
var id1 = DataId("data_a");
expect(
() => validateSteps([
@@ -78,7 +78,7 @@ validateSteps(List<ModularStep> 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);
_NoopPipeline(List<ModularStep> steps) : super(steps, false);
@override
Future<void> runStep(ModularStep step, Module module,
@@ -0,0 +1,61 @@
// 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';
main() {
test('module test is not empty', () {
expect(
() => ModularTest([], null, []), throwsA(TypeMatcher<ArgumentError>()));
var m = Module("a", [], Uri.parse("app:/"), []);
expect(() => ModularTest([], m, []), throwsA(TypeMatcher<ArgumentError>()));
});
test('module test must have a main module', () {
var m = Module("a", [], Uri.parse("app:/"), []);
expect(() => ModularTest([m], null, []),
throwsA(TypeMatcher<ArgumentError>()));
});
test('package must depend on package', () {
var m1a = Module("a", const [], Uri.parse("app:/"),
[Uri.parse("a1.dart"), Uri.parse("a2.dart")],
isPackage: false);
var m1b = Module("a", const [], Uri.parse("app:/"),
[Uri.parse("a1.dart"), Uri.parse("a2.dart")],
isPackage: true);
var m2a = Module("b", [m1a], Uri.parse("app:/"),
[Uri.parse("b/b1.dart"), Uri.parse("b/b2.dart")],
isPackage: true);
var m2b = Module("b", [m1b], Uri.parse("app:/"),
[Uri.parse("b/b1.dart"), Uri.parse("b/b2.dart")],
isPackage: true);
expect(() => ModularTest([m1a, m2a], m2a, []),
throwsA(TypeMatcher<InvalidModularTestError>()));
expect(ModularTest([m1b, m2b], m2b, []), isNotNull);
});
test('shared module must depend on shared modules', () {
var m1a = Module("a", const [], Uri.parse("app:/"),
[Uri.parse("a1.dart"), Uri.parse("a2.dart")],
isShared: false);
var m1b = Module("a", const [], Uri.parse("app:/"),
[Uri.parse("a1.dart"), Uri.parse("a2.dart")],
isShared: true);
var m2a = Module("b", [m1a], Uri.parse("app:/"),
[Uri.parse("b/b1.dart"), Uri.parse("b/b2.dart")],
isShared: true);
var m2b = Module("b", [m1b], Uri.parse("app:/"),
[Uri.parse("b/b1.dart"), Uri.parse("b/b2.dart")],
isShared: true);
expect(() => ModularTest([m1a, m2a], m2a, []),
throwsA(TypeMatcher<InvalidModularTestError>()));
expect(ModularTest([m1b, m2b], m2b, []), isNotNull);
});
}
+1 -1
View File
@@ -178,10 +178,10 @@ analyzer/tool/*: Skip # Issue 26813
[ $compiler != dart2analyzer && $runtime != vm ]
dev_compiler/test/*: Skip
modular_test/test/dependency_parser_test: SkipByDesign
modular_test/test/find_sdk_root1_test: SkipByDesign
modular_test/test/io_pipeline_test: SkipByDesign
modular_test/test/loader/loader_test: SkipByDesign
modular_test/test/specification_parser_test: SkipByDesign
modular_test/test/src/find_sdk_root2_test: SkipByDesign
[ $compiler == dart2js && $runtime == chrome && $system == macos ]
@@ -23,15 +23,23 @@ main(List<String> 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(entry.uri, baseUri);
await _runTest(pipeline, entry.uri, baseUri);
}
}
await pipeline.cleanup();
});
}
Future<void> _runTest(Uri uri, Uri baseDir) async {
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");
@@ -41,13 +49,6 @@ Future<void> _runTest(Uri uri, Uri baseDir) async {
print("testing: $dirName");
ModularTest test = await loadTest(uri);
if (_options.verbose) print(test.debugString());
var pipeline = new IOPipeline([
SourceToDillStep(),
GlobalAnalysisStep(),
Dart2jsBackendStep(),
RunD8(),
]);
await pipeline.run(test);
}
@@ -153,6 +154,11 @@ class SourceToDillStep implements IOModularStep {
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
@@ -194,6 +200,12 @@ class GlobalAnalysisStep implements IOModularStep {
_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
@@ -231,6 +243,11 @@ class Dart2jsBackendStep implements IOModularStep {
_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.
@@ -269,6 +286,11 @@ class RunD8 implements IOModularStep {
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) {