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}) {
/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
/// final ui.Picture picture = recorder.endRecording();
/// addTearDown(picture.dispose);
/// ui.Image referenceImage = await picture.toImage(50, 50);
/// addTearDown(referenceImage.dispose);
///
/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
/// await expectLater(image, matchesReferenceImage(referenceImage));
......@@ -2139,10 +2141,13 @@ class _MatchesReferenceImage extends AsyncMatcher {
@override
Future<String?> matchAsync(dynamic item) async {
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>) {
imageFuture = item;
disposeImage = false;
} else if (item is ui.Image) {
imageFuture = Future<ui.Image>.value(item);
disposeImage = false;
} else {
final Finder finder = item as Finder;
final Iterable<Element> elements = finder.evaluate();
......@@ -2152,30 +2157,37 @@ class _MatchesReferenceImage extends AsyncMatcher {
return 'matched too many widgets';
}
imageFuture = captureImage(elements.single);
disposeImage = true;
}
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
return binding.runAsync<String?>(() async {
final ui.Image image = await imageFuture;
final ByteData? bytes = await image.toByteData();
if (bytes == null) {
return 'could not be encoded.';
}
try {
final ByteData? bytes = await image.toByteData();
if (bytes == null) {
return 'could not be encoded.';
}
final ByteData? referenceBytes = await referenceImage.toByteData();
if (referenceBytes == null) {
return 'could not have its reference image encoded.';
}
final ByteData? referenceBytes = await referenceImage.toByteData();
if (referenceBytes == null) {
return 'could not have its reference image encoded.';
}
if (referenceImage.height != image.height || referenceImage.width != image.width) {
return 'does not match as width or height do not match. $image != $referenceImage';
}
if (referenceImage.height != image.height || referenceImage.width != image.width) {
return 'does not match as width or height do not match. $image != $referenceImage';
}
final int countDifferentPixels = _countDifferentPixels(
Uint8List.view(bytes.buffer),
Uint8List.view(referenceBytes.buffer),
);
return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
final int countDifferentPixels = _countDifferentPixels(
Uint8List.view(bytes.buffer),
Uint8List.view(referenceBytes.buffer),
);
return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
} finally {
if (disposeImage) {
image.dispose();
}
}
});
}
......
......@@ -44,5 +44,43 @@ dependencies:
dev_dependencies:
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 @@
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.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()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1.0
......@@ -15,7 +18,9 @@ Future<ui.Image> createTestImage(int width, int height, ui.Color color) {
final ui.Canvas pictureCanvas = ui.Canvas(recorder);
pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
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() {
......@@ -24,50 +29,121 @@ void main() {
const ui.Color transparentRed = ui.Color.fromARGB(128, 255, 0, 0);
group('succeeds', () {
testWidgets('when images have the same content', (WidgetTester tester) async {
await expectLater(
await createTestImage(100, 100, red),
matchesReferenceImage(await createTestImage(100, 100, red)),
);
await expectLater(
await createTestImage(100, 100, green),
matchesReferenceImage(await createTestImage(100, 100, green)),
);
testWidgetsWithLeakTracking('when images have the same content', (WidgetTester tester) async {
final ui.Image image1 = await createTestImage(100, 100, red);
addTearDown(image1.dispose);
final ui.Image referenceImage1 = await createTestImage(100, 100, red);
addTearDown(referenceImage1.dispose);
await expectLater(
await createTestImage(100, 100, transparentRed),
matchesReferenceImage(await createTestImage(100, 100, transparentRed)),
);
await expectLater(image1, matchesReferenceImage(referenceImage1));
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);
addTearDown(image.dispose);
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', () {
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);
addTearDown(red50.dispose);
final ui.Image red100 = await createTestImage(100, 100, red);
addTearDown(red100.dispose);
expect(
await matchesReferenceImage(red50).matchAsync(red100),
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);
addTearDown(red100.dispose);
final ui.Image transparentRed100 = await createTestImage(100, 100, transparentRed);
addTearDown(transparentRed100.dispose);
expect(
await matchesReferenceImage(red100).matchAsync(transparentRed100),
equals('does not match on 57 pixels'),
);
final ui.Image green100 = await createTestImage(100, 100, green);
addTearDown(green100.dispose);
expect(
await matchesReferenceImage(red100).matchAsync(green100),
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