Combine expression evaluation tests to reduce testing time (#169860)

Each `flutter run` invocation is expensive (time- and memory-wise). Each
invocation also launches its own Chrome instance.

This PR is attempting to minimize the number of launches by combining
tests that need the same `flutter run` configuration into a single test.
That way, we are reusing a single instance to do many checks, instead of
launching a new instance for each check.

Currently, I did this for expression evaluation tests, which cut their
runtime in half on my local machine.

This PR aims to help with issues like
https://github.com/flutter/flutter/issues/169574 and
https://github.com/flutter/flutter/issues/169304.

Closes https://github.com/flutter/flutter/issues/110879
This commit is contained in:
Mouad Debbar 2025-06-05 12:10:14 -04:00 committed by GitHub
parent 736053538b
commit 817fa97014
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 55 additions and 162 deletions

View File

@ -14,7 +14,6 @@ import 'hot_reload_const_project.dart';
void testAll({
bool chrome = false,
List<String> additionalCommandArgs = const <String>[],
String constClassFieldRemovalErrorMessage = 'Try performing a hot restart instead.',
Object? skip = false,
}) {
group('chrome: $chrome'
@ -35,35 +34,28 @@ void testAll({
});
testWithoutContext(
'hot reload displays a formatted error message when removing a field from a const class',
'hot reload displays a formatted error message when removing a field from a const class, and hot restart succeeds',
() async {
await flutter.run(
device:
chrome ? GoogleChromeDevice.kChromeDeviceId : FlutterTesterDevices.kTesterDeviceId,
additionalCommandArgs: additionalCommandArgs,
);
project.removeFieldFromConstClass();
expect(
project.removeFieldFromConstClass();
await expectLater(
flutter.hotReload(),
throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'message',
contains(constClassFieldRemovalErrorMessage),
contains('Try performing a hot restart instead.'),
),
),
);
await expectLater(flutter.hotRestart(), completes);
},
);
testWithoutContext('hot restart succeeds when removing a field from a const class', () async {
await flutter.run(
device: chrome ? GoogleChromeDevice.kChromeDeviceId : FlutterTesterDevices.kTesterDeviceId,
additionalCommandArgs: additionalCommandArgs,
);
project.removeFieldFromConstClass();
await flutter.hotRestart();
});
});
}

View File

