This reverts commit 53e6876226a7729531be7be4fa7321363c5a3b30.
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/file.dart';
|
||||||
import 'package:file/memory.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:flutter_test/flutter_test.dart';
|
||||||
import 'package:platform/platform.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 _findGoldenTestPattern = RegExp(r'matchesGoldenFile\(');
|
||||||
final RegExp _findGoldenDefinitionPattern = RegExp(r'(matchesGoldenFile|expectFlakyGolden)\(Object');
|
final RegExp _findGoldenDefinitionPattern = RegExp(r'matchesGoldenFile\(Object');
|
||||||
final RegExp _leadingComment = RegExp(r'//');
|
final RegExp _leadingComment = RegExp(r'//');
|
||||||
final RegExp _goldenTagPattern1 = RegExp(r'@Tags\(');
|
final RegExp _goldenTagPattern1 = RegExp(r'@Tags\(');
|
||||||
final RegExp _goldenTagPattern2 = RegExp(r"'reduced-test-set'");
|
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)';
|
const String _ignoreGoldenTagForFile = '// flutter_ignore_for_file: golden_tag (see analyze.dart)';
|
||||||
|
|
||||||
Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 2000 }) async {
|
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 List<String> errors = <String>[];
|
||||||
final Stream<File> allTestFiles = _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)
|
await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
|
||||||
.where((File file) => !isWithinFlutterGoldenLib(file));
|
|
||||||
await for (final File file in allTestFiles) {
|
|
||||||
bool needsTag = false;
|
bool needsTag = false;
|
||||||
bool hasTagNotation = false;
|
bool hasTagNotation = false;
|
||||||
bool hasReducedTag = 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) {
|
void matchesGoldenFile(Object key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectFlakyGolden(Object key, String string){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
@ -35,11 +35,8 @@
|
|||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
///
|
///
|
||||||
/// expectFlakyGolden(a, b)
|
|
||||||
|
|
||||||
// Other comments
|
// Other comments
|
||||||
// matchesGoldenFile('comment.png');
|
// matchesGoldenFile('comment.png');
|
||||||
// expectFlakyGolden(a, b);
|
|
||||||
|
|
||||||
String literal = 'matchesGoldenFile()'; // flutter_ignore: golden_tag (see analyze.dart)
|
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.';
|
'at the top of the file before import statements.';
|
||||||
const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
|
const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
|
||||||
final List<String> lines = <String>[
|
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/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',
|
'║ test/analyze-test-input/root/packages/foo/golden_no_tag.dart: $noTag',
|
||||||
]
|
]
|
||||||
.map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
|
.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 ignoreCost = 2003.0; // four average SWE days, in dollars
|
||||||
const double pythonCost = 3001.0; // six 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 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 ignoreForFileCost = 2477.0; // similar thinking as skipCost
|
||||||
const double asDynamicCost = 2011.0; // a few days to refactor the code.
|
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.
|
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]')) {
|
if (isTest && line.contains('skip:') && !line.contains('[intended]')) {
|
||||||
total += skipCost;
|
total += skipCost;
|
||||||
}
|
}
|
||||||
if (isTest && line.contains('expectFlakyGolden(')) {
|
|
||||||
total += flakyGoldenCost;
|
|
||||||
}
|
|
||||||
if (isDart && isOptingOutOfNullSafety(line)) {
|
if (isDart && isOptingOutOfNullSafety(line)) {
|
||||||
total += fileNullSafetyMigrationCost;
|
total += fileNullSafetyMigrationCost;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import 'dart:async';
|
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) {
|
Future<void> testExecutable(FutureOr<void> Function() testMain) {
|
||||||
// Enable golden file testing using Skia Gold.
|
// 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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
// The tag is missing. This should fail analysis.
|
export 'package:flutter_goldens/flutter_goldens.dart' show testExecutable;
|
||||||
|
|
||||||
import 'golden_class.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
expectFlakyGolden('key', 'missing_tag.png');
|
|
||||||
}
|
|
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/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_goldens/flutter_goldens.dart' show expectFlakyGolden;
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
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
|
// 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
|
// 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,
|
// 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));
|
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.time));
|
||||||
|
if (!skipPerspectiveTextGoldens) {
|
||||||
if (perspectiveTestIsFlaky) {
|
|
||||||
await expectFlakyGolden(
|
|
||||||
find.byType(CupertinoDatePicker),
|
|
||||||
'date_picker_test.time.initial.png',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(CupertinoDatePicker),
|
find.byType(CupertinoDatePicker),
|
||||||
matchesGoldenFile('date_picker_test.time.initial.png'),
|
matchesGoldenFile('date_picker_test.time.initial.png'),
|
||||||
@ -1212,13 +1205,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.date));
|
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.date));
|
||||||
|
if (!skipPerspectiveTextGoldens) {
|
||||||
if (perspectiveTestIsFlaky) {
|
|
||||||
await expectFlakyGolden(
|
|
||||||
find.byType(CupertinoDatePicker),
|
|
||||||
'date_picker_test.date.initial.png',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(CupertinoDatePicker),
|
find.byType(CupertinoDatePicker),
|
||||||
matchesGoldenFile('date_picker_test.date.initial.png'),
|
matchesGoldenFile('date_picker_test.date.initial.png'),
|
||||||
@ -1226,13 +1213,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.dateAndTime));
|
await tester.pumpWidget(buildApp(CupertinoDatePickerMode.dateAndTime));
|
||||||
|
if (!skipPerspectiveTextGoldens) {
|
||||||
if (perspectiveTestIsFlaky) {
|
|
||||||
await expectFlakyGolden(
|
|
||||||
find.byType(CupertinoDatePicker),
|
|
||||||
'date_picker_test.datetime.initial.png',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(CupertinoDatePicker),
|
find.byType(CupertinoDatePicker),
|
||||||
matchesGoldenFile('date_picker_test.datetime.initial.png'),
|
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.drag(find.text('4'), Offset(0, _kRowOffset.dy / 2), warnIfMissed: false); // see top of file
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
if (perspectiveTestIsFlaky) {
|
if (!skipPerspectiveTextGoldens) {
|
||||||
await expectFlakyGolden(
|
|
||||||
find.byType(CupertinoDatePicker),
|
|
||||||
'date_picker_test.datetime.drag.png',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(CupertinoDatePicker),
|
find.byType(CupertinoDatePicker),
|
||||||
matchesGoldenFile('date_picker_test.datetime.drag.png'),
|
matchesGoldenFile('date_picker_test.datetime.drag.png'),
|
||||||
@ -1338,12 +1314,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (perspectiveTestIsFlaky) {
|
if (!skipPerspectiveTextGoldens) {
|
||||||
await expectFlakyGolden(
|
|
||||||
find.byType(CupertinoTimerPicker),
|
|
||||||
'timer_picker_test.datetime.initial.png',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(CupertinoTimerPicker),
|
find.byType(CupertinoTimerPicker),
|
||||||
matchesGoldenFile('timer_picker_test.datetime.initial.png'),
|
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.drag(find.text('59'), Offset(0, _kRowOffset.dy / 2), warnIfMissed: false); // see top of file
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
if (perspectiveTestIsFlaky) {
|
if (!skipPerspectiveTextGoldens) {
|
||||||
await expectFlakyGolden(
|
|
||||||
find.byType(CupertinoTimerPicker),
|
|
||||||
'timer_picker_test.datetime.drag.png',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(CupertinoTimerPicker),
|
find.byType(CupertinoTimerPicker),
|
||||||
matchesGoldenFile('timer_picker_test.datetime.drag.png'),
|
matchesGoldenFile('timer_picker_test.datetime.drag.png'),
|
||||||
|
@ -5,9 +5,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_goldens/flutter_goldens.dart' as flutter_goldens;
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
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) {
|
Future<void> testExecutable(FutureOr<void> Function() testMain) {
|
||||||
// Enable checks because there are many implementations of [RenderBox] in this
|
// Enable checks because there are many implementations of [RenderBox] in this
|
||||||
// package can benefit from the additional validations.
|
// 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.
|
// Enable golden file testing using Skia Gold.
|
||||||
return flutter_goldens.testExecutable(testMain);
|
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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
/// This library exposes functions that enhance the test with custom golden
|
import 'dart:async' show FutureOr;
|
||||||
/// configuration for the Flutter repository.
|
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 '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'
|
// If you are here trying to figure out how to use golden files in the Flutter
|
||||||
show processBrowserCommand, testExecutable;
|
// 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
|
// See also dev/automated_tests/flutter_test/flutter_gold_test.dart
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io' hide Directory;
|
import 'dart:io' hide Directory;
|
||||||
|
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter/foundation.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:flutter_test/flutter_test.dart';
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
import 'utils/fakes.dart';
|
import 'json_templates.dart';
|
||||||
|
|
||||||
const String _kFlutterRoot = '/flutter';
|
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() {
|
void main() {
|
||||||
late MemoryFileSystem fs;
|
late MemoryFileSystem fs;
|
||||||
late FakePlatform platform;
|
late FakePlatform platform;
|
||||||
|
late FakeProcessManager process;
|
||||||
|
late FakeHttpClient fakeHttpClient;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
fs = MemoryFileSystem();
|
fs = MemoryFileSystem();
|
||||||
@ -27,9 +39,567 @@ void main() {
|
|||||||
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
|
environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
|
||||||
operatingSystem: 'macos'
|
operatingSystem: 'macos'
|
||||||
);
|
);
|
||||||
|
process = FakeProcessManager();
|
||||||
|
fakeHttpClient = FakeHttpClient();
|
||||||
fs.directory(_kFlutterRoot).createSync(recursive: true);
|
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', () {
|
group('FlutterGoldenFileComparator', () {
|
||||||
late FlutterGoldenFileComparator comparator;
|
late FlutterGoldenFileComparator comparator;
|
||||||
|
|
||||||
@ -80,7 +650,7 @@ void main() {
|
|||||||
namePrefix: namePrefix,
|
namePrefix: namePrefix,
|
||||||
);
|
);
|
||||||
await comparator.compare(
|
await comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse(fileName),
|
Uri.parse(fileName),
|
||||||
);
|
);
|
||||||
expect(fakeSkiaClient.testNames.single, '$namePrefix.$libraryName.$fileName');
|
expect(fakeSkiaClient.testNames.single, '$namePrefix.$libraryName.$fileName');
|
||||||
@ -105,7 +675,7 @@ void main() {
|
|||||||
await expectLater(
|
await expectLater(
|
||||||
() async {
|
() async {
|
||||||
return comparator.compare(
|
return comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse('flutter.golden_test.1'),
|
Uri.parse('flutter.golden_test.1'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -124,26 +694,12 @@ void main() {
|
|||||||
test('calls init during compare', () {
|
test('calls init during compare', () {
|
||||||
expect(fakeSkiaClient.initCalls, 0);
|
expect(fakeSkiaClient.initCalls, 0);
|
||||||
comparator.compare(
|
comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse('flutter.golden_test.1.png'),
|
Uri.parse('flutter.golden_test.1.png'),
|
||||||
);
|
);
|
||||||
expect(fakeSkiaClient.initCalls, 1);
|
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', () {
|
test('does not call init in during construction', () {
|
||||||
expect(fakeSkiaClient.initCalls, 0);
|
expect(fakeSkiaClient.initCalls, 0);
|
||||||
FlutterPostSubmitFileComparator.fromDefaultComparator(
|
FlutterPostSubmitFileComparator.fromDefaultComparator(
|
||||||
@ -237,7 +793,7 @@ void main() {
|
|||||||
await expectLater(
|
await expectLater(
|
||||||
() async {
|
() async {
|
||||||
return comparator.compare(
|
return comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse('flutter.golden_test.1'),
|
Uri.parse('flutter.golden_test.1'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -256,27 +812,12 @@ void main() {
|
|||||||
test('calls init during compare', () {
|
test('calls init during compare', () {
|
||||||
expect(fakeSkiaClient.tryInitCalls, 0);
|
expect(fakeSkiaClient.tryInitCalls, 0);
|
||||||
comparator.compare(
|
comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse('flutter.golden_test.1.png'),
|
Uri.parse('flutter.golden_test.1.png'),
|
||||||
);
|
);
|
||||||
expect(fakeSkiaClient.tryInitCalls, 1);
|
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', () {
|
test('does not call init in during construction', () {
|
||||||
expect(fakeSkiaClient.tryInitCalls, 0);
|
expect(fakeSkiaClient.tryInitCalls, 0);
|
||||||
FlutterPostSubmitFileComparator.fromDefaultComparator(
|
FlutterPostSubmitFileComparator.fromDefaultComparator(
|
||||||
@ -366,19 +907,6 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('Skipping', () {
|
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', () {
|
group('correctly determines testing environment', () {
|
||||||
test('returns true on Cirrus builds', () {
|
test('returns true on Cirrus builds', () {
|
||||||
platform = FakePlatform(
|
platform = FakePlatform(
|
||||||
@ -443,7 +971,7 @@ void main() {
|
|||||||
|
|
||||||
const String hash = '55109a4bed52acc780530f7a9aeff6c0';
|
const String hash = '55109a4bed52acc780530f7a9aeff6c0';
|
||||||
fakeSkiaClient.expectationForTestValues['flutter.golden_test.1'] = hash;
|
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';
|
fakeSkiaClient.cleanTestNameValues['library.flutter.golden_test.1.png'] = 'flutter.golden_test.1';
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -451,7 +979,7 @@ void main() {
|
|||||||
await expectLater(
|
await expectLater(
|
||||||
() async {
|
() async {
|
||||||
return comparator.compare(
|
return comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse('flutter.golden_test.1'),
|
Uri.parse('flutter.golden_test.1'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -470,25 +998,13 @@ void main() {
|
|||||||
test('passes when bytes match', () async {
|
test('passes when bytes match', () async {
|
||||||
expect(
|
expect(
|
||||||
await comparator.compare(
|
await comparator.compare(
|
||||||
Uint8List.fromList(kTestPngBytes),
|
Uint8List.fromList(_kTestPngBytes),
|
||||||
Uri.parse('flutter.golden_test.1.png'),
|
Uri.parse('flutter.golden_test.1.png'),
|
||||||
),
|
),
|
||||||
isTrue,
|
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 {
|
test('returns FlutterSkippingGoldenFileComparator when network connection is unavailable', () async {
|
||||||
final FakeDirectory fakeDirectory = FakeDirectory();
|
final FakeDirectory fakeDirectory = FakeDirectory();
|
||||||
fakeDirectory.existsSyncValue = true;
|
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:crypto/crypto.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
import 'package:process/process.dart';
|
import 'package:process/process.dart';
|
||||||
@ -139,7 +138,7 @@ class SkiaGoldClient {
|
|||||||
/// The `imgtest` command collects and uploads test results to the Skia Gold
|
/// The `imgtest` command collects and uploads test results to the Skia Gold
|
||||||
/// backend, the `init` argument initializes the current test. Used by the
|
/// backend, the `init` argument initializes the current test. Used by the
|
||||||
/// [FlutterPostSubmitFileComparator].
|
/// [FlutterPostSubmitFileComparator].
|
||||||
Future<void> imgtestInit({ bool isFlaky = false }) async {
|
Future<void> imgtestInit() async {
|
||||||
// This client has already been initialized
|
// This client has already been initialized
|
||||||
if (_initialized) {
|
if (_initialized) {
|
||||||
return;
|
return;
|
||||||
@ -148,7 +147,7 @@ class SkiaGoldClient {
|
|||||||
final File keys = workDirectory.childFile('keys.json');
|
final File keys = workDirectory.childFile('keys.json');
|
||||||
final File failures = workDirectory.childFile('failures.json');
|
final File failures = workDirectory.childFile('failures.json');
|
||||||
|
|
||||||
await keys.writeAsString(getKeysJSON(isFlaky: isFlaky));
|
await keys.writeAsString(_getKeysJSON());
|
||||||
await failures.create();
|
await failures.create();
|
||||||
final String commitHash = await _getCurrentCommit();
|
final String commitHash = await _getCurrentCommit();
|
||||||
|
|
||||||
@ -200,7 +199,7 @@ class SkiaGoldClient {
|
|||||||
///
|
///
|
||||||
/// The [testName] and [goldenFile] parameters reference the current
|
/// The [testName] and [goldenFile] parameters reference the current
|
||||||
/// comparison being evaluated by the [FlutterPostSubmitFileComparator].
|
/// 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>[
|
final List<String> imgtestCommand = <String>[
|
||||||
_goldctl,
|
_goldctl,
|
||||||
'imgtest', 'add',
|
'imgtest', 'add',
|
||||||
@ -210,7 +209,7 @@ class SkiaGoldClient {
|
|||||||
'--test-name', cleanTestName(testName),
|
'--test-name', cleanTestName(testName),
|
||||||
'--png-file', goldenFile.path,
|
'--png-file', goldenFile.path,
|
||||||
'--passfail',
|
'--passfail',
|
||||||
..._getPixelMatchingArguments(isFlaky: isFlaky),
|
..._getPixelMatchingArguments(),
|
||||||
];
|
];
|
||||||
|
|
||||||
final io.ProcessResult result = await process.run(imgtestCommand);
|
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
|
/// The `imgtest` command collects and uploads test results to the Skia Gold
|
||||||
/// backend, the `init` argument initializes the current tryjob. Used by the
|
/// backend, the `init` argument initializes the current tryjob. Used by the
|
||||||
/// [FlutterPreSubmitFileComparator].
|
/// [FlutterPreSubmitFileComparator].
|
||||||
Future<void> tryjobInit({ bool isFlaky = false }) async {
|
Future<void> tryjobInit() async {
|
||||||
// This client has already been initialized
|
// This client has already been initialized
|
||||||
if (_tryjobInitialized) {
|
if (_tryjobInitialized) {
|
||||||
return;
|
return;
|
||||||
@ -269,7 +268,7 @@ class SkiaGoldClient {
|
|||||||
final File keys = workDirectory.childFile('keys.json');
|
final File keys = workDirectory.childFile('keys.json');
|
||||||
final File failures = workDirectory.childFile('failures.json');
|
final File failures = workDirectory.childFile('failures.json');
|
||||||
|
|
||||||
await keys.writeAsString(getKeysJSON(isFlaky: isFlaky));
|
await keys.writeAsString(_getKeysJSON());
|
||||||
await failures.create();
|
await failures.create();
|
||||||
final String commitHash = await _getCurrentCommit();
|
final String commitHash = await _getCurrentCommit();
|
||||||
|
|
||||||
@ -324,7 +323,7 @@ class SkiaGoldClient {
|
|||||||
///
|
///
|
||||||
/// The [testName] and [goldenFile] parameters reference the current
|
/// The [testName] and [goldenFile] parameters reference the current
|
||||||
/// comparison being evaluated by the [FlutterPreSubmitFileComparator].
|
/// 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>[
|
final List<String> imgtestCommand = <String>[
|
||||||
_goldctl,
|
_goldctl,
|
||||||
'imgtest', 'add',
|
'imgtest', 'add',
|
||||||
@ -333,7 +332,7 @@ class SkiaGoldClient {
|
|||||||
.path,
|
.path,
|
||||||
'--test-name', cleanTestName(testName),
|
'--test-name', cleanTestName(testName),
|
||||||
'--png-file', goldenFile.path,
|
'--png-file', goldenFile.path,
|
||||||
..._getPixelMatchingArguments(isFlaky: isFlaky),
|
..._getPixelMatchingArguments(),
|
||||||
];
|
];
|
||||||
|
|
||||||
final io.ProcessResult result = await process.run(imgtestCommand);
|
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.
|
// Constructs arguments for `goldctl` for controlling how pixels are compared.
|
||||||
//
|
//
|
||||||
// For AOT and CanvasKit exact pixel matching is used. For the HTML renderer
|
// 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.
|
// because Chromium cannot exactly reproduce the same golden on all computers.
|
||||||
// It seems to depend on the hardware/OS/driver combination. However, those
|
// It seems to depend on the hardware/OS/driver combination. However, those
|
||||||
// differences are very small (typically not noticeable to human eye).
|
// 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.
|
// Only use fuzzy pixel matching in the HTML renderer.
|
||||||
if (!_isBrowserTest || _isBrowserCanvasKitTest) {
|
if (!_isBrowserTest || _isBrowserCanvasKitTest) {
|
||||||
return const <String>[];
|
return const <String>[];
|
||||||
@ -522,17 +485,17 @@ class SkiaGoldClient {
|
|||||||
/// Currently, the only key value pairs being tracked is the platform the
|
/// 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
|
/// image was rendered on, and for web tests, the browser the image was
|
||||||
/// rendered on.
|
/// rendered on.
|
||||||
@visibleForTesting
|
String _getKeysJSON() {
|
||||||
String getKeysJSON({ bool isFlaky = false}) {
|
|
||||||
final Map<String, dynamic> keys = <String, dynamic>{
|
final Map<String, dynamic> keys = <String, dynamic>{
|
||||||
'Platform' : platform.operatingSystem,
|
'Platform' : platform.operatingSystem,
|
||||||
'CI' : 'luci',
|
'CI' : 'luci',
|
||||||
'markedFlaky' : isFlaky.toString(),
|
|
||||||
};
|
};
|
||||||
if (_isBrowserTest) {
|
if (_isBrowserTest) {
|
||||||
keys['Browser'] = _browserKey;
|
keys['Browser'] = _browserKey;
|
||||||
keys['Platform'] = '${keys['Platform']}-browser';
|
keys['Platform'] = '${keys['Platform']}-browser';
|
||||||
keys['WebRenderer'] = _isBrowserCanvasKitTest ? 'canvaskit' : 'html';
|
if (_isBrowserCanvasKitTest) {
|
||||||
|
keys['WebRenderer'] = 'canvaskit';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return json.encode(keys);
|
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
|
/// * [matchesGoldenFile], the function from [flutter_test] that invokes the
|
||||||
/// comparator.
|
/// comparator.
|
||||||
class DefaultWebGoldenComparator extends WebGoldenComparator {
|
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
|
/// 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);
|
DefaultWebGoldenComparator(this.testUri);
|
||||||
|
|
||||||
/// The test file currently being executed.
|
/// The test file currently being executed.
|
||||||
|
@ -238,14 +238,6 @@ set webGoldenComparator(WebGoldenComparator value) {
|
|||||||
_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
|
/// Whether golden files should be automatically updated during tests rather
|
||||||
/// than compared to the image bytes recorded by the tests.
|
/// than compared to the image bytes recorded by the tests.
|
||||||
///
|
///
|
||||||
|
@ -98,14 +98,14 @@ class TestGoldenComparator {
|
|||||||
return _processManager.start(command, environment: environment);
|
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 File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
|
||||||
final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
|
final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
|
||||||
if (process == null) {
|
if (process == null) {
|
||||||
return 'process was null';
|
return 'process was null';
|
||||||
}
|
}
|
||||||
|
|
||||||
process.sendCommand(imageFile, goldenKey, updateGoldens, customProperties);
|
process.sendCommand(imageFile, goldenKey, updateGoldens);
|
||||||
|
|
||||||
final Map<String, dynamic> result = await process.getResponse();
|
final Map<String, dynamic> result = await process.getResponse();
|
||||||
|
|
||||||
@ -152,13 +152,11 @@ class TestGoldenComparatorProcess {
|
|||||||
await process.exitCode;
|
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>{
|
final Object command = jsonEncode(<String, dynamic>{
|
||||||
'imageFile': imageFile.path,
|
'imageFile': imageFile.path,
|
||||||
'key': goldenKey.toString(),
|
'key': goldenKey.toString(),
|
||||||
'update': updateGoldens,
|
'update': updateGoldens,
|
||||||
if (customProperties != null)
|
|
||||||
'customProperties': customProperties,
|
|
||||||
});
|
});
|
||||||
_logger.printTrace('Preparing to send command: $command');
|
_logger.printTrace('Preparing to send command: $command');
|
||||||
process.stdin.writeln(command);
|
process.stdin.writeln(command);
|
||||||
@ -170,22 +168,7 @@ class TestGoldenComparatorProcess {
|
|||||||
return streamIterator.current;
|
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}) {
|
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);
|
final File? testConfigFile = findTestConfigFile(testFile, logger);
|
||||||
// Generate comparator process for the file.
|
// Generate comparator process for the file.
|
||||||
return '''
|
return '''
|
||||||
@ -228,19 +211,6 @@ void main() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
${testConfigFile != null ? '});' : ''}
|
${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 {
|
Future<shelf.Response> _goldenFileHandler(shelf.Request request) async {
|
||||||
if (request.method == 'POST' && request.url.path.contains('flutter_goldens')) {
|
if (request.url.path.contains('flutter_goldens')) {
|
||||||
final String requestJson = await request.readAsString();
|
final Map<String, Object?> body = json.decode(await request.readAsString()) as Map<String, Object?>;
|
||||||
final Map<String, Object?> body = json.decode(requestJson) 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 width = body['width']! as num;
|
||||||
final num height = body['height']! as num;
|
final num height = body['height']! as num;
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
@ -382,10 +383,7 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
return shelf.Response.ok('Unknown error, bytes is null');
|
return shelf.Response.ok('Unknown error, bytes is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri goldenKey = Uri.parse(body['key']! as String);
|
final String? errorMessage = await _testGoldenComparator.compareGoldens(testUri, bytes, goldenKey, updateGoldens);
|
||||||
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);
|
|
||||||
return shelf.Response.ok(errorMessage ?? 'true');
|
return shelf.Response.ok(errorMessage ?? 'true');
|
||||||
} else {
|
} else {
|
||||||
return shelf.Response.notFound('Not Found');
|
return shelf.Response.notFound('Not Found');
|
||||||
|
@ -9,36 +9,23 @@ import '../base/logger.dart';
|
|||||||
/// test harness if it exists in the project directory hierarchy.
|
/// test harness if it exists in the project directory hierarchy.
|
||||||
const String _kTestConfigFileName = 'flutter_test_config.dart';
|
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
|
/// 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.
|
/// cause the test harness to stop scanning for configuration files.
|
||||||
const String _kProjectRootSentinel = 'pubspec.yaml';
|
const String _kProjectRootSentinel = 'pubspec.yaml';
|
||||||
|
|
||||||
/// Find the `flutter_test_config.dart` file for a specific test file.
|
/// Find the `flutter_test_config.dart` file for a specific test file.
|
||||||
File? findTestConfigFile(File testFile, Logger logger) {
|
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;
|
File? testConfigFile;
|
||||||
Directory directory = testFile.parent;
|
Directory directory = testFile.parent;
|
||||||
while (directory.path != directory.parent.path) {
|
while (directory.path != directory.parent.path) {
|
||||||
final File configFile = directory.childFile(configFileName);
|
final File configFile = directory.childFile(_kTestConfigFileName);
|
||||||
if (configFile.existsSync()) {
|
if (configFile.existsSync()) {
|
||||||
logger.printTrace('Discovered $configFileName in ${directory.path}');
|
logger.printTrace('Discovered $_kTestConfigFileName in ${directory.path}');
|
||||||
testConfigFile = configFile;
|
testConfigFile = configFile;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (directory.childFile(_kProjectRootSentinel).existsSync()) {
|
if (directory.childFile(_kProjectRootSentinel).existsSync()) {
|
||||||
logger.printTrace('Stopping scan for $configFileName; '
|
logger.printTrace('Stopping scan for $_kTestConfigFileName; '
|
||||||
'found project root at ${directory.path}');
|
'found project root at ${directory.path}');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -224,8 +224,7 @@ String generateTestEntrypoint({
|
|||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
ui.debugEmulateFlutterTesterEnvironment = true;
|
ui.debugEmulateFlutterTesterEnvironment = true;
|
||||||
await ui.webOnlyInitializePlatform();
|
await ui.webOnlyInitializePlatform();
|
||||||
webTestUri = Uri.parse('${Uri.file(absolutePath)}');
|
webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}'));
|
||||||
webGoldenComparator = DefaultWebGoldenComparator(webTestUri);
|
|
||||||
(ui.window as dynamic).debugOverrideDevicePixelRatio(3.0);
|
(ui.window as dynamic).debugOverrideDevicePixelRatio(3.0);
|
||||||
(ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800);
|
(ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800);
|
||||||
|
|
||||||
|
@ -41,13 +41,13 @@ void main() {
|
|||||||
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
||||||
|
|
||||||
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess, logger: BufferLogger.test());
|
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 Map<String, dynamic> response = await process.getResponse();
|
||||||
final String stringToStdin = ioSink.getAndClear();
|
final String stringToStdin = ioSink.getAndClear();
|
||||||
|
|
||||||
expect(response, expectedResponse);
|
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 {
|
testWithoutContext('can handle multiple requests', () async {
|
||||||
@ -64,21 +64,18 @@ void main() {
|
|||||||
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
final MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
||||||
|
|
||||||
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess, logger: BufferLogger.test());
|
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();
|
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 Map<String, dynamic> response2 = await process.getResponse();
|
||||||
final String stringToStdin = ioSink.getAndClear();
|
final String stringToStdin = ioSink.getAndClear();
|
||||||
|
|
||||||
expect(response1, expectedResponse1);
|
expect(response1, expectedResponse1);
|
||||||
expect(response2, expectedResponse2);
|
expect(response2, expectedResponse2);
|
||||||
expect(
|
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');
|
||||||
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 {
|
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 MemoryIOSink ioSink = mockProcess.stdin as MemoryIOSink;
|
||||||
|
|
||||||
final TestGoldenComparatorProcess process = TestGoldenComparatorProcess(mockProcess,logger: BufferLogger.test());
|
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 Map<String, dynamic> response = await process.getResponse();
|
||||||
final String stringToStdin = ioSink.getAndClear();
|
final String stringToStdin = ioSink.getAndClear();
|
||||||
|
@ -61,7 +61,7 @@ void main() {
|
|||||||
webRenderer: WebRendererMode.html,
|
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);
|
expect(result, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ void main() {
|
|||||||
webRenderer: WebRendererMode.canvaskit,
|
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');
|
expect(result, 'some message');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -123,10 +123,10 @@ void main() {
|
|||||||
webRenderer: WebRendererMode.html,
|
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');
|
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');
|
expect(result2, 'some other message');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -168,10 +168,10 @@ void main() {
|
|||||||
webRenderer: WebRendererMode.canvaskit,
|
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');
|
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');
|
expect(result2, 'some other message');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ void main() {
|
|||||||
webRenderer: WebRendererMode.html,
|
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);
|
expect(result, null);
|
||||||
|
|
||||||
await comparator.close();
|
await comparator.close();
|
||||||
|
@ -117,7 +117,7 @@ void main() {
|
|||||||
final List<String> allowedPaths = <String>[
|
final List<String> allowedPaths = <String>[
|
||||||
fileSystem.path.join(flutterTools, 'lib', 'src', 'test', 'flutter_platform.dart'),
|
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_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);
|
bool isNotAllowed(FileSystemEntity entity) => allowedPaths.every((String path) => path != entity.path);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user