mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
736053538b
commit
817fa97014
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user