blob: bd02bd2c781ddc2472d5c824c2487429a22ccf55 [file] [log] [blame] [edit]
// 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);
}
}