Unverified Commit a4e3f933 authored by Kostia Sokolovskyi's avatar Kostia Sokolovskyi Committed by GitHub

Fix memory leak in _MatchesReferenceImage (#135150)

parent 579e1960
...@@ -576,7 +576,9 @@ AsyncMatcher matchesGoldenFile(Object key, {int? version}) { ...@@ -576,7 +576,9 @@ AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
/// final ui.Canvas pictureCanvas = ui.Canvas(recorder); /// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint); /// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
/// final ui.Picture picture = recorder.endRecording(); /// final ui.Picture picture = recorder.endRecording();
/// addTearDown(picture.dispose);
/// ui.Image referenceImage = await picture.toImage(50, 50); /// ui.Image referenceImage = await picture.toImage(50, 50);
/// addTearDown(referenceImage.dispose);
/// ///
/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage)); /// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
/// await expectLater(image, matchesReferenceImage(referenceImage)); /// await expectLater(image, matchesReferenceImage(referenceImage));
...@@ -2139,10 +2141,13 @@ class _MatchesReferenceImage extends AsyncMatcher { ...@@ -2139,10 +2141,13 @@ class _MatchesReferenceImage extends AsyncMatcher {
@override @override
Future<String?> matchAsync(dynamic item) async { Future<String?> matchAsync(dynamic item) async {
Future<ui.Image> imageFuture; Future<ui.Image> imageFuture;
final bool disposeImage; // set to true if the matcher created and owns the image and must therefore dispose it.
if (item is Future<ui.Image>) { if (item is Future<ui.Image>) {
imageFuture = item; imageFuture = item;
disposeImage = false;
} else if (item is ui.Image) { } else if (item is ui.Image) {
imageFuture = Future<ui.Image>.value(item); imageFuture = Future<ui.Image>.value(item);
disposeImage = false;
} else { } else {
final Finder finder = item as Finder; final Finder finder = item as Finder;
final Iterable<Element> elements = finder.evaluate(); final Iterable<Element> elements = finder.evaluate();
...@@ -2152,30 +2157,37 @@ class _MatchesReferenceImage extends AsyncMatcher { ...@@ -2152,30 +2157,37 @@ class _MatchesReferenceImage extends AsyncMatcher {
return 'matched too many widgets'; return 'matched too many widgets';
} }
imageFuture = captureImage(elements.single); imageFuture = captureImage(elements.single);
disposeImage = true;
} }
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
return binding.runAsync<String?>(() async { return binding.runAsync<String?>(() async {
final ui.Image image = await imageFuture; final ui.Image image = await imageFuture;
final ByteData? bytes = await image.toByteData(); try {
if (bytes == null) { final ByteData? bytes = await image.toByteData();
return 'could not be encoded.'; if (bytes == null) {
} return 'could not be encoded.';
}
final ByteData? referenceBytes = await referenceImage.toByteData(); final ByteData? referenceBytes = await referenceImage.toByteData();
if (referenceBytes == null) { if (referenceBytes == null) {
return 'could not have its reference image encoded.'; return 'could not have its reference image encoded.';
} }
if (referenceImage.height != image.height || referenceImage.width != image.width) { if (referenceImage.height != image.height || referenceImage.width != image.width) {
return 'does not match as width or height do not match. $image != $referenceImage'; return 'does not match as width or height do not match. $image != $referenceImage';
} }
final int countDifferentPixels = _countDifferentPixels( final int countDifferentPixels = _countDifferentPixels(
Uint8List.view(bytes.buffer), Uint8List.view(bytes.buffer),
Uint8List.view(referenceBytes.buffer), Uint8List.view(referenceBytes.buffer),
); );
return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels'; return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
} finally {
if (disposeImage) {
image.dispose();
}
}
}); });
} }
......
...@@ -44,5 +44,43 @@ dependencies: ...@@ -44,5 +44,43 @@ dependencies:
dev_dependencies: dev_dependencies:
file: 6.1.4 file: 6.1.4
# Used to detect memory leaks.
leak_tracker_flutter_testing: 1.0.5
# PUBSPEC CHECKSUM: 9e5d _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_multi_server: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
intl: 0.18.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
io: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker: 9.0.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker_testing: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test: 1.24.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
# PUBSPEC CHECKSUM: aa8d
...@@ -4,9 +4,12 @@ ...@@ -4,9 +4,12 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
Future<ui.Image> createTestImage(int width, int height, ui.Color color) { Future<ui.Image> createTestImage(int width, int height, ui.Color color) async {
final ui.Paint paint = ui.Paint() final ui.Paint paint = ui.Paint()
..style = ui.PaintingStyle.stroke ..style = ui.PaintingStyle.stroke
..strokeWidth = 1.0 ..strokeWidth = 1.0
...@@ -15,7 +18,9 @@ Future<ui.Image> createTestImage(int width, int height, ui.Color color) { ...@@ -15,7 +18,9 @@ Future<ui.Image> createTestImage(int width, int height, ui.Color color) {
final ui.Canvas pictureCanvas = ui.Canvas(recorder); final ui.Canvas pictureCanvas = ui.Canvas(recorder);
pictureCanvas.drawCircle(Offset.zero, 20.0, paint); pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
final ui.Picture picture = recorder.endRecording(); final ui.Picture picture = recorder.endRecording();
return picture.toImage(width, height); final ui.Image image = await picture.toImage(width, height);
picture.dispose();
return image;
} }
void main() { void main() {
...@@ -24,50 +29,121 @@ void main() { ...@@ -24,50 +29,121 @@ void main() {
const ui.Color transparentRed = ui.Color.fromARGB(128, 255, 0, 0); const ui.Color transparentRed = ui.Color.fromARGB(128, 255, 0, 0);
group('succeeds', () { group('succeeds', () {
testWidgets('when images have the same content', (WidgetTester tester) async { testWidgetsWithLeakTracking('when images have the same content', (WidgetTester tester) async {
await expectLater( final ui.Image image1 = await createTestImage(100, 100, red);
await createTestImage(100, 100, red), addTearDown(image1.dispose);
matchesReferenceImage(await createTestImage(100, 100, red)), final ui.Image referenceImage1 = await createTestImage(100, 100, red);
); addTearDown(referenceImage1.dispose);
await expectLater(
await createTestImage(100, 100, green),
matchesReferenceImage(await createTestImage(100, 100, green)),
);
await expectLater( await expectLater(image1, matchesReferenceImage(referenceImage1));
await createTestImage(100, 100, transparentRed),
matchesReferenceImage(await createTestImage(100, 100, transparentRed)), final ui.Image image2 = await createTestImage(100, 100, green);
); addTearDown(image2.dispose);
final ui.Image referenceImage2 = await createTestImage(100, 100, green);
addTearDown(referenceImage2.dispose);
await expectLater(image2, matchesReferenceImage(referenceImage2));
final ui.Image image3 = await createTestImage(100, 100, transparentRed);
addTearDown(image3.dispose);
final ui.Image referenceImage3 = await createTestImage(100, 100, transparentRed);
addTearDown(referenceImage3.dispose);
await expectLater(image3, matchesReferenceImage(referenceImage3));
}); });
testWidgets('when images are identical', (WidgetTester tester) async { testWidgetsWithLeakTracking('when images are identical', (WidgetTester tester) async {
final ui.Image image = await createTestImage(100, 100, red); final ui.Image image = await createTestImage(100, 100, red);
addTearDown(image.dispose);
await expectLater(image, matchesReferenceImage(image)); await expectLater(image, matchesReferenceImage(image));
}); });
testWidgetsWithLeakTracking('when widget looks the same', (WidgetTester tester) async {
addTearDown(tester.view.reset);
tester.view
..physicalSize = const Size(10, 10)
..devicePixelRatio = 1;
const ValueKey<String> repaintBoundaryKey = ValueKey<String>('boundary');
await tester.pumpWidget(
const RepaintBoundary(
key: repaintBoundaryKey,
child: ColoredBox(color: red),
),
);
final ui.Image referenceImage = (tester.renderObject(find.byKey(repaintBoundaryKey)) as RenderRepaintBoundary).toImageSync();
addTearDown(referenceImage.dispose);
await expectLater(find.byKey(repaintBoundaryKey), matchesReferenceImage(referenceImage));
});
}); });
group('fails', () { group('fails', () {
testWidgets('when image sizes do not match', (WidgetTester tester) async { testWidgetsWithLeakTracking('when image sizes do not match', (WidgetTester tester) async {
final ui.Image red50 = await createTestImage(50, 50, red); final ui.Image red50 = await createTestImage(50, 50, red);
addTearDown(red50.dispose);
final ui.Image red100 = await createTestImage(100, 100, red); final ui.Image red100 = await createTestImage(100, 100, red);
addTearDown(red100.dispose);
expect( expect(
await matchesReferenceImage(red50).matchAsync(red100), await matchesReferenceImage(red50).matchAsync(red100),
equals('does not match as width or height do not match. [100×100] != [50×50]'), equals('does not match as width or height do not match. [100×100] != [50×50]'),
); );
}); });
testWidgets('when image pixels do not match', (WidgetTester tester) async { testWidgetsWithLeakTracking('when image pixels do not match', (WidgetTester tester) async {
final ui.Image red100 = await createTestImage(100, 100, red); final ui.Image red100 = await createTestImage(100, 100, red);
addTearDown(red100.dispose);
final ui.Image transparentRed100 = await createTestImage(100, 100, transparentRed); final ui.Image transparentRed100 = await createTestImage(100, 100, transparentRed);
addTearDown(transparentRed100.dispose);
expect( expect(
await matchesReferenceImage(red100).matchAsync(transparentRed100), await matchesReferenceImage(red100).matchAsync(transparentRed100),
equals('does not match on 57 pixels'), equals('does not match on 57 pixels'),
); );
final ui.Image green100 = await createTestImage(100, 100, green); final ui.Image green100 = await createTestImage(100, 100, green);
addTearDown(green100.dispose);
expect( expect(
await matchesReferenceImage(red100).matchAsync(green100), await matchesReferenceImage(red100).matchAsync(green100),
equals('does not match on 57 pixels'), equals('does not match on 57 pixels'),
); );
}); });
testWidgetsWithLeakTracking('when widget does not look the same', (WidgetTester tester) async {
addTearDown(tester.view.reset);
tester.view
..physicalSize = const Size(10, 10)
..devicePixelRatio = 1;
const ValueKey<String> repaintBoundaryKey = ValueKey<String>('boundary');
await tester.pumpWidget(
const RepaintBoundary(
key: repaintBoundaryKey,
child: ColoredBox(color: red),
),
);
final ui.Image referenceImage = (tester.renderObject(find.byKey(repaintBoundaryKey)) as RenderRepaintBoundary).toImageSync();
addTearDown(referenceImage.dispose);
await tester.pumpWidget(
const RepaintBoundary(
key: repaintBoundaryKey,
child: ColoredBox(color: green),
),
);
expect(
await matchesReferenceImage(referenceImage).matchAsync(
find.byKey(repaintBoundaryKey),
),
equals('does not match on 100 pixels'),
);
});
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment