Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 9d4c951

Browse files
authored
[Impeller] Skia gold for flutter_tester dart tests. (#47066)
This removes skips for the golden tests in `//testing/dart/canvas_test.dart` and instead passes them up to Skia gold. Adds a utility class for dealing with Skia gold from these tests, as well as the existing fuzzy identical image comparison for tests that just want to do in memory comparison of images generated from the same test. Removes the old golden files that were in tree. Part of flutter/flutter#53784
1 parent b11e318 commit 9d4c951

20 files changed

+244
-85
lines changed

.ci.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ targets:
293293
add_recipes_cq: "true"
294294
release_build: "true"
295295
config_name: linux_host_engine
296+
dependencies: >-
297+
[
298+
{"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"}
299+
]
296300
drone_dimensions:
297301
- os=Linux
298302

ci/builders/linux_host_engine.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@
185185
"device_type=none",
186186
"os=Linux"
187187
],
188+
"dependencies": [
189+
{
190+
"dependency": "goldctl",
191+
"version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
192+
}
193+
],
188194
"gclient_variables": {
189195
"download_android_deps": false
190196
},

impeller/golden_tests_harvester/bin/golden_tests_harvester.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ bool get isLuciEnv => Platform.environment.containsKey(_kLuciEnvName);
1616

1717
/// Fake SkiaGoldClient that is used if the harvester is run outside of Luci.
1818
class FakeSkiaGoldClient implements SkiaGoldClient {
19-
FakeSkiaGoldClient(this._workingDirectory, {this.dimensions});
19+
FakeSkiaGoldClient(this._workingDirectory, {this.dimensions, this.verbose = false});
2020

2121
final Directory _workingDirectory;
2222

2323
@override
2424
final Map<String, String>? dimensions;
2525

26+
@override
27+
final bool verbose;
28+
2629
@override
2730
Future<void> addImg(String testName, File goldenFile,
2831
{double differentPixelsRate = 0.01,

impeller/golden_tests_harvester/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ dependency_overrides:
3030
path: ../../../third_party/dart/third_party/pkg/file/packages/file
3131
meta:
3232
path: ../../../third_party/dart/pkg/meta
33+
engine_repo_tools:
34+
path: ../../tools/pkg/engine_repo_tools
3335
path:
3436
path: ../../../third_party/dart/third_party/pkg/path
3537
platform:

lib/web_ui/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ dev_dependencies:
5454
path: ../../web_sdk/web_engine_tester
5555
skia_gold_client:
5656
path: ../../testing/skia_gold_client
57+
58+
dependency_overrides:
59+
engine_repo_tools:
60+
path: ../../tools/pkg/engine_repo_tools

testing/dart/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ tests = [
4848
]
4949

5050
foreach(test, tests) {
51+
skia_gold_work_dir = rebase_path("$root_gen_dir/skia_gold_$test")
5152
flutter_frontend_server("compile_$test") {
5253
main_dart = test
5354
kernel_output = "$root_gen_dir/$test.dill"
55+
extra_args = [ "-DkSkiaGoldWorkDirectory=$skia_gold_work_dir" ]
5456
package_config = ".dart_tool/package_config.json"
5557
}
5658
}

testing/dart/canvas_test.dart

Lines changed: 14 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:litetest/litetest.dart';
1212
import 'package:path/path.dart' as path;
1313
import 'package:vector_math/vector_math_64.dart';
1414

15+
import 'goldens.dart';
1516
import 'impeller_enabled.dart';
1617

1718
typedef CanvasCallback = void Function(Canvas canvas);
@@ -123,59 +124,9 @@ void testNoCrashes() {
123124
});
124125
}
125126

126-
/// @returns true When the images are reasonably similar.
127-
/// @todo Make the search actually fuzzy to a certain degree.
128-
Future<bool> fuzzyCompareImages(Image golden, Image img) async {
129-
if (golden.width != img.width || golden.height != img.height) {
130-
return false;
131-
}
132-
int getPixel(ByteData data, int x, int y) => data.getUint32((x + y * golden.width) * 4);
133-
final ByteData goldenData = (await golden.toByteData())!;
134-
final ByteData imgData = (await img.toByteData())!;
135-
for (int y = 0; y < golden.height; y++) {
136-
for (int x = 0; x < golden.width; x++) {
137-
if (getPixel(goldenData, x, y) != getPixel(imgData, x, y)) {
138-
return false;
139-
}
140-
}
141-
}
142-
return true;
143-
}
144-
145-
Future<void> saveTestImage(Image image, String filename) async {
146-
final String imagesPath = path.join('flutter', 'testing', 'resources');
147-
final ByteData pngData = (await image.toByteData(format: ImageByteFormat.png))!;
148-
final String outPath = path.join(imagesPath, filename);
149-
File(outPath).writeAsBytesSync(pngData.buffer.asUint8List());
150-
print('wrote: $outPath');
151-
}
152-
153-
/// @returns true When the images are reasonably similar.
154-
Future<bool> fuzzyGoldenImageCompare(
155-
Image image, String goldenImageName) async {
156-
final String imagesPath = path.join('flutter', 'testing', 'resources');
157-
final File file = File(path.join(imagesPath, goldenImageName));
158-
159-
bool areEqual = false;
160-
161-
if (file.existsSync()) {
162-
final Uint8List goldenData = await file.readAsBytes();
163-
164-
final Codec codec = await instantiateImageCodec(goldenData);
165-
final FrameInfo frame = await codec.getNextFrame();
166-
expect(frame.image.height, equals(image.height));
167-
expect(frame.image.width, equals(image.width));
168-
169-
areEqual = await fuzzyCompareImages(frame.image, image);
170-
}
127+
void main() async {
128+
final ImageComparer comparer = await ImageComparer.create();
171129

172-
if (!areEqual) {
173-
saveTestImage(image, 'found_$goldenImageName');
174-
}
175-
return areEqual;
176-
}
177-
178-
void main() {
179130
testNoCrashes();
180131

181132
test('Simple .toImage', () async {
@@ -190,11 +141,8 @@ void main() {
190141
}, 100, 100);
191142
expect(image.width, equals(100));
192143
expect(image.height, equals(100));
193-
194-
final bool areEqual =
195-
await fuzzyGoldenImageCompare(image, 'canvas_test_toImage.png');
196-
expect(areEqual, true);
197-
}, skip: impellerEnabled);
144+
await comparer.addGoldenImage(image, 'canvas_test_toImage.png');
145+
});
198146

