Revert "Allow Flutter golden file tests to be flaky (#114450)" (#114902)

This reverts commit 53e6876226.
This commit is contained in:
Zachary Anderson 2022-11-08 09:26:55 -08:00 committed by GitHub
parent a1432a9c5d
commit a84e369bd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1351 additions and 2093 deletions

View file

@ -8,7 +8,7 @@ import 'dart:typed_data';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_goldens/src/flutter_goldens_io.dart';
import 'package:flutter_goldens/flutter_goldens.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform/platform.dart';

View file

@ -418,8 +418,8 @@ Future<void> verifyNoSyncAsyncStar(String workingDirectory, {int minimumMatches
}
}
final RegExp _findGoldenTestPattern = RegExp(r'(matchesGoldenFile|expectFlakyGolden)\(');
final RegExp _findGoldenDefinitionPattern = RegExp(r'(matchesGoldenFile|expectFlakyGolden)\(Object');
final RegExp _findGoldenTestPattern = RegExp(r'matchesGoldenFile\(');
final RegExp _findGoldenDefinitionPattern = RegExp(r'matchesGoldenFile\(Object');
final RegExp _leadingComment = RegExp(r'//');
final RegExp _goldenTagPattern1 = RegExp(r'@Tags\(');
final RegExp _goldenTagPattern2 = RegExp(r"'reduced-test-set'");
@ -431,17 +431,8 @@ const String _ignoreGoldenTag = '// flutter_ignore: golden_tag (see analyze.dart
const String _ignoreGoldenTagForFile = '// flutter_ignore_for_file: golden_tag (see analyze.dart)';
Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 2000 }) async {
// Skip flutter_goldens/lib because this library uses `matchesGoldenFile`
// but is not itself a test that needs tags.
final String flutterGoldensPackageLib = path.join(flutterPackages, 'flutter_goldens', 'lib');
bool isWithinFlutterGoldenLib(File file) {
return path.isWithin(flutterGoldensPackageLib, file.path);
}
final List<String> errors = <String>[];
final Stream<File> allTestFiles = _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)
.where((File file) => !isWithinFlutterGoldenLib(file));
await for (final File file in allTestFiles) {
await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
bool needsTag = false;
bool hasTagNotation = false;
bool hasReducedTag = false;

View file

@ -1,16 +0,0 @@
// 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.
// This would fail analysis, but it is ignored
// flutter_ignore_for_file: golden_tag (see analyze.dart)
@Tags(<String>['some-other-tag'])
import 'package:test/test.dart';
import 'golden_class.dart';
void main() {
expectFlakyGolden('key', 'String');
}

View file

@ -1,14 +0,0 @@
// 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.
// The reduced test set tag is missing. This should fail analysis.
@Tags(<String>['some-other-tag'])
import 'package:test/test.dart';
import 'golden_class.dart';
void main() {
expectFlakyGolden('finder', 'missing_tag.png');
}

View file

@ -7,7 +7,3 @@
void matchesGoldenFile(Object key) {
return;
}
void expectFlakyGolden(Object key, String string){
return;
}

View file

@ -35,11 +35,8 @@
/// ```
/// {@end-tool}
///
/// expectFlakyGolden(a, b)
// Other comments
// matchesGoldenFile('comment.png');
// expectFlakyGolden(a, b);
String literal = 'matchesGoldenFile()'; // flutter_ignore: golden_tag (see analyze.dart)
String flakyLiteral = 'expectFlakyGolden';

View file

@ -79,9 +79,7 @@ void main() {
'at the top of the file before import statements.';
const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
final List<String> lines = <String>[
'║ test/analyze-test-input/root/packages/foo/flaky_golden_no_tag.dart: $noTag',
'║ test/analyze-test-input/root/packages/foo/golden_missing_tag.dart: $missingTag',
'║ test/analyze-test-input/root/packages/foo/flaky_golden_missing_tag.dart: $missingTag',
'║ test/analyze-test-input/root/packages/foo/golden_no_tag.dart: $noTag',
]
.map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))

View file

