// 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 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 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 (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. GoldenFileComparator get goldenFileComparator => _goldenFileComparator; GoldenFileComparator _goldenFileComparator = const _UninitializedComparator(); set goldenFileComparator(GoldenFileComparator comparator) { _goldenFileComparator = comparator ?? 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 compare(Uint8List imageBytes, Uri golden) { throw new StateError('goldenFileComparator has not been initialized'); } @override Future 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, {path.Style pathStyle}) : assert(testFile.scheme == 'file'), basedir = _getBasedir(testFile, pathStyle), _path = _getPath(pathStyle); static path.Context _getPath(path.Style style) { return new path.Context(style: style ?? path.Style.platform); } static Uri _getBasedir(Uri testFile, path.Style pathStyle) { final path.Context context = _getPath(pathStyle); return context.toUri(context.dirname(context.fromUri(testFile)) + context.separator); } /// The directory in which the test was loaded. /// /// Golden file keys will be interpreted as file paths relative to this /// directory. final Uri basedir; /// 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; @override Future 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 goldenBytes = await goldenFile.readAsBytes(); return _areListsEqual(imageBytes, goldenBytes); } @override Future update(Uri golden, Uint8List imageBytes) async { final File goldenFile = _getFile(golden); await goldenFile.parent.create(recursive: true); await goldenFile.writeAsBytes(imageBytes, flush: true); } File _getFile(Uri golden) { return new File(_path.join(_path.fromUri(basedir), _path.fromUri(golden.path))); } static bool _areListsEqual(List list1, List 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; } }