From 183fe75d58131c10a4abb920c4d540c877c9e897 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 26 Oct 2020 10:11:30 -0700 Subject: [PATCH] [flutter_tools] reland: drive service (#68887) Overhaul of flutter drive in order to deliver a better experience, namely: * flutter run and flutter drive now share more flags, so code paths that were previously only testable on run are now testable on drive. * Removes web-initialize-platform as this is no longer used * flutter drive correctly sets up a logger that shows native exceptions, by connecting to the vm service. * VM service connection now provides access to memory info without launching devtools (only for debug/profile mode) Web changes * Passes on the one test in the repo, otherwise the webdriver code has been isolated as much as possible Additional NNBD related bug fixes: No longer passes --enable-experiment to the test script. (FYI @blasten ). earlier we might have assumed that the flutter gallery benchmarks would be migrated along side the app and flutter driver, but only the app under test needs to be migrated. The test scripts should never be run with the experiment. --- packages/flutter_tools/lib/executable.dart | 5 +- .../lib/src/android/android_device.dart | 12 +- .../lib/src/build_system/targets/web.dart | 12 +- .../lib/src/commands/attach.dart | 1 - .../lib/src/commands/build_web.dart | 7 - .../lib/src/commands/daemon.dart | 1 - .../flutter_tools/lib/src/commands/drive.dart | 591 ++---------- .../flutter_tools/lib/src/commands/run.dart | 239 +++-- packages/flutter_tools/lib/src/device.dart | 4 - .../lib/src/drive/drive_service.dart | 245 +++++ .../lib/src/drive/web_driver_service.dart | 273 ++++++ .../lib/src/isolated/resident_web_runner.dart | 2 - .../lib/src/resident_runner.dart | 1 - packages/flutter_tools/lib/src/vmservice.dart | 7 +- .../flutter_tools/lib/src/web/compile.dart | 2 - .../hermetic/build_web_test.dart | 1 - .../commands.shard/hermetic/drive_test.dart | 882 ------------------ .../build_system/targets/web_test.dart | 39 - .../general.shard/desktop_device_test.dart | 18 +- .../drive/drive_service_test.dart | 380 ++++++++ .../drive/web_driver_service_test.dart | 160 ++++ .../general.shard/resident_runner_test.dart | 4 - .../integration.shard/analyze_size_test.dart | 3 +- 23 files changed, 1277 insertions(+), 1612 deletions(-) create mode 100644 packages/flutter_tools/lib/src/drive/drive_service.dart create mode 100644 packages/flutter_tools/lib/src/drive/web_driver_service.dart delete mode 100644 packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart create mode 100644 packages/flutter_tools/test/general.shard/drive/drive_service_test.dart create mode 100644 packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index e55a7af752..aeed10fd6a 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -95,7 +95,10 @@ Future main(List args) async { DevicesCommand(), DoctorCommand(verbose: verbose), DowngradeCommand(), - DriveCommand(verboseHelp: verboseHelp), + DriveCommand(verboseHelp: verboseHelp, + fileSystem: globals.fs, + logger: globals.logger, + ), EmulatorsCommand(), FormatCommand(), GenerateCommand(), diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 0f7ee7036e..a47ef50dc9 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -590,13 +590,17 @@ class AndroidDevice extends Device { } final bool traceStartup = platformArgs['trace-startup'] as bool ?? false; - _logger.printTrace('$this startApp'); - ProtocolDiscovery observatoryDiscovery; if (debuggingOptions.debuggingEnabled) { observatoryDiscovery = ProtocolDiscovery.observatory( - await getLogReader(), + // Avoid using getLogReader, which returns a singleton instance, because the + // observatory discovery will dipose at the end. creating a new logger here allows + // logs to be surfaced normally during `flutter drive`. + await AdbLogReader.createLogReader( + this, + _processManager, + ), portForwarder: portForwarder, hostPort: debuggingOptions.hostVmServicePort, devicePort: debuggingOptions.deviceVmServicePort, @@ -669,8 +673,6 @@ class AndroidDevice extends Device { // Wait for the service protocol port here. This will complete once the // device has printed "Observatory is listening on...". _logger.printTrace('Waiting for observatory port to be available...'); - - // TODO(danrubel): Waiting for observatory services can be made common across all devices. try { Uri observatoryUri; if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) { diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 9b1f86b6f2..47c13d6593 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -22,9 +22,6 @@ import 'assets.dart'; import 'common.dart'; import 'localizations.dart'; -/// Whether web builds should call the platform initialization logic. -const String kInitializePlatform = 'InitializePlatform'; - /// Whether the application has web plugins. const String kHasWebPlugins = 'HasWebPlugins'; @@ -89,7 +86,6 @@ class WebEntrypointTarget extends Target { @override Future build(Environment environment) async { final String targetFile = environment.defines[kTargetFile]; - final bool shouldInitializePlatform = environment.defines[kInitializePlatform] == 'true'; final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true'; final Uri importUri = environment.fileSystem.file(targetFile).absolute.uri; // TODO(jonahwilliams): support configuration of this file. @@ -137,9 +133,7 @@ import '$mainImport' as entrypoint; Future main() async { registerPlugins(webPluginRegistry); - if ($shouldInitializePlatform) { - await ui.webOnlyInitializePlatform(); - } + await ui.webOnlyInitializePlatform(); entrypoint.main(); } '''; @@ -152,9 +146,7 @@ import 'dart:ui' as ui; import '$mainImport' as entrypoint; Future main() async { - if ($shouldInitializePlatform) { - await ui.webOnlyInitializePlatform(); - } + await ui.webOnlyInitializePlatform(); entrypoint.main(); } '''; diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 853f42131c..db5f592738 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -384,7 +384,6 @@ known, it can be explicitly provided to attach via the command-line, e.g. final FlutterDevice flutterDevice = await FlutterDevice.create( device, - flutterProject: flutterProject, fileSystemRoots: stringsArg('filesystem-root'), fileSystemScheme: stringArg('filesystem-scheme'), target: stringArg('target'), diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart index 0e4e59c000..d5b1f74cab 100644 --- a/packages/flutter_tools/lib/src/commands/build_web.dart +++ b/packages/flutter_tools/lib/src/commands/build_web.dart @@ -25,12 +25,6 @@ class BuildWebCommand extends BuildSubCommand { usesDartDefineOption(); addEnableExperimentation(hide: !verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); - argParser.addFlag('web-initialize-platform', - defaultsTo: true, - negatable: true, - hide: true, - help: 'Whether to automatically invoke webOnlyInitializePlatform.', - ); argParser.addFlag('csp', defaultsTo: false, negatable: false, @@ -92,7 +86,6 @@ class BuildWebCommand extends BuildSubCommand { flutterProject, target, buildInfo, - boolArg('web-initialize-platform'), boolArg('csp'), stringArg('pwa-strategy'), boolArg('source-maps') diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index bf069016ad..a7bdda01a9 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -466,7 +466,6 @@ class AppDomain extends Domain { final FlutterDevice flutterDevice = await FlutterDevice.create( device, - flutterProject: flutterProject, target: target, buildInfo: options.buildInfo, platform: globals.platform, diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index d966bacd35..f436fffd82 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -3,29 +3,21 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:math' as math; -import 'package:dds/dds.dart' as dds; -import 'package:vm_service/vm_service_io.dart' as vm_service; -import 'package:vm_service/vm_service.dart' as vm_service; import 'package:meta/meta.dart'; -import 'package:webdriver/async_io.dart' as async_io; import '../android/android_device.dart'; import '../application_package.dart'; import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; +import '../base/logger.dart'; import '../build_info.dart'; -import '../convert.dart'; -import '../dart/package_map.dart'; import '../device.dart'; +import '../drive/drive_service.dart'; import '../globals.dart' as globals; -import '../project.dart'; -import '../resident_runner.dart'; import '../runner/flutter_command.dart' show FlutterCommandResult, FlutterOptions; -import '../vmservice.dart'; -import '../web/web_runner.dart'; +import '../web/web_device.dart'; import 'run.dart'; /// Runs integration (a.k.a. end-to-end) tests. @@ -51,7 +43,12 @@ import 'run.dart'; class DriveCommand extends RunCommandBase { DriveCommand({ bool verboseHelp = false, - }) { + @visibleForTesting FlutterDriverFactory flutterDriverFactory, + @required FileSystem fileSystem, + @required Logger logger, + }) : _flutterDriverFactory = flutterDriverFactory, + _fileSystem = fileSystem, + _logger = logger { requiresPubspecYaml(); addEnableExperimentation(hide: !verboseHelp); @@ -109,7 +106,7 @@ class DriveCommand extends RunCommandBase { 'firefox', 'ios-safari', 'safari', - ] + ], ) ..addOption('browser-dimension', defaultsTo: '1600,1024', @@ -127,10 +124,15 @@ class DriveCommand extends RunCommandBase { ..addOption('write-sksl-on-exit', help: 'Attempts to write an SkSL file when the drive process is finished ' - 'to the provided file, overwriting it if necessary.', - ); + 'to the provided file, overwriting it if necessary.') + ..addMultiOption('test-arguments', help: 'Additional arguments to pass to the ' + 'Dart VM running The test script.'); } + FlutterDriverFactory _flutterDriverFactory; + final FileSystem _fileSystem; + final Logger _logger; + @override final String name = 'drive'; @@ -140,20 +142,15 @@ class DriveCommand extends RunCommandBase { @override final List aliases = ['driver']; - Device _device; - Device get device => _device; - - bool get verboseSystemLogs => boolArg('verbose-system-logs'); String get userIdentifier => stringArg(FlutterOptions.kDeviceUser); - /// Subscription to log messages printed on the device or simulator. - // ignore: cancel_subscriptions - StreamSubscription _deviceLogSubscription; + @override + bool get startPausedDefault => true; @override Future validateCommand() async { if (userIdentifier != null) { - final Device device = await findTargetDevice(timeout: deviceDiscoveryTimeout); + final Device device = await findTargetDevice(); if (device is! AndroidDevice) { throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android'); } @@ -167,229 +164,70 @@ class DriveCommand extends RunCommandBase { if (testFile == null) { throwToolExit(null); } - - _device = await findTargetDevice(timeout: deviceDiscoveryTimeout); + if (await _fileSystem.type(testFile) != FileSystemEntityType.file) { + throwToolExit('Test file not found: $testFile'); + } + final Device device = await findTargetDevice(includeUnsupportedDevices: stringArg('use-application-binary') == null); if (device == null) { throwToolExit(null); } - if (await globals.fs.type(testFile) != FileSystemEntityType.file) { - throwToolExit('Test file not found: $testFile'); - } - - String observatoryUri; - ResidentRunner residentRunner; + final bool web = device is WebServerDevice || device is ChromiumDevice; + _flutterDriverFactory ??= FlutterDriverFactory( + applicationPackageFactory: ApplicationPackageFactory.instance, + logger: _logger, + processUtils: globals.processUtils, + dartSdkPath: globals.artifacts.getArtifactPath(Artifact.engineDartBinary), + ); + final DriverService driverService = _flutterDriverFactory.createDriverService(web); final BuildInfo buildInfo = getBuildInfo(); - final bool isWebPlatform = await device.targetPlatform == TargetPlatform.web_javascript; + final DebuggingOptions debuggingOptions = createDebuggingOptions(); final File applicationBinary = stringArg('use-application-binary') == null ? null - : globals.fs.file(stringArg('use-application-binary')); - final ApplicationPackage package = await applicationPackages.getPackageForPlatform( - await device.targetPlatform, - buildInfo: buildInfo, + : _fileSystem.file(stringArg('use-application-binary')); + + await driverService.start( + buildInfo, + device, + debuggingOptions, + ipv6, applicationBinary: applicationBinary, + route: route, + userIdentifier: userIdentifier, + mainPath: targetFile, + platformArgs: { + if (traceStartup) + 'trace-startup': traceStartup, + if (web) + '--no-launch-chrome': true, + } ); - if (argResults['use-existing-app'] == null) { - globals.printStatus('Starting application: $targetFile'); - if (buildInfo.isRelease && !isWebPlatform) { - // This is because we need VM service to be able to drive the app. - // For Flutter Web, testing in release mode is allowed. - throwToolExit( - 'Flutter Driver (non-web) does not support running in release mode.\n' - '\n' - 'Use --profile mode for testing application performance.\n' - 'Use --debug (default) mode for testing correctness (with assertions).' - ); - } + final int testResult = await driverService.startTest( + testFile, + stringsArg('test-arguments'), + {}, + chromeBinary: stringArg('chrome-binary'), + headless: boolArg('headless'), + browserDimension: stringArg('browser-dimension').split(','), + browserName: stringArg('browser-name'), + driverPort: stringArg('driver-port') != null + ? int.tryParse(stringArg('driver-port')) + : null, + androidEmulator: boolArg('android-emulator'), + ); - Uri webUri; - - if (isWebPlatform) { - // Start Flutter web application for current test - final FlutterProject flutterProject = FlutterProject.current(); - final FlutterDevice flutterDevice = await FlutterDevice.create( - device, - flutterProject: flutterProject, - target: targetFile, - buildInfo: buildInfo, - platform: globals.platform, - ); - residentRunner = webRunnerFactory.createWebRunner( - flutterDevice, - target: targetFile, - flutterProject: flutterProject, - ipv6: ipv6, - debuggingOptions: getBuildInfo().isRelease ? - DebuggingOptions.disabled( - getBuildInfo(), - port: stringArg('web-port') - ) - : DebuggingOptions.enabled( - getBuildInfo(), - port: stringArg('web-port'), - disablePortPublication: disablePortPublication, - ), - stayResident: false, - urlTunneller: null, - ); - final Completer appStartedCompleter = Completer.sync(); - final int result = await residentRunner.run( - appStartedCompleter: appStartedCompleter, - route: route, - ); - if (result != 0) { - throwToolExit(null, exitCode: result); - } - // Wait until the app is started. - await appStartedCompleter.future; - webUri = residentRunner.uri; - } - - // Attempt to launch the application up to 3 times, to validate whether it - // is possible to reduce flakiness by hardnening the launch code. - int attempt = 0; - LaunchResult result; - while (attempt < 3) { - // On attempts past 1, assume the application is built correctly and re-use it. - result = await appStarter(this, webUri, package, applicationBinary != null || attempt > 0); - if (result != null) { - break; - } - attempt += 1; - globals.printError('Application failed to start on attempt: $attempt'); - } - if (result == null) { - throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1); - } - observatoryUri = result.observatoryUri.toString(); - // TODO(bkonyi): add web support (https://github.com/flutter/flutter/issues/61259) - if (!isWebPlatform && !disableDds) { - try { - // If there's another flutter_tools instance still connected to the target - // application, DDS will already be running remotely and this call will fail. - // We can ignore this and continue to use the remote DDS instance. - await device.dds.startDartDevelopmentService( - Uri.parse(observatoryUri), - ddsPort, - ipv6, - disableServiceAuthCodes, - ); - observatoryUri = device.dds.uri.toString(); - } on dds.DartDevelopmentServiceException catch(_) { - globals.printTrace('Note: DDS is already connected to $observatoryUri.'); - } - } + if (boolArg('keep-app-running') ?? (argResults['use-existing-app'] != null)) { + _logger.printStatus('Leaving the application running.'); } else { - globals.printStatus('Will connect to already running application instance.'); - observatoryUri = stringArg('use-existing-app'); + final File skslFile = stringArg('write-sksl-on-exit') != null + ? _fileSystem.file(stringArg('write-sksl-on-exit')) + : null; + await driverService.stop(userIdentifier: userIdentifier, writeSkslOnExit: skslFile); } - - final Map environment = { - 'VM_SERVICE_URL': observatoryUri, - }; - - async_io.WebDriver driver; - // For web device, WebDriver session will be launched beforehand - // so that FlutterDriver can reuse it. - if (isWebPlatform) { - final Browser browser = _browserNameToEnum( - argResults['browser-name'].toString()); - final String driverPort = argResults['driver-port'].toString(); - // start WebDriver - try { - driver = await _createDriver( - driverPort, - browser, - argResults['headless'].toString() == 'true', - stringArg('chrome-binary'), - ); - } on Exception catch (ex) { - throwToolExit( - 'Unable to start WebDriver Session for Flutter for Web testing. \n' - 'Make sure you have the correct WebDriver Server running at $driverPort. \n' - 'Make sure the WebDriver Server matches option --browser-name. \n' - '$ex' - ); - } - - final bool isAndroidChrome = browser == Browser.androidChrome; - final bool useEmulator = argResults['android-emulator'] as bool; - // set window size - // for android chrome, skip such action - if (!isAndroidChrome) { - final List dimensions = argResults['browser-dimension'].split( - ',') as List; - assert(dimensions.length == 2); - int x, y; - try { - x = int.parse(dimensions[0]); - y = int.parse(dimensions[1]); - } on FormatException catch (ex) { - throwToolExit(''' -Dimension provided to --browser-dimension is invalid: -$ex - '''); - } - final async_io.Window window = await driver.window; - await window.setLocation(const math.Point(0, 0)); - await window.setSize(math.Rectangle(0, 0, x, y)); - } - - // add driver info to environment variables - environment.addAll( { - 'DRIVER_SESSION_ID': driver.id, - 'DRIVER_SESSION_URI': driver.uri.toString(), - 'DRIVER_SESSION_SPEC': driver.spec.toString(), - 'DRIVER_SESSION_CAPABILITIES': json.encode(driver.capabilities), - 'SUPPORT_TIMELINE_ACTION': (browser == Browser.chrome).toString(), - 'FLUTTER_WEB_TEST': 'true', - 'ANDROID_CHROME_ON_EMULATOR': (isAndroidChrome && useEmulator).toString(), - }); + if (testResult != 0) { + return FlutterCommandResult.fail(); } - - try { - await testRunner( - [ - if (buildInfo.dartExperiments.isNotEmpty) - '--enable-experiment=${buildInfo.dartExperiments.join(',')}', - if (buildInfo.nullSafetyMode == NullSafetyMode.sound) - '--sound-null-safety', - if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) - '--no-sound-null-safety', - testFile, - ], - environment, - ); - } on Exception catch (error, stackTrace) { - if (error is ToolExit) { - rethrow; - } - throw Exception('Unable to run test: $error\n$stackTrace'); - } finally { - await residentRunner?.exit(); - await driver?.quit(); - if (stringArg('write-sksl-on-exit') != null) { - final File outputFile = globals.fs.file(stringArg('write-sksl-on-exit')); - final vm_service.VmService vmService = await connectToVmService( - Uri.parse(observatoryUri), - ); - final FlutterView flutterView = (await vmService.getFlutterViews()).first; - final Map result = await vmService.getSkSLs( - viewId: flutterView.id - ); - await sharedSkSlWriter(_device, result, outputFile: outputFile); - } - if (boolArg('keep-app-running') ?? (argResults['use-existing-app'] != null)) { - globals.printStatus('Leaving the application running.'); - } else { - globals.printStatus('Stopping application instance.'); - await appStopper(this, package); - } - - await device?.dispose(); - } - return FlutterCommandResult.success(); } @@ -400,28 +238,28 @@ $ex // If the --driver argument wasn't provided, then derive the value from // the target file. - String appFile = globals.fs.path.normalize(targetFile); + String appFile = _fileSystem.path.normalize(targetFile); // This command extends `flutter run` and therefore CWD == package dir - final String packageDir = globals.fs.currentDirectory.path; + final String packageDir = _fileSystem.currentDirectory.path; // Make appFile path relative to package directory because we are looking // for the corresponding test file relative to it. - if (!globals.fs.path.isRelative(appFile)) { - if (!globals.fs.path.isWithin(packageDir, appFile)) { - globals.printError( + if (!_fileSystem.path.isRelative(appFile)) { + if (!_fileSystem.path.isWithin(packageDir, appFile)) { + _logger.printError( 'Application file $appFile is outside the package directory $packageDir' ); return null; } - appFile = globals.fs.path.relative(appFile, from: packageDir); + appFile = _fileSystem.path.relative(appFile, from: packageDir); } - final List parts = globals.fs.path.split(appFile); + final List parts = _fileSystem.path.split(appFile); if (parts.length < 2) { - globals.printError( + _logger.printError( 'Application file $appFile must reside in one of the sub-directories ' 'of the package structure, not in the root directory.' ); @@ -431,277 +269,8 @@ $ex // Look for the test file inside `test_driver/` matching the sub-path, e.g. // if the application is `lib/foo/bar.dart`, the test file is expected to // be `test_driver/foo/bar_test.dart`. - final String pathWithNoExtension = globals.fs.path.withoutExtension(globals.fs.path.joinAll( + final String pathWithNoExtension = _fileSystem.path.withoutExtension(_fileSystem.path.joinAll( [packageDir, 'test_driver', ...parts.skip(1)])); - return '${pathWithNoExtension}_test${globals.fs.path.extension(appFile)}'; - } -} - -Future findTargetDevice({ @required Duration timeout }) async { - final DeviceManager deviceManager = globals.deviceManager; - final List devices = await deviceManager.findTargetDevices(null, timeout: timeout); - - if (deviceManager.hasSpecifiedDeviceId) { - if (devices.isEmpty) { - globals.printStatus("No devices found with name or id matching '${deviceManager.specifiedDeviceId}'"); - return null; - } - if (devices.length > 1) { - globals.printStatus("Found ${devices.length} devices with name or id matching '${deviceManager.specifiedDeviceId}':"); - await Device.printDevices(devices, globals.logger); - return null; - } - return devices.first; - } - - if (devices.isEmpty) { - globals.printError('No devices found.'); - return null; - } else if (devices.length > 1) { - globals.printStatus('Found multiple connected devices:'); - await Device.printDevices(devices, globals.logger); - } - globals.printStatus('Using device ${devices.first.name}.'); - return devices.first; -} - -/// Starts the application on the device given command configuration. -typedef AppStarter = Future Function(DriveCommand command, Uri webUri, ApplicationPackage applicationPackage, bool prebuiltApplication); - -AppStarter appStarter = _startApp; // (mutable for testing) -void restoreAppStarter() { - appStarter = _startApp; -} - -Future _startApp( - DriveCommand command, - Uri webUri, - ApplicationPackage applicationPackage, - bool prebuiltApplication, -) async { - final String mainPath = findMainDartFile(command.targetFile); - if (await globals.fs.type(mainPath) != FileSystemEntityType.file) { - globals.printError('Tried to run $mainPath, but that file does not exist.'); - return null; - } - - globals.printTrace('Stopping previously running application, if any.'); - await appStopper(command, applicationPackage); - - - final Map platformArgs = {}; - if (command.traceStartup) { - platformArgs['trace-startup'] = command.traceStartup; - } - - if (webUri != null) { - platformArgs['uri'] = webUri.toString(); - if (!command.getBuildInfo().isDebug) { - // For web device, startApp will be triggered twice - // and it will error out for chrome the second time. - platformArgs['no-launch-chrome'] = true; - } - } - - globals.printTrace('Starting application.'); - - // Forward device log messages to the terminal window running the "drive" command. - final DeviceLogReader logReader = await command.device.getLogReader(app: applicationPackage); - command._deviceLogSubscription = logReader - .logLines - .listen(globals.printStatus); - - final LaunchResult result = await command.device.startApp( - applicationPackage, - mainPath: mainPath, - route: command.route, - debuggingOptions: DebuggingOptions.enabled( - command.getBuildInfo(), - startPaused: true, - hostVmServicePort: webUri != null ? command.hostVmservicePort : 0, - disablePortPublication: command.disablePortPublication, - ddsPort: command.ddsPort, - verboseSystemLogs: command.verboseSystemLogs, - cacheSkSL: command.cacheSkSL, - dumpSkpOnShaderCompilation: command.dumpSkpOnShaderCompilation, - purgePersistentCache: command.purgePersistentCache, - ), - platformArgs: platformArgs, - userIdentifier: command.userIdentifier, - prebuiltApplication: prebuiltApplication, - ); - - if (!result.started) { - await command._deviceLogSubscription.cancel(); - return null; - } - - return result; -} - -/// Runs driver tests. -typedef TestRunner = Future Function(List testArgs, Map environment); -TestRunner testRunner = _runTests; -void restoreTestRunner() { - testRunner = _runTests; -} - -Future _runTests(List testArgs, Map environment) async { - globals.printTrace('Running driver tests.'); - - globalPackagesPath = globals.fs.path.normalize(globals.fs.path.absolute(globalPackagesPath)); - final int result = await globals.processUtils.stream( - [ - globals.artifacts.getArtifactPath(Artifact.engineDartBinary), - ...testArgs, - '--packages=$globalPackagesPath', - '-rexpanded', - ], - environment: environment, - ); - if (result != 0) { - throwToolExit('Driver tests failed: $result', exitCode: result); - } -} - - -/// Stops the application. -typedef AppStopper = Future Function(DriveCommand command, ApplicationPackage applicationPackage); -AppStopper appStopper = _stopApp; -void restoreAppStopper() { - appStopper = _stopApp; -} - -Future _stopApp(DriveCommand command, ApplicationPackage package) async { - globals.printTrace('Stopping application.'); - final bool stopped = await command.device.stopApp(package, userIdentifier: command.userIdentifier); - await command.device.uninstallApp(package); - await command._deviceLogSubscription?.cancel(); - return stopped; -} - -/// A list of supported browsers. -@visibleForTesting -enum Browser { - /// Chrome on Android: https://developer.chrome.com/multidevice/android/overview - androidChrome, - /// Chrome: https://www.google.com/chrome/ - chrome, - /// Edge: https://www.microsoft.com/en-us/windows/microsoft-edge - edge, - /// Firefox: https://www.mozilla.org/en-US/firefox/ - firefox, - /// Safari in iOS: https://www.apple.com/safari/ - iosSafari, - /// Safari in macOS: https://www.apple.com/safari/ - safari, -} - -/// Converts [browserName] string to [Browser] -Browser _browserNameToEnum(String browserName){ - switch (browserName) { - case 'android-chrome': return Browser.androidChrome; - case 'chrome': return Browser.chrome; - case 'edge': return Browser.edge; - case 'firefox': return Browser.firefox; - case 'ios-safari': return Browser.iosSafari; - case 'safari': return Browser.safari; - } - throw UnsupportedError('Browser $browserName not supported'); -} - -Future _createDriver(String driverPort, Browser browser, bool headless, String chromeBinary) async { - return async_io.createDriver( - uri: Uri.parse('http://localhost:$driverPort/'), - desired: getDesiredCapabilities(browser, headless, chromeBinary), - spec: async_io.WebDriverSpec.Auto - ); -} - -/// Returns desired capabilities for given [browser], [headless] and -/// [chromeBinary]. -@visibleForTesting -Map getDesiredCapabilities(Browser browser, bool headless, [String chromeBinary]) { - switch (browser) { - case Browser.chrome: - return { - 'acceptInsecureCerts': true, - 'browserName': 'chrome', - 'goog:loggingPrefs': { async_io.LogType.performance: 'ALL'}, - 'chromeOptions': { - if (chromeBinary != null) - 'binary': chromeBinary, - 'w3c': false, - 'args': [ - '--bwsi', - '--disable-background-timer-throttling', - '--disable-default-apps', - '--disable-extensions', - '--disable-popup-blocking', - '--disable-translate', - '--no-default-browser-check', - '--no-sandbox', - '--no-first-run', - if (headless) '--headless' - ], - 'perfLoggingPrefs': { - 'traceCategories': - 'devtools.timeline,' - 'v8,blink.console,benchmark,blink,' - 'blink.user_timing' - } - }, - }; - break; - case Browser.firefox: - return { - 'acceptInsecureCerts': true, - 'browserName': 'firefox', - 'moz:firefoxOptions' : { - 'args': [ - if (headless) '-headless' - ], - 'prefs': { - 'dom.file.createInChild': true, - 'dom.timeout.background_throttling_max_budget': -1, - 'media.autoplay.default': 0, - 'media.gmp-manager.url': '', - 'media.gmp-provider.enabled': false, - 'network.captive-portal-service.enabled': false, - 'security.insecure_field_warning.contextual.enabled': false, - 'test.currentTimeOffsetSeconds': 11491200 - }, - 'log': {'level': 'trace'} - } - }; - break; - case Browser.edge: - return { - 'acceptInsecureCerts': true, - 'browserName': 'edge', - }; - break; - case Browser.safari: - return { - 'browserName': 'safari', - }; - break; - case Browser.iosSafari: - return { - 'platformName': 'ios', - 'browserName': 'safari', - 'safari:useSimulator': true - }; - case Browser.androidChrome: - return { - 'browserName': 'chrome', - 'platformName': 'android', - 'goog:chromeOptions': { - 'androidPackage': 'com.android.chrome', - 'args': ['--disable-fullscreen'] - }, - }; - default: - throw UnsupportedError('Browser $browser not supported.'); + return '${pathWithNoExtension}_test${_fileSystem.path.extension(appFile)}'; } } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index fb0a27c587..4e93282f0e 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -4,8 +4,6 @@ import 'dart:async'; -import 'package:args/command_runner.dart'; - import '../android/android_device.dart'; import '../base/common.dart'; import '../base/file_system.dart'; @@ -25,8 +23,8 @@ import '../tracing.dart'; import '../web/web_runner.dart'; import 'daemon.dart'; +/// Shared logic between `flutter run` and `flutter drive` commands. abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts { - // Used by run and drive commands. RunCommandBase({ bool verboseHelp = false }) { addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp); usesDartDefineOption(); @@ -77,6 +75,45 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment 'this must be the path to an APK. For iOS applications, the path to an IPA. Other device types ' 'do not yet support prebuilt application binaries', valueHelp: 'path/to/app.apk', + ) + ..addFlag('start-paused', + defaultsTo: startPausedDefault, + help: 'Start in a paused mode and wait for a debugger to connect.', + ) + ..addOption('dart-flags', + hide: !verboseHelp, + help: 'Pass a list of comma separated flags to the Dart instance at ' + 'application startup. Flags passed through this option must be ' + 'present on the allowlist defined within the Flutter engine. If ' + 'a disallowed flag is encountered, the process will be ' + 'terminated immediately.\n\n' + 'This flag is not available on the stable channel and is only ' + 'applied in debug and profile modes. This option should only ' + 'be used for experiments and should not be used by typical users.' + ) + ..addFlag('endless-trace-buffer', + negatable: false, + help: 'Enable tracing to the endless tracer. This is useful when ' + 'recording huge amounts of traces. If we need to use endless buffer to ' + 'record startup traces, we can combine the ("--trace-startup"). ' + 'For example, flutter run --trace-startup --endless-trace-buffer. ', + ) + ..addFlag('trace-systrace', + negatable: false, + help: 'Enable tracing to the system tracer. This is only useful on ' + 'platforms where such a tracer is available (Android and Fuchsia).', + ) + ..addFlag('trace-skia', + negatable: false, + help: 'Enable tracing of Skia code. This is useful when debugging ' + 'the raster thread (formerly known as the GPU thread). ' + 'By default, Flutter will not log skia code.', + ) + ..addOption('trace-allowlist', + hide: true, + help: 'Filters out all trace events except those that are specified in ' + 'this comma separated list of allowed prefixes.', + valueHelp: 'foo,bar', ); usesWebOptions(hide: !verboseHelp); usesTargetOption(); @@ -96,7 +133,71 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment bool get dumpSkpOnShaderCompilation => boolArg('dump-skp-on-shader-compilation'); bool get purgePersistentCache => boolArg('purge-persistent-cache'); bool get disableServiceAuthCodes => boolArg('disable-service-auth-codes'); + bool get runningWithPrebuiltApplication => argResults['use-application-binary'] != null; + bool get trackWidgetCreation => boolArg('track-widget-creation'); + + /// Whether to start the application paused by default. + bool get startPausedDefault; + String get route => stringArg('route'); + + String get traceAllowlist => stringArg('trace-allowlist'); + + /// Create a debugging options instance for the current `run` or `drive` invocation. + DebuggingOptions createDebuggingOptions() { + final BuildInfo buildInfo = getBuildInfo(); + final int browserDebugPort = featureFlags.isWebEnabled && argResults.wasParsed('web-browser-debug-port') + ? int.parse(stringArg('web-browser-debug-port')) + : null; + if (buildInfo.mode.isRelease) { + return DebuggingOptions.disabled( + buildInfo, + hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', + port: featureFlags.isWebEnabled ? stringArg('web-port') : '', + webUseSseForDebugProxy: featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse', + webUseSseForDebugBackend: featureFlags.isWebEnabled && stringArg('web-server-debug-backend-protocol') == 'sse', + webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'), + webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), + webBrowserDebugPort: browserDebugPort, + ); + } else { + return DebuggingOptions.enabled( + buildInfo, + startPaused: boolArg('start-paused'), + disableServiceAuthCodes: boolArg('disable-service-auth-codes'), + disableDds: boolArg('disable-dds'), + dartFlags: stringArg('dart-flags') ?? '', + useTestFonts: argParser.options.containsKey('use-test-fonts') && boolArg('use-test-fonts'), + enableSoftwareRendering: argParser.options.containsKey('enable-software-rendering') && boolArg('enable-software-rendering'), + skiaDeterministicRendering: argParser.options.containsKey('skia-deterministic-rendering') && boolArg('skia-deterministic-rendering'), + traceSkia: boolArg('trace-skia'), + traceAllowlist: traceAllowlist, + traceSystrace: boolArg('trace-systrace'), + endlessTraceBuffer: boolArg('endless-trace-buffer'), + dumpSkpOnShaderCompilation: dumpSkpOnShaderCompilation, + cacheSkSL: cacheSkSL, + purgePersistentCache: purgePersistentCache, + deviceVmServicePort: deviceVmservicePort, + hostVmServicePort: hostVmservicePort, + disablePortPublication: disablePortPublication, + ddsPort: ddsPort, + verboseSystemLogs: boolArg('verbose-system-logs'), + hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', + port: featureFlags.isWebEnabled ? stringArg('web-port') : '', + webUseSseForDebugProxy: featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse', + webUseSseForDebugBackend: featureFlags.isWebEnabled && stringArg('web-server-debug-backend-protocol') == 'sse', + webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'), + webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), + webBrowserDebugPort: browserDebugPort, + webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), + vmserviceOutFile: stringArg('vmservice-out-file'), + fastStart: argParser.options.containsKey('fast-start') + && boolArg('fast-start') + && !runningWithPrebuiltApplication, + nullAssertions: boolArg('null-assertions'), + ); + } + } } class RunCommand extends RunCommandBase { @@ -111,10 +212,6 @@ class RunCommand extends RunCommandBase { // without needing to know the port. addPublishPort(enabledByDefault: true, verboseHelp: verboseHelp); argParser - ..addFlag('start-paused', - negatable: false, - help: 'Start in a paused mode and wait for a debugger to connect.', - ) ..addFlag('enable-software-rendering', negatable: false, help: 'Enable rendering using the Skia software backend. ' @@ -127,35 +224,6 @@ class RunCommand extends RunCommandBase { help: 'When combined with --enable-software-rendering, provides 100% ' 'deterministic Skia rendering.', ) - ..addFlag('trace-skia', - negatable: false, - help: 'Enable tracing of Skia code. This is useful when debugging ' - 'the raster thread (formerly known as the GPU thread). ' - 'By default, Flutter will not log skia code.', - ) - ..addOption('trace-whitelist', - hide: true, - help: '(deprecated) Use --trace-allowlist instead', - valueHelp: 'foo,bar', - ) - ..addOption('trace-allowlist', - hide: true, - help: 'Filters out all trace events except those that are specified in ' - 'this comma separated list of allowed prefixes.', - valueHelp: 'foo,bar', - ) - ..addFlag('endless-trace-buffer', - negatable: false, - help: 'Enable tracing to the endless tracer. This is useful when ' - 'recording huge amounts of traces. If we need to use endless buffer to ' - 'record startup traces, we can combine the ("--trace-startup"). ' - 'For example, flutter run --trace-startup --endless-trace-buffer. ', - ) - ..addFlag('trace-systrace', - negatable: false, - help: 'Enable tracing to the system tracer. This is only useful on ' - 'platforms where such a tracer is available (Android and Fuchsia).', - ) ..addFlag('await-first-frame-when-tracing', defaultsTo: true, help: 'Whether to wait for the first frame when tracing startup ("--trace-startup"), ' @@ -176,16 +244,6 @@ class RunCommand extends RunCommandBase { defaultsTo: true, help: 'If necessary, build the app before running.', ) - ..addOption('dart-flags', - hide: !verboseHelp, - help: 'Pass a list of comma separated flags to the Dart instance at ' - 'application startup. Flags passed through this option must be ' - 'present on the allowlist defined within the Flutter engine. If ' - 'a disallowed flag is encountered, the process will be ' - 'terminated immediately.\n\n' - 'This flag is not available on the stable channel and is only ' - 'applied in debug and profile modes. This option should only ' - 'be used for experiments and should not be used by typical users.') ..addOption('project-root', hide: !verboseHelp, help: 'Specify the project root directory.', @@ -248,6 +306,9 @@ class RunCommand extends RunCommandBase { String get userIdentifier => stringArg(FlutterOptions.kDeviceUser); + @override + bool get startPausedDefault => false; + @override Future get usagePath async { final String command = await super.usagePath; @@ -342,9 +403,6 @@ class RunCommand extends RunCommandBase { return getBuildInfo().isDebug && shouldUseHotMode; } - bool get runningWithPrebuiltApplication => - argResults['use-application-binary'] != null; - bool get stayResident => boolArg('resident'); bool get awaitFirstFrameWhenTracing => boolArg('await-first-frame-when-tracing'); @@ -372,73 +430,6 @@ class RunCommand extends RunCommandBase { } } - String get _traceAllowlist { - final String deprecatedValue = stringArg('trace-whitelist'); - if (deprecatedValue != null) { - globals.printError('--trace-whitelist has been deprecated, use --trace-allowlist instead'); - } - return stringArg('trace-allowlist') ?? deprecatedValue; - } - - DebuggingOptions _createDebuggingOptions() { - final BuildInfo buildInfo = getBuildInfo(); - final int browserDebugPort = featureFlags.isWebEnabled && argResults.wasParsed('web-browser-debug-port') - ? int.parse(stringArg('web-browser-debug-port')) - : null; - if (buildInfo.mode.isRelease) { - return DebuggingOptions.disabled( - buildInfo, - initializePlatform: boolArg('web-initialize-platform'), - hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', - port: featureFlags.isWebEnabled ? stringArg('web-port') : '', - webUseSseForDebugProxy: featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse', - webUseSseForDebugBackend: featureFlags.isWebEnabled && stringArg('web-server-debug-backend-protocol') == 'sse', - webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'), - webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), - webBrowserDebugPort: browserDebugPort, - ); - } else { - return DebuggingOptions.enabled( - buildInfo, - startPaused: boolArg('start-paused'), - disableServiceAuthCodes: boolArg('disable-service-auth-codes'), - disableDds: boolArg('disable-dds'), - dartFlags: stringArg('dart-flags') ?? '', - useTestFonts: boolArg('use-test-fonts'), - enableSoftwareRendering: boolArg('enable-software-rendering'), - skiaDeterministicRendering: boolArg('skia-deterministic-rendering'), - traceSkia: boolArg('trace-skia'), - traceAllowlist: _traceAllowlist, - traceSystrace: boolArg('trace-systrace'), - endlessTraceBuffer: boolArg('endless-trace-buffer'), - dumpSkpOnShaderCompilation: dumpSkpOnShaderCompilation, - cacheSkSL: cacheSkSL, - purgePersistentCache: purgePersistentCache, - deviceVmServicePort: deviceVmservicePort, - hostVmServicePort: hostVmservicePort, - disablePortPublication: disablePortPublication, - ddsPort: ddsPort, - verboseSystemLogs: boolArg('verbose-system-logs'), - initializePlatform: boolArg('web-initialize-platform'), - hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', - port: featureFlags.isWebEnabled ? stringArg('web-port') : '', - webUseSseForDebugProxy: featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse', - webUseSseForDebugBackend: featureFlags.isWebEnabled && stringArg('web-server-debug-backend-protocol') == 'sse', - webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'), - webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), - webBrowserDebugPort: browserDebugPort, - webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), - vmserviceOutFile: stringArg('vmservice-out-file'), - // Allow forcing fast-start to off to prevent doing more work on devices that - // don't support it. - fastStart: boolArg('fast-start') - && !runningWithPrebuiltApplication - && devices.every((Device device) => device.supportsFastStart), - nullAssertions: boolArg('null-assertions'), - ); - } - } - @override Future runCommand() async { // Enable hot mode by default if `--no-hot` was not passed and we are in @@ -464,11 +455,11 @@ class RunCommand extends RunCommandBase { final String applicationBinaryPath = stringArg('use-application-binary'); app = await daemon.appDomain.startApp( devices.first, globals.fs.currentDirectory.path, targetFile, route, - _createDebuggingOptions(), hotMode, + createDebuggingOptions(), hotMode, applicationBinary: applicationBinaryPath == null ? null : globals.fs.file(applicationBinaryPath), - trackWidgetCreation: boolArg('track-widget-creation'), + trackWidgetCreation: trackWidgetCreation, projectRootPath: stringArg('project-root'), packagesFilePath: globalResults['packages'] as String, dillOutputPath: stringArg('output-dill'), @@ -491,11 +482,6 @@ class RunCommand extends RunCommandBase { } globals.terminal.usesTerminalUi = true; - if (argResults['dart-flags'] != null && !globals.flutterVersion.isMaster) { - throw UsageException('--dart-flags is not available on the stable ' - 'channel.', null); - } - final BuildMode buildMode = getBuildMode(); for (final Device device in devices) { if (!await device.supportsRuntimeMode(buildMode)) { @@ -534,7 +520,6 @@ class RunCommand extends RunCommandBase { for (final Device device in devices) await FlutterDevice.create( device, - flutterProject: flutterProject, fileSystemRoots: stringsArg('filesystem-root'), fileSystemScheme: stringArg('filesystem-scheme'), experimentalFlags: expFlags, @@ -556,7 +541,7 @@ class RunCommand extends RunCommandBase { runner = HotRunner( flutterDevices, target: targetFile, - debuggingOptions: _createDebuggingOptions(), + debuggingOptions: createDebuggingOptions(), benchmarkMode: boolArg('benchmark'), applicationBinary: applicationBinaryPath == null ? null @@ -572,7 +557,7 @@ class RunCommand extends RunCommandBase { target: targetFile, flutterProject: flutterProject, ipv6: ipv6, - debuggingOptions: _createDebuggingOptions(), + debuggingOptions: createDebuggingOptions(), stayResident: stayResident, urlTunneller: null, ); @@ -580,7 +565,7 @@ class RunCommand extends RunCommandBase { runner = ColdRunner( flutterDevices, target: targetFile, - debuggingOptions: _createDebuggingOptions(), + debuggingOptions: createDebuggingOptions(), traceStartup: traceStartup, awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing, applicationBinary: applicationBinaryPath == null diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index c76b9a1e35..5fffc87074 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -847,7 +847,6 @@ class DebuggingOptions { this.disablePortPublication = false, this.deviceVmServicePort, this.ddsPort, - this.initializePlatform = true, this.hostname, this.port, this.webEnableExposeUrl, @@ -862,7 +861,6 @@ class DebuggingOptions { }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildInfo, { - this.initializePlatform = true, this.port, this.hostname, this.webEnableExposeUrl, @@ -913,8 +911,6 @@ class DebuggingOptions { final bool purgePersistentCache; final bool useTestFonts; final bool verboseSystemLogs; - /// Whether to invoke webOnlyInitializePlatform in Flutter for web. - final bool initializePlatform; final int hostVmServicePort; final int deviceVmServicePort; final bool disablePortPublication; diff --git a/packages/flutter_tools/lib/src/drive/drive_service.dart b/packages/flutter_tools/lib/src/drive/drive_service.dart new file mode 100644 index 0000000000..f985df32d4 --- /dev/null +++ b/packages/flutter_tools/lib/src/drive/drive_service.dart @@ -0,0 +1,245 @@ +// 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:dds/dds.dart' as dds; +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:vm_service/vm_service.dart' as vm_service; + +import '../application_package.dart'; +import '../base/common.dart'; +import '../base/logger.dart'; +import '../base/process.dart'; +import '../build_info.dart'; +import '../device.dart'; +import '../vmservice.dart'; +import 'web_driver_service.dart'; + +class FlutterDriverFactory { + FlutterDriverFactory({ + @required ApplicationPackageFactory applicationPackageFactory, + @required Logger logger, + @required ProcessUtils processUtils, + @required String dartSdkPath, + }) : _applicationPackageFactory = applicationPackageFactory, + _logger = logger, + _processUtils = processUtils, + _dartSdkPath = dartSdkPath; + + final ApplicationPackageFactory _applicationPackageFactory; + final Logger _logger; + final ProcessUtils _processUtils; + final String _dartSdkPath; + + /// Create a driver service for running `flutter drive`. + DriverService createDriverService(bool web) { + if (web) { + return WebDriverService( + processUtils: _processUtils, + dartSdkPath: _dartSdkPath, + ); + } + return FlutterDriverService( + logger: _logger, + processUtils: _processUtils, + dartSdkPath: _dartSdkPath, + applicationPackageFactory: _applicationPackageFactory, + ); + } +} + +/// An interface for the `flutter driver` integration test operations. +abstract class DriverService { + /// Install and launch the application for the provided [device]. + Future start( + BuildInfo buildInfo, + Device device, + DebuggingOptions debuggingOptions, + bool ipv6, { + File applicationBinary, + String route, + String userIdentifier, + String mainPath, + Map platformArgs = const {}, + }); + + /// Start the test file with the provided [arguments] and [environment], returning + /// the test process exit code. + Future startTest( + String testFile, + List arguments, + Map environment, { + bool headless, + String chromeBinary, + String browserName, + bool androidEmulator, + int driverPort, + List browserDimension, + }); + + /// Stop the running application and uninstall it from the device. + /// + /// If [writeSkslOnExit] is non-null, will connect to the VM Service + /// and write SkSL to the file. This is only supported on mobile and + /// desktop devices. + Future stop({ + File writeSkslOnExit, + String userIdentifier, + }); +} + +/// An implementation of the driver service that connects to mobile and desktop +/// applications. +class FlutterDriverService extends DriverService { + FlutterDriverService({ + @required ApplicationPackageFactory applicationPackageFactory, + @required Logger logger, + @required ProcessUtils processUtils, + @required String dartSdkPath, + @visibleForTesting VMServiceConnector vmServiceConnector = connectToVmService, + }) : _applicationPackageFactory = applicationPackageFactory, + _logger = logger, + _processUtils = processUtils, + _dartSdkPath = dartSdkPath, + _vmServiceConnector = vmServiceConnector; + + static const int _kLaunchAttempts = 3; + + final ApplicationPackageFactory _applicationPackageFactory; + final Logger _logger; + final ProcessUtils _processUtils; + final String _dartSdkPath; + final VMServiceConnector _vmServiceConnector; + + Device _device; + ApplicationPackage _applicationPackage; + String _vmServiceUri; + vm_service.VmService _vmService; + + @override + Future start( + BuildInfo buildInfo, + Device device, + DebuggingOptions debuggingOptions, + bool ipv6, { + File applicationBinary, + String route, + String userIdentifier, + Map platformArgs = const {}, + String mainPath, + }) async { + if (buildInfo.isRelease) { + throwToolExit( + 'Flutter Driver (non-web) does not support running in release mode.\n' + '\n' + 'Use --profile mode for testing application performance.\n' + 'Use --debug (default) mode for testing correctness (with assertions).' + ); + } + _device = device; + final TargetPlatform targetPlatform = await device.targetPlatform; + _applicationPackage = await _applicationPackageFactory.getPackageForPlatform( + targetPlatform, + buildInfo: buildInfo, + applicationBinary: applicationBinary, + ); + int attempt = 0; + LaunchResult result; + bool prebuiltApplication = applicationBinary != null; + while (attempt < _kLaunchAttempts) { + result = await device.startApp( + _applicationPackage, + mainPath: mainPath, + route: route, + debuggingOptions: debuggingOptions, + platformArgs: platformArgs, + userIdentifier: userIdentifier, + prebuiltApplication: prebuiltApplication, + ); + if (result != null && result.started) { + break; + } + // On attempts past 1, assume the application is built correctly and re-use it. + attempt += 1; + prebuiltApplication = true; + _logger.printError('Application failed to start on attempt: $attempt'); + } + if (result == null || !result.started) { + throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1); + } + _vmServiceUri = result.observatoryUri.toString(); + try { + await device.dds.startDartDevelopmentService( + result.observatoryUri, + debuggingOptions.ddsPort, + ipv6, + debuggingOptions.disableServiceAuthCodes, + ); + _vmServiceUri = device.dds.uri.toString(); + } on dds.DartDevelopmentServiceException { + // If there's another flutter_tools instance still connected to the target + // application, DDS will already be running remotely and this call will fail. + // This can be ignored to continue to use the existing remote DDS instance. + } + _vmService = await _vmServiceConnector(Uri.parse(_vmServiceUri), device: _device); + final DeviceLogReader logReader = await device.getLogReader(app: _applicationPackage); + logReader.logLines.listen(_logger.printStatus); + + final vm_service.VM vm = await _vmService.getVM(); + logReader.appPid = vm.pid; + } + + @override + Future startTest( + String testFile, + List arguments, + Map environment, { + bool headless, + String chromeBinary, + String browserName, + bool androidEmulator, + int driverPort, + List browserDimension, + }) async { + return _processUtils.stream([ + _dartSdkPath, + ...arguments, + testFile, + '-rexpanded', + ], environment: { + 'VM_SERVICE_URL': _vmServiceUri, + ...environment, + }); + } + + @override + Future stop({ + File writeSkslOnExit, + String userIdentifier, + }) async { + if (writeSkslOnExit != null) { + final FlutterView flutterView = (await _vmService.getFlutterViews()).first; + final Map result = await _vmService.getSkSLs( + viewId: flutterView.id + ); + await sharedSkSlWriter(_device, result, outputFile: writeSkslOnExit, logger: _logger); + } + try { + if (!await _device.stopApp(_applicationPackage, userIdentifier: userIdentifier)) { + _logger.printError('Failed to stop app'); + } + } on Exception catch (err) { + _logger.printError('Failed to stop app due to unhandled error: $err'); + } + + try { + if (!await _device.uninstallApp(_applicationPackage, userIdentifier: userIdentifier)) { + _logger.printError('Failed to uninstall app'); + } + } on Exception catch (err) { + _logger.printError('Failed to uninstall app due to unhandled error: $err'); + } + await _device.dispose(); + } +} diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart new file mode 100644 index 0000000000..9a84c3412f --- /dev/null +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -0,0 +1,273 @@ +// 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 'dart:async'; +import 'dart:math' as math; + +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:webdriver/async_io.dart' as async_io; + +import '../base/common.dart'; +import '../base/process.dart'; +import '../build_info.dart'; +import '../convert.dart'; +import '../device.dart'; +import '../globals.dart' as globals; +import '../project.dart'; +import '../resident_runner.dart'; +import '../web/web_runner.dart'; +import 'drive_service.dart'; + +/// An implementation of the driver service for web debug and release applications. +class WebDriverService extends DriverService { + WebDriverService({ + @required ProcessUtils processUtils, + @required String dartSdkPath, + }) : _processUtils = processUtils, + _dartSdkPath = dartSdkPath; + + final ProcessUtils _processUtils; + final String _dartSdkPath; + + ResidentRunner _residentRunner; + Uri _webUri; + + @override + Future start( + BuildInfo buildInfo, + Device device, + DebuggingOptions debuggingOptions, + bool ipv6, { + File applicationBinary, + String route, + String userIdentifier, + String mainPath, + Map platformArgs = const {}, + }) async { + final FlutterDevice flutterDevice = await FlutterDevice.create( + device, + target: mainPath, + buildInfo: buildInfo, + platform: globals.platform, + ); + _residentRunner = webRunnerFactory.createWebRunner( + flutterDevice, + target: mainPath, + ipv6: ipv6, + debuggingOptions: buildInfo.isRelease ? + DebuggingOptions.disabled( + buildInfo, + port: debuggingOptions.port, + ) + : DebuggingOptions.enabled( + buildInfo, + port: debuggingOptions.port, + disablePortPublication: debuggingOptions.disablePortPublication, + ), + stayResident: false, + urlTunneller: null, + flutterProject: FlutterProject.current(), + ); + final Completer appStartedCompleter = Completer.sync(); + final int result = await _residentRunner.run( + appStartedCompleter: appStartedCompleter, + route: route, + ); + _webUri = _residentRunner.uri; + if (result != 0) { + throwToolExit(null); + } + } + + @override + Future startTest(String testFile, List arguments, Map environment, { + bool headless, + String chromeBinary, + String browserName, + bool androidEmulator, + int driverPort, + List browserDimension, + }) async { + async_io.WebDriver webDriver; + final Browser browser = _browserNameToEnum(browserName); + try { + webDriver = await async_io.createDriver( + uri: Uri.parse('http://localhost:$driverPort/'), + desired: getDesiredCapabilities(browser, headless, chromeBinary), + spec: async_io.WebDriverSpec.Auto + ); + } on Exception catch (ex) { + throwToolExit( + 'Unable to start WebDriver Session for Flutter for Web testing. \n' + 'Make sure you have the correct WebDriver Server running at $driverPort. \n' + 'Make sure the WebDriver Server matches option --browser-name. \n' + '$ex' + ); + } + + final bool isAndroidChrome = browser == Browser.androidChrome; + // Do not set the window size for android chrome browser. + if (!isAndroidChrome) { + assert(browserDimension.length == 2); + int x; + int y; + try { + x = int.parse(browserDimension[0]); + y = int.parse(browserDimension[1]); + } on FormatException catch (ex) { + throwToolExit('Dimension provided to --browser-dimension is invalid: $ex'); + } + final async_io.Window window = await webDriver.window; + await window.setLocation(const math.Point(0, 0)); + await window.setSize(math.Rectangle(0, 0, x, y)); + } + final int result = await _processUtils.stream([ + _dartSdkPath, + ...arguments, + testFile, + '-rexpanded', + ], environment: { + 'VM_SERVICE_URL': _webUri.toString(), + ..._additionalDriverEnvironment(webDriver, browserName, androidEmulator), + ...environment, + }); + await webDriver.quit(); + return result; + } + + @override + Future stop({File writeSkslOnExit, String userIdentifier}) async { + await _residentRunner.cleanupAtFinish(); + } + + Map _additionalDriverEnvironment(async_io.WebDriver webDriver, String browserName, bool androidEmulator) { + return { + 'DRIVER_SESSION_ID': webDriver.id, + 'DRIVER_SESSION_URI': webDriver.uri.toString(), + 'DRIVER_SESSION_SPEC': webDriver.spec.toString(), + 'DRIVER_SESSION_CAPABILITIES': json.encode(webDriver.capabilities), + 'SUPPORT_TIMELINE_ACTION': (_browserNameToEnum(browserName) == Browser.chrome).toString(), + 'FLUTTER_WEB_TEST': 'true', + 'ANDROID_CHROME_ON_EMULATOR': (_browserNameToEnum(browserName) == Browser.androidChrome && androidEmulator).toString(), + }; + } +} + +/// A list of supported browsers. +enum Browser { + /// Chrome on Android: https://developer.chrome.com/multidevice/android/overview + androidChrome, + /// Chrome: https://www.google.com/chrome/ + chrome, + /// Edge: https://www.microsoft.com/en-us/windows/microsoft-edge + edge, + /// Firefox: https://www.mozilla.org/en-US/firefox/ + firefox, + /// Safari in iOS: https://www.apple.com/safari/ + iosSafari, + /// Safari in macOS: https://www.apple.com/safari/ + safari, +} + +/// Returns desired capabilities for given [browser], [headless] and +/// [chromeBinary]. +@visibleForTesting +Map getDesiredCapabilities(Browser browser, bool headless, [String chromeBinary]) { + switch (browser) { + case Browser.chrome: + return { + 'acceptInsecureCerts': true, + 'browserName': 'chrome', + 'goog:loggingPrefs': { async_io.LogType.performance: 'ALL'}, + 'chromeOptions': { + if (chromeBinary != null) + 'binary': chromeBinary, + 'w3c': false, + 'args': [ + '--bwsi', + '--disable-background-timer-throttling', + '--disable-default-apps', + '--disable-extensions', + '--disable-popup-blocking', + '--disable-translate', + '--no-default-browser-check', + '--no-sandbox', + '--no-first-run', + if (headless) '--headless' + ], + 'perfLoggingPrefs': { + 'traceCategories': + 'devtools.timeline,' + 'v8,blink.console,benchmark,blink,' + 'blink.user_timing' + } + }, + }; + break; + case Browser.firefox: + return { + 'acceptInsecureCerts': true, + 'browserName': 'firefox', + 'moz:firefoxOptions' : { + 'args': [ + if (headless) '-headless' + ], + 'prefs': { + 'dom.file.createInChild': true, + 'dom.timeout.background_throttling_max_budget': -1, + 'media.autoplay.default': 0, + 'media.gmp-manager.url': '', + 'media.gmp-provider.enabled': false, + 'network.captive-portal-service.enabled': false, + 'security.insecure_field_warning.contextual.enabled': false, + 'test.currentTimeOffsetSeconds': 11491200 + }, + 'log': {'level': 'trace'} + } + }; + break; + case Browser.edge: + return { + 'acceptInsecureCerts': true, + 'browserName': 'edge', + }; + break; + case Browser.safari: + return { + 'browserName': 'safari', + }; + break; + case Browser.iosSafari: + return { + 'platformName': 'ios', + 'browserName': 'safari', + 'safari:useSimulator': true + }; + case Browser.androidChrome: + return { + 'browserName': 'chrome', + 'platformName': 'android', + 'goog:chromeOptions': { + 'androidPackage': 'com.android.chrome', + 'args': ['--disable-fullscreen'] + }, + }; + default: + throw UnsupportedError('Browser $browser not supported.'); + } +} + +/// Converts [browserName] string to [Browser] +Browser _browserNameToEnum(String browserName){ + switch (browserName) { + case 'android-chrome': return Browser.androidChrome; + case 'chrome': return Browser.chrome; + case 'edge': return Browser.edge; + case 'firefox': return Browser.firefox; + case 'ios-safari': return Browser.iosSafari; + case 'safari': return Browser.safari; + } + throw UnsupportedError('Browser $browserName not supported'); +} diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 2205adc24f..a908af6652 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -528,7 +528,6 @@ class _ResidentWebRunner extends ResidentWebRunner { flutterProject, target, debuggingOptions.buildInfo, - debuggingOptions.initializePlatform, false, kNoneWorker, true, @@ -596,7 +595,6 @@ class _ResidentWebRunner extends ResidentWebRunner { flutterProject, target, debuggingOptions.buildInfo, - debuggingOptions.initializePlatform, false, kNoneWorker, true, diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index dd4c0e212d..3c3757c905 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -70,7 +70,6 @@ class FlutterDevice { /// Create a [FlutterDevice] with optional code generation enabled. static Future create( Device device, { - @required FlutterProject flutterProject, @required String target, @required BuildInfo buildInfo, @required Platform platform, diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 79e4e95c50..1e8acae81e 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -8,6 +8,7 @@ import 'package:vm_service/vm_service.dart' as vm_service; import 'base/context.dart'; import 'base/io.dart' as io; +import 'base/logger.dart'; import 'build_info.dart'; import 'convert.dart'; import 'device.dart'; @@ -806,9 +807,11 @@ bool isPauseEvent(String kind) { // or delete it. Future sharedSkSlWriter(Device device, Map data, { File outputFile, + Logger logger, }) async { + logger ??= globals.logger; if (data.isEmpty) { - globals.logger.printStatus( + logger.printStatus( 'No data was received. To ensure SkSL data can be generated use a ' 'physical device then:\n' ' 1. Pass "--cache-sksl" as an argument to flutter run.\n' @@ -844,7 +847,7 @@ Future sharedSkSlWriter(Device device, Map data, { 'data': data, }; outputFile.writeAsStringSync(json.encode(manifest)); - globals.logger.printStatus('Wrote SkSL data to ${outputFile.path}.'); + logger.printStatus('Wrote SkSL data to ${outputFile.path}.'); return outputFile.path; } diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index d494c57675..85006cc163 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -26,7 +26,6 @@ Future buildWeb( FlutterProject flutterProject, String target, BuildInfo buildInfo, - bool initializePlatform, bool csp, String serviceWorkerStrategy, bool sourceMaps, @@ -54,7 +53,6 @@ Future buildWeb( defines: { kBuildMode: getNameForBuildMode(buildInfo.mode), kTargetFile: target, - kInitializePlatform: initializePlatform.toString(), kHasWebPlugins: hasWebPlugins.toString(), kDartDefines: encodeDartDefines(buildInfo.dartDefines), kCspMode: csp.toString(), diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart index 41555ceb9e..4a76cf5bab 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart @@ -54,7 +54,6 @@ void main() { fileSystem.path.join('lib', 'main.dart'), BuildInfo.debug, false, - false, null, true, ), throwsToolExit()); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart deleted file mode 100644 index 9538adab9a..0000000000 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ /dev/null @@ -1,882 +0,0 @@ -// 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:file_testing/file_testing.dart'; -import 'package:flutter_tools/src/android/android_device.dart'; -import 'package:flutter_tools/src/application_package.dart'; -import 'package:flutter_tools/src/base/common.dart'; -import 'package:flutter_tools/src/base/dds.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/cache.dart'; -import 'package:flutter_tools/src/commands/drive.dart'; -import 'package:flutter_tools/src/device.dart'; -import 'package:flutter_tools/src/globals.dart' as globals; -import 'package:mockito/mockito.dart'; -import 'package:webdriver/sync_io.dart' as sync_io; -import 'package:flutter_tools/src/vmservice.dart'; - -import '../../src/common.dart'; -import '../../src/context.dart'; -import '../../src/fakes.dart'; - -void main() { - group('drive', () { - DriveCommand command; - Device mockUnsupportedDevice; - MemoryFileSystem fs; - Directory tempDir; - - setUpAll(() { - Cache.disableLocking(); - }); - - setUp(() { - command = DriveCommand(); - fs = MemoryFileSystem.test(); - tempDir = fs.systemTempDirectory.createTempSync('flutter_drive_test.'); - fs.currentDirectory = tempDir; - fs.directory('test').createSync(); - fs.directory('test_driver').createSync(); - fs.file('pubspec.yaml').createSync(); - fs.file('.packages').createSync(); - setExitFunctionForTests(); - appStarter = (DriveCommand command, Uri webUri, ApplicationPackage package, bool prebuiltApplication) { - throw 'Unexpected call to appStarter'; - }; - testRunner = (List testArgs, Map environment) { - throw 'Unexpected call to testRunner'; - }; - appStopper = (DriveCommand command, ApplicationPackage package) { - throw 'Unexpected call to appStopper'; - }; - command.applicationPackages = FakeApplicationPackageFactory(); - }); - - tearDown(() { - command = null; - restoreExitFunction(); - restoreAppStarter(); - restoreAppStopper(); - restoreTestRunner(); - tryToDelete(tempDir); - }); - - void applyDdsMocks(Device device) { - final MockDartDevelopmentService mockDds = MockDartDevelopmentService(); - when(device.dds).thenReturn(mockDds); - when(mockDds.startDartDevelopmentService(any, any, any, any)).thenReturn(null); - when(mockDds.uri).thenReturn(Uri.parse('http://localhost:8181')); - } - - testUsingContext('returns 1 when test file is not found', () async { - testDeviceManager.addDevice(MockDevice()); - - final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - globals.fs.file(testApp).createSync(recursive: true); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - fail('Expect exception'); - } on ToolExit catch (e) { - expect(e.exitCode ?? 1, 1); - expect(e.message, contains('Test file not found: $testFile')); - } - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('returns 1 when app fails to run', () async { - testDeviceManager.addDevice(MockDevice()); - appStarter = expectAsync4((DriveCommand command, Uri webUri, ApplicationPackage package, bool prebuiltApplication) async => null, count: 3); - - final String testApp = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e.dart'); - final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() { }'); - await memFs.file(testFile).writeAsString('main() { }'); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - fail('Expect exception'); - } on ToolExit catch (e) { - expect(e.exitCode, 1); - expect(e.message, contains('Application failed to start. Will not run test. Quitting.')); - } - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('returns 1 when app file is outside package', () async { - final String appFile = globals.fs.path.join(tempDir.dirname, 'other_app', 'app.dart'); - globals.fs.file(appFile).createSync(recursive: true); - final List args = [ - '--no-wrap', - 'drive', - '--target=$appFile', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - fail('Expect exception'); - } on ToolExit catch (e) { - expect(e.exitCode ?? 1, 1); - expect(testLogger.errorText, contains( - 'Application file $appFile is outside the package directory ${tempDir.path}', - )); - } - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('returns 1 when app file is in the root dir', () async { - final String appFile = globals.fs.path.join(tempDir.path, 'main.dart'); - globals.fs.file(appFile).createSync(recursive: true); - final List args = [ - '--no-wrap', - 'drive', - '--target=$appFile', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - fail('Expect exception'); - } on ToolExit catch (e) { - expect(e.exitCode ?? 1, 1); - expect(testLogger.errorText, contains( - 'Application file main.dart must reside in one of the ' - 'sub-directories of the package structure, not in the root directory.', - )); - } - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('returns 1 when targeted device is not Android with --device-user', () async { - testDeviceManager.addDevice(MockDevice()); - - final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - globals.fs.file(testApp).createSync(recursive: true); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - '--device-user', - '10', - ]; - - expect(() async => await createTestCommandRunner(command).run(args), - throwsToolExit(message: '--device-user is only supported for Android')); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('returns 0 when test ends successfully', () async { - final MockAndroidDevice mockDevice = MockAndroidDevice(); - applyDdsMocks(mockDevice); - testDeviceManager.addDevice(mockDevice); - - final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - appStarter = expectAsync4((DriveCommand command, Uri webUri, ApplicationPackage package, bool prebuiltApplication) async { - return LaunchResult.succeeded(); - }); - testRunner = expectAsync2((List testArgs, Map environment) async { - expect(testArgs, ['--no-sound-null-safety', testFile]); - // VM_SERVICE_URL is not set by drive command arguments - expect(environment, { - 'VM_SERVICE_URL': 'null', - }); - }); - appStopper = expectAsync2((DriveCommand command, ApplicationPackage package) async { - return true; - }); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() {}'); - await memFs.file(testFile).writeAsString('main() {}'); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - '--disable-dds', - '--device-user', - '10', - ]; - await createTestCommandRunner(command).run(args); - verify(mockDevice.dispose()); - expect(testLogger.errorText, isEmpty); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('returns exitCode set by test runner', () async { - final MockDevice mockDevice = MockDevice(); - applyDdsMocks(mockDevice); - testDeviceManager.addDevice(mockDevice); - - final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - appStarter = expectAsync4((DriveCommand command, Uri webUri, ApplicationPackage package, bool prebuiltApplication) async { - return LaunchResult.succeeded(); - }); - testRunner = (List testArgs, Map environment) async { - throwToolExit(null, exitCode: 123); - }; - appStopper = expectAsync2((DriveCommand command, ApplicationPackage package) async { - return true; - }); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() {}'); - await memFs.file(testFile).writeAsString('main() {}'); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - fail('Expect exception'); - } on ToolExit catch (e) { - expect(e.exitCode ?? 1, 123); - expect(e.message, isNull); - } - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('enable experiment', () async { - final MockAndroidDevice mockDevice = MockAndroidDevice(); - applyDdsMocks(mockDevice); - testDeviceManager.addDevice(mockDevice); - - final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - appStarter = expectAsync4((DriveCommand command, Uri webUri, ApplicationPackage package, bool prebuiltApplication) async { - return LaunchResult.succeeded(); - }); - testRunner = expectAsync2((List testArgs, Map environment) async { - expect( - testArgs, - [ - '--enable-experiment=experiment1,experiment2', - '--no-sound-null-safety', - testFile, - ] - ); - }); - appStopper = expectAsync2((DriveCommand command, ApplicationPackage package) async { - return true; - }); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() {}'); - await memFs.file(testFile).writeAsString('main() {}'); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - '--enable-experiment=experiment1', - '--enable-experiment=experiment2', - ]; - await createTestCommandRunner(command).run(args); - expect(testLogger.errorText, isEmpty); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('sound null safety', () async { - final MockAndroidDevice mockDevice = MockAndroidDevice(); - applyDdsMocks(mockDevice); - testDeviceManager.addDevice(mockDevice); - - final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - appStarter = expectAsync4((DriveCommand command, Uri webUri, ApplicationPackage package, bool prebuiltApplication) async { - return LaunchResult.succeeded(); - }); - testRunner = expectAsync2((List testArgs, Map environment) async { - expect( - testArgs, - [ - '--sound-null-safety', - testFile, - ] - ); - }); - appStopper = expectAsync2((DriveCommand command, ApplicationPackage package) async { - return true; - }); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() {}'); - await memFs.file(testFile).writeAsString('main() {}'); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - '--sound-null-safety', - ]; - await createTestCommandRunner(command).run(args); - expect(testLogger.errorText, isEmpty); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - group('findTargetDevice', () { - testUsingContext('uses specified device', () async { - testDeviceManager.specifiedDeviceId = '123'; - final Device mockDevice = MockDevice(); - testDeviceManager.addDevice(mockDevice); - when(mockDevice.name).thenReturn('specified-device'); - when(mockDevice.id).thenReturn('123'); - - final Device device = await findTargetDevice(timeout: null); - expect(device.name, 'specified-device'); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - }); - - void findTargetDeviceOnOperatingSystem(String operatingSystem) { - Platform platform() => FakePlatform(operatingSystem: operatingSystem); - - testUsingContext('returns null if no devices found', () async { - expect(await findTargetDevice(timeout: null), isNull); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - Platform: platform, - }); - - testUsingContext('uses existing Android device', () async { - final Device mockDevice = MockAndroidDevice(); - when(mockDevice.name).thenReturn('mock-android-device'); - testDeviceManager.addDevice(mockDevice); - - final Device device = await findTargetDevice(timeout: null); - expect(device.name, 'mock-android-device'); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - Platform: platform, - }); - - testUsingContext('skips unsupported device', () async { - final Device mockDevice = MockAndroidDevice(); - mockUnsupportedDevice = MockDevice(); - when(mockUnsupportedDevice.isSupportedForProject(any)) - .thenReturn(false); - when(mockUnsupportedDevice.isSupported()) - .thenReturn(false); - when(mockUnsupportedDevice.name).thenReturn('mock-web'); - when(mockUnsupportedDevice.id).thenReturn('web-1'); - when(mockUnsupportedDevice.targetPlatform).thenAnswer((_) => Future(() => TargetPlatform.web_javascript)); - when(mockUnsupportedDevice.isLocalEmulator).thenAnswer((_) => Future(() => false)); - when(mockUnsupportedDevice.sdkNameAndVersion).thenAnswer((_) => Future(() => 'html5')); - when(mockDevice.name).thenReturn('mock-android-device'); - when(mockDevice.id).thenReturn('mad-28'); - when(mockDevice.isSupported()) - .thenReturn(true); - when(mockDevice.isSupportedForProject(any)) - .thenReturn(true); - when(mockDevice.targetPlatform).thenAnswer((_) => Future(() => TargetPlatform.android_x64)); - when(mockDevice.isLocalEmulator).thenAnswer((_) => Future(() => false)); - when(mockDevice.sdkNameAndVersion).thenAnswer((_) => Future(() => 'sdk-28')); - testDeviceManager.addDevice(mockDevice); - testDeviceManager.addDevice(mockUnsupportedDevice); - - final Device device = await findTargetDevice(timeout: null); - expect(device.name, 'mock-android-device'); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - Platform: platform, - }); - } - - group('findTargetDevice on Linux', () { - findTargetDeviceOnOperatingSystem('linux'); - }); - - group('findTargetDevice on Windows', () { - findTargetDeviceOnOperatingSystem('windows'); - }); - - group('findTargetDevice on macOS', () { - findTargetDeviceOnOperatingSystem('macos'); - - Platform macOsPlatform() => FakePlatform(operatingSystem: 'macos'); - - testUsingContext('uses existing simulator', () async { - final Device mockDevice = MockDevice(); - testDeviceManager.addDevice(mockDevice); - when(mockDevice.name).thenReturn('mock-simulator'); - when(mockDevice.isLocalEmulator) - .thenAnswer((Invocation invocation) => Future.value(true)); - - final Device device = await findTargetDevice(timeout: null); - expect(device.name, 'mock-simulator'); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - Platform: macOsPlatform, - }); - }); - - group('build arguments', () { - String testApp, testFile; - - setUp(() { - restoreAppStarter(); - }); - - Future appStarterSetup() async { - final Device mockDevice = MockDevice(); - applyDdsMocks(mockDevice); - testDeviceManager.addDevice(mockDevice); - - final FakeDeviceLogReader mockDeviceLogReader = FakeDeviceLogReader(); - when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader); - final MockLaunchResult mockLaunchResult = MockLaunchResult(); - when(mockLaunchResult.started).thenReturn(true); - when(mockDevice.startApp( - null, - mainPath: anyNamed('mainPath'), - route: anyNamed('route'), - debuggingOptions: anyNamed('debuggingOptions'), - platformArgs: anyNamed('platformArgs'), - prebuiltApplication: anyNamed('prebuiltApplication'), - userIdentifier: anyNamed('userIdentifier'), - )).thenAnswer((_) => Future.value(mockLaunchResult)); - when(mockDevice.isAppInstalled(any, userIdentifier: anyNamed('userIdentifier'))) - .thenAnswer((_) => Future.value(false)); - - testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - testRunner = (List testArgs, Map environment) async { - throwToolExit(null, exitCode: 123); - }; - appStopper = expectAsync2( - (DriveCommand command, ApplicationPackage package) async { - return true; - }, - count: 2, - ); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() {}'); - await memFs.file(testFile).writeAsString('main() {}'); - return mockDevice; - } - - testUsingContext('does not use pre-built app if no build arg provided', () async { - final Device mockDevice = await appStarterSetup(); - - final List args = [ - 'drive', - '--target=$testApp', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - } on ToolExit catch (e) { - expect(e.exitCode, 123); - expect(e.message, null); - } - verify(mockDevice.startApp( - null, - mainPath: anyNamed('mainPath'), - route: anyNamed('route'), - debuggingOptions: anyNamed('debuggingOptions'), - platformArgs: anyNamed('platformArgs'), - prebuiltApplication: false, - userIdentifier: anyNamed('userIdentifier'), - )); - verify(mockDevice.dispose()); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('does not use pre-built app if --build arg provided', () async { - final Device mockDevice = await appStarterSetup(); - - final List args = [ - 'drive', - '--build', - '--target=$testApp', - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - } on ToolExit catch (e) { - expect(e.exitCode, 123); - expect(e.message, null); - } - verify(mockDevice.startApp( - null, - mainPath: anyNamed('mainPath'), - route: anyNamed('route'), - debuggingOptions: anyNamed('debuggingOptions'), - platformArgs: anyNamed('platformArgs'), - prebuiltApplication: false, - userIdentifier: anyNamed('userIdentifier'), - )); - verify(mockDevice.dispose()); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - }); - - group('debugging options', () { - DebuggingOptions debuggingOptions; - - String testApp, testFile; - - setUp(() { - restoreAppStarter(); - }); - - Future appStarterSetup() async { - final Device mockDevice = MockDevice(); - applyDdsMocks(mockDevice); - testDeviceManager.addDevice(mockDevice); - - final FakeDeviceLogReader mockDeviceLogReader = FakeDeviceLogReader(); - when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader); - final MockLaunchResult mockLaunchResult = MockLaunchResult(); - when(mockLaunchResult.started).thenReturn(true); - when(mockDevice.startApp( - null, - mainPath: anyNamed('mainPath'), - route: anyNamed('route'), - debuggingOptions: anyNamed('debuggingOptions'), - platformArgs: anyNamed('platformArgs'), - prebuiltApplication: anyNamed('prebuiltApplication'), - userIdentifier: anyNamed('userIdentifier'), - )).thenAnswer((Invocation invocation) async { - debuggingOptions = invocation.namedArguments[#debuggingOptions] as DebuggingOptions; - return mockLaunchResult; - }); - when(mockDevice.isAppInstalled(any, userIdentifier: anyNamed('userIdentifier'))) - .thenAnswer((_) => Future.value(false)); - - testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); - testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); - - testRunner = (List testArgs, Map environment) async { - throwToolExit(null, exitCode: 123); - }; - appStopper = expectAsync2( - (DriveCommand command, ApplicationPackage package) async { - return true; - }, - count: 2, - ); - - final MemoryFileSystem memFs = fs; - await memFs.file(testApp).writeAsString('main() {}'); - await memFs.file(testFile).writeAsString('main() {}'); - return mockDevice; - } - - void _testOptionThatDefaultsToFalse( - String optionName, - bool setToTrue, - bool optionValue(), - ) { - testUsingContext('$optionName ${setToTrue ? 'works' : 'defaults to false'}', () async { - final Device mockDevice = await appStarterSetup(); - - final List args = [ - 'drive', - '--target=$testApp', - if (setToTrue) optionName, - '--no-pub', - ]; - try { - await createTestCommandRunner(command).run(args); - } on ToolExit catch (e) { - expect(e.exitCode, 123); - expect(e.message, null); - } - verify(mockDevice.startApp( - null, - mainPath: anyNamed('mainPath'), - route: anyNamed('route'), - debuggingOptions: anyNamed('debuggingOptions'), - platformArgs: anyNamed('platformArgs'), - prebuiltApplication: false, - userIdentifier: anyNamed('userIdentifier'), - )); - expect(optionValue(), setToTrue ? isTrue : isFalse); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - } - - void testOptionThatDefaultsToFalse( - String optionName, - bool optionValue(), - ) { - _testOptionThatDefaultsToFalse(optionName, true, optionValue); - _testOptionThatDefaultsToFalse(optionName, false, optionValue); - } - - testOptionThatDefaultsToFalse( - '--dump-skp-on-shader-compilation', - () => debuggingOptions.dumpSkpOnShaderCompilation, - ); - - testOptionThatDefaultsToFalse( - '--verbose-system-logs', - () => debuggingOptions.verboseSystemLogs, - ); - - testOptionThatDefaultsToFalse( - '--cache-sksl', - () => debuggingOptions.cacheSkSL, - ); - - testOptionThatDefaultsToFalse( - '--purge-persistent-cache', - () => debuggingOptions.purgePersistentCache, - ); - - testOptionThatDefaultsToFalse( - '--publish-port', - () => !debuggingOptions.disablePortPublication, - ); - }); - }); - - group('getDesiredCapabilities', () { - test('Chrome with headless on', () { - final Map expected = { - 'acceptInsecureCerts': true, - 'browserName': 'chrome', - 'goog:loggingPrefs': { sync_io.LogType.performance: 'ALL'}, - 'chromeOptions': { - 'w3c': false, - 'args': [ - '--bwsi', - '--disable-background-timer-throttling', - '--disable-default-apps', - '--disable-extensions', - '--disable-popup-blocking', - '--disable-translate', - '--no-default-browser-check', - '--no-sandbox', - '--no-first-run', - '--headless' - ], - 'perfLoggingPrefs': { - 'traceCategories': - 'devtools.timeline,' - 'v8,blink.console,benchmark,blink,' - 'blink.user_timing' - } - } - }; - - expect(getDesiredCapabilities(Browser.chrome, true), expected); - }); - - test('Chrome with headless off', () { - const String chromeBinary = 'random-binary'; - final Map expected = { - 'acceptInsecureCerts': true, - 'browserName': 'chrome', - 'goog:loggingPrefs': { sync_io.LogType.performance: 'ALL'}, - 'chromeOptions': { - 'binary': chromeBinary, - 'w3c': false, - 'args': [ - '--bwsi', - '--disable-background-timer-throttling', - '--disable-default-apps', - '--disable-extensions', - '--disable-popup-blocking', - '--disable-translate', - '--no-default-browser-check', - '--no-sandbox', - '--no-first-run', - ], - 'perfLoggingPrefs': { - 'traceCategories': - 'devtools.timeline,' - 'v8,blink.console,benchmark,blink,' - 'blink.user_timing' - } - } - }; - - expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary), expected); - - }); - - test('Firefox with headless on', () { - final Map expected = { - 'acceptInsecureCerts': true, - 'browserName': 'firefox', - 'moz:firefoxOptions' : { - 'args': ['-headless'], - 'prefs': { - 'dom.file.createInChild': true, - 'dom.timeout.background_throttling_max_budget': -1, - 'media.autoplay.default': 0, - 'media.gmp-manager.url': '', - 'media.gmp-provider.enabled': false, - 'network.captive-portal-service.enabled': false, - 'security.insecure_field_warning.contextual.enabled': false, - 'test.currentTimeOffsetSeconds': 11491200 - }, - 'log': {'level': 'trace'} - } - }; - - expect(getDesiredCapabilities(Browser.firefox, true), expected); - }); - - test('Firefox with headless off', () { - final Map expected = { - 'acceptInsecureCerts': true, - 'browserName': 'firefox', - 'moz:firefoxOptions' : { - 'args': [], - 'prefs': { - 'dom.file.createInChild': true, - 'dom.timeout.background_throttling_max_budget': -1, - 'media.autoplay.default': 0, - 'media.gmp-manager.url': '', - 'media.gmp-provider.enabled': false, - 'network.captive-portal-service.enabled': false, - 'security.insecure_field_warning.contextual.enabled': false, - 'test.currentTimeOffsetSeconds': 11491200 - }, - 'log': {'level': 'trace'} - } - }; - - expect(getDesiredCapabilities(Browser.firefox, false), expected); - }); - - test('Edge', () { - final Map expected = { - 'acceptInsecureCerts': true, - 'browserName': 'edge', - }; - - expect(getDesiredCapabilities(Browser.edge, false), expected); - }); - - test('macOS Safari', () { - final Map expected = { - 'browserName': 'safari', - }; - - expect(getDesiredCapabilities(Browser.safari, false), expected); - }); - - test('iOS Safari', () { - final Map expected = { - 'platformName': 'ios', - 'browserName': 'safari', - 'safari:useSimulator': true - }; - - expect(getDesiredCapabilities(Browser.iosSafari, false), expected); - }); - - test('android chrome', () { - final Map expected = { - 'browserName': 'chrome', - 'platformName': 'android', - 'goog:chromeOptions': { - 'androidPackage': 'com.android.chrome', - 'args': ['--disable-fullscreen'] - }, - }; - - expect(getDesiredCapabilities(Browser.androidChrome, false), expected); - }); - }); - - testUsingContext('Can write SkSL file with provided output file', () async { - final MockDevice device = MockDevice(); - when(device.name).thenReturn('foo'); - when(device.targetPlatform).thenAnswer((Invocation invocation) async { - return TargetPlatform.android_arm; - }); - final File outputFile = globals.fs.file('out/foo'); - - final String result = await sharedSkSlWriter( - device, - {'foo': 'bar'}, - outputFile: outputFile, - ); - - expect(result, 'out/foo'); - expect(outputFile, exists); - expect(outputFile.readAsStringSync(), '{"platform":"android","name":"foo","engineRevision":null,"data":{"foo":"bar"}}'); - }, overrides: { - FileSystem: () => MemoryFileSystem.test(), - ProcessManager: () => FakeProcessManager.any(), - }); -} - -class MockDevice extends Mock implements Device { - MockDevice() { - when(isSupported()).thenReturn(true); - } -} - -class MockAndroidDevice extends Mock implements AndroidDevice { } -class MockDartDevelopmentService extends Mock implements DartDevelopmentService { } -class MockLaunchResult extends Mock implements LaunchResult { } -class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory { - @override - Future getPackageForPlatform(TargetPlatform platform, {BuildInfo buildInfo, File applicationBinary}) { - return null; - } -} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index dcc2d8fd1f..615d0e00ed 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -77,7 +77,6 @@ void main() { ..writeAsStringSync('void main() {}'); environment.defines[kTargetFile] = mainFile.path; environment.defines[kHasWebPlugins] = 'true'; - environment.defines[kInitializePlatform] = 'true'; await const WebEntrypointTarget().build(environment); final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); @@ -86,9 +85,6 @@ void main() { expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';")); expect(generated, contains('registerPlugins(webPluginRegistry);')); - // Platform - expect(generated, contains('if (true) {')); - // Main expect(generated, contains('entrypoint.main();')); @@ -183,7 +179,6 @@ void main() { environment.defines[kTargetFile] = mainFile.path; environment.defines[kHasWebPlugins] = 'true'; - environment.defines[kInitializePlatform] = 'true'; await const WebEntrypointTarget().build(environment); final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); @@ -192,9 +187,6 @@ void main() { expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';")); expect(generated, contains('registerPlugins(webPluginRegistry);')); - // Platform - expect(generated, contains('if (true) {')); - // Main expect(generated, contains('entrypoint.main();')); @@ -210,7 +202,6 @@ void main() { ..writeAsStringSync('void main() {}'); environment.defines[kTargetFile] = mainFile.path; environment.defines[kHasWebPlugins] = 'false'; - environment.defines[kInitializePlatform] = 'true'; await const WebEntrypointTarget().build(environment); final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); @@ -218,32 +209,6 @@ void main() { // Plugins expect(generated, isNot(contains("import 'package:foo/generated_plugin_registrant.dart';"))); expect(generated, isNot(contains('registerPlugins(webPluginRegistry);'))); - - // Platform - expect(generated, contains('if (true) {')); - - // Main - expect(generated, contains('entrypoint.main();')); - })); - - test('WebEntrypointTarget generates an entrypoint with plugins and without init platform', () => testbed.run(() async { - final File mainFile = globals.fs.file(globals.fs.path.join('foo', 'lib', 'main.dart')) - ..createSync(recursive: true) - ..writeAsStringSync('void main() {}'); - environment.defines[kTargetFile] = mainFile.path; - environment.defines[kHasWebPlugins] = 'true'; - environment.defines[kInitializePlatform] = 'false'; - await const WebEntrypointTarget().build(environment); - - final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); - - // Plugins - expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';")); - expect(generated, contains('registerPlugins(webPluginRegistry);')); - - // Platform - expect(generated, contains('if (false) {')); - // Main expect(generated, contains('entrypoint.main();')); })); @@ -282,7 +247,6 @@ void main() { ..writeAsStringSync('void main() {}'); environment.defines[kTargetFile] = mainFile.path; environment.defines[kHasWebPlugins] = 'false'; - environment.defines[kInitializePlatform] = 'false'; await const WebEntrypointTarget().build(environment); final String generated = environment.buildDir.childFile('main.dart').readAsStringSync(); @@ -291,9 +255,6 @@ void main() { expect(generated, isNot(contains("import 'package:foo/generated_plugin_registrant.dart';"))); expect(generated, isNot(contains('registerPlugins(webPluginRegistry);'))); - // Platform - expect(generated, contains('if (false) {')); - // Main expect(generated, contains('entrypoint.main();')); })); diff --git a/packages/flutter_tools/test/general.shard/desktop_device_test.dart b/packages/flutter_tools/test/general.shard/desktop_device_test.dart index 76a5604d75..11f4d28a4e 100644 --- a/packages/flutter_tools/test/general.shard/desktop_device_test.dart +++ b/packages/flutter_tools/test/general.shard/desktop_device_test.dart @@ -55,7 +55,7 @@ void main() { testWithoutContext('Install and uninstall are no-ops that report success', () async { final FakeDesktopDevice device = setUpDesktopDevice(); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); expect(await device.uninstallApp(package), true); expect(await device.isAppInstalled(package), true); @@ -71,7 +71,7 @@ void main() { group('Starting and stopping application', () { testWithoutContext('Stop without start is a successful no-op', () async { final FakeDesktopDevice device = setUpDesktopDevice(); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); expect(await device.stopApp(package), true); }); @@ -89,7 +89,7 @@ void main() { final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager, fileSystem: fileSystem); final String executableName = device.executablePathForDevice(null, BuildMode.debug); fileSystem.file(executableName).writeAsStringSync('\n'); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, @@ -103,7 +103,7 @@ void main() { testWithoutContext('Null executable path fails gracefully', () async { final BufferLogger logger = BufferLogger.test(); final DesktopDevice device = setUpDesktopDevice(nullExecutablePathForDevice: true, logger: logger); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, @@ -124,7 +124,7 @@ void main() { ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, @@ -168,7 +168,7 @@ void main() { ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, @@ -191,7 +191,6 @@ void main() { purgePersistentCache: true, useTestFonts: true, verboseSystemLogs: true, - initializePlatform: true, nullAssertions: true, ), ); @@ -217,7 +216,7 @@ void main() { ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); - final FakeAppplicationPackage package = FakeAppplicationPackage(); + final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, @@ -228,7 +227,6 @@ void main() { BuildInfo.debug, traceAllowlist: 'foo,bar', cacheSkSL: true, - initializePlatform: true, ), ); @@ -325,7 +323,7 @@ class FakeDesktopDevice extends DesktopDevice { } } -class FakeAppplicationPackage extends Fake implements ApplicationPackage {} +class FakeApplicationPackage extends Fake implements ApplicationPackage {} class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils { @override String get name => 'Example'; diff --git a/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart b/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart new file mode 100644 index 0000000000..9aa02a5f06 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart @@ -0,0 +1,380 @@ +// 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/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/application_package.dart'; +import 'package:flutter_tools/src/base/dds.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/process.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/convert.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/drive/drive_service.dart'; +import 'package:flutter_tools/src/vmservice.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; +import 'package:vm_service/vm_service.dart' as vm_service; + +import '../../src/common.dart'; +import '../../src/context.dart'; + + +final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( + id: '1', + pauseEvent: vm_service.Event( + kind: vm_service.EventKind.kResume, + timestamp: 0 + ), + breakpoints: [], + exceptionPauseMode: null, + libraries: [ + vm_service.LibraryRef( + id: '1', + uri: 'file:///hello_world/main.dart', + name: '', + ), + ], + livePorts: 0, + name: 'test', + number: '1', + pauseOnExit: false, + runnable: true, + startTime: 0, +); + +final vm_service.Isolate fakePausedIsolate = vm_service.Isolate( + id: '1', + pauseEvent: vm_service.Event( + kind: vm_service.EventKind.kPauseException, + timestamp: 0 + ), + breakpoints: [ + vm_service.Breakpoint( + breakpointNumber: 123, + id: 'test-breakpoint', + location: vm_service.SourceLocation( + tokenPos: 0, + script: vm_service.ScriptRef(id: 'test-script', uri: 'foo.dart'), + ), + resolved: true, + ), + ], + exceptionPauseMode: null, + libraries: [], + livePorts: 0, + name: 'test', + number: '1', + pauseOnExit: false, + runnable: true, + startTime: 0, +); + +final vm_service.VM fakeVM = vm_service.VM( + isolates: [fakeUnpausedIsolate], + pid: 1, + hostCPU: '', + isolateGroups: [], + targetCPU: '', + startTime: 0, + name: 'dart', + architectureBits: 64, + operatingSystem: '', + version: '', +); + +final FlutterView fakeFlutterView = FlutterView( + id: 'a', + uiIsolate: fakeUnpausedIsolate, +); + +final FakeVmServiceRequest listViews = FakeVmServiceRequest( + method: kListViewsMethod, + jsonResponse: { + 'views': [ + fakeFlutterView.toJson(), + ], + }, +); + +final FakeVmServiceRequest getVM = FakeVmServiceRequest( + method: 'getVM', + args: {}, + jsonResponse: fakeVM.toJson(), +); + +void main() { + testWithoutContext('Exits if device fails to start', () { + final DriverService driverService = setUpDriverService(); + final Device device = FakeDevice(LaunchResult.failed()); + + expect( + () => driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true), + throwsToolExit(message: 'Application failed to start. Will not run test. Quitting.'), + ); + }); + + testWithoutContext('Retries application launch if it fails the first time', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + getVM, + ]); + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'], + exitCode: 23, + environment: { + 'FOO': 'BAR', + 'VM_SERVICE_URL': 'http://127.0.0.1:1234/' // dds forwarded URI + }, + ), + ]); + final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService); + final Device device = FakeDevice(LaunchResult.succeeded( + observatoryUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'), + ))..failOnce = true; + + await expectLater( + () async => await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true), + returnsNormally, + ); + }); + + testWithoutContext('Connects to device VM Service and runs test application', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + getVM, + ]); + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'], + exitCode: 23, + environment: { + 'FOO': 'BAR', + 'VM_SERVICE_URL': 'http://127.0.0.1:1234/' // dds forwarded URI + }, + ), + ]); + final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService); + final Device device = FakeDevice(LaunchResult.succeeded( + observatoryUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'), + )); + + await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true); + final int testResult = await driverService.startTest( + 'foo.test', + ['--enable-experiment=non-nullable'], + {'FOO': 'BAR'}, + ); + + expect(testResult, 23); + }); + + testWithoutContext('Connects to device VM Service and runs test application without dds', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + getVM, + ]); + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['dart', 'foo.test', '-rexpanded'], + exitCode: 11, + environment: { + 'VM_SERVICE_URL': 'http://127.0.0.1:1234/' + }, + ), + ]); + final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService); + final Device device = FakeDevice(LaunchResult.succeeded( + observatoryUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'), + )); + + await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile, disableDds: true), true); + final int testResult = await driverService.startTest( + 'foo.test', + [], + {}, + ); + + expect(testResult, 11); + }); + + testWithoutContext('Safely stops and uninstalls application', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + getVM, + ]); + final FakeProcessManager processManager = FakeProcessManager.list([]); + final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService); + final FakeDevice device = FakeDevice(LaunchResult.succeeded( + observatoryUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'), + )); + + await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true); + await driverService.stop(); + + expect(device.didStopApp, true); + expect(device.didUninstallApp, true); + expect(device.didDispose, true); + }); + + // FlutterVersion requires context. + testUsingContext('Writes SkSL to file when provided with out file', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + getVM, + listViews, + const FakeVmServiceRequest( + method: '_flutter.getSkSLs', + args: { + 'viewId': 'a' + }, + jsonResponse: { + 'SkSLs': { + 'A': 'B', + } + } + ), + ]); + final FakeProcessManager processManager = FakeProcessManager.list([]); + final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService); + final FakeDevice device = FakeDevice(LaunchResult.succeeded( + observatoryUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'), + )); + + await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true); + await driverService.stop(writeSkslOnExit: fileSystem.file('out.json')); + + expect(device.didStopApp, true); + expect(device.didUninstallApp, true); + expect(json.decode(fileSystem.file('out.json').readAsStringSync()), { + 'platform': 'android', + 'name': 'test', + 'engineRevision': null, + 'data': {'A': 'B'} + }); + }); +} + +FlutterDriverService setUpDriverService({ + Logger logger, + ProcessManager processManager, + vm_service.VmService vmService, +}) { + logger ??= BufferLogger.test(); + return FlutterDriverService( + applicationPackageFactory: FakeApplicationPackageFactory(FakeApplicationPackage()), + logger: logger, + processUtils: ProcessUtils( + logger: logger, + processManager: processManager ?? FakeProcessManager.any(), + ), + dartSdkPath: 'dart', + vmServiceConnector: (Uri httpUri, { + ReloadSources reloadSources, + Restart restart, + CompileExpression compileExpression, + GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, + Object compression, + Device device, + }) async { + return vmService; + } + ); +} + +class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory { + FakeApplicationPackageFactory(this.applicationPackage); + + ApplicationPackage applicationPackage; + + @override + Future getPackageForPlatform( + TargetPlatform platform, { + BuildInfo buildInfo, + File applicationBinary, + }) async => applicationPackage; +} + +class FakeApplicationPackage extends Fake implements ApplicationPackage {} + +class FakeDevice extends Fake implements Device { + FakeDevice(this.result); + + LaunchResult result; + bool didStopApp = false; + bool didUninstallApp = false; + bool didDispose = false; + bool failOnce = false; + + @override + String get name => 'test'; + + @override + final DartDevelopmentService dds = FakeDartDevelopmentService(); + + @override + Future get targetPlatform async => TargetPlatform.android_arm; + + @override + Future getLogReader({ + covariant ApplicationPackage app, + bool includePastLogs = false, + }) async => NoOpDeviceLogReader('test'); + + @override + Future startApp( + covariant ApplicationPackage package, { + String mainPath, + String route, + DebuggingOptions debuggingOptions, + Map platformArgs, + bool prebuiltApplication = false, + bool ipv6 = false, + String userIdentifier, + }) async { + if (failOnce) { + failOnce = false; + return LaunchResult.failed(); + } + return result; + } + + @override + Future stopApp(covariant ApplicationPackage app, {String userIdentifier}) async { + didStopApp = true; + return true; + } + + @override + Future uninstallApp(covariant ApplicationPackage app, {String userIdentifier}) async { + didUninstallApp = true; + return true; + } + + @override + Future dispose() async { + didDispose = true; + } +} + +class FakeDartDevelopmentService extends Fake implements DartDevelopmentService { + bool started = false; + bool disposed = false; + + @override + final Uri uri = Uri.parse('http://127.0.0.1:1234/'); + + @override + Future startDartDevelopmentService( + Uri observatoryUri, + int hostPort, + bool ipv6, + bool disableServiceAuthCodes, + ) async { + started = true; + } + + @override + Future shutdown() async { + disposed = true; + } +} diff --git a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart new file mode 100644 index 0000000000..9b85ab1565 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart @@ -0,0 +1,160 @@ +// 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:flutter_tools/src/drive/web_driver_service.dart'; +import 'package:webdriver/sync_io.dart' as sync_io; + +import '../../src/common.dart'; + +void main() { + testWithoutContext('getDesiredCapabilities Chrome with headless on', () { + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'chrome', + 'goog:loggingPrefs': { sync_io.LogType.performance: 'ALL'}, + 'chromeOptions': { + 'w3c': false, + 'args': [ + '--bwsi', + '--disable-background-timer-throttling', + '--disable-default-apps', + '--disable-extensions', + '--disable-popup-blocking', + '--disable-translate', + '--no-default-browser-check', + '--no-sandbox', + '--no-first-run', + '--headless' + ], + 'perfLoggingPrefs': { + 'traceCategories': + 'devtools.timeline,' + 'v8,blink.console,benchmark,blink,' + 'blink.user_timing' + } + } + }; + + expect(getDesiredCapabilities(Browser.chrome, true), expected); + }); + + testWithoutContext('getDesiredCapabilities Chrome with headless off', () { + const String chromeBinary = 'random-binary'; + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'chrome', + 'goog:loggingPrefs': { sync_io.LogType.performance: 'ALL'}, + 'chromeOptions': { + 'binary': chromeBinary, + 'w3c': false, + 'args': [ + '--bwsi', + '--disable-background-timer-throttling', + '--disable-default-apps', + '--disable-extensions', + '--disable-popup-blocking', + '--disable-translate', + '--no-default-browser-check', + '--no-sandbox', + '--no-first-run', + ], + 'perfLoggingPrefs': { + 'traceCategories': + 'devtools.timeline,' + 'v8,blink.console,benchmark,blink,' + 'blink.user_timing' + } + } + }; + + expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary), expected); + + }); + + testWithoutContext('getDesiredCapabilities Firefox with headless on', () { + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'firefox', + 'moz:firefoxOptions' : { + 'args': ['-headless'], + 'prefs': { + 'dom.file.createInChild': true, + 'dom.timeout.background_throttling_max_budget': -1, + 'media.autoplay.default': 0, + 'media.gmp-manager.url': '', + 'media.gmp-provider.enabled': false, + 'network.captive-portal-service.enabled': false, + 'security.insecure_field_warning.contextual.enabled': false, + 'test.currentTimeOffsetSeconds': 11491200 + }, + 'log': {'level': 'trace'} + } + }; + + expect(getDesiredCapabilities(Browser.firefox, true), expected); + }); + + testWithoutContext('getDesiredCapabilities Firefox with headless off', () { + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'firefox', + 'moz:firefoxOptions' : { + 'args': [], + 'prefs': { + 'dom.file.createInChild': true, + 'dom.timeout.background_throttling_max_budget': -1, + 'media.autoplay.default': 0, + 'media.gmp-manager.url': '', + 'media.gmp-provider.enabled': false, + 'network.captive-portal-service.enabled': false, + 'security.insecure_field_warning.contextual.enabled': false, + 'test.currentTimeOffsetSeconds': 11491200 + }, + 'log': {'level': 'trace'} + } + }; + + expect(getDesiredCapabilities(Browser.firefox, false), expected); + }); + + testWithoutContext('getDesiredCapabilities Edge', () { + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'edge', + }; + + expect(getDesiredCapabilities(Browser.edge, false), expected); + }); + + testWithoutContext('getDesiredCapabilities macOS Safari', () { + final Map expected = { + 'browserName': 'safari', + }; + + expect(getDesiredCapabilities(Browser.safari, false), expected); + }); + + testWithoutContext('getDesiredCapabilities iOS Safari', () { + final Map expected = { + 'platformName': 'ios', + 'browserName': 'safari', + 'safari:useSimulator': true + }; + + expect(getDesiredCapabilities(Browser.iosSafari, false), expected); + }); + + testWithoutContext('getDesiredCapabilities android chrome', () { + final Map expected = { + 'browserName': 'chrome', + 'platformName': 'android', + 'goog:chromeOptions': { + 'androidPackage': 'com.android.chrome', + 'args': ['--disable-fullscreen'] + }, + }; + + expect(getDesiredCapabilities(Browser.androidChrome, false), expected); + }); +} diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 93117e8c9b..e51dd65ed1 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -24,7 +24,6 @@ import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; -import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_cold.dart'; @@ -2418,7 +2417,6 @@ void main() { treeShakeIcons: false, nullSafetyMode: NullSafetyMode.unsound, ), - flutterProject: FlutterProject.current(), target: null, platform: FakePlatform(operatingSystem: 'linux'), )).generator as DefaultResidentCompiler; @@ -2453,7 +2451,6 @@ void main() { treeShakeIcons: false, extraFrontEndOptions: ['--enable-experiment=non-nullable'], ), - flutterProject: FlutterProject.current(), target: null, platform: FakePlatform(operatingSystem: 'linux'), )).generator as DefaultResidentCompiler; @@ -2488,7 +2485,6 @@ void main() { treeShakeIcons: false, extraFrontEndOptions: [], ), - flutterProject: FlutterProject.current(), target: null, platform: null, )).generator as DefaultResidentCompiler; diff --git a/packages/flutter_tools/test/integration.shard/analyze_size_test.dart b/packages/flutter_tools/test/integration.shard/analyze_size_test.dart index 824cf240c6..702d93316d 100644 --- a/packages/flutter_tools/test/integration.shard/analyze_size_test.dart +++ b/packages/flutter_tools/test/integration.shard/analyze_size_test.dart @@ -4,7 +4,6 @@ import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/io.dart'; -import 'package:flutter_tools/src/base/platform.dart'; import '../src/common.dart'; import 'test_utils.dart'; @@ -61,7 +60,7 @@ void main() { final String outputFilePath = line.split(iosDebugMessage).last.trim(); expect(fileSystem.file(fileSystem.path.join(woringDirectory, outputFilePath)), exists); expect(result.exitCode, 0); - }, skip: !const LocalPlatform().isMacOS); // Only supported on macOS + }, skip: true); // Extremely flaky due to https://github.com/flutter/flutter/issues/68144 testWithoutContext('--analyze-size is only supported in release mode', () async { final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');