@ -17,7 +17,6 @@ const double todoCost = 1009.0; // about two average SWE days, in dollars
const double ignoreCost = 2003.0; // four average SWE days, in dollars
const double pythonCost = 3001.0; // six average SWE days, in dollars
const double skipCost = 2473.0; // 20 hours: 5 to fix the issue we're ignoring, 15 to fix the bugs we missed because the test was off
const double flakyGoldenCost = 2467.0; // Similar to skip cost
const double ignoreForFileCost = 2477.0; // similar thinking as skipCost
const double asDynamicCost = 2011.0; // a few days to refactor the code.
const double deprecationCost = 233.0; // a few hours to remove the old code.
@ -70,9 +69,6 @@ Future<double> findCostsForFile(File file) async {
if (isTest && line.contains('skip:') && !line.contains('[intended]')) {
total += skipCost;
}
if (isTest && line.contains('expectFlakyGolden(')) {
total += flakyGoldenCost;
}
if (isDart && isOptingOutOfNullSafety(line)) {
total += fileNullSafetyMigrationCost;
}

View file

@ -5,7 +5,7 @@
import 'dart:async';
import 'package:flutter_goldens/flutter_goldens.dart' as flutter_goldens;
import 'goldens_io.dart' if (dart.library.html) 'goldens_web.dart' as flutter_goldens;
Future<void> testExecutable(FutureOr<void> Function() testMain) {
// Enable golden file testing using Skia Gold.

View file

@ -2,10 +2,4 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The tag is missing. This should fail analysis.
import 'golden_class.dart';
void main() {
expectFlakyGolden('key', 'missing_tag.png');
}
export 'package:flutter_goldens/flutter_goldens.dart' show testExecutable;

View file

@ -0,0 +1,8 @@
// 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';
// package:flutter_goldens is not used as part of the test process for web.
Future<void> testExecutable(FutureOr<void> Function() testMain) async => testMain();

View file

@ -0,0 +1,5 @@
// 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.
export 'package:flutter_goldens/flutter_goldens.dart' show testExecutable;

View file

@ -0,0 +1,8 @@
// 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';
// package:flutter_goldens is not used as part of the test process for web.
Future<void> testExecutable(FutureOr<void> Function() testMain) async => testMain();

View file

@ -17,11 +17,10 @@ import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_goldens/flutter_goldens.dart' show expectFlakyGolden;
import 'package:flutter_test/flutter_test.dart';
// TODO(yjbanov): on the web text rendered with perspective produces flaky goldens: https://github.com/flutter/flutter/issues/110785
const bool perspectiveTestIsFlaky = isBrowser;
const bool skipPerspectiveTextGoldens = isBrowser;
// A number of the hit tests below say "warnIfMissed: false". This is because
// the way the CupertinoPicker works, the hits don't actually reach the labels,
@ -1198,13 +1197,7 @@ void main() {
}
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.time));
if (perspectiveTestIsFlaky) {
await expectFlakyGolden(
find.byType(CupertinoDatePicker),
'date_picker_test.time.initial.png',
);
} else {
if (!skipPerspectiveTextGoldens) {
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.time.initial.png'),
@ -1212,13 +1205,7 @@ void main() {
}
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.date));
if (perspectiveTestIsFlaky) {
await expectFlakyGolden(
find.byType(CupertinoDatePicker),
'date_picker_test.date.initial.png',
);
} else {
if (!skipPerspectiveTextGoldens) {
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.date.initial.png'),
@ -1226,13 +1213,7 @@ void main() {
}
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.dateAndTime));
if (perspectiveTestIsFlaky) {
await expectFlakyGolden(
find.byType(CupertinoDatePicker),
'date_picker_test.datetime.initial.png',
);
} else {
if (!skipPerspectiveTextGoldens) {
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.datetime.initial.png'),
@ -1243,12 +1224,7 @@ void main() {
await tester.drag(find.text('4'), Offset(0, _kRowOffset.dy / 2), warnIfMissed: false); // see top of file
await tester.pump();
if (perspectiveTestIsFlaky) {
await expectFlakyGolden(
find.byType(CupertinoDatePicker),
'date_picker_test.datetime.drag.png',
);
} else {
if (!skipPerspectiveTextGoldens) {
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.datetime.drag.png'),
@ -1338,12 +1314,7 @@ void main() {
),
);
if (perspectiveTestIsFlaky) {
await expectFlakyGolden(
find.byType(CupertinoTimerPicker),
'timer_picker_test.datetime.initial.png',
);
} else {
if (!skipPerspectiveTextGoldens) {
await expectLater(
find.byType(CupertinoTimerPicker),
matchesGoldenFile('timer_picker_test.datetime.initial.png'),
@ -1354,12 +1325,7 @@ void main() {
await tester.drag(find.text('59'), Offset(0, _kRowOffset.dy / 2), warnIfMissed: false); // see top of file
await tester.pump();
if (perspectiveTestIsFlaky) {
await expectFlakyGolden(
find.byType(CupertinoTimerPicker),
'timer_picker_test.datetime.drag.png',
);
} else {
if (!skipPerspectiveTextGoldens) {
await expectLater(
find.byType(CupertinoTimerPicker),
matchesGoldenFile('timer_picker_test.datetime.drag.png'),

View file

@ -5,9 +5,11 @@
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter_goldens/flutter_goldens.dart' as flutter_goldens;
import 'package:flutter_test/flutter_test.dart';
import '_goldens_io.dart'
if (dart.library.html) '_goldens_web.dart' as flutter_goldens;
Future<void> testExecutable(FutureOr<void> Function() testMain) {
// Enable checks because there are many implementations of [RenderBox] in this
// package can benefit from the additional validations.
@ -20,7 +22,3 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) {
// Enable golden file testing using Skia Gold.
return flutter_goldens.testExecutable(testMain);
}
Future<void> processBrowserCommand(dynamic command) {
return flutter_goldens.processBrowserCommand(command);
}

View file

@ -1,25 +0,0 @@
// 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:convert';
import 'dart:io';
import 'flutter_test_config.dart';
/// A custom host configuration for browser tests that supports flaky golden
/// checks.
///
/// See also [processBrowserCommand].
Future<void> startWebTestHostConfiguration(String testUri) async {
testExecutable(() async {
final Stream<dynamic> commands = stdin
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.map<dynamic>(jsonDecode);
await for (final dynamic command in commands) {
await processBrowserCommand(command);
}
});
}

View file

@ -2,10 +2,544 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// This library exposes functions that enhance the test with custom golden
/// configuration for the Flutter repository.
import 'dart:async' show FutureOr;
import 'dart:io' as io show OSError, SocketException;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_goldens_client/skia_client.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform/platform.dart';
export 'package:flutter_goldens_client/skia_client.dart';
export 'src/flaky_goldens.dart' show expectFlakyGolden;
export 'src/flutter_goldens_io.dart' if (dart.library.js_util) 'src/flutter_goldens_web.dart'
show processBrowserCommand, testExecutable;
// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter
const String _kFlutterRootKey = 'FLUTTER_ROOT';
/// Main method that can be used in a `flutter_test_config.dart` file to set
/// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that
/// works for the current test. _Which_ FlutterGoldenFileComparator is
/// instantiated is based on the current testing environment.
///
/// When set, the `namePrefix` is prepended to the names of all gold images.
Future<void> testExecutable(FutureOr<void> Function() testMain, {String? namePrefix}) async {
const Platform platform = LocalPlatform();
if (FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = await FlutterPostSubmitFileComparator.fromDefaultComparator(platform, namePrefix: namePrefix);
} else if (FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = await FlutterPreSubmitFileComparator.fromDefaultComparator(platform, namePrefix: namePrefix);
} else if (FlutterSkippingFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = FlutterSkippingFileComparator.fromDefaultComparator(
'Golden file testing is not executed on Cirrus, or LUCI environments outside of flutter/flutter.',
namePrefix: namePrefix
);
} else {
goldenFileComparator = await FlutterLocalFileComparator.fromDefaultComparator(platform);
}
await testMain();
}
/// Abstract base class golden file comparator specific to the `flutter/flutter`
/// repository.
///
/// Golden file testing for the `flutter/flutter` repository is handled by three
/// different [FlutterGoldenFileComparator]s, depending on the current testing
/// environment.
///
/// * The [FlutterPostSubmitFileComparator] is utilized during post-submit
/// testing, after a pull request has landed on the master branch. This
/// comparator uses the [SkiaGoldClient] and the `goldctl` tool to upload
/// tests to the [Flutter Gold dashboard](https://flutter-gold.skia.org).
/// Flutter Gold manages the master golden files for the `flutter/flutter`
/// repository.
///
/// * The [FlutterPreSubmitFileComparator] is utilized in pre-submit testing,
/// before a pull request lands on the master branch. This
/// comparator uses the [SkiaGoldClient] to execute tryjobs, allowing
/// contributors to view and check in visual differences before landing the
/// change.
///
/// * The [FlutterLocalFileComparator] is used for local development testing.
/// This comparator will use the [SkiaGoldClient] to request baseline images
/// from [Flutter Gold](https://flutter-gold.skia.org) and manually compare
/// pixels. If a difference is detected, this comparator will
/// generate failure output illustrating the found difference. If a baseline
/// is not found for a given test image, it will consider it a new test and
/// output the new image for verification.
///
/// The [FlutterSkippingFileComparator] is utilized to skip tests outside
/// of the appropriate environments described above. Currently, some Luci
/// environments do not execute golden file testing, and as such do not require
/// a comparator. This comparator is also used when an internet connection is
/// unavailable.
abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
/// Creates a [FlutterGoldenFileComparator] that will resolve golden file
/// URIs relative to the specified [basedir], and retrieve golden baselines
/// using the [skiaClient]. The [basedir] is used for writing and accessing
/// information and files for interacting with the [skiaClient]. When testing
/// locally, the [basedir] will also contain any diffs from failed tests, or
/// goldens generated from newly introduced tests.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
@visibleForTesting
FlutterGoldenFileComparator(
this.basedir,
this.skiaClient, {
this.fs = const LocalFileSystem(),
this.platform = const LocalPlatform(),
this.namePrefix,
});
/// The directory to which golden file URIs will be resolved in [compare] and
/// [update], cannot be null.
final Uri basedir;
/// A client for uploading image tests and making baseline requests to the
/// Flutter Gold Dashboard, cannot be null.
final SkiaGoldClient skiaClient;
/// The file system used to perform file access.
@visibleForTesting
final FileSystem fs;
/// A wrapper for the [dart:io.Platform] API.
@visibleForTesting
final Platform platform;
/// The prefix that is added to all golden names.
final String? namePrefix;
@override
Future<void> update(Uri golden, Uint8List imageBytes) async {
final File goldenFile = getGoldenFile(golden);
await goldenFile.parent.create(recursive: true);
await goldenFile.writeAsBytes(imageBytes, flush: true);
}
@override
Uri getTestUri(Uri key, int? version) => key;
/// Calculate the appropriate basedir for the current test context.
///
/// The optional [suffix] argument is used by the
/// [FlutterPostSubmitFileComparator] and the [FlutterPreSubmitFileComparator].
/// These [FlutterGoldenFileComparators] randomize their base directories to
/// maintain thread safety while using the `goldctl` tool.
@protected
@visibleForTesting
static Directory getBaseDirectory(
LocalFileComparator defaultComparator,
Platform platform, {
String? suffix,
}) {
const FileSystem fs = LocalFileSystem();
final Directory flutterRoot = fs.directory(platform.environment[_kFlutterRootKey]);
Directory comparisonRoot;
if (suffix != null) {
comparisonRoot = fs.systemTempDirectory.createTempSync(suffix);
} else {
comparisonRoot = flutterRoot.childDirectory(
fs.path.join(
'bin',
'cache',
'pkg',
'skia_goldens',
)
);
}
final Directory testDirectory = fs.directory(defaultComparator.basedir);
final String testDirectoryRelativePath = fs.path.relative(
testDirectory.path,
from: flutterRoot.path,
);
return comparisonRoot.childDirectory(testDirectoryRelativePath);
}
/// Returns the golden [File] identified by the given [Uri].
@protected
File getGoldenFile(Uri uri) {
final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path);
return goldenFile;
}
/// Prepends the golden URL with the library name that encloses the current
/// test.
Uri _addPrefix(Uri golden) {
// Ensure the Uri ends in .png as the SkiaClient expects
assert(
golden.toString().split('.').last == 'png',
'Golden files in the Flutter framework must end with the file extension '
'.png.'
);
return Uri.parse(<String>[
if (namePrefix != null)
namePrefix!,
basedir.pathSegments[basedir.pathSegments.length - 2],
golden.toString(),
].join('.'));
}
}
/// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold in
/// post-submit.
///
/// For testing across all platforms, the [SkiaGoldClient] is used to upload
/// images for framework-related golden tests and process results.
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterPostSubmitFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterPostSubmitFileComparator] that will test golden file
/// images against Skia Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterPostSubmitFileComparator(
super.basedir,
super.skiaClient, {
super.fs,
super.platform,
super.namePrefix,
});
/// Creates a new [FlutterPostSubmitFileComparator] that mirrors the relative
/// path resolution of the default [goldenFileComparator].
///
/// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only.
static Future<FlutterPostSubmitFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient? goldens,
LocalFileComparator? defaultComparator,
String? namePrefix,
}) async {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
suffix: 'flutter_goldens_postsubmit.',
);
baseDirectory.createSync(recursive: true);
goldens ??= SkiaGoldClient(baseDirectory);
await goldens.auth();
return FlutterPostSubmitFileComparator(baseDirectory.uri, goldens, namePrefix: namePrefix);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
await skiaClient.imgtestInit();
golden = _addPrefix(golden);
await update(golden, imageBytes);
final File goldenFile = getGoldenFile(golden);
return skiaClient.imgtestAdd(golden.path, goldenFile);
}
/// Decides based on the current environment if goldens tests should be
/// executed through Skia Gold.
static bool isAvailableForEnvironment(Platform platform) {
final bool luciPostSubmit = platform.environment.containsKey('SWARMING_TASK_ID')
&& platform.environment.containsKey('GOLDCTL')
// Luci tryjob environments contain this value to inform the [FlutterPreSubmitComparator].
&& !platform.environment.containsKey('GOLD_TRYJOB');
return luciPostSubmit;
}
}
/// A [FlutterGoldenFileComparator] for testing golden images before changes are
/// merged into the master branch. The comparator executes tryjobs using the
/// [SkiaGoldClient].
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterPostSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
/// dashboard in post-submit.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterPreSubmitFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterPreSubmitFileComparator] that will test golden file
/// images against baselines requested from Flutter Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterPreSubmitFileComparator(
super.basedir,
super.skiaClient, {
super.fs,
super.platform,
super.namePrefix,
});
/// Creates a new [FlutterPreSubmitFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
///
/// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only.
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient? goldens,
LocalFileComparator? defaultComparator,
Directory? testBasedir,
String? namePrefix,
}) async {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
final Directory baseDirectory = testBasedir ?? FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
suffix: 'flutter_goldens_presubmit.',
);
if (!baseDirectory.existsSync()) {
baseDirectory.createSync(recursive: true);
}
goldens ??= SkiaGoldClient(baseDirectory);
await goldens.auth();
return FlutterPreSubmitFileComparator(
baseDirectory.uri,
goldens, platform: platform,
namePrefix: namePrefix,
);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
await skiaClient.tryjobInit();
golden = _addPrefix(golden);
await update(golden, imageBytes);
final File goldenFile = getGoldenFile(golden);
await skiaClient.tryjobAdd(golden.path, goldenFile);
// This will always return true since golden file test failures are managed
// in pre-submit checks by the flutter-gold status check.
return true;
}
/// Decides based on the current environment if goldens tests should be
/// executed as pre-submit tests with Skia Gold.
static bool isAvailableForEnvironment(Platform platform) {
final bool luciPreSubmit = platform.environment.containsKey('SWARMING_TASK_ID')
&& platform.environment.containsKey('GOLDCTL')
&& platform.environment.containsKey('GOLD_TRYJOB');
return luciPreSubmit;
}
}
/// A [FlutterGoldenFileComparator] for testing conditions that do not execute
/// golden file tests.
///
/// Currently, this comparator is used on Cirrus, or in Luci environments when executing tests
/// outside of the flutter/flutter repository.
///
/// See also:
///
/// * [FlutterPostSubmitFileComparator], another [FlutterGoldenFileComparator]
/// that tests golden images through Skia Gold.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterSkippingFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterSkippingFileComparator] that will skip tests that
/// are not in the right environment for golden file testing.
FlutterSkippingFileComparator(
super.basedir,
super.skiaClient,
this.reason, {
super.namePrefix,
});
/// Describes the reason for using the [FlutterSkippingFileComparator].
///
/// Cannot be null.
final String reason;
/// Creates a new [FlutterSkippingFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
static FlutterSkippingFileComparator fromDefaultComparator(
String reason, {
LocalFileComparator? defaultComparator,
String? namePrefix,
}) {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
const FileSystem fs = LocalFileSystem();
final Uri basedir = defaultComparator.basedir;
final SkiaGoldClient skiaClient = SkiaGoldClient(fs.directory(basedir));
return FlutterSkippingFileComparator(basedir, skiaClient, reason, namePrefix: namePrefix);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
// Ideally we would use markTestSkipped here but in some situations,
// comparators are called outside of tests.
// See also: https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
print('Skipping "$golden" test: $reason');
return true;
}
@override
Future<void> update(Uri golden, Uint8List imageBytes) async {}
/// Decides, based on the current environment, if this comparator should be
/// used.
///
/// If we are in a CI environment, LUCI or Cirrus, but are not using the other
/// comparators, we skip.
static bool isAvailableForEnvironment(Platform platform) {
return platform.environment.containsKey('SWARMING_TASK_ID')
// Some builds are still being run on Cirrus, we should skip these.
|| platform.environment.containsKey('CIRRUS_CI');
}
}
/// A [FlutterGoldenFileComparator] for testing golden images locally on your
/// current machine.
///
/// This comparator utilizes the [SkiaGoldClient] to request baseline images for
/// the given device under test for comparison. This comparator is initialized
/// when conditions for all other [FlutterGoldenFileComparators] have not been
/// met, see the `isAvailableForEnvironment` method for each one listed below.
///
/// The [FlutterLocalFileComparator] is intended to run on local machines and
/// serve as a smoke test during development. As such, it will not be able to
/// detect unintended changes on environments other than the currently executing
/// machine, until they are tested using the [FlutterPreSubmitFileComparator].
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterPostSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
/// dashboard.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterSkippingFileComparator], another
/// [FlutterGoldenFileComparator] that controls post-submit testing
/// conditions that do not execute golden file tests.
class FlutterLocalFileComparator extends FlutterGoldenFileComparator with LocalComparisonOutput {
/// Creates a [FlutterLocalFileComparator] that will test golden file
/// images against baselines requested from Flutter Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterLocalFileComparator(
super.basedir,
super.skiaClient, {
super.fs,
super.platform,
});
/// Creates a new [FlutterLocalFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
///
/// The [goldens], [defaultComparator], and [baseDirectory] parameters are
/// visible for testing purposes only.
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient? goldens,
LocalFileComparator? defaultComparator,
Directory? baseDirectory,
}) async {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
baseDirectory ??= FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
);
if(!baseDirectory.existsSync()) {
baseDirectory.createSync(recursive: true);
}
goldens ??= SkiaGoldClient(baseDirectory);
try {
// Check if we can reach Gold.
await goldens.getExpectationForTest('');
} on io.OSError catch (_) {
return FlutterSkippingFileComparator(
baseDirectory.uri,
goldens,
'OSError occurred, could not reach Gold. '
'Switching to FlutterSkippingGoldenFileComparator.',
);
} on io.SocketException catch (_) {
return FlutterSkippingFileComparator(
baseDirectory.uri,
goldens,
'SocketException occurred, could not reach Gold. '
'Switching to FlutterSkippingGoldenFileComparator.',
);
}
return FlutterLocalFileComparator(baseDirectory.uri, goldens);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
golden = _addPrefix(golden);
final String testName = skiaClient.cleanTestName(golden.path);
late String? testExpectation;
testExpectation = await skiaClient.getExpectationForTest(testName);
if (testExpectation == null || testExpectation.isEmpty) {
// There is no baseline for this test.
// Ideally we would use markTestSkipped here but in some situations,
// comparators are called outside of tests.
// See also: https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
print(
'No expectations provided by Skia Gold for test: $golden. '
'This may be a new test. If this is an unexpected result, check '
'https://flutter-gold.skia.org.\n'
'Validate image output found at $basedir'
);
update(golden, imageBytes);
return true;
}
ComparisonResult result;
final List<int> goldenBytes = await skiaClient.getImageBytes(testExpectation);
result = await GoldenFileComparator.compareLists(
imageBytes,
goldenBytes,
);
if (result.passed) {
return true;
}
final String error = await generateFailureOutput(result, golden, basedir);
throw FlutterError(error);
}
}

