blob: 31ee456c68553f3ce1c6688767d603c947281433 [file] [log] [blame]
// Copyright (c) 2014, 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';
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'assets.dart';
import 'in_memory_reader.dart';
import 'in_memory_writer.dart';
import 'multi_asset_reader.dart';
import 'resolve_source.dart';
import 'written_asset_reader.dart';
AssetId _passThrough(AssetId id) => id;
/// Validates that [actualAssets] matches the expected [outputs].
///
/// The keys in [outputs] should be serialized [AssetId]s in the form
/// `'package|path'`. The values should match the expected content for the
/// written asset and may be a String (for `writeAsString`), a `List<int>` (for
/// `writeAsBytes`) or a [Matcher] for a String or bytes.
///
/// [actualAssets] are the IDs that were recorded as written during the build.
///
/// Assets are checked against those that were written to [writer]. If other
/// assets were written through the writer, but not as part of the build
/// process, they will be ignored. Only the IDs in [actualAssets] are checked.
///
/// If assets are written to a location that does not match their logical
/// association to a package pass [mapAssetIds] to translate from the logical
/// location to the actual written location.
void checkOutputs(
Map<String, /*List<int>|String|Matcher<String|List<int>>*/ dynamic> outputs,
Iterable<AssetId> actualAssets,
RecordingAssetWriter writer,
{AssetId Function(AssetId id) mapAssetIds = _passThrough}) {
var modifiableActualAssets = Set.from(actualAssets);
if (outputs != null) {
outputs.forEach((serializedId, contentsMatcher) {
assert(contentsMatcher is String ||
contentsMatcher is List<int> ||
contentsMatcher is Matcher);
var assetId = makeAssetId(serializedId);
// Check that the asset was produced.
expect(modifiableActualAssets, contains(assetId),
reason: 'Builder failed to write asset $assetId');
modifiableActualAssets.remove(assetId);
var actual = writer.assets[mapAssetIds(assetId)];
Object expected;
if (contentsMatcher is String) {
expected = utf8.decode(actual);
} else if (contentsMatcher is List<int>) {
expected = actual;
} else if (contentsMatcher is Matcher) {
expected = actual;
} else {
throw ArgumentError('Expected values for `outputs` to be of type '
'`String`, `List<int>`, or `Matcher`, but got `$contentsMatcher`.');
}
expect(expected, contentsMatcher,
reason: 'Unexpected content for $assetId in result.outputs.');
});
// Check that no extra assets were produced.
expect(modifiableActualAssets, isEmpty,
reason:
'Unexpected outputs found `$actualAssets`. Only expected $outputs');
}
}
/// Runs [builder] in a test environment.
///
/// The test environment supplies in-memory build [sourceAssets] to the builders
/// under test. [outputs] may be optionally provided to verify that the builders
/// produce the expected output. If [outputs] is omitted the only validation
/// this method provides is that the build did not `throw`.
///
/// Either [generateFor] or the [isInput] callback can specify which assets
/// should be given as inputs to the builder. These can be omitted if every
/// asset in [sourceAssets] should be considered an input. [generateFor] is
/// ignored if both [isInput] and [generateFor] are provided.
///
/// The keys in [sourceAssets] and [outputs] are paths to file assets and the
/// values are file contents. The paths must use the following format:
///
/// PACKAGE_NAME|PATH_WITHIN_PACKAGE
///
/// Where `PACKAGE_NAME` is the name of the package, and `PATH_WITHIN_PACKAGE`
/// is the path to a file relative to the package. `PATH_WITHIN_PACKAGE` must
/// include `lib`, `web`, `bin` or `test`. Example: "myapp|lib/utils.dart".
///
/// If a [reader] is provided, then any asset not in [sourceAssets] will be
/// read from the provided reader. This allows you to more easily provide
/// sources of entire packages to the test, instead of mocking them out, for
/// example, this exposes all assets available to the test itself:
///
///
/// ```dart
/// testBuilder(yourBuilder, {}/* test assets here */,
/// reader: await PackageAssetReader.currentIsolate());
/// ```
///
/// Callers may optionally provide a [writer] to stub different behavior or do
/// more complex validation than what is possible with [outputs].
///
/// Callers may optionally provide an [onLog] callback to do validaiton on the
/// logging output of the builder.
Future testBuilder(
Builder builder, Map<String, /*String|List<int>*/ dynamic> sourceAssets,
{Set<String> generateFor,
bool Function(String assetId) isInput,
String rootPackage,
MultiPackageAssetReader reader,
RecordingAssetWriter writer,
Map<String, /*String|List<int>|Matcher<String|List<int>>*/ dynamic> outputs,
void Function(LogRecord log) onLog,
void Function(AssetId, Iterable<AssetId>)
reportUnusedAssetsForInput}) async {
writer ??= InMemoryAssetWriter();
var inputIds = {
for (var descriptor in sourceAssets.keys) makeAssetId(descriptor)
};
var allPackages = {for (var id in inputIds) id.package};
if (allPackages.length == 1) rootPackage ??= allPackages.first;
inputIds.addAll([
for (var package in allPackages) AssetId(package, r'lib/$lib$'),
if (rootPackage != null) ...[
AssetId(rootPackage, r'$package$'),
AssetId(rootPackage, r'test/$test$'),
AssetId(rootPackage, r'web/$web$'),
]
]);
final inMemoryReader = InMemoryAssetReader(rootPackage: rootPackage);
sourceAssets.forEach((serializedId, contents) {
var id = makeAssetId(serializedId);
if (contents is String) {
inMemoryReader.cacheStringAsset(id, contents);
} else if (contents is List<int>) {
inMemoryReader.cacheBytesAsset(id, contents);
}
});
isInput ??= generateFor?.contains ?? (_) => true;
inputIds.retainWhere((id) => isInput('$id'));
var writerSpy = AssetWriterSpy(writer);
var logger = Logger('testBuilder');
var logSubscription = logger.onRecord.listen(onLog);
for (var input in inputIds) {
// create another writer spy and reader for each input. This prevents writes
// from a previous input being readable when processing the current input.
final spyForStep = AssetWriterSpy(writerSpy);
final readerForStep = MultiAssetReader([
inMemoryReader,
if (reader != null) reader,
WrittenAssetReader(writer, spyForStep),
]);
await runBuilder(
builder, {input}, readerForStep, spyForStep, defaultResolvers,
logger: logger, reportUnusedAssetsForInput: reportUnusedAssetsForInput);
}
await logSubscription.cancel();
var actualOutputs = writerSpy.assetsWritten;
checkOutputs(outputs, actualOutputs, writer);
}