Add basic support for golden image file testing (#17094)

* Add a `matchesGoldenFile()` async matcher that will match
  a finder's widget's rasterized image against a golden file.
* Add support for pluggable image comparison backends
* Add a default backend that does simplistic PNG byte
  comparison on locally stored golden files.
* Add support for `flutter test --update-goldens`, which will
  treat the rasterized image bytes produced during the test
  as the new golden bytes and update the golden file accordingly

Still TODO:

* Add support for the `flutter_test_config.dart` test config hook
* Utilize `flutter_test_config.dart` in `packages/flutter/test`
  to install a backend that retrieves golden files from a dedicated
  `flutter/goldens` repo

https://github.com/flutter/flutter/issues/16859
diff --git a/dev/tools/vitool/pubspec.yaml b/dev/tools/vitool/pubspec.yaml
index a435e89..aa03baa 100644
--- a/dev/tools/vitool/pubspec.yaml
+++ b/dev/tools/vitool/pubspec.yaml
@@ -19,7 +19,8 @@
   stack_trace: 1.9.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
 
 dev_dependencies:
-  test: 0.12.34
+  flutter_test:
+    sdk: flutter
 
   async: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   barback: 0.15.2+15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
diff --git a/dev/tools/vitool/test/vitool_test.dart b/dev/tools/vitool/test/vitool_test.dart
index 73a89d0..d25f741 100644
--- a/dev/tools/vitool/test/vitool_test.dart
+++ b/dev/tools/vitool/test/vitool_test.dart
@@ -5,8 +5,8 @@
 import 'dart:math';
 
 import 'package:collection/collection.dart';
+import 'package:flutter_test/flutter_test.dart';
 import 'package:vitool/vitool.dart';
-import 'package:test/test.dart';
 import 'package:path/path.dart' as path;
 
 const String kPackagePath = '..';
diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart
index 02f5370..cd52e74 100644
--- a/packages/flutter_test/lib/flutter_test.dart
+++ b/packages/flutter_test/lib/flutter_test.dart
@@ -11,6 +11,7 @@
 export 'src/binding.dart';
 export 'src/controller.dart';
 export 'src/finders.dart';
+export 'src/goldens.dart';
 export 'src/matchers.dart';
 export 'src/nonconst.dart';
 export 'src/platform.dart';
diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart
new file mode 100644
index 0000000..4e99e78
--- /dev/null
+++ b/packages/flutter_test/lib/src/goldens.dart
@@ -0,0 +1,171 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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:io';
+import 'dart:typed_data';
+
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart' show TestFailure;
+
+/// Compares rasterized image bytes against a golden image file.
+///
+/// Instances of this comparator will be used as the backend for
+/// [matchesGoldenFile].
+///
+/// Instances of this comparator will be invoked by the test framework in the
+/// [TestWidgetsFlutterBinding.runAsync] zone and are thus not subject to the
+/// fake async constraints that are normally imposed on widget tests (i.e. the
+/// need or the ability to call [WidgetTester.pump] to advance the microtask
+/// queue).
+abstract class GoldenFileComparator {
+  /// Compares [imageBytes] against the golden file identified by [golden].
+  ///
+  /// The returned future completes with a boolean value that indicates whether
+  /// [imageBytes] matches the golden file's bytes within the tolerance defined
+  /// by the comparator.
+  ///
+  /// In the case of comparison mismatch, the comparator may choose to throw a
+  /// [TestFailure] if it wants to control the failure message.
+  ///
+  /// The method by which [golden] is located and by which its bytes are loaded
+  /// is left up to the implementation class. For instance, some implementations
+  /// may load files from the local file system, whereas others may load files
+  /// over the network or from a remote repository.
+  Future<bool> compare(Uint8List imageBytes, Uri golden);
+
+  /// Updates the golden file identified by [golden] with [imageBytes].
+  ///
+  /// This will be invoked in lieu of [compare] when [autoUpdateGoldenFiles]
+  /// is `true` (which gets set automatically by the test framework when the
+  /// user runs `flutter test --update-goldens`).
+  ///
+  /// The method by which [golden] is located and by which its bytes are written
+  /// is left up to the implementation class.
+  Future<void> update(Uri golden, Uint8List imageBytes);
+}
+
+/// Compares rasterized image bytes against a golden image file.
+///
+/// This comparator is used as the backend for [matchesGoldenFile].
+///
+/// The default comparator, [LocalFileComparator], will treat the golden key as
+/// a relative path from the test file's directory. It will then load the
+/// golden file's bytes from disk and perform a byte-for-byte comparison of the
+/// encoded PNGs, returning true only if there's an exact match.
+///
+/// Callers may choose to override the default comparator by setting this to a
+/// custom comparator during test set-up. For example, some projects may wish to
+/// install a more intelligent comparator that knows how to decode the PNG
+/// images to raw pixels and compare pixel vales, reporting specific differences
+/// between the images.
+GoldenFileComparator goldenFileComparator = const _UninitializedComparator();
+
+/// Whether golden files should be automatically updated during tests rather
+/// than compared to the image bytes recorded by the tests.
+///
+/// When this is `true`, [matchesGoldenFile] will always report a successful
+/// match, because the bytes being tested implicitly become the new golden.
+///
+/// The Flutter tool will automatically set this to `true` when the user runs
+/// `flutter test --update-goldens`, so callers should generally never have to
+/// explicitly modify this value.
+///
+/// See also:
+///
+///   * [goldenFileComparator]
+bool autoUpdateGoldenFiles = false;
+
+/// Placeholder to signal an unexpected error in the testing framework itself.
+///
+/// The test harness file that gets generated by the Flutter tool when the
+/// user runs `flutter test` is expected to set [goldenFileComparator] to
+/// a valid comparator. From there, the caller may choose to override it by
+/// setting the comparator during test initialization (e.g. in `setUpAll()`).
+/// But under no circumstances do we expect it to remain uninitialized.
+class _UninitializedComparator implements GoldenFileComparator {
+  const _UninitializedComparator();
+
+  @override
+  Future<bool> compare(Uint8List imageBytes, Uri golden) {
+    throw new StateError('goldenFileComparator has not been initialized');
+  }
+
+  @override
+  Future<void> update(Uri golden, Uint8List imageBytes) {
+    throw new StateError('goldenFileComparator has not been initialized');
+  }
+}
+
+/// The default [GoldenFileComparator] implementation.
+///
+/// This comparator loads golden files from the local file system, treating the
+/// golden key as a relative path from the test file's directory.
+///
+/// This comparator performs a very simplistic comparison, doing a byte-for-byte
+/// comparison of the encoded PNGs, returning true only if there's an exact
+/// match. This means it will fail the test if two PNGs represent the same
+/// pixels but are encoded differently.
+class LocalFileComparator implements GoldenFileComparator {
+  /// Creates a new [LocalFileComparator] for the specified [testFile].
+  ///
+  /// Golden file keys will be interpreted as file paths relative to the
+  /// directory in which [testFile] resides.
+  ///
+  /// The [testFile] URI must represent a file.
+  LocalFileComparator(Uri testFile)
+      : assert(testFile.scheme == 'file'),
+        basedir = new Uri.directory(_path.dirname(_path.fromUri(testFile)));
+
+  // Due to https://github.com/flutter/flutter/issues/17118, we need to
+  // explicitly set the path style.
+  static final path.Context _path = new path.Context(style: Platform.isWindows
+      ? path.Style.windows
+      : path.Style.posix);
+
+  /// The directory in which the test was loaded.
+  ///
+  /// Golden file keys will be interpreted as file paths relative to this
+  /// directory.
+  final Uri basedir;
+
+  @override
+  Future<bool> compare(Uint8List imageBytes, Uri golden) async {
+    final File goldenFile = _getFile(golden);
+    if (!goldenFile.existsSync()) {
+      throw new TestFailure('Could not be compared against non-existent file: "$golden"');
+    }
+    final List<int> goldenBytes = await goldenFile.readAsBytes();
+    return _areListsEqual(imageBytes, goldenBytes);
+  }
+
+  @override
+  Future<void> update(Uri golden, Uint8List imageBytes) async {
+    final File goldenFile = _getFile(golden);
+    await goldenFile.writeAsBytes(imageBytes, flush: true);
+  }
+
+  File _getFile(Uri golden) {
+    return new File(_path.join(_path.fromUri(basedir), golden.path));
+  }
+
+  static bool _areListsEqual<T>(List<T> list1, List<T> list2) {
+    if (identical(list1, list2)) {
+      return true;
+    }
+    if (list1 == null || list2 == null) {
+      return false;
+    }
+    final int length = list1.length;
+    if (length != list2.length) {
+      return false;
+    }
+    for (int i = 0; i < length; i++) {
+      if (list1[i] != list2[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart
index c6b4b52..d4df942 100644
--- a/packages/flutter_test/lib/src/matchers.dart
+++ b/packages/flutter_test/lib/src/matchers.dart
@@ -2,15 +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:math' as math;
+import 'dart:ui' as ui;
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/services.dart';
 import 'package:meta/meta.dart';
-import 'package:test/test.dart';
+import 'package:test/test.dart' hide TypeMatcher;
+import 'package:test/src/frontend/async_matcher.dart'; // ignore: implementation_imports
 
+import 'binding.dart';
 import 'finders.dart';
+import 'goldens.dart';
 
 /// Asserts that the [Finder] matches no widgets in the widget tree.
 ///
@@ -234,7 +240,28 @@
 /// the area you expect to paint in for [areaToCompare] to catch errors where
 /// the path draws outside the expected area.
 Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20})
-  => new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize); 
+  => new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
+
+/// Asserts that a [Finder] matches exactly one widget whose rendered image
+/// matches the golden image file identified by [key].
+///
+/// [key] may be either a [Uri] or a [String] representation of a URI.
+///
+/// This is an asynchronous matcher, meaning that callers should use
+/// [expectLater] when using this matcher and await the future returned by
+/// [expectLater].
+///
+/// See also:
+///
+///  * [goldenFileComparator], which acts as the backend for this matcher.
+Matcher matchesGoldenFile(dynamic key) {
+  if (key is Uri) {
+    return new _MatchesGoldenFile(key);
+  } else if (key is String) {
+    return new _MatchesGoldenFile.forStringPath(key);
+  }
+  throw new ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
+}
 
 class _FindsWidgetMatcher extends Matcher {
   const _FindsWidgetMatcher(this.min, this.max);
@@ -1183,3 +1210,51 @@
   Description describe(Description description) =>
     description.add('covers expected area and only expected area');
 }
+
+class _MatchesGoldenFile extends AsyncMatcher {
+  const _MatchesGoldenFile(this.key);
+
+  _MatchesGoldenFile.forStringPath(String path) : key = Uri.parse(path);
+
+  final Uri key;
+
+  @override
+  Future<String> matchAsync(covariant Finder finder) async {
+    final Iterable<Element> elements = finder.evaluate();
+    if (elements.isEmpty) {
+      return 'could not be rendered because no widget was found';
+    } else if (elements.length > 1) {
+      return 'matched too many widgets';
+    }
+    final Element element = elements.single;
+
+    RenderObject renderObject = element.renderObject;
+    while (!renderObject.isRepaintBoundary) {
+      renderObject = renderObject.parent;
+      assert(renderObject != null);
+    }
+    assert(!renderObject.debugNeedsPaint);
+    final OffsetLayer layer = renderObject.layer;
+    final Future<ui.Image> imageFuture = layer.toImage(element.size);
+
+    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
+    return binding.runAsync<String>(() async {
+      final ui.Image image = await imageFuture;
+      final ByteData bytes = await image.toByteData(format: ui.ImageByteFormat.png);
+      if (autoUpdateGoldenFiles) {
+        await goldenFileComparator.update(key, bytes.buffer.asUint8List());
+      } else {
+        try {
+          final bool success = await goldenFileComparator.compare(bytes.buffer.asUint8List(), key);
+          return success ? null : 'does not match';
+        } on TestFailure catch (ex) {
+          return ex.message;
+        }
+      }
+    });
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('one widget whose rasterized image matches golden image "$key"');
+}
diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml
index 2998e96..8e39c6a 100644
--- a/packages/flutter_test/pubspec.yaml
+++ b/packages/flutter_test/pubspec.yaml
@@ -2,17 +2,20 @@
 dependencies:
   # To update these, use "flutter update-packages --force-upgrade".
 
+  flutter:
+    sdk: flutter
+
   # We depend on very specific internal implementation details of the
   # 'test' package, which change between versions, so when upgrading
   # this, make sure the tests are still running correctly.
   test: 0.12.34
 
+  # Used by golden file comparator
+  path: 1.5.1
+
   # We use FakeAsync and other testing utilities.
   quiver: 0.29.0+1
 
-  flutter:
-    sdk: flutter
-
   # We import stack_trace because the test packages uses it and we
   # need to be able to unmangle the stack traces that it passed to
   # stack_trace. See https://github.com/dart-lang/test/issues/590
@@ -47,7 +50,6 @@
   node_preamble: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   package_config: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   package_resolver: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
-  path: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   plugin: 0.2.0+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   pool: 1.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   pub_semver: 1.3.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@@ -67,4 +69,9 @@
   web_socket_channel: 1.0.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   yaml: 2.1.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
 
-# PUBSPEC CHECKSUM: 1343
+dev_dependencies:
+  file: 5.0.0
+
+  intl: 0.15.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+# PUBSPEC CHECKSUM: 67b7
diff --git a/packages/flutter_test/test/goldens_test.dart b/packages/flutter_test/test/goldens_test.dart
new file mode 100644
index 0000000..7266815
--- /dev/null
+++ b/packages/flutter_test/test/goldens_test.dart
@@ -0,0 +1,151 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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:io' as io;
+import 'dart:typed_data';
+
+import 'package:file/memory.dart';
+import 'package:test/test.dart' as test_package;
+import 'package:test/test.dart' hide test;
+
+import 'package:flutter_test/flutter_test.dart' show goldenFileComparator, LocalFileComparator;
+
+const List<int> _kExpectedBytes = const <int>[1, 2, 3];
+
+void main() {
+  MemoryFileSystem fs;
+
+  setUp(() {
+    final FileSystemStyle style = io.Platform.isWindows
+        ? FileSystemStyle.windows
+        : FileSystemStyle.posix;
+    fs = new MemoryFileSystem(style: style);
+  });
+
+  void test(String description, FutureOr<void> body()) {
+    test_package.test(description, () {
+      return io.IOOverrides.runZoned(
+        body,
+        createDirectory: (String path) => fs.directory(path),
+        createFile: (String path) => fs.file(path),
+        createLink: (String path) => fs.link(path),
+        getCurrentDirectory: () => fs.currentDirectory,
+        setCurrentDirectory: (String path) => fs.currentDirectory = path,
+        getSystemTempDirectory: () => fs.systemTempDirectory,
+        stat: (String path) => fs.stat(path),
+        statSync: (String path) => fs.statSync(path),
+        fseIdentical: (String p1, String p2) => fs.identical(p1, p2),
+        fseIdenticalSync: (String p1, String p2) => fs.identicalSync(p1, p2),
+        fseGetType: (String path, bool followLinks) => fs.type(path, followLinks: followLinks),
+        fseGetTypeSync: (String path, bool followLinks) => fs.typeSync(path, followLinks: followLinks),
+        fsWatch: (String a, int b, bool c) => throw new UnsupportedError('unsupported'),
+        fsWatchIsSupported: () => fs.isWatchSupported,
+      );
+    });
+  }
+
+  group('goldenFileComparator', () {
+    test('is initialized by test framework', () {
+      expect(goldenFileComparator, isNotNull);
+      expect(goldenFileComparator, const isInstanceOf<LocalFileComparator>());
+      final LocalFileComparator comparator = goldenFileComparator;
+      expect(comparator.basedir.path, contains('flutter_test'));
+    });
+  });
+
+  group('LocalFileComparator', () {
+    LocalFileComparator comparator;
+
+    setUp(() {
+      comparator = new LocalFileComparator(new Uri.file('/golden_test.dart'));
+    });
+
+    test('calculates basedir correctly', () {
+      expect(comparator.basedir, new Uri.file('/'));
+      comparator = new LocalFileComparator(new Uri.file('/foo/bar/golden_test.dart'));
+      expect(comparator.basedir, new Uri.file('/foo/bar/'));
+    });
+
+    group('compare', () {
+      Future<bool> doComparison([String golden = 'golden.png']) {
+        final Uri uri = new Uri.file(golden);
+        return comparator.compare(
+          new Uint8List.fromList(_kExpectedBytes),
+          uri,
+        );
+      }
+
+      group('succeeds', () {
+        test('when golden file is in same folder as test', () async {
+          fs.file('/golden.png').writeAsBytesSync(_kExpectedBytes);
+          final bool success = await doComparison();
+          expect(success, isTrue);
+        });
+
+        test('when golden file is in subfolder of test', () async {
+          fs.file('/sub/foo.png')
+            ..createSync(recursive: true)
+            ..writeAsBytesSync(_kExpectedBytes);
+          final bool success = await doComparison('sub/foo.png');
+          expect(success, isTrue);
+        });
+      });
+
+      group('fails', () {
+        test('when golden file does not exist', () async {
+          final Future<bool> comparison = doComparison();
+          expect(comparison, throwsA(const isInstanceOf<TestFailure>()));
+        });
+
+        test('when golden bytes are leading subset of image bytes', () async {
+          fs.file('/golden.png').writeAsBytesSync(<int>[1, 2]);
+          expect(await doComparison(), isFalse);
+        });
+
+        test('when golden bytes are leading superset of image bytes', () async {
+          fs.file('/golden.png').writeAsBytesSync(<int>[1, 2, 3, 4]);
+          expect(await doComparison(), isFalse);
+        });
+
+        test('when golden bytes are trailing subset of image bytes', () async {
+          fs.file('/golden.png').writeAsBytesSync(<int>[2, 3]);
+          expect(await doComparison(), isFalse);
+        });
+
+        test('when golden bytes are trailing superset of image bytes', () async {
+          fs.file('/golden.png').writeAsBytesSync(<int>[0, 1, 2, 3]);
+          expect(await doComparison(), isFalse);
+        });
+
+        test('when golden bytes are disjoint from image bytes', () async {
+          fs.file('/golden.png').writeAsBytesSync(<int>[4, 5, 6]);
+          expect(await doComparison(), isFalse);
+        });
+
+        test('when golden bytes are empty', () async {
+          fs.file('/golden.png').writeAsBytesSync(<int>[]);
+          expect(await doComparison(), isFalse);
+        });
+      });
+    });
+
+    group('update', () {
+      test('updates existing file', () async {
+        fs.file('/golden.png').writeAsBytesSync(_kExpectedBytes);
+        const List<int> newBytes = const <int>[11, 12, 13];
+        await comparator.update(new Uri.file('golden.png'), new Uint8List.fromList(newBytes));
+        expect(fs.file('/golden.png').readAsBytesSync(), newBytes);
+      });
+
+      test('creates non-existent file', () async {
+        expect(fs.file('/foo.png').existsSync(), isFalse);
+        const List<int> newBytes = const <int>[11, 12, 13];
+        await comparator.update(new Uri.file('foo.png'), new Uint8List.fromList(newBytes));
+        expect(fs.file('/foo.png').existsSync(), isTrue);
+        expect(fs.file('/foo.png').readAsBytesSync(), newBytes);
+      });
+    });
+  });
+}
diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart
index 1fd404e..39d3649 100644
--- a/packages/flutter_test/test/matchers_test.dart
+++ b/packages/flutter_test/test/matchers_test.dart
@@ -2,8 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:typed_data';
 import 'dart:ui';
 
+import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 /// Class that makes it easy to mock common toStringDeep behavior.
@@ -288,4 +290,137 @@
       );
     });
   });
