Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b7dddee

Browse files
authored
Fail pre-submit if a negative image is encountered as part of goldctl imgtest add. (#51685)
`flutter/engine`-side fix for flutter/flutter#145043. - Before this PR, if a negative image was encountered, we'd silently pass pre-submit, merge, and turn the tree red. - After this PR, a negative image both makes pre and post-submit red. Added tests, and fixed up some unrelated tests that were accidentally setting `pid` instead of `exitCode`. Oops! /cc @zanderso and @eyebrowsoffire (current engine sheriff).
1 parent 01d42ad commit b7dddee

File tree

3 files changed

+98
-22
lines changed

3 files changed

+98
-22
lines changed

testing/skia_gold_client/lib/skia_gold_client.dart

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import 'package:process/process.dart';
1313

1414
import 'src/errors.dart';
1515

16-
export 'src/errors.dart' show SkiaGoldProcessError;
16+
export 'src/errors.dart' show SkiaGoldNegativeImageError, SkiaGoldProcessError;
1717

1818
const String _kGoldctlKey = 'GOLDCTL';
1919
const String _kPresubmitEnvName = 'GOLD_TRYJOB';
@@ -406,30 +406,46 @@ interface class SkiaGoldClient {
406406
_stderr.writeln('stderr:\n${result.stderr}');
407407
}
408408
} else {
409-
// Neither of these conditions are considered failures during tryjobs.
410-
final bool isUntriaged = resultStdout.contains('Untriaged');
411-
final bool isNegative = resultStdout.contains('negative image');
412-
if (!isUntriaged && !isNegative) {
413-
final StringBuffer buf = StringBuffer()
414-
..writeln('Unexpected Gold tryjobAdd failure.')
415-
..writeln('Tryjob execution for golden file test $testName failed for')
416-
..writeln('a reason unrelated to pixel comparison.');
417-
throw SkiaGoldProcessError(
418-
command: tryjobCommand,
419-
stdout: resultStdout,
420-
stderr: result.stderr.toString(),
421-
message: buf.toString(),
422-
);
423-
}
409+
// Untriaged images are not considered failures during tryjobs.
424410
// ... but we want to know about them anyway.
425411
// See https://github.com/flutter/flutter/issues/145219.
426412
// TODO(matanlurey): Update the documentation to reflect the new behavior.
413+
final bool isUntriaged = resultStdout.contains('Untriaged');
427414
if (isUntriaged) {
428415
_stderr
429416
..writeln('NOTE: Untriaged image detected in tryjob.')
430417
..writeln('Triage should be required by the "Flutter Gold" check')
431418
..writeln('stdout:\n$resultStdout');
419+
return;
432420
}
421+
422+
// Negative images are considered failures during tryjobs.
423+
//
424+
// We don't actually use negative images as part of our workflow, but
425+
// if they *are* used (i.e. someone accidentally marks a digest a
426+
// negative image), we want to fail the tryjob - otherwise we just end up
427+
// with a negative image in the tree (a post-submit failure).
428+
final bool isNegative = resultStdout.contains('negative image');
429+
if (isNegative) {
430+
throw SkiaGoldNegativeImageError(
431+
testName: testName,
432+
command: tryjobCommand,
433+
stdout: resultStdout,
434+
stderr: result.stderr.toString(),
435+
);
436+
}
437+
438+
// If the tryjob failed for some other reason, throw an error.
439+
final StringBuffer buf = StringBuffer()
440+
..writeln('Unexpected Gold tryjobAdd failure.')
441+
..writeln('Tryjob execution for golden file test $testName failed for')
442+
..writeln('a reason unrelated to pixel comparison.');
443+
throw SkiaGoldProcessError(
444+
command: tryjobCommand,
445+
stdout: resultStdout,
446+
stderr: result.stderr.toString(),
447+
message: buf.toString(),
448+
);
433449
}
434450
}
435451

testing/skia_gold_client/lib/src/errors.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,26 @@ final class SkiaGoldProcessError extends Error {
5151
].join('\n');
5252
}
5353
}
54+
55+
/// Error thrown when the Skia Gold process exits due to a negative image.
56+
final class SkiaGoldNegativeImageError extends SkiaGoldProcessError {
57+
/// Creates a new [SkiaGoldNegativeImageError] from the provided origin.
58+
///
59+
/// See [SkiaGoldProcessError.new] for more information.
60+
SkiaGoldNegativeImageError({
61+
required this.testName,
62+
required super.command,
63+
required super.stdout,
64+
required super.stderr,
65+
});
66+
67+
/// Name of the test that produced the negative image.
68+
final String testName;
69+
70+
@override
71+
String get message => 'Negative image detected for test: "$testName".\n\n'
72+
'The flutter/engine workflow should never produce negative images; it is '
73+
'possible that someone accidentally (or without knowing our policy) '
74+
'marked a test as negative.\n\n'
75+
'See https://github.com/flutter/flutter/issues/145043 for details.';
76+
}

