From e22369193fc5e4c823101eca30e462234e08bddf Mon Sep 17 00:00:00 2001
From: Jacob MacDonald <jakemac@google.com>
Date: Tue, 9 Feb 2016 11:15:48 -0800
Subject: [PATCH] add AssetGraph/AssetNode

---
 lib/src/asset_graph/exceptions.dart | 13 +++++
 lib/src/asset_graph/graph.dart      | 41 +++++++++++++++
 lib/src/asset_graph/node.dart       | 44 ++++++++++++++++
 lib/src/generate/build.dart         | 45 ++++++++++++++--
 test/asset_graph/graph_test.dart    | 80 +++++++++++++++++++++++++++++
 test/common/assets.dart             |  4 ++
 test/common/matchers.dart           |  3 ++
 7 files changed, 225 insertions(+), 5 deletions(-)
 create mode 100644 lib/src/asset_graph/exceptions.dart
 create mode 100644 lib/src/asset_graph/graph.dart
 create mode 100644 lib/src/asset_graph/node.dart
 create mode 100644 test/asset_graph/graph_test.dart

diff --git a/lib/src/asset_graph/exceptions.dart b/lib/src/asset_graph/exceptions.dart
new file mode 100644
index 000000000..3d477a59b
--- /dev/null
+++ b/lib/src/asset_graph/exceptions.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2016, 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 'node.dart';
+
+class DuplicateAssetNodeException implements Exception {
+  final AssetNode assetNode;
+
+  DuplicateAssetNodeException(this.assetNode);
+
+  @override
+  String toString() => 'DuplicateAssetNodeError: $assetNode';
+}
diff --git a/lib/src/asset_graph/graph.dart b/lib/src/asset_graph/graph.dart
new file mode 100644
index 000000000..7843c2182
--- /dev/null
+++ b/lib/src/asset_graph/graph.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2016, 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 '../asset/id.dart';
+import 'exceptions.dart';
+import 'node.dart';
+
+/// Represents all the [Asset]s in the system, and all of their dependencies.
+class AssetGraph {
+  /// All the [AssetNode]s in the system, indexed by [AssetId].
+  final _nodesById = <AssetId, AssetNode>{};
+
+  AssetGraph();
+
+  /// Checks if [id] exists in the graph.
+  bool contains(AssetId id) => _nodesById.containsKey(id);
+
+  /// Gets the [AssetNode] for [id], if one exists.
+  AssetNode get(AssetId id) => _nodesById[id];
+
+  /// Adds [node] to the graph.
+  void add(AssetNode node) {
+    if (_nodesById.containsKey(node.id)) {
+      throw new DuplicateAssetNodeException(node);
+    }
+    _nodesById[node.id] = node;
+  }
+
+  /// Adds the node returned by [ifAbsent] to the graph, if [id] doesn't
+  /// already exist.
+  ///
+  /// Returns either the existing value or the one just added.
+  AssetNode addIfAbsent(AssetId id, AssetNode ifAbsent()) =>
+      _nodesById.putIfAbsent(id, ifAbsent);
+
+  /// Removes [node] from the graph.
+  AssetNode remove(AssetId id) => _nodesById.remove(id);
+
+  @override
+  toString() => _nodesById.values.toList().toString();
+}
diff --git a/lib/src/asset_graph/node.dart b/lib/src/asset_graph/node.dart
new file mode 100644
index 000000000..2c0c8e294
--- /dev/null
+++ b/lib/src/asset_graph/node.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2016, 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 '../asset/id.dart';
+import '../builder/builder.dart';
+
+/// A node in the asset graph.
+///
+/// This class specifically represents normal (ie: non-generated) assets.
+class AssetNode {
+  /// The asset this node represents.
+  final AssetId id;
+
+  /// The [AssetId]s of all generated assets which depend on this node.
+  final outputs = new Set<AssetId>();
+
+  AssetNode(this.id);
+
+  @override
+  String toString() => 'AssetNode: $id';
+}
+
+/// A generated node in the asset graph.
+class GeneratedAssetNode extends AssetNode {
+  /// The builder which generated this node.
+  final Builder builder;
+
+  /// The primary input which generated this node.
+  final AssetId primaryInput;
+
+  /// The phase group number which generates this asset.
+  final int generatingPhaseGroup;
+
+  /// Whether or not this asset needs to be updated.
+  bool needsUpdate;
+
+  GeneratedAssetNode(this.builder, this.primaryInput, this.generatingPhaseGroup,
+      this.needsUpdate, AssetId id)
+      : super(id);
+
+  @override
+  toString() => 'GeneratedAssetNode: $id generated for input $primaryInput to '
+      '$builder in phase group $generatingPhaseGroup.';
+}
diff --git a/lib/src/generate/build.dart b/lib/src/generate/build.dart
index 8b8cdf46f..1946c84a7 100644
--- a/lib/src/generate/build.dart
+++ b/lib/src/generate/build.dart
@@ -15,6 +15,8 @@ import '../asset/file_based.dart';
 import '../asset/id.dart';
 import '../asset/reader.dart';
 import '../asset/writer.dart';
