Chromium Code Reviews

Side by Side Diff: third_party/pkg/barback-0.13.0/lib/src/transform_node.dart

Issue 291843011: Run pub tests against older versions of barback. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: code review Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | | Annotate | Revision Log
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 library barback.transform_node;
6
7 import 'dart:async';
8
9 import 'asset.dart';
10 import 'asset_id.dart';
11 import 'asset_node.dart';
12 import 'declaring_transform.dart';
13 import 'errors.dart';
14 import 'lazy_transformer.dart';
15 import 'log.dart';
16 import 'phase.dart';
17 import 'stream_pool.dart';
18 import 'transform.dart';
19 import 'transformer.dart';
20 import 'utils.dart';
21
22 /// Describes a transform on a set of assets and its relationship to the build
23 /// dependency graph.
24 ///
25 /// Keeps track of whether it's dirty and needs to be run and which assets it
26 /// depends on.
27 class TransformNode {
28 /// The [Phase] that this transform runs in.
29 final Phase phase;
30
31 /// The [Transformer] to apply to this node's inputs.
32 final Transformer transformer;
33
34 /// The node for the primary asset this transform depends on.
35 final AssetNode primary;
36
37 /// A string describing the location of [this] in the transformer graph.
38 final String _location;
39
40 /// The subscription to [primary]'s [AssetNode.onStateChange] stream.
41 StreamSubscription _primarySubscription;
42
43 /// The subscription to [phase]'s [Phase.onAsset] stream.
44 StreamSubscription<AssetNode> _phaseSubscription;
45
46 /// Whether [this] is dirty and still has more processing to do.
47 bool get isDirty => !_state.isDone;
48
49 /// Whether [transformer] is lazy and this transform has yet to be forced.
50 bool _isLazy;
51
52 /// The subscriptions to each input's [AssetNode.onStateChange] stream.
53 final _inputSubscriptions = new Map<AssetId, StreamSubscription>();
54
55 /// The controllers for the asset nodes emitted by this node.
56 final _outputControllers = new Map<AssetId, AssetNodeController>();
57
58 final _missingInputs = new Set<AssetId>();
59
60 /// The controller that's used to pass [primary] through [this] if it's not
61 /// consumed or overwritten.
62 ///
63 /// This needs an intervening controller to ensure that the output can be
64 /// marked dirty when determining whether [this] will consume or overwrite it,
65 /// and be marked removed if it does. [_passThroughController] will be null
66 /// if the asset is not being passed through.
67 AssetNodeController _passThroughController;
68
69 /// A stream that emits an event whenever [this] is no longer dirty.
70 ///
71 /// This is synchronous in order to guarantee that it will emit an event as
72 /// soon as [isDirty] flips from `true` to `false`.
73 Stream get onDone => _onDoneController.stream;
74 final _onDoneController = new StreamController.broadcast(sync: true);
75
76 /// A stream that emits any new assets emitted by [this].
77 ///
78 /// Assets are emitted synchronously to ensure that any changes are thoroughly
79 /// propagated as soon as they occur.
80 Stream<AssetNode> get onAsset => _onAssetController.stream;
81 final _onAssetController =
82 new StreamController<AssetNode>.broadcast(sync: true);
83
84 /// A stream that emits an event whenever this transform logs an entry.
85 ///
86 /// This is synchronous because error logs can cause the transform to fail, so
87 /// we need to ensure that their processing isn't delayed until after the
88 /// transform or build has finished.
89 Stream<LogEntry> get onLog => _onLogPool.stream;
90 final _onLogPool = new StreamPool<LogEntry>.broadcast();
91
92 /// The current state of [this].
93 var _state = _TransformNodeState.PROCESSING;
94
95 /// Whether [this] has been marked as removed.
96 bool get _isRemoved => _onAssetController.isClosed;
97
98 /// Whether the most recent run of this transform has declared that it
99 /// consumes the primary input.
100 ///
101 /// Defaults to `false`. This is not meaningful unless [_state] is
102 /// [_TransformNodeState.APPLIED].
103 bool _consumePrimary = false;
104
105 TransformNode(this.phase, Transformer transformer, this.primary,
106 this._location)
107 : transformer = transformer,
108 _isLazy = transformer is LazyTransformer {
109 _primarySubscription = primary.onStateChange.listen((state) {
110 if (state.isRemoved) {
111 remove();
112 } else {
113 _dirty(primaryChanged: true);
114 }
115 });
116
117 _phaseSubscription = phase.previous.onAsset.listen((node) {
118 if (_missingInputs.contains(node.id)) _dirty(primaryChanged: false);
119 });
120
121 _process();
122 }
123
124 /// The [TransformInfo] describing this node.
125 ///
126 /// [TransformInfo] is the publicly-visible representation of a transform
127 /// node.
128 TransformInfo get info => new TransformInfo(transformer, primary.id);
129
130 /// Marks this transform as removed.
131 ///
132 /// This causes all of the transform's outputs to be marked as removed as
133 /// well. Normally this will be automatically done internally based on events
134 /// from the primary input, but it's possible for a transform to no longer be
135 /// valid even if its primary input still exists.
136 void remove() {
137 _onAssetController.close();
138 _onDoneController.close();
139 _primarySubscription.cancel();
140 _phaseSubscription.cancel();
141 _clearInputSubscriptions();
142 _clearOutputs();
143 if (_passThroughController != null) {
144 _passThroughController.setRemoved();
145 _passThroughController = null;
146 }
147 }
148
149 /// If [transformer] is lazy, ensures that its concrete outputs will be
150 /// generated.
151 void force() {
152 // TODO(nweiz): we might want to have a timeout after which, if the
153 // transform's outputs have gone unused, we switch it back to lazy mode.
154 if (!_isLazy) return;
155 _isLazy = false;
156 _dirty(primaryChanged: false);
157 }
158
159 /// Marks this transform as dirty.
160 ///
161 /// This causes all of the transform's outputs to be marked as dirty as well.
162 /// [primaryChanged] should be true if and only if [this] was set dirty
163 /// because [primary] changed.
164 void _dirty({bool primaryChanged: false}) {
165 if (!primaryChanged && _state.isNotPrimary) return;
166
167 if (_passThroughController != null) _passThroughController.setDirty();
168 for (var controller in _outputControllers.values) {
169 controller.setDirty();
170 }
171
172 if (_state.isDone) {
173 if (primaryChanged) {
174 _process();
175 } else {
176 _apply();
177 }
178 } else if (primaryChanged) {
179 _state = _TransformNodeState.NEEDS_IS_PRIMARY;
180 } else if (!_state.needsIsPrimary) {
181 _state = _TransformNodeState.NEEDS_APPLY;
182 }
183 }
184
185 /// Determines whether [primary] is primary for [transformer], and if so runs
186 /// [transformer.apply].
187 void _process() {
188 // Clear all the old input subscriptions. If an input is re-used, we'll
189 // re-subscribe.
190 _clearInputSubscriptions();
191 _state = _TransformNodeState.PROCESSING;
192 primary.whenAvailable((_) {
193 _state = _TransformNodeState.PROCESSING;
194 return transformer.isPrimary(primary.asset.id);
195 }).catchError((error, stackTrace) {
196 // If the transform became dirty while processing, ignore any errors from
197 // it.
198 if (_state.needsIsPrimary || _isRemoved) return false;
199
200 // Catch all transformer errors and pipe them to the results stream. This
201 // is so a broken transformer doesn't take down the whole graph.
202 phase.cascade.reportError(_wrapException(error, stackTrace));
203
204 return false;
205 }).then((isPrimary) {
206 if (_isRemoved) return;
207 if (_state.needsIsPrimary) {
208 _process();
209 } else if (isPrimary) {
210 _apply();
211 } else {
212 _clearOutputs();
213 _emitPassThrough();
214 _state = _TransformNodeState.NOT_PRIMARY;
215 _onDoneController.add(null);
216 }
217 });
218 }
219
220 /// Applies this transform.
221 void _apply() {
222 assert(!_onAssetController.isClosed);
223
224 // Clear input subscriptions here as well as in [_process] because [_apply]
225 // may be restarted independently if only a secondary input changes.
226 _clearInputSubscriptions();
227 _state = _TransformNodeState.PROCESSING;
228 primary.whenAvailable((_) {
229 if (_state.needsIsPrimary) return null;
230 _state = _TransformNodeState.PROCESSING;
231 // TODO(nweiz): If [transformer] is a [DeclaringTransformer] but not a
232 // [LazyTransformer], we can get some mileage out of doing a declarative
233 // first so we know how to hook up the assets.
234 if (_isLazy) return _declareLazy();
235 return _applyImmediate();
236 }).catchError((error, stackTrace) {
237 // If the transform became dirty while processing, ignore any errors from
238 // it.
239 if (!_state.isProcessing || _isRemoved) return false;
240
241 // Catch all transformer errors and pipe them to the results stream. This
242 // is so a broken transformer doesn't take down the whole graph.
243 phase.cascade.reportError(_wrapException(error, stackTrace));
244 return true;
245 }).then((hadError) {
246 if (_isRemoved) return;
247
248 if (_state.needsIsPrimary) {
249 _process();
250 } else if (_state.needsApply) {
251 _apply();
252 } else {
253 assert(_state.isProcessing);
254 if (hadError) {
255 _clearOutputs();
256 _dontEmitPassThrough();
257 }
258
259 _state = _TransformNodeState.APPLIED;
260 _onDoneController.add(null);
261 }
262 });
263 }
264
265 /// Gets the asset for an input [id].
266 ///
267 /// If an input with [id] cannot be found, throws an [AssetNotFoundException].
268 Future<Asset> getInput(AssetId id) {
269 return phase.previous.getOutput(id).then((node) {
270 // Throw if the input isn't found. This ensures the transformer's apply
271 // is exited. We'll then catch this and report it through the proper
272 // results stream.
273 if (node == null) {
274 _missingInputs.add(id);
275 throw new AssetNotFoundException(id);
276 }
277
278 _inputSubscriptions.putIfAbsent(node.id, () {
279 return node.onStateChange.listen((_) => _dirty(primaryChanged: false));
280 });
281
282 return node.asset;
283 });
284 }
285
286 /// Applies the transform so that it produces concrete (as opposed to lazy)
287 /// outputs.
288 ///
289 /// Returns whether or not the transformer logged an error.
290 Future<bool> _applyImmediate() {
291 var transformController = new TransformController(this);
292 _onLogPool.add(transformController.onLog);
293
294 return syncFuture(() {
295 return transformer.apply(transformController.transform);
296 }).then((_) {
297 if (!_state.isProcessing || _onAssetController.isClosed) return false;
298 if (transformController.loggedError) return true;
299
300 _consumePrimary = transformController.consumePrimary;
301
302 var newOutputs = transformController.outputs;
303 // Any ids that are for a different package are invalid.
304 var invalidIds = newOutputs
305 .map((asset) => asset.id)
306 .where((id) => id.package != phase.cascade.package)
307 .toSet();
308 for (var id in invalidIds) {
309 newOutputs.removeId(id);
310 // TODO(nweiz): report this as a warning rather than a failing error.
311 phase.cascade.reportError(new InvalidOutputException(info, id));
312 }
313
314 // Remove outputs that used to exist but don't anymore.
315 for (var id in _outputControllers.keys.toList()) {
316 if (newOutputs.containsId(id)) continue;
317 _outputControllers.remove(id).setRemoved();
318 }
319
320 // Emit or stop emitting the pass-through asset between removing and
321 // adding outputs to ensure there are no collisions.
322 if (!newOutputs.containsId(primary.id)) {
323 _emitPassThrough();
324 } else {
325 _dontEmitPassThrough();
326 }
327
328 // Store any new outputs or new contents for existing outputs.
329 for (var asset in newOutputs) {
330 var controller = _outputControllers[asset.id];
331 if (controller != null) {
332 controller.setAvailable(asset);
333 } else {
334 var controller = new AssetNodeController.available(asset, this);
335 _outputControllers[asset.id] = controller;
336 _onAssetController.add(controller.node);
337 }
338 }
339
340 return false;
341 });
342 }
343
344 /// Applies the transform in declarative mode so that it produces lazy
345 /// outputs.
346 ///
347 /// Returns whether or not the transformer logged an error.
348 Future<bool> _declareLazy() {
349 var transformController = new DeclaringTransformController(this);
350
351 return syncFuture(() {
352 return (transformer as LazyTransformer)
353 .declareOutputs(transformController.transform);
354 }).then((_) {
355 if (!_state.isProcessing || _onAssetController.isClosed) return false;
356 if (transformController.loggedError) return true;
357
358 _consumePrimary = transformController.consumePrimary;
359
360 var newIds = transformController.outputIds;
361 var invalidIds =
362 newIds.where((id) => id.package != phase.cascade.package).toSet();
363 for (var id in invalidIds) {
364 newIds.remove(id);
365 // TODO(nweiz): report this as a warning rather than a failing error.
366 phase.cascade.reportError(new InvalidOutputException(info, id));
367 }
368
369 // Remove outputs that used to exist but don't anymore.
370 for (var id in _outputControllers.keys.toList()) {
371 if (newIds.contains(id)) continue;
372 _outputControllers.remove(id).setRemoved();
373 }
374
375 // Emit or stop emitting the pass-through asset between removing and
376 // adding outputs to ensure there are no collisions.
377 if (!newIds.contains(primary.id)) {
378 _emitPassThrough();
379 } else {
380 _dontEmitPassThrough();
381 }
382
383 for (var id in newIds) {
384 var controller = _outputControllers[id];
385 if (controller != null) {
386 controller.setLazy(force);
387 } else {
388 var controller = new AssetNodeController.lazy(id, force, this);
389 _outputControllers[id] = controller;
390 _onAssetController.add(controller.node);
391 }
392 }
393
394 return false;
395 });
396 }
397
398 /// Cancels all subscriptions to secondary input nodes.
399 void _clearInputSubscriptions() {
400 _missingInputs.clear();
401 for (var subscription in _inputSubscriptions.values) {
402 subscription.cancel();
403 }
404 _inputSubscriptions.clear();
405 }
406
407 /// Removes all output assets.
408 void _clearOutputs() {
409 // Remove all the previously-emitted assets.
410 for (var controller in _outputControllers.values) {
411 controller.setRemoved();
412 }
413 _outputControllers.clear();
414 }
415
416 /// Emit the pass-through asset if it's not being emitted already.
417 void _emitPassThrough() {
418 assert(!_outputControllers.containsKey(primary.id));
419
420 if (_consumePrimary) return;
421 if (_passThroughController == null) {
422 _passThroughController = new AssetNodeController.from(primary);
423 _onAssetController.add(_passThroughController.node);
424 } else {
425 _passThroughController.setAvailable(primary.asset);
426 }
427 }
428
429 /// Stop emitting the pass-through asset if it's being emitted already.
430 void _dontEmitPassThrough() {
431 if (_passThroughController == null) return;
432 _passThroughController.setRemoved();
433 _passThroughController = null;
434 }
435
436 BarbackException _wrapException(error, StackTrace stackTrace) {
437 if (error is! AssetNotFoundException) {
438 return new TransformerException(info, error, stackTrace);
439 } else {
440 return new MissingInputException(info, error.id);
441 }
442 }
443
444 String toString() =>
445 "transform node in $_location for $transformer on $primary";
446 }
447
448 /// The enum of states that [TransformNode] can be in.
449 class _TransformNodeState {
450 /// The transform node is running [Transformer.isPrimary] or
451 /// [Transformer.apply] and doesn't need to re-run them.
452 ///
453 /// If there are no external changes by the time the processing finishes, this
454 /// will transition to [APPLIED] or [NOT_PRIMARY] depending on the result of
455 /// [Transformer.isPrimary]. If the primary input changes, this will
456 /// transition to [NEEDS_IS_PRIMARY]. If a secondary input changes, this will
457 /// transition to [NEEDS_APPLY].
458 static final PROCESSING = const _TransformNodeState._("processing");
459
460 /// The transform is running [Transformer.isPrimary] or [Transformer.apply],
461 /// but since it started the primary input changed, so it will need to re-run
462 /// [Transformer.isPrimary].
463 ///
464 /// This will always transition to [Transformer.PROCESSING].
465 static final NEEDS_IS_PRIMARY =
466 const _TransformNodeState._("needs isPrimary");
467
468 /// The transform is running [Transformer.apply], but since it started a
469 /// secondary input changed, so it will need to re-run [Transformer.apply].
470 ///
471 /// If there are no external changes by the time [Transformer.apply] finishes,
472 /// this will transition to [PROCESSING]. If the primary input changes, this
473 /// will transition to [NEEDS_IS_PRIMARY].
474 static final NEEDS_APPLY = const _TransformNodeState._("needs apply");
475
476 /// The transform has finished running [Transformer.apply], whether or not it
477 /// emitted an error.
478 ///
479 /// If the primary input or a secondary input changes, this will transition to
480 /// [PROCESSING].
481 static final APPLIED = const _TransformNodeState._("applied");
482
483 /// The transform has finished running [Transformer.isPrimary], which returned
484 /// `false`.
485 ///
486 /// If the primary input changes, this will transition to [PROCESSING].
487 static final NOT_PRIMARY = const _TransformNodeState._("not primary");
488
489 /// Whether [this] is [PROCESSING].
490 bool get isProcessing => this == _TransformNodeState.PROCESSING;
491
492 /// Whether [this] is [NEEDS_IS_PRIMARY].
493 bool get needsIsPrimary => this == _TransformNodeState.NEEDS_IS_PRIMARY;
494
495 /// Whether [this] is [NEEDS_APPLY].
496 bool get needsApply => this == _TransformNodeState.NEEDS_APPLY;
497
498 /// Whether [this] is [APPLIED].
499 bool get isApplied => this == _TransformNodeState.APPLIED;
500
501 /// Whether [this] is [NOT_PRIMARY].
502 bool get isNotPrimary => this == _TransformNodeState.NOT_PRIMARY;
503
504 /// Whether the transform has finished running [Transformer.isPrimary] and
505 /// [Transformer.apply].
506 ///
507 /// Specifically, whether [this] is [APPLIED] or [NOT_PRIMARY].
508 bool get isDone => isApplied || isNotPrimary;
509
510 final String name;
511
512 const _TransformNodeState._(this.name);
513
514 String toString() => name;
515 }
OLDNEW
« no previous file with comments | « third_party/pkg/barback-0.13.0/lib/src/transform_logger.dart ('k') | third_party/pkg/barback-0.13.0/lib/src/transformer.dart » ('j') | no next file with comments »

Powered by Google App Engine