From 27418b7ac9987b26a4d541ef0d8b16b930ba806c Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sun, 5 Jan 2025 22:07:24 +0800 Subject: [PATCH 1/4] Add leak testing --- packages/rfw/pubspec.yaml | 1 + packages/rfw/test/flutter_test_config.dart | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 packages/rfw/test/flutter_test_config.dart diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index d152a36c700d..a21a8dc73938 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -17,6 +17,7 @@ dev_dependencies: flutter_test: sdk: flutter lcov_parser: 0.1.2 + leak_tracker_flutter_testing: any topics: - ui diff --git a/packages/rfw/test/flutter_test_config.dart b/packages/rfw/test/flutter_test_config.dart new file mode 100644 index 000000000000..9907e578b84b --- /dev/null +++ b/packages/rfw/test/flutter_test_config.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + LeakTesting.enable(); + LeakTracking.warnForUnsupportedPlatforms = false; + await testMain(); +} From bd9d45d830003a3264d568585e7ad46362efaebc Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sun, 5 Jan 2025 22:07:42 +0800 Subject: [PATCH 2/4] Fix memory leaks in tests --- packages/rfw/test/argument_decoders_test.dart | 2 + packages/rfw/test/core_widgets_test.dart | 2 + packages/rfw/test/material_widgets_test.dart | 4 +- packages/rfw/test/readme_test.dart | 1 + packages/rfw/test/remote_widget_test.dart | 2 + packages/rfw/test/runtime_test.dart | 37 +++++++++++++++++++ packages/rfw/test/source_locations_test.dart | 1 + 7 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/rfw/test/argument_decoders_test.dart b/packages/rfw/test/argument_decoders_test.dart index 7da1e01aae71..f29b0fc093e1 100644 --- a/packages/rfw/test/argument_decoders_test.dart +++ b/packages/rfw/test/argument_decoders_test.dart @@ -70,6 +70,7 @@ void main() { }, })) ..update(const LibraryName(['test']), parseLibraryFile('import core; widget root = SizedBox();')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( @@ -235,6 +236,7 @@ void main() { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()) ..update(const LibraryName(['test']), parseLibraryFile('import core; widget root = SizedBox();')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( diff --git a/packages/rfw/test/core_widgets_test.dart b/packages/rfw/test/core_widgets_test.dart index 75f5141c1c1e..33ae0d7021a0 100644 --- a/packages/rfw/test/core_widgets_test.dart +++ b/packages/rfw/test/core_widgets_test.dart @@ -16,6 +16,7 @@ void main() { testWidgets('Core widgets', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( @@ -248,6 +249,7 @@ void main() { testWidgets('More core widgets', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( diff --git a/packages/rfw/test/material_widgets_test.dart b/packages/rfw/test/material_widgets_test.dart index 12fb4aea9035..5975ec2b1f8a 100644 --- a/packages/rfw/test/material_widgets_test.dart +++ b/packages/rfw/test/material_widgets_test.dart @@ -25,9 +25,11 @@ void main() { const LibraryName testName = LibraryName(['test']); Runtime setupRuntime() { - return Runtime() + final Runtime runtime = Runtime() ..update(coreName, createCoreWidgets()) ..update(materialName, createMaterialWidgets()); + addTearDown(runtime.dispose); + return runtime; } setUpAll(() { diff --git a/packages/rfw/test/readme_test.dart b/packages/rfw/test/readme_test.dart index 7969d86eae4c..8e1f769d5264 100644 --- a/packages/rfw/test/readme_test.dart +++ b/packages/rfw/test/readme_test.dart @@ -285,6 +285,7 @@ void main() { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()) ..update(const LibraryName(['material']), createMaterialWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(parseDataFile(gameData)); for (final String region in rawRemoteWidgetSnippets.keys) { final String body = rawRemoteWidgetSnippets[region]!; diff --git a/packages/rfw/test/remote_widget_test.dart b/packages/rfw/test/remote_widget_test.dart index a83f569961f7..894777355198 100644 --- a/packages/rfw/test/remote_widget_test.dart +++ b/packages/rfw/test/remote_widget_test.dart @@ -15,12 +15,14 @@ void main() { import core; widget root = Placeholder(); ''')); + addTearDown(runtime1.dispose); final Runtime runtime2 = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()) ..update(const LibraryName(['test']), parseLibraryFile(''' import core; widget root = Container(); ''')); + addTearDown(runtime2.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( diff --git a/packages/rfw/test/runtime_test.dart b/packages/rfw/test/runtime_test.dart index 05eeb05a1bac..070f37695c63 100644 --- a/packages/rfw/test/runtime_test.dart +++ b/packages/rfw/test/runtime_test.dart @@ -18,6 +18,7 @@ void main() { testWidgets('list lookup', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({ 'list': [ 0, 1, 2, 3, 4 ], }); @@ -55,6 +56,7 @@ void main() { return const SizedBox.shrink(); }, })); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({ 'list': [ { 'a': 0 }, @@ -131,6 +133,7 @@ void main() { testWidgets('updateText, updateBinary, clearLibraries', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -167,6 +170,7 @@ void main() { testWidgets('Runtime cached build', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -203,6 +207,7 @@ void main() { ..update(const LibraryName(['b']), parseLibraryFile(''' import a; ''')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -219,6 +224,7 @@ void main() { ..update(const LibraryName(['a']), parseLibraryFile(''' import a; ''')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -235,6 +241,7 @@ void main() { ..update(const LibraryName(['a']), parseLibraryFile(''' import b; ''')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -249,6 +256,7 @@ void main() { testWidgets('Missing libraries in specified widget', (WidgetTester tester) async { final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -267,6 +275,7 @@ void main() { import b; widget widget = test(); ''')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -282,6 +291,7 @@ void main() { testWidgets('Missing widget', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['a']), parseLibraryFile('')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -297,6 +307,7 @@ void main() { testWidgets('Runtime', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -327,6 +338,7 @@ void main() { testWidgets('Runtime', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -350,6 +362,7 @@ void main() { testWidgets('Runtime', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); expect(runtime.libraries.length, 1); final LibraryName libraryName = runtime.libraries.entries.first.key; expect('$libraryName', 'core'); @@ -362,6 +375,7 @@ void main() { testWidgets('Runtime', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -389,6 +403,7 @@ void main() { testWidgets('DynamicContent', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -435,6 +450,7 @@ void main() { testWidgets('binding loop variables', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({ 'list': [ { @@ -600,6 +616,7 @@ void main() { testWidgets('list lookup of esoteric values', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -720,6 +737,7 @@ void main() { testWidgets('data lookup', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({ 'map': { 'list': [ 0xAB ] }, }); @@ -744,6 +762,7 @@ void main() { testWidgets('args lookup', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -767,6 +786,7 @@ void main() { testWidgets('state lookup', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -789,6 +809,7 @@ void main() { testWidgets('switch', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -824,6 +845,7 @@ void main() { testWidgets('events with arguments', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( @@ -872,6 +894,7 @@ void main() { testWidgets('_CurriedWidget toStrings', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); runtime.update(const LibraryName(['test']), parseLibraryFile(''' import core; @@ -911,6 +934,7 @@ void main() { testWidgets('state setting', (WidgetTester tester) async { final Runtime runtime = Runtime() ..update(const LibraryName(['core']), createCoreWidgets()); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -1029,6 +1053,7 @@ void main() { testWidgets('DataSource', (WidgetTester tester) async { final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( @@ -1094,6 +1119,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); runtime.update(coreLibraryName, createCoreWidgets()); runtime.update(localLibraryName, LocalWidgetLibrary( { @@ -1127,6 +1153,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); const String expectedErrorMessage = 'Not a builder at [builder] (got core:Text {} {text: Not a builder :/}) for local:Builder.'; @@ -1163,6 +1190,7 @@ void main() { }; final Runtime runtime = Runtime() ..update(const LibraryName(['a']), parseLibraryFile('')); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); await tester.pumpWidget( RemoteWidget( @@ -1182,6 +1210,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final Finder textFinder = find.byType(Text); @@ -1214,6 +1243,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final Finder textFinder = find.byType(Text); @@ -1247,6 +1277,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final Finder textFinder = find.byType(Text); @@ -1283,6 +1314,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({'value': 0}); final Finder textFinder = find.byType(Text); @@ -1322,6 +1354,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final List dispatchedEvents = []; final Finder textFinder = find.byType(Text); @@ -1363,6 +1396,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final Finder textFinder = find.byType(Text); runtime.update(coreLibraryName, createCoreWidgets()); @@ -1408,6 +1442,7 @@ void main() { const LibraryName remoteLibraryName = LibraryName(['remote']); final Map handlers = {}; final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({ 'a1': 'apricot', 'b1': 'blueberry', @@ -1474,6 +1509,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final Finder textFinder = find.byType(Text); @@ -1522,6 +1558,7 @@ void main() { const LibraryName localLibraryName = LibraryName(['local']); const LibraryName remoteLibraryName = LibraryName(['remote']); final Runtime runtime = Runtime(); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent(); final Finder textFinder = find.byType(Text); runtime.update(coreLibraryName, createCoreWidgets()); diff --git a/packages/rfw/test/source_locations_test.dart b/packages/rfw/test/source_locations_test.dart index 284cca39b656..3c42df0c4b09 100644 --- a/packages/rfw/test/source_locations_test.dart +++ b/packages/rfw/test/source_locations_test.dart @@ -55,6 +55,7 @@ widget verify { state: true } = switch args.value.c.0 { // We use the actual source text as the sourceIdentifier to make it trivial to find the source contents. // In normal operation, the sourceIdentifier would be the file name or some similar object. ..update(const LibraryName(['test']), parseLibraryFile(sourceFile, sourceIdentifier: sourceFile)); + addTearDown(runtime.dispose); final DynamicContent data = DynamicContent({ 'list': [ { From d3934197b654102808de26bd74c907b708f06020 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sat, 18 Jan 2025 17:07:04 +0800 Subject: [PATCH 3/4] Add creation stack trace to debug on ci --- packages/rfw/test/flutter_test_config.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rfw/test/flutter_test_config.dart b/packages/rfw/test/flutter_test_config.dart index 9907e578b84b..b069eaff598a 100644 --- a/packages/rfw/test/flutter_test_config.dart +++ b/packages/rfw/test/flutter_test_config.dart @@ -9,5 +9,7 @@ import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Future testExecutable(FutureOr Function() testMain) async { LeakTesting.enable(); LeakTracking.warnForUnsupportedPlatforms = false; + LeakTesting.settings = LeakTesting.settings + .withCreationStackTrace(); // To debug failing test on CI. await testMain(); } From 89fe441c26909994f2182e309108c47aecff2d39 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sat, 18 Jan 2025 19:26:36 +0800 Subject: [PATCH 4/4] fix memory leak in test --- packages/rfw/test/core_widgets_test.dart | 1 + packages/rfw/test/flutter_test_config.dart | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rfw/test/core_widgets_test.dart b/packages/rfw/test/core_widgets_test.dart index 33ae0d7021a0..de2336a3143c 100644 --- a/packages/rfw/test/core_widgets_test.dart +++ b/packages/rfw/test/core_widgets_test.dart @@ -244,6 +244,7 @@ void main() { expect(childSize.height, fractionallySizedBoxSize.height * 0.8); expect(tester.widget(find.text('test')).textScaler, const TextScaler.linear(3)); expect(tester.widget(find.byType(FractionallySizedBox)).alignment, Alignment.center); + imageCache.clear(); }); testWidgets('More core widgets', (WidgetTester tester) async { diff --git a/packages/rfw/test/flutter_test_config.dart b/packages/rfw/test/flutter_test_config.dart index b069eaff598a..9907e578b84b 100644 --- a/packages/rfw/test/flutter_test_config.dart +++ b/packages/rfw/test/flutter_test_config.dart @@ -9,7 +9,5 @@ import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Future testExecutable(FutureOr Function() testMain) async { LeakTesting.enable(); LeakTracking.warnForUnsupportedPlatforms = false; - LeakTesting.settings = LeakTesting.settings - .withCreationStackTrace(); // To debug failing test on CI. await testMain(); }