testing/skia_gold_client/test/skia_gold_client_test.dart

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,41 @@ void main() {
336336
}
337337
});
338338

339+
test('addImg [pre-submit] fails due to a negative image', () async {
340+
final _TestFixture fixture = _TestFixture();
341+
try {
342+
final SkiaGoldClient client = createClient(
343+
fixture,
344+
environment: presubmitEnv,
345+
onRun: (List<String> command) {
346+
if (command case ['git', ...]) {
347+
return io.ProcessResult(0, 0, mockCommitHash, '');
348+
}
349+
if (command case ['python tools/goldctl.py', 'imgtest', 'init', ...]) {
350+
return io.ProcessResult(0, 0, '', '');
351+
}
352+
return io.ProcessResult(0, 1, 'negative image', 'stderr');
353+
},
354+
);
355+
356+
try {
357+
await client.addImg(
358+
'test-name.foo',
359+
io.File(p.join(fixture.workDirectory.path, 'temp', 'golden.png')),
360+
screenshotSize: 1000,
361+
);
362+
fail('addImg should fail due to a negative image');
363+
} on SkiaGoldNegativeImageError catch (error) {
364+
expect(error.testName, 'test-name.foo');
365+
expect(error.stdout, 'negative image');
366+
expect(error.stderr, 'stderr');
367+
expect(error.command.join(' '), contains('imgtest add'));
368+
}
369+
} finally {
370+
fixture.dispose();
371+
}
372+
});
373+
339374
test('addImg [pre-submit] fails due to an unexpected error', () async {
340375
final _TestFixture fixture = _TestFixture();
341376
try {
@@ -349,7 +384,7 @@ void main() {
349384
if (command case ['python tools/goldctl.py', 'imgtest', 'init', ...]) {
350385
return io.ProcessResult(0, 0, '', '');
351386
}
352-
return io.ProcessResult(1, 0, 'stdout-text', 'stderr-text');
387+
return io.ProcessResult(0, 1, 'stdout-text', 'stderr-text');
353388
},
354389
);
355390

@@ -359,11 +394,12 @@ void main() {
359394
io.File(p.join(fixture.workDirectory.path, 'temp', 'golden.png')),
360395
screenshotSize: 1000,
361396
);
397+
fail('addImg should fail due to an unexpected error');
362398
} on SkiaGoldProcessError catch (error) {
363-
expect(error.message, contains('Skia Gold image test failed.'));
399+
expect(error.message, contains('Unexpected Gold tryjobAdd failure.'));
364400
expect(error.stdout, 'stdout-text');
365401
expect(error.stderr, 'stderr-text');
366-
expect(error.command, contains('imgtest add'));
402+
expect(error.command.join(' '), contains('imgtest add'));
367403
}
368404
} finally {
369405
fixture.dispose();
@@ -478,7 +514,7 @@ void main() {
478514
if (command case ['python tools/goldctl.py', 'imgtest', 'init', ...]) {
479515
return io.ProcessResult(0, 0, '', '');
480516
}
481-
return io.ProcessResult(1, 0, 'stdout-text', 'stderr-text');
517+
return io.ProcessResult(0, 1, 'stdout-text', 'stderr-text');
482518
},
483519
);
484520

@@ -488,11 +524,12 @@ void main() {
488524
io.File(p.join(fixture.workDirectory.path, 'temp', 'golden.png')),
489525
screenshotSize: 1000,
490526
);
527+
fail('addImg should fail due to an unapproved image');
491528
} on SkiaGoldProcessError catch (error) {
492-
expect(error.message, contains('Skia Gold image test failed.'));
529+
expect(error.message, contains('Skia Gold received an unapproved image'));
493530
expect(error.stdout, 'stdout-text');
494531
expect(error.stderr, 'stderr-text');
495-
expect(error.command, contains('imgtest add'));
532+
expect(error.command.join(' '), contains('imgtest add'));
496533
}
497534
} finally {
498535
fixture.dispose();

0 commit comments

Comments
 (0)