View file

@ -1,98 +0,0 @@
// 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.
// flutter_ignore_for_file: golden_tag (see analyze.dart)
import 'package:flutter_test/flutter_test.dart';
/// Similar to [matchesGoldenFile] but specialized for Flutter's own tests when
/// they are flaky.
///
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] - the [key] -
/// matches the golden image file identified by [goldenFile].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget. As such, you may choose to wrap a test
/// widget in a [RepaintBoundary] to specify a particular focus for the test.
///
/// The [goldenFile] may be either a [Uri] or a [String] representation of a URL.
///
/// Flaky golden file tests are always uploaded to Skia Gold for manual
/// inspection. This allows contributors to validate when a test is no longer
/// flaky by visiting https://flutter-gold.skia.org/list,
/// and clicking on the respective golden test name. The UI will show the
/// history of generated goldens over time. Each unique golden gets a unique
/// color. If the color is the same for all commits in the recent history, the
/// golden is likely no longer flaky and the standard [matchesGoldenFile] can be
/// used in the given test. If the color changes from commit to commit then it
/// is still flaky.
Future<void> expectFlakyGolden(Object key, String goldenFile) {
if (isBrowser) {
_setFlakyForWeb();
} else {
_setFlakyForIO();
}
return expectLater(key, matchesGoldenFile(goldenFile));
}
void _setFlakyForWeb() {
assert(
webGoldenComparator is FlakyGoldenMixin,
'expectFlakyGolden can only be used with a comparator with the FlakyGoldenMixin '
'but found ${webGoldenComparator.runtimeType}.'
);
(webGoldenComparator as FlakyGoldenMixin).enableFlakyMode();
}
void _setFlakyForIO() {
assert(
goldenFileComparator is FlakyGoldenMixin,
'expectFlakyGolden can only be used with a comparator with the FlakyGoldenMixin '
'but found ${goldenFileComparator.runtimeType}.'
);
(goldenFileComparator as FlakyGoldenMixin).enableFlakyMode();
}
/// Allows flaky test handling for the Flutter framework.
///
/// Mixed in with the [FlutterGoldenFileComparator] and
/// [_FlutterWebGoldenComparator].
mixin FlakyGoldenMixin {
/// Whether this comparator allows flaky goldens.
///
/// If set to true, concrete implementations of this class are expected to
/// generate the golden and submit it for review, but not fail the test.
bool _isFlakyModeEnabled = false;
/// Puts this comparator into flaky comparison mode.
///
/// After calling this method the next invocation of [compare] will allow
/// incorrect golden to pass the check.
///
/// Concrete implementations of [compare] must call [getAndResetFlakyMode] so
/// that subsequent tests can run in non-flaky mode. If a subsequent test
/// needs to run in a flaky mode, it must call this method again.
void enableFlakyMode() {
assert(
!_isFlakyModeEnabled,
'Test is already marked as flaky. Call `getAndResetFlakyMode` to reset the '
'flag before calling this method again.',
);
_isFlakyModeEnabled = true;
}
/// Returns whether flaky comparison mode was enabled via [enableFlakyMode],
/// and if it was, resets the comparator back to non-flaky mode.
bool getAndResetFlakyMode() {
if (!_isFlakyModeEnabled) {
// Not in flaky mode. Nothing to do.
return false;
}
// In flaky mode. Reset it and return true.
_isFlakyModeEnabled = false;
return true;
}
}

View file