+
+  group('matchesGoldenFile', () {
+    _FakeComparator comparator;
+
+    Widget boilerplate(Widget child) {
+      return new Directionality(
+        textDirection: TextDirection.ltr,
+        child: child,
+      );
+    }
+
+    setUp(() {
+      comparator = new _FakeComparator();
+      goldenFileComparator = comparator;
+    });
+
+    group('matches', () {
+      testWidgets('if comparator succeeds', (WidgetTester tester) async {
+        await tester.pumpWidget(boilerplate(const Text('hello')));
+        final Finder finder = find.byType(Text);
+        await expectLater(finder, matchesGoldenFile('foo.png'));
+        expect(comparator.invocation, _ComparatorInvocation.compare);
+        expect(comparator.imageBytes, hasLength(greaterThan(0)));
+        expect(comparator.golden, Uri.parse('foo.png'));
+      });
+    });
+
+    group('does not match', () {
+      testWidgets('if comparator returns false', (WidgetTester tester) async {
+        comparator.behavior = _ComparatorBehavior.returnFalse;
+        await tester.pumpWidget(boilerplate(const Text('hello')));
+        final Finder finder = find.byType(Text);
+        try {
+          await expectLater(finder, matchesGoldenFile('foo.png'));
+          fail('TestFailure expected but not thrown');
+        } on TestFailure catch (error) {
+          expect(comparator.invocation, _ComparatorInvocation.compare);
+          expect(error.message, contains('does not match'));
+        }
+      });
+
+      testWidgets('if comparator throws', (WidgetTester tester) async {
+        comparator.behavior = _ComparatorBehavior.throwTestFailure;
+        await tester.pumpWidget(boilerplate(const Text('hello')));
+        final Finder finder = find.byType(Text);
+        try {
+          await expectLater(finder, matchesGoldenFile('foo.png'));
+          fail('TestFailure expected but not thrown');
+        } on TestFailure catch (error) {
+          expect(comparator.invocation, _ComparatorInvocation.compare);
+          expect(error.message, contains('fake message'));
+        }
+      });
+
+      testWidgets('if finder finds no widgets', (WidgetTester tester) async {
+        await tester.pumpWidget(boilerplate(new Container()));
+        final Finder finder = find.byType(Text);
+        try {
+          await expectLater(finder, matchesGoldenFile('foo.png'));
+          fail('TestFailure expected but not thrown');
+        } on TestFailure catch (error) {
+          expect(comparator.invocation, isNull);
+          expect(error.message, contains('no widget was found'));
+        }
+      });
+
+      testWidgets('if finder finds multiple widgets', (WidgetTester tester) async {
+        await tester.pumpWidget(boilerplate(new Column(
+          children: const <Widget>[const Text('hello'), const Text('world')],
+        )));
+        final Finder finder = find.byType(Text);
+        try {
+          await expectLater(finder, matchesGoldenFile('foo.png'));
+          fail('TestFailure expected but not thrown');
+        } on TestFailure catch (error) {
+          expect(comparator.invocation, isNull);
+          expect(error.message, contains('too many widgets'));
+        }
+      });
+    });
+
+    testWidgets('calls update on comparator if autoUpdateGoldenFiles is true', (WidgetTester tester) async {
+      autoUpdateGoldenFiles = true;
+      await tester.pumpWidget(boilerplate(const Text('hello')));
+      final Finder finder = find.byType(Text);
+      await expectLater(finder, matchesGoldenFile('foo.png'));
+      expect(comparator.invocation, _ComparatorInvocation.update);
+      expect(comparator.imageBytes, hasLength(greaterThan(0)));
+      expect(comparator.golden, Uri.parse('foo.png'));
+    });
+  });
+}
+
+enum _ComparatorBehavior {
+  returnTrue,
+  returnFalse,
+  throwTestFailure,
+}
+
+enum _ComparatorInvocation {
+  compare,
+  update,
+}
+
+class _FakeComparator implements GoldenFileComparator {
+  _ComparatorBehavior behavior = _ComparatorBehavior.returnTrue;
+  _ComparatorInvocation invocation;
+  Uint8List imageBytes;
+  Uri golden;
+
+  @override
+  Future<bool> compare(Uint8List imageBytes, Uri golden) {
+    invocation = _ComparatorInvocation.compare;
+    this.imageBytes = imageBytes;
+    this.golden = golden;
+    switch (behavior) {
+      case _ComparatorBehavior.returnTrue:
+        return new Future<bool>.value(true);
+      case _ComparatorBehavior.returnFalse:
+        return new Future<bool>.value(false);
+      case _ComparatorBehavior.throwTestFailure:
+        throw new TestFailure('fake message');
+    }
+    return new Future<bool>.value(false);
+  }
+
+  @override
+  Future<void> update(Uri golden, Uint8List imageBytes) {
+    invocation = _ComparatorInvocation.update;
+    this.golden = golden;
+    this.imageBytes = imageBytes;
+    return new Future<void>.value();
+  }
 }
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index bf78e74..88ea238 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -78,6 +78,11 @@
         hide: !verboseHelp,
         help: 'Track widget creation locations.\n'
               'This enables testing of features such as the widget inspector.',
