flutter/packages/flutter_tools/test/general.shard/hot_test.dart
Matan Lurey 5458f42782
Remove isExplicitPackageDependenciesEnabled: true, it is the default. (#169156)
This flag has been enabled by default for quite some time in `master`,
and in the current `stable`.

This is the first of many PRs to get rid of the flag and the deprecated
code it is guarding.
2025-05-21 04:26:10 +00:00

721 lines
26 KiB
Dart

// 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:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
import '../src/package_config.dart';
import '../src/throwing_pub.dart';
import 'hot_shared.dart';
void main() {
group('validateReloadReport', () {
testUsingContext('invalid', () async {
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{'notices': <Map<String, dynamic>>[]},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <String, dynamic>{'message': 'error'},
},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{'notices': <Map<String, dynamic>>[]},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': false},
],
},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{
'message': <String>['error'],
},
],
},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': 'error'},
<String, dynamic>{
'message': <String>['error'],
},
],
},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': 'error'},
],
},
}),
),
false,
);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{'type': 'ReloadReport', 'success': true}),
),
true,
);
});
testWithoutContext('ReasonForCancelling toString has a hint for specific errors', () {
final ReasonForCancelling reasonForCancelling = ReasonForCancelling(
message: 'Const class cannot remove fields',
);
expect(reasonForCancelling.toString(), contains('Try performing a hot restart instead.'));
});
});
group('hotRestart', () {
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
late MemoryFileSystem fileSystem;
late TestUsage testUsage;
late FakeAnalytics fakeAnalytics;
setUp(() {
fileSystem = MemoryFileSystem.test();
testUsage = TestUsage();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fileSystem,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
group('fails to setup', () {
late TestHotRunnerConfig failingTestingConfig;
setUp(() {
failingTestingConfig = TestHotRunnerConfig(
successfulHotRestartSetup: false,
successfulHotReloadSetup: false,
);
});
testUsingContext(
'setupHotRestart function fails',
() async {
fileSystem.file('pubspec.yaml').writeAsStringSync('''
name: my_app
''');
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app');
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[FakeFlutterDevice(device)];
final OperationResult result = await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
analytics: fakeAnalytics,
).restart(fullRestart: true);
expect(result.isOk, false);
expect(result.message, 'setupHotRestart failed');
expect(failingTestingConfig.updateDevFSCompleteCalled, false);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => failingTestingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
Pub: ThrowingPub.new,
},
);
testUsingContext(
'setupHotReload function fails',
() async {
fileSystem.file('pubspec.yaml').writeAsStringSync('''
name: my_app
''');
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app');
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[fakeFlutterDevice];
final OperationResult result =
await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
reassembleHelper:
(
List<FlutterDevice?> flutterDevices,
Map<FlutterDevice?, List<FlutterView>> viewCache,
void Function(String message)? onSlow,
String reloadMessage,
) async => ReassembleResult(
<FlutterView?, FlutterVmService?>{null: null},
false,
true,
),
analytics: fakeAnalytics,
).restart();
expect(result.isOk, false);
expect(result.message, 'setupHotReload failed');
expect(failingTestingConfig.updateDevFSCompleteCalled, false);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => failingTestingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
Pub: ThrowingPub.new,
},
);
});
group('shutdown hook tests', () {
late TestHotRunnerConfig shutdownTestingConfig;
setUp(() {
shutdownTestingConfig = TestHotRunnerConfig();
});
testUsingContext(
'shutdown hook called after signal',
() async {
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app');
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[
FlutterDevice(
device,
generator: residentCompiler,
buildInfo: BuildInfo.debug,
developmentShaderCompiler: const FakeShaderCompiler(),
),
];
await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
analytics: fakeAnalytics,
).cleanupAfterSignal();
expect(shutdownTestingConfig.shutdownHookCalled, true);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => shutdownTestingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'shutdown hook called after app stop',
() async {
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app');
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[
FlutterDevice(
device,
generator: residentCompiler,
buildInfo: BuildInfo.debug,
developmentShaderCompiler: const FakeShaderCompiler(),
),
];
await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
analytics: fakeAnalytics,
).preExit();
expect(shutdownTestingConfig.shutdownHookCalled, true);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => shutdownTestingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
},
);
});
group('successful hot restart', () {
late TestHotRunnerConfig testingConfig;
setUp(() {
testingConfig = TestHotRunnerConfig(successfulHotRestartSetup: true);
});
testUsingContext(
'correctly tracks time spent for analytics for hot restart',
() async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[fakeFlutterDevice];
fakeFlutterDevice.updateDevFSReportCallback =
() async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 2,
syncedBytes: 4,
scannedSourcesCount: 8,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
final FakeStopwatchFactory fakeStopwatchFactory = FakeStopwatchFactory(
stopwatches: <String, Stopwatch>{
'fullRestartHelper': FakeStopwatch()..elapsed = const Duration(seconds: 64),
'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 128),
},
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final OperationResult result = await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
stopwatchFactory: fakeStopwatchFactory,
analytics: fakeAnalytics,
).restart(fullRestart: true);
expect(result.isOk, true);
expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent(
'hot',
'restart',
parameters: CustomDimensions(
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: true,
hotEventOverallTimeInMs: 64000,
hotEventSyncedBytes: 4,
hotEventInvalidatedSourcesCount: 2,
hotEventTransferTimeInMs: 32000,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 128000,
hotEventScannedSourcesCount: 8,
),
),
]);
expect(
fakeAnalytics.sentEvents,
contains(
Event.hotRunnerInfo(
label: 'restart',
targetPlatform: 'flutter-tester',
sdkName: 'Tester',
emulator: false,
fullRestart: true,
syncedBytes: 4,
invalidatedSourcesCount: 2,
transferTimeInMs: 32000,
overallTimeInMs: 64000,
compileTimeInMs: 16000,
findInvalidatedTimeInMs: 128000,
scannedSourcesCount: 8,
),
),
);
expect(testingConfig.updateDevFSCompleteCalled, true);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
Usage: () => testUsage,
},
);
});
group('successful hot reload', () {
late TestHotRunnerConfig testingConfig;
setUp(() {
testingConfig = TestHotRunnerConfig(successfulHotReloadSetup: true);
});
testUsingContext(
'correctly tracks time spent for analytics for hot reload',
() async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[fakeFlutterDevice];
fakeFlutterDevice.updateDevFSReportCallback =
() async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
final FakeStopwatchFactory fakeStopwatchFactory = FakeStopwatchFactory(
stopwatches: <String, Stopwatch>{
'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 64),
'reloadSources:reload': FakeStopwatch()..elapsed = const Duration(seconds: 128),
'reloadSources:reassemble': FakeStopwatch()..elapsed = const Duration(seconds: 256),
'reloadSources:vm': FakeStopwatch()..elapsed = const Duration(seconds: 512),
},
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final OperationResult result =
await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
stopwatchFactory: fakeStopwatchFactory,
analytics: fakeAnalytics,
reloadSourcesHelper: (
HotRunner hotRunner,
List<FlutterDevice?> flutterDevices,
bool? pause,
Map<String, dynamic> firstReloadDetails,
String? targetPlatform,
String? sdkName,
bool? emulator,
String? reason,
Analytics? analytics,
) async {
firstReloadDetails['finalLibraryCount'] = 2;
firstReloadDetails['receivedLibraryCount'] = 3;
firstReloadDetails['receivedClassesCount'] = 4;
firstReloadDetails['receivedProceduresCount'] = 5;
return OperationResult.ok;
},
reassembleHelper:
(
List<FlutterDevice?> flutterDevices,
Map<FlutterDevice?, List<FlutterView>> viewCache,
void Function(String message)? onSlow,
String reloadMessage,
) async => ReassembleResult(
<FlutterView?, FlutterVmService?>{null: null},
false,
true,
),
).restart();
expect(result.isOk, true);
expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent(
'hot',
'reload',
parameters: CustomDimensions(
hotEventFinalLibraryCount: 2,
hotEventSyncedLibraryCount: 3,
hotEventSyncedClassesCount: 4,
hotEventSyncedProceduresCount: 5,
hotEventSyncedBytes: 8,
hotEventInvalidatedSourcesCount: 6,
hotEventTransferTimeInMs: 32000,
hotEventOverallTimeInMs: 128000,
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: false,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 64000,
hotEventScannedSourcesCount: 16,
hotEventReassembleTimeInMs: 256000,
hotEventReloadVMTimeInMs: 512000,
),
),
]);
expect(
fakeAnalytics.sentEvents,
contains(
Event.hotRunnerInfo(
label: 'reload',
targetPlatform: 'flutter-tester',
sdkName: 'Tester',
emulator: false,
fullRestart: false,
finalLibraryCount: 2,
syncedLibraryCount: 3,
syncedClassesCount: 4,
syncedProceduresCount: 5,
syncedBytes: 8,
invalidatedSourcesCount: 6,
transferTimeInMs: 32000,
overallTimeInMs: 128000,
compileTimeInMs: 16000,
findInvalidatedTimeInMs: 64000,
scannedSourcesCount: 16,
reassembleTimeInMs: 256000,
reloadVMTimeInMs: 512000,
),
),
);
expect(testingConfig.updateDevFSCompleteCalled, true);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
Usage: () => testUsage,
},
);
});
group('hot restart that failed to sync dev fs', () {
late TestHotRunnerConfig testingConfig;
setUp(() {
testingConfig = TestHotRunnerConfig(successfulHotRestartSetup: true);
});
testUsingContext(
'still calls the devfs complete callback',
() async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[fakeFlutterDevice];
fakeFlutterDevice.updateDevFSReportCallback =
() async => throw Exception('updateDevFS failed');
final HotRunner runner = HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
analytics: fakeAnalytics,
);
await expectLater(
runner.restart(fullRestart: true),
throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'message',
'Exception: updateDevFS failed',
),
),
);
expect(testingConfig.updateDevFSCompleteCalled, true);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
Usage: () => testUsage,
},
);
});
group('hot reload that failed to sync dev fs', () {
late TestHotRunnerConfig testingConfig;
setUp(() {
testingConfig = TestHotRunnerConfig(successfulHotReloadSetup: true);
});
testUsingContext(
'still calls the devfs complete callback',
() async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[fakeFlutterDevice];
fakeFlutterDevice.updateDevFSReportCallback =
() async => throw Exception('updateDevFS failed');
final HotRunner runner = HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
analytics: fakeAnalytics,
);
await expectLater(
runner.restart(),
throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'message',
'Exception: updateDevFS failed',
),
),
);
expect(testingConfig.updateDevFSCompleteCalled, true);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
Usage: () => testUsage,
},
);
});
});
group('hot attach', () {
late MemoryFileSystem fileSystem;
late FakeAnalytics fakeAnalytics;
setUp(() {
fileSystem = MemoryFileSystem.test();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fileSystem,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
testUsingContext(
'Exits with code 2 when HttpException is thrown '
'during VM service connection',
() async {
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app');
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[
TestFlutterDevice(
device: device,
generator: residentCompiler,
exception: const HttpException(
'Connection closed before full header was received, '
'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws',
),
),
];
final int exitCode = await HotRunner(
devices,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
target: 'main.dart',
analytics: fakeAnalytics,
).attach(needsFullRestart: false);
expect(exitCode, 2);
},
overrides: <Type, Generator>{
HotRunnerConfig: () => TestHotRunnerConfig(),
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.any(),
},
);
});
group('hot cleanupAtFinish()', () {
late MemoryFileSystem fileSystem;
late FakeAnalytics fakeAnalytics;
setUp(() {
fileSystem = MemoryFileSystem.test();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fileSystem,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
testUsingContext('disposes each device', () async {
final FakeDevice device1 = FakeDevice();
final FakeDevice device2 = FakeDevice();
final FakeFlutterDevice flutterDevice1 = FakeFlutterDevice(device1);
final FakeFlutterDevice flutterDevice2 = FakeFlutterDevice(device2);
final List<FlutterDevice> devices = <FlutterDevice>[flutterDevice1, flutterDevice2];
await HotRunner(
devices,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
target: 'main.dart',
analytics: fakeAnalytics,
).cleanupAtFinish();
expect(device1.disposed, true);
expect(device2.disposed, true);
expect(flutterDevice1.stoppedEchoingDeviceLog, true);
expect(flutterDevice2.stoppedEchoingDeviceLog, true);
});
});
}