Index: packages/barback/test/utils.dart |
diff --git a/packages/barback/test/utils.dart b/packages/barback/test/utils.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c24b58c3e58e488a5b360453d673731c47d88b6e |
--- /dev/null |
+++ b/packages/barback/test/utils.dart |
@@ -0,0 +1,613 @@ |
+// Copyright (c) 2013, 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. |
+ |
+library barback.test.utils; |
+ |
+import 'dart:async'; |
+import 'dart:convert' show Encoding; |
+ |
+import 'package:barback/barback.dart'; |
+import 'package:barback/src/utils.dart'; |
+import 'package:barback/src/utils/cancelable_future.dart'; |
+import 'package:path/path.dart' as pathos; |
+import 'package:scheduled_test/scheduled_test.dart'; |
+import 'package:stack_trace/stack_trace.dart'; |
+import 'package:unittest/compact_vm_config.dart'; |
+ |
+export 'transformer/aggregate_many_to_many.dart'; |
+export 'transformer/aggregate_many_to_one.dart'; |
+export 'transformer/bad.dart'; |
+export 'transformer/bad_log.dart'; |
+export 'transformer/catch_asset_not_found.dart'; |
+export 'transformer/check_content.dart'; |
+export 'transformer/check_content_and_rename.dart'; |
+export 'transformer/conditionally_consume_primary.dart'; |
+export 'transformer/create_asset.dart'; |
+export 'transformer/declare_assets.dart'; |
+export 'transformer/declaring_aggregate_many_to_many.dart'; |
+export 'transformer/declaring_aggregate_many_to_one.dart'; |
+export 'transformer/declaring_bad.dart'; |
+export 'transformer/declaring_check_content_and_rename.dart'; |
+export 'transformer/declaring_rewrite.dart'; |
+export 'transformer/emit_nothing.dart'; |
+export 'transformer/has_input.dart'; |
+export 'transformer/lazy_aggregate_many_to_many.dart'; |
+export 'transformer/lazy_aggregate_many_to_one.dart'; |
+export 'transformer/lazy_assets.dart'; |
+export 'transformer/lazy_bad.dart'; |
+export 'transformer/lazy_check_content_and_rename.dart'; |
+export 'transformer/lazy_many_to_one.dart'; |
+export 'transformer/lazy_rewrite.dart'; |
+export 'transformer/many_to_one.dart'; |
+export 'transformer/mock.dart'; |
+export 'transformer/mock_aggregate.dart'; |
+export 'transformer/one_to_many.dart'; |
+export 'transformer/rewrite.dart'; |
+export 'transformer/sync_rewrite.dart'; |
+ |
+var _configured = false; |
+ |
+MockProvider _provider; |
+Barback _barback; |
+ |
+/// Calls to [buildShouldSucceed] and [buildShouldFail] set expectations on |
+/// successive [BuildResult]s from [_barback]. This keeps track of how many |
+/// calls have already been made so later calls know which result to look for. |
+int _nextBuildResult; |
+ |
+/// Calls to [buildShouldLog] set expectations on successive log entries from |
+/// [_barback]. This keeps track of how many calls have already been made so |
+/// later calls know which result to look for. |
+int _nextLog; |
+ |
+void initConfig() { |
+ if (_configured) return; |
+ _configured = true; |
+ useCompactVMConfiguration(); |
+ filterStacks = true; |
+} |
+ |
+/// Creates a new [PackageProvider] and [PackageGraph] with the given [assets] |
+/// and [transformers]. |
+/// |
+/// This graph is used internally by most of the other functions in this |
+/// library so you must call it in the test before calling any of the other |
+/// functions. |
+/// |
+/// [assets] may either be an [Iterable] or a [Map]. If it's an [Iterable], |
+/// each element may either be an [AssetId] or a string that can be parsed to |
+/// one. If it's a [Map], each key should be a string that can be parsed to an |
+/// [AssetId] and the value should be a string defining the contents of that |
+/// asset. |
+/// |
+/// [transformers] is a map from package names to the transformers for each |
+/// package. |
+void initGraph([assets, |
+ Map<String, Iterable<Iterable<Transformer>>> transformers]) => |
+ initStaticGraph(assets, transformers: transformers); |
+ |
+void initStaticGraph(assets, {Iterable<String> staticPackages, |
+ Map<String, Iterable<Iterable<Transformer>>> transformers}) { |
+ if (assets == null) assets = []; |
+ if (staticPackages == null) staticPackages = []; |
+ if (transformers == null) transformers = {}; |
+ |
+ _provider = new MockProvider(assets, |
+ staticPackages: staticPackages, |
+ additionalPackages: transformers.keys); |
+ _barback = new Barback(_provider); |
+ // Add a dummy listener to the log so it doesn't print to stdout. |
+ _barback.log.listen((_) {}); |
+ _nextBuildResult = 0; |
+ _nextLog = 0; |
+ |
+ schedule(() => transformers.forEach(_barback.updateTransformers)); |
+ |
+ // There should be one successful build after adding all the transformers but |
+ // before adding any sources. |
+ if (!transformers.isEmpty) buildShouldSucceed(); |
+} |
+ |
+/// Updates [assets] in the current [PackageProvider]. |
+/// |
+/// Each item in the list may either be an [AssetId] or a string that can be |
+/// parsed as one. |
+void updateSources(Iterable assets) { |
+ assets = _parseAssets(assets); |
+ schedule(() => _barback.updateSources(assets), |
+ "updating ${assets.join(', ')}"); |
+} |
+ |
+/// Updates [assets] in the current [PackageProvider]. |
+/// |
+/// Each item in the list may either be an [AssetId] or a string that can be |
+/// parsed as one. Unlike [updateSources], this is not automatically scheduled |
+/// and will be run synchronously when called. |
+void updateSourcesSync(Iterable assets) => |
+ _barback.updateSources(_parseAssets(assets)); |
+ |
+/// Removes [assets] from the current [PackageProvider]. |
+/// |
+/// Each item in the list may either be an [AssetId] or a string that can be |
+/// parsed as one. |
+void removeSources(Iterable assets) { |
+ assets = _parseAssets(assets); |
+ schedule(() => _barback.removeSources(assets), |
+ "removing ${assets.join(', ')}"); |
+} |
+ |
+/// Removes [assets] from the current [PackageProvider]. |
+/// |
+/// Each item in the list may either be an [AssetId] or a string that can be |
+/// parsed as one. Unlike [removeSources], this is not automatically scheduled |
+/// and will be run synchronously when called. |
+void removeSourcesSync(Iterable assets) => |
+ _barback.removeSources(_parseAssets(assets)); |
+ |
+/// Sets the transformers for [package] to [transformers]. |
+void updateTransformers(String package, Iterable<Iterable> transformers) { |
+ schedule(() => _barback.updateTransformers(package, transformers), |
+ "updating transformers for $package"); |
+} |
+ |
+/// Parse a list of strings or [AssetId]s into a list of [AssetId]s. |
+List<AssetId> _parseAssets(Iterable assets) { |
+ return assets.map((asset) { |
+ if (asset is String) return new AssetId.parse(asset); |
+ return asset; |
+ }).toList(); |
+} |
+ |
+/// Schedules a change to the contents of an asset identified by [name] to |
+/// [contents]. |
+/// |
+/// Does not update it in the graph. |
+void modifyAsset(String name, String contents) { |
+ schedule(() { |
+ _provider._modifyAsset(name, contents); |
+ }, "modify asset $name"); |
+} |
+ |
+/// Schedules an error to be generated when loading the asset identified by |
+/// [name]. |
+/// |
+/// Does not update the asset in the graph. If [async] is true, the error is |
+/// thrown asynchronously. |
+void setAssetError(String name, {bool async: true}) { |
+ schedule(() { |
+ _provider._setAssetError(name, async); |
+ }, "set error for asset $name"); |
+} |
+ |
+/// Schedules a pause of the internally created [PackageProvider]. |
+/// |
+/// All asset requests that the [PackageGraph] makes to the provider after this |
+/// will not complete until [resumeProvider] is called. |
+void pauseProvider() { |
+ schedule(() => _provider._pause(), "pause provider"); |
+} |
+ |
+/// Schedules an unpause of the provider after a call to [pauseProvider] and |
+/// allows all pending asset loads to finish. |
+void resumeProvider() { |
+ schedule(() => _provider._resume(), "resume provider"); |
+} |
+ |
+/// Asserts that the current build step shouldn't have finished by this point in |
+/// the schedule. |
+/// |
+/// This uses the same build counter as [buildShouldSucceed] and |
+/// [buildShouldFail], so those can be used to validate build results before and |
+/// after this. |
+void buildShouldNotBeDone() { |
+ _futureShouldNotCompleteUntil( |
+ _barback.results.elementAt(_nextBuildResult), |
+ schedule(() => pumpEventQueue(), "build should not terminate"), |
+ "build"); |
+} |
+ |
+/// Expects that the next [BuildResult] is a build success. |
+void buildShouldSucceed() { |
+ expect(_getNextBuildResult("build should succeed").then((result) { |
+ result.errors.forEach(currentSchedule.signalError); |
+ expect(result.succeeded, isTrue); |
+ }), completes); |
+} |
+ |
+/// Expects that the next [BuildResult] emitted is a failure. |
+/// |
+/// [matchers] is a list of matchers to match against the errors that caused the |
+/// build to fail. Every matcher is expected to match an error, but the order of |
+/// matchers is unimportant. |
+void buildShouldFail(List matchers) { |
+ expect(_getNextBuildResult("build should fail").then((result) { |
+ expect(result.succeeded, isFalse); |
+ expect(result.errors.length, equals(matchers.length)); |
+ for (var matcher in matchers) { |
+ expect(result.errors, contains(matcher)); |
+ } |
+ }), completes); |
+} |
+ |
+/// Expects that the nexted logged [LogEntry] matches [matcher] which may be |
+/// either a [Matcher] or a string to match a literal string. |
+void buildShouldLog(LogLevel level, matcher) { |
+ expect(_getNextLog("build should log").then((log) { |
+ expect(log.level, equals(level)); |
+ expect(log.message, matcher); |
+ }), completes); |
+} |
+ |
+Future<BuildResult> _getNextBuildResult(String description) { |
+ var result = currentSchedule.wrapFuture( |
+ _barback.results.elementAt(_nextBuildResult++)); |
+ return schedule(() => result, description); |
+} |
+ |
+Future<LogEntry> _getNextLog(String description) { |
+ var result = currentSchedule.wrapFuture( |
+ _barback.log.elementAt(_nextLog++)); |
+ return schedule(() => result, description); |
+} |
+ |
+/// Schedules an expectation that the graph will deliver an asset matching |
+/// [name] and [contents]. |
+/// |
+/// [contents] may be a [String] or a [Matcher] that matches a string. If |
+/// [contents] is omitted, defaults to the asset's filename without an extension |
+/// (which is the same default that [initGraph] uses). |
+void expectAsset(String name, [contents]) { |
+ var id = new AssetId.parse(name); |
+ |
+ if (contents == null) { |
+ contents = pathos.basenameWithoutExtension(id.path); |
+ } |
+ |
+ schedule(() { |
+ return _barback.getAssetById(id).then((asset) { |
+ // TODO(rnystrom): Make an actual Matcher class for this. |
+ expect(asset.id, equals(id)); |
+ expect(asset.readAsString(), completion(contents)); |
+ }); |
+ }, "get asset $name"); |
+} |
+ |
+/// Schedules an expectation that the graph will not find an asset matching |
+/// [name]. |
+void expectNoAsset(String name) { |
+ var id = new AssetId.parse(name); |
+ |
+ // Make sure the future gets the error. |
+ schedule(() { |
+ return _barback.getAssetById(id).then((asset) { |
+ fail("Should have thrown error but got $asset."); |
+ }).catchError((error) { |
+ expect(error, new isInstanceOf<AssetNotFoundException>()); |
+ expect(error.id, equals(id)); |
+ }); |
+ }, "get asset $name"); |
+} |
+ |
+/// Schedules an expectation that the graph will output all of the given |
+/// assets, and no others. |
+/// |
+/// [assets] may be an iterable of asset id strings, in which case this asserts |
+/// that the graph outputs exactly the assets with those ids. It may also be a |
+/// map from asset id strings to asset contents, in which case the contents must |
+/// also match. |
+void expectAllAssets(assets) { |
+ var expected; |
+ var expectedString; |
+ if (assets is Map) { |
+ expected = mapMapKeys(assets, (key, _) => new AssetId.parse(key)); |
+ expectedString = expected.toString(); |
+ } else { |
+ expected = assets.map((asset) => new AssetId.parse(asset)); |
+ expectedString = expected.join(', '); |
+ } |
+ |
+ schedule(() { |
+ return _barback.getAllAssets().then((actualAssets) { |
+ var actualIds = actualAssets.map((asset) => asset.id).toSet(); |
+ |
+ if (expected is Map) { |
+ expected.forEach((id, contents) { |
+ expect(actualIds, contains(id)); |
+ actualIds.remove(id); |
+ expect(actualAssets[id].readAsString(), completion(equals(contents))); |
+ }); |
+ } else { |
+ for (var id in expected) { |
+ expect(actualIds, contains(id)); |
+ actualIds.remove(id); |
+ } |
+ } |
+ |
+ expect(actualIds, isEmpty); |
+ }); |
+ }, "get all assets, expecting $expectedString"); |
+} |
+ |
+/// Schedules an expectation that [Barback.getAllAssets] will return a [Future] |
+/// that completes to a error that matches [matcher]. |
+/// |
+/// If [match] is a [List], then it expects the completed error to be an |
+/// [AggregateException] whose errors match each matcher in the list. Otherwise, |
+/// [match] should be a single matcher that the error should match. |
+void expectAllAssetsShouldFail(Matcher matcher) { |
+ schedule(() { |
+ expect(_barback.getAllAssets(), throwsA(matcher)); |
+ }, "get all assets should fail"); |
+} |
+ |
+/// Schedules an expectation that a [getAssetById] call for the given asset |
+/// won't terminate at this point in the schedule. |
+void expectAssetDoesNotComplete(String name) { |
+ var id = new AssetId.parse(name); |
+ |
+ schedule(() { |
+ return _futureShouldNotCompleteUntil( |
+ _barback.getAssetById(id), |
+ pumpEventQueue(), |
+ "asset $id"); |
+ }, "asset $id should not complete"); |
+} |
+ |
+/// Returns a matcher for an [AggregateException] containing errors that match |
+/// [matchers]. |
+Matcher isAggregateException(Iterable<Matcher> errors) { |
+ // Match the aggregate error itself. |
+ var matchers = [ |
+ new isInstanceOf<AggregateException>(), |
+ transform((error) => error.errors, hasLength(errors.length), |
+ 'errors.length == ${errors.length}') |
+ ]; |
+ |
+ // Make sure its contained errors match the matchers. |
+ for (var error in errors) { |
+ matchers.add(transform((error) => error.errors, contains(error), |
+ error.toString())); |
+ } |
+ |
+ return allOf(matchers); |
+} |
+ |
+/// Returns a matcher for an [AssetNotFoundException] with the given [id]. |
+Matcher isAssetNotFoundException(String name) { |
+ var id = new AssetId.parse(name); |
+ return allOf( |
+ new isInstanceOf<AssetNotFoundException>(), |
+ predicate((error) => error.id == id, 'id == $name')); |
+} |
+ |
+/// Returns a matcher for an [AssetCollisionException] with the given [id]. |
+Matcher isAssetCollisionException(String name) { |
+ var id = new AssetId.parse(name); |
+ return allOf( |
+ new isInstanceOf<AssetCollisionException>(), |
+ predicate((error) => error.id == id, 'id == $name')); |
+} |
+ |
+/// Returns a matcher for a [MissingInputException] with the given [id]. |
+Matcher isMissingInputException(String name) { |
+ var id = new AssetId.parse(name); |
+ return allOf( |
+ new isInstanceOf<MissingInputException>(), |
+ predicate((error) => error.id == id, 'id == $name')); |
+} |
+ |
+/// Returns a matcher for an [InvalidOutputException] with the given id. |
+Matcher isInvalidOutputException(String name) { |
+ var id = new AssetId.parse(name); |
+ return allOf( |
+ new isInstanceOf<InvalidOutputException>(), |
+ predicate((error) => error.id == id, 'id == $name')); |
+} |
+ |
+/// Returns a matcher for an [AssetLoadException] with the given id and a |
+/// wrapped error that matches [error]. |
+Matcher isAssetLoadException(String name, error) { |
+ var id = new AssetId.parse(name); |
+ return allOf( |
+ new isInstanceOf<AssetLoadException>(), |
+ transform((error) => error.id, equals(id), 'id'), |
+ transform((error) => error.error, wrapMatcher(error), 'error')); |
+} |
+ |
+/// Returns a matcher for a [TransformerException] with a wrapped error that |
+/// matches [error]. |
+Matcher isTransformerException(error) { |
+ return allOf( |
+ new isInstanceOf<TransformerException>(), |
+ transform((error) => error.error, wrapMatcher(error), 'error')); |
+} |
+ |
+/// Returns a matcher for a [MockLoadException] with the given [id]. |
+Matcher isMockLoadException(String name) { |
+ var id = new AssetId.parse(name); |
+ return allOf( |
+ new isInstanceOf<MockLoadException>(), |
+ predicate((error) => error.id == id, 'id == $name')); |
+} |
+ |
+/// Returns a matcher that runs [transformation] on its input, then matches |
+/// the output against [matcher]. |
+/// |
+/// [description] should be a noun phrase that describes the relation of the |
+/// output of [transformation] to its input. |
+Matcher transform(transformation(value), matcher, String description) => |
+ new _TransformMatcher(transformation, wrapMatcher(matcher), description); |
+ |
+class _TransformMatcher extends Matcher { |
+ final Function _transformation; |
+ final Matcher _matcher; |
+ final String _description; |
+ |
+ _TransformMatcher(this._transformation, this._matcher, this._description); |
+ |
+ bool matches(item, Map matchState) => |
+ _matcher.matches(_transformation(item), matchState); |
+ |
+ Description describe(Description description) => |
+ description.add(_description).add(' ').addDescriptionOf(_matcher); |
+} |
+ |
+/// Asserts that [future] shouldn't complete until after [delay] completes. |
+/// |
+/// Once [delay] completes, the output of [future] is ignored, even if it's an |
+/// error. |
+/// |
+/// [description] should describe [future]. |
+Future _futureShouldNotCompleteUntil(Future future, Future delay, |
+ String description) { |
+ var trace = new Trace.current(); |
+ var cancelable = new CancelableFuture(future); |
+ cancelable.then((result) { |
+ currentSchedule.signalError( |
+ new Exception("Expected $description not to complete here, but it " |
+ "completed with result: $result"), |
+ trace); |
+ }).catchError((error) { |
+ currentSchedule.signalError(error); |
+ }); |
+ |
+ return delay.then((_) => cancelable.cancel()); |
+} |
+ |
+/// An [AssetProvider] that provides the given set of assets. |
+class MockProvider implements StaticPackageProvider { |
+ Iterable<String> get packages => |
+ _assets.keys.toSet().difference(staticPackages); |
+ |
+ final Set<String> staticPackages; |
+ |
+ final Map<String, AssetSet> _assets; |
+ |
+ /// The set of assets for which [MockLoadException]s should be emitted if |
+ /// they're loaded. |
+ final _errors = new Set<AssetId>(); |
+ |
+ /// The set of assets for which synchronous [MockLoadException]s should be |
+ /// emitted if they're loaded. |
+ final _syncErrors = new Set<AssetId>(); |
+ |
+ /// The completer that [getAsset()] is waiting on to complete when paused. |
+ /// |
+ /// If `null` it will return the asset immediately. |
+ Completer _pauseCompleter; |
+ |
+ /// Tells the provider to wait during [getAsset] until [complete()] |
+ /// is called. |
+ /// |
+ /// Lets you test the asynchronous behavior of loading. |
+ void _pause() { |
+ _pauseCompleter = new Completer(); |
+ } |
+ |
+ void _resume() { |
+ _pauseCompleter.complete(); |
+ _pauseCompleter = null; |
+ } |
+ |
+ MockProvider(assets, {Iterable<String> staticPackages, |
+ Iterable<String> additionalPackages}) |
+ : staticPackages = staticPackages == null ? new Set() : |
+ staticPackages.toSet(), |
+ _assets = _normalizeAssets(assets, additionalPackages); |
+ |
+ static Map<String, AssetSet> _normalizeAssets(assets, |
+ Iterable<String> additionalPackages) { |
+ var assetList; |
+ if (assets is Map) { |
+ assetList = assets.keys.map((asset) { |
+ var id = new AssetId.parse(asset); |
+ return new _MockAsset(id, assets[asset]); |
+ }); |
+ } else if (assets is Iterable) { |
+ assetList = assets.map((asset) { |
+ var id = new AssetId.parse(asset); |
+ var contents = pathos.basenameWithoutExtension(id.path); |
+ return new _MockAsset(id, contents); |
+ }); |
+ } |
+ |
+ var assetMap = mapMapValues(groupBy(assetList, (asset) => asset.id.package), |
+ (package, assets) => new AssetSet.from(assets)); |
+ |
+ // Make sure that packages that have transformers but no assets are |
+ // considered by MockProvider to exist. |
+ if (additionalPackages != null) { |
+ for (var package in additionalPackages) { |
+ assetMap.putIfAbsent(package, () => new AssetSet()); |
+ } |
+ } |
+ |
+ // If there are no assets or transformers, add a dummy package. This better |
+ // simulates the real world, where there'll always be at least the |
+ // entrypoint package. |
+ return assetMap.isEmpty ? {"app": new AssetSet()} : assetMap; |
+ } |
+ |
+ void _modifyAsset(String name, String contents) { |
+ var id = new AssetId.parse(name); |
+ _errors.remove(id); |
+ _syncErrors.remove(id); |
+ (_assets[id.package][id] as _MockAsset).contents = contents; |
+ } |
+ |
+ void _setAssetError(String name, bool async) { |
+ (async ? _errors : _syncErrors).add(new AssetId.parse(name)); |
+ } |
+ |
+ Stream<AssetId> getAllAssetIds(String package) => |
+ new Stream.fromIterable(_assets[package].map((asset) => asset.id)); |
+ |
+ Future<Asset> getAsset(AssetId id) { |
+ // Eagerly load the asset so we can test an asset's value changing between |
+ // when a load starts and when it finishes. |
+ var assets = _assets[id.package]; |
+ var asset; |
+ if (assets != null) asset = assets[id]; |
+ |
+ if (_syncErrors.contains(id)) throw new MockLoadException(id); |
+ var hasError = _errors.contains(id); |
+ |
+ var future; |
+ if (_pauseCompleter != null) { |
+ future = _pauseCompleter.future; |
+ } else { |
+ future = new Future.value(); |
+ } |
+ |
+ return future.then((_) { |
+ if (hasError) throw new MockLoadException(id); |
+ if (asset == null) throw new AssetNotFoundException(id); |
+ return asset; |
+ }); |
+ } |
+} |
+ |
+/// Error thrown for assets with [setAssetError] set. |
+class MockLoadException implements Exception { |
+ final AssetId id; |
+ |
+ MockLoadException(this.id); |
+ |
+ String toString() => "Error loading $id."; |
+} |
+ |
+/// An implementation of [Asset] that never hits the file system. |
+class _MockAsset implements Asset { |
+ final AssetId id; |
+ String contents; |
+ |
+ _MockAsset(this.id, this.contents); |
+ |
+ Future<String> readAsString({Encoding encoding}) => |
+ new Future.value(contents); |
+ |
+ Stream<List<int>> read() => throw new UnimplementedError(); |
+ |
+ String toString() => "MockAsset $id $contents"; |
+} |