+      )
+      ..addFlag('update-goldens',
+        negatable: false,
+        help: 'Whether matchesGoldenFile() calls within your test methods should\n'
+              'update the golden files rather than test for an existing match.',
       );
   }
 
@@ -220,6 +225,7 @@
       machine: machine,
       previewDart2: argResults['preview-dart-2'],
       trackWidgetCreation: argResults['track-widget-creation'],
+      updateGoldens: argResults['update-goldens'],
     );
 
     if (collector != null) {
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 32753f2..1427463 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -63,6 +63,7 @@
   int port: 0,
   String precompiledDillPath,
   bool trackWidgetCreation: false,
+  bool updateGoldens: false,
   int observatoryPort,
   InternetAddressType serverType: InternetAddressType.IP_V4,
 }) {
@@ -81,6 +82,7 @@
       port: port,
       precompiledDillPath: precompiledDillPath,
       trackWidgetCreation: trackWidgetCreation,
+      updateGoldens: updateGoldens,
     ),
   );
 }
@@ -211,6 +213,7 @@
     this.port,
     this.precompiledDillPath,
     this.trackWidgetCreation,
+    this.updateGoldens,
   }) : assert(shellPath != null);
 
   final String shellPath;
@@ -224,6 +227,7 @@
   final int port;
   final String precompiledDillPath;
   final bool trackWidgetCreation;
