Integrate testWidgets with leak tracking. (#138057)

Contributes to: https://github.com/flutter/flutter/issues/135856

TODO:
diff --git a/packages/flutter_test/lib/src/test_compat.dart b/packages/flutter_test/lib/src/test_compat.dart
index 201f616..9e64974 100644
--- a/packages/flutter_test/lib/src/test_compat.dart
+++ b/packages/flutter_test/lib/src/test_compat.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
 import 'package:meta/meta.dart';
 import 'package:test_api/scaffolding.dart' show Timeout;
 import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports
@@ -163,6 +164,7 @@
   Map<String, dynamic>? onPlatform,
   int? retry,
 }) {
+  _configureTearDownForTestFile();
   _declarer.test(
     description.toString(),
     body,
@@ -186,6 +188,7 @@
 /// of running the group's tests.
 @isTestGroup
 void group(Object description, void Function() body, { dynamic skip, int? retry }) {
+  _configureTearDownForTestFile();
   _declarer.group(description.toString(), body, skip: skip, retry: retry);
 }
 
@@ -201,6 +204,7 @@
 /// Each callback at the top level or in a given group will be run in the order
 /// they were declared.
 void setUp(dynamic Function() body) {
+  _configureTearDownForTestFile();
   _declarer.setUp(body);
 }
 
@@ -218,6 +222,7 @@
 ///
 /// See also [addTearDown], which adds tear-downs to a running test.
 void tearDown(dynamic Function() body) {
+  _configureTearDownForTestFile();
   _declarer.tearDown(body);
 }
 
@@ -235,6 +240,7 @@
 /// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
 /// slow.
 void setUpAll(dynamic Function() body) {
+  _configureTearDownForTestFile();
   _declarer.setUpAll(body);
 }
 
@@ -250,9 +256,27 @@
 /// prefer [tearDown], and only use [tearDownAll] if the callback is
 /// prohibitively slow.
 void tearDownAll(dynamic Function() body) {
+  _configureTearDownForTestFile();
   _declarer.tearDownAll(body);
 }
 
+bool _isTearDownForTestFileConfigured = false;
+/// Configures `tearDownAll` after all user defined `tearDownAll` in the test file.
+///
+/// This function should be invoked in all functions, that may be invoked by user in the test file,
+/// to be invoked before any other `tearDownAll`.
+void _configureTearDownForTestFile() {
+  if (_isTearDownForTestFileConfigured) {
+    return;
+  }
+  _declarer.tearDownAll(_tearDownForTestFile);
+  _isTearDownForTestFileConfigured = true;
+}
+
+/// Tear down that should happen after all user defined tear down.
+Future<void> _tearDownForTestFile() async {
+  await maybeTearDownLeakTrackingForAll();
+}
 
 /// A reporter that prints each test on its own line.
 ///
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 2a8c36d..b035088 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -9,6 +9,7 @@
 import 'package:flutter/rendering.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/services.dart';
+import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
 import 'package:matcher/expect.dart' as matcher_expect;
 import 'package:meta/meta.dart';
 import 'package:test_api/scaffolding.dart' as test_package;
@@ -116,6 +117,18 @@
 /// If the [tags] are passed, they declare user-defined tags that are implemented by
 /// the `test` package.
 ///
+/// The argument [experimentalLeakTesting] is experimental and is not recommended
+/// for use outside of the Flutter framework.
+/// When [experimentalLeakTesting] is set, it is used to leak track objects created
+/// during test execution.
+/// Otherwise [LeakTesting.settings] is used.
+/// Adjust [LeakTesting.settings] in flutter_test_config.dart
+/// (see https://github.com/flutter/flutter/blob/master/packages/flutter_test/lib/flutter_test.dart)
+/// for the entire package or folder, or in the test's main for a test file
+/// (don't use [setUp] or [setUpAll]).
+/// To turn off leak tracking just for one test, set [experimentalLeakTesting] to
+/// `LeakTrackingForTests.ignore()`.
+///
 /// ## Sample code
 ///
 /// ```dart
@@ -135,6 +148,7 @@
   TestVariant<Object?> variant = const DefaultTestVariant(),
   dynamic tags,
   int? retry,
+  LeakTesting? experimentalLeakTesting,
 }) {
   assert(variant.values.isNotEmpty, 'There must be at least one value to test in the testing variant.');
   final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
@@ -165,9 +179,11 @@
             Object? memento;
             try {
               memento = await variant.setUp(value);
+              maybeSetupLeakTrackingForTest(experimentalLeakTesting, combinedDescription);
               await callback(tester);
             } finally {
               await variant.tearDown(value, memento);
+              maybeTearDownLeakTrackingForTest();
             }
             semanticsHandle?.dispose();
           },