@ -32,21 +32,18 @@ void testAll({bool chrome = false, List<String> additionalCommandArgs = const <S
tryToDelete(tempDir);
});
testWithoutContext('hot reload works without error', () async {
testWithoutContext('single and multiple hot reloads', () async {
await runFlutterWithDevice(
flutter,
chrome: chrome,
additionalCommandArgs: additionalCommandArgs,
);
await flutter.hotReload();
});
testWithoutContext('multiple overlapping hot reload are debounced and queued', () async {
await runFlutterWithDevice(
flutter,
chrome: chrome,
additionalCommandArgs: additionalCommandArgs,
);
// Hot reload works without error.
await flutter.hotReload();
// Multiple overlapping hot reloads are debounced and queued.
// Capture how many *real* hot reloads occur.
int numReloads = 0;
final StreamSubscription<void> subscription = flutter.stdout

View File

@ -39,7 +39,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
rootBundle.evict('pubspec.yaml');
rootBundle.load('pubspec.yaml').then((_) {
print('LOADED DATA');
print('LOADED DATA'); // HOT RELOAD PRINT
}, onError: (dynamic error, StackTrace stackTrace) {
print('FAILED TO LOAD');
});
@ -48,8 +48,11 @@ class MyApp extends StatelessWidget {
}
''';
void uncommentHotReloadPrint() {
final String newMainContents = main.replaceAll('LOADED DATA', 'SECOND DATA');
void replaceHotReloadPrint(String newPrint) {
final String newMainContents = main.replaceAll(
RegExp(r"print\('.*'\); // HOT RELOAD PRINT"),
"print('$newPrint'); // HOT RELOAD PRINT",
);
writeFile(
fileSystem.path.join(dir.path, 'lib', 'main.dart'),
newMainContents,

View File

@ -31,9 +31,10 @@ void testAll({bool chrome = false, List<String> additionalCommandArgs = const <S
tryToDelete(tempDir);
});
testWithoutContext('hot reload does not need to sync assets on the first reload', () async {
testWithoutContext('hot reload/restart do not need to sync assets on reload', () async {
final Completer<void> onFirstLoad = Completer<void>();
final Completer<void> onSecondLoad = Completer<void>();
final Completer<void> onThirdLoad = Completer<void>();
flutter.stdout.listen((String line) {
// If the asset fails to load, this message will be printed instead.
@ -48,6 +49,9 @@ void testAll({bool chrome = false, List<String> additionalCommandArgs = const <S
if (line.contains('SECOND DATA')) {
onSecondLoad.complete();
}
if (line.contains('THIRD DATA')) {
onThirdLoad.complete();
}
});
flutter.stdout.listen(printOnFailure);
await flutter.run(
@ -56,39 +60,13 @@ void testAll({bool chrome = false, List<String> additionalCommandArgs = const <S
);
await onFirstLoad.future;
project.uncommentHotReloadPrint();
project.replaceHotReloadPrint('SECOND DATA');
await flutter.hotReload();
await onSecondLoad.future;
});
testWithoutContext('hot restart does not need to sync assets on the first reload', () async {
final Completer<void> onFirstLoad = Completer<void>();
final Completer<void> onSecondLoad = Completer<void>();
flutter.stdout.listen((String line) {
// If the asset fails to load, this message will be printed instead.
// this indicates that the devFS was not able to locate the asset
// after the hot reload.
if (line.contains('FAILED TO LOAD')) {
fail('Did not load asset: $line');
}
if (line.contains('LOADED DATA')) {
onFirstLoad.complete();
}
if (line.contains('SECOND DATA')) {
onSecondLoad.complete();
}
});
flutter.stdout.listen(printOnFailure);
await flutter.run(
device: chrome ? GoogleChromeDevice.kChromeDeviceId : FlutterTesterDevices.kTesterDeviceId,
additionalCommandArgs: additionalCommandArgs,
);
await onFirstLoad.future;
project.uncommentHotReloadPrint();
project.replaceHotReloadPrint('THIRD DATA');
await flutter.hotRestart();
await onSecondLoad.future;
await onThirdLoad.future;
});
});
}

View File

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@TestOn('linux') // https://github.com/flutter/flutter/issues/169304
@Tags(<String>['flutter-test-driver'])
library;

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@TestOn('linux') // https://github.com/flutter/flutter/issues/169304
@TestOn('linux') // https://github.com/flutter/flutter/issues/170062
@Tags(<String>['flutter-test-driver'])
library;
@ -10,11 +10,5 @@ import '../integration.shard/test_data/hot_reload_errors_common.dart';
import '../src/common.dart';
void main() {
testAll(
chrome: true,
additionalCommandArgs: <String>['--web-experimental-hot-reload'],
// TODO(srujzs): Remove this custom message once we have the delta inspector emitting the same
// string as the VM.
constClassFieldRemovalErrorMessage: 'Const class cannot remove fields',
);
testAll(chrome: true, additionalCommandArgs: <String>['--web-experimental-hot-reload']);
}

View File

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@TestOn('linux') // https://github.com/flutter/flutter/issues/169304
@Tags(<String>['flutter-test-driver'])
library;

View File

@ -70,60 +70,19 @@ Future<void> testAll({required bool useDDCLibraryBundleFormat}) async {
await failToEvaluateExpression(flutter);
});
testWithoutContext('shows no native javascript objects in static scope', () async {
testWithoutContext('can evaluate expressions in library, top level and build method', () async {
await start(expressionEvaluation: true);
await evaluateTrivialExpressionsInLibrary(flutter);
await evaluateComplexExpressionsInLibrary(flutter);
await evaluateWebLibraryBooleanFromEnvironmentInLibrary(flutter);
await breakInTopLevelFunction(flutter);
await checkStaticScope(flutter);
});
testWithoutContext('can handle compilation errors', () async {
await start(expressionEvaluation: true);
await breakInTopLevelFunction(flutter);
await evaluateErrorExpressions(flutter);
});
testWithoutContext('can evaluate trivial expressions in top level function', () async {
await start(expressionEvaluation: true);
await breakInTopLevelFunction(flutter);
await evaluateTrivialExpressions(flutter);
});
testWithoutContext('can evaluate trivial expressions in build method', () async {
await start(expressionEvaluation: true);
await breakInBuildMethod(flutter);
await evaluateTrivialExpressions(flutter);
});
testWithoutContext('can evaluate complex expressions in top level function', () async {
await start(expressionEvaluation: true);
await breakInTopLevelFunction(flutter);
await evaluateComplexExpressions(flutter);
});
testWithoutContext('can evaluate complex expressions in build method', () async {
await start(expressionEvaluation: true);
await breakInBuildMethod(flutter);
await evaluateComplexExpressions(flutter);
});
testWithoutContext('can evaluate trivial expressions in library without pause', () async {
await start(expressionEvaluation: true);
await evaluateTrivialExpressionsInLibrary(flutter);
});
testWithoutContext('can evaluate complex expressions in library without pause', () async {
await start(expressionEvaluation: true);
await evaluateComplexExpressionsInLibrary(flutter);
});
testWithoutContext('evaluated expression includes web library environment defines', () async {
await start(expressionEvaluation: true);
await evaluateWebLibraryBooleanFromEnvironmentInLibrary(flutter);
});
testWithoutContext('evaluated expression includes correctly mapped stack trace', () async {
await start(expressionEvaluation: true);
await breakInTopLevelFunction(flutter);
// Test that the call comes from some Dart getter called `current` (the
// location of which will be compiler-specific) and that the lines and
// file name of the current location is correct and reports a Dart path.
@ -140,6 +99,10 @@ Future<void> testAll({required bool useDDCLibraryBundleFormat}) async {
end = stackTrace.indexOf('package:test/main.dart 15:7', end);
return end != -1;
});
await breakInBuildMethod(flutter);
await evaluateTrivialExpressions(flutter);
await evaluateComplexExpressions(flutter);
});
});
@ -190,35 +153,17 @@ Future<void> testAll({required bool useDDCLibraryBundleFormat}) async {
await failToEvaluateExpression(flutter);
});
testWithoutContext('can evaluate trivial expressions in a test', () async {
testWithoutContext('can evaluate expressions in a test', () async {
await startPaused(expressionEvaluation: true);
await evaluateTrivialExpressionsInLibrary(flutter);
await evaluateComplexExpressionsInLibrary(flutter);
await evaluateWebLibraryBooleanFromEnvironmentInLibrary(flutter);
await breakInMethod(flutter);
await evaluateTrivialExpressions(flutter);
});
testWithoutContext('can evaluate complex expressions in a test', () async {
await startPaused(expressionEvaluation: true);
await breakInMethod(flutter);
await evaluateComplexExpressions(flutter);
});
testWithoutContext('can evaluate trivial expressions in library without pause', () async {
await startPaused(expressionEvaluation: true);
await evaluateTrivialExpressionsInLibrary(flutter);
});
testWithoutContext('can evaluate complex expressions in library without pause', () async {
await startPaused(expressionEvaluation: true);
await evaluateComplexExpressionsInLibrary(flutter);
});
testWithoutContext('evaluated expression includes web library environment defines', () async {
await startPaused(expressionEvaluation: true);
await evaluateWebLibraryBooleanFromEnvironmentInLibrary(flutter);
});
testWithoutContext('evaluated expression includes correctly mapped stack trace', () async {
await startPaused(expressionEvaluation: true);
await breakInMethod(flutter);
await evaluateStackTraceCurrent(flutter, (String stackTrace) {
final Iterable<RegExpMatch> matches = stackTraceCurrentRegexp.allMatches(stackTrace);
if (matches.length != 1) {

View File

@ -84,31 +84,9 @@ Future<void> _testProject(
tryToDelete(tempDir);
});
testWithoutContext('$testName: hot restart works without error', () async {
flutter.stdout.listen(printOnFailure);
await flutter.run(
device: GoogleChromeDevice.kChromeDeviceId,
additionalCommandArgs: <String>[
'--verbose',
if (useDDCLibraryBundleFormat)
'--web-experimental-hot-reload'
else
'--no-web-experimental-hot-reload',
],
);
await flutter.hotRestart();
});
testWithoutContext(
'$testName: newly added code executes during hot restart - canvaskit',
'$testName: hot restart works without error and newly added code executes',
() async {
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
printOnFailure(line);
if (line.contains('(((((RELOAD WORKED)))))')) {
completer.complete();
}
});
await flutter.run(
device: GoogleChromeDevice.kChromeDeviceId,
additionalCommandArgs: <String>[
@ -119,6 +97,16 @@ Future<void> _testProject(
'--no-web-experimental-hot-reload',
],
);
// hot restart works without error
await flutter.hotRestart();
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
printOnFailure(line);
if (line.contains('(((((RELOAD WORKED)))))')) {
completer.complete();
}
});
project.uncommentHotReloadPrint();
try {
await flutter.hotRestart();
@ -127,7 +115,5 @@ Future<void> _testProject(
await subscription.cancel();
}
},
// Skipped for https://github.com/flutter/flutter/issues/110879.
skip: true,
);
}