Fix memory leak in _MatchesReferenceImage (#135150)
This commit is contained in:
parent
579e196073
commit
a4e3f93367
@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user