| // 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/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:leak_tracker/leak_tracker.dart'; |
| import 'package:leak_tracker_testing/leak_tracker_testing.dart'; |
| |
| import 'leak_tracking.dart'; |
| |
| final String _leakTrackedClassName = '$_LeakTrackedClass'; |
| |
| Leaks _leaksOfAllTypes() => Leaks(<LeakType, List<LeakReport>> { |
| LeakType.notDisposed: <LeakReport>[LeakReport(code: 1, context: <String, dynamic>{}, type:'myNotDisposedClass', trackedClass: 'myTrackedClass')], |
| LeakType.notGCed: <LeakReport>[LeakReport(code: 2, context: <String, dynamic>{}, type:'myNotGCedClass', trackedClass: 'myTrackedClass')], |
| LeakType.gcedLate: <LeakReport>[LeakReport(code: 3, context: <String, dynamic>{}, type:'myGCedLateClass', trackedClass: 'myTrackedClass')], |
| }); |
| |
| Future<void> main() async { |
| test('Trivial $LeakCleaner returns all leaks.', () { |
| final LeakCleaner leakCleaner = LeakCleaner(const LeakTrackingTestConfig()); |
| final Leaks leaks = _leaksOfAllTypes(); |
| final int leakTotal = leaks.total; |
| |
| final Leaks cleanedLeaks = leakCleaner.clean(leaks); |
| |
| expect(leaks.total, leakTotal); |
| expect(cleanedLeaks.total, 3); |
| }); |
| |
| test('$LeakCleaner catches extra leaks', () { |
| Leaks leaks = _leaksOfAllTypes(); |
| final LeakReport leak = leaks.notDisposed.first; |
| leaks.notDisposed.add(leak); |
| |
| final LeakTrackingTestConfig config = LeakTrackingTestConfig( |
| notDisposedAllowList: <String, int?>{leak.type: 1}, |
| ); |
| leaks = LeakCleaner(config).clean(leaks); |
| |
| expect(leaks.notDisposed, hasLength(2)); |
| }); |
| |
| group('Leak tracking works for non-web, and', () { |
| testWidgetsWithLeakTracking( |
| 'respects all allow lists', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| }, |
| leakTrackingTestConfig: LeakTrackingTestConfig( |
| notDisposedAllowList: <String, int?>{_leakTrackedClassName: null}, |
| notGCedAllowList: <String, int?>{_leakTrackedClassName: null}, |
| ), |
| ); |
| |
| testWidgetsWithLeakTracking( |
| 'respects allowAllNotDisposed', |
| (WidgetTester tester) async { |
| // ignore: avoid_redundant_argument_values, for readability. |
| await tester.pumpWidget(_StatelessLeakingWidget(notDisposed: true, notGCed: false)); |
| }, |
| leakTrackingTestConfig: const LeakTrackingTestConfig( |
| allowAllNotDisposed: true, |
| ), |
| ); |
| |
| testWidgetsWithLeakTracking( |
| 'respects allowAllNotGCed', |
| (WidgetTester tester) async { |
| // ignore: avoid_redundant_argument_values, for readability. |
| await tester.pumpWidget(_StatelessLeakingWidget(notDisposed: false, notGCed: true)); |
| }, |
| leakTrackingTestConfig: const LeakTrackingTestConfig( |
| allowAllNotGCed: true, |
| ), |
| ); |
| |
| testWidgetsWithLeakTracking( |
| 'respects count in allow lists', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| }, |
| leakTrackingTestConfig: LeakTrackingTestConfig( |
| notDisposedAllowList: <String, int?>{_leakTrackedClassName: 1}, |
| notGCedAllowList: <String, int?>{_leakTrackedClassName: 1}, |
| ), |
| ); |
| |
| group('fails if number or leaks is more than allowed', () { |
| // This test cannot run inside other tests because test nesting is forbidden. |
| // So, `expect` happens outside the tests, in `tearDown`. |
| late Leaks leaks; |
| |
| testWidgetsWithLeakTracking( |
| 'for $_StatelessLeakingWidget', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| }, |
| leakTrackingTestConfig: LeakTrackingTestConfig( |
| onLeaks: (Leaks theLeaks) { |
| leaks = theLeaks; |
| }, |
| failTestOnLeaks: false, |
| notDisposedAllowList: <String, int?>{_leakTrackedClassName: 1}, |
| ), |
| ); |
| |
| tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 2, expectedNotGCed: 2, shouldContainDebugInfo: false)); |
| }); |
| |
| group('respects notGCed allow lists', () { |
| // These tests cannot run inside other tests because test nesting is forbidden. |
| // So, `expect` happens outside the tests, in `tearDown`. |
| late Leaks leaks; |
| |
| testWidgetsWithLeakTracking( |
| 'when $_StatelessLeakingWidget leaks', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| }, |
| leakTrackingTestConfig: LeakTrackingTestConfig( |
| onLeaks: (Leaks theLeaks) { |
| leaks = theLeaks; |
| }, |
| failTestOnLeaks: false, |
| notGCedAllowList: <String, int?>{_leakTrackedClassName: null}, |
| ), |
| ); |
| |
| tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 1, shouldContainDebugInfo: false)); |
| }); |
| |
| group('catches that', () { |
| // These test cannot run inside other tests because test nesting is forbidden. |
| // So, `expect` happens outside the tests, in `tearDown`. |
| late Leaks leaks; |
| |
| testWidgetsWithLeakTracking( |
| '$_StatelessLeakingWidget leaks', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| }, |
| leakTrackingTestConfig: LeakTrackingTestConfig( |
| onLeaks: (Leaks theLeaks) { |
| leaks = theLeaks; |
| }, |
| failTestOnLeaks: false, |
| ), |
| ); |
| |
| tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 1, expectedNotGCed: 1, shouldContainDebugInfo: false)); |
| }); |
| }, |
| skip: isBrowser); // [intended] Leak detection is off for web. |
| |
| testWidgetsWithLeakTracking('Leak tracking is no-op for web', (WidgetTester tester) async { |
| await tester.pumpWidget(_StatelessLeakingWidget()); |
| }, |
| skip: !isBrowser); // [intended] Leaks detection is off for web. |
| } |
| |
| /// Verifies [leaks] contains expected number of leaks for [_LeakTrackedClass]. |
| void _verifyLeaks( |
| Leaks leaks, { |
| int expectedNotDisposed = 0, |
| int expectedNotGCed = 0, |
| required bool shouldContainDebugInfo, |
| }) { |
| const String linkToLeakTracker = 'https://github.com/dart-lang/leak_tracker'; |
| |
| expect( |
| () => expect(leaks, isLeakFree), |
| throwsA( |
| predicate((Object? e) { |
| return e is TestFailure && e.toString().contains(linkToLeakTracker); |
| }), |
| ), |
| ); |
| |
| _verifyLeakList(leaks.notDisposed, expectedNotDisposed, shouldContainDebugInfo); |
| _verifyLeakList(leaks.notGCed, expectedNotGCed, shouldContainDebugInfo); |
| } |
| |
| void _verifyLeakList(List<LeakReport> list, int expectedCount, bool shouldContainDebugInfo){ |
| expect(list.length, expectedCount); |
| |
| for (final LeakReport leak in list) { |
| if (shouldContainDebugInfo) { |
| expect(leak.context, isNotEmpty); |
| } else { |
| expect(leak.context ?? <String, dynamic>{}, isEmpty); |
| } |
| |
| expect(leak.trackedClass, contains(_LeakTrackedClass.library)); |
| expect(leak.trackedClass, contains(_leakTrackedClassName)); |
| } |
| } |
| |
| /// Storage to keep disposed objects, to generate not-gced leaks. |
| final List<_LeakTrackedClass> _notGcedStorage = <_LeakTrackedClass>[]; |
| |
| class _StatelessLeakingWidget extends StatelessWidget { |
| _StatelessLeakingWidget({bool notDisposed = true, bool notGCed = true}) { |
| if (notDisposed) { |
| // ignore: unused_local_variable, the variable is used to create non disposed leak |
| final _LeakTrackedClass notDisposed = _LeakTrackedClass(); |
| } |
| if (notGCed) { |
| _notGcedStorage.add(_LeakTrackedClass()..dispose()); |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return const Placeholder(); |
| } |
| } |
| |
| class _LeakTrackedClass { |
| _LeakTrackedClass() { |
| dispatchObjectCreated( |
| library: library, |
| className: '$_LeakTrackedClass', |
| object: this, |
| ); |
| } |
| |
| static const String library = 'package:my_package/lib/src/my_lib.dart'; |
| |
| void dispose() { |
| dispatchObjectDisposed(object: this); |
| } |
| } |