Upload DerivedData logs in CI (#142643)

When the Dart VM is not found within 10 minutes in CI on CoreDevices (iOS 17+), stop the app and upload the logs from DerivedData. The app has to be stopped first since the logs are not put in DerivedData until it's stopped.

Also, rearranged some logic to have CoreDevice have its own function for Dart VM url discovery.

Debugging for https://github.com/flutter/flutter/issues/142448.
This commit is contained in:
Victoria Ashworth 2024-02-01 15:31:28 -06:00 committed by GitHub
parent 899f423464
commit e5c286e02e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 255 additions and 97 deletions

View file

@ -469,6 +469,7 @@ List<String> _flutterCommandArgs(String command, List<String> options) {
final String? localEngineHost = localEngineHostFromEnv;
final String? localEngineSrcPath = localEngineSrcPathFromEnv;
final String? localWebSdk = localWebSdkFromEnv;
final bool pubOrPackagesCommand = command.startsWith('packages') || command.startsWith('pub');
return <String>[
command,
if (deviceOperatingSystem == DeviceOperatingSystem.ios && supportedDeviceTimeoutCommands.contains(command))
@ -489,7 +490,9 @@ List<String> _flutterCommandArgs(String command, List<String> options) {
// Use CI flag when running devicelab tests, except for `packages`/`pub` commands.
// `packages`/`pub` commands effectively runs the `pub` tool, which does not have
// the same allowed args.
if (!command.startsWith('packages') && !command.startsWith('pub')) '--ci',
if (!pubOrPackagesCommand) '--ci',
if (!pubOrPackagesCommand && hostAgent.dumpDirectory != null)
'--debug-logs-dir=${hostAgent.dumpDirectory!.path}'
];
}

View file

@ -525,6 +525,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
devToolsServerAddress: devToolsServerAddress,
serveObservatory: serveObservatory,
usingCISystem: usingCISystem,
debugLogsDirectoryPath: debugLogsDirectoryPath,
);
return buildInfo.isDebug

View file

@ -264,6 +264,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
enableDartProfiling: enableDartProfiling,
enableEmbedderApi: enableEmbedderApi,
usingCISystem: usingCISystem,
debugLogsDirectoryPath: debugLogsDirectoryPath,
);
} else {
return DebuggingOptions.enabled(
@ -319,6 +320,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
enableDartProfiling: enableDartProfiling,
enableEmbedderApi: enableEmbedderApi,
usingCISystem: usingCISystem,
debugLogsDirectoryPath: debugLogsDirectoryPath,
);
}
}

View file

@ -364,6 +364,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
nullAssertions: boolArg(FlutterOptions.kNullAssertions),
usingCISystem: usingCISystem,
enableImpeller: ImpellerStatus.fromBool(argResults!['enable-impeller'] as bool?),
debugLogsDirectoryPath: debugLogsDirectoryPath,
);
String? testAssetDirectory;

View file

@ -963,6 +963,7 @@ class DebuggingOptions {
this.enableDartProfiling = true,
this.enableEmbedderApi = false,
this.usingCISystem = false,
this.debugLogsDirectoryPath,
}) : debuggingEnabled = true;
DebuggingOptions.disabled(this.buildInfo, {
@ -988,6 +989,7 @@ class DebuggingOptions {
this.enableDartProfiling = true,
this.enableEmbedderApi = false,
this.usingCISystem = false,
this.debugLogsDirectoryPath,
}) : debuggingEnabled = false,
useTestFonts = false,
startPaused = false,
@ -1069,6 +1071,7 @@ class DebuggingOptions {
required this.enableDartProfiling,
required this.enableEmbedderApi,
required this.usingCISystem,
required this.debugLogsDirectoryPath,
});
final bool debuggingEnabled;
@ -1112,6 +1115,7 @@ class DebuggingOptions {
final bool enableDartProfiling;
final bool enableEmbedderApi;
final bool usingCISystem;
final String? debugLogsDirectoryPath;
/// Whether the tool should try to uninstall a previously installed version of the app.
///
@ -1258,6 +1262,7 @@ class DebuggingOptions {
'enableDartProfiling': enableDartProfiling,
'enableEmbedderApi': enableEmbedderApi,
'usingCISystem': usingCISystem,
'debugLogsDirectoryPath': debugLogsDirectoryPath,
};
static DebuggingOptions fromJson(Map<String, Object?> json, BuildInfo buildInfo) =>
@ -1313,6 +1318,7 @@ class DebuggingOptions {
enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true,
enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false,
usingCISystem: (json['usingCISystem'] as bool?) ?? false,
debugLogsDirectoryPath: json['debugLogsDirectoryPath'] as String?,
);
}

