blob: 7bfbf7794e7423421c30114c276066277dc35f2e [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// 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,
{bool cacheSharedModules: false});
/// Create a step that applies [action] on all input files of the module, and
/// emits a result with the given [id]
S createSourceOnlyStep(
{String Function(Map<Uri, String>) action,
DataId resultId,
bool requestSources: true});
/// Create a step that applies [action] on the module [inputId] data, and
/// emits a result with the given [resultId].
S createModuleDataStep(
{String Function(String) action,
DataId inputId,
DataId resultId,
bool requestModuleData: true});
/// Create a step that applies [action] on the module [inputId] data and the
/// the [depId] data of dependencies and finally emits a result with the given
/// [resultId].
///
/// [depId] may be the same as [resultId] or [inputId].
S createLinkStep(
{String Function(String, List<String>) action,
DataId inputId,
DataId depId,
DataId resultId,
bool requestDependenciesData: true});
/// Create a step that applies [action] only on the main module [inputId] data
/// and the the [depId] data of transitive dependencies and finally emits a
/// result with the given [resultId].
///
/// [depId] may be the same as [inputId] but not [resultId] since this action
/// is only applied on the main module.
S createMainOnlyStep(
{String Function(String, List<String>) action,
DataId inputId,
DataId depId,
DataId resultId,
bool requestDependenciesData: true});
/// Create a step that applies [action1] and [action2] on the module [inputId]
/// data, and emits two results with the given [result1Id] and [result2Id].
S createTwoOutputStep(
{String Function(String) action1,
String Function(String) action2,
DataId inputId,
DataId result1Id,
DataId result2Id});
/// 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',
testStrategy.testRootUri.resolve("c.dart"): 'C0',
};
var m1 = Module("a", const [], testStrategy.testRootUri,
[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, []);
test('can read source data if requested', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var pipeline = await testStrategy.createPipeline(sources, <S>[concatStep]);
await pipeline.run(singleModuleInput);
expect(testStrategy.getResult(pipeline, m1, _concatId),
"a1.dart: A1\na2.dart: A2\n");
await testStrategy.cleanup(pipeline);
});
test('cannot read source data if not requested', () async {
var concatStep = testStrategy.createSourceOnlyStep(
action: _concat, resultId: _concatId, requestSources: false);
var pipeline = await testStrategy.createPipeline(sources, <S>[concatStep]);
await pipeline.run(singleModuleInput);
expect(testStrategy.getResult(pipeline, m1, _concatId),
"a1.dart: null\na2.dart: null\n");
await testStrategy.cleanup(pipeline);
});
test('step is applied to all modules', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var pipeline = await testStrategy.createPipeline(sources, <S>[concatStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _concatId),
"a1.dart: A1\na2.dart: A2\n");
expect(testStrategy.getResult(pipeline, m2, _concatId),
"b/b1.dart: B1\nb/b2.dart: B2\n");
await testStrategy.cleanup(pipeline);
});
test('can read previous step results if requested', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var pipeline = await testStrategy
.createPipeline(sources, <S>[concatStep, lowercaseStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _lowercaseId),
"a1.dart: a1\na2.dart: a2\n");
expect(testStrategy.getResult(pipeline, m2, _lowercaseId),
"b/b1.dart: b1\nb/b2.dart: b2\n");
await testStrategy.cleanup(pipeline);
});
test('cannot read previous step results if not requested', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase,
inputId: _concatId,
resultId: _lowercaseId,
requestModuleData: false);
var pipeline = await testStrategy
.createPipeline(sources, <S>[concatStep, lowercaseStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _lowercaseId),
"data for [module a] was null");
expect(testStrategy.getResult(pipeline, m2, _lowercaseId),
"data for [module b] was null");
await testStrategy.cleanup(pipeline);
});
test('all outputs of a step are created together', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var twoOutputStep = testStrategy.createTwoOutputStep(
action1: _lowercase,
action2: _uppercase,
inputId: _concatId,
result1Id: _lowercaseId,
result2Id: _uppercaseId);
var pipeline = await testStrategy
.createPipeline(sources, <S>[concatStep, twoOutputStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m2, _lowercaseId),
"b/b1.dart: b1\nb/b2.dart: b2\n");
expect(testStrategy.getResult(pipeline, m2, _uppercaseId),
"B/B1.DART: B1\nB/B2.DART: B2\n");
await testStrategy.cleanup(pipeline);
});
test('can read same-step results of dependencies if requested', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var replaceJoinStep = testStrategy.createLinkStep(
action: _replaceAndJoin,
inputId: _lowercaseId,
depId: _joinId,
resultId: _joinId);
var pipeline = await testStrategy.createPipeline(
sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _joinId), "a1 a1\na2 a2\n");
expect(testStrategy.getResult(pipeline, m2, _joinId),
"a1 a1\na2 a2\n\nb/b1 b1\nb/b2 b2\n");
await testStrategy.cleanup(pipeline);
});
test('cannot read same-step results of dependencies if not requested',
() async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var replaceJoinStep = testStrategy.createLinkStep(
action: _replaceAndJoin,
inputId: _lowercaseId,
depId: _joinId,
resultId: _joinId,
requestDependenciesData: false);
var pipeline = await testStrategy.createPipeline(
sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _joinId), "a1 a1\na2 a2\n");
expect(testStrategy.getResult(pipeline, m2, _joinId),
"null\nb/b1 b1\nb/b2 b2\n");
await testStrategy.cleanup(pipeline);
});
test('can read prior step results of dependencies if requested', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var replaceJoinStep = testStrategy.createLinkStep(
action: _replaceAndJoin,
inputId: _lowercaseId,
depId: _lowercaseId,
resultId: _joinId);
var pipeline = await testStrategy.createPipeline(
sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _joinId), "a1 a1\na2 a2\n");
expect(testStrategy.getResult(pipeline, m2, _joinId),
"a1.dart: a1\na2.dart: a2\n\nb/b1 b1\nb/b2 b2\n");
await testStrategy.cleanup(pipeline);
});
test('cannot read prior step results of dependencies if not requested',
() async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var replaceJoinStep = testStrategy.createLinkStep(
action: _replaceAndJoin,
inputId: _lowercaseId,
depId: _lowercaseId,
resultId: _joinId,
requestDependenciesData: false);
var pipeline = await testStrategy.createPipeline(
sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
await pipeline.run(twoModuleInput);
expect(testStrategy.getResult(pipeline, m1, _joinId), "a1 a1\na2 a2\n");
expect(testStrategy.getResult(pipeline, m2, _joinId),
"null\nb/b1 b1\nb/b2 b2\n");
await testStrategy.cleanup(pipeline);
});
test('only main applies to main module', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var replaceJoinStep = testStrategy.createMainOnlyStep(
action: _replaceAndJoin,
inputId: _lowercaseId,
depId: _lowercaseId,
resultId: _joinId,
requestDependenciesData: true);
var pipeline = await testStrategy.createPipeline(
sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
await pipeline.run(threeModuleInput);
expect(testStrategy.getResult(pipeline, m1, _joinId), null);
expect(testStrategy.getResult(pipeline, m3, _joinId),
"b/b1.dart: b1\nb/b2.dart: b2\n\na1.dart: a1\na2.dart: a2\n\nc c0\n");
await testStrategy.cleanup(pipeline);
});
test('only main also needs to request transitive dependencies', () async {
var concatStep =
testStrategy.createSourceOnlyStep(action: _concat, resultId: _concatId);
var lowercaseStep = testStrategy.createModuleDataStep(
action: _lowercase, inputId: _concatId, resultId: _lowercaseId);
var replaceJoinStep = testStrategy.createMainOnlyStep(
action: _replaceAndJoin,
inputId: _lowercaseId,
depId: _lowercaseId,
resultId: _joinId,
requestDependenciesData: false);
var pipeline = await testStrategy.createPipeline(
sources, <S>[concatStep, lowercaseStep, replaceJoinStep]);
await pipeline.run(threeModuleInput);
expect(testStrategy.getResult(pipeline, m1, _joinId), null);
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");
DataId _lowercaseId = const DataId("lowercase");
DataId _uppercaseId = const DataId("uppercase");
DataId _joinId = const DataId("join");
String _concat(Map<Uri, String> sources) {
var buffer = new StringBuffer();
sources.forEach((uri, contents) {
buffer.write("$uri: $contents\n");
});
return '$buffer';
}
String _lowercase(String contents) => contents.toLowerCase();
String _uppercase(String contents) => contents.toUpperCase();
String _replaceAndJoin(String moduleData, List<String> depContents) {
var buffer = new StringBuffer();
depContents.forEach(buffer.writeln);
buffer.write(moduleData.replaceAll(".dart:", ""));
return '$buffer';
}