Skip to content

Commit 133711b

Browse files
authored
Analyze against using Stopwatches in the framework (#138507)
1 parent 4d8ae57 commit 133711b

File tree

10 files changed

+124
-6
lines changed

10 files changed

+124
-6
lines changed

dev/bots/analyze.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ Future<void> run(List<String> arguments) async {
124124
printProgress('Goldens...');
125125
await verifyGoldenTags(flutterPackages);
126126

127+
printProgress('Prevent flakes from Stopwatches...');
128+
await verifyNoStopwatches(flutterPackages);
129+
127130
printProgress('Skip test comments...');
128131
await verifySkipTestComments(flutterRoot);
129132

@@ -584,6 +587,48 @@ Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 20
584587
}
585588
}
586589

590+
/// Use of Stopwatches can introduce test flakes as the logical time of a
591+
/// stopwatch can fall out of sync with the mocked time of FakeAsync in testing.
592+
/// The Clock object provides a safe stopwatch instead, which is paired with
593+
/// FakeAsync as part of the test binding.
594+
final RegExp _findStopwatchPattern = RegExp(r'Stopwatch\(\)');
595+
const String _ignoreStopwatch = '// flutter_ignore: stopwatch (see analyze.dart)';
596+
const String _ignoreStopwatchForFile = '// flutter_ignore_for_file: stopwatch (see analyze.dart)';
597+
598+
Future<void> verifyNoStopwatches(String workingDirectory, { int minimumMatches = 2000 }) async {
599+
final List<String> errors = <String>[];
600+
await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
601+
if (file.path.contains('flutter_tool')) {
602+
// Skip flutter_tool package.
603+
continue;
604+
}
605+
int lineNumber = 1;
606+
final List<String> lines = file.readAsLinesSync();
607+
for (final String line in lines) {
608+
// If the file is being ignored, skip parsing the rest of the lines.
609+
if (line.contains(_ignoreStopwatchForFile)) {
610+
break;
611+
}
612+
613+
if (line.contains(_findStopwatchPattern)
614+
&& !line.contains(_leadingComment)
615+
&& !line.contains(_ignoreStopwatch)) {
616+
// Stopwatch found
617+
errors.add('\t${file.path}:$lineNumber');
618+
}
619+
lineNumber++;
620+
}
621+
}
622+
if (errors.isNotEmpty) {
623+
foundError(<String>[
624+
'Stopwatch use was found in the following files:',
625+
...errors,
626+
'${bold}Stopwatches introduce flakes by falling out of sync with the FakeAsync used in testing.$reset',
627+
'A Stopwatch that stays in sync with FakeAsync is available through the Gesture or Test bindings, through samplingClock.'
628+
]);
629+
}
630+
}
631+
587632
final RegExp _findDeprecationPattern = RegExp(r'@[Dd]eprecated');
588633
final RegExp _deprecationStartPattern = RegExp(r'^(?<indent> *)@Deprecated\($'); // flutter_ignore: deprecation_syntax (see analyze.dart)
589634
final RegExp _deprecationMessagePattern = RegExp(r"^ *'(?<message>.+) '$");
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// Sample Code
6+
///
7+
/// No analysis failures should be found.
8+
///
9+
/// {@tool snippet}
10+
/// Sample invocations of [Stopwatch].
11+
///
12+
/// ```dart
13+
/// Stopwatch();
14+
/// ```
15+
/// {@end-tool}
16+
String? foo;
17+
// Other comments
18+
// Stopwatch();
19+
20+
String literal = 'Stopwatch()'; // flutter_ignore: stopwatch (see analyze.dart)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// This should fail analysis.
6+
7+
void main() {
8+
Stopwatch();
9+
10+
// Identify more than one in a file.
11+
Stopwatch myStopwatch;
12+
myStopwatch = Stopwatch();
13+
myStopwatch.reset();
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// This would fail analysis, but it is ignored
6+
// flutter_ignore_for_file: stopwatch (see analyze.dart)
7+
8+
void main() {
9+
Stopwatch();
10+
}

dev/bots/test/analyze_test.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ void main() {
9292
expect(result[result.length - 1], ''); // trailing newline
9393
});
9494