199147
Gradient makeGradient() {
200148
return Gradient.linear(
@@ -212,10 +160,8 @@ void main() {
212160
expect(image.width, equals(100));
213161
expect(image.height, equals(100));
214162

215-
final bool areEqual =
216-
await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png');
217-
expect(areEqual, true);
218-
}, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784
163+
await comparer.addGoldenImage(image, 'canvas_test_dithered_gradient.png');
164+
});
219165

220166
test('Null values allowed for drawAtlas methods', () async {
221167
final Image image = await createImage(100, 100);
@@ -302,12 +248,8 @@ void main() {
302248
});
303249
}, width, height);
304250

305-
final bool areEqual = await fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage);
251+
final bool areEqual = await comparer.fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage);
306252

307-
if (!areEqual) {
308-
saveTestImage(incrementalMatrixImage, 'incremental_3D_transform_test_image.png');
309-
saveTestImage(combinedMatrixImage, 'combined_3D_transform_test_image.png');
310-
}
311253
expect(areEqual, true);
312254
});
313255

@@ -348,10 +290,8 @@ void main() {
348290
expect(image.width, equals(200));
349291
expect(image.height, equals(250));
350292

351-
final bool areEqual =
352-
await fuzzyGoldenImageCompare(image, 'dotted_path_effect_mixed_with_stroked_geometry.png');
353-
expect(areEqual, true);
354-
}, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784
293+
await comparer.addGoldenImage(image, 'dotted_path_effect_mixed_with_stroked_geometry.png');
294+
});
355295

