Instrument pending timers in tests (#37646)
Flutter widget tests assert if a test completes with timers still
pending. However, it can be hard to diagnose where a pending timer
came from. For example, a widget might consume a third-party library
that internally uses a timer.
I added a FakeAsync.pendingTimersDebugInfo getter to quiver
(https://github.com/google/quiver-dart/pull/500). Make flutter_test
use it.
Additionally modify Flutter's debugPrintStack to take an optional
StackTrace argument instead of always printing StackTrace.current.
Fixes #4237.
diff --git a/dev/automated_tests/test_smoke_test/pending_timer_fail_test.dart b/dev/automated_tests/test_smoke_test/pending_timer_fail_test.dart
new file mode 100644
index 0000000..ce223e6
--- /dev/null
+++ b/dev/automated_tests/test_smoke_test/pending_timer_fail_test.dart
@@ -0,0 +1,17 @@
+// Copyright 2018 The Chromium 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 'dart:async';
+
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ failingPendingTimerTest();
+}
+
+void failingPendingTimerTest() {
+ testWidgets('flutter_test pending timer - negative', (WidgetTester tester) async {
+ Timer(const Duration(minutes: 10), () {});
+ });
+}
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index c546075..48a729f 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -111,6 +111,16 @@
printOutput: false,
timeout: _kShortTimeout,
);
+ await _runFlutterTest(automatedTests,
+ script: path.join('test_smoke_test', 'pending_timer_fail_test.dart'),
+ expectFailure: true,
+ printOutput: false,
+ outputChecker: (CapturedOutput output) =>
+ output.stdout.contains('failingPendingTimerTest')
+ ? null
+ : 'Failed to find the stack trace for the pending Timer.',
+ timeout: _kShortTimeout,
+ );
// We run the remaining smoketests in parallel, because they each take some
// time to run (e.g. compiling), so we don't want to run them in series,
// especially on 20-core machines...
diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart
index adcb3a1..6697b15 100644
--- a/packages/flutter/lib/src/foundation/assertions.dart
+++ b/packages/flutter/lib/src/foundation/assertions.dart
@@ -737,20 +737,25 @@
}
}
-/// Dump the current stack to the console using [debugPrint] and
+/// Dump the stack to the console using [debugPrint] and
/// [FlutterError.defaultStackFilter].
///
-/// The current stack is obtained using [StackTrace.current].
+/// If the `stackTrace` parameter is null, the [StackTrace.current] is used to
+/// obtain the stack.
///
/// The `maxFrames` argument can be given to limit the stack to the given number
-/// of lines. By default, all non-filtered stack lines are shown.
+/// of lines before filtering is applied. By default, all stack lines are
+/// included.
///
/// The `label` argument, if present, will be printed before the stack.
-void debugPrintStack({ String label, int maxFrames }) {
+void debugPrintStack({StackTrace stackTrace, String label, int maxFrames}) {
if (label != null)
debugPrint(label);
- Iterable<String> lines = StackTrace.current.toString().trimRight().split('\n');
- if (kIsWeb) {
+ stackTrace ??= StackTrace.current;
+ Iterable<String> lines = stackTrace.toString().trimRight().split('\n');
+ if ( kIsWeb
+ && lines.isNotEmpty
+ && lines.first.contains('StackTrace.current')) {
// Remove extra call to StackTrace.current for web platform.
// TODO(ferhat): remove when https://github.com/flutter/flutter/issues/37635
// is addressed.
diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart
index 88e1ccd..0389e50 100644
--- a/packages/flutter_test/lib/src/binding.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -1048,14 +1048,29 @@
@override
void _verifyInvariants() {
super._verifyInvariants();
- assert(
- _currentFakeAsync.periodicTimerCount == 0,
- 'A periodic Timer is still running even after the widget tree was disposed.'
- );
- assert(
- _currentFakeAsync.nonPeriodicTimerCount == 0,
- 'A Timer is still pending even after the widget tree was disposed.'
- );
+
+ assert(() {
+ if ( _currentFakeAsync.periodicTimerCount == 0
+ && _currentFakeAsync.nonPeriodicTimerCount == 0) {
+ return true;
+ }
+
+ debugPrint('Pending timers:');
+ for (String timerInfo in _currentFakeAsync.pendingTimersDebugInfo) {
+ final int firstLineEnd = timerInfo.indexOf('\n');
+ assert(firstLineEnd != -1);
+
+ // No need to include the newline.
+ final String firstLine = timerInfo.substring(0, firstLineEnd);
+ final String stackTrace = timerInfo.substring(firstLineEnd + 1);
+
+ debugPrint(firstLine);
+ debugPrintStack(stackTrace: StackTrace.fromString(stackTrace));
+ debugPrint('');
+ }
+ return false;
+ }(), 'A Timer is still pending even after the widget tree was disposed.');
+
assert(_currentFakeAsync.microtaskCount == 0); // Shouldn't be possible.
}