@ -1,611 +0,0 @@
// 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' show FutureOr;
import 'dart:convert' show jsonEncode;
import 'dart:io' as io show File, OSError, SocketException, stdout;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_goldens_client/skia_client.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform/platform.dart';
import 'flaky_goldens.dart';
export 'package:flutter_goldens_client/skia_client.dart';
// If you are here trying to figure out how to use golden files in the Flutter
// repo itself, consider reading this wiki page:
// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package%3Aflutter
const String _kFlutterRootKey = 'FLUTTER_ROOT';
/// Main method that can be used in a `flutter_test_config.dart` file to set
/// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that
/// works for the current test. _Which_ FlutterGoldenFileComparator is
/// instantiated is based on the current testing environment.
///
/// When set, the `namePrefix` is prepended to the names of all gold images.
Future<void> testExecutable(FutureOr<void> Function() testMain, {String? namePrefix}) async {
const Platform platform = LocalPlatform();
if (FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = await FlutterPostSubmitFileComparator.fromDefaultComparator(platform, namePrefix: namePrefix);
} else if (FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = await FlutterPreSubmitFileComparator.fromDefaultComparator(platform, namePrefix: namePrefix);
} else if (FlutterSkippingFileComparator.isAvailableForEnvironment(platform)) {
goldenFileComparator = FlutterSkippingFileComparator.fromDefaultComparator(
'Golden file testing is not executed on Cirrus, or LUCI environments outside of flutter/flutter.',
namePrefix: namePrefix
);
} else {
goldenFileComparator = await FlutterLocalFileComparator.fromDefaultComparator(platform);
}
await testMain();
}
/// Processes golden check commands sent from the browser process.
///
/// When running browser tests, goldens are not generated within the app itself
/// due to browser restrictions. Instead, when a test calls [expectFlakyGolden]
/// the browser sends a [command] to a host process. This function handles the
/// command.
///
/// This custom command handler is used for Flutter's own goldens. It
/// understands the "isFlaky" property, a boolean encoded in the command as a
/// custom command property. If true, uses a golden comparator that submits the
/// golden, but does not fail the test, allowing manual inspection in the Skia
/// Gold UI and verification of fixes.
///
/// See also:
/// * [FlakyWebGoldenComparator], which implements custom browser-side logic.
Future<void> processBrowserCommand(dynamic command) async {
if (command is Map<String, dynamic>) {
final io.File imageFile = io.File(command['imageFile'] as String);
final Uri goldenKey = Uri.parse(command['key'] as String);
final bool update = command['update'] as bool;
final Map<String, dynamic> customProperties = (command['customProperties'] as Map<String, dynamic>?) ?? const <String, dynamic>{};
final bool isFlaky = (customProperties['isFlaky'] as bool?) ?? false;
final Uint8List bytes = await io.File(imageFile.path).readAsBytes();
if (update) {
await goldenFileComparator.update(goldenKey, bytes);
io.stdout.writeln(jsonEncode(<String, dynamic>{'success': true}));
} else {
try {
assert(
goldenFileComparator is FlutterGoldenFileComparator,
'matchesFlutterGolden can only be used with FlutterGoldenFileComparator '
'but found ${goldenFileComparator.runtimeType}.'
);
if (isFlaky) {
(goldenFileComparator as FlutterGoldenFileComparator).enableFlakyMode();
}
final bool success = await goldenFileComparator.compare(bytes, goldenKey);
io.stdout.writeln(jsonEncode(<String, dynamic>{'success': success}));
} on Exception catch (exception) {
io.stdout.writeln(jsonEncode(<String, dynamic>{'success': false, 'message': '$exception'}));
}
}
} else {
io.stdout.writeln('object type is not right');
}
}
/// Abstract base class golden file comparator specific to the `flutter/flutter`
/// repository.
///
/// Golden file testing for the `flutter/flutter` repository is handled by three
/// different [FlutterGoldenFileComparator]s, depending on the current testing
/// environment.
///
/// * The [FlutterPostSubmitFileComparator] is utilized during post-submit
/// testing, after a pull request has landed on the master branch. This
/// comparator uses the [SkiaGoldClient] and the `goldctl` tool to upload
/// tests to the [Flutter Gold dashboard](https://flutter-gold.skia.org).
/// Flutter Gold manages the master golden files for the `flutter/flutter`
/// repository.
///
/// * The [FlutterPreSubmitFileComparator] is utilized in pre-submit testing,
/// before a pull request lands on the master branch. This
/// comparator uses the [SkiaGoldClient] to execute tryjobs, allowing
/// contributors to view and check in visual differences before landing the
/// change.
///
/// * The [FlutterLocalFileComparator] is used for local development testing.
/// This comparator will use the [SkiaGoldClient] to request baseline images
/// from [Flutter Gold](https://flutter-gold.skia.org) and manually compare
/// pixels. If a difference is detected, this comparator will
/// generate failure output illustrating the found difference. If a baseline
/// is not found for a given test image, it will consider it a new test and
/// output the new image for verification.
///
/// The [FlutterSkippingFileComparator] is utilized to skip tests outside
/// of the appropriate environments described above. Currently, some Luci
/// environments do not execute golden file testing, and as such do not require
/// a comparator. This comparator is also used when an internet connection is
/// unavailable.
abstract class FlutterGoldenFileComparator extends GoldenFileComparator with FlakyGoldenMixin {
/// Creates a [FlutterGoldenFileComparator] that will resolve golden file
/// URIs relative to the specified [basedir], and retrieve golden baselines
/// using the [skiaClient]. The [basedir] is used for writing and accessing
/// information and files for interacting with the [skiaClient]. When testing
/// locally, the [basedir] will also contain any diffs from failed tests, or
/// goldens generated from newly introduced tests.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
@visibleForTesting
FlutterGoldenFileComparator(
this.basedir,
this.skiaClient, {
this.fs = const LocalFileSystem(),
this.platform = const LocalPlatform(),
this.namePrefix,
});
/// The directory to which golden file URIs will be resolved in [compare] and
/// [update], cannot be null.
final Uri basedir;
/// A client for uploading image tests and making baseline requests to the
/// Flutter Gold Dashboard, cannot be null.
final SkiaGoldClient skiaClient;
/// The file system used to perform file access.
@visibleForTesting
final FileSystem fs;
/// A wrapper for the [dart:io.Platform] API.
@visibleForTesting
final Platform platform;
/// The prefix that is added to all golden names.
final String? namePrefix;
@override
Future<void> update(Uri golden, Uint8List imageBytes) async {
final File goldenFile = getGoldenFile(golden);
await goldenFile.parent.create(recursive: true);
await goldenFile.writeAsBytes(imageBytes, flush: true);
}
@override
Uri getTestUri(Uri key, int? version) => key;
/// Calculate the appropriate basedir for the current test context.
///
/// The optional [suffix] argument is used by the
/// [FlutterPostSubmitFileComparator] and the [FlutterPreSubmitFileComparator].
/// These [FlutterGoldenFileComparators] randomize their base directories to
/// maintain thread safety while using the `goldctl` tool.
@protected
@visibleForTesting
static Directory getBaseDirectory(
LocalFileComparator defaultComparator,
Platform platform, {
String? suffix,
}) {
const FileSystem fs = LocalFileSystem();
final Directory flutterRoot = fs.directory(platform.environment[_kFlutterRootKey]);
Directory comparisonRoot;
if (suffix != null) {
comparisonRoot = fs.systemTempDirectory.createTempSync(suffix);
} else {
comparisonRoot = flutterRoot.childDirectory(
fs.path.join(
'bin',
'cache',
'pkg',
'skia_goldens',
)
);
}
final Directory testDirectory = fs.directory(defaultComparator.basedir);
final String testDirectoryRelativePath = fs.path.relative(
testDirectory.path,
from: flutterRoot.path,
);
return comparisonRoot.childDirectory(testDirectoryRelativePath);
}
/// Returns the golden [File] identified by the given [Uri].
@protected
File getGoldenFile(Uri uri) {
final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path);
return goldenFile;
}
/// Prepends the golden URL with the library name that encloses the current
/// test.
Uri _addPrefix(Uri golden) {
// Ensure the Uri ends in .png as the SkiaClient expects
assert(
golden.toString().split('.').last == 'png',
'Golden files in the Flutter framework must end with the file extension '
'.png.'
);
return Uri.parse(<String>[
if (namePrefix != null)
namePrefix!,
basedir.pathSegments[basedir.pathSegments.length - 2],
golden.toString(),
].join('.'));
}
}
/// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold in
/// post-submit.
///
/// For testing across all platforms, the [SkiaGoldClient] is used to upload
/// images for framework-related golden tests and process results.
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterPostSubmitFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterPostSubmitFileComparator] that will test golden file
/// images against Skia Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterPostSubmitFileComparator(
super.basedir,
super.skiaClient, {
super.fs,
super.platform,
super.namePrefix,
});
/// Creates a new [FlutterPostSubmitFileComparator] that mirrors the relative
/// path resolution of the default [goldenFileComparator].
///
/// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only.
static Future<FlutterPostSubmitFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient? goldens,
LocalFileComparator? defaultComparator,
String? namePrefix,
}) async {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
suffix: 'flutter_goldens_postsubmit.',
);
baseDirectory.createSync(recursive: true);
goldens ??= SkiaGoldClient(baseDirectory);
await goldens.auth();
return FlutterPostSubmitFileComparator(baseDirectory.uri, goldens, namePrefix: namePrefix);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
final bool isFlaky = getAndResetFlakyMode();
await skiaClient.imgtestInit(isFlaky: isFlaky);
golden = _addPrefix(golden);
await update(golden, imageBytes);
final File goldenFile = getGoldenFile(golden);
return skiaClient.imgtestAdd(golden.path, goldenFile, isFlaky: isFlaky);
}
/// Decides based on the current environment if goldens tests should be
/// executed through Skia Gold.
static bool isAvailableForEnvironment(Platform platform) {
final bool luciPostSubmit = platform.environment.containsKey('SWARMING_TASK_ID')
&& platform.environment.containsKey('GOLDCTL')
// Luci tryjob environments contain this value to inform the [FlutterPreSubmitComparator].
&& !platform.environment.containsKey('GOLD_TRYJOB');
return luciPostSubmit;
}
}
/// A [FlutterGoldenFileComparator] for testing golden images before changes are
/// merged into the master branch. The comparator executes tryjobs using the
/// [SkiaGoldClient].
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterPostSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
/// dashboard in post-submit.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterPreSubmitFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterPreSubmitFileComparator] that will test golden file
/// images against baselines requested from Flutter Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterPreSubmitFileComparator(
super.basedir,
super.skiaClient, {
super.fs,
super.platform,
super.namePrefix,
});
/// Creates a new [FlutterPreSubmitFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
///
/// The [goldens] and [defaultComparator] parameters are visible for testing
/// purposes only.
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient? goldens,
LocalFileComparator? defaultComparator,
Directory? testBasedir,
String? namePrefix,
}) async {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
final Directory baseDirectory = testBasedir ?? FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
suffix: 'flutter_goldens_presubmit.',
);
if (!baseDirectory.existsSync()) {
baseDirectory.createSync(recursive: true);
}
goldens ??= SkiaGoldClient(baseDirectory);
await goldens.auth();
return FlutterPreSubmitFileComparator(
baseDirectory.uri,
goldens, platform: platform,
namePrefix: namePrefix,
);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
final bool isFlaky = getAndResetFlakyMode();
await skiaClient.tryjobInit(isFlaky: isFlaky);
golden = _addPrefix(golden);
await update(golden, imageBytes);
final File goldenFile = getGoldenFile(golden);
await skiaClient.tryjobAdd(golden.path, goldenFile, isFlaky: isFlaky);
// This will always return true since golden file test failures are managed
// in pre-submit checks by the flutter-gold status check.
return true;
}
/// Decides based on the current environment if goldens tests should be
/// executed as pre-submit tests with Skia Gold.
static bool isAvailableForEnvironment(Platform platform) {
final bool luciPreSubmit = platform.environment.containsKey('SWARMING_TASK_ID')
&& platform.environment.containsKey('GOLDCTL')
&& platform.environment.containsKey('GOLD_TRYJOB');
return luciPreSubmit;
}
}
/// A [FlutterGoldenFileComparator] for testing conditions that do not execute
/// golden file tests.
///
/// Currently, this comparator is used on Cirrus, or in Luci environments when executing tests
/// outside of the flutter/flutter repository.
///
/// See also:
///
/// * [FlutterPostSubmitFileComparator], another [FlutterGoldenFileComparator]
/// that tests golden images through Skia Gold.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterLocalFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images locally on your
/// current machine.
class FlutterSkippingFileComparator extends FlutterGoldenFileComparator {
/// Creates a [FlutterSkippingFileComparator] that will skip tests that
/// are not in the right environment for golden file testing.
FlutterSkippingFileComparator(
super.basedir,
super.skiaClient,
this.reason, {
super.namePrefix,
});
/// Describes the reason for using the [FlutterSkippingFileComparator].
///
/// Cannot be null.
final String reason;
/// Creates a new [FlutterSkippingFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
static FlutterSkippingFileComparator fromDefaultComparator(
String reason, {
LocalFileComparator? defaultComparator,
String? namePrefix,
bool isFlaky = false,
}) {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
const FileSystem fs = LocalFileSystem();
final Uri basedir = defaultComparator.basedir;
final SkiaGoldClient skiaClient = SkiaGoldClient(fs.directory(basedir));
return FlutterSkippingFileComparator(basedir, skiaClient, reason, namePrefix: namePrefix);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
// Ideally we would use markTestSkipped here but in some situations,
// comparators are called outside of tests.
// See also: https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
getAndResetFlakyMode();
// ignore: avoid_print
print('Skipping "$golden" test: $reason');
return true;
}
@override
Future<void> update(Uri golden, Uint8List imageBytes) async {}
/// Decides, based on the current environment, if this comparator should be
/// used.
///
/// If we are in a CI environment, LUCI or Cirrus, but are not using the other
/// comparators, we skip.
static bool isAvailableForEnvironment(Platform platform) {
return platform.environment.containsKey('SWARMING_TASK_ID')
// Some builds are still being run on Cirrus, we should skip these.
|| platform.environment.containsKey('CIRRUS_CI');
}
}
/// A [FlutterGoldenFileComparator] for testing golden images locally on your
/// current machine.
///
/// This comparator utilizes the [SkiaGoldClient] to request baseline images for
/// the given device under test for comparison. This comparator is initialized
/// when conditions for all other [FlutterGoldenFileComparators] have not been
/// met, see the `isAvailableForEnvironment` method for each one listed below.
///
/// The [FlutterLocalFileComparator] is intended to run on local machines and
/// serve as a smoke test during development. As such, it will not be able to
/// detect unintended changes on environments other than the currently executing
/// machine, until they are tested using the [FlutterPreSubmitFileComparator].
///
/// See also:
///
/// * [GoldenFileComparator], the abstract class that
/// [FlutterGoldenFileComparator] implements.
/// * [FlutterPostSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
/// dashboard.
/// * [FlutterPreSubmitFileComparator], another
/// [FlutterGoldenFileComparator] that tests golden images before changes are
/// merged into the master branch.
/// * [FlutterSkippingFileComparator], another
/// [FlutterGoldenFileComparator] that controls post-submit testing
/// conditions that do not execute golden file tests.
class FlutterLocalFileComparator extends FlutterGoldenFileComparator with LocalComparisonOutput {
/// Creates a [FlutterLocalFileComparator] that will test golden file
/// images against baselines requested from Flutter Gold.
///
/// The [fs] and [platform] parameters are useful in tests, where the default
/// file system and platform can be replaced by mock instances.
FlutterLocalFileComparator(
super.basedir,
super.skiaClient, {
super.fs,
super.platform,
});
/// Creates a new [FlutterLocalFileComparator] that mirrors the
/// relative path resolution of the default [goldenFileComparator].
///
/// The [goldens], [defaultComparator], and [baseDirectory] parameters are
/// visible for testing purposes only.
static Future<FlutterGoldenFileComparator> fromDefaultComparator(
final Platform platform, {
SkiaGoldClient? goldens,
LocalFileComparator? defaultComparator,
Directory? baseDirectory,
}) async {
defaultComparator ??= goldenFileComparator as LocalFileComparator;
baseDirectory ??= FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
);
if(!baseDirectory.existsSync()) {
baseDirectory.createSync(recursive: true);
}
goldens ??= SkiaGoldClient(baseDirectory);
try {
// Check if we can reach Gold.
await goldens.getExpectationForTest('');
} on io.OSError catch (_) {
return FlutterSkippingFileComparator(
baseDirectory.uri,
goldens,
'OSError occurred, could not reach Gold. '
'Switching to FlutterSkippingGoldenFileComparator.',
);
} on io.SocketException catch (_) {
return FlutterSkippingFileComparator(
baseDirectory.uri,
goldens,
'SocketException occurred, could not reach Gold. '
'Switching to FlutterSkippingGoldenFileComparator.',
);
}
return FlutterLocalFileComparator(baseDirectory.uri, goldens);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
final bool isFlaky = getAndResetFlakyMode();
golden = _addPrefix(golden);
final String testName = skiaClient.cleanTestName(golden.path);
late String? testExpectation;
testExpectation = await skiaClient.getExpectationForTest(testName);
if (testExpectation == null || testExpectation.isEmpty) {
// There is no baseline for this test.
// Ideally we would use markTestSkipped here but in some situations,
// comparators are called outside of tests.
// See also: https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
print(
'No expectations provided by Skia Gold for test: $golden. '
'This may be a new test. If this is an unexpected result, check '
'https://flutter-gold.skia.org.\n'
'Validate image output found at $basedir'
);
update(golden, imageBytes);
return true;
}
ComparisonResult result;
final List<int> goldenBytes = await skiaClient.getImageBytes(testExpectation);
result = await GoldenFileComparator.compareLists(
imageBytes,
goldenBytes,
);
if (result.passed) {
return true;
}
final String error = await generateFailureOutput(result, golden, basedir);
if (!isFlaky) {
throw FlutterError(error);
} else {
// The test was marked as flaky. Do not fail the test.
// TODO(yjbanov): there's no way to communicate warnings to the caller https://github.com/flutter/flutter/issues/91285
// ignore: avoid_print
print('Golden $golden is marked as flaky and will not fail the test.');
// ignore: avoid_print
print(error);
}
return true;
}
}

View file

@ -1,71 +0,0 @@
// 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' show FutureOr;
import 'dart:convert' show json;
import 'dart:html' as html;
import 'package:flutter_test/flutter_test.dart';
import 'flaky_goldens.dart';
export 'package:flutter_goldens_client/skia_client.dart';
/// Wraps a web test, supplying a custom comparator that supports flaky goldens.
Future<void> testExecutable(FutureOr<void> Function() testMain, {String? namePrefix}) async {
webGoldenComparator = FlutterWebGoldenComparator(webTestUri);
await testMain();
}
/// See the io implementation of this function.
Future<void> processBrowserCommand(dynamic command) async {
throw UnimplementedError('processBrowserCommand is not used inside the browser');
}
/// Same as [DefaultWebGoldenComparator] but supports flaky golden checks.
class FlutterWebGoldenComparator extends WebGoldenComparator with FlakyGoldenMixin {
/// Creates a new [FlutterWebGoldenComparator] for the specified [testUri].
///
/// Golden file keys will be interpreted as file paths relative to the
/// directory in which [testUri] resides.
///
/// The [testUri] URL must represent a file.
FlutterWebGoldenComparator(this.testUri);
/// The test file currently being executed.
///
/// Golden file keys will be interpreted as file paths relative to the
/// directory in which this file resides.
Uri testUri;
@override
Future<bool> compare(double width, double height, Uri golden) async {
final bool isFlaky = getAndResetFlakyMode();
final String key = golden.toString();
final html.HttpRequest request = await html.HttpRequest.request(
'flutter_goldens',
method: 'POST',
sendData: json.encode(<String, Object>{
'testUri': testUri.toString(),
'key': key,
'width': width.round(),
'height': height.round(),
'customProperties': <String, dynamic>{
'isFlaky': isFlaky,
},
}),
);
final String response = request.response as String;
if (response == 'true') {
return true;
}
fail(response);
}
@override
Future<void> update(double width, double height, Uri golden) async {
// Update is handled on the server side, just use the same logic here
await compare(width, height, golden);
}
}

View file

