Pre-Submit Tryjobs for Flutter Gold (#44474)
diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart
index f27dea9..d2fa734 100644
--- a/packages/flutter/lib/src/animation/animation_controller.dart
+++ b/packages/flutter/lib/src/animation/animation_controller.dart
@@ -41,16 +41,17 @@
distance: 0.01,
);
-/// Configures how an [AnimationController] behaves when animations are disabled.
+/// Configures how an [AnimationController] behaves when animations are
+/// disabled.
///
/// When [AccessibilityFeatures.disableAnimations] is true, the device is asking
/// Flutter to reduce or disable animations as much as possible. To honor this,
-/// we reduce the duration and the corresponding number of frames for animations.
-/// This enum is used to allow certain [AnimationController]s to opt out of this
-/// behavior.
+/// we reduce the duration and the corresponding number of frames for
+/// animations. This enum is used to allow certain [AnimationController]s to opt
+/// out of this behavior.
///
/// For example, the [AnimationController] which controls the physics simulation
-/// for a scrollable list will have [AnimationBehavior.preserve] so that when
+/// for a scrollable list will have [AnimationBehavior.preserve], so that when
/// a user attempts to scroll it does not jump to the end/beginning too quickly.
enum AnimationBehavior {
/// The [AnimationController] will reduce its duration when
diff --git a/packages/flutter_goldens/lib/flutter_goldens.dart b/packages/flutter_goldens/lib/flutter_goldens.dart
index defb030..ceaf2f7 100644
--- a/packages/flutter_goldens/lib/flutter_goldens.dart
+++ b/packages/flutter_goldens/lib/flutter_goldens.dart
@@ -129,6 +129,11 @@
Uri getTestUri(Uri key, int version) => key;
/// Calculate the appropriate basedir for the current test context.
+ ///
+ /// The optional [suffix] argument is used by the
+ /// [FlutterSkiaGoldFileComparator] 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 = ''}) {
@@ -217,10 +222,7 @@
platform,
suffix: '${math.Random().nextInt(10000)}',
);
-
- if(!baseDirectory.existsSync()) {
- baseDirectory.createSync(recursive: true);
- }
+ baseDirectory.createSync(recursive: true);
goldens ??= SkiaGoldClient(baseDirectory);
await goldens.auth();
@@ -299,47 +301,23 @@
final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
defaultComparator,
platform,
+ suffix: '${math.Random().nextInt(10000)}',
);
-
- if(!baseDirectory.existsSync()) {
- baseDirectory.createSync(recursive: true);
- }
+ baseDirectory.createSync(recursive: true);
goldens ??= SkiaGoldClient(baseDirectory);
- await goldens.getExpectations();
-
+ await goldens.auth();
+ await goldens.tryjobInit();
return FlutterPreSubmitFileComparator(baseDirectory.uri, goldens);
}
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
golden = _addPrefix(golden);
- final String testName = skiaClient.cleanTestName(golden.path);
- final List<String> testExpectations = skiaClient.expectations[testName];
+ await update(golden, imageBytes);
+ final File goldenFile = getGoldenFile(golden);
- if (testExpectations == null) {
- // There is no baseline for this test
- return true;
- }
-
- ComparisonResult result;
- for (String expectation in testExpectations) {
- final List<int> goldenBytes = await skiaClient.getImageBytes(expectation);
-
- result = GoldenFileComparator.compareLists(
- imageBytes,
- goldenBytes,
- );
-
- if (result.passed) {
- return true;
- }
- }
-
- return skiaClient.testIsIgnoredForPullRequest(
- platform.environment['CIRRUS_PR'] ?? '',
- golden.path,
- );
+ return skiaClient.tryjobAdd(golden.path, goldenFile);
}
/// Decides based on the current environment whether goldens tests should be
diff --git a/packages/flutter_goldens/test/flutter_goldens_test.dart b/packages/flutter_goldens/test/flutter_goldens_test.dart
index 1f8c566..a120f3f 100644
--- a/packages/flutter_goldens/test/flutter_goldens_test.dart
+++ b/packages/flutter_goldens/test/flutter_goldens_test.dart
@@ -84,14 +84,12 @@
group('Request Handling', () {
String testName;
- String pullRequestNumber;
String expectation;
Uri url;
MockHttpClientRequest mockHttpRequest;
setUp(() {
testName = 'flutter.golden_test.1.png';
- pullRequestNumber = '1234';
expectation = '55109a4bed52acc780530f7a9aeff6c0';
mockHttpRequest = MockHttpClientRequest();
});
@@ -212,120 +210,6 @@
expect(masterBytes, equals(_kTestPngBytes));
});
- group('ignores', () {
- Uri url;
- MockHttpClientRequest mockHttpRequest;
- MockHttpClientResponse mockHttpResponse;
-
- setUp(() {
- url = Uri.parse('https://flutter-gold.skia.org/json/ignores');
- mockHttpRequest = MockHttpClientRequest();
- mockHttpResponse = MockHttpClientResponse(utf8.encode(
- ignoreResponseTemplate(
- pullRequestNumber: pullRequestNumber,
- expires: DateTime.now()
- .add(const Duration(days: 1))
- .toString(),
- otherTestName: 'unrelatedTest.1'
- )
- ));
- when(mockHttpClient.getUrl(url))
- .thenAnswer((_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
- when(mockHttpRequest.close())
- .thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
- });
-
- test('returns true for ignored test and ignored pull request number', () async {
- expect(
- await skiaClient.testIsIgnoredForPullRequest(
- pullRequestNumber,
- testName,
- ),
- isTrue,
- );
- });
-
- test('returns true for ignored test and not ignored pull request number', () async {
- expect(
- await skiaClient.testIsIgnoredForPullRequest(
- '5678',
- testName,
- ),
- isTrue,
- );
- });
-
- test('returns false for not ignored test and ignored pull request number', () async {
- expect(
- await skiaClient.testIsIgnoredForPullRequest(
- pullRequestNumber,
- 'failure.png',
- ),
- isFalse,
- );
- });
-
- test('throws exception for expired ignore', () async {
- mockHttpResponse = MockHttpClientResponse(utf8.encode(
- ignoreResponseTemplate(
- pullRequestNumber: pullRequestNumber,
- )
- ));
- when(mockHttpRequest.close())
- .thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
- final Future<bool> test = skiaClient.testIsIgnoredForPullRequest(
- pullRequestNumber,
- testName,
- );
- expect(
- test,
- throwsException,
- );
- });
-
- test('throws exception for first expired ignore among multiple', () async {
- mockHttpResponse = MockHttpClientResponse(utf8.encode(
- ignoreResponseTemplate(
- pullRequestNumber: pullRequestNumber,
- otherExpires: DateTime.now()
- .add(const Duration(days: 1))
- .toString(),
- )
- ));
- when(mockHttpRequest.close())
- .thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
- final Future<bool> test = skiaClient.testIsIgnoredForPullRequest(
- pullRequestNumber,
- testName,
- );
- expect(
- test,
- throwsException,
- );
- });
-
- test('throws exception for later expired ignore among multiple', () async {
- mockHttpResponse = MockHttpClientResponse(utf8.encode(
- ignoreResponseTemplate(
- pullRequestNumber: pullRequestNumber,
- expires: DateTime.now()
- .add(const Duration(days: 1))
- .toString(),
- )
- ));
- when(mockHttpRequest.close())
- .thenAnswer((_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
- final Future<bool> test = skiaClient.testIsIgnoredForPullRequest(
- pullRequestNumber,
- testName,
- );
- expect(
- test,
- throwsException,
- );
- });
- });
-
group('digest parsing', () {
Uri url;
MockHttpClientRequest mockHttpRequest;
@@ -507,39 +391,6 @@
});
group('Pre-Submit', () {
- FlutterPreSubmitFileComparator comparator;
- final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
-
- setUp(() {
- final Directory basedir = fs.directory('flutter/test/library/')
- ..createSync(recursive: true);
- comparator = FlutterPreSubmitFileComparator(
- basedir.uri,
- mockSkiaClient,
- fs: fs,
- platform: FakePlatform(
- environment: <String, String>{
- 'FLUTTER_ROOT': _kFlutterRoot,
- 'CIRRUS_CI' : 'true',
- 'CIRRUS_PR' : '1234',
- 'GOLD_SERVICE_ACCOUNT' : 'service account...'
- },
- operatingSystem: 'macos'
- ),
- );
-
- when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
- .thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
- when(mockSkiaClient.expectations)
- .thenReturn(expectationsTemplate());
- when(mockSkiaClient.cleanTestName('library.flutter.golden_test.1.png'))
- .thenReturn('flutter.golden_test.1');
- when(mockSkiaClient.isValidDigestForExpectation(
- '55109a4bed52acc780530f7a9aeff6c0',
- 'library.flutter.golden_test.1.png',
- ))
- .thenAnswer((_) => Future<bool>.value(false));
- });
group('correctly determines testing environment', () {
test('returns true', () {
platform = FakePlatform(
@@ -601,50 +452,6 @@
);
});
});
-
- test('comparison passes test that is ignored for this PR', () async {
- when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
- .thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
- when(mockSkiaClient.testIsIgnoredForPullRequest(
- '1234',
- 'library.flutter.golden_test.1.png',
- ))
- .thenAnswer((_) => Future<bool>.value(true));
- expect(
- await comparator.compare(
- Uint8List.fromList(_kFailPngBytes),
- Uri.parse('flutter.golden_test.1.png'),
- ),
- isTrue,
- );
- });
-
- test('fails test that is not ignored', () async {
- when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
- .thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
- when(mockSkiaClient.testIsIgnoredForPullRequest(
- '1234',
- 'library.flutter.golden_test.1.png',
- ))
- .thenAnswer((_) => Future<bool>.value(false));
- expect(
- await comparator.compare(
- Uint8List.fromList(_kFailPngBytes),
- Uri.parse('flutter.golden_test.1.png'),
- ),
- isFalse,
- );
- });
-
- test('passes non-existent baseline for new test', () async {
- expect(
- await comparator.compare(
- Uint8List.fromList(_kFailPngBytes),
- Uri.parse('flutter.new_golden_test.1.png'),
- ),
- isTrue,
- );
- });
});
group('Skipping', () {
diff --git a/packages/flutter_goldens/test/json_templates.dart b/packages/flutter_goldens/test/json_templates.dart
index 427bb4e..c74792a 100644
--- a/packages/flutter_goldens/test/json_templates.dart
+++ b/packages/flutter_goldens/test/json_templates.dart
@@ -169,37 +169,6 @@
''';
}
-/// Json response template for Skia Gold ignore request:
-/// https://flutter-gold.skia.org/json/ignores
-String ignoreResponseTemplate({
- String pullRequestNumber = '0000',
- String testName = 'flutter.golden_test.1',
- String otherTestName = 'flutter.golden_test.1',
- String expires = '2019-09-06T21:28:18.815336Z',
- String otherExpires = '2019-09-06T21:28:18.815336Z',
-}) {
- return '''
- [
- {
- "id": "7579425228619212078",
- "name": "contributor@getMail.com",
- "updatedBy": "contributor@getMail.com",
- "expires": "$expires",
- "query": "ext=png&name=$testName",
- "note": "https://github.com/flutter/flutter/pull/$pullRequestNumber"
- },
- {
- "id": "7579425228619212078",
- "name": "contributor@getMail.com",
- "updatedBy": "contributor@getMail.com",
- "expires": "$otherExpires",
- "query": "ext=png&name=$otherTestName",
- "note": "https://github.com/flutter/flutter/pull/99999"
- }
- ]
- ''';
-}
-
/// Json response template for Skia Gold image request:
/// https://flutter-gold.skia.org/img/images/[imageHash].png
List<List<int>> imageResponseTemplate() {
diff --git a/packages/flutter_goldens_client/lib/skia_client.dart b/packages/flutter_goldens_client/lib/skia_client.dart
index aa111bf..b89ddd1 100644
--- a/packages/flutter_goldens_client/lib/skia_client.dart
+++ b/packages/flutter_goldens_client/lib/skia_client.dart
@@ -209,6 +209,96 @@
return true;
}
+ /// Executes the `imgtest init` command in the goldctl tool for tryjobs.
+ ///
+ /// The `imgtest` command collects and uploads test results to the Skia Gold
+ /// backend, the `init` argument initializes the current tryjob.
+ Future<void> tryjobInit() async {
+ final File keys = workDirectory.childFile('keys.json');
+ final File failures = workDirectory.childFile('failures.json');
+
+ await keys.writeAsString(_getKeysJSON());
+ await failures.create();
+ final String commitHash = await _getCurrentCommit();
+ final String pullRequest = platform.environment['CIRRUS_PR'];
+ final String cirrusTaskID = platform.environment['CIRRUS_TASK_ID'];
+
+
+ final List<String> imgtestInitArguments = <String>[
+ 'imgtest', 'init',
+ '--instance', 'flutter',
+ '--work-dir', workDirectory
+ .childDirectory('temp')
+ .path,
+ '--commit', commitHash,
+ '--keys-file', keys.path,
+ '--failure-file', failures.path,
+ '--passfail',
+ '--crs', 'github',
+ '--changelist', pullRequest,
+ '--cis', 'cirrus',
+ '--jobid', cirrusTaskID,
+ '--patchset_id', commitHash,
+ ];
+
+ if (imgtestInitArguments.contains(null)) {
+ final StringBuffer buf = StringBuffer()
+ ..writeln('Null argument for Skia Gold tryjobInit:');
+ imgtestInitArguments.forEach(buf.writeln);
+ throw NonZeroExitCode(1, buf.toString());
+ }
+
+ final io.ProcessResult result = await io.Process.run(
+ _goldctl,
+ imgtestInitArguments,
+ );
+
+ if (result.exitCode != 0) {
+ final StringBuffer buf = StringBuffer()
+ ..writeln('Skia Gold tryjobInit failure.')
+ ..writeln('stdout: ${result.stdout}')
+ ..writeln('stderr: ${result.stderr}');
+ throw NonZeroExitCode(1, buf.toString());
+ }
+ }
+
+ /// Executes the `imgtest add` command in the goldctl tool for tryjobs.
+ ///
+ /// The `imgtest` command collects and uploads test results to the Skia Gold
+ /// backend, the `add` argument uploads the current image test. A response is
+ /// returned from the invocation of this command that indicates a pass or fail
+ /// result for the tryjob.
+ ///
+ /// The testName and goldenFile parameters reference the current comparison
+ /// being evaluated by the [FlutterSkiaGoldFileComparator].
+ Future<bool> tryjobAdd(String testName, File goldenFile) async {
+ assert(testName != null);
+ assert(goldenFile != null);
+
+ final List<String> imgtestArguments = <String>[
+ 'imgtest', 'add',
+ '--work-dir', workDirectory
+ .childDirectory('temp')
+ .path,
+ '--test-name', cleanTestName(testName),
+ '--png-file', goldenFile.path,
+ ];
+
+ final io.ProcessResult result = await io.Process.run(
+ _goldctl,
+ imgtestArguments,
+ );
+
+ if (result.exitCode != 0) {
+ final StringBuffer buf = StringBuffer()
+ ..writeln('Skia Gold tryjobAdd failure.')
+ ..writeln('stdout: ${result.stdout}')
+ ..writeln('stderr: ${result.stderr}\n');
+ throw NonZeroExitCode(1, buf.toString());
+ }
+ return result.exitCode == 0;
+ }
+
/// Requests and sets the [_expectations] known to Flutter Gold at head.
Future<void> getExpectations() async {
_expectations = <String, List<String>>{};
@@ -262,69 +352,6 @@
return imageBytes;
}
- /// Returns a boolean value for whether or not the given test and current pull
- /// request are ignored on Flutter Gold.
- ///
- /// This is only relevant when used by the [FlutterPreSubmitFileComparator]
- /// when a golden file test fails. In order to land a change to an existing
- /// golden file, an ignore must be set up in Flutter Gold. This will serve as
- /// a flag to permit the change to land, protect against any unwanted changes,
- /// and ensure that changes that have landed are triaged.
- Future<bool> testIsIgnoredForPullRequest(String pullRequest, String testName) async {
- bool ignoreIsActive = false;
- testName = cleanTestName(testName);
- String rawResponse;
- await io.HttpOverrides.runWithHttpOverrides<Future<void>>(() async {
- final Uri requestForIgnores = Uri.parse(
- 'https://flutter-gold.skia.org/json/ignores'
- );
-
- try {
- final io.HttpClientRequest request = await httpClient.getUrl(requestForIgnores);
- final io.HttpClientResponse response = await request.close();
- rawResponse = await utf8.decodeStream(response);
- final List<dynamic> ignores = json.decode(rawResponse) as List<dynamic>;
- for(dynamic ignore in ignores) {
- final List<String> ignoredQueries = (ignore['query'] as String).split('&');
- final String ignoredPullRequest = (ignore['note'] as String).split('/').last;
- final DateTime expiration = DateTime.parse(ignore['expires'] as String);
- // The currently failing test is in the process of modification.
- if (ignoredQueries.contains('name=$testName')) {
- if (expiration.isAfter(DateTime.now())) {
- ignoreIsActive = true;
- } else {
- // If any ignore is expired for the given test, throw with
- // guidance.
- final StringBuffer buf = StringBuffer()
- ..writeln('This test has an expired ignore in place, and the')
- ..writeln('change has not been triaged.')
- ..writeln('The associated pull request is:')
- ..writeln('https://github.com/flutter/flutter/pull/$ignoredPullRequest');
- throw NonZeroExitCode(1, buf.toString());
- }
- }
- }
- } on FormatException catch(_) {
- if (rawResponse.contains('stream timeout')) {
- final StringBuffer buf = StringBuffer()
- ..writeln('Stream timeout on /ignores api.')
- ..writeln('This may be caused by a failure to triage a change.')
- ..writeln('Check https://flutter-gold.skia.org/ignores, or')
- ..writeln('https://flutter-gold.skia.org/?query=source_type%3Dflutter')
- ..writeln('for untriaged golden files.');
- throw NonZeroExitCode(1, buf.toString());
- } else {
- print('Formatting error detected requesting /ignores from Flutter Gold.'
- '\nrawResponse: $rawResponse');
- rethrow;
- }
- }
- },
- SkiaGoldHttpOverrides(),
- );
- return ignoreIsActive;
- }
-
/// The [_expectations] retrieved from Flutter Gold do not include the
/// parameters of the given test. This function queries the Flutter Gold
/// details api to determine if the given expectation for a test matches the