| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:leak_tracker/leak_tracker.dart'; |
| import 'package:leak_tracker/src/leak_tracking/_leak_checker.dart'; |
| import 'package:test/test.dart'; |
| |
| // Enum-like static classes are ok. |
| // ignore: avoid_classes_with_only_static_members |
| class _SummaryValues { |
| static final zero = LeakSummary({}); |
| |
| static final nonZero = LeakSummary({ |
| LeakType.gcedLate: 1, |
| LeakType.notDisposed: 2, |
| LeakType.notGCed: 3, |
| }); |
| |
| static final nonZeroCopy = |
| LeakSummary(<LeakType, int>{}..addAll(nonZero.totals)); |
| } |
| |
| void main() { |
| late _MockLeakProvider leakProvider; |
| |
| late _MockStdoutSink stdout; |
| late _MockDevToolsSink devtools; |
| late _ListenedSink listened; |
| |
| const period = Duration(milliseconds: 5); |
| late final doublePeriod = Duration(microseconds: period.inMicroseconds); |
| |
| LeakChecker leakChecker({ |
| required bool checkPeriodically, |
| required bool hasListener, |
| required bool hasStdout, |
| required bool hasDevtools, |
| }) => |
| LeakChecker( |
| leakProvider: leakProvider, |
| checkPeriod: checkPeriodically ? period : null, |
| onLeaks: hasListener ? (summary) => listened.store.add(summary) : null, |
| stdoutSink: hasStdout ? stdout : null, |
| devToolsSink: hasDevtools ? devtools : null, |
| ); |
| |
| LeakChecker defaultLeakChecker() => leakChecker( |
| checkPeriodically: true, |
| hasListener: false, |
| hasStdout: true, |
| hasDevtools: true, |
| ); |
| |
| setUp(() { |
| leakProvider = _MockLeakProvider(); |
| |
| stdout = _MockStdoutSink(); |
| devtools = _MockDevToolsSink(); |
| listened = _ListenedSink(); |
| }); |
| |
| test('Mocks emulate production well.', () { |
| expect( |
| _SummaryValues.zero.toMessage(), |
| isNot(_SummaryValues.nonZero.toMessage()), |
| ); |
| expect( |
| _SummaryValues.nonZero.toMessage(), |
| _SummaryValues.nonZeroCopy.toMessage(), |
| ); |
| |
| // Mock defaults match real configuration defaults. |
| const config = LeakTrackingConfiguration(); |
| final checker = defaultLeakChecker(); |
| expect(config.notifyDevTools, checker.devToolsSink != null); |
| expect(config.stdoutLeaks, checker.stdoutSink != null); |
| expect(config.onLeaks == null, checker.onLeaks == null); |
| expect(config.checkPeriod == null, checker.checkPeriod == null); |
| }); |
| |
| test('Default checker sends leaks to stdout and devtools.', () async { |
| // ignore: unused_local_variable |
| final checker = defaultLeakChecker(); |
| |
| // Make sure there is no leaks. |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| |
| // Report leaks and make sure they signaled one time. |
| leakProvider.value = _SummaryValues.nonZero; |
| await Future.delayed(doublePeriod); |
| stdout.checkStoreAndClear([_SummaryValues.nonZero]); |
| devtools.checkStoreAndClear([_SummaryValues.nonZero]); |
| |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| |
| // Report the same leak totals and make sure there is no signals. |
| leakProvider.value = _SummaryValues.nonZeroCopy; |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| |
| // Drop totals and check signal. |
| leakProvider.value = _SummaryValues.zero; |
| await Future.delayed(doublePeriod); |
| stdout.checkStoreAndClear([_SummaryValues.zero]); |
| devtools.checkStoreAndClear([_SummaryValues.zero]); |
| |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| }); |
| |
| test('Listener-only checker sends leaks to just listener.', () async { |
| // ignore: unused_local_variable |
| final checker = leakChecker( |
| hasDevtools: false, |
| hasStdout: false, |
| hasListener: true, |
| checkPeriodically: true, |
| ); |
| |
| // Make sure there is no leaks. |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| |
| // Report leaks and make sure they signaled one time. |
| leakProvider.value = _SummaryValues.nonZero; |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| listened.checkStoreAndClear([_SummaryValues.nonZero]); |
| |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| |
| // Report the same leak totals and make sure there is no signals. |
| leakProvider.value = _SummaryValues.nonZeroCopy; |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| |
| // Drop totals and check signal. |
| leakProvider.value = _SummaryValues.zero; |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| listened.checkStoreAndClear([_SummaryValues.zero]); |
| |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| }); |
| |
| test('No-timer checker sends leaks when checked.', () async { |
| // ignore: unused_local_variable |
| final checker = leakChecker( |
| checkPeriodically: false, |
| hasDevtools: true, |
| hasStdout: true, |
| hasListener: true, |
| ); |
| |
| // Make sure there is no leaks. |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| |
| // Report leaks and make sure did not signal. |
| leakProvider.value = _SummaryValues.nonZero; |
| await Future.delayed(doublePeriod); |
| expect(stdout.store, isEmpty); |
| expect(devtools.store, isEmpty); |
| expect(listened.store, isEmpty); |
| |
| // Check leaks and make sure they signaled one time. |
| checker.checkLeaks(); |
| stdout.checkStoreAndClear([_SummaryValues.nonZero]); |
| devtools.checkStoreAndClear([_SummaryValues.nonZero]); |
| listened.checkStoreAndClear([_SummaryValues.nonZero]); |
| }); |
| } |
| |
| class _ListenedSink { |
| final store = <LeakSummary>[]; |
| |
| void checkStoreAndClear(List<LeakSummary> items) { |
| expect(store, hasLength(items.length)); |
| for (final i in Iterable.generate(store.length)) { |
| expect(store[i].toMessage(), contains(items[i].toMessage())); |
| } |
| store.clear(); |
| } |
| } |
| |
| class _MockStdoutSink implements StdoutSummarySink { |
| final store = <LeakSummary>[]; |
| |
| @override |
| void send(LeakSummary summary) => store.add(summary); |
| |
| void checkStoreAndClear(List<LeakSummary> items) { |
| expect(store, hasLength(items.length)); |
| for (final i in Iterable.generate(store.length)) { |
| expect(store[i].toMessage(), contains(items[i].toMessage())); |
| } |
| store.clear(); |
| } |
| } |
| |
| class _MockDevToolsSink implements DevToolsSummarySink { |
| final store = <LeakSummary>[]; |
| |
| @override |
| void send(LeakSummary summary) => store.add(summary); |
| |
| void checkStoreAndClear(List<LeakSummary> items) { |
| expect(store, hasLength(items.length)); |
| for (final i in Iterable.generate(store.length)) { |
| expect(store[i].toMessage(), contains(items[i].toMessage())); |
| } |
| store.clear(); |
| } |
| } |
| |
| class _MockLeakProvider implements LeakProvider { |
| LeakSummary value = LeakSummary({}); |
| |
| @override |
| LeakSummary leaksSummary() => value; |
| |
| @override |
| Leaks collectLeaks() => throw UnimplementedError(); |
| } |