mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
This reverts commit 53e6876226
.
This commit is contained in:
parent
a1432a9c5d
commit
a84e369bd2
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
|
@ -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');
|
||||
}
|
|
@ -7,7 +7,3 @@
|
|||
void matchesGoldenFile(Object key) {
|
||||
return;
|
||||
}
|
||||
|
||||
void expectFlakyGolden(Object key, String string){
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'\' : '/'))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
8
examples/api/test/goldens_web.dart
Normal file
8
examples/api/test/goldens_web.dart
Normal 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();
|
5
packages/flutter/test/_goldens_io.dart
Normal file
5
packages/flutter/test/_goldens_io.dart
Normal 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;
|
8
packages/flutter/test/_goldens_web.dart
Normal file
8
packages/flutter/test/_goldens_web.dart
Normal 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();
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
''';
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue