Index: pkg/barback/test/utils.dart |
diff --git a/pkg/barback/test/utils.dart b/pkg/barback/test/utils.dart |
index fcda96266b3e226e1f6c97f9b8d42f64fb9f8072..43d3777fbb494963dbe06c30e2c7c56b8336d98a 100644 |
--- a/pkg/barback/test/utils.dart |
+++ b/pkg/barback/test/utils.dart |
@@ -10,6 +10,8 @@ import 'dart:io'; |
import 'package:barback/barback.dart'; |
import 'package:barback/src/asset_cascade.dart'; |
+import 'package:barback/src/asset_set.dart'; |
+import 'package:barback/src/cancelable_future.dart'; |
import 'package:barback/src/package_graph.dart'; |
import 'package:barback/src/utils.dart'; |
import 'package:path/path.dart' as pathos; |
@@ -17,6 +19,14 @@ import 'package:scheduled_test/scheduled_test.dart'; |
import 'package:stack_trace/stack_trace.dart'; |
import 'package:unittest/compact_vm_config.dart'; |
+export 'transformer/bad.dart'; |
+export 'transformer/check_content.dart'; |
+export 'transformer/create_asset.dart'; |
+export 'transformer/many_to_one.dart'; |
+export 'transformer/mock.dart'; |
+export 'transformer/one_to_many.dart'; |
+export 'transformer/rewrite.dart'; |
+ |
var _configured = false; |
MockProvider _provider; |
@@ -96,6 +106,15 @@ void modifyAsset(String name, String contents) { |
}, "modify asset $name"); |
} |
+/// Schedules an error when loading the asset identified by [name]. |
Bob Nystrom
2013/07/30 22:25:02
"error" -> "error to be generated".
nweiz
2013/07/30 22:46:21
Done.
|
+/// |
+/// Does not update the asset in the graph. |
+void setAssetError(String name) { |
+ schedule(() { |
+ _provider._setAssetError(name); |
+ }, "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 |
@@ -117,31 +136,16 @@ void resumeProvider() { |
/// [buildShouldFail], so those can be used to validate build results before and |
/// after this. |
void buildShouldNotBeDone() { |
- var resultAllowed = false; |
- var trace = new Trace.current(); |
- _graph.results.elementAt(_nextBuildResult).then((result) { |
- if (resultAllowed) return; |
- |
- currentSchedule.signalError( |
- new Exception("Expected build not to terminate " |
- "here, but it terminated with result: $result"), trace); |
- }).catchError((error) { |
- if (resultAllowed) return; |
- currentSchedule.signalError(error); |
- }); |
- |
- schedule(() { |
- // Pump the event queue in case the build completes out-of-band after we get |
- // here. If it does, we want to signal an error. |
- return pumpEventQueue().then((_) { |
- resultAllowed = true; |
- }); |
- }, "ensuring build doesn't terminate"); |
+ _futureShouldNotCompleteUntil( |
+ _graph.results.elementAt(_nextBuildResult), |
+ schedule(() => pumpEventQueue(), "build should not terminate"), |
+ "build"); |
} |
/// Expects that the next [BuildResult] is a build success. |
void buildShouldSucceed() { |
expect(_getNextBuildResult().then((result) { |
+ result.errors.forEach(currentSchedule.signalError); |
expect(result.succeeded, isTrue); |
}), completes); |
} |
@@ -190,9 +194,8 @@ void expectAsset(String name, [String contents]) { |
schedule(() { |
return _graph.getAssetById(id).then((asset) { |
// TODO(rnystrom): Make an actual Matcher class for this. |
- expect(asset, new isInstanceOf<MockAsset>()); |
expect(asset.id, equals(id)); |
- expect(asset.contents, equals(contents)); |
+ expect(asset.readAsString(), completion(equals(contents))); |
}); |
}, "get asset $name"); |
} |
@@ -213,6 +216,19 @@ void expectNoAsset(String name) { |
}, "get asset $name"); |
} |
+/// 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( |
+ _graph.getAssetById(id), |
+ pumpEventQueue(), |
+ "asset $id"); |
+ }, "asset $id should not complete"); |
+} |
+ |
/// Returns a matcher for an [AssetNotFoundException] with the given [id]. |
Matcher isAssetNotFoundException(String name) { |
var id = new AssetId.parse(name); |
@@ -247,12 +263,44 @@ Matcher isInvalidOutputException(String package, String name) { |
predicate((error) => error.id == id, 'id is $name')); |
} |
+/// 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 is $name')); |
Bob Nystrom
2013/07/30 22:25:02
"is" -> "=="
nweiz
2013/07/30 22:46:21
Done.
|
+} |
+ |
+/// 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 PackageProvider { |
Iterable<String> get packages => _packages.keys; |
Map<String, _MockPackage> _packages; |
+ final _errors = new Set<AssetId>(); |
Bob Nystrom
2013/07/30 22:25:02
Document.
nweiz
2013/07/30 22:46:21
Done.
|
+ |
/// The completer that [getAsset()] is waiting on to complete when paused. |
/// |
/// If `null` it will return the asset immediately. |
@@ -277,13 +325,13 @@ class MockProvider implements PackageProvider { |
if (assets is Map) { |
assetList = assets.keys.map((asset) { |
var id = new AssetId.parse(asset); |
- return new MockAsset(id, assets[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); |
+ return new _MockAsset(id, contents); |
}); |
} |
@@ -291,21 +339,26 @@ class MockProvider implements PackageProvider { |
(package, assets) { |
var packageTransformers = transformers[package]; |
if (packageTransformers == null) packageTransformers = []; |
- return new _MockPackage(assets, packageTransformers.toList()); |
+ return new _MockPackage( |
+ new AssetSet.from(assets), packageTransformers.toList()); |
}); |
// 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. |
- if (_packages.isEmpty) _packages = {"app": new _MockPackage([], [])}; |
+ if (_packages.isEmpty) { |
+ _packages = {"app": new _MockPackage(new AssetSet(), [])}; |
+ } |
} |
void _modifyAsset(String name, String contents) { |
var id = new AssetId.parse(name); |
- var asset = _packages[id.package].assets.firstWhere((a) => a.id == id); |
- asset.contents = contents; |
+ _errors.remove(id); |
+ _packages[id.package].assets[id].contents = contents; |
} |
+ void _setAssetError(String name) => _errors.add(new AssetId.parse(name)); |
Bob Nystrom
2013/07/30 22:25:02
Document.
nweiz
2013/07/30 22:46:21
Like [_modifyAsset], I didn't document this becaus
|
+ |
List<AssetId> listAssets(String package, {String within}) { |
if (within != null) { |
throw new UnimplementedError("Doesn't handle 'within' yet."); |
@@ -323,6 +376,14 @@ class MockProvider implements PackageProvider { |
} |
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 package = _packages[id.package]; |
+ var asset; |
+ if (package != null) asset = package.assets[id]; |
+ |
+ var error = _errors.contains(id); |
Bob Nystrom
2013/07/30 22:25:02
"hasError"
nweiz
2013/07/30 22:46:21
Done.
|
+ |
var future; |
if (_pauseCompleter != null) { |
future = _pauseCompleter.future; |
@@ -331,225 +392,38 @@ class MockProvider implements PackageProvider { |
} |
return future.then((_) { |
- var package = _packages[id.package]; |
- if (package == null) throw new AssetNotFoundException(id); |
- |
- return package.assets.firstWhere((asset) => asset.id == id, |
- orElse: () => throw new AssetNotFoundException(id)); |
+ if (error) 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."; |
+} |
+ |
/// Used by [MockProvider] to keep track of which assets and transformers exist |
/// for each package. |
class _MockPackage { |
- final List<MockAsset> assets; |
+ final AssetSet assets; |
final List<List<Transformer>> transformers; |
_MockPackage(this.assets, Iterable<Iterable<Transformer>> transformers) |
: transformers = transformers.map((phase) => phase.toList()).toList(); |
} |
-/// A [Transformer] that takes assets ending with one extension and generates |
-/// assets with a given extension. |
-/// |
-/// Appends the output extension to the contents of the input file. |
-class RewriteTransformer extends Transformer { |
- final String from; |
- final String to; |
- |
- /// The number of times the transformer has been applied. |
- int numRuns = 0; |
- |
- /// The number of currently running transforms. |
- int _runningTransforms = 0; |
- |
- /// The completer that the transform is waiting on to complete. |
- /// |
- /// If `null` the transform will complete immediately. |
- Completer _wait; |
- |
- /// A future that completes when the first apply of this transformer begins. |
- Future get started => _started.future; |
- final _started = new Completer(); |
- |
- /// Creates a transformer that rewrites assets whose extension is [from] to |
- /// one whose extension is [to]. |
- /// |
- /// [to] may be a space-separated list in which case multiple outputs will be |
- /// created for each input. |
- RewriteTransformer(this.from, this.to); |
- |
- /// `true` if any transforms are currently running. |
- bool get isRunning => _runningTransforms > 0; |
- |
- /// Tells the transform to wait during its transformation until [complete()] |
- /// is called. |
- /// |
- /// Lets you test the asynchronous behavior of transformers. |
- void wait() { |
- _wait = new Completer(); |
- } |
- |
- void complete() { |
- _wait.complete(); |
- _wait = null; |
- } |
- |
- Future<bool> isPrimary(Asset asset) { |
- return new Future.value(asset.id.extension == ".$from"); |
- } |
- |
- Future apply(Transform transform) { |
- numRuns++; |
- if (!_started.isCompleted) _started.complete(); |
- _runningTransforms++; |
- return transform.primaryInput.then((input) { |
- return Future.wait(to.split(" ").map((extension) { |
- var id = transform.primaryId.changeExtension(".$extension"); |
- return input.readAsString().then((content) { |
- transform.addOutput(new MockAsset(id, "$content.$extension")); |
- }); |
- })).then((_) { |
- if (_wait != null) return _wait.future; |
- }); |
- }).whenComplete(() { |
- _runningTransforms--; |
- }); |
- } |
- |
- String toString() => "$from->$to"; |
-} |
- |
-/// A [Transformer] that takes an input asset that contains a comma-separated |
-/// list of paths and outputs a file for each path. |
-class OneToManyTransformer extends Transformer { |
- final String extension; |
- |
- /// The number of times the transformer has been applied. |
- int numRuns = 0; |
- |
- /// Creates a transformer that consumes assets with [extension]. |
- /// |
- /// That file contains a comma-separated list of paths and it will output |
- /// files at each of those paths. |
- OneToManyTransformer(this.extension); |
- |
- Future<bool> isPrimary(Asset asset) { |
- return new Future.value(asset.id.extension == ".$extension"); |
- } |
- |
- Future apply(Transform transform) { |
- numRuns++; |
- return transform.primaryInput.then((input) { |
- return input.readAsString().then((lines) { |
- for (var line in lines.split(",")) { |
- var id = new AssetId(transform.primaryId.package, line); |
- transform.addOutput(new MockAsset(id, "spread $extension")); |
- } |
- }); |
- }); |
- } |
- |
- String toString() => "1->many $extension"; |
-} |
- |
-/// A transformer that uses the contents of a file to define the other inputs. |
-/// |
-/// Outputs a file with the same name as the primary but with an "out" |
-/// extension containing the concatenated contents of all non-primary inputs. |
-class ManyToOneTransformer extends Transformer { |
- final String extension; |
- |
- /// The number of times the transformer has been applied. |
- int numRuns = 0; |
- |
- /// Creates a transformer that consumes assets with [extension]. |
- /// |
- /// That file contains a comma-separated list of paths and it will input |
- /// files at each of those paths. |
- ManyToOneTransformer(this.extension); |
- |
- Future<bool> isPrimary(Asset asset) { |
- return new Future.value(asset.id.extension == ".$extension"); |
- } |
- |
- Future apply(Transform transform) { |
- numRuns++; |
- return transform.primaryInput.then((primary) { |
- return primary.readAsString().then((contents) { |
- // Get all of the included inputs. |
- var inputs = contents.split(",").map((path) { |
- var id = new AssetId(transform.primaryId.package, path); |
- return transform.getInput(id); |
- }); |
- |
- return Future.wait(inputs); |
- }).then((inputs) { |
- // Concatenate them to one output. |
- var output = ""; |
- return Future.forEach(inputs, (input) { |
- return input.readAsString().then((contents) { |
- output += contents; |
- }); |
- }).then((_) { |
- var id = transform.primaryId.changeExtension(".out"); |
- transform.addOutput(new MockAsset(id, output)); |
- }); |
- }); |
- }); |
- } |
- |
- String toString() => "many->1 $extension"; |
-} |
- |
-/// A transformer that throws an exception when run, after generating the |
-/// given outputs. |
-class BadTransformer extends Transformer { |
- /// The error it throws. |
- static const ERROR = "I am a bad transformer!"; |
- |
- /// The list of asset names that it should output. |
- final List<String> outputs; |
- |
- BadTransformer(this.outputs); |
- |
- Future<bool> isPrimary(Asset asset) => new Future.value(true); |
- Future apply(Transform transform) { |
- return newFuture(() { |
- // Create the outputs first. |
- for (var output in outputs) { |
- var id = new AssetId.parse(output); |
- transform.addOutput(new MockAsset(id, output)); |
- } |
- |
- // Then fail. |
- throw ERROR; |
- }); |
- } |
-} |
- |
-/// A transformer that outputs an asset with the given id. |
-class CreateAssetTransformer extends Transformer { |
- final String output; |
- |
- CreateAssetTransformer(this.output); |
- |
- Future<bool> isPrimary(Asset asset) => new Future.value(true); |
- |
- Future apply(Transform transform) { |
- return newFuture(() { |
- transform.addOutput(new MockAsset(new AssetId.parse(output), output)); |
- }); |
- } |
-} |
- |
/// An implementation of [Asset] that never hits the file system. |
-class MockAsset implements Asset { |
+class _MockAsset implements Asset { |
Bob Nystrom
2013/07/30 22:25:02
Can you get rid of this entirely?
nweiz
2013/07/30 22:46:21
I considered that, but the fact that MockProvider
Bob Nystrom
2013/07/30 23:14:08
SGTM.
|
final AssetId id; |
String contents; |
- MockAsset(this.id, this.contents); |
+ _MockAsset(this.id, this.contents); |
Future<String> readAsString({Encoding encoding}) => |
new Future.value(contents); |
@@ -557,4 +431,4 @@ class MockAsset implements Asset { |
Stream<List<int>> read() => throw new UnimplementedError(); |
String toString() => "MockAsset $id $contents"; |
-} |
+} |