[dart][inspect] Example app with tests
fx set core.chromebook-x64 --with '//topaz/bundles:buildbot' --with //bundles:kitchen_sink --with //topaz/public/dart/fuchsia_inspect/examples/inspect_mod:inspect_mod --with //topaz/public/dart/fuchsia_inspect/examples/inspect_mod:inspect_mod_test
fx run-host-tests fuchsia_inspect_package_unittests
fx run-test inspect_mod_test
Test: see above
CF-597 #progress
Change-Id: Idae642d35dd4bccf7c20512700fb32e01d80d9f6
diff --git a/packages/examples/BUILD.gn b/packages/examples/BUILD.gn
index 9b563dc..0abfc71 100644
--- a/packages/examples/BUILD.gn
+++ b/packages/examples/BUILD.gn
@@ -59,6 +59,8 @@
"//topaz/examples/test/driver_example_mod:driver_example_mod_tests",
"//topaz/public/dart/fuchsia_modular/examples/slider_mod",
"//topaz/public/dart/fuchsia_modular/examples/slider_mod:slider_mod_tests",
+ "//topaz/public/dart/fuchsia_inspect/examples/inspect_mod",
+ "//topaz/public/dart/fuchsia_inspect/examples/inspect_mod:inspect_mod_test",
]
}
diff --git a/public/dart/fuchsia_inspect/BUILD.gn b/public/dart/fuchsia_inspect/BUILD.gn
index ddc149e..dde9b6a 100644
--- a/public/dart/fuchsia_inspect/BUILD.gn
+++ b/public/dart/fuchsia_inspect/BUILD.gn
@@ -30,7 +30,6 @@
deps = [
"//third_party/dart-pkg/pub/meta",
"//topaz/public/dart/fuchsia_services",
- "//topaz/public/dart/fuchsia_vfs",
"//topaz/public/dart/zircon",
]
}
@@ -40,8 +39,8 @@
dart_test("fuchsia_inspect_package_unittests") {
sources = [
- "inspect/inspect_test.dart",
"inspect/internal/inspect_impl_test.dart",
+ "inspect/inspect_test.dart",
"inspect/node_test.dart",
"inspect/property_test.dart",
"integration/writer.dart",
diff --git a/public/dart/fuchsia_inspect/examples/inspect_mod/BUILD.gn b/public/dart/fuchsia_inspect/examples/inspect_mod/BUILD.gn
index e0e55e8..46ecfc0 100644
--- a/public/dart/fuchsia_inspect/examples/inspect_mod/BUILD.gn
+++ b/public/dart/fuchsia_inspect/examples/inspect_mod/BUILD.gn
@@ -3,6 +3,7 @@
# found in the LICENSE file.
import("//topaz/runtime/flutter_runner/flutter_app.gni")
+import("//topaz/runtime/dart/dart_fuchsia_test.gni")
flutter_app("inspect_mod") {
main_dart = "lib/main.dart"
@@ -10,6 +11,8 @@
fuchsia_package_name = "inspect_mod"
+ flutter_driver_extendable = true
+
meta = [
{
path = rebase_path("meta/inspect_mod.cmx")
@@ -25,3 +28,38 @@
"//topaz/public/dart/fuchsia_modular",
]
}
+
+dart_fuchsia_test("inspect_mod_test") {
+ sources = [
+ "inspect_mod_test.dart",
+ "util.dart",
+ "vmo_reader.dart"
+ ]
+
+ deps = [
+ "//sdk/fidl/fuchsia.modular.testing",
+ "//sdk/fidl/fuchsia.sys",
+ "//third_party/dart-pkg/git/flutter/packages/flutter_driver",
+ "//third_party/dart-pkg/pub/test",
+ "//topaz/public/dart/fuchsia_inspect",
+ "//topaz/public/dart/fuchsia_services",
+ ]
+
+ meta = [
+ {
+ path = rebase_path("meta/inspect_mod_test.cmx")
+ dest = "inspect_mod_test.cmx"
+ },
+ ]
+
+ environments = []
+
+ # Flutter driver is only available in debug builds, so don't try to run in
+ # release CI/CQ.
+ if (is_debug) {
+ environments += [
+ nuc_env,
+ vim2_env,
+ ]
+ }
+}
diff --git a/public/dart/fuchsia_inspect/examples/inspect_mod/lib/src/inspect_example_app.dart b/public/dart/fuchsia_inspect/examples/inspect_mod/lib/src/inspect_example_app.dart
index 0d6973b..ad1d6ef 100644
--- a/public/dart/fuchsia_inspect/examples/inspect_mod/lib/src/inspect_example_app.dart
+++ b/public/dart/fuchsia_inspect/examples/inspect_mod/lib/src/inspect_example_app.dart
@@ -2,11 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+import 'dart:developer' show Timeline;
+import 'dart:typed_data';
+
+import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:fuchsia_inspect/inspect.dart' as inspect;
+import 'package:fuchsia_logger/logger.dart';
/// A Flutter app that demonstrates usage of the [Inspect] API.
class InspectExampleApp extends StatelessWidget {
+ /// Call InspectExampleApp.stateBloc.updateValue('new state') to display and
+ /// key-publish 'new state'.
+ static final StateBloc stateBloc = StateBloc();
+
static const _appColor = Colors.blue;
final inspect.Node _inspectNode;
@@ -30,7 +40,48 @@
/// Initializes the [Inspect] properties for this widget.
void _initProperties() {
- _inspectNode.stringProperty('app-color').setValue('$_appColor');
+ _inspectNode.stringProperty('greeting').setValue('Hello World');
+ _inspectNode.doubleProperty('double down')
+ ..setValue(1.23)
+ ..add(2);
+ _inspectNode.intProperty('interesting')
+ ..setValue(123)
+ ..subtract(5);
+ _inspectNode
+ .byteDataProperty('bytes')
+ .setValue(ByteData(4)..setUint32(0, 0x01020304));
+ }
+}
+
+/// The [StateBloc] provides actions and streams associated with
+/// the agent that displays state on-screen and exports state keys for test.
+class StateBloc {
+ final _valueController = StreamController<String>.broadcast();
+ String _lastKnownValue = 'Program has started';
+
+ Stream<String> get valueStream => _valueController.stream;
+ String get currentValue => _lastKnownValue;
+
+ void updateValue(String newState) {
+ _lastKnownValue = newState;
+ _valueController.add(newState);
+ }
+
+ void dispose() {
+ _valueController.close();
+ }
+}
+
+class _AnswerFinder {
+ static final _funnel = StreamController<int>();
+ static final _faucet = StreamQueue<int>(_funnel.stream);
+
+ Future<int> getTheAnswer() async {
+ return await _faucet.next;
+ }
+
+ void takeAHint(int n) async {
+ _funnel.add(n);
}
}
@@ -54,7 +105,15 @@
Colors.orange,
];
+ // Helpers to demo tree building and deletion
final inspect.Node _inspectNode;
+ inspect.Node _subtree;
+ int _id = 0;
+
+ // Helpers to demo auto-deletion lifecycle
+ final _answerFinder = _AnswerFinder();
+ int _nextHint = 40;
+ String _answer = 'No answer requested yet';
/// A property that tracks [_counter].
final inspect.IntProperty _counterProperty;
@@ -80,8 +139,11 @@
// to the new value:
//
// _counterProperty.setValue(_counter);
- _counterProperty.add(1);
+ Timeline.timeSync('Inc counter', () {
+ _counterProperty.add(1);
+ });
});
+ InspectExampleApp.stateBloc.updateValue('Counter was incremented');
}
void _decrementCounter() {
@@ -89,6 +151,7 @@
_counter--;
_counterProperty.subtract(1);
});
+ InspectExampleApp.stateBloc.updateValue('Counter was decremented');
}
/// Increments through the possible [_colors].
@@ -98,22 +161,75 @@
setState(() {
_colorIndex++;
- if (_colorIndex >= _colors.length) {
- _colorIndex = 0;
-
- // Contrived example of removing an Inspect property:
- // Once we've looped through the colors once, delete the to.
- //
- // A more realistic example would be if something were being removed
- // from the UI, but this is intended to be super simple.
- _backgroundProperty.delete();
- // Setting _backgroundProperty to null is optional; it's fine to
- // call setValue() on a deleted property - it will just have no effect.
- _backgroundProperty = null;
- }
+ _colorIndex %= _colors.length;
_backgroundProperty?.setValue('$_backgroundColor');
});
+ InspectExampleApp.stateBloc.updateValue('Color was changed');
+ }
+
+ void _makeTree() {
+ _subtree = _inspectNode.child('I think that I shall never see')
+ ..intProperty('int$_id').setValue(_id++);
+ InspectExampleApp.stateBloc.updateValue('Tree was made');
+ }
+
+ void _addToTree() {
+ _subtree?.intProperty('int$_id')?.setValue(_id++);
+ InspectExampleApp.stateBloc.updateValue('Tree was grown');
+ }
+
+ void _deleteTree() {
+ _subtree?.delete();
+ InspectExampleApp.stateBloc.updateValue('Tree was deleted');
+ }
+
+ void _giveHint() {
+ _answerFinder.takeAHint(_nextHint++);
+ InspectExampleApp.stateBloc.updateValue('Gave a hint');
+ }
+
+ void _showAnswer() {
+ var answerFuture = _answerFinder.getTheAnswer();
+ var wait = _inspectNode.stringProperty('waiting')..setValue('for a hint');
+ answerFuture.whenComplete(wait.delete);
+ setState(() {
+ _answer = 'Waiting for answer';
+ });
+ InspectExampleApp.stateBloc.updateValue('Waiting for answer');
+ answerFuture.then((answer) {
+ setState(() {
+ _answer = 'Answer is: $answer';
+ });
+ InspectExampleApp.stateBloc.updateValue('Displayed answer');
+ }).catchError(
+ (e, s) {
+ log.info(' * * Hi2 from inspect_mod');
+ setState(() {
+ _answer = 'Something went wrong getting answer:\n$e\n$s';
+ });
+ },
+ );
+ }
+
+ StreamBuilder<String> buildProgramStateWidget() {
+ var stateBloc = InspectExampleApp.stateBloc;
+ return StreamBuilder<String>(
+ stream: stateBloc.valueStream,
+ initialData: stateBloc.currentValue,
+ builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+ if (snapshot.data == '') {
+ // don't display anything
+ return Offstage();
+ } else {
+ return Container(
+ alignment: Alignment.center,
+ child: Text('State: ${snapshot.data}',
+ style: Theme.of(context).textTheme.display1),
+ key: Key(snapshot.data),
+ );
+ }
+ });
}
@override
@@ -126,14 +242,35 @@
),
backgroundColor: _backgroundColor,
body: Center(
- child: Text(
- 'Counter: $_counter.',
- ),
- ),
+ child: Column(children: [
+ buildProgramStateWidget(),
+ Text('Counter: $_counter', style: Theme.of(context).textTheme.display2),
+ Text('$_answer', style: Theme.of(context).textTheme.display2),
+ ])),
persistentFooterButtons: <Widget>[
FlatButton(
+ onPressed: _giveHint,
+ child: Text('Give hint'),
+ ),
+ FlatButton(
+ onPressed: _showAnswer,
+ child: Text('Get answer'),
+ ),
+ FlatButton(
onPressed: _changeBackground,
- child: Text('Change background color'),
+ child: Text('Change color'),
+ ),
+ FlatButton(
+ onPressed: _makeTree,
+ child: Text('Make tree'),
+ ),
+ FlatButton(
+ onPressed: _addToTree,
+ child: Text('Grow tree'),
+ ),
+ FlatButton(
+ onPressed: _deleteTree,
+ child: Text('Delete tree'),
),
FlatButton(
onPressed: _incrementCounter,
diff --git a/public/dart/fuchsia_inspect/examples/inspect_mod/meta/inspect_mod_test.cmx b/public/dart/fuchsia_inspect/examples/inspect_mod/meta/inspect_mod_test.cmx
new file mode 100644
index 0000000..a5bad84
--- /dev/null
+++ b/public/dart/fuchsia_inspect/examples/inspect_mod/meta/inspect_mod_test.cmx
@@ -0,0 +1,33 @@
+{
+ "facets": {
+ "fuchsia.test": {
+ "injected-services": {
+ "fuchsia.auth.account.AccountManager": "fuchsia-pkg://fuchsia.com/account_manager#meta/account_manager.cmx",
+ "fuchsia.devicesettings.DeviceSettingsManager": "fuchsia-pkg://fuchsia.com/device_settings_manager#meta/device_settings_manager.cmx",
+ "fuchsia.fonts.Provider": "fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx",
+ "fuchsia.sysmem.Allocator": "fuchsia-pkg://fuchsia.com/sysmem_connector#meta/sysmem_connector.cmx",
+ "fuchsia.tracelink.Registry": "fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx",
+ "fuchsia.ui.input.ImeService": "fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx",
+ "fuchsia.ui.policy.Presenter": "fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx",
+ "fuchsia.ui.scenic.Scenic": "fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx",
+ "fuchsia.vulkan.loader.Loader": "fuchsia-pkg://fuchsia.com/vulkan_loader#meta/vulkan_loader.cmx"
+ },
+ "system-services": [
+ "fuchsia.net.SocketProvider"
+ ]
+ }
+ },
+ "program": {
+ "data": "data/inspect_mod_test"
+ },
+ "sandbox": {
+ "features": [
+ "shell"
+ ],
+ "services": [
+ "fuchsia.net.SocketProvider",
+ "fuchsia.sys.Launcher",
+ "fuchsia.sys.Environment"
+ ]
+ }
+}
diff --git a/public/dart/fuchsia_inspect/examples/inspect_mod/test/inspect_mod_test.dart b/public/dart/fuchsia_inspect/examples/inspect_mod/test/inspect_mod_test.dart
new file mode 100644
index 0000000..c6cfbae
--- /dev/null
+++ b/public/dart/fuchsia_inspect/examples/inspect_mod/test/inspect_mod_test.dart
@@ -0,0 +1,229 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: implementation_imports
+
+import 'dart:io' as dartio;
+import 'dart:typed_data';
+import 'package:fidl_fuchsia_modular/fidl_async.dart';
+import 'package:fidl_fuchsia_modular_testing/fidl_async.dart';
+import 'package:fidl_fuchsia_sys/fidl_async.dart';
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:fuchsia_remote_debug_protocol/logging.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:glob/glob.dart';
+import 'package:test/test.dart';
+import 'util.dart';
+
+import 'vmo_reader.dart' show VmoReader;
+
+const Pattern _testAppName = 'inspect_mod.cmx';
+const _testAppUrl = 'fuchsia-pkg://fuchsia.com/inspect_mod#meta/$_testAppName';
+const _modularTestHarnessURL =
+ 'fuchsia-pkg://fuchsia.com/modular_test_harness#meta/modular_test_harness.cmx';
+
+TestHarnessProxy testHarnessProxy = TestHarnessProxy();
+ComponentControllerProxy testHarnessController = ComponentControllerProxy();
+
+// TODO(CF-603) Replace the test-harness / launch-mod boilerplate when possible.
+// Starts Modular TestHarness with dev shells. This should be called from within
+// a try/finally or similar construct that closes the component controller.
+Future<void> _startTestHarness() async {
+ final launcher = LauncherProxy();
+ final incoming = Incoming();
+
+ // launch TestHarness component
+ StartupContext.fromStartupInfo().incoming.connectToService(launcher);
+ await launcher.createComponent(
+ LaunchInfo(
+ url: _modularTestHarnessURL,
+ directoryRequest: incoming.request().passChannel()),
+ testHarnessController.ctrl.request());
+
+ // connect to TestHarness service
+ incoming.connectToService(testHarnessProxy);
+
+ // helper function to convert a map of service to url into list of
+ // [InjectedService]
+ List<InjectedService> _toInjectedServices(Map<String, String> serviceToUrl) {
+ final injectedServices = <InjectedService>[];
+ for (final svcName in serviceToUrl.keys) {
+ injectedServices
+ .add(InjectedService(name: svcName, url: serviceToUrl[svcName]));
+ }
+ return injectedServices;
+ }
+
+ final testHarnessSpec = TestHarnessSpec(
+ envServicesToInherit: ['fuchsia.net.SocketProvider'],
+ envServicesToInject: _toInjectedServices(
+ {
+ 'fuchsia.auth.account.AccountManager':
+ 'fuchsia-pkg://fuchsia.com/account_manager#meta/account_manager.cmx',
+ 'fuchsia.devicesettings.DeviceSettingsManager':
+ 'fuchsia-pkg://fuchsia.com/device_settings_manager#meta/device_settings_manager.cmx',
+ 'fuchsia.fonts.Provider':
+ 'fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx',
+ 'fuchsia.sysmem.Allocator':
+ 'fuchsia-pkg://fuchsia.com/sysmem_connector#meta/sysmem_connector.cmx',
+ 'fuchsia.tracelink.Registry':
+ 'fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx',
+ 'fuchsia.ui.input.ImeService':
+ 'fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx',
+ 'fuchsia.ui.policy.Presenter':
+ 'fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx',
+ 'fuchsia.ui.scenic.Scenic':
+ 'fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx',
+ 'fuchsia.vulkan.loader.Loader':
+ 'fuchsia-pkg://fuchsia.com/vulkan_loader#meta/vulkan_loader.cmx'
+ },
+ ));
+
+ // run the test harness which will create an encapsulated test env
+ await testHarnessProxy.run(testHarnessSpec);
+}
+
+Future<void> _launchModUnderTest() async {
+ // get the puppetMaster service from the encapsulated test env
+ final puppetMasterProxy = PuppetMasterProxy();
+ await testHarnessProxy.connectToModularService(
+ ModularService.withPuppetMaster(puppetMasterProxy.ctrl.request()));
+ // use puppetMaster to start a fake story an launch the mod under test
+ final storyPuppetMasterProxy = StoryPuppetMasterProxy();
+ await puppetMasterProxy.controlStory(
+ 'fooStoryName', storyPuppetMasterProxy.ctrl.request());
+ await storyPuppetMasterProxy.enqueue(<StoryCommand>[
+ StoryCommand.withAddMod(AddMod(
+ modName: ['inspect_mod'],
+ modNameTransitional: 'root',
+ intent: Intent(action: 'action', handler: _testAppUrl),
+ surfaceRelation: SurfaceRelation()))
+ ]);
+ await storyPuppetMasterProxy.execute();
+}
+
+Future<String> _readInspect() async {
+ String globString =
+ '/hub/r/modular_test_harness_*/*/r/session-*/*/r/*/*/c/flutter*/*/c/$_testAppName/*/out/debug/*';
+ String fileName = (await Glob(globString).list().toList())[0].path;
+ var f = dartio.File(fileName);
+ // WARNING: 1) This read is not atomic.
+ // WARNING: 2) This won't work with VMOs written in C++ and maybe elsewhere.
+ // TODO(CF-603): Use direct VMO read when possible.
+ var vmoBytes = await f.readAsBytes();
+ var vmoData = ByteData(vmoBytes.length);
+ for (int i = 0; i < vmoBytes.length; i++) {
+ vmoData.setUint8(i, vmoBytes[i]);
+ }
+ var vmo = FakeVmoReader.usingData(vmoData);
+ return VmoReader(vmo).toString();
+}
+
+void main() {
+ final controller = ComponentControllerProxy();
+ FlutterDriver driver;
+
+ // The following boilerplate is a one time setup required to make
+ // flutter_driver work in Fuchsia.
+ //
+ // When a module built using Flutter starts up in debug mode, it creates an
+ // instance of the Dart VM, and spawns an Isolate (isolated Dart execution
+ // context) containing your module.
+ setUpAll(() async {
+ Logger.globalLevel = LoggingLevel.all;
+
+ await _startTestHarness();
+ await _launchModUnderTest();
+
+ // Creates an object you can use to search for your mod on the machine
+ driver = await FlutterDriver.connect(
+ fuchsiaModuleTarget: _testAppName,
+ printCommunication: true,
+ logCommunicationToFile: false);
+ });
+
+ tearDownAll(() async {
+ await driver?.close();
+ controller.ctrl.close();
+
+ testHarnessProxy.ctrl.close();
+ testHarnessController.ctrl.close();
+ });
+
+ Future<String> tapAndWait(String buttonName, String nextState) async {
+ await driver.tap(find.text(buttonName));
+ await driver.waitFor(find.byValueKey(nextState));
+ return await _readInspect();
+ }
+
+ test('Put the program through its paces', () async {
+ // Wait for initial StateBloc value to appear
+ await driver.waitFor(find.byValueKey('Program has started'));
+ var inspect = await _readInspect();
+/* Expected "inspect" contents (order not guaranteed):
+<> Node: "root"
+<> >> IntProperty "interesting": 118
+<> >> DoubleProperty "double down": 3.23
+<> >> ByteDataProperty "bytes": 01 02 03 04
+<> >> StringProperty "greeting": "Hello World"
+<> >> Node: "home-page"
+<> >> >> IntProperty "counter": 0
+<> >> >> StringProperty "background-color": "Color(0xffffffff)"
+<> >> >> StringProperty "title": "Hello Inspect!"
+*/
+ expect('<> >> IntProperty "interesting": 118', isIn(inspect));
+ expect('<> >> DoubleProperty "double down": 3.23', isIn(inspect));
+ expect('<> >> ByteDataProperty "bytes": 01 02 03 04', isIn(inspect));
+ expect('<> >> StringProperty "greeting": "Hello World"', isIn(inspect));
+ expect('<> >> Node: "home-page"', isIn(inspect));
+ expect('<> >> >> IntProperty "counter": 0', isIn(inspect));
+ expect('<> >> >> StringProperty "background-color": "Color(0xffffffff)"',
+ isIn(inspect));
+ expect('<> >> >> StringProperty "title": "Hello Inspect!"', isIn(inspect));
+
+ // Tap the "Increment counter" button
+ inspect = await tapAndWait('Increment counter', 'Counter was incremented');
+ expect('IntProperty "counter": 0', isNot(isIn(inspect)));
+ expect('IntProperty "counter": 1', isIn(inspect));
+
+ // Tap the "Decrement counter" button
+ inspect = await tapAndWait('Decrement counter', 'Counter was decremented');
+ expect('IntProperty "counter": 1', isNot(isIn(inspect)));
+ expect('IntProperty "counter": 0', isIn(inspect));
+
+ var preTreeInspect = inspect;
+ inspect = await tapAndWait('Make tree', 'Tree was made');
+ expect(
+ '<> >> >> Node: "I think that I shall nev"\n'
+ '<> >> >> >> IntProperty "int0": 0',
+ isIn(inspect));
+
+ inspect = await tapAndWait('Grow tree', 'Tree was grown');
+ expect('<> >> >> >> IntProperty "int0": 0', isIn(inspect));
+ expect('<> >> >> >> IntProperty "int1": 1', isIn(inspect));
+
+ inspect = await tapAndWait('Delete tree', 'Tree was deleted');
+ expect(inspect, preTreeInspect);
+
+ inspect = await tapAndWait('Grow tree', 'Tree was grown');
+ expect(inspect, preTreeInspect);
+
+ inspect = await tapAndWait('Make tree', 'Tree was made');
+ expect(
+ '<> >> >> Node: "I think that I shall nev"\n'
+ '<> >> >> >> IntProperty "int3": 3',
+ isIn(inspect));
+
+ inspect = await tapAndWait('Get answer', 'Waiting for answer');
+ expect('>> StringProperty "waiting": "for a hint"', isIn(inspect));
+
+ inspect = await tapAndWait('Give hint', 'Displayed answer');
+ expect('>> StringProperty "waiting": "for a hint"', isNot(isIn(inspect)));
+
+ inspect = await tapAndWait('Change color', 'Color was changed');
+ expect('<> >> >> StringProperty "background-color": "Color(0xffffffff)"',
+ isNot(isIn(inspect)));
+ expect('<> >> >> StringProperty "background-color": "', isIn(inspect));
+ });
+}
diff --git a/public/dart/fuchsia_inspect/examples/inspect_mod/test/util.dart b/public/dart/fuchsia_inspect/examples/inspect_mod/test/util.dart
new file mode 100644
index 0000000..e2d407d
--- /dev/null
+++ b/public/dart/fuchsia_inspect/examples/inspect_mod/test/util.dart
@@ -0,0 +1,57 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: implementation_imports
+
+import 'dart:typed_data';
+
+import 'package:zircon/zircon.dart';
+import 'package:fuchsia_inspect/src/vmo/vmo_holder.dart';
+
+class FakeVmoReader implements VmoHolder {
+ /// The memory contents of this "VMO".
+ final ByteData bytes;
+
+ @override
+ Vmo get vmo => null;
+
+ /// Size of the "VMO".
+ @override
+ final int size;
+
+ /// Wraps a [FakeVmoReader] around the given data.
+ FakeVmoReader.usingData(this.bytes) : size = bytes.lengthInBytes;
+
+ @override
+ void beginWork() {}
+
+ @override
+ void commit() {}
+
+ /// Writes to the "VMO".
+ @override
+ void write(int offset, ByteData data) {}
+
+ /// Reads from the "VMO".
+ @override
+ ByteData read(int offset, int size) {
+ var reading = ByteData(size);
+ reading.buffer
+ .asUint8List()
+ .setAll(0, bytes.buffer.asUint8List(offset, size));
+ return reading;
+ }
+
+ /// Writes int64 to VMO.
+ @override
+ void writeInt64(int offset, int value) {}
+
+ /// Writes int64 directly to VMO for immediate visibility.
+ @override
+ void writeInt64Direct(int offset, int value) {}
+
+ /// Reads int64 from VMO.
+ @override
+ int readInt64(int offset) => bytes.getInt64(offset, Endian.little);
+}
diff --git a/public/dart/fuchsia_inspect/examples/inspect_mod/test/vmo_reader.dart b/public/dart/fuchsia_inspect/examples/inspect_mod/test/vmo_reader.dart
new file mode 100644
index 0000000..1965196
--- /dev/null
+++ b/public/dart/fuchsia_inspect/examples/inspect_mod/test/vmo_reader.dart
@@ -0,0 +1,199 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: implementation_imports, avoid_relative_lib_imports
+
+import 'dart:convert' show utf8;
+import 'dart:math' show min, max;
+import 'dart:typed_data';
+
+import 'package:fuchsia_inspect/src/vmo/block.dart';
+import 'package:fuchsia_inspect/src/vmo/vmo_fields.dart';
+import 'package:fuchsia_inspect/src/vmo/vmo_holder.dart';
+import 'package:fuchsia_inspect/src/vmo/vmo_writer.dart';
+
+String _utf8ToString(ByteData bytes) {
+ return utf8.decode(bytes.buffer
+ .asUint8ClampedList(bytes.offsetInBytes, bytes.lengthInBytes));
+}
+
+class _Metric {
+ num value;
+
+ _INode parent;
+
+ String _name;
+
+ String get name => _name;
+
+ BlockType _type;
+
+ BlockType get type => _type;
+
+ _Metric(Block metric) {
+ var nameBlock = Block.read(metric.vmo, metric.nameIndex);
+ _name = _utf8ToString(nameBlock.nameUtf8);
+ _type = metric.type;
+ switch (metric.type) {
+ case BlockType.doubleValue:
+ value = metric.doubleValue;
+ break;
+ case BlockType.intValue:
+ value = metric.intValue;
+ break;
+ case BlockType.uintValue:
+ throw StateError('Dart does not expect uint metrics.');
+ default:
+ throw StateError('Type was ${metric.type.name} not numeric.');
+ }
+ }
+
+ @override
+ String toString() =>
+ '${_type == BlockType.intValue ? 'Int' : 'Double'}Property "$_name": $value';
+}
+
+class _Property {
+ _INode parent;
+
+ String _name;
+
+ String get name => _name;
+
+ final int bytesToPrint = 20;
+
+ bool isString;
+
+ int payloadLength;
+
+ ByteData bytes;
+
+ _Property(Block property) {
+ var nameBlock = Block.read(property.vmo, property.nameIndex);
+ _name = _utf8ToString(nameBlock.nameUtf8);
+ isString = property.propertyFlags == propertyUtf8Flag;
+ payloadLength = property.propertyTotalLength;
+ bytes = ByteData(payloadLength);
+ int amountCopied = 0;
+ for (Block extentBlock =
+ Block.read(property.vmo, property.propertyExtentIndex);;
+ extentBlock = Block.read(property.vmo, extentBlock.nextExtent)) {
+ int copyEnd =
+ min(payloadLength, amountCopied + extentBlock.payloadSpaceBytes);
+ bytes.buffer.asUint8List().setRange(
+ amountCopied, copyEnd, extentBlock.payloadBytes.buffer.asUint8List());
+ if (extentBlock.nextExtent == invalidIndex) {
+ break;
+ }
+ }
+ }
+
+ @override
+ String toString() {
+ if (isString) {
+ return 'StringProperty "$name": "${_utf8ToString(bytes)}"';
+ } else {
+ var buffer = StringBuffer('ByteDataProperty "$name":');
+ for (int i = 0; i < bytesToPrint && i < payloadLength; i++) {
+ buffer.write(' ${bytes.getUint8(i).toRadixString(16).padLeft(2, '0')}');
+ }
+ return buffer.toString();
+ }
+ }
+}
+
+class _INode {
+ _INode parent;
+
+ List<_INode> children = <_INode>[];
+
+ List<_Metric> metrics = <_Metric>[];
+
+ List<_Property> properties = <_Property>[];
+
+ String _name;
+
+ String get name => _name;
+
+ void setFrom(Block block) {
+ var nameBlock = Block.read(block.vmo, block.nameIndex);
+ _name = _utf8ToString(nameBlock.nameUtf8);
+ }
+
+ @override
+ String toString() => 'Node: "$_name"';
+
+ String treeToString(String indentStep, [String currentIndent = '']) {
+ var buffer = StringBuffer()
+ ..write(currentIndent)
+ ..writeln(this);
+ String nextIndent = '$currentIndent$indentStep';
+ for (var metric in metrics) {
+ buffer
+ ..write(nextIndent)
+ ..writeln(metric);
+ }
+ for (var property in properties) {
+ buffer
+ ..write(nextIndent)
+ ..writeln(property);
+ }
+ for (var node in children) {
+ buffer.write(node.treeToString(indentStep, nextIndent));
+ }
+ return buffer.toString();
+ }
+}
+
+class _NodeTree {
+ final _nodes = <int, _INode>{};
+ final VmoHolder vmo;
+
+ _NodeTree(this.vmo);
+
+ _INode node(int index) {
+ if (!_nodes.containsKey(index)) {
+ _nodes[index] = _INode();
+ }
+ return _nodes[index];
+ }
+}
+
+class VmoReader {
+ final _NodeTree _nodes;
+
+ VmoReader(VmoHolder vmo) : _nodes = _NodeTree(vmo) {
+ for (int readPosBytes = 0; readPosBytes < vmo.size;) {
+ var block = Block.read(vmo, readPosBytes ~/ bytesPerIndex);
+ switch (block.type) {
+ case BlockType.nodeValue:
+ _INode parentNode = _nodes.node(block.parentIndex);
+ _INode node = _nodes.node(block.index)
+ ..setFrom(block)
+ ..parent = parentNode;
+ parentNode.children.add(node);
+ break;
+ case BlockType.propertyValue:
+ _INode parentNode = _nodes.node(block.parentIndex);
+ var property = _Property(block);
+ property.parent = parentNode;
+ parentNode.properties.add(property);
+ break;
+ case BlockType.intValue:
+ case BlockType.doubleValue:
+ _INode parentNode = _nodes.node(block.parentIndex);
+ var metric = _Metric(block);
+ metric.parent = parentNode;
+ parentNode.metrics.add(metric);
+ break;
+ }
+ readPosBytes += max(block.size, 16);
+ }
+ }
+
+ _INode get _rootNode => _nodes.node(rootNodeIndex);
+
+ @override
+ String toString() => _rootNode.treeToString('>> ', '<> ');
+}
diff --git a/public/dart/fuchsia_inspect/lib/src/vmo/block.dart b/public/dart/fuchsia_inspect/lib/src/vmo/block.dart
index b959501..43911aa 100644
--- a/public/dart/fuchsia_inspect/lib/src/vmo/block.dart
+++ b/public/dart/fuchsia_inspect/lib/src/vmo/block.dart
@@ -25,6 +25,10 @@
/// Index of the block within the VMO
final int index;
+ /// The VMO this Block lives inside.
+ @visibleForTesting
+ VmoHolder get vmo => _vmo;
+
/// Initializes an empty [BlockType.reserved] block that isn't in the VMO yet.
Block.create(this._vmo, this.index) {
_header