diff --git a/lib/build.dart b/lib/build.dart index 0e5b4c162..fd60cb242 100644 --- a/lib/build.dart +++ b/lib/build.dart @@ -2,9 +2,13 @@ // 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. export 'src/asset/asset.dart'; +export 'src/asset/exceptions.dart'; export 'src/asset/id.dart'; +export 'src/asset/reader.dart'; +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_result.dart'; export 'src/generate/build.dart'; export 'src/generate/input_set.dart'; diff --git a/lib/src/asset/exceptions.dart b/lib/src/asset/exceptions.dart new file mode 100644 index 000000000..4051a1b68 --- /dev/null +++ b/lib/src/asset/exceptions.dart @@ -0,0 +1,13 @@ +// 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 'id.dart'; + +class AssetNotFoundException implements Exception { + final AssetId assetId; + + AssetNotFoundException(this.assetId); + + @override + String toString() => 'AssetNotFoundException: $assetId'; +} diff --git a/lib/src/builder/build_step_impl.dart b/lib/src/builder/build_step_impl.dart index 48b842fc4..6ccbcd21d 100644 --- a/lib/src/builder/build_step_impl.dart +++ b/lib/src/builder/build_step_impl.dart @@ -10,6 +10,7 @@ import '../asset/id.dart'; import '../asset/reader.dart'; import '../asset/writer.dart'; import 'build_step.dart'; +import 'exceptions.dart'; /// A single step in the build processes. This represents a single input and /// its expected and real outputs. It also handles tracking of dependencies. @@ -61,8 +62,14 @@ class BuildStepImpl implements BuildStep { /// Outputs an [Asset] using the current [AssetWriter], and adds [asset] to /// [outputs]. + /// + /// Throws an [UnexpectedOutputException] if [asset] is not in + /// [expectedOutputs]. @override void writeAsString(Asset asset, {Encoding encoding: UTF8}) { + if (!expectedOutputs.any((id) => id == asset.id)) { + throw new UnexpectedOutputException(asset); + } _outputs.add(asset); var done = _writer.writeAsString(asset, encoding: encoding); _outputsCompleted = _outputsCompleted.then((_) => done); diff --git a/lib/src/builder/exceptions.dart b/lib/src/builder/exceptions.dart new file mode 100644 index 000000000..3906cb9bc --- /dev/null +++ b/lib/src/builder/exceptions.dart @@ -0,0 +1,13 @@ +// 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 '../asset/asset.dart'; + +class UnexpectedOutputException implements Exception { + final Asset asset; + + UnexpectedOutputException(this.asset); + + @override + String toString() => 'UnexpectedOutputException: $asset'; +} diff --git a/test/builder/build_step_impl_test.dart b/test/builder/build_step_impl_test.dart new file mode 100644 index 000000000..0cff6eaf5 --- /dev/null +++ b/test/builder/build_step_impl_test.dart @@ -0,0 +1,111 @@ +// 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. +@TestOn('vm') +import 'package:test/test.dart'; + +import 'package:build/build.dart'; +import 'package:build/src/builder/build_step_impl.dart'; + +import '../common/common.dart'; + +main() { + group('BuildStepImpl ', () { + AssetWriter writer; + AssetReader reader; + + group('with reader/writer stub', () { + Asset primary; + BuildStepImpl buildStep; + + setUp(() { + reader = new StubAssetReader(); + writer = new StubAssetWriter(); + primary = makeAsset(); + buildStep = new BuildStepImpl(primary, [], reader, writer); + }); + + test('tracks dependencies on read', () async { + expect(buildStep.dependencies, [primary.id]); + + var a1 = makeAssetId(); + await buildStep.readAsString(a1); + expect(buildStep.dependencies, [primary.id, a1]); + + var a2 = makeAssetId(); + await buildStep.readAsString(a2); + expect(buildStep.dependencies, [primary.id, a1, a2]); + }); + + test('addDependency adds dependencies', () { + expect(buildStep.dependencies, [primary.id]); + + var a1 = makeAssetId(); + buildStep.addDependency(a1); + expect(buildStep.dependencies, [primary.id, a1]); + + var a2 = makeAssetId(); + buildStep.addDependency(a2); + expect(buildStep.dependencies, [primary.id, a1, a2]); + }); + + test('tracks outputs', () async { + var a1 = makeAsset(); + var a2 = makeAsset(); + buildStep = new BuildStepImpl(primary, [a1.id, a2.id], reader, writer); + + buildStep.writeAsString(a1); + expect(buildStep.outputs, [a1]); + + buildStep.writeAsString(a2); + expect(buildStep.outputs, [a1, a2]); + + expect(buildStep.outputsCompleted, completes); + }); + + test('doesnt allow non-expected outputs', () { + var asset = makeAsset(); + expect(() => buildStep.writeAsString(asset), + throwsA(new isInstanceOf())); + }); + }); + + group('with in memory file system', () { + InMemoryAssetWriter writer; + InMemoryAssetReader reader; + + setUp(() { + writer = new InMemoryAssetWriter(); + reader = new InMemoryAssetReader(writer.assets); + }); + + test('tracks dependencies and outputs when used by a builder', () async { + var fileCombiner = new FileCombinerBuilder(); + var primary = 'a|web/primary.txt'; + var unUsed = 'a|web/not_used.txt'; + var inputs = makeAssets({ + primary: 'a|web/a.txt\na|web/b.txt', + 'a|web/a.txt': 'A', + 'a|web/b.txt': 'B', + unUsed: 'C', + }); + addAssets(inputs.values, writer); + var outputId = new AssetId.parse('$primary.combined'); + var buildStep = new BuildStepImpl( + inputs[new AssetId.parse(primary)], [outputId], reader, writer); + + await fileCombiner.build(buildStep); + await buildStep.outputsCompleted; + + // All the assets should be read and marked as deps, except [unUsed]. + expect(buildStep.dependencies, + inputs.keys.where((k) => k != new AssetId.parse(unUsed))); + + // One output. + expect(buildStep.outputs[0].id, outputId); + expect(buildStep.outputs[0].stringContents, 'AB'); + expect(writer.assets[outputId], 'AB'); + }); + }); + }); +} diff --git a/test/common/common.dart b/test/common/common.dart index 9b3b9b4fa..2f5f3fb09 100644 --- a/test/common/common.dart +++ b/test/common/common.dart @@ -1,5 +1,43 @@ // 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:build/build.dart'; + +import 'in_memory_writer.dart'; + export 'copy_builder.dart'; +export 'file_combiner_builder.dart'; +export 'in_memory_reader.dart'; +export 'in_memory_writer.dart'; export 'generic_builder_transformer.dart'; +export 'stub_reader.dart'; +export 'stub_writer.dart'; + +int _nextId = 0; +AssetId makeAssetId([String assetIdString]) { + if (assetIdString == null) { + assetIdString = 'a|web/asset_$_nextId.txt'; + _nextId++; + } + return new AssetId.parse(assetIdString); +} + +Asset makeAsset([String assetIdString, String contents]) { + var id = makeAssetId(assetIdString); + return new Asset(id, contents ?? '$id'); +} + +Map makeAssets(Map assetsMap) { + var assets = {}; + assetsMap.forEach((idString, content) { + var asset = makeAsset(idString, content); + assets[asset.id] = asset; + }); + return assets; +} + +void addAssets(Iterable assets, InMemoryAssetWriter writer) { + for (var asset in assets) { + writer.assets[asset.id] = asset.stringContents; + } +} diff --git a/test/common/file_combiner_builder.dart b/test/common/file_combiner_builder.dart new file mode 100644 index 000000000..f07a26aa3 --- /dev/null +++ b/test/common/file_combiner_builder.dart @@ -0,0 +1,28 @@ +// 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 'dart:async'; + +import 'package:build/build.dart'; + +/// Takes an input file which points at a bunch of other files, and then it +/// writes an `$input.combined` file which concats all the files. +class FileCombinerBuilder implements Builder { + Future build(BuildStep buildStep) async { + var lines = buildStep.input.stringContents.split('\n'); + var content = new StringBuffer(); + for (var line in lines) { + content.write(await buildStep.readAsString(new AssetId.parse(line))); + } + + var outputId = _combinedAssetId(buildStep.input.id); + buildStep.writeAsString(new Asset(outputId, content.toString())); + } + + List declareOutputs(AssetId input) { + return [_combinedAssetId(input)]; + } +} + +AssetId _combinedAssetId(AssetId inputId) => + inputId.addExtension('.combined'); diff --git a/test/common/in_memory_reader.dart b/test/common/in_memory_reader.dart new file mode 100644 index 000000000..df6d6a919 --- /dev/null +++ b/test/common/in_memory_reader.dart @@ -0,0 +1,18 @@ +// 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 'dart:async'; +import 'dart:convert'; + +import 'package:build/build.dart'; + +class InMemoryAssetReader implements AssetReader { + final Map assets; + + InMemoryAssetReader(this.assets); + + Future readAsString(AssetId id, {Encoding encoding: UTF8}) async { + if (!assets.containsKey(id)) throw new AssetNotFoundException(id); + return assets[id]; + } +} diff --git a/test/common/in_memory_writer.dart b/test/common/in_memory_writer.dart new file mode 100644 index 000000000..a43f2e06b --- /dev/null +++ b/test/common/in_memory_writer.dart @@ -0,0 +1,17 @@ +// 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 'dart:async'; +import 'dart:convert'; + +import 'package:build/build.dart'; + +class InMemoryAssetWriter implements AssetWriter { + final Map assets = {}; + + InMemoryAssetWriter(); + + Future writeAsString(Asset asset, {Encoding encoding: UTF8}) async { + assets[asset.id] = asset.stringContents; + } +} diff --git a/test/common/stub_reader.dart b/test/common/stub_reader.dart new file mode 100644 index 000000000..660ac8990 --- /dev/null +++ b/test/common/stub_reader.dart @@ -0,0 +1,12 @@ +// 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 'dart:async'; +import 'dart:convert'; + +import 'package:build/build.dart'; + +class StubAssetReader implements AssetReader { + Future readAsString(AssetId id, {Encoding encoding: UTF8}) => + new Future.value(null); +} diff --git a/test/common/stub_writer.dart b/test/common/stub_writer.dart new file mode 100644 index 000000000..64d747665 --- /dev/null +++ b/test/common/stub_writer.dart @@ -0,0 +1,12 @@ +// 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 'dart:async'; +import 'dart:convert'; + +import 'package:build/build.dart'; + +class StubAssetWriter implements AssetWriter { + Future writeAsString(Asset asset, {Encoding encoding: UTF8}) => + new Future.value(null); +}