• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:io';
7import 'dart:typed_data';
8
9import 'package:flutter/foundation.dart';
10import 'package:path/path.dart' as path;
11import 'package:test_api/test_api.dart' as test_package show TestFailure;
12
13/// Compares rasterized image bytes against a golden image file.
14///
15/// Instances of this comparator will be used as the backend for
16/// [matchesGoldenFile].
17///
18/// Instances of this comparator will be invoked by the test framework in the
19/// [TestWidgetsFlutterBinding.runAsync] zone and are thus not subject to the
20/// fake async constraints that are normally imposed on widget tests (i.e. the
21/// need or the ability to call [WidgetTester.pump] to advance the microtask
22/// queue).
23abstract class GoldenFileComparator {
24  /// Compares [imageBytes] against the golden file identified by [golden].
25  ///
26  /// The returned future completes with a boolean value that indicates whether
27  /// [imageBytes] matches the golden file's bytes within the tolerance defined
28  /// by the comparator.
29  ///
30  /// In the case of comparison mismatch, the comparator may choose to throw a
31  /// [TestFailure] if it wants to control the failure message.
32  ///
33  /// The method by which [golden] is located and by which its bytes are loaded
34  /// is left up to the implementation class. For instance, some implementations
35  /// may load files from the local file system, whereas others may load files
36  /// over the network or from a remote repository.
37  Future<bool> compare(Uint8List imageBytes, Uri golden);
38
39  /// Updates the golden file identified by [golden] with [imageBytes].
40  ///
41  /// This will be invoked in lieu of [compare] when [autoUpdateGoldenFiles]
42  /// is `true` (which gets set automatically by the test framework when the
43  /// user runs `flutter test --update-goldens`).
44  ///
45  /// The method by which [golden] is located and by which its bytes are written
46  /// is left up to the implementation class.
47  Future<void> update(Uri golden, Uint8List imageBytes);
48
49  /// Returns a new golden file [Uri] to incorporate any [version] number with
50  /// the [key].
51  ///
52  /// The [version] is an optional int that can be used to differentiate
53  /// historical golden files.
54  ///
55  /// Version numbers are used in golden file tests for package:flutter. You can
56  /// learn more about these tests [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).
57  Uri getTestUri(Uri key, int version) {
58    if (version == null)
59      return key;
60    final String keyString = key.toString();
61    final String extension = path.extension(keyString);
62    return Uri.parse(
63      keyString
64        .split(extension)
65        .join() + '.' + version.toString() + extension
66    );
67  }
68}
69
70/// Compares rasterized image bytes against a golden image file.
71///
72/// This comparator is used as the backend for [matchesGoldenFile].
73///
74/// When using `flutter test`, a comparator implemented by [LocalFileComparator]
75/// is used if no other comparator is specified. It treats the golden key as
76/// a relative path from the test file's directory. It will then load the
77/// golden file's bytes from disk and perform a byte-for-byte comparison of the
78/// encoded PNGs, returning true only if there's an exact match.
79///
80/// When using `flutter test --update-goldens`, the [LocalFileComparator]
81/// updates the files on disk to match the rendering.
82///
83/// When using `flutter run`, the default comparator ([TrivialComparator])
84/// is used. It prints a message to the console but otherwise does nothing. This
85/// allows tests to be developed visually on a real device.
86///
87/// Callers may choose to override the default comparator by setting this to a
88/// custom comparator during test set-up (or using directory-level test
89/// configuration). For example, some projects may wish to install a more
90/// intelligent comparator that knows how to decode the PNG images to raw
91/// pixels and compare pixel vales, reporting specific differences between the
92/// images.
93///
94/// See also:
95///
96///  * [flutter_test] for more information about how to configure tests at the
97///    directory-level.
98GoldenFileComparator get goldenFileComparator => _goldenFileComparator;
99GoldenFileComparator _goldenFileComparator = const TrivialComparator._();
100set goldenFileComparator(GoldenFileComparator value) {
101  assert(value != null);
102  _goldenFileComparator = value;
103}
104
105/// Whether golden files should be automatically updated during tests rather
106/// than compared to the image bytes recorded by the tests.
107///
108/// When this is `true`, [matchesGoldenFile] will always report a successful
109/// match, because the bytes being tested implicitly become the new golden.
110///
111/// The Flutter tool will automatically set this to `true` when the user runs
112/// `flutter test --update-goldens`, so callers should generally never have to
113/// explicitly modify this value.
114///
115/// See also:
116///
117///   * [goldenFileComparator]
118bool autoUpdateGoldenFiles = false;
119
120/// Placeholder comparator that is set as the value of [goldenFileComparator]
121/// when the initialization that happens in the test bootstrap either has not
122/// yet happened or has been bypassed.
123///
124/// The test bootstrap file that gets generated by the Flutter tool when the
125/// user runs `flutter test` is expected to set [goldenFileComparator] to
126/// a comparator that resolves golden file references relative to the test
127/// directory. From there, the caller may choose to override the comparator by
128/// setting it to another value during test initialization. The only case
129/// where we expect it to remain uninitialized is when the user runs a test
130/// via `flutter run`. In this case, the [compare] method will just print a
131/// message that it would have otherwise run a real comparison, and it will
132/// return trivial success.
133///
134/// This class can't be constructed. It represents the default value of
135/// [goldenFileComparator].
136class TrivialComparator implements GoldenFileComparator {
137  const TrivialComparator._();
138
139  @override
140  Future<bool> compare(Uint8List imageBytes, Uri golden) {
141    debugPrint('Golden file comparison requested for "$golden"; skipping...');
142    return Future<bool>.value(true);
143  }
144
145  @override
146  Future<void> update(Uri golden, Uint8List imageBytes) {
147    throw StateError('goldenFileComparator has not been initialized');
148  }
149
150  @override
151  Uri getTestUri(Uri key, int version) {
152    return key;
153  }
154}
155
156/// The default [GoldenFileComparator] implementation for `flutter test`.
157///
158/// This comparator loads golden files from the local file system, treating the
159/// golden key as a relative path from the test file's directory.
160///
161/// This comparator performs a very simplistic comparison, doing a byte-for-byte
162/// comparison of the encoded PNGs, returning true only if there's an exact
163/// match. This means it will fail the test if two PNGs represent the same
164/// pixels but are encoded differently.
165///
166/// When using `flutter test --update-goldens`, [LocalFileComparator]
167/// updates the files on disk to match the rendering.
168class LocalFileComparator extends GoldenFileComparator {
169  /// Creates a new [LocalFileComparator] for the specified [testFile].
170  ///
171  /// Golden file keys will be interpreted as file paths relative to the
172  /// directory in which [testFile] resides.
173  ///
174  /// The [testFile] URL must represent a file.
175  LocalFileComparator(Uri testFile, {path.Style pathStyle})
176    : basedir = _getBasedir(testFile, pathStyle),
177      _path = _getPath(pathStyle);
178
179  static path.Context _getPath(path.Style style) {
180    return path.Context(style: style ?? path.Style.platform);
181  }
182
183  static Uri _getBasedir(Uri testFile, path.Style pathStyle) {
184    final path.Context context = _getPath(pathStyle);
185    final String testFilePath = context.fromUri(testFile);
186    final String testDirectoryPath = context.dirname(testFilePath);
187    return context.toUri(testDirectoryPath + context.separator);
188  }
189
190  /// The directory in which the test was loaded.
191  ///
192  /// Golden file keys will be interpreted as file paths relative to this
193  /// directory.
194  final Uri basedir;
195
196  /// Path context exists as an instance variable rather than just using the
197  /// system path context in order to support testing, where we can spoof the
198  /// platform to test behaviors with arbitrary path styles.
199  final path.Context _path;
200
201  @override
202  Future<bool> compare(Uint8List imageBytes, Uri golden) async {
203    final File goldenFile = _getFile(golden);
204    if (!goldenFile.existsSync()) {
205      throw test_package.TestFailure('Could not be compared against non-existent file: "$golden"');
206    }
207    final List<int> goldenBytes = await goldenFile.readAsBytes();
208    return _areListsEqual<int>(imageBytes, goldenBytes);
209  }
210
211  @override
212  Future<void> update(Uri golden, Uint8List imageBytes) async {
213    final File goldenFile = _getFile(golden);
214    await goldenFile.parent.create(recursive: true);
215    await goldenFile.writeAsBytes(imageBytes, flush: true);
216  }
217
218  File _getFile(Uri golden) {
219    return File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path)));
220  }
221
222  static bool _areListsEqual<T>(List<T> list1, List<T> list2) {
223    if (identical(list1, list2)) {
224      return true;
225    }
226    if (list1 == null || list2 == null) {
227      return false;
228    }
229    final int length = list1.length;
230    if (length != list2.length) {
231      return false;
232    }
233    for (int i = 0; i < length; i++) {
234      if (list1[i] != list2[i]) {
235        return false;
236      }
237    }
238    return true;
239  }
240}
241