View file

@ -620,105 +620,75 @@ class IOSDevice extends Device {
});
Uri? localUri;
if (isWirelesslyConnected) {
// When using a CoreDevice, device logs are unavailable and therefore
// cannot be used to get the Dart VM url. Instead, get the Dart VM
// Service by finding services matching the app bundle id and the
// device name.
//
// If not using a CoreDevice, wait for the Dart VM url to be discovered
// via logs and then get the Dart VM Service by finding services matching
// the app bundle id and the Dart VM port.
//
// Then in both cases, get the device IP from the Dart VM Service to
// construct the Dart VM url using the device IP as the host.
if (isCoreDevice) {
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
useDeviceIPAsHost: true,
if (isCoreDevice || forceXcodeDebugWorkflow) {
localUri = await _discoverDartVMForCoreDevice(
debuggingOptions: debuggingOptions,
packageId: packageId,
ipv6: ipv6,
vmServiceDiscovery: vmServiceDiscovery,
);
} else if (isWirelesslyConnected) {
// Wait for the Dart VM url to be discovered via logs (from `ios-deploy`)
// in ProtocolDiscovery. Then via mDNS, construct the Dart VM url using
// the device IP as the host by finding Dart VM services matching the
// app bundle id and Dart VM port.
// Wait for Dart VM Service to start up.
final Uri? serviceURL = await vmServiceDiscovery?.uri;
if (serviceURL == null) {
await iosDeployDebugger?.stopAndDumpBacktrace();
await dispose();
return LaunchResult.failed();
}
// If Dart VM Service URL with the device IP is not found within 5 seconds,
// change the status message to prompt users to click Allow. Wait 5 seconds because it
// should only show this message if they have not already approved the permissions.
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
startAppStatus.stop();
startAppStatus = _logger.startProgress(
'Waiting for approval of local network permissions...',
);
} else {
// Wait for Dart VM Service to start up.
final Uri? serviceURL = await vmServiceDiscovery?.uri;
if (serviceURL == null) {
await iosDeployDebugger?.stopAndDumpBacktrace();
});
// Get Dart VM Service URL with the device IP as the host.
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
deviceVmservicePort: serviceURL.port,
useDeviceIPAsHost: true,
);
mDNSLookupTimer.cancel();
} else {
localUri = await vmServiceDiscovery?.uri;
// If the `ios-deploy` debugger loses connection before it finds the
// Dart Service VM url, try starting the debugger and launching the
// app again.
if (localUri == null &&
debuggingOptions.usingCISystem &&
iosDeployDebugger != null &&
iosDeployDebugger!.lostConnection) {
_logger.printStatus('Lost connection to device. Trying to connect again...');
await dispose();
vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery(
package: package,
bundle: bundle,
debuggingOptions: debuggingOptions,
launchArguments: launchArguments,
ipv6: ipv6,
uninstallFirst: false,
skipInstall: true,
);
installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
if (installationResult != 0) {
_printInstallError(bundle);
await dispose();
return LaunchResult.failed();
}
// If Dart VM Service URL with the device IP is not found within 5 seconds,
// change the status message to prompt users to click Allow. Wait 5 seconds because it
// should only show this message if they have not already approved the permissions.
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
startAppStatus.stop();
startAppStatus = _logger.startProgress(
'Waiting for approval of local network permissions...',
);
});
// Get Dart VM Service URL with the device IP as the host.
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
deviceVmservicePort: serviceURL.port,
useDeviceIPAsHost: true,
);
mDNSLookupTimer.cancel();
}
} else {
if ((isCoreDevice || forceXcodeDebugWorkflow) && vmServiceDiscovery != null) {
// When searching for the Dart VM url, search for it via ProtocolDiscovery
// (device logs) and mDNS simultaneously, since both can be flaky at times.
final Future<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
);
final Future<Uri?> vmUrlFromLogs = vmServiceDiscovery.uri;
localUri = await Future.any(
<Future<Uri?>>[vmUrlFromMDns, vmUrlFromLogs]
);
// If the first future to return is null, wait for the other to complete.
if (localUri == null) {
final List<Uri?> vmUrls = await Future.wait(
<Future<Uri?>>[vmUrlFromMDns, vmUrlFromLogs]
);
localUri = vmUrls.where((Uri? vmUrl) => vmUrl != null).firstOrNull;
}
} else {
localUri = await vmServiceDiscovery?.uri;
// If the `ios-deploy` debugger loses connection before it finds the
// Dart Service VM url, try starting the debugger and launching the
// app again.
if (localUri == null &&
debuggingOptions.usingCISystem &&
iosDeployDebugger != null &&
iosDeployDebugger!.lostConnection) {
_logger.printStatus('Lost connection to device. Trying to connect again...');
await dispose();
vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery(
package: package,
bundle: bundle,
debuggingOptions: debuggingOptions,
launchArguments: launchArguments,
ipv6: ipv6,
uninstallFirst: false,
skipInstall: true,
);
installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
if (installationResult != 0) {
_printInstallError(bundle);
await dispose();
return LaunchResult.failed();
}
localUri = await vmServiceDiscovery.uri;
}
localUri = await vmServiceDiscovery.uri;
}
}
timer.cancel();
@ -757,6 +727,96 @@ class IOSDevice extends Device {
_logger.printError('');
}
/// Find the Dart VM url using ProtocolDiscovery (logs from `idevicesyslog`)
/// and mDNS simultaneously, using whichever is found first. `idevicesyslog`
/// does not work on wireless devices, so only use mDNS for wireless devices.
/// Wireless devices require using the device IP as the host.
Future<Uri?> _discoverDartVMForCoreDevice({
required String packageId,
required bool ipv6,
required DebuggingOptions debuggingOptions,
ProtocolDiscovery? vmServiceDiscovery,
}) async {
Timer? maxWaitForCI;
final Completer<Uri?> cancelCompleter = Completer<Uri?>();
// When testing in CI, wait a max of 10 minutes for the Dart VM to be found.
// Afterwards, stop the app from running and upload DerivedData Logs to debug
// logs directory. CoreDevices are run through Xcode and launch logs are
// therefore found in DerivedData.
if (debuggingOptions.usingCISystem && debuggingOptions.debugLogsDirectoryPath != null) {
maxWaitForCI = Timer(const Duration(minutes: 10), () async {
_logger.printError('Failed to find Dart VM after 10 minutes.');
await _xcodeDebug.exit();
final String? homePath = _platform.environment['HOME'];
Directory? derivedData;
if (homePath != null) {
derivedData = _fileSystem.directory(
_fileSystem.path.join(homePath, 'Library', 'Developer', 'Xcode', 'DerivedData'),
);
}
if (derivedData != null && derivedData.existsSync()) {
final Directory debugLogsDirectory = _fileSystem.directory(
debuggingOptions.debugLogsDirectoryPath,
);
debugLogsDirectory.createSync(recursive: true);
for (final FileSystemEntity entity in derivedData.listSync()) {
if (entity is! Directory || !entity.childDirectory('Logs').existsSync()) {
continue;
}
final Directory logsToCopy = entity.childDirectory('Logs');
final Directory copyDestination = debugLogsDirectory
.childDirectory('DerivedDataLogs')
.childDirectory(entity.basename)
.childDirectory('Logs');
_logger.printTrace('Copying logs ${logsToCopy.path} to ${copyDestination.path}...');
copyDirectory(logsToCopy, copyDestination);
}
}
cancelCompleter.complete();
});
}
final Future<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
useDeviceIPAsHost: isWirelesslyConnected,
);
final List<Future<Uri?>> discoveryOptions = <Future<Uri?>>[
vmUrlFromMDns,
];
// vmServiceDiscovery uses device logs (`idevicesyslog`), which doesn't work
// on wireless devices.
if (vmServiceDiscovery != null && !isWirelesslyConnected) {
final Future<Uri?> vmUrlFromLogs = vmServiceDiscovery.uri;
discoveryOptions.add(vmUrlFromLogs);
}
Uri? localUri = await Future.any(
<Future<Uri?>>[...discoveryOptions, cancelCompleter.future],
);
// If the first future to return is null, wait for the other to complete
// unless canceled.
if (localUri == null && !cancelCompleter.isCompleted) {
final Future<List<Uri?>> allDiscoveryOptionsComplete = Future.wait(discoveryOptions);
await Future.any(<Future<Object?>>[
allDiscoveryOptionsComplete,
cancelCompleter.future,
]);
if (!cancelCompleter.isCompleted) {
// If it wasn't cancelled, that means one of the discovery options completed.
final List<Uri?> vmUrls = await allDiscoveryOptionsComplete;
localUri = vmUrls.where((Uri? vmUrl) => vmUrl != null).firstOrNull;
}
}
maxWaitForCI?.cancel();
return localUri;
}
ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({
required IOSApp package,
required Directory bundle,

View file

@ -377,6 +377,8 @@ abstract class FlutterCommand extends Command<void> {
/// Whether flutter is being run from our CI.
bool get usingCISystem => boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true);
String? get debugLogsDirectoryPath => stringArg(FlutterGlobalOptions.kDebugLogsDirectoryFlag, global: true);
/// The value of the `--filesystem-scheme` argument.
///
/// This can be overridden by some of its subclasses.

View file

@ -44,6 +44,7 @@ abstract final class FlutterGlobalOptions {
static const String kVersionFlag = 'version';
static const String kWrapColumnOption = 'wrap-column';
static const String kWrapFlag = 'wrap';
static const String kDebugLogsDirectoryFlag = 'debug-logs-dir';
}
class FlutterCommandRunner extends CommandRunner<void> {
@ -164,6 +165,11 @@ class FlutterCommandRunner extends CommandRunner<void> {
help: 'Enable a set of CI-specific test debug settings.',
hide: !verboseHelp,
);
argParser.addOption(
FlutterGlobalOptions.kDebugLogsDirectoryFlag,
help: 'Path to a directory where logs for debugging may be added.',
hide: !verboseHelp,
);
}
@override

View file

@ -427,6 +427,7 @@ void main() {
'--skia-deterministic-rendering',
'--enable-embedder-api',
'--ci',
'--debug-logs-dir=path/to/logs'
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
@ -444,6 +445,7 @@ void main() {
expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
expect(options.debugLogsDirectoryPath, 'path/to/logs');
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),

View file

@ -1262,6 +1262,7 @@ void main() {
'--skia-deterministic-rendering',
'--enable-embedder-api',
'--ci',
'--debug-logs-dir=path/to/logs'
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
@ -1281,6 +1282,7 @@ void main() {
expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
expect(options.debugLogsDirectoryPath, 'path/to/logs');
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),

View file

@ -966,6 +966,78 @@ void main() {
MDnsVmServiceDiscovery: () => mdnsDiscovery,
});
});
testUsingContext('IOSDevice.startApp fails to find Dart VM in CI', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeProcessManager processManager = FakeProcessManager.empty();
const String pathToFlutterLogs = '/path/to/flutter/logs';
const String pathToHome = '/path/to/home';
final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
final Directory bundleLocation = fileSystem.currentDirectory;
final IOSDevice device = setUpIOSDevice(
processManager: processManager,
fileSystem: fileSystem,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(
expectedProject: XcodeDebugProject(
scheme: 'Runner',
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
expectedBundlePath: bundleLocation.path,
),
platform: FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{
'HOME': pathToHome,
},
),
);
final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app',
bundleName: 'Runner',
uncompressedBundle: bundleLocation,
applicationPackage: bundleLocation,
);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
device.portForwarder = const NoOpDevicePortForwarder();
device.setLogReader(iosApp, deviceLogReader);
const String projectLogsPath = 'Runner-project1/Logs/Launch/Runner.xcresults';
fileSystem.directory('$pathToHome/Library/Developer/Xcode/DerivedData/$projectLogsPath').createSync(recursive: true);
final Completer<void> completer = Completer<void>();
await FakeAsync().run((FakeAsync time) {
final Future<LaunchResult> futureLaunchResult = device.startApp(iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(
BuildInfo.debug,
usingCISystem: true,
debugLogsDirectoryPath: pathToFlutterLogs,
),
platformArgs: <String, dynamic>{},
);
futureLaunchResult.then((LaunchResult launchResult) {
expect(launchResult.started, false);
expect(launchResult.hasVmService, false);
expect(fileSystem.directory('$pathToFlutterLogs/DerivedDataLogs/$projectLogsPath').existsSync(), true);
completer.complete();
});
time.elapse(const Duration(minutes: 15));
time.flushMicrotasks();
return completer.future;
});
}, overrides: <Type, Generator>{
MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(returnsNull: true),
});
});
});
}
@ -980,9 +1052,10 @@ IOSDevice setUpIOSDevice({
bool isCoreDevice = false,
IOSCoreDeviceControl? coreDeviceControl,
FakeXcodeDebug? xcodeDebug,
FakePlatform? platform,
}) {
final Artifacts artifacts = Artifacts.test();
final FakePlatform macPlatform = FakePlatform(
final FakePlatform macPlatform = platform ?? FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{},
);