mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Improve testing for leak tracking. (#140553)
This commit is contained in:
parent
0409a55076
commit
34f1f5f19e
@ -44,6 +44,7 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) {
|
|||||||
LeakTesting.settings = LeakTesting
|
LeakTesting.settings = LeakTesting
|
||||||
.settings
|
.settings
|
||||||
.withIgnored(
|
.withIgnored(
|
||||||
|
createdByTestHelpers: true,
|
||||||
allNotGCed: true,
|
allNotGCed: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
95
packages/flutter_test/test/utils/memory_leak_tests.dart
Normal file
95
packages/flutter_test/test/utils/memory_leak_tests.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright 2014 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 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||||
|
|
||||||
|
/// Objects that should not be GCed during test run.
|
||||||
|
final List<InstrumentedDisposable> _retainer = <InstrumentedDisposable>[];
|
||||||
|
|
||||||
|
/// Test cases for memory leaks.
|
||||||
|
///
|
||||||
|
/// They are separate from test execution to allow
|
||||||
|
/// excluding them from test helpers.
|
||||||
|
final List<LeakTestCase> memoryLeakTests = <LeakTestCase>[
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'no leaks',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
await pumpWidgets!(Container());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'not disposed disposable',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
InstrumentedDisposable();
|
||||||
|
},
|
||||||
|
notDisposedTotal: 1,
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'not GCed disposable',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
_retainer.add(InstrumentedDisposable()..dispose());
|
||||||
|
},
|
||||||
|
notGCedTotal: 1,
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'leaking widget',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
StatelessLeakingWidget();
|
||||||
|
},
|
||||||
|
notDisposedTotal: 1,
|
||||||
|
notGCedTotal: 1,
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'dispose in tear down',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
final InstrumentedDisposable myClass = InstrumentedDisposable();
|
||||||
|
addTearDown(myClass.dispose);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'pumped leaking widget',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
await pumpWidgets!(StatelessLeakingWidget());
|
||||||
|
},
|
||||||
|
notDisposedTotal: 1,
|
||||||
|
notGCedTotal: 1,
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'leaking widget in runAsync',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
await runAsync!(() async {
|
||||||
|
StatelessLeakingWidget();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
notDisposedTotal: 1,
|
||||||
|
notGCedTotal: 1,
|
||||||
|
),
|
||||||
|
LeakTestCase(
|
||||||
|
name: 'pumped in runAsync',
|
||||||
|
body: (PumpWidgetsCallback? pumpWidgets,
|
||||||
|
RunAsyncCallback<dynamic>? runAsync) async {
|
||||||
|
await runAsync!(() async {
|
||||||
|
await pumpWidgets!(StatelessLeakingWidget());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
notDisposedTotal: 1,
|
||||||
|
notGCedTotal: 1,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
String memoryLeakTestsFilePath() {
|
||||||
|
return RegExp(r'(\/[^\/]*.dart):')
|
||||||
|
.firstMatch(StackTrace.current.toString())!
|
||||||
|
.group(1).toString();
|
||||||
|
}
|
@ -2,199 +2,62 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.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';
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||||
|
|
||||||
late final String _test1TrackingOnNoLeaks;
|
import 'utils/memory_leak_tests.dart';
|
||||||
late final String _test2TrackingOffLeaks;
|
|
||||||
late final String _test3TrackingOnLeaks;
|
class _TestExecution {
|
||||||
late final String _test4TrackingOnWithCreationStackTrace;
|
_TestExecution(
|
||||||
late final String _test5TrackingOnWithDisposalStackTrace;
|
{required this.settings, required this.settingName, required this.test});
|
||||||
late final String _test6TrackingOnNoLeaks;
|
|
||||||
late final String _test7TrackingOnNoLeaks;
|
final String settingName;
|
||||||
late final String _test8TrackingOnNotDisposed;
|
final LeakTesting settings;
|
||||||
|
final LeakTestCase test;
|
||||||
|
|
||||||
|
String get name => '${test.name}, $settingName';
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<_TestExecution> _testExecutions = <_TestExecution>[];
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
LeakTesting.collectedLeaksReporter = _verifyLeaks;
|
||||||
LeakTesting.enable();
|
LeakTesting.enable();
|
||||||
LeakTesting.collectedLeaksReporter = (Leaks leaks) => verifyLeaks(leaks);
|
|
||||||
LeakTesting.settings = LeakTesting.settings.copyWith(ignore: false);
|
|
||||||
|
|
||||||
// It is important that the test file starts with group, to test that leaks are collected for all tests after group too.
|
LeakTesting.settings = LeakTesting.settings
|
||||||
group('Group', () {
|
.withTrackedAll()
|
||||||
testWidgets('test', (_) async {
|
.withTracked(allNotDisposed: true, allNotGCed: true)
|
||||||
StatelessLeakingWidget();
|
.withIgnored(
|
||||||
});
|
createdByTestHelpers: true,
|
||||||
});
|
testHelperExceptions: <RegExp>[
|
||||||
|
RegExp(RegExp.escape(memoryLeakTestsFilePath()))
|
||||||
testWidgets(_test1TrackingOnNoLeaks = 'test1, tracking-on, no leaks', (WidgetTester widgetTester) async {
|
],
|
||||||
expect(LeakTracking.isStarted, true);
|
|
||||||
expect(LeakTracking.phase.name, _test1TrackingOnNoLeaks);
|
|
||||||
expect(LeakTracking.phase.ignoreLeaks, false);
|
|
||||||
await widgetTester.pumpWidget(Container());
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
_test2TrackingOffLeaks = 'test2, tracking-off, leaks',
|
|
||||||
experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), // this test is not tracked by design
|
|
||||||
(WidgetTester widgetTester) async {
|
|
||||||
await widgetTester.pumpWidget(StatelessLeakingWidget());
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(_test3TrackingOnLeaks = 'test3, tracking-on, leaks', (WidgetTester widgetTester) async {
|
|
||||||
expect(LeakTracking.isStarted, true);
|
|
||||||
expect(LeakTracking.phase.name, _test3TrackingOnLeaks);
|
|
||||||
expect(LeakTracking.phase.ignoreLeaks, false);
|
|
||||||
await widgetTester.pumpWidget(StatelessLeakingWidget());
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
_test4TrackingOnWithCreationStackTrace = 'test4, tracking-on, with creation stack trace',
|
|
||||||
experimentalLeakTesting: LeakTesting.settings.withCreationStackTrace(),
|
|
||||||
(WidgetTester widgetTester) async {
|
|
||||||
expect(LeakTracking.isStarted, true);
|
|
||||||
expect(LeakTracking.phase.name, _test4TrackingOnWithCreationStackTrace);
|
|
||||||
expect(LeakTracking.phase.ignoreLeaks, false);
|
|
||||||
await widgetTester.pumpWidget(StatelessLeakingWidget());
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
for (final LeakTestCase test in memoryLeakTests) {
|
||||||
_test5TrackingOnWithDisposalStackTrace = 'test5, tracking-on, with disposal stack trace',
|
for (final MapEntry<String,
|
||||||
experimentalLeakTesting: LeakTesting.settings.withDisposalStackTrace(),
|
LeakTesting Function(LeakTesting settings)> settingsCase
|
||||||
(WidgetTester widgetTester) async {
|
in leakTestingSettingsCases.entries) {
|
||||||
expect(LeakTracking.isStarted, true);
|
final LeakTesting settings = settingsCase.value(LeakTesting.settings);
|
||||||
expect(LeakTracking.phase.name, _test5TrackingOnWithDisposalStackTrace);
|
if (settings.leakDiagnosticConfig.collectRetainingPathForNotGCed) {
|
||||||
expect(LeakTracking.phase.ignoreLeaks, false);
|
// Retaining path requires vm to be started, so skipping.
|
||||||
await widgetTester.pumpWidget(StatelessLeakingWidget());
|
continue;
|
||||||
},
|
}
|
||||||
);
|
final _TestExecution execution = _TestExecution(
|
||||||
|
settingName: settingsCase.key, test: test, settings: settings);
|
||||||
testWidgets(_test6TrackingOnNoLeaks = 'test6, tracking-on, no leaks', (_) async {
|
_testExecutions.add(execution);
|
||||||
InstrumentedDisposable().dispose();
|
testWidgets(execution.name, experimentalLeakTesting: settings,
|
||||||
});
|
(WidgetTester tester) async {
|
||||||
|
await test.body(tester.pumpWidget, tester.runAsync);
|
||||||
testWidgets(_test7TrackingOnNoLeaks = 'test7, tracking-on, tear down, no leaks', (_) async {
|
|
||||||
final InstrumentedDisposable myClass = InstrumentedDisposable();
|
|
||||||
addTearDown(myClass.dispose);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(_test8TrackingOnNotDisposed = 'test8, tracking-on, not disposed leak', (_) async {
|
|
||||||
expect(LeakTracking.isStarted, true);
|
|
||||||
expect(LeakTracking.phase.name, _test8TrackingOnNotDisposed);
|
|
||||||
expect(LeakTracking.phase.ignoreLeaks, false);
|
|
||||||
InstrumentedDisposable();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
int _leakReporterInvocationCount = 0;
|
|
||||||
|
|
||||||
void verifyLeaks(Leaks leaks) {
|
|
||||||
_leakReporterInvocationCount += 1;
|
|
||||||
expect(_leakReporterInvocationCount, 1);
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(leaks, isLeakFree);
|
|
||||||
} on TestFailure catch (e) {
|
|
||||||
expect(e.message, contains('https://github.com/dart-lang/leak_tracker'));
|
|
||||||
|
|
||||||
expect(e.message, isNot(contains(_test1TrackingOnNoLeaks)));
|
|
||||||
expect(e.message, isNot(contains(_test2TrackingOffLeaks)));
|
|
||||||
expect(e.message, contains('test: $_test3TrackingOnLeaks'));
|
|
||||||
expect(e.message, contains('test: $_test4TrackingOnWithCreationStackTrace'));
|
|
||||||
expect(e.message, contains('test: $_test5TrackingOnWithDisposalStackTrace'));
|
|
||||||
expect(e.message, isNot(contains(_test6TrackingOnNoLeaks)));
|
|
||||||
expect(e.message, isNot(contains(_test7TrackingOnNoLeaks)));
|
|
||||||
expect(e.message, contains('test: $_test8TrackingOnNotDisposed'));
|
|
||||||
}
|
|
||||||
|
|
||||||
_verifyLeaks(
|
|
||||||
leaks,
|
|
||||||
_test3TrackingOnLeaks,
|
|
||||||
notDisposed: 1,
|
|
||||||
notGCed: 1,
|
|
||||||
expectedContextKeys: <LeakType, List<String>>{
|
|
||||||
LeakType.notGCed: <String>[],
|
|
||||||
LeakType.notDisposed: <String>[],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_verifyLeaks(
|
|
||||||
leaks,
|
|
||||||
_test4TrackingOnWithCreationStackTrace,
|
|
||||||
notDisposed: 1,
|
|
||||||
notGCed: 1,
|
|
||||||
expectedContextKeys: <LeakType, List<String>>{
|
|
||||||
LeakType.notGCed: <String>['start'],
|
|
||||||
LeakType.notDisposed: <String>['start'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_verifyLeaks(
|
|
||||||
leaks,
|
|
||||||
_test5TrackingOnWithDisposalStackTrace,
|
|
||||||
notDisposed: 1,
|
|
||||||
notGCed: 1,
|
|
||||||
expectedContextKeys: <LeakType, List<String>>{
|
|
||||||
LeakType.notGCed: <String>['disposal'],
|
|
||||||
LeakType.notDisposed: <String>[],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_verifyLeaks(
|
|
||||||
leaks,
|
|
||||||
_test8TrackingOnNotDisposed,
|
|
||||||
notDisposed: 1,
|
|
||||||
expectedContextKeys: <LeakType, List<String>>{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies [allLeaks] contain expected number of leaks for the test [testDescription].
|
|
||||||
///
|
|
||||||
/// [notDisposed] and [notGCed] set number for expected leaks by leak type.
|
|
||||||
/// The method will fail if the leaks context does not contain [expectedContextKeys].
|
|
||||||
void _verifyLeaks(
|
|
||||||
Leaks allLeaks,
|
|
||||||
String testDescription, {
|
|
||||||
int notDisposed = 0,
|
|
||||||
int notGCed = 0,
|
|
||||||
Map<LeakType, List<String>> expectedContextKeys = const <LeakType, List<String>>{},
|
|
||||||
}) {
|
|
||||||
final Leaks testLeaks = Leaks(
|
|
||||||
allLeaks.byType.map(
|
|
||||||
(LeakType key, List<LeakReport> value) =>
|
|
||||||
MapEntry<LeakType, List<LeakReport>>(key, value.where((LeakReport leak) => leak.phase == testDescription).toList()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final LeakType type in expectedContextKeys.keys) {
|
|
||||||
final List<LeakReport> leaks = testLeaks.byType[type]!;
|
|
||||||
final List<String> expectedKeys = expectedContextKeys[type]!..sort();
|
|
||||||
for (final LeakReport leak in leaks) {
|
|
||||||
final List<String> actualKeys = leak.context?.keys.toList() ?? <String>[];
|
|
||||||
expect(actualKeys..sort(), equals(expectedKeys), reason: '$testDescription, $type');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_verifyLeakList(
|
void _verifyLeaks(Leaks leaks) {
|
||||||
testLeaks.notDisposed,
|
for (final _TestExecution execution in _testExecutions) {
|
||||||
notDisposed,
|
final Leaks testLeaks = leaks.byPhase[execution.name] ?? Leaks.empty();
|
||||||
testDescription,
|
execution.test.verifyLeaks(testLeaks, execution.settings,
|
||||||
);
|
testDescription: execution.name);
|
||||||
_verifyLeakList(
|
|
||||||
testLeaks.notGCed,
|
|
||||||
notGCed,
|
|
||||||
testDescription,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _verifyLeakList(
|
|
||||||
List<LeakReport> list,
|
|
||||||
int expectedCount,
|
|
||||||
String testDescription,
|
|
||||||
) {
|
|
||||||
expect(list.length, expectedCount, reason: testDescription);
|
|
||||||
|
|
||||||
for (final LeakReport leak in list) {
|
|
||||||
expect(leak.trackedClass, contains(InstrumentedDisposable.library));
|
|
||||||
expect(leak.trackedClass, contains('$InstrumentedDisposable'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user