diff --git a/packages/flutter_test/test/utils/leaking_classes.dart b/packages/flutter_test/test/utils/leaking_classes.dart
new file mode 100644
index 0000000..b49463f
--- /dev/null
+++ b/packages/flutter_test/test/utils/leaking_classes.dart
@@ -0,0 +1,54 @@
+// 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:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
+
+class LeakTrackedClass {
+  LeakTrackedClass() {
+    LeakTracking.dispatchObjectCreated(
+      library: library,
+      className: '$LeakTrackedClass',
+      object: this,
+    );
+  }
+
+  static const String library = 'package:my_package/lib/src/my_lib.dart';
+
+  void dispose() {
+    LeakTracking.dispatchObjectDisposed(object: this);
+  }
+}
+
+final List<LeakTrackedClass> _notGCedObjects = <LeakTrackedClass>[];
+
+class LeakingClass {
+  LeakingClass() {
+    _notGCedObjects.add(LeakTrackedClass()..dispose());
+  }
+}
+
+class StatelessLeakingWidget extends StatelessWidget {
+  StatelessLeakingWidget({
+    super.key,
+    this.notGCed = true,
+    this.notDisposed = true,
+  }) {
+    if (notGCed) {
+      _notGCedObjects.add(LeakTrackedClass()..dispose());
+    }
+    if (notDisposed) {
+      // ignore: unused_local_variable, it is unused intentionally, to illustrate not disposed object.
+      final LeakTrackedClass notDisposedObject = LeakTrackedClass();
+    }
+  }
+
+  final bool notGCed;
+  final bool notDisposed;
+
+  @override
+  Widget build(BuildContext context) {
+    return const Placeholder();
+  }
+}
diff --git a/packages/flutter_test/test/widget_tester_leaks_test.dart b/packages/flutter_test/test/widget_tester_leaks_test.dart
new file mode 100644
index 0000000..9c614db
--- /dev/null
+++ b/packages/flutter_test/test/widget_tester_leaks_test.dart
@@ -0,0 +1,204 @@
+// 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';
+
+import 'utils/leaking_classes.dart';
+
+late final String _test1TrackingOnNoLeaks;
+late final String _test2TrackingOffLeaks;
+late final String _test3TrackingOnLeaks;
+late final String _test4TrackingOnWithCreationStackTrace;
+late final String _test5TrackingOnWithDisposalStackTrace;
+late final String _test6TrackingOnNoLeaks;
+late final String _test7TrackingOnNoLeaks;
+late final String _test8TrackingOnNotDisposed;
+
+void main() {
+  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.
+  group('Group', () {
+    testWidgets('test', (_) async {
+      StatelessLeakingWidget();
+    });
+  });
+
+  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(),
+  (WidgetTester widgetTester) async {
+    expect(LeakTracking.isStarted, true);
+    expect(LeakTracking.phase.name, null);
+    expect(LeakTracking.phase.ignoreLeaks, true);
+    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(
+    _test5TrackingOnWithDisposalStackTrace = 'test5, tracking-on, with disposal stack trace',
+  experimentalLeakTesting: LeakTesting.settings.withDisposalStackTrace(),
+    (WidgetTester widgetTester) async {
+      expect(LeakTracking.isStarted, true);
+      expect(LeakTracking.phase.name, _test5TrackingOnWithDisposalStackTrace);
+      expect(LeakTracking.phase.ignoreLeaks, false);
+      await widgetTester.pumpWidget(StatelessLeakingWidget());
+    },
+  );
+
+  testWidgets(_test6TrackingOnNoLeaks = 'test6, tracking-on, no leaks', (_) async {
+    LeakTrackedClass().dispose();
+  });
+
+  testWidgets(_test7TrackingOnNoLeaks = 'test7, tracking-on, tear down, no leaks', (_) async {
+    final LeakTrackedClass myClass = LeakTrackedClass();
+    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);
+    LeakTrackedClass();
+  });
+}
+
+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(
+    testLeaks.notDisposed,
+    notDisposed,
+    testDescription,
+  );
+  _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(LeakTrackedClass.library));
+    expect(leak.trackedClass, contains('$LeakTrackedClass'));
+  }
+}