Skip to content

add some basic build tests to get started, as well as a basic test framework #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 5, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/build.dart
Original file line number Diff line number Diff line change
@@ -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';
5 changes: 5 additions & 0 deletions lib/src/asset/cache.dart
Original file line number Diff line number Diff line change
@@ -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<AssetId> listAssetIds(List<InputSet> inputSets) =>
_reader.listAssetIds(inputSets);
}

/// An [AssetWriter] which takes both an [AssetCache] and an [AssetWriter]. It
35 changes: 34 additions & 1 deletion lib/src/asset/file_based.dart
Original file line number Diff line number Diff line change
@@ -12,15 +12,17 @@ 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';

/// Basic [AssetReader] which uses a [PackageGraph] to look up where to read
/// files from disk.
class FileBasedAssetReader implements AssetReader {
final PackageGraph packageGraph;
final ignoredDirs;

FileBasedAssetReader(this.packageGraph);
FileBasedAssetReader(this.packageGraph, {this.ignoredDirs: const ['build']});

@override
Future<bool> hasInput(AssetId id) async {
@@ -46,6 +48,37 @@ class FileBasedAssetReader implements AssetReader {
throw new InvalidInputException(id);
}
}

/// Searches for all [AssetId]s matching [inputSet]s.
Stream<AssetId> listAssetIds(Iterable<InputSet> inputSets) async* {
var seenAssets = new Set<AssetId>();
for (var inputSet in inputSets) {
var packageNode = packageGraph[inputSet.package];
var packagePath = packageNode.location.toFilePath();
for (var glob in inputSet.globs) {
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;
}
}
}
}
}

/// 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
3 changes: 3 additions & 0 deletions lib/src/asset/reader.dart
Original file line number Diff line number Diff line change
@@ -4,10 +4,13 @@
import 'dart:async';
import 'dart:convert';

import '../generate/input_set.dart';
import 'id.dart';

abstract class AssetReader {
Future<String> readAsString(AssetId id, {Encoding encoding: UTF8});

Future<bool> hasInput(AssetId id);

Stream<AssetId> listAssetIds(List<InputSet> inputSets);
}
52 changes: 11 additions & 41 deletions lib/src/generate/build.dart
Original file line number Diff line number Diff line change
@@ -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,14 +41,14 @@ Future<BuildResult> build(List<List<Phase>> 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 ??=
new CachedAssetReader(cache, new FileBasedAssetReader(packageGraph));
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<List<Phase>> 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<BuildResult>] which completes
/// once all [Phase]s are done.
Future<BuildResult> _runPhases(List<List<Phase>> 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<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
return new BuildResult(BuildStatus.Success, BuildType.Full, outputs);
}

/// Gets all [AssetId]s matching [inputSets] in the current package.
List<AssetId> _assetIdsFor(List<InputSet> 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<File> _filesMatching(InputSet inputSet) {
var files = new Set<File>();
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<Asset> _runBuilder(Builder builder, List<AssetId> inputs) async* {
Stream<Asset> _runBuilder(Builder builder, Iterable<AssetId> inputs) async* {
for (var input in inputs) {
var expectedOutputs = builder.declareOutputs(input);
var inputAsset = new Asset(input, await _reader.readAsString(input));
4 changes: 2 additions & 2 deletions lib/src/generate/build_result.dart
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 7 additions & 2 deletions lib/src/generate/input_set.dart
Original file line number Diff line number Diff line change
@@ -17,6 +17,11 @@ class InputSet {
final List<Glob> globs;

InputSet(this.package, {Iterable<String> filePatterns})
: this.globs = new List.unmodifiable(
filePatterns.map((pattern) => new Glob(pattern)));
: this.globs = _globsFor(filePatterns);
}

List<Glob> _globsFor(Iterable<String> filePatterns) {
filePatterns ??= ['**/*'];
return new List.unmodifiable(
filePatterns.map((pattern) => new Glob(pattern)));
}
4 changes: 4 additions & 0 deletions lib/src/transformer/transformer.dart
Original file line number Diff line number Diff line change
@@ -97,6 +97,10 @@ class _TransformAssetReader implements AssetReader {
@override
Future<String> 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<build.AssetId> listAssetIds(_) => throw new UnimplementedError();
}

/// Very simple [AssetWriter] which uses a [Transform].
25 changes: 24 additions & 1 deletion test/asset/file_based_test.dart
Original file line number Diff line number Diff line change
@@ -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', () {
16 changes: 11 additions & 5 deletions test/common/copy_builder.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 13 additions & 0 deletions test/common/in_memory_reader.dart
Original file line number Diff line number Diff line change
@@ -11,12 +11,25 @@ class InMemoryAssetReader implements AssetReader {

InMemoryAssetReader(this.assets);

@override
Future<bool> hasInput(AssetId id) {
return new Future.value(assets.containsKey(id));
}

@override
Future<String> readAsString(AssetId id, {Encoding encoding: UTF8}) async {
if (!await hasInput(id)) throw new AssetNotFoundException(id);
return assets[id];
}

@override
Stream<AssetId> listAssetIds(Iterable<InputSet> 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;
}
}
}
4 changes: 4 additions & 0 deletions test/common/stub_reader.dart
Original file line number Diff line number Diff line change
@@ -7,8 +7,12 @@ import 'dart:convert';
import 'package:build/build.dart';

class StubAssetReader implements AssetReader {
@override
Future<bool> hasInput(AssetId id) => new Future.value(null);

@override
Future<String> readAsString(AssetId id, {Encoding encoding: UTF8}) =>
new Future.value(null);

Stream<AssetId> listAssetIds(_) async* {}
}
226 changes: 226 additions & 0 deletions test/generate/build_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// 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 '
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird indentation from dartfmt? :-/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya :/

I can file a bug in a bit

'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 previous full builds shouldn\'t be inputs to later ones',
() {},
skip: 'Unimplemented');
}

testPhases(List<List<Phase>> phases, Map<String, String> inputs,
{Map<String, String> 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,
reason: 'Expected to find ${asset.id} in ${result.outputs}.');
expect(asset, equalsAsset(actual));
});

expect(remainingOutputIds, isEmpty,
reason: 'Unexpected outputs found `$remainingOutputIds`.');
}
}