+  final bool updateGoldens;
 
   _Compiler compiler;
 
@@ -568,7 +572,7 @@
     final File listenerFile = fs.file('${temporaryDirectory.path}/listener.dart');
     listenerFile.createSync();
     listenerFile.writeAsStringSync(_generateTestMain(
-      testUrl: fs.path.toUri(fs.path.absolute(testPath)).toString(),
+      testUrl: fs.path.toUri(fs.path.absolute(testPath)),
       encodedWebsocketUrl: Uri.encodeComponent(_getWebSocketUrl()),
     ));
     return listenerFile.path;
@@ -616,7 +620,7 @@
   }
 
   String _generateTestMain({
-    String testUrl,
+    Uri testUrl,
     String encodedWebsocketUrl,
   }) {
     return '''
@@ -628,6 +632,7 @@
 // to add a dependency on package:test.
 import 'package:test/src/runner/plugin/remote_platform_helpers.dart';
 
+import 'package:flutter_test/flutter_test.dart';
 import 'package:stream_channel/stream_channel.dart';
 import 'package:test/src/runner/vm/catch_isolate_errors.dart';
 
@@ -639,6 +644,8 @@
   String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
   StreamChannel channel = serializeSuite(() {
     catchIsolateErrors();
+    goldenFileComparator = new LocalFileComparator(Uri.parse('$testUrl'));
+    autoUpdateGoldenFiles = $updateGoldens;
     return test.main;
   });
   WebSocket.connect(server).then((WebSocket socket) {
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index 0848a6e..ea6f9c8 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -31,6 +31,7 @@
     bool machine: false,
     bool previewDart2: false,
     bool trackWidgetCreation: false,
+    bool updateGoldens: false,
     TestWatcher watcher,
     }) async {
   if (trackWidgetCreation && !previewDart2) {
@@ -87,6 +88,7 @@
     serverType: serverType,
     previewDart2: previewDart2,
     trackWidgetCreation: trackWidgetCreation,
+    updateGoldens: updateGoldens,
   );
 
   // Make the global packages path absolute.