leak_tracking_test.dart 5.03 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
// 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 '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 only non-disposed 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, 1);
  });

  group('Leak tracking works for non-web', () {
    testWidgetsWithLeakTracking(
      'Leak tracker respects all allow lists',
      (WidgetTester tester) async {
        await tester.pumpWidget(_StatelessLeakingWidget());
      },
      leakTrackingConfig: LeakTrackingTestConfig(
38 39
        notDisposedAllowList: <String, int?>{_leakTrackedClassName: null},
        notGCedAllowList: <String, int?>{_leakTrackedClassName: null},
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
      ),
    );

    group('Leak tracker 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());
        },
        leakTrackingConfig: LeakTrackingTestConfig(
          onLeaks: (Leaks theLeaks) {
            leaks = theLeaks;
          },
          failTestOnLeaks: false,
58
          notGCedAllowList: <String, int?>{_leakTrackedClassName: null},
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
        ),
      );

      tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 1));
    });

    group('Leak tracker catches that', () {
      // These tests 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());
        },
        leakTrackingConfig: LeakTrackingTestConfig(
          onLeaks: (Leaks theLeaks) {
            leaks = theLeaks;
          },
          failTestOnLeaks: false,
        ),
      );

      tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 1));
    });
  },
  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 }) {
  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);
  _verifyLeakList(leaks.notGCed, expectedNotGCed);
}

void _verifyLeakList(List<LeakReport> list, int expectedCount){
  expect(list.length, expectedCount);

  for (final LeakReport leak in list) {
    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() {
    // ignore: unused_local_variable, the variable is used to create non disposed leak
    final _LeakTrackedClass notDisposed = _LeakTrackedClass();
    _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);
  }
}