95+
test('analyze.dart - verifyNoStopwatches', () async {
96+
final List<String> result = (await capture(() => verifyNoStopwatches(testRootPath, minimumMatches: 6), shouldHaveErrors: true)).split('\n');
97+
final List<String> lines = <String>[
98+
'║ \ttest/analyze-test-input/root/packages/foo/stopwatch_fail.dart:8',
99+
'║ \ttest/analyze-test-input/root/packages/foo/stopwatch_fail.dart:12',
100+
]
101+
.map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/'))
102+
.toList();
103+
expect(result.length, 6 + lines.length, reason: 'output had unexpected number of lines:\n${result.join('\n')}');
104+
expect(result[0], '╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════');
105+
expect(result[1], '║ Stopwatch use was found in the following files:');
106+
expect(result.getRange(2, result.length - 4).toSet(), lines.toSet());
107+
expect(result[result.length - 4], '║ Stopwatches introduce flakes by falling out of sync with the FakeAsync used in testing.');
108+
expect(result[result.length - 3], '║ A Stopwatch that stays in sync with FakeAsync is available through the Gesture or Test bindings, through samplingClock.');
109+
expect(result[result.length - 2], '╚═══════════════════════════════════════════════════════════════════════════════');
110+
expect(result[result.length - 1], ''); // trailing newline
111+
});
112+
95113
test('analyze.dart - verifyNoMissingLicense', () async {
96114
final String result = await capture(() => verifyNoMissingLicense(testRootPath, checkMinimums: false), shouldHaveErrors: true);
97115
final String file = 'test/analyze-test-input/root/packages/foo/foo.dart'

packages/flutter/lib/src/foundation/debug.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ Future<T> debugInstrumentAction<T>(String description, Future<T> Function() acti
7575
return true;
7676
}());
7777
if (instrument) {
78-
final Stopwatch stopwatch = Stopwatch()..start();
78+
final Stopwatch stopwatch = Stopwatch()..start(); // flutter_ignore: stopwatch (see analyze.dart)
79+
// Ignore context: The framework does not use this function internally so it will not cause flakes.
7980
try {
8081
return await action();
8182
} finally {

packages/flutter/lib/src/foundation/print.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ int _debugPrintedCharacters = 0;
6666
const int _kDebugPrintCapacity = 12 * 1024;
6767
const Duration _kDebugPrintPauseTime = Duration(seconds: 1);
6868
final Queue<String> _debugPrintBuffer = Queue<String>();
69-
final Stopwatch _debugPrintStopwatch = Stopwatch();
69+
final Stopwatch _debugPrintStopwatch = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
70+
// Ignore context: This is not used in tests, only for throttled logging.
7071
Completer<void>? _debugPrintCompleter;
7172
bool _debugPrintScheduled = false;
7273
void _debugPrintTask() {

packages/flutter/lib/src/gestures/binding.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ class SamplingClock {
3636
DateTime now() => DateTime.now();
3737

3838
/// Returns a new stopwatch that uses the current time as reported by `this`.
39-
Stopwatch stopwatch() => Stopwatch();
39+
///
40+
/// See also:
41+
///
42+
/// * [GestureBinding.debugSamplingClock], which is used in tests and
43+
/// debug builds to observe [FakeAsync].
44+
Stopwatch stopwatch() => Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
45+
// Ignore context: This is replaced by debugSampling clock in the test binding.
4046
}
4147

4248
// Class that handles resampling of touch events for multiple pointer
@@ -59,7 +65,8 @@ class _Resampler {
5965
Duration _frameTime = Duration.zero;
6066

6167
// Time since `_frameTime` was updated.
62-
Stopwatch _frameTimeAge = Stopwatch();
68+
Stopwatch _frameTimeAge = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
69+
// Ignore context: This is tested safely outside of FakeAsync.
6370

6471
// Last sample time and time stamp of last event.
6572
//

packages/flutter/test/foundation/timeline_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ void main() {
7272
// a bit inconsistent with Stopwatch.
7373
final int start = FlutterTimeline.now - 1;
7474
FlutterTimeline.timeSync('TEST', () {
75-
final Stopwatch watch = Stopwatch()..start();
75+
final Stopwatch watch = Stopwatch()..start(); // flutter_ignore: stopwatch (see analyze.dart)
76+
// Ignore context: Used safely for benchmarking.
7677
while (watch.elapsedMilliseconds < 5) {}
7778
watch.stop();
7879
});

packages/flutter_test/lib/src/test_compat.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,8 @@ class _Reporter {
297297
final bool _printPath;
298298

299299
/// A stopwatch that tracks the duration of the full run.
300-
final Stopwatch _stopwatch = Stopwatch();
300+
final Stopwatch _stopwatch = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
301+
// Ignore context: Used for logging of actual test runs, outside of FakeAsync.
301302

302303
/// The size of `_engine.passed` last time a progress notification was
303304
/// printed.

0 commit comments

Comments
 (0)