356296
test('Gradients with matrices in Paragraphs render correctly', () async {
357297
final Image image = await toImage((Canvas canvas) {
@@ -400,10 +340,8 @@ void main() {
400340
expect(image.width, equals(600));
401341
expect(image.height, equals(400));
402342

403-
final bool areEqual =
404-
await fuzzyGoldenImageCompare(image, 'text_with_gradient_with_matrix.png');
405-
expect(areEqual, true);
406-
}, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784
343+
await comparer.addGoldenImage(image, 'text_with_gradient_with_matrix.png');
344+
});
407345

408346
test('toImageSync - too big', () async {
409347
PictureRecorder recorder = PictureRecorder();
@@ -602,8 +540,8 @@ void main() {
602540
final Image tofuImage = await drawText('>\b<');
603541

604542
// The tab's image should be identical to the space's image but not the tofu's image.
605-
final bool tabToSpaceComparison = await fuzzyCompareImages(tabImage, spaceImage);
606-
final bool tabToTofuComparison = await fuzzyCompareImages(tabImage, tofuImage);
543+
final bool tabToSpaceComparison = await comparer.fuzzyCompareImages(tabImage, spaceImage);
544+
final bool tabToTofuComparison = await comparer.fuzzyCompareImages(tabImage, tofuImage);
607545

608546
expect(tabToSpaceComparison, isTrue);
609547
expect(tabToTofuComparison, isFalse);

testing/dart/goldens.dart

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2013 The Flutter 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+
5+
import 'dart:io';
6+
import 'dart:typed_data';
7+
import 'dart:ui';
8+
9+
import 'package:path/path.dart' as path;
10+
import 'package:skia_gold_client/skia_gold_client.dart';
11+
12+
import 'impeller_enabled.dart';
13+
14+
const String _kSkiaGoldWorkDirectoryKey = 'kSkiaGoldWorkDirectory';
15+
16+
/// A helper for doing image comparison (golden) tests.
17+
///
18+
/// Contains utilities for comparing two images in memory that are expected to
19+
/// be identical, or for adding images to Skia gold for comparison.
20+
class ImageComparer {
21+
ImageComparer._({
22+
required SkiaGoldClient client,
23+
}) : _client = client;
24+
25+
// Avoid talking to Skia gold for the force-multithreading variants.
26+
static bool get _useSkiaGold =>
27+
!Platform.executableArguments.contains('--force-multithreading');
28+
29+
/// Creates an image comparer and authorizes.
30+
static Future<ImageComparer> create({
31+
bool verbose = false,
32+
}) async {
33+
const String workDirectoryPath =
34+
String.fromEnvironment(_kSkiaGoldWorkDirectoryKey);
35+
if (workDirectoryPath.isEmpty) {
36+
throw UnsupportedError(
37+
'Using ImageComparer requries defining kSkiaGoldWorkDirectoryKey.');
38+
}
39+
40+
final Directory workDirectory = Directory(
41+
impellerEnabled ? '${workDirectoryPath}_iplr' : workDirectoryPath,
42+
)..createSync();
43+
final Map<String, String> dimensions = <String, String>{
44+
'impeller_enabled': impellerEnabled.toString(),
45+
};
46+
final SkiaGoldClient client = isSkiaGoldClientAvailable && _useSkiaGold
47+
? SkiaGoldClient(workDirectory,
48+
dimensions: dimensions, verbose: verbose)
49+
: _FakeSkiaGoldClient(workDirectory, dimensions, verbose: verbose);
50+
51+
await client.auth();
52+
return ImageComparer._(client: client);
53+
}
54+
55+
final SkiaGoldClient _client;
56+
57+
/// Adds an [Image] to Skia Gold for comparison.
58+
///
59+
/// The [fileName] must be unique.
60+
Future<void> addGoldenImage(Image image, String fileName) async {
61+
final ByteData data =
62+
(await image.toByteData(format: ImageByteFormat.png))!;
63+
64+
final File file = File(path.join(_client.workDirectory.path, fileName))
65+
..writeAsBytesSync(data.buffer.asUint8List());
66+
await _client.addImg(
67+
fileName,
68+
file,
69+
screenshotSize: image.width * image.height,
70+
).catchError((dynamic error) {
71+
print('Skia gold comparison failed: $error');
72+
throw Exception('Failed comparison: $fileName');
73+
});
74+
}
75+
76+
Future<bool> fuzzyCompareImages(Image golden, Image testImage) async {
77+
if (golden.width != testImage.width || golden.height != testImage.height) {
78+
return false;
79+
}
80+
int getPixel(ByteData data, int x, int y) =>
81+
data.getUint32((x + y * golden.width) * 4);
82+
final ByteData goldenData = (await golden.toByteData())!;
83+
final ByteData testImageData = (await testImage.toByteData())!;
84+
for (int y = 0; y < golden.height; y++) {
85+
for (int x = 0; x < golden.width; x++) {
86+
if (getPixel(goldenData, x, y) != getPixel(testImageData, x, y)) {
87+
return false;
88+
}
89+
}
90+
}
91+
return true;
92+
}
93+
}
94+
95+
// TODO(dnfield): add local comparison against baseline,
96+
// https://github.com/flutter/flutter/issues/136831
97+
class _FakeSkiaGoldClient implements SkiaGoldClient {
98+
_FakeSkiaGoldClient(
99+
this.workDirectory,
100+
this.dimensions, {
101+
this.verbose = false,
102+
});
103+
104+
@override
105+
final Directory workDirectory;
106+
107+
@override
108+
final Map<String, String> dimensions;
109+
110+
@override
111+
final bool verbose;
112+
113+
@override
114+
Future<void> auth() async {}
115+
116+
@override
117+
Future<void> addImg(
118+
String testName,
119+
File goldenFile, {
120+
double differentPixelsRate = 0.01,
121+
int pixelColorDelta = 0,
122+
required int screenshotSize,
123+
}) async {}
124+
125+
@override
126+
dynamic noSuchMethod(Invocation invocation) {
127+
throw UnimplementedError(invocation.memberName.toString().split('"')[1]);
128+
}
129+
}

testing/dart/pubspec.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ environment:
1919
dependencies:
2020
litetest: any
2121
path: any
22+
skia_gold_client: any
2223
sky_engine: any
2324
vector_math: any
2425
vm_service: any
@@ -29,8 +30,14 @@ dependency_overrides:
2930
path: ../../../third_party/dart/pkg/async_helper
3031
collection:
3132
path: ../../../third_party/dart/third_party/pkg/collection
33+
crypto:
34+
path: ../../../third_party/dart/third_party/pkg/crypto
35+
engine_repo_tools:
36+
path: ../../tools/pkg/engine_repo_tools
3237
expect:
3338
path: ../../../third_party/dart/pkg/expect
39+
file:
40+
path: ../../../third_party/dart/third_party/pkg/file/packages/file
3441
fixnum:
3542
path: ../../../third_party/dart/third_party/pkg/fixnum
3643
litetest:
@@ -39,12 +46,20 @@ dependency_overrides:
3946
path: ../../../third_party/dart/pkg/meta
4047
path:
4148
path: ../../../third_party/dart/third_party/pkg/path
49+
platform:
50+
path: ../../../third_party/pkg/platform
51+
process:
52+
path: ../../../third_party/pkg/process
4253
protobuf:
4354
path: ../../../third_party/dart/third_party/pkg/protobuf/protobuf
4455
smith:
4556
path: ../../../third_party/dart/pkg/smith
57+
skia_gold_client:
58+
path: ../skia_gold_client
4659
sky_engine:
4760
path: ../../sky/packages/sky_engine
61+
typed_data:
62+
path: ../../../third_party/dart/third_party/pkg/typed_data
4863
vector_math:
4964
path: ../../../third_party/pkg/vector_math
5065
vm_service:
-1.66 KB
Binary file not shown.

0 commit comments

Comments
 (0)