[inspect][dart] Health node API

TESTED=fx run-host-tests fuchsia_inspect_package_unittests

Change-Id: I785fb2df3d4731f1f641522c7d18f682dcc98ff4
diff --git a/public/dart/fuchsia_inspect/lib/src/inspect/inspect.dart b/public/dart/fuchsia_inspect/lib/src/inspect/inspect.dart
index 85c86f0..d961693 100644
--- a/public/dart/fuchsia_inspect/lib/src/inspect/inspect.dart
+++ b/public/dart/fuchsia_inspect/lib/src/inspect/inspect.dart
@@ -136,4 +136,9 @@
   ///
   /// This node can't be deleted; trying to delete it is a NOP.
   Node get root => _singleton.root;
+
+  /// The health [Node] of this Inspect tree.
+  ///
+  /// This node can't be deleted once created; but its creation is on demand.
+  HealthNode get health => _singleton.health;
 }
diff --git a/public/dart/fuchsia_inspect/lib/src/inspect/internal/_inspect_impl.dart b/public/dart/fuchsia_inspect/lib/src/inspect/internal/_inspect_impl.dart
index e102a66..ebe2078 100644
--- a/public/dart/fuchsia_inspect/lib/src/inspect/internal/_inspect_impl.dart
+++ b/public/dart/fuchsia_inspect/lib/src/inspect/internal/_inspect_impl.dart
@@ -8,6 +8,8 @@
 import '../../vmo/vmo_writer.dart';
 import '../inspect.dart';
 
+const _kHealthNodeName = 'fuchsia.inspect.Health';
+
 /// A concrete implementation of the [Inspect] interface.
 ///
 /// This class is not intended to be used directly by authors but instead
@@ -15,6 +17,7 @@
 class InspectImpl implements Inspect {
   Node _root;
   Vmo _vmo;
+  HealthNode _healthNodeSingleton;
 
   /// The default constructor for this instance.
   InspectImpl(vfs.PseudoDir directory, String fileName, VmoWriter writer) {
@@ -27,6 +30,10 @@
   @override
   Node get root => _root;
 
+  @override
+  HealthNode get health =>
+      _healthNodeSingleton ??= HealthNode(_root.child(_kHealthNodeName));
+
   /// For use in testing only. There's probably no way to put @visibleForTesting
   /// because this needs to be used by the Validator Puppet, outside the current
   /// library.
diff --git a/public/dart/fuchsia_inspect/lib/src/inspect/node.dart b/public/dart/fuchsia_inspect/lib/src/inspect/node.dart
index aa886e6..339877d 100644
--- a/public/dart/fuchsia_inspect/lib/src/inspect/node.dart
+++ b/public/dart/fuchsia_inspect/lib/src/inspect/node.dart
@@ -4,6 +4,9 @@
 
 part of 'inspect.dart';
 
+const _kHealthMessageName = 'message';
+const _kHealthStatusName = 'status';
+
 /// A named node in the Inspect tree that can have [Node]s and
 /// properties under it.
 class Node {
@@ -210,3 +213,59 @@
   @override
   void delete() {}
 }
+
+enum _Status {
+  startingUp,
+  ok,
+  unhealthy,
+}
+
+/// Contains subsystem health information.
+class HealthNode {
+  _Status _status;
+  Node _node;
+
+  /// Creates a new health node on the given node.
+  HealthNode(Node node) {
+    _node = node;
+    _setStatus(_Status.startingUp);
+  }
+
+  /// Sets the status of the health node to STARTING_UP.
+  void setStartingUp() {
+    _setStatus(_Status.startingUp);
+  }
+
+  /// Sets the status of the health node to OK.
+  void setOk() {
+    _setStatus(_Status.ok);
+  }
+
+  /// Sets the status of the health node to UNHEALTHY and records the given
+  /// `message`.
+  void setUnhealthy(String message) {
+    _setStatus(_Status.unhealthy, message: message);
+  }
+
+  String _statusString() {
+    switch (_status) {
+      case _Status.startingUp:
+        return 'STARTING_UP';
+      case _Status.ok:
+        return 'OK';
+      case _Status.unhealthy:
+        return 'UNHEALTHY';
+    }
+    return 'UNKNOWN';
+  }
+
+  void _setStatus(_Status status, {String message}) {
+    _status = status;
+    _node.stringProperty(_kHealthStatusName).setValue(_statusString());
+    if (message != null) {
+      _node.stringProperty(_kHealthMessageName).setValue(message);
+    } else {
+      _node.stringProperty(_kHealthMessageName).delete();
+    }
+  }
+}
diff --git a/public/dart/fuchsia_inspect/test/inspect/node_test.dart b/public/dart/fuchsia_inspect/test/inspect/node_test.dart
index f62caf8..c737744 100644
--- a/public/dart/fuchsia_inspect/test/inspect/node_test.dart
+++ b/public/dart/fuchsia_inspect/test/inspect/node_test.dart
@@ -15,14 +15,14 @@
 
 void main() {
   VmoHolder vmo;
+  Inspect inspect;
   Node root;
 
   setUp(() {
     var context = StartupContext.fromStartupInfo();
     vmo = FakeVmoHolder(512);
     var writer = VmoWriter.withVmo(vmo);
-    Inspect inspect =
-        InspectImpl(context.outgoing.debugDir(), 'root.inspect', writer);
+    inspect = InspectImpl(context.outgoing.debugDir(), 'root.inspect', writer);
     root = inspect.root;
   });
 
@@ -210,4 +210,49 @@
       expect(missingProperty.valid, false);
     });
   });
+
+  group('health', () {
+    test('health statuses', () {
+      const kNodeName = 'fuchsia.inspect.Health';
+
+      final health = inspect.health;
+
+      expect(VmoMatcher(vmo).node().at([kNodeName]), hasNoErrors);
+      expect(
+          VmoMatcher(vmo)
+              .node()
+              .at([kNodeName]).propertyEquals('status', 'STARTING_UP'),
+          hasNoErrors);
+      expect(VmoMatcher(vmo).node().at([kNodeName])..missingChild('message'),
+          hasNoErrors);
+
+      health.setOk();
+      expect(
+          VmoMatcher(vmo).node().at([kNodeName]).propertyEquals('status', 'OK'),
+          hasNoErrors);
+      expect(VmoMatcher(vmo).node().at([kNodeName])..missingChild('message'),
+          hasNoErrors);
+
+      health.setStartingUp();
+      expect(
+          VmoMatcher(vmo)
+              .node()
+              .at([kNodeName]).propertyEquals('status', 'STARTING_UP'),
+          hasNoErrors);
+      expect(VmoMatcher(vmo).node().at([kNodeName])..missingChild('message'),
+          hasNoErrors);
+
+      health.setUnhealthy('Oh no');
+      expect(
+          VmoMatcher(vmo)
+              .node()
+              .at([kNodeName]).propertyEquals('status', 'UNHEALTHY'),
+          hasNoErrors);
+      expect(
+          VmoMatcher(vmo)
+              .node()
+              .at([kNodeName]).propertyEquals('message', 'Oh no'),
+          hasNoErrors);
+    });
+  });
 }