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