Unverified Commit 1eb5bb65 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Revert "Use skia golden files in driver test (#49750)" (#49900)

parent a50743f6
......@@ -10,5 +10,5 @@ import 'package:flutter_devicelab/tasks/integration_tests.dart';
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.fuchsia;
await task(createFlutterDriverScreenshotTest(useFlutterGold: true));
await task(createFlutterDriverScreenshotTest());
}
......@@ -107,22 +107,15 @@ TaskFunction createAndroidSplashScreenKitchenSinkTest() {
);
}
/// Executes a driver test that takes a screenshot and compares it against a golden image.
/// If [useFlutterGold] is true, the golden image is served by Flutter Gold
/// (https://flutter-gold.skia.org/), otherwise the golden image is read from the disk.
TaskFunction createFlutterDriverScreenshotTest({
bool useFlutterGold = false,
}) {
TaskFunction createFlutterDriverScreenshotTest() {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/flutter_driver_screenshot_test',
'lib/main.dart',
extraOptions: useFlutterGold ? const <String>[
'--driver', 'test_driver/flutter_gold_main_test.dart'
] : null
);
}
class DriverTest {
DriverTest(
this.testDirectory,
this.testTarget, {
......
......@@ -4,12 +4,7 @@ This tests contains an app with a main page and sub pages.
The main page contains a list of buttons; each button leads to a designated sub page when tapped on.
Each sub page should displays some simple UIs to screenshot tested.
The flutter driver test runs the app and opens each page to take a screenshot.
Use `test_driver/flutter_gold_main_test.dart` to test against golden files stored on Flutter Gold.
Otherwise, use `main_test.dart` to test against golden files stored on `test_driver/goldens/<some_test_page_name>/<device_model>.png`.
Note that new binaries can't be checked in the Flutter repo, so use [Flutter Gold](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter) instead.
The flutter driver test runs the app and opens each page to take a screenshot. Then it compares the screenshot against a golden image stored in `test_driver/goldens/<some_test_page_name>/<device_model>.png`.
# Add a new page to test
......@@ -21,7 +16,6 @@ Note that new binaries can't be checked in the Flutter repo, so use [Flutter Gol
An example of a `Page` subclass can be found in `lib/image_page.dart`
# Environments
# Experiments
* Device Lab which runs the app on iPhone 6s.
* LUCI which runs the app on a Fuchsia NUC device.
The test currently only runs on device lab ["mac/ios"] which runs the app on iPhone 6s.
\ No newline at end of file
// Copyright 2014 The Flutter 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 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
import 'package:flutter_test/src/buffer_matcher.dart';
Future<void> main() async {
FlutterDriver driver;
String deviceModel;
setUpAll(() async {
driver = await FlutterDriver.connect();
deviceModel = await driver.requestData('device_model');
});
tearDownAll(() => driver.close());
test('A page with an image screenshot', () async {
final SerializableFinder imagePageListTile =
find.byValueKey('image_page');
await driver.waitFor(imagePageListTile);
await driver.tap(imagePageListTile);
await driver.waitFor(find.byValueKey('red_square_image'));
await driver.waitUntilNoTransientCallbacks();
// TODO(egarciad): This is currently a no-op on LUCI.
// https://github.com/flutter/flutter/issues/49837
await expectLater(
driver.screenshot(),
bufferMatchesGoldenFile('red_square_driver_screenshot__$deviceModel.png'),
);
await driver.tap(find.byTooltip('Back'));
});
}
......@@ -4,8 +4,11 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/widgets.dart' show Element;
import 'package:image/image.dart';
import 'package:path/path.dart' as path;
// ignore: deprecated_member_use
......@@ -163,3 +166,92 @@ class LocalComparisonOutput {
));
}
}
/// Returns a [ComparisonResult] to describe the pixel differential of the
/// [test] and [master] image bytes provided.
ComparisonResult compareLists(List<int> test, List<int> master) {
if (identical(test, master))
return ComparisonResult(passed: true);
if (test == null || master == null || test.isEmpty || master.isEmpty) {
return ComparisonResult(
passed: false,
error: 'Pixel test failed, null image provided.',
);
}
final Image testImage = decodePng(test);
final Image masterImage = decodePng(master);
assert(testImage != null);
assert(masterImage != null);
final int width = testImage.width;
final int height = testImage.height;
if (width != masterImage.width || height != masterImage.height) {
return ComparisonResult(
passed: false,
error: 'Pixel test failed, image sizes do not match.\n'
'Master Image: ${masterImage.width} X ${masterImage.height}\n'
'Test Image: ${testImage.width} X ${testImage.height}',
);
}
int pixelDiffCount = 0;
final int totalPixels = width * height;
final Image invertedMaster = invert(Image.from(masterImage));
final Image invertedTest = invert(Image.from(testImage));
final Map<String, Image> diffs = <String, Image>{
'masterImage' : masterImage,
'testImage' : testImage,
'maskedDiff' : Image.from(testImage),
'isolatedDiff' : Image(width, height),
};
for (int x = 0; x < width; x++) {
for (int y =0; y < height; y++) {
final int testPixel = testImage.getPixel(x, y);
final int masterPixel = masterImage.getPixel(x, y);
final int diffPixel = (getRed(testPixel) - getRed(masterPixel)).abs()
+ (getGreen(testPixel) - getGreen(masterPixel)).abs()
+ (getBlue(testPixel) - getBlue(masterPixel)).abs()
+ (getAlpha(testPixel) - getAlpha(masterPixel)).abs();
if (diffPixel != 0 ) {
final int invertedMasterPixel = invertedMaster.getPixel(x, y);
final int invertedTestPixel = invertedTest.getPixel(x, y);
final int maskPixel = math.max(invertedMasterPixel, invertedTestPixel);
diffs['maskedDiff'].setPixel(x, y, maskPixel);
diffs['isolatedDiff'].setPixel(x, y, maskPixel);
pixelDiffCount++;
}
}
}
if (pixelDiffCount > 0) {
return ComparisonResult(
passed: false,
error: 'Pixel test failed, '
'${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% '
'diff detected.',
diffs: diffs,
);
}
return ComparisonResult(passed: true);
}
/// An unsupported [WebGoldenComparator] that exists for API compatibility.
class DefaultWebGoldenComparator extends WebGoldenComparator {
@override
Future<bool> compare(Element element, Size size, Uri golden) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
}
@override
Future<void> update(Uri golden, Element element, Size size) {
throw UnsupportedError('DefaultWebGoldenComparator is only supported on the web.');
}
}
......@@ -9,7 +9,6 @@ import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart' as path;
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart' as test_package show TestFailure;
......@@ -36,115 +35,6 @@ ComparisonResult compareLists(List<int> test, List<int> master) {
throw UnsupportedError('Golden testing is not supported on the web.');
}
/// Compares image pixels against a golden image file.
///
/// Instances of this comparator will be used as the backend for
/// [matchesGoldenFile] when tests are running on Flutter Web, and will usually
/// implemented by deferring the screenshot taking and image comparison to a
/// test server.
///
/// 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). Prior to the invocation, the test framework will render only the
/// [Element] to be compared on the screen.
///
/// See also:
///
/// * [GoldenFileComparator] for the comparator to be used when the test is
/// not running in a web browser.
/// * [DefaultWebGoldenComparator] for the default [WebGoldenComparator]
/// implementation for `flutter test`.
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
/// comparator.
abstract class WebGoldenComparator {
/// Compares the rendered pixels of [element] of size [size] that is being
/// rendered on the top left of the screen against the golden file identified
/// by [golden].
///
/// The returned future completes with a boolean value that indicates whether
/// the pixels rendered on screen match the golden file's pixels.
///
/// In the case of comparison mismatch, the comparator may choose to throw a
/// [TestFailure] if it wants to control the failure message, often in the
/// form of a [ComparisonResult] that provides detailed information about the
/// mismatch.
///
/// 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(Element element, Size size, Uri golden);
/// Updates the golden file identified by [golden] with rendered pixels of
/// [element].
///
/// 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 --platform=chrome`).
///
/// 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, Element element, Size size);
/// Returns a new golden file [Uri] to incorporate any [version] number with
/// the [key].
///
/// The [version] is an optional int that can be used to differentiate
/// historical golden files.
///
/// Version numbers are used in golden file tests for package:flutter. You can
/// learn more about these tests [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).
Uri getTestUri(Uri key, int version) {
if (version == null)
return key;
final String keyString = key.toString();
final String extension = path.extension(keyString);
return Uri.parse(
keyString
.split(extension)
.join() + '.' + version.toString() + extension
);
}
}
/// Compares pixels against those of a golden image file.
///
/// This comparator is used as the backend for [matchesGoldenFile] when tests
/// are running in a web browser.
///
/// When using `flutter test --platform=chrome`, a comparator implemented by
/// [DefaultWebGoldenComparator] is used if no other comparator is specified. It
/// will send a request to the test server, which uses [goldenFileComparator]
/// for golden file compatison.
///
/// When using `flutter test --update-goldens`, the [DefaultWebGoldenComparator]
/// updates the files on disk to match the rendering.
///
/// When using `flutter run`, the default comparator
/// ([_TrivialWebGoldenComparator]) is used. It prints a message to the console
/// but otherwise does nothing. This allows tests to be developed visually on a
/// web browser.
///
/// 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 comparator
/// with tolerance levels for allowable differences.
///
/// See also:
///
/// * [flutter_test] for more information about how to configure tests at the
/// directory-level.
/// * [goldenFileComparator], the comparator used when tests are not running on
/// a web browser.
WebGoldenComparator get webGoldenComparator => _webGoldenComparator;
WebGoldenComparator _webGoldenComparator = const _TrivialWebGoldenComparator._();
set webGoldenComparator(WebGoldenComparator value) {
assert(value != null);
_webGoldenComparator = value;
}
/// The default [WebGoldenComparator] implementation for `flutter test`.
///
/// This comparator will send a request to the test server for golden comparison
......@@ -197,23 +87,3 @@ class DefaultWebGoldenComparator extends WebGoldenComparator {
await compare(element, size, golden);
}
}
class _TrivialWebGoldenComparator implements WebGoldenComparator {
const _TrivialWebGoldenComparator._();
@override
Future<bool> compare(Element element, Size size, Uri golden) {
print('Golden comparison requested for "$golden"; skipping...');
return Future<bool>.value(true);
}
@override
Future<void> update(Uri golden, Element element, Size size) {
throw StateError('webGoldenComparator has not been initialized');
}
@override
Uri getTestUri(Uri key, int version) {
return key;
}
}
......@@ -11,7 +11,6 @@ import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementa
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
import '_goldens_web.dart';
import 'binding.dart';
import 'finders.dart';
import 'goldens.dart';
......
// Copyright 2014 The Flutter 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:typed_data';
import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementation_imports
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart' show Description, TestFailure;
import 'goldens.dart';
/// Matcher created by [bufferMatchesGoldenFile].
class _BufferGoldenMatcher extends AsyncMatcher {
/// Creates an instance of [BufferGoldenMatcher]. Called by [bufferMatchesGoldenFile].
const _BufferGoldenMatcher(this.key, this.version);
/// The [key] to the golden image.
final Uri key;
/// The [version] of the golden image.
final int version;
@override
Future<String> matchAsync(dynamic item) async {
Uint8List buffer;
if (item is List<int>) {
buffer = Uint8List.fromList(item);
} else if (item is Future<List<int>>) {
buffer = Uint8List.fromList(await item);
} else {
throw 'Expected `List<int>` or `Future<List<int>>`, instead found: ${item.runtimeType}';
}
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
if (autoUpdateGoldenFiles) {
await goldenFileComparator.update(testNameUri, buffer);
return null;
}
try {
final bool success = await goldenFileComparator.compare(buffer, testNameUri);
return success ? null : 'does not match';
} on TestFailure catch (ex) {
return ex.message;
}
}
@override
Description describe(Description description) {
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
return description.add('Byte buffer matches golden image "$testNameUri"');
}
}
/// Asserts that a [Future<List<int>>], or [List<int] matches the
/// golden image file identified by [key], with an optional [version] number.
///
/// The [key] is the [String] representation of a URL.
///
/// The [version] is a number that can be used to differentiate historical
/// golden files. This parameter is optional.
///
/// {@tool snippet}
/// Sample invocations of [matchesGoldenFile].
///
/// ```dart
/// await expectLater(
/// const <int>[],
/// bufferMatchesGoldenFile('sample.png'),
/// );
/// ```
/// {@end-tool}
AsyncMatcher bufferMatchesGoldenFile(String key, {int version}) {
return _BufferGoldenMatcher(Uri.parse(key), version);
}
......@@ -3,12 +3,12 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart' as path;
import 'package:image/image.dart';
import '_goldens_io.dart' if (dart.library.html) '_goldens_web.dart' as _goldens;
/// Compares image pixels against a golden image file.
///
......@@ -98,77 +98,7 @@ abstract class GoldenFileComparator {
/// Returns a [ComparisonResult] to describe the pixel differential of the
/// [test] and [master] image bytes provided.
static ComparisonResult compareLists(List<int> test, List<int> master) {
if (identical(test, master))
return ComparisonResult(passed: true);
if (test == null || master == null || test.isEmpty || master.isEmpty) {
return ComparisonResult(
passed: false,
error: 'Pixel test failed, null image provided.',
);
}
final Image testImage = decodePng(test);
final Image masterImage = decodePng(master);
assert(testImage != null);
assert(masterImage != null);
final int width = testImage.width;
final int height = testImage.height;
if (width != masterImage.width || height != masterImage.height) {
return ComparisonResult(
passed: false,
error: 'Pixel test failed, image sizes do not match.\n'
'Master Image: ${masterImage.width} X ${masterImage.height}\n'
'Test Image: ${testImage.width} X ${testImage.height}',
);
}
int pixelDiffCount = 0;
final int totalPixels = width * height;
final Image invertedMaster = invert(Image.from(masterImage));
final Image invertedTest = invert(Image.from(testImage));
final Map<String, Image> diffs = <String, Image>{
'masterImage' : masterImage,
'testImage' : testImage,
'maskedDiff' : Image.from(testImage),
'isolatedDiff' : Image(width, height),
};
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
final int testPixel = testImage.getPixel(x, y);
final int masterPixel = masterImage.getPixel(x, y);
final int diffPixel = (getRed(testPixel) - getRed(masterPixel)).abs()
+ (getGreen(testPixel) - getGreen(masterPixel)).abs()
+ (getBlue(testPixel) - getBlue(masterPixel)).abs()
+ (getAlpha(testPixel) - getAlpha(masterPixel)).abs();
if (diffPixel != 0 ) {
final int invertedMasterPixel = invertedMaster.getPixel(x, y);
final int invertedTestPixel = invertedTest.getPixel(x, y);
final int maskPixel = math.max(invertedMasterPixel, invertedTestPixel);
diffs['maskedDiff'].setPixel(x, y, maskPixel);
diffs['isolatedDiff'].setPixel(x, y, maskPixel);
pixelDiffCount++;
}
}
}
if (pixelDiffCount > 0) {
return ComparisonResult(
passed: false,
error: 'Pixel test failed, '
'${((pixelDiffCount/totalPixels) * 100).toStringAsFixed(2)}% '
'diff detected.',
diffs: diffs,
);
}
return ComparisonResult(passed: true);
return _goldens.compareLists(test, master);
}
}
......@@ -205,6 +135,115 @@ set goldenFileComparator(GoldenFileComparator value) {
_goldenFileComparator = value;
}
/// Compares image pixels against a golden image file.
///
/// Instances of this comparator will be used as the backend for
/// [matchesGoldenFile] when tests are running on Flutter Web, and will usually
/// implemented by deferring the screenshot taking and image comparison to a
/// test server.
///
/// 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). Prior to the invocation, the test framework will render only the
/// [Element] to be compared on the screen.
///
/// See also:
///
/// * [GoldenFileComparator] for the comparator to be used when the test is
/// not running in a web browser.
/// * [DefaultWebGoldenComparator] for the default [WebGoldenComparator]
/// implementation for `flutter test`.
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
/// comparator.
abstract class WebGoldenComparator {
/// Compares the rendered pixels of [element] of size [size] that is being
/// rendered on the top left of the screen against the golden file identified
/// by [golden].
///
/// The returned future completes with a boolean value that indicates whether
/// the pixels rendered on screen match the golden file's pixels.
///
/// In the case of comparison mismatch, the comparator may choose to throw a
/// [TestFailure] if it wants to control the failure message, often in the
/// form of a [ComparisonResult] that provides detailed information about the
/// mismatch.
///
/// 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(Element element, Size size, Uri golden);
/// Updates the golden file identified by [golden] with rendered pixels of
/// [element].
///
/// 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 --platform=chrome`).
///
/// 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, Element element, Size size);
/// Returns a new golden file [Uri] to incorporate any [version] number with
/// the [key].
///
/// The [version] is an optional int that can be used to differentiate
/// historical golden files.
///
/// Version numbers are used in golden file tests for package:flutter. You can
/// learn more about these tests [here](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).
Uri getTestUri(Uri key, int version) {
if (version == null)
return key;
final String keyString = key.toString();
final String extension = path.extension(keyString);
return Uri.parse(
keyString
.split(extension)
.join() + '.' + version.toString() + extension
);
}
}
/// Compares pixels against those of a golden image file.
///
/// This comparator is used as the backend for [matchesGoldenFile] when tests
/// are running in a web browser.
///
/// When using `flutter test --platform=chrome`, a comparator implemented by
/// [DefaultWebGoldenComparator] is used if no other comparator is specified. It
/// will send a request to the test server, which uses [goldenFileComparator]
/// for golden file compatison.
///
/// When using `flutter test --update-goldens`, the [DefaultWebGoldenComparator]
/// updates the files on disk to match the rendering.
///
/// When using `flutter run`, the default comparator
/// ([_TrivialWebGoldenComparator]) is used. It prints a message to the console
/// but otherwise does nothing. This allows tests to be developed visually on a
/// web browser.
///
/// 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 comparator
/// with tolerance levels for allowable differences.
///
/// See also:
///
/// * [flutter_test] for more information about how to configure tests at the
/// directory-level.
/// * [goldenFileComparator], the comparator used when tests are not running on
/// a web browser.
WebGoldenComparator get webGoldenComparator => _webGoldenComparator;
WebGoldenComparator _webGoldenComparator = const _TrivialWebGoldenComparator._();
set webGoldenComparator(WebGoldenComparator value) {
assert(value != null);
_webGoldenComparator = value;
}
/// Whether golden files should be automatically updated during tests rather
/// than compared to the image bytes recorded by the tests.
///
......@@ -241,7 +280,7 @@ class TrivialComparator implements GoldenFileComparator {
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) {
print('Golden file comparison requested for "$golden"; skipping...');
debugPrint('Golden file comparison requested for "$golden"; skipping...');
return Future<bool>.value(true);
}
......@@ -256,6 +295,26 @@ class TrivialComparator implements GoldenFileComparator {
}
}
class _TrivialWebGoldenComparator implements WebGoldenComparator {
const _TrivialWebGoldenComparator._();
@override
Future<bool> compare(Element element, Size size, Uri golden) {
debugPrint('Golden comparison requested for "$golden"; skipping...');
return Future<bool>.value(true);
}
@override
Future<void> update(Uri golden, Element element, Size size) {
throw StateError('webGoldenComparator has not been initialized');
}
@override
Uri getTestUri(Uri key, int version) {
return key;
}
}
/// The result of a pixel comparison test.
///
/// The [ComparisonResult] will always indicate if a test has [passed]. The
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment