goldens.dart 8.65 KB
Newer Older
1 2 3 4 5 6 7 8
// 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';

9
import 'package:flutter/foundation.dart';
10
import 'package:path/path.dart' as path;
11
import 'package:test_api/test_api.dart' as test_package show TestFailure;
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

/// 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].
///
54 55
/// When using `flutter test`, a comparator implemented by [LocalFileComparator]
/// is used if no other comparator is specified. It treats the golden key as
56 57 58 59
/// 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.
///
60 61 62
/// When using `flutter test --update-goldens`, the [LocalFileComparator]
/// updates the files on disk to match the rendering.
///
63 64 65
/// When using `flutter run`, the default comparator ([TrivialComparator])
/// is used. It prints a message to the console but otherwise does nothing. This
/// allows tests to be developed visually on a real device.
66
///
67
/// Callers may choose to override the default comparator by setting this to a
68 69 70 71 72 73 74 75 76 77
/// custom comparator during test set-up (or using directory-level test
/// configuration). 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.
///
/// See also:
///
///  * [flutter_test] for more information about how to configure tests at the
///    directory-level.
78 79 80 81 82
GoldenFileComparator get goldenFileComparator => _goldenFileComparator;
GoldenFileComparator _goldenFileComparator = const TrivialComparator._();
set goldenFileComparator(GoldenFileComparator value) {
  assert(value != null);
  _goldenFileComparator = value;
83
}
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

/// 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;

100 101 102
/// Placeholder comparator that is set as the value of [goldenFileComparator]
/// when the initialization that happens in the test bootstrap either has not
/// yet happened or has been bypassed.
103
///
104
/// The test bootstrap file that gets generated by the Flutter tool when the
105
/// user runs `flutter test` is expected to set [goldenFileComparator] to
106 107 108 109 110 111 112
/// a comparator that resolves golden file references relative to the test
/// directory. From there, the caller may choose to override the comparator by
/// setting it to another value during test initialization. The only case
/// where we expect it to remain uninitialized is when the user runs a test
/// via `flutter run`. In this case, the [compare] method will just print a
/// message that it would have otherwise run a real comparison, and it will
/// return trivial success.
113 114 115 116 117
///
/// This class can't be constructed. It represents the default value of
/// [goldenFileComparator].
class TrivialComparator implements GoldenFileComparator {
  const TrivialComparator._();
118 119 120

  @override
  Future<bool> compare(Uint8List imageBytes, Uri golden) {
121
    debugPrint('Golden file comparison requested for "$golden"; skipping...');
122
    return Future<bool>.value(true);
123 124 125 126
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) {
127
    throw StateError('goldenFileComparator has not been initialized');
128 129 130
  }
}

131
/// The default [GoldenFileComparator] implementation for `flutter test`.
132 133 134 135 136 137 138 139
///
/// 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.
140 141 142
///
/// When using `flutter test --update-goldens`, [LocalFileComparator]
/// updates the files on disk to match the rendering.
143 144 145 146 147 148
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.
  ///
149
  /// The [testFile] URL must represent a file.
150
  LocalFileComparator(Uri testFile, {path.Style pathStyle})
151 152
    : basedir = _getBasedir(testFile, pathStyle),
      _path = _getPath(pathStyle);
153

154
  static path.Context _getPath(path.Style style) {
155
    return path.Context(style: style ?? path.Style.platform);
156 157 158 159
  }

  static Uri _getBasedir(Uri testFile, path.Style pathStyle) {
    final path.Context context = _getPath(pathStyle);
160 161 162
    final String testFilePath = context.fromUri(testFile);
    final String testDirectoryPath = context.dirname(testFilePath);
    return context.toUri(testDirectoryPath + context.separator);
163
  }
164 165 166 167 168 169 170

  /// The directory in which the test was loaded.
  ///
  /// Golden file keys will be interpreted as file paths relative to this
  /// directory.
  final Uri basedir;

171 172 173 174 175
  /// Path context exists as an instance variable rather than just using the
  /// system path context in order to support testing, where we can spoof the
  /// platform to test behaviors with arbitrary path styles.
  final path.Context _path;

176 177 178 179
  @override
  Future<bool> compare(Uint8List imageBytes, Uri golden) async {
    final File goldenFile = _getFile(golden);
    if (!goldenFile.existsSync()) {
180
      throw test_package.TestFailure('Could not be compared against non-existent file: "$golden"');
181 182
    }
    final List<int> goldenBytes = await goldenFile.readAsBytes();
183
    return _areListsEqual<int>(imageBytes, goldenBytes);
184 185 186 187 188
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) async {
    final File goldenFile = _getFile(golden);
189
    await goldenFile.parent.create(recursive: true);
190 191 192 193
    await goldenFile.writeAsBytes(imageBytes, flush: true);
  }

  File _getFile(Uri golden) {
194
    return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path)));
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
  }

  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;
  }
}