| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library barback.test.utils; | 5 library barback.test.utils; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:collection'; | 8 import 'dart:collection'; |
| 9 import 'dart:io'; | 9 import 'dart:io'; |
| 10 | 10 |
| 11 import 'package:barback/barback.dart'; | 11 import 'package:barback/barback.dart'; |
| 12 import 'package:barback/src/asset_cascade.dart'; | 12 import 'package:barback/src/asset_cascade.dart'; |
| 13 import 'package:barback/src/asset_set.dart'; |
| 14 import 'package:barback/src/cancelable_future.dart'; |
| 13 import 'package:barback/src/package_graph.dart'; | 15 import 'package:barback/src/package_graph.dart'; |
| 14 import 'package:barback/src/utils.dart'; | 16 import 'package:barback/src/utils.dart'; |
| 15 import 'package:path/path.dart' as pathos; | 17 import 'package:path/path.dart' as pathos; |
| 16 import 'package:scheduled_test/scheduled_test.dart'; | 18 import 'package:scheduled_test/scheduled_test.dart'; |
| 17 import 'package:stack_trace/stack_trace.dart'; | 19 import 'package:stack_trace/stack_trace.dart'; |
| 18 import 'package:unittest/compact_vm_config.dart'; | 20 import 'package:unittest/compact_vm_config.dart'; |
| 19 | 21 |
| 22 export 'transformer/bad.dart'; |
| 23 export 'transformer/check_content.dart'; |
| 24 export 'transformer/create_asset.dart'; |
| 25 export 'transformer/many_to_one.dart'; |
| 26 export 'transformer/mock.dart'; |
| 27 export 'transformer/one_to_many.dart'; |
| 28 export 'transformer/rewrite.dart'; |
| 29 |
| 20 var _configured = false; | 30 var _configured = false; |
| 21 | 31 |
| 22 MockProvider _provider; | 32 MockProvider _provider; |
| 23 PackageGraph _graph; | 33 PackageGraph _graph; |
| 24 | 34 |
| 25 /// Calls to [buildShouldSucceed] and [buildShouldFail] set expectations on | 35 /// Calls to [buildShouldSucceed] and [buildShouldFail] set expectations on |
| 26 /// successive [BuildResult]s from [_graph]. This keeps track of how many calls | 36 /// successive [BuildResult]s from [_graph]. This keeps track of how many calls |
| 27 /// have already been made so later calls know which result to look for. | 37 /// have already been made so later calls know which result to look for. |
| 28 int _nextBuildResult; | 38 int _nextBuildResult; |
| 29 | 39 |
| (...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 89 /// Schedules a change to the contents of an asset identified by [name] to | 99 /// Schedules a change to the contents of an asset identified by [name] to |
| 90 /// [contents]. | 100 /// [contents]. |
| 91 /// | 101 /// |
| 92 /// Does not update it in the graph. | 102 /// Does not update it in the graph. |
| 93 void modifyAsset(String name, String contents) { | 103 void modifyAsset(String name, String contents) { |
| 94 schedule(() { | 104 schedule(() { |
| 95 _provider._modifyAsset(name, contents); | 105 _provider._modifyAsset(name, contents); |
| 96 }, "modify asset $name"); | 106 }, "modify asset $name"); |
| 97 } | 107 } |
| 98 | 108 |
| 109 /// Schedules an error to be generated when loading the asset identified by |
| 110 /// [name]. |
| 111 /// |
| 112 /// Does not update the asset in the graph. |
| 113 void setAssetError(String name) { |
| 114 schedule(() { |
| 115 _provider._setAssetError(name); |
| 116 }, "set error for asset $name"); |
| 117 } |
| 118 |
| 99 /// Schedules a pause of the internally created [PackageProvider]. | 119 /// Schedules a pause of the internally created [PackageProvider]. |
| 100 /// | 120 /// |
| 101 /// All asset requests that the [PackageGraph] makes to the provider after this | 121 /// All asset requests that the [PackageGraph] makes to the provider after this |
| 102 /// will not complete until [resumeProvider] is called. | 122 /// will not complete until [resumeProvider] is called. |
| 103 void pauseProvider() { | 123 void pauseProvider() { |
| 104 schedule(() => _provider._pause(), "pause provider"); | 124 schedule(() => _provider._pause(), "pause provider"); |
| 105 } | 125 } |
| 106 | 126 |
| 107 /// Schedules an unpause of the provider after a call to [pauseProvider] and | 127 /// Schedules an unpause of the provider after a call to [pauseProvider] and |
| 108 /// allows all pending asset loads to finish. | 128 /// allows all pending asset loads to finish. |
| 109 void resumeProvider() { | 129 void resumeProvider() { |
| 110 schedule(() => _provider._resume(), "resume provider"); | 130 schedule(() => _provider._resume(), "resume provider"); |
| 111 } | 131 } |
| 112 | 132 |
| 113 /// Asserts that the current build step shouldn't have finished by this point in | 133 /// Asserts that the current build step shouldn't have finished by this point in |
| 114 /// the schedule. | 134 /// the schedule. |
| 115 /// | 135 /// |
| 116 /// This uses the same build counter as [buildShouldSucceed] and | 136 /// This uses the same build counter as [buildShouldSucceed] and |
| 117 /// [buildShouldFail], so those can be used to validate build results before and | 137 /// [buildShouldFail], so those can be used to validate build results before and |
| 118 /// after this. | 138 /// after this. |
| 119 void buildShouldNotBeDone() { | 139 void buildShouldNotBeDone() { |
| 120 var resultAllowed = false; | 140 _futureShouldNotCompleteUntil( |
| 121 var trace = new Trace.current(); | 141 _graph.results.elementAt(_nextBuildResult), |
| 122 _graph.results.elementAt(_nextBuildResult).then((result) { | 142 schedule(() => pumpEventQueue(), "build should not terminate"), |
| 123 if (resultAllowed) return; | 143 "build"); |
| 124 | |
| 125 currentSchedule.signalError( | |
| 126 new Exception("Expected build not to terminate " | |
| 127 "here, but it terminated with result: $result"), trace); | |
| 128 }).catchError((error) { | |
| 129 if (resultAllowed) return; | |
| 130 currentSchedule.signalError(error); | |
| 131 }); | |
| 132 | |
| 133 schedule(() { | |
| 134 // Pump the event queue in case the build completes out-of-band after we get | |
| 135 // here. If it does, we want to signal an error. | |
| 136 return pumpEventQueue().then((_) { | |
| 137 resultAllowed = true; | |
| 138 }); | |
| 139 }, "ensuring build doesn't terminate"); | |
| 140 } | 144 } |
| 141 | 145 |
| 142 /// Expects that the next [BuildResult] is a build success. | 146 /// Expects that the next [BuildResult] is a build success. |
| 143 void buildShouldSucceed() { | 147 void buildShouldSucceed() { |
| 144 expect(_getNextBuildResult().then((result) { | 148 expect(_getNextBuildResult().then((result) { |
| 149 result.errors.forEach(currentSchedule.signalError); |
| 145 expect(result.succeeded, isTrue); | 150 expect(result.succeeded, isTrue); |
| 146 }), completes); | 151 }), completes); |
| 147 } | 152 } |
| 148 | 153 |
| 149 /// Expects that the next [BuildResult] emitted is a failure. | 154 /// Expects that the next [BuildResult] emitted is a failure. |
| 150 /// | 155 /// |
| 151 /// [matchers] is a list of matchers to match against the errors that caused the | 156 /// [matchers] is a list of matchers to match against the errors that caused the |
| 152 /// build to fail. Every matcher is expected to match an error, but the order of | 157 /// build to fail. Every matcher is expected to match an error, but the order of |
| 153 /// matchers is unimportant. | 158 /// matchers is unimportant. |
| 154 void buildShouldFail(List matchers) { | 159 void buildShouldFail(List matchers) { |
| (...skipping 28 matching lines...) Expand all Loading... |
| 183 void expectAsset(String name, [String contents]) { | 188 void expectAsset(String name, [String contents]) { |
| 184 var id = new AssetId.parse(name); | 189 var id = new AssetId.parse(name); |
| 185 | 190 |
| 186 if (contents == null) { | 191 if (contents == null) { |
| 187 contents = pathos.basenameWithoutExtension(id.path); | 192 contents = pathos.basenameWithoutExtension(id.path); |
| 188 } | 193 } |
| 189 | 194 |
| 190 schedule(() { | 195 schedule(() { |
| 191 return _graph.getAssetById(id).then((asset) { | 196 return _graph.getAssetById(id).then((asset) { |
| 192 // TODO(rnystrom): Make an actual Matcher class for this. | 197 // TODO(rnystrom): Make an actual Matcher class for this. |
| 193 expect(asset, new isInstanceOf<MockAsset>()); | |
| 194 expect(asset.id, equals(id)); | 198 expect(asset.id, equals(id)); |
| 195 expect(asset.contents, equals(contents)); | 199 expect(asset.readAsString(), completion(equals(contents))); |
| 196 }); | 200 }); |
| 197 }, "get asset $name"); | 201 }, "get asset $name"); |
| 198 } | 202 } |
| 199 | 203 |
| 200 /// Schedules an expectation that the graph will not find an asset matching | 204 /// Schedules an expectation that the graph will not find an asset matching |
| 201 /// [name]. | 205 /// [name]. |
| 202 void expectNoAsset(String name) { | 206 void expectNoAsset(String name) { |
| 203 var id = new AssetId.parse(name); | 207 var id = new AssetId.parse(name); |
| 204 | 208 |
| 205 // Make sure the future gets the error. | 209 // Make sure the future gets the error. |
| 206 schedule(() { | 210 schedule(() { |
| 207 return _graph.getAssetById(id).then((asset) { | 211 return _graph.getAssetById(id).then((asset) { |
| 208 fail("Should have thrown error but got $asset."); | 212 fail("Should have thrown error but got $asset."); |
| 209 }).catchError((error) { | 213 }).catchError((error) { |
| 210 expect(error, new isInstanceOf<AssetNotFoundException>()); | 214 expect(error, new isInstanceOf<AssetNotFoundException>()); |
| 211 expect(error.id, equals(id)); | 215 expect(error.id, equals(id)); |
| 212 }); | 216 }); |
| 213 }, "get asset $name"); | 217 }, "get asset $name"); |
| 214 } | 218 } |
| 215 | 219 |
| 220 /// Schedules an expectation that a [getAssetById] call for the given asset |
| 221 /// won't terminate at this point in the schedule. |
| 222 void expectAssetDoesNotComplete(String name) { |
| 223 var id = new AssetId.parse(name); |
| 224 |
| 225 schedule(() { |
| 226 return _futureShouldNotCompleteUntil( |
| 227 _graph.getAssetById(id), |
| 228 pumpEventQueue(), |
| 229 "asset $id"); |
| 230 }, "asset $id should not complete"); |
| 231 } |
| 232 |
| 216 /// Returns a matcher for an [AssetNotFoundException] with the given [id]. | 233 /// Returns a matcher for an [AssetNotFoundException] with the given [id]. |
| 217 Matcher isAssetNotFoundException(String name) { | 234 Matcher isAssetNotFoundException(String name) { |
| 218 var id = new AssetId.parse(name); | 235 var id = new AssetId.parse(name); |
| 219 return allOf( | 236 return allOf( |
| 220 new isInstanceOf<AssetNotFoundException>(), | 237 new isInstanceOf<AssetNotFoundException>(), |
| 221 predicate((error) => error.id == id, 'id is $name')); | 238 predicate((error) => error.id == id, 'id == $name')); |
| 222 } | 239 } |
| 223 | 240 |
| 224 /// Returns a matcher for an [AssetCollisionException] with the given [id]. | 241 /// Returns a matcher for an [AssetCollisionException] with the given [id]. |
| 225 Matcher isAssetCollisionException(String name) { | 242 Matcher isAssetCollisionException(String name) { |
| 226 var id = new AssetId.parse(name); | 243 var id = new AssetId.parse(name); |
| 227 return allOf( | 244 return allOf( |
| 228 new isInstanceOf<AssetCollisionException>(), | 245 new isInstanceOf<AssetCollisionException>(), |
| 229 predicate((error) => error.id == id, 'id is $name')); | 246 predicate((error) => error.id == id, 'id == $name')); |
| 230 } | 247 } |
| 231 | 248 |
| 232 /// Returns a matcher for a [MissingInputException] with the given [id]. | 249 /// Returns a matcher for a [MissingInputException] with the given [id]. |
| 233 Matcher isMissingInputException(String name) { | 250 Matcher isMissingInputException(String name) { |
| 234 var id = new AssetId.parse(name); | 251 var id = new AssetId.parse(name); |
| 235 return allOf( | 252 return allOf( |
| 236 new isInstanceOf<MissingInputException>(), | 253 new isInstanceOf<MissingInputException>(), |
| 237 predicate((error) => error.id == id, 'id is $name')); | 254 predicate((error) => error.id == id, 'id == $name')); |
| 238 } | 255 } |
| 239 | 256 |
| 240 /// Returns a matcher for an [InvalidOutputException] with the given id and | 257 /// Returns a matcher for an [InvalidOutputException] with the given id and |
| 241 /// package name. | 258 /// package name. |
| 242 Matcher isInvalidOutputException(String package, String name) { | 259 Matcher isInvalidOutputException(String package, String name) { |
| 243 var id = new AssetId.parse(name); | 260 var id = new AssetId.parse(name); |
| 244 return allOf( | 261 return allOf( |
| 245 new isInstanceOf<InvalidOutputException>(), | 262 new isInstanceOf<InvalidOutputException>(), |
| 246 predicate((error) => error.package == package, 'package is $package'), | 263 predicate((error) => error.package == package, 'package is $package'), |
| 247 predicate((error) => error.id == id, 'id is $name')); | 264 predicate((error) => error.id == id, 'id == $name')); |
| 265 } |
| 266 |
| 267 /// Returns a matcher for a [MockLoadException] with the given [id]. |
| 268 Matcher isMockLoadException(String name) { |
| 269 var id = new AssetId.parse(name); |
| 270 return allOf( |
| 271 new isInstanceOf<MockLoadException>(), |
| 272 predicate((error) => error.id == id, 'id == $name')); |
| 273 } |
| 274 |
| 275 /// Asserts that [future] shouldn't complete until after [delay] completes. |
| 276 /// |
| 277 /// Once [delay] completes, the output of [future] is ignored, even if it's an |
| 278 /// error. |
| 279 /// |
| 280 /// [description] should describe [future]. |
| 281 Future _futureShouldNotCompleteUntil(Future future, Future delay, |
| 282 String description) { |
| 283 var trace = new Trace.current(); |
| 284 var cancelable = new CancelableFuture(future); |
| 285 cancelable.then((result) { |
| 286 currentSchedule.signalError( |
| 287 new Exception("Expected $description not to complete here, but it " |
| 288 "completed with result: $result"), |
| 289 trace); |
| 290 }).catchError((error) { |
| 291 currentSchedule.signalError(error); |
| 292 }); |
| 293 |
| 294 return delay.then((_) => cancelable.cancel()); |
| 248 } | 295 } |
| 249 | 296 |
| 250 /// An [AssetProvider] that provides the given set of assets. | 297 /// An [AssetProvider] that provides the given set of assets. |
| 251 class MockProvider implements PackageProvider { | 298 class MockProvider implements PackageProvider { |
| 252 Iterable<String> get packages => _packages.keys; | 299 Iterable<String> get packages => _packages.keys; |
| 253 | 300 |
| 254 Map<String, _MockPackage> _packages; | 301 Map<String, _MockPackage> _packages; |
| 255 | 302 |
| 303 /// The set of assets for which [MockLoadException]s should be emitted if |
| 304 /// they're loaded. |
| 305 final _errors = new Set<AssetId>(); |
| 306 |
| 256 /// The completer that [getAsset()] is waiting on to complete when paused. | 307 /// The completer that [getAsset()] is waiting on to complete when paused. |
| 257 /// | 308 /// |
| 258 /// If `null` it will return the asset immediately. | 309 /// If `null` it will return the asset immediately. |
| 259 Completer _pauseCompleter; | 310 Completer _pauseCompleter; |
| 260 | 311 |
| 261 /// Tells the provider to wait during [getAsset] until [complete()] | 312 /// Tells the provider to wait during [getAsset] until [complete()] |
| 262 /// is called. | 313 /// is called. |
| 263 /// | 314 /// |
| 264 /// Lets you test the asynchronous behavior of loading. | 315 /// Lets you test the asynchronous behavior of loading. |
| 265 void _pause() { | 316 void _pause() { |
| 266 _pauseCompleter = new Completer(); | 317 _pauseCompleter = new Completer(); |
| 267 } | 318 } |
| 268 | 319 |
| 269 void _resume() { | 320 void _resume() { |
| 270 _pauseCompleter.complete(); | 321 _pauseCompleter.complete(); |
| 271 _pauseCompleter = null; | 322 _pauseCompleter = null; |
| 272 } | 323 } |
| 273 | 324 |
| 274 MockProvider(assets, | 325 MockProvider(assets, |
| 275 Map<String, Iterable<Iterable<Transformer>>> transformers) { | 326 Map<String, Iterable<Iterable<Transformer>>> transformers) { |
| 276 var assetList; | 327 var assetList; |
| 277 if (assets is Map) { | 328 if (assets is Map) { |
| 278 assetList = assets.keys.map((asset) { | 329 assetList = assets.keys.map((asset) { |
| 279 var id = new AssetId.parse(asset); | 330 var id = new AssetId.parse(asset); |
| 280 return new MockAsset(id, assets[asset]); | 331 return new _MockAsset(id, assets[asset]); |
| 281 }); | 332 }); |
| 282 } else if (assets is Iterable) { | 333 } else if (assets is Iterable) { |
| 283 assetList = assets.map((asset) { | 334 assetList = assets.map((asset) { |
| 284 var id = new AssetId.parse(asset); | 335 var id = new AssetId.parse(asset); |
| 285 var contents = pathos.basenameWithoutExtension(id.path); | 336 var contents = pathos.basenameWithoutExtension(id.path); |
| 286 return new MockAsset(id, contents); | 337 return new _MockAsset(id, contents); |
| 287 }); | 338 }); |
| 288 } | 339 } |
| 289 | 340 |
| 290 _packages = mapMapValues(groupBy(assetList, (asset) => asset.id.package), | 341 _packages = mapMapValues(groupBy(assetList, (asset) => asset.id.package), |
| 291 (package, assets) { | 342 (package, assets) { |
| 292 var packageTransformers = transformers[package]; | 343 var packageTransformers = transformers[package]; |
| 293 if (packageTransformers == null) packageTransformers = []; | 344 if (packageTransformers == null) packageTransformers = []; |
| 294 return new _MockPackage(assets, packageTransformers.toList()); | 345 return new _MockPackage( |
| 346 new AssetSet.from(assets), packageTransformers.toList()); |
| 295 }); | 347 }); |
| 296 | 348 |
| 297 // If there are no assets or transformers, add a dummy package. This better | 349 // If there are no assets or transformers, add a dummy package. This better |
| 298 // simulates the real world, where there'll always be at least the | 350 // simulates the real world, where there'll always be at least the |
| 299 // entrypoint package. | 351 // entrypoint package. |
| 300 if (_packages.isEmpty) _packages = {"app": new _MockPackage([], [])}; | 352 if (_packages.isEmpty) { |
| 353 _packages = {"app": new _MockPackage(new AssetSet(), [])}; |
| 354 } |
| 301 } | 355 } |
| 302 | 356 |
| 303 void _modifyAsset(String name, String contents) { | 357 void _modifyAsset(String name, String contents) { |
| 304 var id = new AssetId.parse(name); | 358 var id = new AssetId.parse(name); |
| 305 var asset = _packages[id.package].assets.firstWhere((a) => a.id == id); | 359 _errors.remove(id); |
| 306 asset.contents = contents; | 360 _packages[id.package].assets[id].contents = contents; |
| 307 } | 361 } |
| 308 | 362 |
| 363 void _setAssetError(String name) => _errors.add(new AssetId.parse(name)); |
| 364 |
| 309 List<AssetId> listAssets(String package, {String within}) { | 365 List<AssetId> listAssets(String package, {String within}) { |
| 310 if (within != null) { | 366 if (within != null) { |
| 311 throw new UnimplementedError("Doesn't handle 'within' yet."); | 367 throw new UnimplementedError("Doesn't handle 'within' yet."); |
| 312 } | 368 } |
| 313 | 369 |
| 314 return _packages[package].assets.map((asset) => asset.id); | 370 return _packages[package].assets.map((asset) => asset.id); |
| 315 } | 371 } |
| 316 | 372 |
| 317 Iterable<Iterable<Transformer>> getTransformers(String package) { | 373 Iterable<Iterable<Transformer>> getTransformers(String package) { |
| 318 var mockPackage = _packages[package]; | 374 var mockPackage = _packages[package]; |
| 319 if (mockPackage == null) { | 375 if (mockPackage == null) { |
| 320 throw new ArgumentError("No package named $package."); | 376 throw new ArgumentError("No package named $package."); |
| 321 } | 377 } |
| 322 return mockPackage.transformers; | 378 return mockPackage.transformers; |
| 323 } | 379 } |
| 324 | 380 |
| 325 Future<Asset> getAsset(AssetId id) { | 381 Future<Asset> getAsset(AssetId id) { |
| 382 // Eagerly load the asset so we can test an asset's value changing between |
| 383 // when a load starts and when it finishes. |
| 384 var package = _packages[id.package]; |
| 385 var asset; |
| 386 if (package != null) asset = package.assets[id]; |
| 387 |
| 388 var hasError = _errors.contains(id); |
| 389 |
| 326 var future; | 390 var future; |
| 327 if (_pauseCompleter != null) { | 391 if (_pauseCompleter != null) { |
| 328 future = _pauseCompleter.future; | 392 future = _pauseCompleter.future; |
| 329 } else { | 393 } else { |
| 330 future = new Future.value(); | 394 future = new Future.value(); |
| 331 } | 395 } |
| 332 | 396 |
| 333 return future.then((_) { | 397 return future.then((_) { |
| 334 var package = _packages[id.package]; | 398 if (hasError) throw new MockLoadException(id); |
| 335 if (package == null) throw new AssetNotFoundException(id); | 399 if (asset == null) throw new AssetNotFoundException(id); |
| 336 | 400 return asset; |
| 337 return package.assets.firstWhere((asset) => asset.id == id, | |
| 338 orElse: () => throw new AssetNotFoundException(id)); | |
| 339 }); | 401 }); |
| 340 } | 402 } |
| 341 } | 403 } |
| 342 | 404 |
| 405 /// Error thrown for assets with [setAssetError] set. |
| 406 class MockLoadException implements Exception { |
| 407 final AssetId id; |
| 408 |
| 409 MockLoadException(this.id); |
| 410 |
| 411 String toString() => "Error loading $id."; |
| 412 } |
| 413 |
| 343 /// Used by [MockProvider] to keep track of which assets and transformers exist | 414 /// Used by [MockProvider] to keep track of which assets and transformers exist |
| 344 /// for each package. | 415 /// for each package. |
| 345 class _MockPackage { | 416 class _MockPackage { |
| 346 final List<MockAsset> assets; | 417 final AssetSet assets; |
| 347 final List<List<Transformer>> transformers; | 418 final List<List<Transformer>> transformers; |
| 348 | 419 |
| 349 _MockPackage(this.assets, Iterable<Iterable<Transformer>> transformers) | 420 _MockPackage(this.assets, Iterable<Iterable<Transformer>> transformers) |
| 350 : transformers = transformers.map((phase) => phase.toList()).toList(); | 421 : transformers = transformers.map((phase) => phase.toList()).toList(); |
| 351 } | 422 } |
| 352 | 423 |
| 353 /// A [Transformer] that takes assets ending with one extension and generates | |
| 354 /// assets with a given extension. | |
| 355 /// | |
| 356 /// Appends the output extension to the contents of the input file. | |
| 357 class RewriteTransformer extends Transformer { | |
| 358 final String from; | |
| 359 final String to; | |
| 360 | |
| 361 /// The number of times the transformer has been applied. | |
| 362 int numRuns = 0; | |
| 363 | |
| 364 /// The number of currently running transforms. | |
| 365 int _runningTransforms = 0; | |
| 366 | |
| 367 /// The completer that the transform is waiting on to complete. | |
| 368 /// | |
| 369 /// If `null` the transform will complete immediately. | |
| 370 Completer _wait; | |
| 371 | |
| 372 /// A future that completes when the first apply of this transformer begins. | |
| 373 Future get started => _started.future; | |
| 374 final _started = new Completer(); | |
| 375 | |
| 376 /// Creates a transformer that rewrites assets whose extension is [from] to | |
| 377 /// one whose extension is [to]. | |
| 378 /// | |
| 379 /// [to] may be a space-separated list in which case multiple outputs will be | |
| 380 /// created for each input. | |
| 381 RewriteTransformer(this.from, this.to); | |
| 382 | |
| 383 /// `true` if any transforms are currently running. | |
| 384 bool get isRunning => _runningTransforms > 0; | |
| 385 | |
| 386 /// Tells the transform to wait during its transformation until [complete()] | |
| 387 /// is called. | |
| 388 /// | |
| 389 /// Lets you test the asynchronous behavior of transformers. | |
| 390 void wait() { | |
| 391 _wait = new Completer(); | |
| 392 } | |
| 393 | |
| 394 void complete() { | |
| 395 _wait.complete(); | |
| 396 _wait = null; | |
| 397 } | |
| 398 | |
| 399 Future<bool> isPrimary(Asset asset) { | |
| 400 return new Future.value(asset.id.extension == ".$from"); | |
| 401 } | |
| 402 | |
| 403 Future apply(Transform transform) { | |
| 404 numRuns++; | |
| 405 if (!_started.isCompleted) _started.complete(); | |
| 406 _runningTransforms++; | |
| 407 return transform.primaryInput.then((input) { | |
| 408 return Future.wait(to.split(" ").map((extension) { | |
| 409 var id = transform.primaryId.changeExtension(".$extension"); | |
| 410 return input.readAsString().then((content) { | |
| 411 transform.addOutput(new MockAsset(id, "$content.$extension")); | |
| 412 }); | |
| 413 })).then((_) { | |
| 414 if (_wait != null) return _wait.future; | |
| 415 }); | |
| 416 }).whenComplete(() { | |
| 417 _runningTransforms--; | |
| 418 }); | |
| 419 } | |
| 420 | |
| 421 String toString() => "$from->$to"; | |
| 422 } | |
| 423 | |
| 424 /// A [Transformer] that takes an input asset that contains a comma-separated | |
| 425 /// list of paths and outputs a file for each path. | |
| 426 class OneToManyTransformer extends Transformer { | |
| 427 final String extension; | |
| 428 | |
| 429 /// The number of times the transformer has been applied. | |
| 430 int numRuns = 0; | |
| 431 | |
| 432 /// Creates a transformer that consumes assets with [extension]. | |
| 433 /// | |
| 434 /// That file contains a comma-separated list of paths and it will output | |
| 435 /// files at each of those paths. | |
| 436 OneToManyTransformer(this.extension); | |
| 437 | |
| 438 Future<bool> isPrimary(Asset asset) { | |
| 439 return new Future.value(asset.id.extension == ".$extension"); | |
| 440 } | |
| 441 | |
| 442 Future apply(Transform transform) { | |
| 443 numRuns++; | |
| 444 return transform.primaryInput.then((input) { | |
| 445 return input.readAsString().then((lines) { | |
| 446 for (var line in lines.split(",")) { | |
| 447 var id = new AssetId(transform.primaryId.package, line); | |
| 448 transform.addOutput(new MockAsset(id, "spread $extension")); | |
| 449 } | |
| 450 }); | |
| 451 }); | |
| 452 } | |
| 453 | |
| 454 String toString() => "1->many $extension"; | |
| 455 } | |
| 456 | |
| 457 /// A transformer that uses the contents of a file to define the other inputs. | |
| 458 /// | |
| 459 /// Outputs a file with the same name as the primary but with an "out" | |
| 460 /// extension containing the concatenated contents of all non-primary inputs. | |
| 461 class ManyToOneTransformer extends Transformer { | |
| 462 final String extension; | |
| 463 | |
| 464 /// The number of times the transformer has been applied. | |
| 465 int numRuns = 0; | |
| 466 | |
| 467 /// Creates a transformer that consumes assets with [extension]. | |
| 468 /// | |
| 469 /// That file contains a comma-separated list of paths and it will input | |
| 470 /// files at each of those paths. | |
| 471 ManyToOneTransformer(this.extension); | |
| 472 | |
| 473 Future<bool> isPrimary(Asset asset) { | |
| 474 return new Future.value(asset.id.extension == ".$extension"); | |
| 475 } | |
| 476 | |
| 477 Future apply(Transform transform) { | |
| 478 numRuns++; | |
| 479 return transform.primaryInput.then((primary) { | |
| 480 return primary.readAsString().then((contents) { | |
| 481 // Get all of the included inputs. | |
| 482 var inputs = contents.split(",").map((path) { | |
| 483 var id = new AssetId(transform.primaryId.package, path); | |
| 484 return transform.getInput(id); | |
| 485 }); | |
| 486 | |
| 487 return Future.wait(inputs); | |
| 488 }).then((inputs) { | |
| 489 // Concatenate them to one output. | |
| 490 var output = ""; | |
| 491 return Future.forEach(inputs, (input) { | |
| 492 return input.readAsString().then((contents) { | |
| 493 output += contents; | |
| 494 }); | |
| 495 }).then((_) { | |
| 496 var id = transform.primaryId.changeExtension(".out"); | |
| 497 transform.addOutput(new MockAsset(id, output)); | |
| 498 }); | |
| 499 }); | |
| 500 }); | |
| 501 } | |
| 502 | |
| 503 String toString() => "many->1 $extension"; | |
| 504 } | |
| 505 | |
| 506 /// A transformer that throws an exception when run, after generating the | |
| 507 /// given outputs. | |
| 508 class BadTransformer extends Transformer { | |
| 509 /// The error it throws. | |
| 510 static const ERROR = "I am a bad transformer!"; | |
| 511 | |
| 512 /// The list of asset names that it should output. | |
| 513 final List<String> outputs; | |
| 514 | |
| 515 BadTransformer(this.outputs); | |
| 516 | |
| 517 Future<bool> isPrimary(Asset asset) => new Future.value(true); | |
| 518 Future apply(Transform transform) { | |
| 519 return newFuture(() { | |
| 520 // Create the outputs first. | |
| 521 for (var output in outputs) { | |
| 522 var id = new AssetId.parse(output); | |
| 523 transform.addOutput(new MockAsset(id, output)); | |
| 524 } | |
| 525 | |
| 526 // Then fail. | |
| 527 throw ERROR; | |
| 528 }); | |
| 529 } | |
| 530 } | |
| 531 | |
| 532 /// A transformer that outputs an asset with the given id. | |
| 533 class CreateAssetTransformer extends Transformer { | |
| 534 final String output; | |
| 535 | |
| 536 CreateAssetTransformer(this.output); | |
| 537 | |
| 538 Future<bool> isPrimary(Asset asset) => new Future.value(true); | |
| 539 | |
| 540 Future apply(Transform transform) { | |
| 541 return newFuture(() { | |
| 542 transform.addOutput(new MockAsset(new AssetId.parse(output), output)); | |
| 543 }); | |
| 544 } | |
| 545 } | |
| 546 | |
| 547 /// An implementation of [Asset] that never hits the file system. | 424 /// An implementation of [Asset] that never hits the file system. |
| 548 class MockAsset implements Asset { | 425 class _MockAsset implements Asset { |
| 549 final AssetId id; | 426 final AssetId id; |
| 550 String contents; | 427 String contents; |
| 551 | 428 |
| 552 MockAsset(this.id, this.contents); | 429 _MockAsset(this.id, this.contents); |
| 553 | 430 |
| 554 Future<String> readAsString({Encoding encoding}) => | 431 Future<String> readAsString({Encoding encoding}) => |
| 555 new Future.value(contents); | 432 new Future.value(contents); |
| 556 | 433 |
| 557 Stream<List<int>> read() => throw new UnimplementedError(); | 434 Stream<List<int>> read() => throw new UnimplementedError(); |
| 558 | 435 |
| 559 String toString() => "MockAsset $id $contents"; | 436 String toString() => "MockAsset $id $contents"; |
| 560 } | 437 } |
| OLD | NEW |