@ -1,52 +0,0 @@
// 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.
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
// flutter_ignore_for_file: golden_tag (see analyze.dart)
import 'package:flutter_goldens/flutter_goldens.dart';
import 'package:flutter_test/flutter_test.dart';
import 'utils/fakes.dart';
void main() {
test('Sets flaky flag', () {
final FakeFlakyLocalFileComparator comparator = FakeFlakyLocalFileComparator();
// Is not flaky
expect(comparator.getAndResetFlakyMode(), isFalse);
comparator.enableFlakyMode();
// Flaky was set
expect(comparator.getAndResetFlakyMode(), isTrue);
// Flaky was unset
expect(comparator.getAndResetFlakyMode(), isFalse);
});
test('Asserts when comparator is missing mixin', (){
final GoldenFileComparator oldComparator = goldenFileComparator;
goldenFileComparator = FakeLocalFileComparator();
expect(
() {
expect(
expectFlakyGolden(<int>[0, 1, 2, 3], 'golden_file.png'),
throwsAssertionError,
);
},
throwsA(
isA<AssertionError>().having((AssertionError error) => error.toString(),
'description', contains('FlakyGoldenMixin')),
),
);
goldenFileComparator = oldComparator;
});
test('top level function sets flag', () {
final GoldenFileComparator oldComparator = goldenFileComparator;
goldenFileComparator = FakeFlakyLocalFileComparator();
expectFlakyGolden(<int>[0, 1, 2, 3], 'golden_file.png');
final bool wasFlaky = (goldenFileComparator as FakeFlakyLocalFileComparator).getAndResetFlakyMode();
expect(wasFlaky, isTrue);
goldenFileComparator = oldComparator;
});
}

View file

