From cb7da4f672a92121e9c7c379447c105eae142159 Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Fri, 5 Feb 2016 11:16:24 -0800 Subject: [PATCH 1/3] add some basic tests to get started, as well as a basic test framework --- lib/build.dart | 4 + lib/src/asset/cache.dart | 5 + lib/src/asset/file_based.dart | 33 +++- lib/src/asset/reader.dart | 3 + lib/src/generate/build.dart | 52 ++----- lib/src/generate/build_result.dart | 4 +- lib/src/generate/input_set.dart | 9 +- lib/src/transformer/transformer.dart | 4 + test/common/copy_builder.dart | 16 +- test/common/in_memory_reader.dart | 13 ++ test/common/stub_reader.dart | 4 + test/generate/build_test.dart | 224 +++++++++++++++++++++++++++ 12 files changed, 320 insertions(+), 51 deletions(-) create mode 100644 test/generate/build_test.dart diff --git a/lib/build.dart b/lib/build.dart index 892f87f5d..405e7820e 100644 --- a/lib/build.dart +++ b/lib/build.dart @@ -11,5 +11,9 @@ export 'src/asset/writer.dart'; export 'src/builder/build_step.dart'; export 'src/builder/builder.dart'; export 'src/builder/exceptions.dart'; +export 'src/generate/build.dart'; +export 'src/generate/build_result.dart'; +export 'src/generate/input_set.dart'; +export 'src/generate/phase.dart'; export 'src/package_graph/package_graph.dart'; export 'src/transformer/transformer.dart'; diff --git a/lib/src/asset/cache.dart b/lib/src/asset/cache.dart index 50e3ecf58..ef7663d19 100644 --- a/lib/src/asset/cache.dart +++ b/lib/src/asset/cache.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; +import '../generate/input_set.dart'; import 'asset.dart'; import 'id.dart'; import 'reader.dart'; @@ -74,6 +75,10 @@ class CachedAssetReader extends AssetReader { }); return _pendingReads[id]; } + + @override + Stream listAssetIds(List inputSets) => + _reader.listAssetIds(inputSets); } /// An [AssetWriter] which takes both an [AssetCache] and an [AssetWriter]. It diff --git a/lib/src/asset/file_based.dart b/lib/src/asset/file_based.dart index a8a8a915d..655c760b7 100644 --- a/lib/src/asset/file_based.dart +++ b/lib/src/asset/file_based.dart @@ -12,6 +12,7 @@ import '../asset/exceptions.dart'; import '../asset/id.dart'; import '../asset/reader.dart'; import '../asset/writer.dart'; +import '../generate/input_set.dart'; import '../package_graph/package_graph.dart'; import 'exceptions.dart'; @@ -19,8 +20,9 @@ import 'exceptions.dart'; /// files from disk. class FileBasedAssetReader implements AssetReader { final PackageGraph packageGraph; + final ignoredDirs; - FileBasedAssetReader(this.packageGraph); + FileBasedAssetReader(this.packageGraph, {this.ignoredDirs: const ['build']}); @override Future hasInput(AssetId id) async { @@ -46,6 +48,35 @@ class FileBasedAssetReader implements AssetReader { throw new InvalidInputException(id); } } + + /// Searches for all [AssetId]s matching [inputSet]s. + Stream listAssetIds(Iterable inputSets) async* { + var seenAssets = new Set(); + for (var inputSet in inputSets) { + var packageNode = packageGraph[inputSet.package]; + var packagePath = packageNode.location.toFilePath(); + for (var glob in inputSet.globs) { + var files = glob.listSync(followLinks: false, root: packagePath).where( + (e) => e is File && !ignoredDirs.contains(path.split(e.path)[1])); + for (var file in files) { + var id = _fileToAssetId(file, packageNode); + if (!seenAssets.add(id)) continue; + yield id; + } + } + } + } +} + +/// Creates an [AssetId] for [file], which is a part of [packageNode]. +AssetId _fileToAssetId(File file, PackageNode packageNode) { + var filePath = path.normalize(file.absolute.path); + var packageUri = packageNode.location; + var packagePath = path.normalize(packageUri.isAbsolute + ? packageUri.toFilePath() + : path.absolute(packageUri.toFilePath())); + var relativePath = path.relative(filePath, from: packagePath); + return new AssetId(packageNode.name, relativePath); } /// Basic [AssetWriter] which uses a [PackageGraph] to look up where to write diff --git a/lib/src/asset/reader.dart b/lib/src/asset/reader.dart index 7a827e317..a590a08b6 100644 --- a/lib/src/asset/reader.dart +++ b/lib/src/asset/reader.dart @@ -4,10 +4,13 @@ import 'dart:async'; import 'dart:convert'; +import '../generate/input_set.dart'; import 'id.dart'; abstract class AssetReader { Future readAsString(AssetId id, {Encoding encoding: UTF8}); Future hasInput(AssetId id); + + Stream listAssetIds(List inputSets); } diff --git a/lib/src/generate/build.dart b/lib/src/generate/build.dart index 1ddba3e15..8a040036b 100644 --- a/lib/src/generate/build.dart +++ b/lib/src/generate/build.dart @@ -2,7 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as path; @@ -17,7 +16,6 @@ import '../builder/builder.dart'; import '../builder/build_step_impl.dart'; import '../package_graph/package_graph.dart'; import 'build_result.dart'; -import 'input_set.dart'; import 'phase.dart'; /// Runs all of the [Phases] in [phaseGroups]. @@ -43,6 +41,7 @@ Future build(List> phaseGroups, Logger.root.level = logLevel; onLog ??= print; var logListener = Logger.root.onRecord.listen(onLog); + // No need to create a package graph if we were supplied a reader/writer. packageGraph ??= new PackageGraph.forThisPackage(); var cache = new AssetCache(); reader ??= @@ -50,7 +49,6 @@ Future build(List> phaseGroups, writer ??= new CachedAssetWriter(cache, new FileBasedAssetWriter(packageGraph)); var result = runZoned(() { - _validatePhases(phaseGroups); return _runPhases(phaseGroups); }, onError: (e, s) { return new BuildResult(BuildStatus.Failure, BuildType.Full, [], @@ -74,23 +72,14 @@ AssetReader get _reader => Zone.current[_assetReaderKey]; AssetWriter get _writer => Zone.current[_assetWriterKey]; PackageGraph get _packageGraph => Zone.current[_packageGraphKey]; -/// Validates the phases. -void _validatePhases(List> phaseGroups) { - if (phaseGroups.length > 1) { - // Don't support using generated files as inputs yet, so we only support - // one phase. - throw new UnimplementedError( - 'Only one phase group is currently supported.'); - } -} - /// Runs the [phaseGroups] and returns a [Future] which completes /// once all [Phase]s are done. Future _runPhases(List> phaseGroups) async { var outputs = []; for (var group in phaseGroups) { for (var phase in group) { - var inputs = _assetIdsFor(phase.inputSets); + var inputs = (await _reader.listAssetIds(phase.inputSets).toList()) + .where(_isValidInput); for (var builder in phase.builders) { // TODO(jakemac): Optimize, we can run all the builders in a phase // at the same time instead of sequentially. @@ -103,36 +92,17 @@ Future _runPhases(List> phaseGroups) async { return new BuildResult(BuildStatus.Success, BuildType.Full, outputs); } -/// Gets all [AssetId]s matching [inputSets] in the current package. -List _assetIdsFor(List inputSets) { - var ids = []; - for (var inputSet in inputSets) { - var files = _filesMatching(inputSet); - for (var file in files) { - var segments = file.uri.pathSegments; - var newPath = path.joinAll(segments.getRange( - segments.indexOf(inputSet.package) + 1, segments.length)); - ids.add(new AssetId(inputSet.package, newPath)); - } - } - return ids; +/// Checks if an [input] is valid. +bool _isValidInput(AssetId input) { + var parts = path.split(input.path); + // Files must be in a top level directory. + if (parts.length == 1) return false; + if (input.package != _packageGraph.root.name) return parts[0] == 'lib'; + return true; } -/// Returns all files matching [inputSet]. -Set _filesMatching(InputSet inputSet) { - var files = new Set(); - var root = _packageGraph[inputSet.package].location.toFilePath(); - for (var glob in inputSet.globs) { - files.addAll(glob.listSync(followLinks: false, root: root).where( - (e) => e is File && !_ignoredDirs.contains(path.split(e.path)[1]))); - } - return files; -} - -const _ignoredDirs = const ['build']; - /// Runs [builder] with [inputs] as inputs. -Stream _runBuilder(Builder builder, List inputs) async* { +Stream _runBuilder(Builder builder, Iterable inputs) async* { for (var input in inputs) { var expectedOutputs = builder.declareOutputs(input); var inputAsset = new Asset(input, await _reader.readAsString(input)); diff --git a/lib/src/generate/build_result.dart b/lib/src/generate/build_result.dart index 2e33080cc..a983f09d5 100644 --- a/lib/src/generate/build_result.dart +++ b/lib/src/generate/build_result.dart @@ -9,8 +9,8 @@ class BuildResult { /// The status of this build. final BuildStatus status; - /// The [Exception] that was thrown during this build if it failed. - final Exception exception; + /// The error that was thrown during this build if it failed. + final Object exception; /// The [StackTrace] for [exception] if non-null. final StackTrace stackTrace; diff --git a/lib/src/generate/input_set.dart b/lib/src/generate/input_set.dart index ba6e4691c..5dd61f8e0 100644 --- a/lib/src/generate/input_set.dart +++ b/lib/src/generate/input_set.dart @@ -17,6 +17,11 @@ class InputSet { final List globs; InputSet(this.package, {Iterable filePatterns}) - : this.globs = new List.unmodifiable( - filePatterns.map((pattern) => new Glob(pattern))); + : this.globs = _globsFor(filePatterns); +} + +List _globsFor(Iterable filePatterns) { + filePatterns ??= ['**/*']; + return new List.unmodifiable( + filePatterns.map((pattern) => new Glob(pattern))); } diff --git a/lib/src/transformer/transformer.dart b/lib/src/transformer/transformer.dart index f1a0d39dd..a566a675f 100644 --- a/lib/src/transformer/transformer.dart +++ b/lib/src/transformer/transformer.dart @@ -97,6 +97,10 @@ class _TransformAssetReader implements AssetReader { @override Future readAsString(build.AssetId id, {Encoding encoding: UTF8}) => transform.readInputAsString(toBarbackAssetId(id), encoding: encoding); + + @override + /// No way to implement this, but luckily its not necessary. + Stream listAssetIds(_) => throw new UnimplementedError(); } /// Very simple [AssetWriter] which uses a [Transform]. diff --git a/test/common/copy_builder.dart b/test/common/copy_builder.dart index 0177e4236..4b8d852f9 100644 --- a/test/common/copy_builder.dart +++ b/test/common/copy_builder.dart @@ -6,9 +6,11 @@ import 'dart:async'; import 'package:build/build.dart'; class CopyBuilder implements Builder { - int numCopies; + final int numCopies; + final String extension; + final String outputPackage; - CopyBuilder({this.numCopies: 1}); + CopyBuilder({this.numCopies: 1, this.extension: 'copy', this.outputPackage}); Future build(BuildStep buildStep) async { var ids = declareOutputs(buildStep.input.id); @@ -24,7 +26,11 @@ class CopyBuilder implements Builder { } return outputs; } -} -AssetId _copiedAssetId(AssetId inputId, int copyNum) => - inputId.addExtension('.copy${copyNum == null ? '' : '.$copyNum'}'); + AssetId _copiedAssetId(AssetId inputId, int copyNum) { + var withExtension = inputId + .addExtension('.$extension${copyNum == null ? '' : '.$copyNum'}'); + if (outputPackage == null) return withExtension; + return new AssetId(outputPackage, withExtension.path); + } +} diff --git a/test/common/in_memory_reader.dart b/test/common/in_memory_reader.dart index b2c23a2b6..50fb5f8b1 100644 --- a/test/common/in_memory_reader.dart +++ b/test/common/in_memory_reader.dart @@ -11,12 +11,25 @@ class InMemoryAssetReader implements AssetReader { InMemoryAssetReader(this.assets); + @override Future hasInput(AssetId id) { return new Future.value(assets.containsKey(id)); } + @override Future readAsString(AssetId id, {Encoding encoding: UTF8}) async { if (!await hasInput(id)) throw new AssetNotFoundException(id); return assets[id]; } + + @override + Stream listAssetIds(Iterable inputSets) async* { + for (var id in assets.keys) { + var matches = inputSets.any((inputSet) { + if (inputSet.package != id.package) return false; + return inputSet.globs.any((glob) => glob.matches(id.path)); + }); + if (matches) yield id; + } + } } diff --git a/test/common/stub_reader.dart b/test/common/stub_reader.dart index ea208e120..2e291bf05 100644 --- a/test/common/stub_reader.dart +++ b/test/common/stub_reader.dart @@ -7,8 +7,12 @@ import 'dart:convert'; import 'package:build/build.dart'; class StubAssetReader implements AssetReader { + @override Future hasInput(AssetId id) => new Future.value(null); + @override Future readAsString(AssetId id, {Encoding encoding: UTF8}) => new Future.value(null); + + Stream listAssetIds(_) async* {} } diff --git a/test/generate/build_test.dart b/test/generate/build_test.dart new file mode 100644 index 000000000..70dafa08f --- /dev/null +++ b/test/generate/build_test.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:test/test.dart'; + +import 'package:build/build.dart'; + +import '../common/common.dart'; + +main() { + group('build', () { + group('with root package inputs', () { + test('one phase, one builder, one-to-one outputs', () async { + var phases = [ + [ + new Phase([new CopyBuilder()], [new InputSet('a')]), + ] + ]; + await testPhases(phases, {'a|web/a.txt': 'a', 'a|lib/b.txt': 'b',}, + outputs: {'a|web/a.txt.copy': 'a', 'a|lib/b.txt.copy': 'b',}); + }); + + test('one phase, one builder, one-to-many outputs', () async { + var phases = [ + [ + new Phase([new CopyBuilder(numCopies: 2)], [new InputSet('a')]), + ] + ]; + await testPhases(phases, { + 'a|web/a.txt': 'a', + 'a|lib/b.txt': 'b', + }, outputs: { + 'a|web/a.txt.copy.0': 'a', + 'a|web/a.txt.copy.1': 'a', + 'a|lib/b.txt.copy.0': 'b', + 'a|lib/b.txt.copy.1': 'b', + }); + }); + + test('one phase, multiple builders', () async { + var phases = [ + [ + new Phase([new CopyBuilder(), new CopyBuilder(extension: 'clone')], + [new InputSet('a')]), + new Phase([new CopyBuilder(numCopies: 2)], [new InputSet('a')]), + ] + ]; + await testPhases(phases, { + 'a|web/a.txt': 'a', + 'a|lib/b.txt': 'b', + }, outputs: { + 'a|web/a.txt.copy': 'a', + 'a|web/a.txt.clone': 'a', + 'a|lib/b.txt.copy': 'b', + 'a|lib/b.txt.clone': 'b', + 'a|web/a.txt.copy.0': 'a', + 'a|web/a.txt.copy.1': 'a', + 'a|lib/b.txt.copy.0': 'b', + 'a|lib/b.txt.copy.1': 'b', + }); + }, skip: 'Failing, outputs from phases in same group are available'); + + test('multiple phases, multiple builders', () async { + var phases = [ + [ + new Phase([new CopyBuilder()], [new InputSet('a')]), + ], + [ + new Phase([ + new CopyBuilder(extension: 'clone') + ], [ + new InputSet('a', filePatterns: ['**/*.txt']) + ]), + ], + [ + new Phase([ + new CopyBuilder(numCopies: 2) + ], [ + new InputSet('a', filePatterns: ['web/*.txt.clone']) + ]), + ] + ]; + await testPhases(phases, { + 'a|web/a.txt': 'a', + 'a|lib/b.txt': 'b', + }, outputs: { + 'a|web/a.txt.copy': 'a', + 'a|web/a.txt.clone': 'a', + 'a|lib/b.txt.copy': 'b', + 'a|lib/b.txt.clone': 'b', + 'a|web/a.txt.clone.copy.0': 'a', + 'a|web/a.txt.clone.copy.1': 'a', + }); + }); + }); + + group('inputs from other packages', () { + test('only gets inputs from lib, can output to root package', () async { + var phases = [ + [ + new Phase( + [new CopyBuilder(outputPackage: 'a')], [new InputSet('b')]), + ] + ]; + await testPhases(phases, {'b|web/b.txt': 'b', 'b|lib/b.txt': 'b',}, + outputs: {'a|lib/b.txt.copy': 'b',}); + }); + + test('can\'t output files in non-root packages', () async { + var phases = [ + [ + new Phase([new CopyBuilder()], [new InputSet('b')]), + ] + ]; + await testPhases(phases, {'b|lib/b.txt': 'b',}, + outputs: {}, status: BuildStatus.Failure); + }, + skip: 'Failing, InMemoryAssetWriter doesn\'t throw. Need to handle ' + 'this in BuildStep instead of AssetWriter.'); + }); + + test('multiple phases, inputs from multiple packages', () async { + var phases = [ + [ + new Phase([new CopyBuilder()], [new InputSet('a')]), + new Phase([ + new CopyBuilder(extension: 'clone', outputPackage: 'a') + ], [ + new InputSet('b', filePatterns: ['**/*.txt']), + ]), + ], + [ + new Phase([ + new CopyBuilder(numCopies: 2, outputPackage: 'a') + ], [ + new InputSet('a', filePatterns: ['lib/*.txt.*']), + new InputSet('b', filePatterns: ['**/*.dart']), + ]), + ] + ]; + await testPhases(phases, { + 'a|web/1.txt': '1', + 'a|lib/2.txt': '2', + 'b|lib/3.txt': '3', + 'b|lib/b.dart': 'main() {}', + }, outputs: { + 'a|web/1.txt.copy': '1', + 'a|lib/2.txt.copy': '2', + 'a|lib/3.txt.clone': '3', + 'a|lib/2.txt.copy.copy.0': '2', + 'a|lib/2.txt.copy.copy.1': '2', + 'a|lib/3.txt.clone.copy.0': '3', + 'a|lib/3.txt.clone.copy.1': '3', + 'a|lib/b.dart.copy.0': 'main() {}', + 'a|lib/b.dart.copy.1': 'main() {}', + }); + }); + }); + + group('errors', () { + test('when overwriting files', () async { + var phases = [ + [ + new Phase([new CopyBuilder()], [new InputSet('a')]), + ] + ]; + await testPhases(phases, {'a|lib/a.txt': 'a', 'a|lib/a.txt.copy': 'a'}, + status: BuildStatus.Failure, + exceptionMatcher: invalidOutputException); + }, + skip: 'InMemoryAssetWriter doesn\'t check this, should be handled at ' + 'the BuildStep level.'); + }); + + test('outputs from builds shouldn\'t be inputs to later ones', () {}, + skip: 'Unimplemented'); +} + +testPhases(List> phases, Map inputs, + {Map outputs, + PackageGraph packageGraph, + BuildStatus status: BuildStatus.Success, + exceptionMatcher}) async { + final writer = new InMemoryAssetWriter(); + final actualAssets = writer.assets; + final reader = new InMemoryAssetReader(actualAssets); + inputs.forEach((serializedId, contents) { + writer.writeAsString(makeAsset(serializedId, contents)); + }); + + if (packageGraph == null) { + var rootPackage = new PackageNode('a', null, null, null); + packageGraph = new PackageGraph.fromRoot(rootPackage); + } + + var result = await build(phases, + reader: reader, writer: writer, packageGraph: packageGraph); + expect(result.status, status); + if (exceptionMatcher != null) { + expect(result.exception, exceptionMatcher); + } + + if (outputs != null) { + var remainingOutputIds = + new List.from(result.outputs.map((asset) => asset.id)); + outputs.forEach((serializedId, contents) { + var asset = makeAsset(serializedId, contents); + remainingOutputIds.remove(asset.id); + + /// Check that the writer wrote the assets + expect(actualAssets, contains(asset.id)); + expect(actualAssets[asset.id], asset.stringContents); + + /// Check that the assets exist in [result.outputs]. + var actual = result.outputs + .firstWhere((output) => output.id == asset.id, orElse: () => null); + expect(actual, isNotNull); + expect(asset, equalsAsset(actual)); + }); + + expect(remainingOutputIds, isEmpty, + reason: 'Unexpected outputs found `$remainingOutputIds`.'); + } +} From 85c3c608b71038cc2afc16e4b61da8996435c916 Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Fri, 5 Feb 2016 12:11:35 -0800 Subject: [PATCH 2/3] add listFiles test for FileBasedAssetReader --- test/asset/file_based_test.dart | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/asset/file_based_test.dart b/test/asset/file_based_test.dart index b63e48fef..85c30129f 100644 --- a/test/asset/file_based_test.dart +++ b/test/asset/file_based_test.dart @@ -15,7 +15,7 @@ final packageGraph = new PackageGraph.forPath('test/fixtures/basic_pkg'); main() { group('FileBasedAssetReader', () { - final reader = new FileBasedAssetReader(packageGraph); + final reader = new FileBasedAssetReader(packageGraph, ignoredDirs: ['pkg']); test('can read any application package files', () async { expect(await reader.readAsString(makeAssetId('basic_pkg|hello.txt')), @@ -66,6 +66,29 @@ main() { expect(reader.readAsString(makeAssetId('foo|lib/bar.txt')), throwsA(packageNotFoundException)); }); + + test('can list files based on simple InputSets', () async { + var inputSets = [ + new InputSet('basic_pkg'), + new InputSet('a'), + ]; + expect(await reader.listAssetIds(inputSets).toList(), unorderedEquals([ + makeAssetId('basic_pkg|lib/hello.txt'), + makeAssetId('basic_pkg|web/hello.txt'), + makeAssetId('a|lib/a.txt'), + ])); + }, skip: 'Fails for multiple reasons.'); + + test('can list files based on InputSets with globs', () async { + var inputSets = [ + new InputSet('basic_pkg', filePatterns: ['web/*.txt']), + new InputSet('a', filePatterns: ['lib/*']), + ]; + expect(await reader.listAssetIds(inputSets).toList(), unorderedEquals([ + makeAssetId('basic_pkg|web/hello.txt'), + makeAssetId('a|lib/a.txt'), + ])); + }); }); group('FileBasedAssetWriter', () { From f382599aa639ab5f6907b80a6ddfcc840e1bab2b Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Fri, 5 Feb 2016 12:25:31 -0800 Subject: [PATCH 3/3] code review updates --- lib/src/asset/file_based.dart | 10 ++++++---- test/generate/build_test.dart | 16 +++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/src/asset/file_based.dart b/lib/src/asset/file_based.dart index 655c760b7..8839d55d6 100644 --- a/lib/src/asset/file_based.dart +++ b/lib/src/asset/file_based.dart @@ -56,10 +56,12 @@ class FileBasedAssetReader implements AssetReader { var packageNode = packageGraph[inputSet.package]; var packagePath = packageNode.location.toFilePath(); for (var glob in inputSet.globs) { - var files = glob.listSync(followLinks: false, root: packagePath).where( - (e) => e is File && !ignoredDirs.contains(path.split(e.path)[1])); - for (var file in files) { - var id = _fileToAssetId(file, packageNode); + var fileStream = glob + .list(followLinks: false, root: packagePath) + .where((e) => + e is File && !ignoredDirs.contains(path.split(e.path)[1])); + await for (var entity in fileStream) { + var id = _fileToAssetId(entity, packageNode); if (!seenAssets.add(id)) continue; yield id; } diff --git a/test/generate/build_test.dart b/test/generate/build_test.dart index 70dafa08f..0f64e838b 100644 --- a/test/generate/build_test.dart +++ b/test/generate/build_test.dart @@ -16,8 +16,8 @@ main() { new Phase([new CopyBuilder()], [new InputSet('a')]), ] ]; - await testPhases(phases, {'a|web/a.txt': 'a', 'a|lib/b.txt': 'b',}, - outputs: {'a|web/a.txt.copy': 'a', 'a|lib/b.txt.copy': 'b',}); + await testPhases(phases, {'a|web/a.txt': 'a', 'a|lib/b.txt': 'b'}, + outputs: {'a|web/a.txt.copy': 'a', 'a|lib/b.txt.copy': 'b'}); }); test('one phase, one builder, one-to-many outputs', () async { @@ -102,8 +102,8 @@ main() { [new CopyBuilder(outputPackage: 'a')], [new InputSet('b')]), ] ]; - await testPhases(phases, {'b|web/b.txt': 'b', 'b|lib/b.txt': 'b',}, - outputs: {'a|lib/b.txt.copy': 'b',}); + await testPhases(phases, {'b|web/b.txt': 'b', 'b|lib/b.txt': 'b'}, + outputs: {'a|lib/b.txt.copy': 'b'}); }); test('can\'t output files in non-root packages', () async { @@ -112,7 +112,7 @@ main() { new Phase([new CopyBuilder()], [new InputSet('b')]), ] ]; - await testPhases(phases, {'b|lib/b.txt': 'b',}, + await testPhases(phases, {'b|lib/b.txt': 'b'}, outputs: {}, status: BuildStatus.Failure); }, skip: 'Failing, InMemoryAssetWriter doesn\'t throw. Need to handle ' @@ -172,7 +172,8 @@ main() { 'the BuildStep level.'); }); - test('outputs from builds shouldn\'t be inputs to later ones', () {}, + test('outputs from previous full builds shouldn\'t be inputs to later ones', + () {}, skip: 'Unimplemented'); } @@ -214,7 +215,8 @@ testPhases(List> phases, Map inputs, /// Check that the assets exist in [result.outputs]. var actual = result.outputs .firstWhere((output) => output.id == asset.id, orElse: () => null); - expect(actual, isNotNull); + expect(actual, isNotNull, + reason: 'Expected to find ${asset.id} in ${result.outputs}.'); expect(asset, equalsAsset(actual)); });