+import '../asset_graph/graph.dart';
+import '../asset_graph/node.dart';
 import '../builder/builder.dart';
 import '../builder/build_step_impl.dart';
 import '../package_graph/package_graph.dart';
@@ -45,7 +47,6 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
   Logger.root.level = logLevel;
   onLog ??= print;
   var logListener = Logger.root.onRecord.listen(onLog);
-  // No need to create a package graph if we were supplied a reader/writer.
   packageGraph ??= new PackageGraph.forThisPackage();
   var cache = new AssetCache();
   reader ??=
@@ -75,6 +76,7 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
           exception: e, stackTrace: s);
     }
   }, zoneValues: {
+    _assetGraphKey: new AssetGraph(),
     _assetReaderKey: reader,
     _assetWriterKey: writer,
     _packageGraphKey: packageGraph,
@@ -86,11 +88,13 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
 }
 
 /// Keys for reading zone local values.
+Symbol _assetGraphKey = #buildAssetGraph;
 Symbol _assetReaderKey = #buildAssetReader;
 Symbol _assetWriterKey = #buildAssetWriter;
 Symbol _packageGraphKey = #buildPackageGraph;
 
 /// Getters for zone local values.
+AssetGraph get _assetGraph => Zone.current[_assetGraphKey];
 AssetReader get _reader => Zone.current[_assetReaderKey];
 AssetWriter get _writer => Zone.current[_assetWriterKey];
 PackageGraph get _packageGraph => Zone.current[_packageGraphKey];