@ -4,22 +4,34 @@
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
import 'dart:convert';
import 'dart:io' hide Directory;
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_goldens/src/flutter_goldens_io.dart';
import 'package:flutter_goldens/flutter_goldens.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'utils/fakes.dart';
import 'json_templates.dart';
const String _kFlutterRoot = '/flutter';
// 1x1 transparent pixel
const List<int> _kTestPngBytes = <int>[
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84,
120, 1, 99, 97, 0, 2, 0, 0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69,
78, 68, 174, 66, 96, 130,
];
void main() {
late MemoryFileSystem fs;
late FakePlatform platform;
late FakeProcessManager process;
late FakeHttpClient fakeHttpClient;
setUp(() {
fs = MemoryFileSystem();
@ -27,9 +39,567 @@ void main() {
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
operatingSystem: 'macos'
);
process = FakeProcessManager();
fakeHttpClient = FakeHttpClient();
fs.directory(_kFlutterRoot).createSync(recursive: true);
});
group('SkiaGoldClient', () {
late SkiaGoldClient skiaClient;
late Directory workDirectory;
setUp(() {
workDirectory = fs.directory('/workDirectory')
..createSync(recursive: true);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
});
test('web HTML test', () async {
platform = FakePlatform(
environment: <String, String>{
'GOLDCTL': 'goldctl',
'FLUTTER_ROOT': _kFlutterRoot,
'FLUTTER_TEST_BROWSER': 'Chrome',
'FLUTTER_WEB_RENDERER': 'html',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
'--add-test-optional-key', 'image_matching_algorithm:fuzzy',
'--add-test-optional-key', 'fuzzy_max_different_pixels:20',
'--add-test-optional-key', 'fuzzy_pixel_delta_threshold:4',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 0, '', '');
expect(
await skiaClient.imgtestAdd('golden_file_test.png', goldenFile),
isTrue,
);
});
test('web CanvasKit test', () async {
platform = FakePlatform(
environment: <String, String>{
'GOLDCTL': 'goldctl',
'FLUTTER_ROOT': _kFlutterRoot,
'FLUTTER_TEST_BROWSER': 'Chrome',
'FLUTTER_WEB_RENDERER': 'canvaskit',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 0, '', '');
expect(
await skiaClient.imgtestAdd('golden_file_test.png', goldenFile),
isTrue,
);
});
test('auth performs minimal work if already authorized', () async {
final File authFile = fs.file('/workDirectory/temp/auth_opt.json')
..createSync(recursive: true);
authFile.writeAsStringSync(authTemplate());
process.fallbackProcessResult = ProcessResult(123, 0, '', '');
await skiaClient.auth();
expect(process.workingDirectories, isEmpty);
});
test('gsutil is checked when authorization file is present', () async {
final File authFile = fs.file('/workDirectory/temp/auth_opt.json')
..createSync(recursive: true);
authFile.writeAsStringSync(authTemplate(gsutil: true));
expect(
await skiaClient.clientIsAuthorized(),
isFalse,
);
});
test('throws for error state from auth', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLD_SERVICE_ACCOUNT' : 'Service Account',
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.auth(),
throwsException,
);
});
test('throws for error state from init', () {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation gitInvocation = RunInvocation(
<String>['git', 'rev-parse', 'HEAD'],
'/flutter',
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'init',
'--instance', 'flutter',
'--work-dir', '/workDirectory/temp',
'--commit', '12345678',
'--keys-file', '/workDirectory/keys.json',
'--failure-file', '/workDirectory/failures.json',
'--passfail',
],
null,
);
process.processResults[gitInvocation] = ProcessResult(12345678, 0, '12345678', '');
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.imgtestInit(),
throwsException,
);
});
test('Only calls init once', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation gitInvocation = RunInvocation(
<String>['git', 'rev-parse', 'HEAD'],
'/flutter',
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'init',
'--instance', 'flutter',
'--work-dir', '/workDirectory/temp',
'--commit', '1234',
'--keys-file', '/workDirectory/keys.json',
'--failure-file', '/workDirectory/failures.json',
'--passfail',
],
null,
);
process.processResults[gitInvocation] = ProcessResult(1234, 0, '1234', '');
process.processResults[goldctlInvocation] = ProcessResult(5678, 0, '5678', '');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
// First call
await skiaClient.imgtestInit();
// Remove fake process result.
// If the init call is executed again, the fallback process will throw.
process.processResults.remove(goldctlInvocation);
// Second call
await skiaClient.imgtestInit();
});
test('Only calls tryjob init once', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation gitInvocation = RunInvocation(
<String>['git', 'rev-parse', 'HEAD'],
'/flutter',
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'init',
'--instance', 'flutter',
'--work-dir', '/workDirectory/temp',
'--commit', '1234',
'--keys-file', '/workDirectory/keys.json',
'--failure-file', '/workDirectory/failures.json',
'--passfail',
'--crs', 'github',
'--patchset_id', '1234',
'--changelist', '49815',
'--cis', 'buildbucket',
'--jobid', '8885996262141582672',
],
null,
);
process.processResults[gitInvocation] = ProcessResult(1234, 0, '1234', '');
process.processResults[goldctlInvocation] = ProcessResult(5678, 0, '5678', '');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
// First call
await skiaClient.tryjobInit();
// Remove fake process result.
// If the init call is executed again, the fallback process will throw.
process.processResults.remove(goldctlInvocation);
// Second call
await skiaClient.tryjobInit();
});
test('throws for error state from imgtestAdd', () {
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.imgtestAdd('golden_file_test', goldenFile),
throwsException,
);
});
test('correctly inits tryjob for luci', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final List<String> ciArguments = skiaClient.getCIArguments();
expect(
ciArguments,
equals(
<String>[
'--changelist', '49815',
'--cis', 'buildbucket',
'--jobid', '8885996262141582672',
],
),
);
});
test('Creates traceID correctly', () async {
String traceID;
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
},
operatingSystem: 'linux'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
traceID = skiaClient.getTraceID('flutter.golden.1');
expect(
traceID,
equals('ae18c7a6aa48e0685525dfe8fdf79003'),
);
// Browser
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
'FLUTTER_TEST_BROWSER' : 'chrome',
},
operatingSystem: 'linux'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
traceID = skiaClient.getTraceID('flutter.golden.1');
expect(
traceID,
equals('e9d5c296c48e7126808520e9cc191243'),
);
// Locally - should defer to luci traceID
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
traceID = skiaClient.getTraceID('flutter.golden.1');
expect(
traceID,
equals('9968695b9ae78cdb77cbb2be621ca2d6'),
);
});
test('throws for error state from imgtestAdd', () {
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.imgtestAdd('golden_file_test', goldenFile),
throwsA(
isA<SkiaException>().having((SkiaException error) => error.message,
'message',
contains('result-state.json'),
),
),
);
});
test('throws for error state from tryjobAdd', () {
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.tryjobAdd('golden_file_test', goldenFile),
throwsA(
isA<SkiaException>().having((SkiaException error) => error.message,
'message',
contains('result-state.json'),
),
),
);
});
group('Request Handling', () {
const String expectation = '55109a4bed52acc780530f7a9aeff6c0';
test('image bytes are processed properly', () async {
final Uri imageUrl = Uri.parse(
'https://flutter-gold.skia.org/img/images/$expectation.png'
);
final FakeHttpClientRequest fakeImageRequest = FakeHttpClientRequest();
final FakeHttpImageResponse fakeImageResponse = FakeHttpImageResponse(
imageResponseTemplate()
);
fakeHttpClient.request = fakeImageRequest;
fakeImageRequest.response = fakeImageResponse;
final List<int> masterBytes = await skiaClient.getImageBytes(expectation);
expect(fakeHttpClient.lastUri, imageUrl);
expect(masterBytes, equals(_kTestPngBytes));
});
});
});
group('FlutterGoldenFileComparator', () {
late FlutterGoldenFileComparator comparator;
@ -80,7 +650,7 @@ void main() {
namePrefix: namePrefix,
);
await comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse(fileName),
);
expect(fakeSkiaClient.testNames.single, '$namePrefix.$libraryName.$fileName');
@ -105,7 +675,7 @@ void main() {
await expectLater(
() async {
return comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1'),
);
},
@ -124,26 +694,12 @@ void main() {
test('calls init during compare', () {
expect(fakeSkiaClient.initCalls, 0);
comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
);
expect(fakeSkiaClient.initCalls, 1);
});
test('Passes on flaky flag to client, resets after comparing', () {
// Not flaky
expect(comparator.getAndResetFlakyMode(), isFalse);
comparator.enableFlakyMode();
expect(fakeSkiaClient.calledWithFlaky, 0);
comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
);
expect(fakeSkiaClient.calledWithFlaky, 1);
// Flaky flag was reset during compare.
expect(comparator.getAndResetFlakyMode(), isFalse);
});
test('does not call init in during construction', () {
expect(fakeSkiaClient.initCalls, 0);
FlutterPostSubmitFileComparator.fromDefaultComparator(
@ -237,7 +793,7 @@ void main() {
await expectLater(
() async {
return comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1'),
);
},
@ -256,27 +812,12 @@ void main() {
test('calls init during compare', () {
expect(fakeSkiaClient.tryInitCalls, 0);
comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
);
expect(fakeSkiaClient.tryInitCalls, 1);
});
test('Passes on flaky flag to client, resets after comparing', () {
// Not flaky
expect(comparator.getAndResetFlakyMode(), isFalse);
comparator.enableFlakyMode();
expect(fakeSkiaClient.calledWithFlaky, 0);
comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
);
// Init & add were called with flaky set.
expect(fakeSkiaClient.calledWithFlaky, 1);
// Flaky flag was reset during compare.
expect(comparator.getAndResetFlakyMode(), isFalse);
});
test('does not call init in during construction', () {
expect(fakeSkiaClient.tryInitCalls, 0);
FlutterPostSubmitFileComparator.fromDefaultComparator(
@ -366,19 +907,6 @@ void main() {
});
group('Skipping', () {
test('Resets flaky flag after comparing', () {
// Not flaky
expect(comparator.getAndResetFlakyMode(), isFalse);
// Set flaky
comparator.enableFlakyMode();
comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
);
// Flaky flag was reset during compare.
expect(comparator.getAndResetFlakyMode(), isFalse);
});
group('correctly determines testing environment', () {
test('returns true on Cirrus builds', () {
platform = FakePlatform(
@ -443,7 +971,7 @@ void main() {
const String hash = '55109a4bed52acc780530f7a9aeff6c0';
fakeSkiaClient.expectationForTestValues['flutter.golden_test.1'] = hash;
fakeSkiaClient.imageBytesValues[hash] =kTestPngBytes;
fakeSkiaClient.imageBytesValues[hash] =_kTestPngBytes;
fakeSkiaClient.cleanTestNameValues['library.flutter.golden_test.1.png'] = 'flutter.golden_test.1';
});
@ -451,7 +979,7 @@ void main() {
await expectLater(
() async {
return comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1'),
);
},
@ -470,25 +998,13 @@ void main() {
test('passes when bytes match', () async {
expect(
await comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uint8List.fromList(_kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
),
isTrue,
);
});
test('Passes when flaky', () {
// Not flaky
expect(comparator.getAndResetFlakyMode(), isFalse);
comparator.enableFlakyMode();
comparator.compare(
Uint8List.fromList(kTestPngBytes),
Uri.parse('flutter.golden_test.1.png'),
);
// Flaky flag was reset during compare.
expect(comparator.getAndResetFlakyMode(), isFalse);
});
test('returns FlutterSkippingGoldenFileComparator when network connection is unavailable', () async {
final FakeDirectory fakeDirectory = FakeDirectory();
fakeDirectory.existsSyncValue = true;
@ -517,3 +1033,156 @@ void main() {
});
});
}
@immutable
class RunInvocation {
const RunInvocation(this.command, this.workingDirectory);
final List<String> command;
final String? workingDirectory;
@override
int get hashCode => Object.hash(Object.hashAll(command), workingDirectory);
bool _commandEquals(List<String> other) {
if (other == command) {
return true;
}
if (other.length != command.length) {
return false;
}
for (int index = 0; index < other.length; index += 1) {
if (other[index] != command[index]) {
return false;
}
}
return true;
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is RunInvocation
&& _commandEquals(other.command)
&& other.workingDirectory == workingDirectory;
}
@override
String toString() => '$command ($workingDirectory)';
}
class FakeProcessManager extends Fake implements ProcessManager {
Map<RunInvocation, ProcessResult> processResults = <RunInvocation, ProcessResult>{};
/// Used if [processResults] does not contain a matching invocation.
ProcessResult? fallbackProcessResult;
final List<String?> workingDirectories = <String?>[];
@override
Future<ProcessResult> run(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) async {
workingDirectories.add(workingDirectory);
final ProcessResult? result = processResults[RunInvocation(command.cast<String>(), workingDirectory)];
if (result == null && fallbackProcessResult == null) {
printOnFailure('ProcessManager.run was called with $command ($workingDirectory) unexpectedly - $processResults.');
fail('See above.');
}
return result ?? fallbackProcessResult!;
}
}
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
class FakeSkiaGoldClient extends Fake implements SkiaGoldClient {
Map<String, String> expectationForTestValues = <String, String>{};
Exception? getExpectationForTestThrowable;
@override
Future<String> getExpectationForTest(String testName) async {
if (getExpectationForTestThrowable != null) {
throw getExpectationForTestThrowable!;
}
return expectationForTestValues[testName] ?? '';
}
@override
Future<void> auth() async {}
final List<String> testNames = <String>[];
int initCalls = 0;
@override
Future<void> imgtestInit() async => initCalls += 1;
@override
Future<bool> imgtestAdd(String testName, File goldenFile) async {
testNames.add(testName);
return true;
}
int tryInitCalls = 0;
@override
Future<void> tryjobInit() async => tryInitCalls += 1;
@override
Future<bool> tryjobAdd(String testName, File goldenFile) async => true;
Map<String, List<int>> imageBytesValues = <String, List<int>>{};
@override
Future<List<int>> getImageBytes(String imageHash) async => imageBytesValues[imageHash]!;
Map<String, String> cleanTestNameValues = <String, String>{};
@override
String cleanTestName(String fileName) => cleanTestNameValues[fileName] ?? '';
}
class FakeLocalFileComparator extends Fake implements LocalFileComparator {
@override
late Uri basedir;
}
class FakeDirectory extends Fake implements Directory {
late bool existsSyncValue;
@override
bool existsSync() => existsSyncValue;
@override
late Uri uri;
}
class FakeHttpClient extends Fake implements HttpClient {
late Uri lastUri;
late FakeHttpClientRequest request;
@override
Future<HttpClientRequest> getUrl(Uri url) async {
lastUri = url;
return request;
}
}
class FakeHttpClientRequest extends Fake implements HttpClientRequest {
late FakeHttpImageResponse response;
@override
Future<HttpClientResponse> close() async {
return response;
}
}
class FakeHttpImageResponse extends Fake implements HttpClientResponse {
FakeHttpImageResponse(this.response);
final List<List<int>> response;
@override
Future<void> forEach(void Function(List<int> element) action) async {
response.forEach(action);
}
}

View file

@ -1,716 +0,0 @@
// 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.
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
import 'dart:convert';
import 'dart:io' hide Directory;
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_goldens/src/flutter_goldens_io.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform/platform.dart';
import 'utils/fakes.dart';
import 'utils/json_templates.dart';
const String _kFlutterRoot = '/flutter';
void main() {
late SkiaGoldClient skiaClient;
late Directory workDirectory;
late MemoryFileSystem fs;
late FakePlatform platform;
late FakeProcessManager process;
late FakeHttpClient fakeHttpClient;
setUp(() {
fs = MemoryFileSystem();
platform = FakePlatform(
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
operatingSystem: 'macos'
);
process = FakeProcessManager();
fakeHttpClient = FakeHttpClient();
fs.directory(_kFlutterRoot).createSync(recursive: true);
});
setUp(() {
workDirectory = fs.directory('/workDirectory')
..createSync(recursive: true);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
});
test('web HTML test', () async {
platform = FakePlatform(
environment: <String, String>{
'GOLDCTL': 'goldctl',
'FLUTTER_ROOT': _kFlutterRoot,
'FLUTTER_TEST_BROWSER': 'Chrome',
'FLUTTER_WEB_RENDERER': 'html',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
'--add-test-optional-key', 'image_matching_algorithm:fuzzy',
'--add-test-optional-key', 'fuzzy_max_different_pixels:20',
'--add-test-optional-key', 'fuzzy_pixel_delta_threshold:4',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 0, '', '');
final Map<String, dynamic> keys = <String, dynamic>{
'Platform' : 'macos-browser',
'CI' : 'luci',
'markedFlaky' : 'false',
'Browser' : 'Chrome',
'WebRenderer' : 'html',
};
expect(
skiaClient.getKeysJSON(),
json.encode(keys),
);
expect(
await skiaClient.imgtestAdd('golden_file_test.png', goldenFile),
isTrue,
);
});
test('isFlaky sets right args - img test', () async {
platform = FakePlatform(
environment: <String, String>{
'GOLDCTL': 'goldctl',
'FLUTTER_ROOT': _kFlutterRoot,
'FLUTTER_TEST_BROWSER': 'Chrome',
'FLUTTER_WEB_RENDERER': 'html',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
'--add-test-optional-key', 'image_matching_algorithm:fuzzy',
'--add-test-optional-key', 'fuzzy_max_different_pixels:1000000000',
'--add-test-optional-key', 'fuzzy_pixel_delta_threshold:1020',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 0, '', '');
final Map<String, dynamic> keys = <String, dynamic>{
'Platform' : 'macos-browser',
'CI' : 'luci',
'markedFlaky' : 'false',
'Browser' : 'Chrome',
'WebRenderer' : 'html',
};
expect(
skiaClient.getKeysJSON(),
json.encode(keys),
);
expect(
await skiaClient.imgtestAdd('golden_file_test.png', goldenFile, isFlaky: true),
isTrue,
);
});
test('isFlaky sets right args - try job', () async {
platform = FakePlatform(
environment: <String, String>{
'GOLDCTL': 'goldctl',
'FLUTTER_ROOT': _kFlutterRoot,
'FLUTTER_TEST_BROWSER': 'Chrome',
'FLUTTER_WEB_RENDERER': 'html',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--add-test-optional-key', 'image_matching_algorithm:fuzzy',
'--add-test-optional-key', 'fuzzy_max_different_pixels:1000000000',
'--add-test-optional-key', 'fuzzy_pixel_delta_threshold:1020',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 0, '', '');
final Map<String, dynamic> keys = <String, dynamic>{
'Platform' : 'macos-browser',
'CI' : 'luci',
'markedFlaky' : 'false',
'Browser' : 'Chrome',
'WebRenderer' : 'html',
};
expect(
skiaClient.getKeysJSON(),
json.encode(keys),
);
await skiaClient.tryjobAdd('golden_file_test.png', goldenFile, isFlaky: true);
});
test('web CanvasKit test', () async {
platform = FakePlatform(
environment: <String, String>{
'GOLDCTL': 'goldctl',
'FLUTTER_ROOT': _kFlutterRoot,
'FLUTTER_TEST_BROWSER': 'Chrome',
'FLUTTER_WEB_RENDERER': 'canvaskit',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 0, '', '');
final Map<String, dynamic> keys = <String, dynamic>{
'Platform' : 'macos-browser',
'CI' : 'luci',
'markedFlaky' : 'false',
'Browser' : 'Chrome',
'WebRenderer' : 'canvaskit',
};
expect(
skiaClient.getKeysJSON(),
json.encode(keys),
);
expect(
await skiaClient.imgtestAdd('golden_file_test.png', goldenFile),
isTrue,
);
});
test('auth performs minimal work if already authorized', () async {
final File authFile = fs.file('/workDirectory/temp/auth_opt.json')
..createSync(recursive: true);
authFile.writeAsStringSync(authTemplate());
process.fallbackProcessResult = ProcessResult(123, 0, '', '');
await skiaClient.auth();
expect(process.workingDirectories, isEmpty);
});
test('gsutil is checked when authorization file is present', () async {
final File authFile = fs.file('/workDirectory/temp/auth_opt.json')
..createSync(recursive: true);
authFile.writeAsStringSync(authTemplate(gsutil: true));
expect(
await skiaClient.clientIsAuthorized(),
isFalse,
);
});
test('throws for error state from auth', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLD_SERVICE_ACCOUNT' : 'Service Account',
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.auth(),
throwsException,
);
});
test('throws for error state from init', () {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation gitInvocation = RunInvocation(
<String>['git', 'rev-parse', 'HEAD'],
'/flutter',
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'init',
'--instance', 'flutter',
'--work-dir', '/workDirectory/temp',
'--commit', '12345678',
'--keys-file', '/workDirectory/keys.json',
'--failure-file', '/workDirectory/failures.json',
'--passfail',
],
null,
);
process.processResults[gitInvocation] = ProcessResult(12345678, 0, '12345678', '');
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.imgtestInit(),
throwsException,
);
});
test('Only calls init once', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation gitInvocation = RunInvocation(
<String>['git', 'rev-parse', 'HEAD'],
'/flutter',
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'init',
'--instance', 'flutter',
'--work-dir', '/workDirectory/temp',
'--commit', '1234',
'--keys-file', '/workDirectory/keys.json',
'--failure-file', '/workDirectory/failures.json',
'--passfail',
],
null,
);
process.processResults[gitInvocation] = ProcessResult(1234, 0, '1234', '');
process.processResults[goldctlInvocation] = ProcessResult(5678, 0, '5678', '');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
// First call
await skiaClient.imgtestInit();
// Remove fake process result.
// If the init call is executed again, the fallback process will throw.
process.processResults.remove(goldctlInvocation);
// Second call
await skiaClient.imgtestInit();
});
test('Only calls tryjob init once', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation gitInvocation = RunInvocation(
<String>['git', 'rev-parse', 'HEAD'],
'/flutter',
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'init',
'--instance', 'flutter',
'--work-dir', '/workDirectory/temp',
'--commit', '1234',
'--keys-file', '/workDirectory/keys.json',
'--failure-file', '/workDirectory/failures.json',
'--passfail',
'--crs', 'github',
'--patchset_id', '1234',
'--changelist', '49815',
'--cis', 'buildbucket',
'--jobid', '8885996262141582672',
],
null,
);
process.processResults[gitInvocation] = ProcessResult(1234, 0, '1234', '');
process.processResults[goldctlInvocation] = ProcessResult(5678, 0, '5678', '');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
// First call
await skiaClient.tryjobInit();
// Remove fake process result.
// If the init call is executed again, the fallback process will throw.
process.processResults.remove(goldctlInvocation);
// Second call
await skiaClient.tryjobInit();
});
test('throws for error state from imgtestAdd', () {
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.imgtestAdd('golden_file_test', goldenFile),
throwsException,
);
});
test('correctly inits tryjob for luci', () async {
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
final List<String> ciArguments = skiaClient.getCIArguments();
expect(
ciArguments,
equals(
<String>[
'--changelist', '49815',
'--cis', 'buildbucket',
'--jobid', '8885996262141582672',
],
),
);
});
test('Creates traceID correctly', () async {
String traceID;
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
},
operatingSystem: 'linux'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
traceID = skiaClient.getTraceID('flutter.golden.1');
expect(
traceID,
equals('ae18c7a6aa48e0685525dfe8fdf79003'),
);
// Browser
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
'GOLD_TRYJOB' : 'refs/pull/49815/head',
'FLUTTER_TEST_BROWSER' : 'chrome',
},
operatingSystem: 'linux'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
traceID = skiaClient.getTraceID('flutter.golden.1');
expect(
traceID,
equals('e9d5c296c48e7126808520e9cc191243'),
);
// Locally - should defer to luci traceID
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
traceID = skiaClient.getTraceID('flutter.golden.1');
expect(
traceID,
equals('9968695b9ae78cdb77cbb2be621ca2d6'),
);
});
test('throws for error state from imgtestAdd', () {
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.imgtestAdd('golden_file_test', goldenFile),
throwsA(
isA<SkiaException>().having((SkiaException error) => error.message,
'message',
contains('result-state.json'),
),
),
);
});
test('throws for error state from tryjobAdd', () {
final File goldenFile = fs.file('/workDirectory/temp/golden_file_test.png')
..createSync(recursive: true);
platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': _kFlutterRoot,
'GOLDCTL' : 'goldctl',
},
operatingSystem: 'macos'
);
skiaClient = SkiaGoldClient(
workDirectory,
fs: fs,
process: process,
platform: platform,
httpClient: fakeHttpClient,
);
const RunInvocation goldctlInvocation = RunInvocation(
<String>[
'goldctl',
'imgtest', 'add',
'--work-dir', '/workDirectory/temp',
'--test-name', 'golden_file_test',
'--png-file', '/workDirectory/temp/golden_file_test.png',
'--passfail',
],
null,
);
process.processResults[goldctlInvocation] = ProcessResult(123, 1, 'Expected failure', 'Expected failure');
process.fallbackProcessResult = ProcessResult(123, 1, 'Fallback failure', 'Fallback failure');
expect(
skiaClient.tryjobAdd('golden_file_test', goldenFile),
throwsA(
isA<SkiaException>().having((SkiaException error) => error.message,
'message',
contains('result-state.json'),
),
),
);
});
group('Request Handling', () {
const String expectation = '55109a4bed52acc780530f7a9aeff6c0';
test('image bytes are processed properly', () async {
final Uri imageUrl = Uri.parse(
'https://flutter-gold.skia.org/img/images/$expectation.png'
);
final FakeHttpClientRequest fakeImageRequest = FakeHttpClientRequest();
final FakeHttpImageResponse fakeImageResponse = FakeHttpImageResponse(
imageResponseTemplate()
);
fakeHttpClient.request = fakeImageRequest;
fakeImageRequest.response = fakeImageResponse;
final List<int> masterBytes = await skiaClient.getImageBytes(expectation);
expect(fakeHttpClient.lastUri, imageUrl);
expect(masterBytes, equals(kTestPngBytes));
});
});
}

View file

@ -1,205 +0,0 @@
// 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.
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
import 'dart:convert';
import 'dart:io' hide Directory;
import 'package:file/file.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_goldens/src/flaky_goldens.dart';
import 'package:flutter_goldens/src/flutter_goldens_io.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:process/process.dart';
// 1x1 transparent pixel
const List<int> kTestPngBytes = <int>[
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84,
120, 1, 99, 97, 0, 2, 0, 0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69,
78, 68, 174, 66, 96, 130,
];
@immutable
class RunInvocation {
const RunInvocation(this.command, this.workingDirectory);
final List<String> command;
final String? workingDirectory;
@override
int get hashCode => Object.hash(Object.hashAll(command), workingDirectory);
bool _commandEquals(List<String> other) {
if (other == command) {
return true;
}
if (other.length != command.length) {
return false;
}
for (int index = 0; index < other.length; index += 1) {
if (other[index] != command[index]) {
return false;
}
}
return true;
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is RunInvocation
&& _commandEquals(other.command)
&& other.workingDirectory == workingDirectory;
}
@override
String toString() => '$command ($workingDirectory)';
}
class FakeProcessManager extends Fake implements ProcessManager {
Map<RunInvocation, ProcessResult> processResults = <RunInvocation, ProcessResult>{};
/// Used if [processResults] does not contain a matching invocation.
ProcessResult? fallbackProcessResult;
final List<String?> workingDirectories = <String?>[];
@override
Future<ProcessResult> run(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) async {
workingDirectories.add(workingDirectory);
final ProcessResult? result = processResults[RunInvocation(command.cast<String>(), workingDirectory)];
if (result == null && fallbackProcessResult == null) {
printOnFailure('ProcessManager.run was called with $command ($workingDirectory) unexpectedly - $processResults.');
fail('See above.');
}
return result ?? fallbackProcessResult!;
}
}
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
class FakeSkiaGoldClient extends Fake implements SkiaGoldClient {
Map<String, String> expectationForTestValues = <String, String>{};
Exception? getExpectationForTestThrowable;
@override
Future<String> getExpectationForTest(String testName) async {
if (getExpectationForTestThrowable != null) {
throw getExpectationForTestThrowable!;
}
return expectationForTestValues[testName] ?? '';
}
@override
Future<void> auth() async {}
final List<String> testNames = <String>[];
int initCalls = 0;
int calledWithFlaky = 0;
@override
Future<void> imgtestInit({ bool isFlaky = false }) async {
initCalls += 1;
if (isFlaky) {
calledWithFlaky += 1;
}
}
@override
Future<bool> imgtestAdd(String testName, File goldenFile, { bool isFlaky = false }) async {
testNames.add(testName);
if (isFlaky) {
calledWithFlaky += 1;
}
return true;
}
int tryInitCalls = 0;
@override
Future<void> tryjobInit({ bool isFlaky = false }) async {
tryInitCalls += 1;
if (isFlaky) {
calledWithFlaky += 1;
}
}
@override
Future<bool> tryjobAdd(String testName, File goldenFile, { bool isFlaky = false }) async {
if (isFlaky) {
calledWithFlaky += 1;
}
return true;
}
Map<String, List<int>> imageBytesValues = <String, List<int>>{};
@override
Future<List<int>> getImageBytes(String imageHash) async => imageBytesValues[imageHash]!;
Map<String, String> cleanTestNameValues = <String, String>{};
@override
String cleanTestName(String fileName) => cleanTestNameValues[fileName] ?? '';
}
class FakeFlakyLocalFileComparator extends FakeLocalFileComparator with FlakyGoldenMixin {}
class FakeLocalFileComparator extends Fake implements LocalFileComparator {
@override
late Uri basedir;
@override
Uri getTestUri(Uri key, int? version) => Uri.parse('fake');
@override
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async => true;
}
class FakeDirectory extends Fake implements Directory {
late bool existsSyncValue;
@override
bool existsSync() => existsSyncValue;
@override
late Uri uri;
}
class FakeHttpClient extends Fake implements HttpClient {
late Uri lastUri;
late FakeHttpClientRequest request;
@override
Future<HttpClientRequest> getUrl(Uri url) async {
lastUri = url;
return request;
}
}
class FakeHttpClientRequest extends Fake implements HttpClientRequest {
late FakeHttpImageResponse response;
@override
Future<HttpClientResponse> close() async {
return response;
}
}
class FakeHttpImageResponse extends Fake implements HttpClientResponse {
FakeHttpImageResponse(this.response);
final List<List<int>> response;
@override
Future<void> forEach(void Function(List<int> element) action) async {
response.forEach(action);
}
}

View file

@ -8,7 +8,6 @@ import 'dart:io' as io;
import 'package:crypto/crypto.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'package:process/process.dart';
@ -139,7 +138,7 @@ class SkiaGoldClient {
/// The `imgtest` command collects and uploads test results to the Skia Gold
/// backend, the `init` argument initializes the current test. Used by the
/// [FlutterPostSubmitFileComparator].
Future<void> imgtestInit({ bool isFlaky = false }) async {
Future<void> imgtestInit() async {
// This client has already been initialized
if (_initialized) {
return;
@ -148,7 +147,7 @@ class SkiaGoldClient {
final File keys = workDirectory.childFile('keys.json');
final File failures = workDirectory.childFile('failures.json');
await keys.writeAsString(getKeysJSON(isFlaky: isFlaky));
await keys.writeAsString(_getKeysJSON());
await failures.create();
final String commitHash = await _getCurrentCommit();
@ -200,7 +199,7 @@ class SkiaGoldClient {
///
/// The [testName] and [goldenFile] parameters reference the current
/// comparison being evaluated by the [FlutterPostSubmitFileComparator].
Future<bool> imgtestAdd(String testName, File goldenFile, { bool isFlaky = false }) async {
Future<bool> imgtestAdd(String testName, File goldenFile) async {
final List<String> imgtestCommand = <String>[
_goldctl,
'imgtest', 'add',
@ -210,7 +209,7 @@ class SkiaGoldClient {
'--test-name', cleanTestName(testName),
'--png-file', goldenFile.path,
'--passfail',
..._getPixelMatchingArguments(isFlaky: isFlaky),
..._getPixelMatchingArguments(),
];
final io.ProcessResult result = await process.run(imgtestCommand);
@ -260,7 +259,7 @@ class SkiaGoldClient {
/// The `imgtest` command collects and uploads test results to the Skia Gold
/// backend, the `init` argument initializes the current tryjob. Used by the
/// [FlutterPreSubmitFileComparator].
Future<void> tryjobInit({ bool isFlaky = false }) async {
Future<void> tryjobInit() async {
// This client has already been initialized
if (_tryjobInitialized) {
return;
@ -269,7 +268,7 @@ class SkiaGoldClient {
final File keys = workDirectory.childFile('keys.json');
final File failures = workDirectory.childFile('failures.json');
await keys.writeAsString(getKeysJSON(isFlaky: isFlaky));
await keys.writeAsString(_getKeysJSON());
await failures.create();
final String commitHash = await _getCurrentCommit();
@ -324,7 +323,7 @@ class SkiaGoldClient {
///
/// The [testName] and [goldenFile] parameters reference the current
/// comparison being evaluated by the [FlutterPreSubmitFileComparator].
Future<void> tryjobAdd(String testName, File goldenFile, { bool isFlaky = false}) async {
Future<void> tryjobAdd(String testName, File goldenFile) async {
final List<String> imgtestCommand = <String>[
_goldctl,
'imgtest', 'add',
@ -333,7 +332,7 @@ class SkiaGoldClient {
.path,
'--test-name', cleanTestName(testName),
'--png-file', goldenFile.path,
..._getPixelMatchingArguments(isFlaky: isFlaky),
..._getPixelMatchingArguments(),
];
final io.ProcessResult result = await process.run(imgtestCommand);
@ -363,42 +362,6 @@ class SkiaGoldClient {
}
}
List<String> _getPixelMatchingArguments({ required bool isFlaky }) {
if (isFlaky) {
return _getFlakyPixelMatchingArguments();
} else {
return _getNormalPixelMatchingArguments();
}
}
List<String> _getFlakyPixelMatchingArguments() {
// The algorithm to be used when matching images. The available options are:
// - "fuzzy": Allows for customizing the thresholds of pixel differences.
// - "sobel": Same as "fuzzy" but performs edge detection before performing
// a fuzzy match.
const String algorithm = 'fuzzy';
// The number of pixels in this image that are allowed to differ from the
// baseline.
//
// The chosen number - 1 billion - indicates that a flaky test should pass
// no matter how many pixels are different from the master golden.
const int maxDifferentPixels = 1000 * 1000 * 1000;
// The maximum acceptable difference per pixel.
//
// The chosen number - 1020 - is the maximum supported pixel delta and
// indicates that a flaky test should pass no matter how far the new pixels
// deviate from the master golden.
const int pixelDeltaThreshold = 1020;
return <String>[
'--add-test-optional-key', 'image_matching_algorithm:$algorithm',
'--add-test-optional-key', 'fuzzy_max_different_pixels:$maxDifferentPixels',
'--add-test-optional-key', 'fuzzy_pixel_delta_threshold:$pixelDeltaThreshold',
];
}
// Constructs arguments for `goldctl` for controlling how pixels are compared.
//
// For AOT and CanvasKit exact pixel matching is used. For the HTML renderer
@ -406,7 +369,7 @@ class SkiaGoldClient {
// because Chromium cannot exactly reproduce the same golden on all computers.
// It seems to depend on the hardware/OS/driver combination. However, those
// differences are very small (typically not noticeable to human eye).
List<String> _getNormalPixelMatchingArguments() {
List<String> _getPixelMatchingArguments() {
// Only use fuzzy pixel matching in the HTML renderer.
if (!_isBrowserTest || _isBrowserCanvasKitTest) {
return const <String>[];
@ -522,17 +485,17 @@ class SkiaGoldClient {
/// Currently, the only key value pairs being tracked is the platform the
/// image was rendered on, and for web tests, the browser the image was
/// rendered on.
@visibleForTesting
String getKeysJSON({ bool isFlaky = false}) {
String _getKeysJSON() {
final Map<String, dynamic> keys = <String, dynamic>{
'Platform' : platform.operatingSystem,
'CI' : 'luci',
'markedFlaky' : isFlaky.toString(),
};
if (_isBrowserTest) {
keys['Browser'] = _browserKey;
keys['Platform'] = '${keys['Platform']}-browser';
keys['WebRenderer'] = _isBrowserCanvasKitTest ? 'canvaskit' : 'html';
if (_isBrowserCanvasKitTest) {
keys['WebRenderer'] = 'canvaskit';
}
}
return json.encode(keys);
}

View file

@ -41,12 +41,12 @@ Future<ComparisonResult> compareLists(List<int> test, List<int> master) async {
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
/// comparator.
class DefaultWebGoldenComparator extends WebGoldenComparator {
/// Creates a new [DefaultWebGoldenComparator] for the specified [testUri].
/// Creates a new [DefaultWebGoldenComparator] for the specified [testFile].
///
/// Golden file keys will be interpreted as file paths relative to the
/// directory in which [testUri] resides.
/// directory in which [testFile] resides.
///
/// The [testUri] URL must represent a file.
/// The [testFile] URL must represent a file.
DefaultWebGoldenComparator(this.testUri);
/// The test file currently being executed.

View file

@ -238,14 +238,6 @@ set webGoldenComparator(WebGoldenComparator value) {
_webGoldenComparator = value;
}
/// The URI of the test file currently being executed.
///
/// This variable is populated by the Flutter Tool automatically.
///
/// Golden file keys will be interpreted as file paths relative to the directory
/// in which this file resides.
late Uri webTestUri;
/// Whether golden files should be automatically updated during tests rather
/// than compared to the image bytes recorded by the tests.
///

View file

@ -98,14 +98,14 @@ class TestGoldenComparator {
return _processManager.start(command, environment: environment);
}
Future<String?> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens, Map<String, dynamic>? customProperties) async {
Future<String?> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens) async {
final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
if (process == null) {
return 'process was null';
}
process.sendCommand(imageFile, goldenKey, updateGoldens, customProperties);
process.sendCommand(imageFile, goldenKey, updateGoldens);
final Map<String, dynamic> result = await process.getResponse();
@ -152,13 +152,11 @@ class TestGoldenComparatorProcess {
await process.exitCode;
}
void sendCommand(File imageFile, Uri? goldenKey, bool? updateGoldens, Map<String, dynamic>? customProperties) {
void sendCommand(File imageFile, Uri? goldenKey, bool? updateGoldens) {
final Object command = jsonEncode(<String, dynamic>{
'imageFile': imageFile.path,
'key': goldenKey.toString(),
'update': updateGoldens,
if (customProperties != null)
'customProperties': customProperties,
});
_logger.printTrace('Preparing to send command: $command');
process.stdin.writeln(command);
@ -170,22 +168,7 @@ class TestGoldenComparatorProcess {
return streamIterator.current;
}
/// Generates the source code for the comparator process for the test file.
///
/// If a test configuation exists for the tested package, uses its
/// implementation. Otherwise, uses the default implementation.
static String generateBootstrap(File testFile, Uri testUri, {required Logger logger}) {
final File? webTestConfigFile = findWebTestConfigFile(testFile, logger);
if (webTestConfigFile != null) {
return _generateBootstrapWithWebTestConfig(webTestConfigFile, testFile, testUri);
} else {
return _generateBasicBootstrap(testFile, testUri, logger: logger);
}
}
// Generates the bootstrap used by tests that either don't have a test
// configuration file, or don't have a `flutter_web_test_config.dart`.
static String _generateBasicBootstrap(File testFile, Uri testUri, {required Logger logger}) {
final File? testConfigFile = findTestConfigFile(testFile, logger);
// Generate comparator process for the file.
return '''
@ -228,19 +211,6 @@ void main() async {
}
}
${testConfigFile != null ? '});' : ''}
}
''';
}
// Generates the bootstrap used by tests that have a `flutter_web_test_config.dart`.
static String _generateBootstrapWithWebTestConfig(File webTestConfigFile, File testFile, Uri testUri) {
return '''
import 'package:flutter_test/flutter_test.dart';
import '${Uri.file(webTestConfigFile.path)}' as web_test_config;
void main() async {
final String testUri = '$testUri';
goldenFileComparator = LocalFileComparator(Uri.parse(testUri));
await web_test_config.startWebTestHostConfiguration(testUri);
}
''';
}

View file

@ -343,9 +343,10 @@ class FlutterWebPlatform extends PlatformPlugin {
}
Future<shelf.Response> _goldenFileHandler(shelf.Request request) async {
if (request.method == 'POST' && request.url.path.contains('flutter_goldens')) {
final String requestJson = await request.readAsString();
final Map<String, Object?> body = json.decode(requestJson) as Map<String, Object?>;
if (request.url.path.contains('flutter_goldens')) {
final Map<String, Object?> body = json.decode(await request.readAsString()) as Map<String, Object?>;
final Uri goldenKey = Uri.parse(body['key']! as String);
final Uri testUri = Uri.parse(body['testUri']! as String);
final num width = body['width']! as num;
final num height = body['height']! as num;
Uint8List bytes;
@ -382,10 +383,7 @@ class FlutterWebPlatform extends PlatformPlugin {
return shelf.Response.ok('Unknown error, bytes is null');
}
final Uri goldenKey = Uri.parse(body['key']! as String);
final Uri testUri = Uri.parse(body['testUri']! as String);
final Map<String, dynamic>? customProperties = body['customProperties'] as Map<String, dynamic>?;
final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens, customProperties);
final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
return shelf.Response.ok(errorMessage ?? 'true');
} else {
return shelf.Response.notFound('Not Found');

View file

@ -9,36 +9,23 @@ import '../base/logger.dart';
/// test harness if it exists in the project directory hierarchy.
const String _kTestConfigFileName = 'flutter_test_config.dart';
/// The name of the web test configuration file that will be discovered by the
/// test harness if it exists in the project directory hierarchy.
const String _kWebTestConfigFileName = 'flutter_web_test_config.dart';
/// The name of the file that signals the root of the project and that will
/// cause the test harness to stop scanning for configuration files.
const String _kProjectRootSentinel = 'pubspec.yaml';
/// Find the `flutter_test_config.dart` file for a specific test file.
File? findTestConfigFile(File testFile, Logger logger) {
return _findConfigFile(testFile, _kTestConfigFileName, logger);
}
/// Find the `flutter_web_test_config.dart` file for a specific test file.
File? findWebTestConfigFile(File testFile, Logger logger) {
return _findConfigFile(testFile, _kWebTestConfigFileName, logger);
}
File? _findConfigFile(File testFile, String configFileName, Logger logger) {
File? testConfigFile;
Directory directory = testFile.parent;
while (directory.path != directory.parent.path) {
final File configFile = directory.childFile(configFileName);
final File configFile = directory.childFile(_kTestConfigFileName);
if (configFile.existsSync()) {
logger.printTrace('Discovered $configFileName in ${directory.path}');
logger.printTrace('Discovered $_kTestConfigFileName in ${directory.path}');
testConfigFile = configFile;
break;
}
if (directory.childFile(_kProjectRootSentinel).existsSync()) {
logger.printTrace('Stopping scan for $configFileName; '
logger.printTrace('Stopping scan for $_kTestConfigFileName; '
'found project root at ${directory.path}');
break;
}

View file

@ -224,8 +224,7 @@ String generateTestEntrypoint({
Future<void> main() async {
ui.debugEmulateFlutterTesterEnvironment = true;
await ui.webOnlyInitializePlatform();
webTestUri = Uri.parse('${Uri.file(absolutePath)}');
webGoldenComparator = DefaultWebGoldenComparator(webTestUri);
webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}'));
(ui.window as dynamic).debugOverrideDevicePixelRatio(3.0);
(ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800);

View file

@ -41,13 +41,13 @@ void main() {
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess, logger: BufferLogger.test());
process.sendCommand(imageFile, goldenKey, false, <String, String>{'additional data' : 'data'});
process.sendCommand(imageFile, goldenKey, false);
final Map<String, dynamic> response = await process.getResponse();
final String stringToStdin = ioSink.getAndClear();
expect(response, expectedResponse);
expect(stringToStdin, '{"imageFile":"test_image_file","key":"file://golden_key/","update":false,"customProperties":{"additional data":"data"}}\n');
expect(stringToStdin, '{"imageFile":"test_image_file","key":"file://golden_key/","update":false}\n');
});
testWithoutContext('can handle multiple requests', () async {
@ -64,21 +64,18 @@ void main() {
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess, logger: BufferLogger.test());
process.sendCommand(imageFile, goldenKey, false, null);
process.sendCommand(imageFile, goldenKey, false);
final Map<String, dynamic> response1 = await process.getResponse();
process.sendCommand(imageFile2, goldenKey2, true, null);
process.sendCommand(imageFile2, goldenKey2, true);
final Map<String, dynamic> response2 = await process.getResponse();
final String stringToStdin = ioSink.getAndClear();
expect(response1, expectedResponse1);
expect(response2, expectedResponse2);
expect(
stringToStdin,
'{"imageFile":"test_image_file","key":"file://golden_key/","update":false}\n'
'{"imageFile":"second_test_image_file","key":"file://second_golden_key/","update":true}\n');
expect(stringToStdin, '{"imageFile":"test_image_file","key":"file://golden_key/","update":false}\n{"imageFile":"second_test_image_file","key":"file://second_golden_key/","update":true}\n');
});
testWithoutContext('ignores anything that does not look like JSON', () async {
@ -97,7 +94,7 @@ Other JSON data after the initial data
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess,logger: BufferLogger.test());
process.sendCommand(imageFile, goldenKey, false, null);
process.sendCommand(imageFile, goldenKey, false);
final Map<String, dynamic> response = await process.getResponse();
final String stringToStdin = ioSink.getAndClear();

View file

@ -61,7 +61,7 @@ void main() {
webRenderer: WebRendererMode.html,
);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false, null);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result, null);
});
@ -90,7 +90,7 @@ void main() {
webRenderer: WebRendererMode.canvaskit,
);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false, null);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result, 'some message');
});
@ -123,10 +123,10 @@ void main() {
webRenderer: WebRendererMode.html,
);
final String? result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false, null);
final String? result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result1, 'some message');
final String? result2 = await comparator.compareGoldens(testUri, imageBytes, goldenKey2, false, null);
final String? result2 = await comparator.compareGoldens(testUri, imageBytes, goldenKey2, false);
expect(result2, 'some other message');
});
@ -168,10 +168,10 @@ void main() {
webRenderer: WebRendererMode.canvaskit,
);
final String? result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false, null);
final String? result1 = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result1, 'some message');
final String? result2 = await comparator.compareGoldens(testUri2, imageBytes, goldenKey2, false, null);
final String? result2 = await comparator.compareGoldens(testUri2, imageBytes, goldenKey2, false);
expect(result2, 'some other message');
});
@ -203,7 +203,7 @@ void main() {
webRenderer: WebRendererMode.html,
);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false, null);
final String? result = await comparator.compareGoldens(testUri, imageBytes, goldenKey, false);
expect(result, null);
await comparator.close();

View file

@ -117,7 +117,7 @@ void main() {
final List<String> allowedPaths = <String>[
fileSystem.path.join(flutterTools, 'lib', 'src', 'test', 'flutter_platform.dart'),
fileSystem.path.join(flutterTools, 'lib', 'src', 'test', 'flutter_web_platform.dart'),
fileSystem.path.join(flutterTools, 'lib', 'src', 'test', 'flutter_goldens.dart'),
fileSystem.path.join(flutterTools, 'lib', 'src', 'test', 'test_wrapper.dart'),
];
bool isNotAllowed(FileSystemEntity entity) => allowedPaths.every((String path) => path != entity.path);