diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index ad057e3fd0..20301e51ee 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -471,6 +471,10 @@ List _flutterCommandArgs(String command, List options) { if (localEngineSrcPath != null) ...['--local-engine-src-path', localEngineSrcPath], if (localWebSdk != null) ...['--local-web-sdk', localWebSdk], ...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', ]; } diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 7e9fba2992..6bd6539181 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -33,6 +33,7 @@ import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; +import '../runner/flutter_command_runner.dart'; import '../vmservice.dart'; /// A Flutter-command that attaches to applications that have been launched @@ -528,6 +529,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. ddsPort: ddsPort, devToolsServerAddress: devToolsServerAddress, serveObservatory: serveObservatory, + usingCISystem: usingCISystem, ); return buildInfo.isDebug @@ -535,7 +537,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. flutterDevices, target: targetFile, debuggingOptions: debuggingOptions, - packagesFilePath: globalResults!['packages'] as String?, + packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?, projectRootPath: stringArg('project-root'), dillOutputPath: stringArg('output-dill'), ipv6: usesIpv6, diff --git a/packages/flutter_tools/lib/src/commands/channel.dart b/packages/flutter_tools/lib/src/commands/channel.dart index 5a8695ed16..4082a973ae 100644 --- a/packages/flutter_tools/lib/src/commands/channel.dart +++ b/packages/flutter_tools/lib/src/commands/channel.dart @@ -6,6 +6,7 @@ import '../base/common.dart'; import '../cache.dart'; import '../globals.dart' as globals; import '../runner/flutter_command.dart'; +import '../runner/flutter_command_runner.dart'; import '../version.dart'; class ChannelCommand extends FlutterCommand { @@ -40,7 +41,7 @@ class ChannelCommand extends FlutterCommand { case 0: await _listChannels( showAll: boolArg('all'), - verbose: globalResults?['verbose'] == true, + verbose: globalResults?[FlutterGlobalOptions.kVerboseFlag] == true, ); return FlutterCommandResult.success(); case 1: diff --git a/packages/flutter_tools/lib/src/commands/custom_devices.dart b/packages/flutter_tools/lib/src/commands/custom_devices.dart index dee0ee7745..3f07e7ff73 100644 --- a/packages/flutter_tools/lib/src/commands/custom_devices.dart +++ b/packages/flutter_tools/lib/src/commands/custom_devices.dart @@ -25,6 +25,7 @@ import '../custom_devices/custom_devices_config.dart'; import '../device_port_forwarder.dart'; import '../features.dart'; import '../runner/flutter_command.dart'; +import '../runner/flutter_command_runner.dart'; /// just the function signature of the [print] function. /// The Object arg may be null. @@ -811,7 +812,7 @@ Delete a device from the config file. Future runCommand() async { checkFeatureEnabled(); - final String? id = globalResults!['device-id'] as String?; + final String? id = globalResults![FlutterGlobalOptions.kDeviceIdOption] as String?; if (id == null || !customDevicesConfig.contains(id)) { throwToolExit('Couldn\'t find device with id "$id" in config at "${customDevicesConfig.configPath}"'); } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 1cc7c3c828..61829b16eb 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -26,6 +26,7 @@ import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; +import '../runner/flutter_command_runner.dart'; import '../tracing.dart'; import '../vmservice.dart'; import '../web/web_runner.dart'; @@ -247,6 +248,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment uninstallFirst: uninstallFirst, enableDartProfiling: enableDartProfiling, enableEmbedderApi: enableEmbedderApi, + usingCISystem: usingCISystem, ); } else { return DebuggingOptions.enabled( @@ -298,6 +300,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment serveObservatory: boolArg('serve-observatory'), enableDartProfiling: enableDartProfiling, enableEmbedderApi: enableEmbedderApi, + usingCISystem: usingCISystem, ); } } @@ -643,7 +646,7 @@ class RunCommand extends RunCommandBase { : globals.fs.file(applicationBinaryPath), trackWidgetCreation: trackWidgetCreation, projectRootPath: stringArg('project-root'), - packagesFilePath: globalResults!['packages'] as String?, + packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?, dillOutputPath: stringArg('output-dill'), ipv6: ipv6 ?? false, multidexEnabled: boolArg('multidex'), diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index fda2142294..091ea5a9f6 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -422,6 +422,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { disablePortPublication: true, enableDds: enableDds, nullAssertions: boolArg(FlutterOptions.kNullAssertions), + usingCISystem: usingCISystem, ); Device? integrationTestDevice; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 80d0fbd3dc..a919109367 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -971,6 +971,7 @@ class DebuggingOptions { this.serveObservatory = false, this.enableDartProfiling = true, this.enableEmbedderApi = false, + this.usingCISystem = false, }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildInfo, { @@ -993,6 +994,7 @@ class DebuggingOptions { this.uninstallFirst = false, this.enableDartProfiling = true, this.enableEmbedderApi = false, + this.usingCISystem = false, }) : debuggingEnabled = false, useTestFonts = false, startPaused = false, @@ -1069,6 +1071,7 @@ class DebuggingOptions { required this.serveObservatory, required this.enableDartProfiling, required this.enableEmbedderApi, + required this.usingCISystem, }); final bool debuggingEnabled; @@ -1109,6 +1112,7 @@ class DebuggingOptions { final bool serveObservatory; final bool enableDartProfiling; final bool enableEmbedderApi; + final bool usingCISystem; /// Whether the tool should try to uninstall a previously installed version of the app. /// @@ -1243,6 +1247,7 @@ class DebuggingOptions { 'serveObservatory': serveObservatory, 'enableDartProfiling': enableDartProfiling, 'enableEmbedderApi': enableEmbedderApi, + 'usingCISystem': usingCISystem, }; static DebuggingOptions fromJson(Map json, BuildInfo buildInfo) => @@ -1294,6 +1299,7 @@ class DebuggingOptions { serveObservatory: (json['serveObservatory'] as bool?) ?? false, enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true, enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false, + usingCISystem: (json['usingCISystem'] as bool?) ?? false, ); } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 494cfcfebd..ceebcb6738 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -482,7 +482,10 @@ class IOSDevice extends Device { int installationResult = 1; if (debuggingOptions.debuggingEnabled) { _logger.printTrace('Debugging is enabled, connecting to vmService'); - final DeviceLogReader deviceLogReader = getLogReader(app: package); + final DeviceLogReader deviceLogReader = getLogReader( + app: package, + usingCISystem: debuggingOptions.usingCISystem, + ); // If the device supports syslog reading, prefer launching the app without // attaching the debugger to avoid the overhead of the unnecessary extra running process. @@ -629,12 +632,14 @@ class IOSDevice extends Device { DeviceLogReader getLogReader({ covariant IOSApp? app, bool includePastLogs = false, + bool usingCISystem = false, }) { assert(!includePastLogs, 'Past log reading not supported on iOS devices.'); return _logReaders.putIfAbsent(app, () => IOSDeviceLogReader.create( device: this, app: app, iMobileDevice: _iMobileDevice, + usingCISystem: usingCISystem, )); } @@ -749,17 +754,20 @@ class IOSDeviceLogReader extends DeviceLogReader { this._deviceId, this.name, String appName, + bool usingCISystem, ) : // Match for lines for the runner in syslog. // // iOS 9 format: Runner[297] : // iOS 10 format: Runner(Flutter)[297] : - _runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '); + _runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '), + _usingCISystem = usingCISystem; /// Create a new [IOSDeviceLogReader]. factory IOSDeviceLogReader.create({ required IOSDevice device, IOSApp? app, required IMobileDevice iMobileDevice, + bool usingCISystem = false, }) { final String appName = app?.name?.replaceAll('.app', '') ?? ''; return IOSDeviceLogReader._( @@ -768,6 +776,7 @@ class IOSDeviceLogReader extends DeviceLogReader { device.id, device.name, appName, + usingCISystem, ); } @@ -775,9 +784,17 @@ class IOSDeviceLogReader extends DeviceLogReader { factory IOSDeviceLogReader.test({ required IMobileDevice iMobileDevice, bool useSyslog = true, + bool usingCISystem = false, + int? majorSdkVersion, }) { + final int sdkVersion; + if (majorSdkVersion != null) { + sdkVersion = majorSdkVersion; + } else { + sdkVersion = useSyslog ? 12 : 13; + } return IOSDeviceLogReader._( - iMobileDevice, useSyslog ? 12 : 13, '1234', 'test', 'Runner'); + iMobileDevice, sdkVersion, '1234', 'test', 'Runner', usingCISystem); } @override @@ -785,6 +802,7 @@ class IOSDeviceLogReader extends DeviceLogReader { final int _majorSdkVersion; final String _deviceId; final IMobileDevice _iMobileDevice; + final bool _usingCISystem; // Matches a syslog line from the runner. RegExp _runnerLineRegex; @@ -810,12 +828,42 @@ class IOSDeviceLogReader extends DeviceLogReader { // Sometimes (race condition?) we try to send a log after the controller has // been closed. See https://github.com/flutter/flutter/issues/99021 for more // context. - void _addToLinesController(String message) { + void _addToLinesController(String message, IOSDeviceLogSource source) { if (!linesController.isClosed) { + if (_excludeLog(message, source)) { + return; + } linesController.add(message); } } + /// Used to track messages prefixed with "flutter:" when [useBothLogDeviceReaders] + /// is true. + final List _streamFlutterMessages = []; + + /// When using both `idevicesyslog` and `ios-deploy`, exclude logs with the + /// "flutter:" prefix if they have already been added to the stream. This is + /// to prevent duplicates from being printed. + /// + /// If a message does not have the prefix, exclude it if the message's + /// source is `idevicesyslog`. This is done because `ios-deploy` and + /// `idevicesyslog` often have different prefixes on non-flutter messages + /// and are often not critical for CI tests. + bool _excludeLog(String message, IOSDeviceLogSource source) { + if (!useBothLogDeviceReaders) { + return false; + } + if (message.startsWith('flutter:')) { + if (_streamFlutterMessages.contains(message)) { + return true; + } + _streamFlutterMessages.add(message); + } else if (source == IOSDeviceLogSource.idevicesyslog) { + return true; + } + return false; + } + final List> _loggingSubscriptions = >[]; @override @@ -835,6 +883,10 @@ class IOSDeviceLogReader extends DeviceLogReader { static const int minimumUniversalLoggingSdkVersion = 13; + /// Listen to Dart VM for logs on iOS 13 or greater. + /// + /// Only send logs to stream if [_iosDeployDebugger] is null or + /// the [_iosDeployDebugger] debugger is not attached. Future _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async { if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) { return; @@ -859,7 +911,7 @@ class IOSDeviceLogReader extends DeviceLogReader { } final String message = processVmServiceMessage(event); if (message.isNotEmpty) { - _addToLinesController(message); + _addToLinesController(message, IOSDeviceLogSource.unifiedLogging); } } @@ -871,8 +923,10 @@ class IOSDeviceLogReader extends DeviceLogReader { /// Log reader will listen to [debugger.logLines] and will detach debugger on dispose. IOSDeployDebugger? get debuggerStream => _iosDeployDebugger; + + /// Send messages from ios-deploy debugger stream to device log reader stream. set debuggerStream(IOSDeployDebugger? debugger) { - // Logging is gathered from syslog on iOS 13 and earlier. + // Logging is gathered from syslog on iOS earlier than 13. if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) { return; } @@ -882,7 +936,10 @@ class IOSDeviceLogReader extends DeviceLogReader { } // Add the debugger logs to the controller created on initialization. _loggingSubscriptions.add(debugger.logLines.listen( - (String line) => _addToLinesController(_debuggerLineHandler(line)), + (String line) => _addToLinesController( + _debuggerLineHandler(line), + IOSDeviceLogSource.iosDeploy, + ), onError: linesController.addError, onDone: linesController.close, cancelOnError: true, @@ -893,18 +950,38 @@ class IOSDeviceLogReader extends DeviceLogReader { // Strip off the logging metadata (leave the category), or just echo the line. String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line; + /// Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system + /// since sometimes `ios-deploy` does not return the device logs: + /// https://github.com/flutter/flutter/issues/121231 + @visibleForTesting + bool get useBothLogDeviceReaders { + return _usingCISystem && _majorSdkVersion >= 16; + } + + /// Start and listen to idevicesyslog to get device logs for iOS versions + /// prior to 13 or if [useBothLogDeviceReaders] is true. void _listenToSysLog() { - // syslog is not written on iOS 13+. - if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) { + // Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133). + // However, from at least iOS 16, it has began working again. It's unclear + // why it started working again so only use syslogs for iOS versions prior + // to 13 unless [useBothLogDeviceReaders] is true. + if (!useBothLogDeviceReaders && _majorSdkVersion >= minimumUniversalLoggingSdkVersion) { return; } _iMobileDevice.startLogger(_deviceId).then((Process process) { process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(_newSyslogLineHandler()); process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(_newSyslogLineHandler()); process.exitCode.whenComplete(() { - if (linesController.hasListener) { - linesController.close(); + if (!linesController.hasListener) { + return; } + // When using both log readers, do not close the stream on exit. + // This is to allow ios-deploy to be the source of authority to close + // the stream. + if (useBothLogDeviceReaders && debuggerStream != null) { + return; + } + linesController.close(); }); assert(idevicesyslogProcess == null); idevicesyslogProcess = process; @@ -926,7 +1003,7 @@ class IOSDeviceLogReader extends DeviceLogReader { return (String line) { if (printing) { if (!_anyLineRegex.hasMatch(line)) { - _addToLinesController(decodeSyslog(line)); + _addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog); return; } @@ -938,8 +1015,7 @@ class IOSDeviceLogReader extends DeviceLogReader { if (match != null) { final String logLine = line.substring(match.end); // Only display the log line after the initial device and executable information. - _addToLinesController(decodeSyslog(logLine)); - + _addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog); printing = true; } }; @@ -955,6 +1031,15 @@ class IOSDeviceLogReader extends DeviceLogReader { } } +enum IOSDeviceLogSource { + /// Gets logs from ios-deploy debugger. + iosDeploy, + /// Gets logs from idevicesyslog. + idevicesyslog, + /// Gets logs from the Dart VM Service. + unifiedLogging, +} + /// A [DevicePortForwarder] specialized for iOS usage with iproxy. class IOSDevicePortForwarder extends DevicePortForwarder { diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 2cbcfd8660..1d55e0d567 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -36,6 +36,8 @@ import 'devfs.dart'; import 'device.dart'; import 'features.dart'; import 'globals.dart' as globals; +import 'ios/application_package.dart'; +import 'ios/devices.dart'; import 'project.dart'; import 'resident_devtools_handler.dart'; import 'run_cold.dart'; @@ -391,11 +393,19 @@ class FlutterDevice { return devFS!.create(); } - Future startEchoingDeviceLog() async { + Future startEchoingDeviceLog(DebuggingOptions debuggingOptions) async { if (_loggingSubscription != null) { return; } - final Stream logStream = (await device!.getLogReader(app: package)).logLines; + final Stream logStream; + if (device is IOSDevice) { + logStream = (device! as IOSDevice).getLogReader( + app: package as IOSApp?, + usingCISystem: debuggingOptions.usingCISystem, + ).logLines; + } else { + logStream = (await device!.getLogReader(app: package)).logLines; + } _loggingSubscription = logStream.listen((String line) { if (!line.contains(globals.kVMServiceMessageRegExp)) { globals.printStatus(line, wrap: false); @@ -451,7 +461,7 @@ class FlutterDevice { 'multidex': hotRunner.multidexEnabled, }; - await startEchoingDeviceLog(); + await startEchoingDeviceLog(hotRunner.debuggingOptions); // Start the application. final Future futureResult = device!.startApp( @@ -519,7 +529,7 @@ class FlutterDevice { platformArgs['trace-startup'] = coldRunner.traceStartup; platformArgs['multidex'] = coldRunner.multidexEnabled; - await startEchoingDeviceLog(); + await startEchoingDeviceLog(coldRunner.debuggingOptions); final LaunchResult result = await device!.startApp( applicationPackage, diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 94586ff0ff..a099d81379 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -304,7 +304,10 @@ abstract class FlutterCommand extends Command { /// Path to the Dart's package config file. /// /// This can be overridden by some of its subclasses. - String? get packagesPath => globalResults?['packages'] as String?; + String? get packagesPath => stringArg(FlutterGlobalOptions.kPackagesOption, global: true); + + /// Whether flutter is being run from our CI. + bool get usingCISystem => boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true); /// The value of the `--filesystem-scheme` argument. /// @@ -1634,17 +1637,30 @@ Run 'flutter -h' (or 'flutter -h') for available flutter commands and /// /// If no flag named [name] was added to the [ArgParser], an [ArgumentError] /// will be thrown. - bool boolArg(String name) => argResults![name] as bool; + bool boolArg(String name, {bool global = false}) { + if (global) { + return globalResults![name] as bool; + } + return argResults![name] as bool; + } /// Gets the parsed command-line option named [name] as a `String`. /// /// If no option named [name] was added to the [ArgParser], an [ArgumentError] /// will be thrown. - String? stringArg(String name) => argResults![name] as String?; + String? stringArg(String name, {bool global = false}) { + if (global) { + return globalResults![name] as String?; + } + return argResults![name] as String?; + } /// Gets the parsed command-line option named [name] as `List`. - List stringsArg(String name) { - return argResults![name]! as List? ?? []; + List stringsArg(String name, {bool global = false}) { + if (global) { + return globalResults![name] as List; + } + return argResults![name] as List; } } diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index ea84a7db40..9f10cd0e9d 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -20,6 +20,30 @@ import '../globals.dart' as globals; import '../tester/flutter_tester.dart'; import '../web/web_device.dart'; +/// Common flutter command line options. +abstract final class FlutterGlobalOptions { + static const String kColorFlag = 'color'; + static const String kContinuousIntegrationFlag = 'ci'; + static const String kDeviceIdOption = 'device-id'; + static const String kDisableTelemetryFlag = 'disable-telemetry'; + static const String kEnableTelemetryFlag = 'enable-telemetry'; + static const String kLocalEngineOption = 'local-engine'; + static const String kLocalEngineSrcPathOption = 'local-engine-src-path'; + static const String kLocalWebSDKOption = 'local-web-sdk'; + static const String kMachineFlag = 'machine'; + static const String kPackagesOption = 'packages'; + static const String kPrefixedErrorsFlag = 'prefixed-errors'; + static const String kQuietFlag = 'quiet'; + static const String kShowTestDeviceFlag = 'show-test-device'; + static const String kShowWebServerDeviceFlag = 'show-web-server-device'; + static const String kSuppressAnalyticsFlag = 'suppress-analytics'; + static const String kVerboseFlag = 'verbose'; + static const String kVersionCheckFlag = 'version-check'; + static const String kVersionFlag = 'version'; + static const String kWrapColumnOption = 'wrap-column'; + static const String kWrapFlag = 'wrap'; +} + class FlutterCommandRunner extends CommandRunner { FlutterCommandRunner({ bool verboseHelp = false }) : super( 'flutter', @@ -33,80 +57,80 @@ class FlutterCommandRunner extends CommandRunner { ' flutter run [options]\n' ' Run your Flutter application on an attached device or in an emulator.', ) { - argParser.addFlag('verbose', + argParser.addFlag(FlutterGlobalOptions.kVerboseFlag, abbr: 'v', negatable: false, help: 'Noisy logging, including all shell commands executed.\n' 'If used with "--help", shows hidden options. ' 'If used with "flutter doctor", shows additional diagnostic information. ' '(Use "-vv" to force verbose logging in those cases.)'); - argParser.addFlag('prefixed-errors', + argParser.addFlag(FlutterGlobalOptions.kPrefixedErrorsFlag, negatable: false, help: 'Causes lines sent to stderr to be prefixed with "ERROR:".', hide: !verboseHelp); - argParser.addFlag('quiet', + argParser.addFlag(FlutterGlobalOptions.kQuietFlag, negatable: false, hide: !verboseHelp, help: 'Reduce the amount of output from some commands.'); - argParser.addFlag('wrap', + argParser.addFlag(FlutterGlobalOptions.kWrapFlag, hide: !verboseHelp, help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.', defaultsTo: true); - argParser.addOption('wrap-column', + argParser.addOption(FlutterGlobalOptions.kWrapColumnOption, hide: !verboseHelp, help: 'Sets the output wrap column. If not set, uses the width of the terminal. No ' 'wrapping occurs if not writing to a terminal. Use "--no-wrap" to turn off wrapping ' 'when connected to a terminal.'); - argParser.addOption('device-id', + argParser.addOption(FlutterGlobalOptions.kDeviceIdOption, abbr: 'd', help: 'Target device id or name (prefixes allowed).'); - argParser.addFlag('version', + argParser.addFlag(FlutterGlobalOptions.kVersionFlag, negatable: false, help: 'Reports the version of this tool.'); - argParser.addFlag('machine', + argParser.addFlag(FlutterGlobalOptions.kMachineFlag, negatable: false, hide: !verboseHelp, help: 'When used with the "--version" flag, outputs the information using JSON.'); - argParser.addFlag('color', + argParser.addFlag(FlutterGlobalOptions.kColorFlag, hide: !verboseHelp, help: 'Whether to use terminal colors (requires support for ANSI escape sequences).', defaultsTo: true); - argParser.addFlag('version-check', + argParser.addFlag(FlutterGlobalOptions.kVersionCheckFlag, defaultsTo: true, hide: !verboseHelp, help: 'Allow Flutter to check for updates when this command runs.'); - argParser.addFlag('suppress-analytics', + argParser.addFlag(FlutterGlobalOptions.kSuppressAnalyticsFlag, negatable: false, help: 'Suppress analytics reporting for the current CLI invocation.'); - argParser.addFlag('disable-telemetry', + argParser.addFlag(FlutterGlobalOptions.kDisableTelemetryFlag, negatable: false, help: 'Disable telemetry reporting each time a flutter or dart ' 'command runs, until it is re-enabled.'); - argParser.addFlag('enable-telemetry', + argParser.addFlag(FlutterGlobalOptions.kEnableTelemetryFlag, negatable: false, help: 'Enable telemetry reporting each time a flutter or dart ' 'command runs.'); - argParser.addOption('packages', + argParser.addOption(FlutterGlobalOptions.kPackagesOption, hide: !verboseHelp, help: 'Path to your "package_config.json" file.'); if (verboseHelp) { argParser.addSeparator('Local build selection options (not normally required):'); } - argParser.addOption('local-engine-src-path', + argParser.addOption(FlutterGlobalOptions.kLocalEngineSrcPathOption, hide: !verboseHelp, help: 'Path to your engine src directory, if you are building Flutter locally.\n' 'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to ' 'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, ' 'if any.'); - argParser.addOption('local-engine', + argParser.addOption(FlutterGlobalOptions.kLocalEngineOption, hide: !verboseHelp, help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n' 'Use this to select a specific version of the engine if you have built multiple engine targets.\n' 'This path is relative to "--local-engine-src-path" (see above).'); - argParser.addOption('local-web-sdk', + argParser.addOption(FlutterGlobalOptions.kLocalWebSDKOption, hide: !verboseHelp, help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n' 'Use this to select a specific version of the web sdk if you have built multiple engine targets.\n' @@ -115,16 +139,22 @@ class FlutterCommandRunner extends CommandRunner { if (verboseHelp) { argParser.addSeparator('Options for testing the "flutter" tool itself:'); } - argParser.addFlag('show-test-device', + argParser.addFlag(FlutterGlobalOptions.kShowTestDeviceFlag, negatable: false, hide: !verboseHelp, help: 'List the special "flutter-tester" device in device listings. ' 'This headless device is used to test Flutter tooling.'); - argParser.addFlag('show-web-server-device', + argParser.addFlag(FlutterGlobalOptions.kShowWebServerDeviceFlag, negatable: false, hide: !verboseHelp, help: 'List the special "web-server" device in device listings.', ); + argParser.addFlag( + FlutterGlobalOptions.kContinuousIntegrationFlag, + negatable: false, + help: 'Enable a set of CI-specific test debug settings.', + hide: !verboseHelp, + ); } @override @@ -198,8 +228,8 @@ class FlutterCommandRunner extends CommandRunner { // If the flag for enabling or disabling telemetry is passed in, // we will return out - if (topLevelResults.wasParsed('disable-telemetry') || - topLevelResults.wasParsed('enable-telemetry')) { + if (topLevelResults.wasParsed(FlutterGlobalOptions.kDisableTelemetryFlag) || + topLevelResults.wasParsed(FlutterGlobalOptions.kEnableTelemetryFlag)) { return; } @@ -207,43 +237,43 @@ class FlutterCommandRunner extends CommandRunner { // wrapping will occur at this width explicitly, and won't adapt if the // terminal size changes during a run. int? wrapColumn; - if (topLevelResults.wasParsed('wrap-column')) { + if (topLevelResults.wasParsed(FlutterGlobalOptions.kWrapColumnOption)) { try { - wrapColumn = int.parse(topLevelResults['wrap-column'] as String); + wrapColumn = int.parse(topLevelResults[FlutterGlobalOptions.kWrapColumnOption] as String); if (wrapColumn < 0) { - throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults['wrap-column'])); + throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults[FlutterGlobalOptions.kWrapColumnOption])); } } on FormatException { - throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults['wrap-column'])); + throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults[FlutterGlobalOptions.kWrapColumnOption])); } } // If we're not writing to a terminal with a defined width, then don't wrap // anything, unless the user explicitly said to. - final bool useWrapping = topLevelResults.wasParsed('wrap') - ? topLevelResults['wrap'] as bool - : globals.stdio.terminalColumns != null && topLevelResults['wrap'] as bool; + final bool useWrapping = topLevelResults.wasParsed(FlutterGlobalOptions.kWrapFlag) + ? topLevelResults[FlutterGlobalOptions.kWrapFlag] as bool + : globals.stdio.terminalColumns != null && topLevelResults[FlutterGlobalOptions.kWrapFlag] as bool; contextOverrides[OutputPreferences] = OutputPreferences( wrapText: useWrapping, - showColor: topLevelResults['color'] as bool?, + showColor: topLevelResults[FlutterGlobalOptions.kColorFlag] as bool?, wrapColumn: wrapColumn, ); - if (((topLevelResults['show-test-device'] as bool?) ?? false) - || topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) { + if (((topLevelResults[FlutterGlobalOptions.kShowTestDeviceFlag] as bool?) ?? false) + || topLevelResults[FlutterGlobalOptions.kDeviceIdOption] == FlutterTesterDevices.kTesterDeviceId) { FlutterTesterDevices.showFlutterTesterDevice = true; } - if (((topLevelResults['show-web-server-device'] as bool?) ?? false) - || topLevelResults['device-id'] == WebServerDevice.kWebServerDeviceId) { + if (((topLevelResults[FlutterGlobalOptions.kShowWebServerDeviceFlag] as bool?) ?? false) + || topLevelResults[FlutterGlobalOptions.kDeviceIdOption] == WebServerDevice.kWebServerDeviceId) { WebServerDevice.showWebServerDevice = true; } // Set up the tooling configuration. final EngineBuildPaths? engineBuildPaths = await globals.localEngineLocator?.findEnginePath( - engineSourcePath: topLevelResults['local-engine-src-path'] as String?, - localEngine: topLevelResults['local-engine'] as String?, - localWebSdk: topLevelResults['local-web-sdk'] as String?, - packagePath: topLevelResults['packages'] as String?, + engineSourcePath: topLevelResults[FlutterGlobalOptions.kLocalEngineSrcPathOption] as String?, + localEngine: topLevelResults[FlutterGlobalOptions.kLocalEngineOption] as String?, + localWebSdk: topLevelResults[FlutterGlobalOptions.kLocalWebSDKOption] as String?, + packagePath: topLevelResults[FlutterGlobalOptions.kPackagesOption] as String?, ); if (engineBuildPaths != null) { contextOverrides.addAll({ @@ -256,24 +286,24 @@ class FlutterCommandRunner extends CommandRunner { return MapEntry(type, () => value); }), body: () async { - globals.logger.quiet = (topLevelResults['quiet'] as bool?) ?? false; + globals.logger.quiet = (topLevelResults[FlutterGlobalOptions.kQuietFlag] as bool?) ?? false; if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') { await globals.cache.lock(); } - if ((topLevelResults['suppress-analytics'] as bool?) ?? false) { + if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) { globals.flutterUsage.suppressAnalytics = true; } globals.flutterVersion.ensureVersionFile(); - final bool machineFlag = topLevelResults['machine'] as bool? ?? false; + final bool machineFlag = topLevelResults[FlutterGlobalOptions.kMachineFlag] as bool? ?? false; final bool ci = await globals.botDetector.isRunningOnBot; final bool redirectedCompletion = !globals.stdio.hasTerminal && (topLevelResults.command?.name ?? '').endsWith('-completion'); final bool isMachine = machineFlag || ci || redirectedCompletion; - final bool versionCheckFlag = topLevelResults['version-check'] as bool? ?? false; - final bool explicitVersionCheckPassed = topLevelResults.wasParsed('version-check') && versionCheckFlag; + final bool versionCheckFlag = topLevelResults[FlutterGlobalOptions.kVersionCheckFlag] as bool? ?? false; + final bool explicitVersionCheckPassed = topLevelResults.wasParsed(FlutterGlobalOptions.kVersionCheckFlag) && versionCheckFlag; if (topLevelResults.command?.name != 'upgrade' && (explicitVersionCheckPassed || (versionCheckFlag && !isMachine))) { @@ -281,13 +311,13 @@ class FlutterCommandRunner extends CommandRunner { } // See if the user specified a specific device. - final String? specifiedDeviceId = topLevelResults['device-id'] as String?; + final String? specifiedDeviceId = topLevelResults[FlutterGlobalOptions.kDeviceIdOption] as String?; if (specifiedDeviceId != null) { globals.deviceManager?.specifiedDeviceId = specifiedDeviceId; } - if ((topLevelResults['version'] as bool?) ?? false) { - globals.flutterUsage.sendCommand('version'); + if ((topLevelResults[FlutterGlobalOptions.kVersionFlag] as bool?) ?? false) { + globals.flutterUsage.sendCommand(FlutterGlobalOptions.kVersionFlag); globals.flutterVersion.fetchTagsAndUpdate(); String status; if (machineFlag) { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index 9a3f6b1f2d..c0c825abed 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -1381,6 +1381,7 @@ class FakeIOSDevice extends Fake implements IOSDevice { DeviceLogReader getLogReader({ IOSApp? app, bool includePastLogs = false, + bool usingCISystem = false, }) { if (onGetLogReader == null) { throw UnimplementedError( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index 82d71c8061..f3df962855 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -425,6 +425,7 @@ void main() { '--enable-software-rendering', '--skia-deterministic-rendering', '--enable-embedder-api', + '--ci', ]), throwsToolExit()); final DebuggingOptions options = await command.createDebuggingOptions(false); @@ -440,6 +441,7 @@ void main() { expect(options.traceSystrace, true); expect(options.enableSoftwareRendering, true); expect(options.skiaDeterministicRendering, true); + expect(options.usingCISystem, true); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 2ffedf4927..5ccba77dcc 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -1096,6 +1096,7 @@ void main() { '--enable-software-rendering', '--skia-deterministic-rendering', '--enable-embedder-api', + '--ci', ]), throwsToolExit()); final DebuggingOptions options = await command.createDebuggingOptions(false); @@ -1114,6 +1115,7 @@ void main() { expect(options.impellerForceGL, true); expect(options.enableSoftwareRendering, true); expect(options.skiaDeterministicRendering, true); + expect(options.usingCISystem, true); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), diff --git a/packages/flutter_tools/test/general.shard/args_test.dart b/packages/flutter_tools/test/general.shard/args_test.dart index a6f207d784..21687133e5 100644 --- a/packages/flutter_tools/test/general.shard/args_test.dart +++ b/packages/flutter_tools/test/general.shard/args_test.dart @@ -30,11 +30,53 @@ void main() { } })); + testUsingContext('Global arg results are available in FlutterCommands', () async { + final DummyFlutterCommand command = DummyFlutterCommand( + commandFunction: () async { + return const FlutterCommandResult(ExitStatus.success); + }, + ); + + final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); + + runner.addCommand(command); + await runner.run(['dummy', '--${FlutterGlobalOptions.kContinuousIntegrationFlag}']); + + expect(command.globalResults, isNotNull); + expect(command.boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true), true); + }); + + testUsingContext('Global arg results are available in FlutterCommands sub commands', () async { + final DummyFlutterCommand command = DummyFlutterCommand( + commandFunction: () async { + return const FlutterCommandResult(ExitStatus.success); + }, + ); + + final DummyFlutterCommand subcommand = DummyFlutterCommand( + name: 'sub', + commandFunction: () async { + return const FlutterCommandResult(ExitStatus.success); + }, + ); + + command.addSubcommand(subcommand); + + final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); + + runner.addCommand(command); + runner.addCommand(subcommand); + await runner.run(['dummy', 'sub', '--${FlutterGlobalOptions.kContinuousIntegrationFlag}']); + + expect(subcommand.globalResults, isNotNull); + expect(subcommand.boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true), true); + }); + testUsingContext('bool? safe argResults', () async { final DummyFlutterCommand command = DummyFlutterCommand( - commandFunction: () async { - return const FlutterCommandResult(ExitStatus.success); - } + commandFunction: () async { + return const FlutterCommandResult(ExitStatus.success); + }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); command.argParser.addFlag('key'); @@ -58,9 +100,9 @@ void main() { testUsingContext('String? safe argResults', () async { final DummyFlutterCommand command = DummyFlutterCommand( - commandFunction: () async { - return const FlutterCommandResult(ExitStatus.success); - } + commandFunction: () async { + return const FlutterCommandResult(ExitStatus.success); + }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); command.argParser.addOption('key'); @@ -80,9 +122,9 @@ void main() { testUsingContext('List safe argResults', () async { final DummyFlutterCommand command = DummyFlutterCommand( - commandFunction: () async { - return const FlutterCommandResult(ExitStatus.success); - } + commandFunction: () async { + return const FlutterCommandResult(ExitStatus.success); + }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); command.argParser.addMultiOption( diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart index 07ea502346..9f65a67072 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart @@ -348,6 +348,165 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt ); }); }); + + group('both syslog and debugger stream', () { + + testWithoutContext('useBothLogDeviceReaders is true when CI option is true and sdk is at least 16', () { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + + expect(logReader.useBothLogDeviceReaders, isTrue); + }); + + testWithoutContext('useBothLogDeviceReaders is false when sdk is less than 16', () { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 15, + ); + + expect(logReader.useBothLogDeviceReaders, isFalse); + }); + + testWithoutContext('useBothLogDeviceReaders is false when CI option is false', () { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 16, + ); + + expect(logReader.useBothLogDeviceReaders, isFalse); + }); + + testWithoutContext('syslog only sends flutter messages to stream when useBothLogDeviceReaders is true', () async { + processManager.addCommand( + FakeCommand( + command: [ + ideviceSyslogPath, '-u', '1234', + ], + stdout: ''' +Runner(Flutter)[297] : A is for ari +Runner(Flutter)[297] : I is for ichigo +May 30 13:56:28 Runner(Flutter)[2037] : flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/ +May 30 13:56:28 Runner(Flutter)[2037] : flutter: This is a test +May 30 13:56:28 Runner(Flutter)[2037] : [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend. +''' + ), + ); + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + final List lines = await logReader.logLines.toList(); + + expect(logReader.useBothLogDeviceReaders, isTrue); + expect(processManager, hasNoRemainingExpectations); + expect(lines, [ + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + 'flutter: This is a test' + ]); + }); + + testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger', () async { + processManager.addCommand( + FakeCommand( + command: [ + ideviceSyslogPath, '-u', '1234', + ], + stdout: ''' +May 30 13:56:28 Runner(Flutter)[2037] : flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/ +May 30 13:56:28 Runner(Flutter)[2037] : flutter: Check for duplicate +May 30 13:56:28 Runner(Flutter)[2037] : [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend. +''' + ), + ); + + final Stream debuggingLogs = Stream.fromIterable([ + '2023-06-01 12:49:01.445093-0500 Runner[2225:533240] flutter: Check for duplicate', + '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', + ]); + + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); + iosDeployDebugger.logLines = debuggingLogs; + logReader.debuggerStream = iosDeployDebugger; + final Future> logLines = logReader.logLines.toList(); + final List lines = await logLines; + + expect(logReader.useBothLogDeviceReaders, isTrue); + expect(processManager, hasNoRemainingExpectations); + expect(lines.length, 3); + expect(lines, containsAll([ + '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + 'flutter: Check for duplicate', + ])); + + }); + + testWithoutContext('IOSDeviceLogReader only uses ios-deploy debugger when useBothLogDeviceReaders is false', () async { + final Stream debuggingLogs = Stream.fromIterable([ + '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', + '', + ]); + + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 16, + ); + final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); + iosDeployDebugger.logLines = debuggingLogs; + logReader.debuggerStream = iosDeployDebugger; + final Future> logLines = logReader.logLines.toList(); + final List lines = await logLines; + + expect(logReader.useBothLogDeviceReaders, isFalse); + expect(processManager, hasNoRemainingExpectations); + expect( + lines.contains( + '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', + ), + isTrue, + ); + }); + }); } class FakeIOSDeployDebugger extends Fake implements IOSDeployDebugger {