@@ -123,6 +127,7 @@ Future _deletePreviousOutputs(List<List<Phase>> phaseGroups) async {
       for (var input in inputs) {
         for (var builder in phase.builders) {
           var outputs = builder.declareOutputs(input);
+
           groupOutputIds.addAll(outputs);
           for (var output in outputs) {
             if (allInputs[output.package]?.contains(output) == true) {
@@ -179,6 +184,7 @@ Future _deletePreviousOutputs(List<List<Phase>> phaseGroups) async {
 Future<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
   final allInputs = await _allInputs(phaseGroups);
   final outputs = <Asset>[];
+  int phaseGroupNum = 0;
   for (var group in phaseGroups) {
     final groupOutputs = <Asset>[];
     for (var phase in group) {
@@ -186,7 +192,8 @@ Future<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
       for (var builder in phase.builders) {
         // TODO(jakemac): Optimize, we can run all the builders in a phase
         // at the same time instead of sequentially.
-        await for (var output in _runBuilder(builder, inputs, allInputs)) {
+        await for (var output
+            in _runBuilder(builder, inputs, allInputs, phaseGroupNum)) {
           groupOutputs.add(output);
           outputs.add(output);
         }
@@ -199,6 +206,7 @@ Future<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
       allInputs.putIfAbsent(output.id.package, () => new Set<AssetId>());
       allInputs[output.id.package].add(output.id);
     }
+    phaseGroupNum++;
   }
   return new BuildResult(BuildStatus.Success, BuildType.Full, outputs);
 }
@@ -254,9 +262,10 @@ bool _isValidInput(AssetId input) {
 
 /// Runs [builder] with [inputs] as inputs.
 Stream<Asset> _runBuilder(Builder builder, Iterable<AssetId> primaryInputs,
-    Map<String, Set<AssetId>> allInputs) async* {
+    Map<String, Set<AssetId>> allInputs, int phaseGroupNum) async* {
   for (var input in primaryInputs) {
     var expectedOutputs = builder.declareOutputs(input);
+
     /// Validate [expectedOutputs].
     for (var output in expectedOutputs) {
       if (output.package != _packageGraph.root.name) {
@@ -267,11 +276,37 @@ Stream<Asset> _runBuilder(Builder builder, Iterable<AssetId> primaryInputs,
       }
     }
 
+    /// Add nodes to the [AssetGraph] for [expectedOutputs] and [input].
+    var inputNode = _assetGraph.addIfAbsent(input, () => new AssetNode(input));
+    for (var output in expectedOutputs) {
+      inputNode.outputs.add(output);
+      _assetGraph.addIfAbsent(
+          output,
+          () => new GeneratedAssetNode(
+              builder, input, phaseGroupNum, true, output));
+    }
+
+    /// Skip the build step if none of the outputs need updating.
+    var skipBuild = !expectedOutputs.any((output) =>
+        (_assetGraph.get(output) as GeneratedAssetNode).needsUpdate);
+    if (skipBuild) continue;
+
     var inputAsset = new Asset(input, await _reader.readAsString(input));
-    var buildStep = new BuildStepImpl(
-        inputAsset, expectedOutputs, _reader, _writer, _packageGraph.root.name);
+    var buildStep = new BuildStepImpl(inputAsset, expectedOutputs, _reader,
+        _writer, _packageGraph.root.name);
     await builder.build(buildStep);
     await buildStep.complete();
+
+    /// Update the asset graph based on the dependencies discovered.
+    for (var dependency in buildStep.dependencies) {
+      var dependencyNode =
+          _assetGraph.addIfAbsent(dependency, () => new AssetNode(dependency));
+
+      /// We care about all [expectedOutputs], not just real outputs.
+      dependencyNode.outputs.addAll(expectedOutputs);
+    }
+
+    /// Yield the outputs.
     for (var output in buildStep.outputs) {
       yield output;
     }
diff --git a/test/asset_graph/graph_test.dart b/test/asset_graph/graph_test.dart
new file mode 100644
index 000000000..f12f73bc7
--- /dev/null
+++ b/test/asset_graph/graph_test.dart
@@ -0,0 +1,80 @@
+// Copyright (c) 2016, 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 'package:test/test.dart';
+
+import 'package:build/src/asset_graph/graph.dart';
+import 'package:build/src/asset_graph/node.dart';
+
+import '../common/common.dart';
+
+main() {
+  group('AssetGraph', () {
+    AssetGraph graph;
+
+    setUp(() {
+      graph = new AssetGraph();
+    });
+
+    void expectNodeDoesNotExist(AssetNode node) {
+      expect(graph.contains(node.id), isFalse);
+      expect(graph.get(node.id), isNull);
+    }
+
+    void expectNodeExists(AssetNode node) {
+      expect(graph.contains(node.id), isTrue);
+      expect(graph.get(node.id), node);
+    }
+
+    AssetNode testAddNode() {
+      var node = makeAssetNode();
+      expectNodeDoesNotExist(node);
+      graph.add(node);
+      expectNodeExists(node);
+      return node;
+    }
+
+    test('add, contains, get', () {
+      for (int i = 0; i < 5; i++) {
+        testAddNode();
+      }
+    });
+
+    test('addIfAbsent', () {
+      var node = makeAssetNode();
+      expect(graph.addIfAbsent(node.id, () => node), same(node));
+      expect(graph.contains(node.id), isTrue);
+      
+      var otherNode = new AssetNode(node.id);
+      expect(graph.addIfAbsent(otherNode.id, () => otherNode), same(node));
+      expect(graph.contains(otherNode.id), isTrue);
+    });
+
+    test('duplicate adds throw DuplicateAssetNodeException', () {
+      var node = testAddNode();
+      expect(() => graph.add(node), throwsA(duplicateAssetNodeException));
+    });
+
+    test('remove', () {
+      var nodes = <AssetNode>[];
+      for (int i = 0; i < 5; i++) {
+        nodes.add(testAddNode());
+      }
+      graph.remove(nodes[1].id);
+      graph.remove(nodes[4].id);
+
+      expectNodeExists(nodes[0]);
+      expectNodeDoesNotExist(nodes[1]);
+      expectNodeExists(nodes[2]);
+      expectNodeDoesNotExist(nodes[4]);
+      expectNodeExists(nodes[3]);
+
+      // Doesn't throw.
+      graph.remove(nodes[1].id);
+
+      // Can be added back
+      graph.add(nodes[1]);
+      expectNodeExists(nodes[1]);
+    });
+  });
+}
diff --git a/test/common/assets.dart b/test/common/assets.dart
index 498f4415b..583f1a192 100644
--- a/test/common/assets.dart
+++ b/test/common/assets.dart
@@ -2,6 +2,7 @@
 // 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 'package:build/build.dart';
+import 'package:build/src/asset_graph/node.dart';
 
 import 'in_memory_writer.dart';
 
@@ -33,3 +34,6 @@ void addAssets(Iterable<Asset> assets, InMemoryAssetWriter writer) {
     writer.assets[asset.id] = asset.stringContents;
   }
 }
+
+AssetNode makeAssetNode([String assetIdString]) =>
+    new AssetNode(makeAssetId(assetIdString));
diff --git a/test/common/matchers.dart b/test/common/matchers.dart
index 33555523c..6fa5f0005 100644
--- a/test/common/matchers.dart
+++ b/test/common/matchers.dart
@@ -4,8 +4,11 @@
 import 'package:test/test.dart';
 
 import 'package:build/build.dart';
+import 'package:build/src/asset_graph/exceptions.dart';
 
 final assetNotFoundException = new isInstanceOf<AssetNotFoundException>();
+final duplicateAssetNodeException =
+    new isInstanceOf<DuplicateAssetNodeException>();
 final invalidInputException = new isInstanceOf<InvalidInputException>();
 final invalidOutputException = new isInstanceOf<InvalidOutputException>();
 final packageNotFoundException = new isInstanceOf<PackageNotFoundException>();