Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .engine-release.version
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Used to branch and track golden-file rendering of release branches.
#
# See ./testing/skia_gold_client/README.md#release-testing for more information.

# On the main branch, this should always read 'none'.
# On release branches, this should be the release version such as '3.21'
# (without the quotes).
none
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -41992,6 +41992,7 @@ ORIGIN: ../../../flutter/vulkan/vulkan_utilities.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/vulkan/vulkan_window.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/vulkan/vulkan_window.h + ../../../flutter/LICENSE
TYPE: LicenseType.bsd
FILE: ../../../flutter/.engine-release.version
FILE: ../../../flutter/.pylintrc
FILE: ../../../flutter/assets/asset_manager.cc
FILE: ../../../flutter/assets/asset_manager.h
Expand Down
5 changes: 5 additions & 0 deletions testing/litetest/lib/src/matchers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ void throwsRangeError(dynamic d) {
Expect.throwsRangeError(d as void Function());
}

/// A [Matcher] that matches functions that throw a [RangeError] when invoked.
void throwsFormatException(dynamic d) {
Expect.throwsFormatException(d as void Function());
}

/// Gives a [Matcher] that asserts that the value being matched is a [String]
/// that contains `s` as a substring.
Matcher contains(String s) => (dynamic d) {
Expand Down
151 changes: 96 additions & 55 deletions testing/skia_gold_client/README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,111 @@
# skia_gold_client
# Skia Gold Client

This package allows to create a Skia gold client in the engine repo.
This package interacts with [Skia Gold][] for uploading and comparing
screenshots.

The client uses the `goldctl` tool on LUCI builders to upload screenshots,
and verify if a new screenshot matches the baseline screenshot.
[skia gold]: https://skia.org/docs/dev/testing/skiagold/

The web UI is available on https://flutter-engine-gold.skia.org/.
The web UI for the engine is located at <https://flutter-engine-gold.skia.org/>.

## Using the client
## Usage

1. In `.ci.yaml`, ensure that the task has a dependency on `goldctl`:
In the simplest case, import the package and establish a working directory:

```yaml
dependencies: [{ "dependency": "goldctl" }]
```
```dart
import 'dart:io' as io;
2. In the builder `.json` file, ensure the drone has a dependency on `goldctl`:
import 'package:skia_gold_client/skia_gold_client.dart';
```yaml
"dependencies": [
{
"dependency": "goldctl",
"version": "git_revision:<sha>"
}
],
void main() async {
// Create a temporary working directory.
final io.Directory tmpDirectory = io.Directory.systemTemp.createTempSync('skia_gold_wd');
try {
final SkiaGoldClient client = SkiaGoldClient(tmpDirectory);
await client.auth();
// ...
} finally {
tmpDirectory.deleteSync(recursive: true);
}
}
```

3. Add dependency in `pubspec.yaml`:
Once you have an authorized instance, use `addImg` to upload a screenshot:

```yaml
dependencies:
# needed for skia_gold_client to avoid a cache miss.
engine_repo_tools:
path: <relative-path>/tools/pkg/engine_repo_tools
skia_gold_client:
path: <relative-path>/testing/skia_gold_client
```dart
await client.addImg(
'my-screenshot',
io.File('path/to/screenshot.png'),
screenshotSize: 400, // i.e. a 20x20 image
);
```

4. Use the client:
## Configuring CI

```dart
import 'package:skia_gold_client/skia_gold_client.dart';
Currently[^1], the client is only available on Flutter Engine's CI platform, and
will fail to authenticate if run elsewhere.

Future<void> main() {
final Directory tmpDirectory = Directory.current.createTempSync('skia_gold_wd');
final SkiaGoldClient client = SkiaGoldClient(
tmpDirectory,
dimensions: <String, String> {'<attribute-name>': '<attribute-value>'},
);
try {
if (SkiaGoldClient.isAvailable()) {
await client.auth();
await client.addImg(
'<file-name>',
File('gold-file.png'),
screenshotSize: 1024,
);
}
} catch (error) {
// Failed to authenticate or compare pixels.
stderr.write(error.toString());
rethrow;
} finally {
tmpDirectory.deleteSync(recursive: true);
}
}
```
To use the client in CI, you'll need to make two changes:

[^1]:
The `flutter/flutter` repository has a workaround which downloads digests
and does basic local image comparison, but because we have forked the
client and not kept it up-to-date, we cannot use that workaround. Send
a PR or file an issue if you'd like to see this fixed!

1. **Add a dependency on `goldctl`**

In your task's configuration in [`.ci.yaml`](../../.ci.yaml) file, add a
dependency on `goldctl`:

```diff
# This is just an example.
targets:
- name: Linux linux_android_emulator_tests
properties:
config_name: linux_android_emulator
+ dependencies: >-
+ [
+ {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"}
+ ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this render correctly? I would have expected the pluses and minuses at the beginning of the line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm good point, let me try again

```

2. **Ensure the builder (i.e. `config_name: {name}`) also has a dependency**

For example, for `linux_android_emulator`, modify
[`ci/builders/linux_android_emulator.json`](../../ci/builders/linux_android_emulator.json):

```json
"dependencies": [
{
"dependency": "goldctl",
"version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
}
]
```

## Release Testing

> [!NOTE]
> This workflow is a work in progress. Contact @matanlurey for more information.
When we create a release branch (i.e. for a beta or stable release), all
golden-file tests will have to be regenerated for the new release. This is
because it's possible that the rendering of the engine has changed in a way
that affects the golden files (either due to a bug, or intentionally) as we
apply cherry-picks and other changes to the release branch.

Fortunately this process is easy and mostly automated. Here's how it works:

1. Create your release branch, e.g. `flutter-3.21-candidate.1`.
1. Edit [`.engine-release.verison`](../../.engine-release.version) to the new
release version (e.g. `3.21`).
1. Run all the tests, generating new golden files.
1. Bulk triage all of the images as positive using the web UI.

![Screenshot](https://github.com/flutter/flutter/assets/168174/a327ffc0-95b3-4d3a-9d36-052e0607a1e5)

All of the tests will have a unique `_Release_{major}}_{minor}` suffix, so you
can easily filter them in the web UI and they can diverge from the `main` branch
as needed. As cherry-picks are applied to the release branch, the tests should
continue to pass, and the golden files should either _not_ change, or change in
a way that is expected (i.e. fixing a bug).
108 changes: 93 additions & 15 deletions testing/skia_gold_client/lib/skia_gold_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:path/path.dart' as path;
import 'package:process/process.dart';

import 'src/errors.dart';
import 'src/release_version.dart';

export 'src/errors.dart' show SkiaGoldProcessError;

Expand All @@ -22,25 +23,89 @@ const String _kLuciEnvName = 'LUCI_CONTEXT';
const String _skiaGoldHost = 'https://flutter-engine-gold.skia.org';
const String _instance = 'flutter-engine';

/// A client for uploading image tests and making baseline requests to the
/// Flutter Gold Dashboard.
/// Uploads images and makes baseline requests to Skia Gold.
///
/// For an example of how to use this class, see `tool/e2e_test.dart`.
interface class SkiaGoldClient {
/// Creates a [SkiaGoldClient] with the given [workDirectory].
///
/// [dimensions] allows to add attributes about the environment
/// used to generate the screenshots.
SkiaGoldClient(
/// A set of [dimensions] can be provided to add attributes about the
/// environment used to generate the screenshots, which are treated as keys
/// for the image:
///
/// ```dart
/// final SkiaGoldClient skiaGoldClient = SkiaGoldClient(
/// someDir,
/// dimensions: <String, String>{
/// 'platform': 'linux',
/// },
/// );
/// ```
///
/// The [verbose] flag is intended for use in debugging CI issues, and
/// produces more detailed output that some may find useful, but would be too
/// spammy for regular use.
factory SkiaGoldClient(
io.Directory workDirectory, {
Map<String, String>? dimensions,
bool verbose = false,
}) {
return SkiaGoldClient.forTesting(
workDirectory,
dimensions: dimensions,
verbose: verbose,
);
}

/// Creates a [SkiaGoldClient] for testing.
///
/// Similar to the default constructor, but allows for dependency injection
/// for testing purposes:
///
/// - [httpClient] makes requests to Skia Gold to fetch expectations.
/// - [processManager] launches sub-processes.
/// - [stderr] is where output is written for diagnostics.
/// - [environment] is the environment variables for the currently running
/// process, and is used to determine if Skia Gold is available, and whether
/// the current environment is CI, and if so, if it's pre-submit or
/// post-submit.
/// - [engineRoot] is the root of the engine repository, which is used for
/// finding the current commit hash, as well as the location of the
/// `.engine-release.version` file.
@visibleForTesting
SkiaGoldClient.forTesting(
this.workDirectory, {
this.dimensions,
this.verbose = false,
io.HttpClient? httpClient,
ProcessManager? processManager,
StringSink? stderr,
Map<String, String>? environment,
Engine? engineRoot,
}) : httpClient = httpClient ?? io.HttpClient(),
process = processManager ?? const LocalProcessManager(),
_stderr = stderr ?? io.stderr,
_environment = environment ?? io.Platform.environment;
_environment = environment ?? io.Platform.environment,
_engineRoot = engineRoot ?? Engine.findWithin() {
// Lookup the release version from the engine repository.
final io.File releaseVersionFile = io.File(path.join(
_engineRoot.flutterDir.path,
'.engine-release.version',
));

// If the file is not found or cannot be read, we are in an invalid state.
try {
_releaseVersion = ReleaseVersion.parse(releaseVersionFile.readAsStringSync());
} on FormatException catch (error) {
throw StateError('Failed to parse release version file: $error.');
} on io.FileSystemException catch (error) {
throw StateError('Failed to read release version file: $error.');
}
}

/// The root of the engine repository.
final Engine _engineRoot;
ReleaseVersion? _releaseVersion;

/// Whether the client is available and can be used in this environment.
static bool isAvailable({
Expand Down Expand Up @@ -244,6 +309,14 @@ interface class SkiaGoldClient {
/// determined by the [pixelColorDelta] parameter. It's in the range [0.0,
/// 1.0] and defaults to 0.01. A value of 0.01 means that 1% of the pixels are
/// allowed to be different.
///
/// ## Release Testing
///
/// In release branches, we add a unique test suffix to the test name. For
/// example "testName" -> "testName_Release_3_21", based on the version in the
/// `.engine-release.version` file at the root of the engine repository.
///
/// See <../README.md#release-testing> for more information.
Future<void> addImg(
String testName,
io.File goldenFile, {
Expand All @@ -252,6 +325,17 @@ interface class SkiaGoldClient {
required int screenshotSize,
}) async {
assert(_isPresubmit || _isPostsubmit);

// Clean the test name to remove the file extension.
testName = path.basenameWithoutExtension(testName);

// In release branches, we add a unique test suffix to the test name.
// For example "testName" -> "testName_Release_3_21".
// See ../README.md#release-testing for more information.
if (_releaseVersion case final ReleaseVersion v) {
testName = '${testName}_Release_${v.major}_${v.minor}';
}

if (_isPresubmit) {
await _tryjobAdd(testName, goldenFile, screenshotSize, pixelColorDelta, differentPixelsRate);
}
Expand Down Expand Up @@ -287,7 +371,7 @@ interface class SkiaGoldClient {
'--work-dir',
_tempPath,
'--test-name',
_cleanTestName(testName),
testName,
'--png-file',
goldenFile.path,
// Otherwise post submit will not fail.
Expand Down Expand Up @@ -391,7 +475,7 @@ interface class SkiaGoldClient {
'--work-dir',
_tempPath,
'--test-name',
_cleanTestName(testName),
testName,
'--png-file',
goldenFile.path,
..._getMatchingArguments(testName, screenshotSize, pixelDeltaThreshold, differentPixelsRate),
Expand Down Expand Up @@ -489,7 +573,7 @@ interface class SkiaGoldClient {

/// Returns the current commit hash of the engine repository.
Future<String> _getCurrentCommit() async {
final String engineCheckout = Engine.findWithin().flutterDir.path;
final String engineCheckout = _engineRoot.flutterDir.path;
final io.ProcessResult revParse = await process.run(
<String>['git', 'rev-parse', 'HEAD'],
workingDirectory: engineCheckout,
Expand Down Expand Up @@ -521,12 +605,6 @@ interface class SkiaGoldClient {
return json.encode(_getKeys());
}

/// Removes the file extension from the [fileName] to represent the test name
/// properly.
static String _cleanTestName(String fileName) {
return path.basenameWithoutExtension(fileName);
}

/// Returns a list of arguments for initializing a tryjob based on the testing
/// environment.
List<String> _getCIArguments() {
Expand Down
Loading