diff --git a/.ci.yaml b/.ci.yaml index d84d5a7f1ea34..2c396e5a80f91 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -293,6 +293,10 @@ targets: add_recipes_cq: "true" release_build: "true" config_name: linux_host_engine + dependencies: >- + [ + {"dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"} + ] drone_dimensions: - os=Linux diff --git a/ci/builders/linux_host_engine.json b/ci/builders/linux_host_engine.json index aae0ac2835190..8c1f46f614f84 100644 --- a/ci/builders/linux_host_engine.json +++ b/ci/builders/linux_host_engine.json @@ -185,6 +185,12 @@ "device_type=none", "os=Linux" ], + "dependencies": [ + { + "dependency": "goldctl", + "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd" + } + ], "gclient_variables": { "download_android_deps": false }, diff --git a/impeller/golden_tests_harvester/bin/golden_tests_harvester.dart b/impeller/golden_tests_harvester/bin/golden_tests_harvester.dart index 21524bf9bc4da..62a5f4525d856 100644 --- a/impeller/golden_tests_harvester/bin/golden_tests_harvester.dart +++ b/impeller/golden_tests_harvester/bin/golden_tests_harvester.dart @@ -16,13 +16,16 @@ bool get isLuciEnv => Platform.environment.containsKey(_kLuciEnvName); /// Fake SkiaGoldClient that is used if the harvester is run outside of Luci. class FakeSkiaGoldClient implements SkiaGoldClient { - FakeSkiaGoldClient(this._workingDirectory, {this.dimensions}); + FakeSkiaGoldClient(this._workingDirectory, {this.dimensions, this.verbose = false}); final Directory _workingDirectory; @override final Map? dimensions; + @override + final bool verbose; + @override Future addImg(String testName, File goldenFile, {double differentPixelsRate = 0.01, diff --git a/impeller/golden_tests_harvester/pubspec.yaml b/impeller/golden_tests_harvester/pubspec.yaml index d733c8c162583..02d10c2c6434f 100644 --- a/impeller/golden_tests_harvester/pubspec.yaml +++ b/impeller/golden_tests_harvester/pubspec.yaml @@ -30,6 +30,8 @@ dependency_overrides: path: ../../../third_party/dart/third_party/pkg/file/packages/file meta: path: ../../../third_party/dart/pkg/meta + engine_repo_tools: + path: ../../tools/pkg/engine_repo_tools path: path: ../../../third_party/dart/third_party/pkg/path platform: diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 1b53ead9bd9c8..e777c0ef26603 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -54,3 +54,7 @@ dev_dependencies: path: ../../web_sdk/web_engine_tester skia_gold_client: path: ../../testing/skia_gold_client + +dependency_overrides: + engine_repo_tools: + path: ../../tools/pkg/engine_repo_tools diff --git a/testing/dart/BUILD.gn b/testing/dart/BUILD.gn index d9bd962168333..8d590a83eeede 100644 --- a/testing/dart/BUILD.gn +++ b/testing/dart/BUILD.gn @@ -48,9 +48,11 @@ tests = [ ] foreach(test, tests) { + skia_gold_work_dir = rebase_path("$root_gen_dir/skia_gold_$test") flutter_frontend_server("compile_$test") { main_dart = test kernel_output = "$root_gen_dir/$test.dill" + extra_args = [ "-DkSkiaGoldWorkDirectory=$skia_gold_work_dir" ] package_config = ".dart_tool/package_config.json" } } diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index d317e2f13e420..8c4937bdab2d1 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -12,6 +12,7 @@ import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; import 'package:vector_math/vector_math_64.dart'; +import 'goldens.dart'; import 'impeller_enabled.dart'; typedef CanvasCallback = void Function(Canvas canvas); @@ -123,59 +124,9 @@ void testNoCrashes() { }); } -/// @returns true When the images are reasonably similar. -/// @todo Make the search actually fuzzy to a certain degree. -Future fuzzyCompareImages(Image golden, Image img) async { - if (golden.width != img.width || golden.height != img.height) { - return false; - } - int getPixel(ByteData data, int x, int y) => data.getUint32((x + y * golden.width) * 4); - final ByteData goldenData = (await golden.toByteData())!; - final ByteData imgData = (await img.toByteData())!; - for (int y = 0; y < golden.height; y++) { - for (int x = 0; x < golden.width; x++) { - if (getPixel(goldenData, x, y) != getPixel(imgData, x, y)) { - return false; - } - } - } - return true; -} - -Future saveTestImage(Image image, String filename) async { - final String imagesPath = path.join('flutter', 'testing', 'resources'); - final ByteData pngData = (await image.toByteData(format: ImageByteFormat.png))!; - final String outPath = path.join(imagesPath, filename); - File(outPath).writeAsBytesSync(pngData.buffer.asUint8List()); - print('wrote: $outPath'); -} - -/// @returns true When the images are reasonably similar. -Future fuzzyGoldenImageCompare( - Image image, String goldenImageName) async { - final String imagesPath = path.join('flutter', 'testing', 'resources'); - final File file = File(path.join(imagesPath, goldenImageName)); - - bool areEqual = false; - - if (file.existsSync()) { - final Uint8List goldenData = await file.readAsBytes(); - - final Codec codec = await instantiateImageCodec(goldenData); - final FrameInfo frame = await codec.getNextFrame(); - expect(frame.image.height, equals(image.height)); - expect(frame.image.width, equals(image.width)); - - areEqual = await fuzzyCompareImages(frame.image, image); - } +void main() async { + final ImageComparer comparer = await ImageComparer.create(); - if (!areEqual) { - saveTestImage(image, 'found_$goldenImageName'); - } - return areEqual; -} - -void main() { testNoCrashes(); test('Simple .toImage', () async { @@ -190,11 +141,8 @@ void main() { }, 100, 100); expect(image.width, equals(100)); expect(image.height, equals(100)); - - final bool areEqual = - await fuzzyGoldenImageCompare(image, 'canvas_test_toImage.png'); - expect(areEqual, true); - }, skip: impellerEnabled); + await comparer.addGoldenImage(image, 'canvas_test_toImage.png'); + }); Gradient makeGradient() { return Gradient.linear( @@ -212,10 +160,8 @@ void main() { expect(image.width, equals(100)); expect(image.height, equals(100)); - final bool areEqual = - await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png'); - expect(areEqual, true); - }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + await comparer.addGoldenImage(image, 'canvas_test_dithered_gradient.png'); + }); test('Null values allowed for drawAtlas methods', () async { final Image image = await createImage(100, 100); @@ -302,12 +248,8 @@ void main() { }); }, width, height); - final bool areEqual = await fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage); + final bool areEqual = await comparer.fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage); - if (!areEqual) { - saveTestImage(incrementalMatrixImage, 'incremental_3D_transform_test_image.png'); - saveTestImage(combinedMatrixImage, 'combined_3D_transform_test_image.png'); - } expect(areEqual, true); }); @@ -348,10 +290,8 @@ void main() { expect(image.width, equals(200)); expect(image.height, equals(250)); - final bool areEqual = - await fuzzyGoldenImageCompare(image, 'dotted_path_effect_mixed_with_stroked_geometry.png'); - expect(areEqual, true); - }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + await comparer.addGoldenImage(image, 'dotted_path_effect_mixed_with_stroked_geometry.png'); + }); test('Gradients with matrices in Paragraphs render correctly', () async { final Image image = await toImage((Canvas canvas) { @@ -400,10 +340,8 @@ void main() { expect(image.width, equals(600)); expect(image.height, equals(400)); - final bool areEqual = - await fuzzyGoldenImageCompare(image, 'text_with_gradient_with_matrix.png'); - expect(areEqual, true); - }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + await comparer.addGoldenImage(image, 'text_with_gradient_with_matrix.png'); + }); test('toImageSync - too big', () async { PictureRecorder recorder = PictureRecorder(); @@ -602,8 +540,8 @@ void main() { final Image tofuImage = await drawText('>\b<'); // The tab's image should be identical to the space's image but not the tofu's image. - final bool tabToSpaceComparison = await fuzzyCompareImages(tabImage, spaceImage); - final bool tabToTofuComparison = await fuzzyCompareImages(tabImage, tofuImage); + final bool tabToSpaceComparison = await comparer.fuzzyCompareImages(tabImage, spaceImage); + final bool tabToTofuComparison = await comparer.fuzzyCompareImages(tabImage, tofuImage); expect(tabToSpaceComparison, isTrue); expect(tabToTofuComparison, isFalse); diff --git a/testing/dart/goldens.dart b/testing/dart/goldens.dart new file mode 100644 index 0000000000000..5a70ecfc95f78 --- /dev/null +++ b/testing/dart/goldens.dart @@ -0,0 +1,129 @@ +// Copyright 2013 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:io'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:path/path.dart' as path; +import 'package:skia_gold_client/skia_gold_client.dart'; + +import 'impeller_enabled.dart'; + +const String _kSkiaGoldWorkDirectoryKey = 'kSkiaGoldWorkDirectory'; + +/// A helper for doing image comparison (golden) tests. +/// +/// Contains utilities for comparing two images in memory that are expected to +/// be identical, or for adding images to Skia gold for comparison. +class ImageComparer { + ImageComparer._({ + required SkiaGoldClient client, + }) : _client = client; + + // Avoid talking to Skia gold for the force-multithreading variants. + static bool get _useSkiaGold => + !Platform.executableArguments.contains('--force-multithreading'); + + /// Creates an image comparer and authorizes. + static Future create({ + bool verbose = false, + }) async { + const String workDirectoryPath = + String.fromEnvironment(_kSkiaGoldWorkDirectoryKey); + if (workDirectoryPath.isEmpty) { + throw UnsupportedError( + 'Using ImageComparer requries defining kSkiaGoldWorkDirectoryKey.'); + } + + final Directory workDirectory = Directory( + impellerEnabled ? '${workDirectoryPath}_iplr' : workDirectoryPath, + )..createSync(); + final Map dimensions = { + 'impeller_enabled': impellerEnabled.toString(), + }; + final SkiaGoldClient client = isSkiaGoldClientAvailable && _useSkiaGold + ? SkiaGoldClient(workDirectory, + dimensions: dimensions, verbose: verbose) + : _FakeSkiaGoldClient(workDirectory, dimensions, verbose: verbose); + + await client.auth(); + return ImageComparer._(client: client); + } + + final SkiaGoldClient _client; + + /// Adds an [Image] to Skia Gold for comparison. + /// + /// The [fileName] must be unique. + Future addGoldenImage(Image image, String fileName) async { + final ByteData data = + (await image.toByteData(format: ImageByteFormat.png))!; + + final File file = File(path.join(_client.workDirectory.path, fileName)) + ..writeAsBytesSync(data.buffer.asUint8List()); + await _client.addImg( + fileName, + file, + screenshotSize: image.width * image.height, + ).catchError((dynamic error) { + print('Skia gold comparison failed: $error'); + throw Exception('Failed comparison: $fileName'); + }); + } + + Future fuzzyCompareImages(Image golden, Image testImage) async { + if (golden.width != testImage.width || golden.height != testImage.height) { + return false; + } + int getPixel(ByteData data, int x, int y) => + data.getUint32((x + y * golden.width) * 4); + final ByteData goldenData = (await golden.toByteData())!; + final ByteData testImageData = (await testImage.toByteData())!; + for (int y = 0; y < golden.height; y++) { + for (int x = 0; x < golden.width; x++) { + if (getPixel(goldenData, x, y) != getPixel(testImageData, x, y)) { + return false; + } + } + } + return true; + } +} + +// TODO(dnfield): add local comparison against baseline, +// https://github.com/flutter/flutter/issues/136831 +class _FakeSkiaGoldClient implements SkiaGoldClient { + _FakeSkiaGoldClient( + this.workDirectory, + this.dimensions, { + this.verbose = false, + }); + + @override + final Directory workDirectory; + + @override + final Map dimensions; + + @override + final bool verbose; + + @override + Future auth() async {} + + @override + Future addImg( + String testName, + File goldenFile, { + double differentPixelsRate = 0.01, + int pixelColorDelta = 0, + required int screenshotSize, + }) async {} + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError(invocation.memberName.toString().split('"')[1]); + } +} diff --git a/testing/dart/pubspec.yaml b/testing/dart/pubspec.yaml index 771026fff3627..e13765a1972be 100644 --- a/testing/dart/pubspec.yaml +++ b/testing/dart/pubspec.yaml @@ -19,6 +19,7 @@ environment: dependencies: litetest: any path: any + skia_gold_client: any sky_engine: any vector_math: any vm_service: any @@ -29,8 +30,14 @@ dependency_overrides: path: ../../../third_party/dart/pkg/async_helper collection: path: ../../../third_party/dart/third_party/pkg/collection + crypto: + path: ../../../third_party/dart/third_party/pkg/crypto + engine_repo_tools: + path: ../../tools/pkg/engine_repo_tools expect: path: ../../../third_party/dart/pkg/expect + file: + path: ../../../third_party/dart/third_party/pkg/file/packages/file fixnum: path: ../../../third_party/dart/third_party/pkg/fixnum litetest: @@ -39,12 +46,20 @@ dependency_overrides: path: ../../../third_party/dart/pkg/meta path: path: ../../../third_party/dart/third_party/pkg/path + platform: + path: ../../../third_party/pkg/platform + process: + path: ../../../third_party/pkg/process protobuf: path: ../../../third_party/dart/third_party/pkg/protobuf/protobuf smith: path: ../../../third_party/dart/pkg/smith + skia_gold_client: + path: ../skia_gold_client sky_engine: path: ../../sky/packages/sky_engine + typed_data: + path: ../../../third_party/dart/third_party/pkg/typed_data vector_math: path: ../../../third_party/pkg/vector_math vm_service: diff --git a/testing/resources/canvas_test_dithered_gradient.png b/testing/resources/canvas_test_dithered_gradient.png deleted file mode 100644 index d8062f2dc1f35..0000000000000 Binary files a/testing/resources/canvas_test_dithered_gradient.png and /dev/null differ diff --git a/testing/resources/canvas_test_gradient.png b/testing/resources/canvas_test_gradient.png deleted file mode 100644 index 89f3f63dc518a..0000000000000 Binary files a/testing/resources/canvas_test_gradient.png and /dev/null differ diff --git a/testing/resources/canvas_test_toImage.png b/testing/resources/canvas_test_toImage.png deleted file mode 100644 index 75d02c2fa967c..0000000000000 Binary files a/testing/resources/canvas_test_toImage.png and /dev/null differ diff --git a/testing/resources/dotted_path_effect_mixed_with_stroked_geometry.png b/testing/resources/dotted_path_effect_mixed_with_stroked_geometry.png deleted file mode 100644 index a8aef6b9936d7..0000000000000 Binary files a/testing/resources/dotted_path_effect_mixed_with_stroked_geometry.png and /dev/null differ diff --git a/testing/resources/text_with_gradient_with_matrix.png b/testing/resources/text_with_gradient_with_matrix.png deleted file mode 100644 index b940c1ee4e08b..0000000000000 Binary files a/testing/resources/text_with_gradient_with_matrix.png and /dev/null differ diff --git a/testing/run_tests.py b/testing/run_tests.py index 419460f43275d..8074d81dd5b9f 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -606,6 +606,11 @@ def threading_description(self): return 'multithreaded' return 'single-threaded' + def impeller_enabled(self): + if self.enable_impeller: + return 'impeller swiftshader' + return 'skia software' + def gather_dart_test(build_dir, dart_file, options): kernel_file_name = os.path.basename(dart_file) + '.dill' @@ -637,8 +642,8 @@ def gather_dart_test(build_dir, dart_file, options): tester_name = 'flutter_tester' logger.info( - "Running test '%s' using '%s' (%s)", kernel_file_name, tester_name, - options.threading_description() + "Running test '%s' using '%s' (%s, %s)", kernel_file_name, tester_name, + options.threading_description(), options.impeller_enabled() ) forbidden_output = [] if 'unopt' in build_dir or options.expect_failure else [ '[ERROR' diff --git a/testing/scenario_app/pubspec.yaml b/testing/scenario_app/pubspec.yaml index 4c579aa4d8230..fb13f5848f68b 100644 --- a/testing/scenario_app/pubspec.yaml +++ b/testing/scenario_app/pubspec.yaml @@ -29,6 +29,8 @@ dependency_overrides: path: ../../../third_party/dart/third_party/pkg/collection crypto: path: ../../../third_party/dart/third_party/pkg/crypto + engine_repo_tools: + path: ../../tools/pkg/engine_repo_tools file: path: ../../../third_party/dart/third_party/pkg/file/packages/file meta: diff --git a/testing/skia_gold_client/README.md b/testing/skia_gold_client/README.md index c9d6a8ce56498..3e7f6ec2d5fea 100644 --- a/testing/skia_gold_client/README.md +++ b/testing/skia_gold_client/README.md @@ -15,15 +15,29 @@ The web UI is available on https://flutter-engine-gold.skia.org/. dependencies: [{"dependency": "goldctl"}] ``` -2. Add dependency in `pubspec.yaml`: +2. In the builder `.json` file, ensure the drone has a dependency on `goldctl`: + +```yaml + "dependencies": [ + { + "dependency": "goldctl", + "version": "git_revision:" + } + ], +``` + +3. Add dependency in `pubspec.yaml`: ```yaml dependencies: + # needed for skia_gold_client to avoid a cache miss. + engine_repo_tools: + path: /tools/pkg/engine_repo_tools skia_gold_client: path: /testing/skia_gold_client ``` -3. Use the client: +4. Use the client: ```dart import 'package:skia_gold_client/skia_gold_client.dart'; diff --git a/testing/skia_gold_client/lib/skia_gold_client.dart b/testing/skia_gold_client/lib/skia_gold_client.dart index b258de044b554..471aec03ece3f 100644 --- a/testing/skia_gold_client/lib/skia_gold_client.dart +++ b/testing/skia_gold_client/lib/skia_gold_client.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; +import 'package:engine_repo_tools/engine_repo_tools.dart'; import 'package:path/path.dart' as path; import 'package:process/process.dart'; @@ -36,7 +37,13 @@ class SkiaGoldClient { /// /// [dimensions] allows to add attributes about the environment /// used to generate the screenshots. - SkiaGoldClient(this.workDirectory, { this.dimensions }); + SkiaGoldClient(this.workDirectory, { this.dimensions, this.verbose = false}); + + /// Whether to print verbose output from goldctl. + /// + /// This flag is intended for use in debugging CI issues, and should not + /// ordinarily be set to true. + final bool verbose; /// Allows to add attributes about the environment used to generate the screenshots. final Map? dimensions; @@ -95,6 +102,7 @@ class SkiaGoldClient { final List authCommand = [ _goldctl, 'auth', + if (verbose) '--verbose', '--work-dir', _tempPath, '--luci', ]; @@ -111,6 +119,9 @@ class SkiaGoldClient { ..writeln('stdout: ${result.stdout}') ..writeln('stderr: ${result.stderr}'); throw Exception(buf.toString()); + } else if (verbose) { + print('stdout:\n${result.stdout}'); + print('stderr:\n${result.stderr}'); } } @@ -134,6 +145,7 @@ class SkiaGoldClient { final List imgtestInitCommand = [ _goldctl, 'imgtest', 'init', + if (verbose) '--verbose', '--instance', _instance, '--work-dir', _tempPath, '--commit', commitHash, @@ -163,7 +175,11 @@ class SkiaGoldClient { ..writeln('stdout: ${result.stdout}') ..writeln('stderr: ${result.stderr}'); throw Exception(buf.toString()); + } else if (verbose) { + print('stdout:\n${result.stdout}'); + print('stderr:\n${result.stderr}'); } + } /// Executes the `imgtest add` command in the `goldctl` tool. @@ -229,6 +245,7 @@ class SkiaGoldClient { final List imgtestCommand = [ _goldctl, 'imgtest', 'add', + if (verbose) '--verbose', '--work-dir', _tempPath, '--test-name', cleanTestName(testName), '--png-file', goldenFile.path, @@ -243,6 +260,9 @@ class SkiaGoldClient { // is meant to inform when an unexpected result occurs. print('goldctl imgtest add stdout: ${result.stdout}'); print('goldctl imgtest add stderr: ${result.stderr}'); + } else if (verbose) { + print('stdout:\n${result.stdout}'); + print('stderr:\n${result.stderr}'); } } @@ -261,6 +281,7 @@ class SkiaGoldClient { final List tryjobInitCommand = [ _goldctl, 'imgtest', 'init', + if (verbose) '--verbose', '--instance', _instance, '--work-dir', _tempPath, '--commit', commitHash, @@ -293,6 +314,9 @@ class SkiaGoldClient { ..writeln('stdout: ${result.stdout}') ..writeln('stderr: ${result.stderr}'); throw Exception(buf.toString()); + } else if (verbose) { + print('stdout:\n${result.stdout}'); + print('stderr:\n${result.stderr}'); } } @@ -317,6 +341,7 @@ class SkiaGoldClient { final List tryjobCommand = [ _goldctl, 'imgtest', 'add', + if (verbose) '--verbose', '--work-dir', _tempPath, '--test-name', cleanTestName(testName), '--png-file', goldenFile.path, @@ -338,6 +363,9 @@ class SkiaGoldClient { ..writeln('stderr: ${result.stderr}') ..writeln(); throw Exception(buf.toString()); + } else if (verbose) { + print('stdout:\n${result.stdout}'); + print('stderr:\n${result.stderr}'); } } @@ -421,13 +449,13 @@ class SkiaGoldClient { /// Returns the current commit hash of the engine repository. Future _getCurrentCommit() async { - final File currentScript = File.fromUri(Platform.script); + final String engineCheckout = Engine.findWithin().flutterDir.path; final ProcessResult revParse = await process.run( ['git', 'rev-parse', 'HEAD'], - workingDirectory: currentScript.parent.absolute.path, + workingDirectory: engineCheckout, ); if (revParse.exitCode != 0) { - throw Exception('Current commit of the engine can not be found from path ${currentScript.path}.'); + throw Exception('Current commit of the engine can not be found from path $engineCheckout.'); } return (revParse.stdout as String).trim(); } diff --git a/testing/skia_gold_client/pubspec.yaml b/testing/skia_gold_client/pubspec.yaml index 8c7bc0a6a7ff9..693424d5350d6 100644 --- a/testing/skia_gold_client/pubspec.yaml +++ b/testing/skia_gold_client/pubspec.yaml @@ -17,6 +17,7 @@ environment: dependencies: crypto: any path: any + engine_repo_tools: any process: any dependency_overrides: @@ -24,6 +25,8 @@ dependency_overrides: path: ../../../third_party/dart/third_party/pkg/collection crypto: path: ../../../third_party/dart/third_party/pkg/crypto + engine_repo_tools: + path: ../../tools/pkg/engine_repo_tools file: path: ../../../third_party/dart/third_party/pkg/file/packages/file meta: diff --git a/web_sdk/web_test_utils/pubspec.yaml b/web_sdk/web_test_utils/pubspec.yaml index ca7efd336eec7..fbdd1be557741 100644 --- a/web_sdk/web_test_utils/pubspec.yaml +++ b/web_sdk/web_test_utils/pubspec.yaml @@ -16,3 +16,7 @@ dependencies: path: ../../testing/skia_gold_client typed_data: 1.3.0 yaml: 3.0.0 + +dependency_overrides: + engine_repo_tools: + path: ../../tools/pkg/engine_repo_tools