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);
|
/// 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 {
|
|||||||
@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,11 +2157,13 @@ 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;
|
||||||
|
try {
|
||||||
final ByteData? bytes = await image.toByteData();
|
final ByteData? bytes = await image.toByteData();
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
return 'could not be encoded.';
|
return 'could not be encoded.';
|
||||||
@ -2176,6 +2183,11 @@ class _MatchesReferenceImage extends AsyncMatcher {
|
|||||||
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:
|
|||||||
|
|
||||||
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 @@
|
|||||||
|
|
||||||
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) {
|
|||||||
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() {
|
|||||||
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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user