Skip to content

Commit a390590

Browse files
author
Emmanuel Garcia
authored
Move Skia gold client to shared location (flutter#33672)
1 parent 1c84b5e commit a390590

File tree

8 files changed

+199
-110
lines changed

8 files changed

+199
-110
lines changed

lib/web_ui/dev/steps/run_tests_step.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:io' as io;
77
import 'package:path/path.dart' as pathlib;
88
// TODO(yjbanov): remove hacks when this is fixed:
99
// https://github.com/dart-lang/test/issues/1521
10+
import 'package:skia_gold_client/skia_gold_client.dart';
1011
import 'package:test_api/src/backend/group.dart' as hack;
1112
import 'package:test_api/src/backend/live_test.dart' as hack;
1213
import 'package:test_api/src/backend/runtime.dart' as hack;
@@ -15,7 +16,6 @@ import 'package:test_core/src/runner/configuration/reporters.dart' as hack;
1516
import 'package:test_core/src/runner/engine.dart' as hack;
1617
import 'package:test_core/src/runner/hack_register_platform.dart' as hack;
1718
import 'package:test_core/src/runner/reporter.dart' as hack;
18-
import 'package:web_test_utils/skia_client.dart';
1919

2020
import '../browser.dart';
2121
import '../common.dart';
@@ -86,7 +86,7 @@ class RunTestsStep implements PipelineStep {
8686
Future<SkiaGoldClient?> _createSkiaClient() async {
8787
final SkiaGoldClient skiaClient = SkiaGoldClient(
8888
environment.webUiSkiaGoldDirectory,
89-
browserName: browserName,
89+
dimensions: <String, String> {'Browser': browserName},
9090
);
9191

9292
if (await _checkSkiaClient(skiaClient)) {
@@ -104,7 +104,7 @@ class RunTestsStep implements PipelineStep {
104104
Future<bool> _checkSkiaClient(SkiaGoldClient skiaClient) async {
105105
// Now let's check whether Skia Gold is reachable or not.
106106
if (isLuci) {
107-
if (SkiaGoldClient.isAvailable) {
107+
if (isSkiaGoldClientAvailable) {
108108
try {
109109
await skiaClient.auth();
110110
return true;

lib/web_ui/dev/test_platform.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'package:shelf/shelf_io.dart' as shelf_io;
1919
import 'package:shelf_packages_handler/shelf_packages_handler.dart';
2020
import 'package:shelf_static/shelf_static.dart';
2121
import 'package:shelf_web_socket/shelf_web_socket.dart';
22+
import 'package:skia_gold_client/skia_gold_client.dart';
2223
import 'package:stream_channel/stream_channel.dart';
2324

2425
import 'package:test_api/src/backend/runtime.dart';
@@ -35,7 +36,6 @@ import 'package:test_core/src/util/stack_trace_mapper.dart';
3536
import 'package:web_socket_channel/web_socket_channel.dart';
3637
import 'package:web_test_utils/goldens.dart';
3738
import 'package:web_test_utils/image_compare.dart';
38-
import 'package:web_test_utils/skia_client.dart';
3939

4040
import 'browser.dart';
4141
import 'common.dart';

lib/web_ui/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ dev_dependencies:
2828
url: https://github.com/flutter/web_installers.git
2929
path: packages/simulators/
3030
ref: 9afed28b771da1c4e82a3382c4a2b31344c04522
31+
skia_gold_client:
32+
path: ../../testing/skia_gold_client

testing/skia_gold_client/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# skia_gold_client
2+
3+
This package allows to create a Skia gold client in the engine repo.
4+
5+
The client uses the `goldctl` tool on LUCI builders to upload screenshots,
6+
and verify if a new screenshot matches the baseline screenshot.
7+
8+
The web UI is available on https://flutter-engine-gold.skia.org/.
9+
10+
## Using the client
11+
12+
1. In `.ci.yaml`, ensure that the task has a dependency on `goldctl`:
13+
14+
```yaml
15+
dependencies: [{"dependency": "goldctl"}]
16+
```
17+
18+
2. Add dependency in `pubspec.yaml`:
19+
20+
```yaml
21+
dependencies:
22+
skia_gold_client:
23+
path: <relative-path>/testing/skia_gold_client
24+
```
25+
26+
3. Use the client:
27+
28+
```dart
29+
import 'package:skia_gold_client/skia_gold_client.dart';
30+
31+
Future<void> main() {
32+
final Directory tmpDirectory = Directory.current.createTempSync('skia_gold_wd');
33+
final SkiaGoldClient client = SkiaGoldClient(
34+
tmpDirectory,
35+
dimensions: <String, String> {'<attribute-name>': '<attribute-value>'},
36+
);
37+
38+
try {
39+
if (isSkiaGoldClientAvailable) {
40+
await client.auth();
41+
42+
await client.addImg(
43+
'<file-name>',
44+
File('gold-file.png'),
45+
screenshotSize: 1024,
46+
);
47+
}
48+
} catch (error) {
49+
// Failed to authenticate or compare pixels.
50+
stderr.write(error.toString());
51+
rethrow;
52+
} finally {
53+
tmpDirectory.deleteSync(recursive: true);
54+
}
55+
}
56+
```

web_sdk/web_test_utils/lib/skia_client.dart renamed to testing/skia_gold_client/lib/skia_gold_client.dart

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,37 @@ import 'package:crypto/crypto.dart';
99
import 'package:path/path.dart' as path;
1010
import 'package:process/process.dart';
1111

12-
import 'environment.dart';
13-
1412
const String _kGoldctlKey = 'GOLDCTL';
13+
const String _kPresubmitEnvName = 'GOLD_TRYJOB';
14+
const String _kLuciEnvName = 'LUCI_CONTEXT';
1515

1616
const String _skiaGoldHost = 'https://flutter-engine-gold.skia.org';
1717
const String _instance = 'flutter-engine';
1818

19-
/// The percentage of accepted pixels to be wrong.
20-
///
21-
/// This should be a double between 0.0 and 1.0. A value of 0.0 means we don't
22-
/// accept any pixel to be different. A value of 1.0 means we accept 100% of
23-
/// pixels to be different.
24-
const double kMaxDifferentPixelsRate = 0.1;
19+
/// Whether the Skia Gold client is available and can be used in this
20+
/// environment.
21+
bool get isSkiaGoldClientAvailable => Platform.environment.containsKey(_kGoldctlKey);
22+
23+
/// Returns true if the current environment is a LUCI builder.
24+
bool get isLuciEnv => Platform.environment.containsKey(_kLuciEnvName);
25+
26+
/// Whether the current task is run during a presubmit check.
27+
bool get _isPresubmit => isLuciEnv && isSkiaGoldClientAvailable && Platform.environment.containsKey(_kPresubmitEnvName);
28+
29+
/// Whether the current task is run during a postsubmit check.
30+
bool get _isPostsubmit => isLuciEnv && isSkiaGoldClientAvailable && !Platform.environment.containsKey(_kPresubmitEnvName);
2531

2632
/// A client for uploading image tests and making baseline requests to the
2733
/// Flutter Gold Dashboard.
2834
class SkiaGoldClient {
2935
/// Creates a [SkiaGoldClient] with the given [workDirectory].
3036
///
31-
/// The [browserName] parameter is the name of the browser that generated the
32-
/// screenshots.
33-
SkiaGoldClient(this.workDirectory, { required this.browserName });
37+
/// [dimensions] allows to add attributes about the environment
38+
/// used to generate the screenshots.
39+
SkiaGoldClient(this.workDirectory, { this.dimensions });
3440

35-
/// Whether the Skia Gold client is available and can be used in this
36-
/// environment.
37-
static bool get isAvailable => Platform.environment.containsKey(_kGoldctlKey);
38-
39-
/// The name of the browser running the tests.
40-
final String browserName;
41+
/// Allows to add attributes about the environment used to generate the screenshots.
42+
final Map<String, String>? dimensions;
4143

4244
/// A controller for launching sub-processes.
4345
final ProcessManager process = const LocalProcessManager();
@@ -73,7 +75,7 @@ class SkiaGoldClient {
7375
/// The path to the local [Directory] where the `goldctl` tool is hosted.
7476
String get _goldctl {
7577
assert(
76-
isAvailable,
78+
isSkiaGoldClientAvailable,
7779
'Trying to use `goldctl` in an environment where it is not available',
7880
);
7981
return Platform.environment[_kGoldctlKey]!;
@@ -165,6 +167,42 @@ class SkiaGoldClient {
165167
_isInitialized = true;
166168
}
167169

170+
/// Executes the `imgtest add` command in the `goldctl` tool.
171+
///
172+
/// The `imgtest` command collects and uploads test results to the Skia Gold
173+
/// backend, the `add` argument uploads the current image test.
174+
///
175+
/// Throws an exception for try jobs that failed to pass the pixel comparison.
176+
///
177+
/// The [testName] and [goldenFile] parameters reference the current
178+
/// comparison being evaluated.
179+
///
180+
/// [pixelColorDelta] defines maximum acceptable difference in RGB channels of each pixel,
181+
/// such that:
182+
///
183+
/// ```
184+
/// abs(r(image) - r(golden)) + abs(g(image) - g(golden)) + abs(b(image) - b(golden)) <= pixelDeltaThreshold
185+
/// ```
186+
///
187+
/// [differentPixelsRate] is the fraction of accepted pixels to be wrong in the range [0.0, 1.0].
188+
/// Defaults to 0.1. A value of 0.1 means that 10% of the pixels are allowed to change.
189+
Future<void> addImg(
190+
String testName,
191+
File goldenFile, {
192+
double differentPixelsRate = 0.1,
193+
int pixelColorDelta = 0,
194+
required int screenshotSize,
195+
}) async {
196+
assert(_isPresubmit || _isPostsubmit);
197+
198+
if (_isPresubmit) {
199+
await _tryjobAdd(testName, goldenFile, screenshotSize, pixelColorDelta, differentPixelsRate);
200+
}
201+
if (_isPostsubmit) {
202+
await _imgtestAdd(testName, goldenFile, screenshotSize, pixelColorDelta, differentPixelsRate);
203+
}
204+
}
205+
168206
/// Executes the `imgtest add` command in the `goldctl` tool.
169207
///
170208
/// The `imgtest` command collects and uploads test results to the Skia Gold
@@ -174,11 +212,12 @@ class SkiaGoldClient {
174212
///
175213
/// The [testName] and [goldenFile] parameters reference the current
176214
/// comparison being evaluated.
177-
Future<bool> imgtestAdd(
215+
Future<void> _imgtestAdd(
178216
String testName,
179217
File goldenFile,
180218
int screenshotSize,
181-
bool isCanvaskitTest,
219+
int pixelDeltaThreshold,
220+
double maxDifferentPixelsRate,
182221
) async {
183222
await _imgtestInit();
184223

@@ -188,7 +227,7 @@ class SkiaGoldClient {
188227
'--work-dir', _tempPath,
189228
'--test-name', cleanTestName(testName),
190229
'--png-file', goldenFile.path,
191-
..._getMatchingArguments(testName, screenshotSize, isCanvaskitTest),
230+
..._getMatchingArguments(testName, screenshotSize, pixelDeltaThreshold, maxDifferentPixelsRate),
192231
];
193232

194233
final ProcessResult result = await _runCommand(imgtestCommand);
@@ -200,8 +239,6 @@ class SkiaGoldClient {
200239
print('goldctl imgtest add stdout: ${result.stdout}');
201240
print('goldctl imgtest add stderr: ${result.stderr}');
202241
}
203-
204-
return true;
205242
}
206243

207244
/// Executes the `imgtest init` command in the `goldctl` tool for tryjobs.
@@ -268,11 +305,12 @@ class SkiaGoldClient {
268305
///
269306
/// The [testName] and [goldenFile] parameters reference the current
270307
/// comparison being evaluated.
271-
Future<void> tryjobAdd(
308+
Future<void> _tryjobAdd(
272309
String testName,
273310
File goldenFile,
274311
int screenshotSize,
275-
bool isCanvaskitTest,
312+
int pixelDeltaThreshold,
313+
double differentPixelsRate,
276314
) async {
277315
await _tryjobInit();
278316

@@ -282,7 +320,7 @@ class SkiaGoldClient {
282320
'--work-dir', _tempPath,
283321
'--test-name', cleanTestName(testName),
284322
'--png-file', goldenFile.path,
285-
..._getMatchingArguments(testName, screenshotSize, isCanvaskitTest),
323+
..._getMatchingArguments(testName, screenshotSize, pixelDeltaThreshold, differentPixelsRate),
286324
];
287325

288326
final ProcessResult result = await _runCommand(tryjobCommand);
@@ -306,7 +344,8 @@ class SkiaGoldClient {
306344
List<String> _getMatchingArguments(
307345
String testName,
308346
int screenshotSize,
309-
bool isCanvaskitTest,
347+
int pixelDeltaThreshold,
348+
double differentPixelsRate,
310349
) {
311350
// The algorithm to be used when matching images. The available options are:
312351
// - "fuzzy": Allows for customizing the thresholds of pixel differences.
@@ -318,22 +357,7 @@ class SkiaGoldClient {
318357
// baseline. It's okay for this to be a slightly high number like 10% of the
319358
// image size because those wrong pixels are constrained by
320359
// `pixelDeltaThreshold` below.
321-
final int maxDifferentPixels = (screenshotSize * kMaxDifferentPixelsRate).toInt();
322-
323-
// The maximum acceptable difference in RGB channels of each pixel.
324-
//
325-
// ```
326-
// abs(r(image) - r(golden)) + abs(g(image) - g(golden)) + abs(b(image) - b(golden)) <= pixelDeltaThreshold
327-
// ```
328-
final String pixelDeltaThreshold;
329-
if (isCanvaskitTest) {
330-
pixelDeltaThreshold = '21';
331-
} else if (browserName == 'ios-safari') {
332-
pixelDeltaThreshold = '15';
333-
} else {
334-
pixelDeltaThreshold = '3';
335-
}
336-
360+
final int maxDifferentPixels = (screenshotSize * differentPixelsRate).toInt();
337361
return <String>[
338362
'--add-test-optional-key', 'image_matching_algorithm:$algorithm',
339363
'--add-test-optional-key', 'fuzzy_max_different_pixels:$maxDifferentPixels',
@@ -396,19 +420,15 @@ class SkiaGoldClient {
396420

397421
/// Returns the current commit hash of the engine repository.
398422
Future<String> _getCurrentCommit() async {
399-
final Directory webUiRoot = environment.webUiRootDir;
400-
if (!webUiRoot.existsSync()) {
401-
throw Exception('Web Engine root could not be found: $webUiRoot\n');
402-
} else {
403-
final ProcessResult revParse = await process.run(
404-
<String>['git', 'rev-parse', 'HEAD'],
405-
workingDirectory: webUiRoot.path,
406-
);
407-
if (revParse.exitCode != 0) {
408-
throw Exception('Current commit of Web Engine can not be found.');
409-
}
410-
return (revParse.stdout as String).trim();
423+
final File currentScript = File.fromUri(Platform.script);
424+
final ProcessResult revParse = await process.run(
425+
<String>['git', 'rev-parse', 'HEAD'],
426+
workingDirectory: currentScript.path,
427+
);
428+
if (revParse.exitCode != 0) {
429+
throw Exception('Current commit of the engine can not be found from path ${currentScript.path}.');
411430
}
431+
return (revParse.stdout as String).trim();
412432
}
413433

414434
/// Returns a Map of key value pairs used to uniquely identify the
@@ -417,11 +437,14 @@ class SkiaGoldClient {
417437
/// Currently, the only key value pairs being tracked are the platform and
418438
/// browser the image was rendered on.
419439
Map<String, dynamic> _getKeys() {
420-
return <String, dynamic>{
421-
'Browser': browserName,
440+
final Map<String, dynamic> initialKeys = <String, dynamic>{
422441
'CI': 'luci',
423442
'Platform': Platform.operatingSystem,
424443
};
444+
if (dimensions != null) {
445+
initialKeys.addAll(dimensions!);
446+
}
447+
return initialKeys;
425448
}
426449

427450
/// Same as [_getKeys] but encodes it in a JSON string.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
name: skia_gold_client
6+
publish_to: none
7+
environment:
8+
sdk: '>=2.12.0 <3.0.0'
9+
10+
# Do not add any dependencies that require more than what is provided in
11+
# //third_party/dart/pkg, //third_party/dart/third_party/pkg, or
12+
# //third_party/pkg. In particular, package:test is not usable here.
13+
14+
# If you do add packages here, make sure you can run `pub get --offline`, and
15+
# check the .packages and .package_config to make sure all the paths are
16+
# relative to this directory into //third_party/dart, or //third_party/pkg
17+
dependencies:
18+
crypto: any
19+
path: any
20+
process: any
21+
22+
dependency_overrides:
23+
collection:
24+
path: ../../../third_party/dart/third_party/pkg/collection
25+
crypto:
26+
path: ../../../third_party/dart/third_party/pkg/crypto
27+
file:
28+
path: ../../../third_party/pkg/file/packages/file
29+
meta:
30+
path: ../../../third_party/dart/pkg/meta
31+
path:
32+
path: ../../../third_party/dart/third_party/pkg/path
33+
platform:
34+
path: ../../../third_party/pkg/platform
35+
process:
36+
path: ../../../third_party/pkg/process
37+
typed_data:
38+
path: ../../../third_party/dart/third_party/pkg/typed_data

0 commit comments

Comments
 (0)