// 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. // @dart = 2.8 import 'dart:async'; import 'dart:typed_data'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/custom_devices.dart'; import 'package:flutter_tools/src/custom_devices/custom_device_config.dart'; import 'package:flutter_tools/src/custom_devices/custom_devices_config.dart'; import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import 'package:meta/meta.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; const String linuxFlutterRoot = '/flutter'; const String windowsFlutterRoot = r'C:\flutter'; const String defaultConfigLinux1 = r''' { "$schema": "file:///flutter/packages/flutter_tools/static/custom-devices.schema.json", "custom-devices": [ { "id": "pi", "label": "Raspberry Pi", "sdkNameAndVersion": "Raspberry Pi 4 Model B+", "platform": "linux-arm64", "enabled": false, "ping": [ "ping", "-w", "1", "-c", "1", "raspberrypi" ], "pingSuccessRegex": null, "postBuild": null, "install": [ "scp", "-r", "-o", "BatchMode=yes", "${localPath}", "pi@raspberrypi:/tmp/${appName}" ], "uninstall": [ "ssh", "-o", "BatchMode=yes", "pi@raspberrypi", "rm -rf \"/tmp/${appName}\"" ], "runDebug": [ "ssh", "-o", "BatchMode=yes", "pi@raspberrypi", "flutter-pi \"/tmp/${appName}\"" ], "forwardPort": [ "ssh", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-L", "127.0.0.1:${hostPort}:127.0.0.1:${devicePort}", "pi@raspberrypi", "echo 'Port forwarding success'; read" ], "forwardPortSuccessRegex": "Port forwarding success", "screenshot": [ "ssh", "-o", "BatchMode=yes", "pi@raspberrypi", "fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \\n\\t'" ] } ] } '''; const String defaultConfigLinux2 = r''' { "custom-devices": [ { "id": "pi", "label": "Raspberry Pi", "sdkNameAndVersion": "Raspberry Pi 4 Model B+", "platform": "linux-arm64", "enabled": false, "ping": [ "ping", "-w", "1", "-c", "1", "raspberrypi" ], "pingSuccessRegex": null, "postBuild": null, "install": [ "scp", "-r", "-o", "BatchMode=yes", "${localPath}", "pi@raspberrypi:/tmp/${appName}" ], "uninstall": [ "ssh", "-o", "BatchMode=yes", "pi@raspberrypi", "rm -rf \"/tmp/${appName}\"" ], "runDebug": [ "ssh", "-o", "BatchMode=yes", "pi@raspberrypi", "flutter-pi \"/tmp/${appName}\"" ], "forwardPort": [ "ssh", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-L", "127.0.0.1:${hostPort}:127.0.0.1:${devicePort}", "pi@raspberrypi", "echo 'Port forwarding success'; read" ], "forwardPortSuccessRegex": "Port forwarding success", "screenshot": [ "ssh", "-o", "BatchMode=yes", "pi@raspberrypi", "fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \\n\\t'" ] } ], "$schema": "file:///flutter/packages/flutter_tools/static/custom-devices.schema.json" } '''; final Platform linuxPlatform = FakePlatform( environment: <String, String>{ 'FLUTTER_ROOT': linuxFlutterRoot, 'HOME': '/', } ); final Platform windowsPlatform = FakePlatform( operatingSystem: 'windows', environment: <String, String>{ 'FLUTTER_ROOT': windowsFlutterRoot, } ); class FakeTerminal implements Terminal { factory FakeTerminal({Platform platform}) { return FakeTerminal._private( stdio: FakeStdio(), platform: platform ); } FakeTerminal._private({ this.stdio, Platform platform }) : terminal = AnsiTerminal( stdio: stdio, platform: platform ); final FakeStdio stdio; final AnsiTerminal terminal; void simulateStdin(String line) { stdio.simulateStdin(line); } @override set usesTerminalUi(bool value) => terminal.usesTerminalUi = value; @override bool get usesTerminalUi => terminal.usesTerminalUi; @override String bolden(String message) => terminal.bolden(message); @override String clearScreen() => terminal.clearScreen(); @override String color(String message, TerminalColor color) => terminal.color(message, color); @override Stream<String> get keystrokes => terminal.keystrokes; @override Future<String> promptForCharInput( List<String> acceptedCharacters, { Logger logger, String prompt, int defaultChoiceIndex, bool displayAcceptedCharacters = true }) => terminal.promptForCharInput( acceptedCharacters, logger: logger, prompt: prompt, defaultChoiceIndex: defaultChoiceIndex, displayAcceptedCharacters: displayAcceptedCharacters ); @override bool get singleCharMode => terminal.singleCharMode; @override set singleCharMode(bool value) => terminal.singleCharMode = value; @override bool get stdinHasTerminal => terminal.stdinHasTerminal; @override String get successMark => terminal.successMark; @override bool get supportsColor => terminal.supportsColor; @override bool get supportsEmoji => terminal.supportsEmoji; @override String get warningMark => terminal.warningMark; @override int get preferredStyle => terminal.preferredStyle; } class FakeCommandRunner extends FlutterCommandRunner { FakeCommandRunner({ @required Platform platform, @required FileSystem fileSystem, @required Logger logger, UserMessages userMessages }) : _platform = platform, _fileSystem = fileSystem, _logger = logger, _userMessages = userMessages ?? UserMessages(), assert(platform != null), assert(fileSystem != null), assert(logger != null); final Platform _platform; final FileSystem _fileSystem; final Logger _logger; final UserMessages _userMessages; @override Future<void> runCommand(ArgResults topLevelResults) async { final Logger logger = (topLevelResults['verbose'] as bool) ? VerboseLogger(_logger) : _logger; return context.run<void>( overrides: <Type, Generator>{ Logger: () => logger }, body: () { Cache.flutterRoot ??= Cache.defaultFlutterRoot( platform: _platform, fileSystem: _fileSystem, userMessages: _userMessages, ); // For compatibility with tests that set this to a relative path. Cache.flutterRoot = _fileSystem.path.normalize(_fileSystem.path.absolute(Cache.flutterRoot)); return super.runCommand(topLevelResults); } ); } } /// May take platform, logger, processManager and fileSystem from context if /// not explicitly specified. CustomDevicesCommand createCustomDevicesCommand({ CustomDevicesConfig Function(FileSystem, Logger) config, Terminal Function(Platform) terminal, Platform platform, FileSystem fileSystem, ProcessManager processManager, Logger logger, PrintFn usagePrintFn, bool featureEnabled = false }) { platform ??= FakePlatform(); processManager ??= FakeProcessManager.any(); fileSystem ??= MemoryFileSystem.test(); usagePrintFn ??= print; logger ??= BufferLogger.test(); return CustomDevicesCommand.test( customDevicesConfig: config != null ? config(fileSystem, logger) : CustomDevicesConfig.test( platform: platform, fileSystem: fileSystem, directory: fileSystem.directory('/'), logger: logger ), operatingSystemUtils: FakeOperatingSystemUtils( hostPlatform: platform.isLinux ? HostPlatform.linux_x64 : platform.isWindows ? HostPlatform.windows_x64 : platform.isMacOS ? HostPlatform.darwin_x64 : throw FallThroughError() ), terminal: terminal != null ? terminal(platform) : FakeTerminal(platform: platform), platform: platform, featureFlags: TestFeatureFlags(areCustomDevicesEnabled: featureEnabled), processManager: processManager, fileSystem: fileSystem, logger: logger, usagePrintFn: usagePrintFn, ); } /// May take platform, logger, processManager and fileSystem from context if /// not explicitly specified. CommandRunner<void> createCustomDevicesCommandRunner({ CustomDevicesConfig Function(FileSystem, Logger) config, Terminal Function(Platform) terminal, Platform platform, FileSystem fileSystem, ProcessManager processManager, Logger logger, PrintFn usagePrintFn, bool featureEnabled = false, }) { platform ??= FakePlatform(); fileSystem ??= MemoryFileSystem.test(); logger ??= BufferLogger.test(); return FakeCommandRunner( platform: platform, fileSystem: fileSystem, logger: logger )..addCommand( createCustomDevicesCommand( config: config, terminal: terminal, platform: platform, fileSystem: fileSystem, processManager: processManager, logger: logger, usagePrintFn: usagePrintFn, featureEnabled: featureEnabled ) ); } FakeTerminal createFakeTerminalForAddingSshDevice({ @required Platform platform, @required String id, @required String label, @required String sdkNameAndVersion, @required String enabled, @required String hostname, @required String username, @required String runDebug, @required String usePortForwarding, @required String screenshot, @required String apply }) { return FakeTerminal(platform: platform) ..simulateStdin(id) ..simulateStdin(label) ..simulateStdin(sdkNameAndVersion) ..simulateStdin(enabled) ..simulateStdin(hostname) ..simulateStdin(username) ..simulateStdin(runDebug) ..simulateStdin(usePortForwarding) ..simulateStdin(screenshot) ..simulateStdin(apply); } void main() { const String featureNotEnabledMessage = 'Custom devices feature must be enabled. Enable using `flutter config --enable-custom-devices`.'; setUpAll(() { Cache.disableLocking(); }); group('linux', () { setUp(() { Cache.flutterRoot = linuxFlutterRoot; }); testUsingContext( 'custom-devices command shows config file in help when feature is enabled', () async { final BufferLogger logger = BufferLogger.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( logger: logger, usagePrintFn: (Object o) => logger.printStatus(o.toString()), featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', '--help']), completes ); expect( logger.statusText, contains('Makes changes to the config file at "/.flutter_custom_devices.json".') ); } ); testUsingContext( 'running custom-devices command without arguments prints usage', () async { final BufferLogger logger = BufferLogger.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( logger: logger, usagePrintFn: (Object o) => logger.printStatus(o.toString()), featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices']), completes ); expect( logger.statusText, contains('Makes changes to the config file at "/.flutter_custom_devices.json".') ); } ); // test behaviour with disabled feature testUsingContext( 'custom-devices add command fails when feature is not enabled', () async { final CommandRunner<void> runner = createCustomDevicesCommandRunner(); expect( runner.run(const <String>['custom-devices', 'add']), throwsToolExit(message: featureNotEnabledMessage), ); } ); testUsingContext( 'custom-devices delete command fails when feature is not enabled', () async { final CommandRunner<void> runner = createCustomDevicesCommandRunner(); expect( runner.run(const <String>['custom-devices', 'delete', '-d', 'testid']), throwsToolExit(message: featureNotEnabledMessage), ); } ); testUsingContext( 'custom-devices list command fails when feature is not enabled', () async { final CommandRunner<void> runner = createCustomDevicesCommandRunner(); expect( runner.run(const <String>['custom-devices', 'list']), throwsToolExit(message: featureNotEnabledMessage), ); } ); testUsingContext( 'custom-devices reset command fails when feature is not enabled', () async { final CommandRunner<void> runner = createCustomDevicesCommandRunner(); expect( runner.run(const <String>['custom-devices', 'reset']), throwsToolExit(message: featureNotEnabledMessage), ); } ); // test add command testUsingContext( 'custom-devices add command correctly adds ssh device config on linux', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( terminal: (Platform platform) => createFakeTerminalForAddingSshDevice( platform: platform, id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: 'y', hostname: 'testhostname', username: 'testuser', runDebug: 'testrundebug', usePortForwarding: 'y', screenshot: 'testscreenshot', apply: 'y' ), fileSystem: fs, processManager: FakeProcessManager.any(), featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'add', '--no-check']), completes ); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test() ); expect( config.devices, contains( CustomDeviceConfig( id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: true, pingCommand: const <String>[ 'ping', '-c', '1', '-w', '1', 'testhostname' ], postBuildCommand: null, // ignore: avoid_redundant_argument_values installCommand: const <String>[ 'scp', '-r', '-o', 'BatchMode=yes', r'${localPath}', r'testuser@testhostname:/tmp/${appName}' ], uninstallCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', r'rm -rf "/tmp/${appName}"' ], runDebugCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testrundebug' ], forwardPortCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'testuser@testhostname', "echo 'Port forwarding success'; read" ], forwardPortSuccessRegex: RegExp('Port forwarding success'), screenshotCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testscreenshot' ], ) ) ); } ); testUsingContext( 'custom-devices add command correctly adds ipv4 ssh device config', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( terminal: (Platform platform) => createFakeTerminalForAddingSshDevice( platform: platform, id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: 'y', hostname: '192.168.178.1', username: 'testuser', runDebug: 'testrundebug', usePortForwarding: 'y', screenshot: 'testscreenshot', apply: 'y', ), processManager: FakeProcessManager.any(), fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'add', '--no-check']), completes ); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test() ); expect( config.devices, contains( CustomDeviceConfig( id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: true, pingCommand: const <String>[ 'ping', '-c', '1', '-w', '1', '192.168.178.1' ], postBuildCommand: null, // ignore: avoid_redundant_argument_values installCommand: const <String>[ 'scp', '-r', '-o', 'BatchMode=yes', r'${localPath}', r'testuser@192.168.178.1:/tmp/${appName}' ], uninstallCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@192.168.178.1', r'rm -rf "/tmp/${appName}"' ], runDebugCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@192.168.178.1', 'testrundebug' ], forwardPortCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'testuser@192.168.178.1', "echo 'Port forwarding success'; read" ], forwardPortSuccessRegex: RegExp('Port forwarding success'), screenshotCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@192.168.178.1', 'testscreenshot' ] ) ) ); } ); testUsingContext( 'custom-devices add command correctly adds ipv6 ssh device config', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( terminal: (Platform platform) => createFakeTerminalForAddingSshDevice( platform: platform, id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: 'y', hostname: '::1', username: 'testuser', runDebug: 'testrundebug', usePortForwarding: 'y', screenshot: 'testscreenshot', apply: 'y', ), fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'add', '--no-check']), completes ); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test() ); expect( config.devices, contains( CustomDeviceConfig( id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: true, pingCommand: const <String>[ 'ping', '-6', '-c', '1', '-w', '1', '::1' ], postBuildCommand: null, // ignore: avoid_redundant_argument_values installCommand: const <String>[ 'scp', '-r', '-o', 'BatchMode=yes', '-6', r'${localPath}', r'testuser@[::1]:/tmp/${appName}' ], uninstallCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-6', 'testuser@[::1]', r'rm -rf "/tmp/${appName}"' ], runDebugCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-6', 'testuser@[::1]', 'testrundebug' ], forwardPortCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-o', 'ExitOnForwardFailure=yes', '-6', '-L', r'[::1]:${hostPort}:[::1]:${devicePort}', 'testuser@[::1]', "echo 'Port forwarding success'; read" ], forwardPortSuccessRegex: RegExp('Port forwarding success'), screenshotCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-6', 'testuser@[::1]', 'testscreenshot' ] ) ) ); } ); testUsingContext( 'custom-devices add command correctly adds non-forwarding ssh device config', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( terminal: (Platform platform) => createFakeTerminalForAddingSshDevice( platform: platform, id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: 'y', hostname: 'testhostname', username: 'testuser', runDebug: 'testrundebug', usePortForwarding: 'n', screenshot: 'testscreenshot', apply: 'y', ), fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'add', '--no-check']), completes ); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test() ); expect( config.devices, contains( const CustomDeviceConfig( id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: true, pingCommand: <String>[ 'ping', '-c', '1', '-w', '1', 'testhostname' ], postBuildCommand: null, // ignore: avoid_redundant_argument_values installCommand: <String>[ 'scp', '-r', '-o', 'BatchMode=yes', r'${localPath}', r'testuser@testhostname:/tmp/${appName}' ], uninstallCommand: <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', r'rm -rf "/tmp/${appName}"' ], runDebugCommand: <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testrundebug' ], screenshotCommand: <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testscreenshot' ] ) ) ); } ); testUsingContext( 'custom-devices add command correctly adds non-screenshotting ssh device config', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( terminal: (Platform platform) => createFakeTerminalForAddingSshDevice( platform: platform, id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: 'y', hostname: 'testhostname', username: 'testuser', runDebug: 'testrundebug', usePortForwarding: 'y', screenshot: '', apply: 'y', ), fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'add', '--no-check']), completes ); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test() ); expect( config.devices, contains( CustomDeviceConfig( id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: true, pingCommand: const <String>[ 'ping', '-c', '1', '-w', '1', 'testhostname' ], postBuildCommand: null, // ignore: avoid_redundant_argument_values installCommand: const <String>[ 'scp', '-r', '-o', 'BatchMode=yes', r'${localPath}', r'testuser@testhostname:/tmp/${appName}' ], uninstallCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', r'rm -rf "/tmp/${appName}"' ], runDebugCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testrundebug' ], forwardPortCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'testuser@testhostname', "echo 'Port forwarding success'; read" ], forwardPortSuccessRegex: RegExp('Port forwarding success'), ) ) ); } ); testUsingContext( 'custom-devices delete command deletes device and creates backup', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test(), ); config.add(CustomDeviceConfig.exampleUnix.copyWith(id: 'testid')); final CommandRunner<void> runner = createCustomDevicesCommandRunner( config: (_, __) => config, fileSystem: fs, featureEnabled: true ); final Uint8List contentsBefore = fs.file('.flutter_custom_devices.json').readAsBytesSync(); await expectLater( runner.run(const <String>['custom-devices', 'delete', '-d', 'testid']), completes ); expect(fs.file('/.flutter_custom_devices.json.bak'), exists); expect(config.devices, hasLength(0)); final Uint8List backupContents = fs.file('.flutter_custom_devices.json.bak').readAsBytesSync(); expect(contentsBefore, equals(backupContents)); } ); testUsingContext( 'custom-devices delete command without device argument throws tool exit', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test(), ); config.add(CustomDeviceConfig.exampleUnix.copyWith(id: 'testid2')); final Uint8List contentsBefore = fs.file('.flutter_custom_devices.json').readAsBytesSync(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'delete']), throwsToolExit() ); final Uint8List contentsAfter = fs.file('.flutter_custom_devices.json').readAsBytesSync(); expect(contentsBefore, equals(contentsAfter)); expect(fs.file('.flutter_custom_devices.json.bak').existsSync(), isFalse); } ); testUsingContext( 'custom-devices delete command throws tool exit with invalid device id', () async { final CommandRunner<void> runner = createCustomDevicesCommandRunner( featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'delete', '-d', 'testid']), throwsToolExit(message: 'Couldn\'t find device with id "testid" in config at "/.flutter_custom_devices.json"') ); } ); testUsingContext( 'custom-devices list command throws tool exit when config contains errors', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final BufferLogger logger = BufferLogger.test(); fs.file('.flutter_custom_devices.json').writeAsStringSync('{"custom-devices": {}}'); final CommandRunner<void> runner = createCustomDevicesCommandRunner( fileSystem: fs, logger: logger, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'list']), throwsToolExit(message: 'Could not list custom devices.') ); expect( logger.errorText, contains("Could not load custom devices config. config['custom-devices'] is not a JSON array.") ); } ); testUsingContext( 'custom-devices list command prints message when no devices found', () async { final BufferLogger logger = BufferLogger.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( logger: logger, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'list']), completes ); expect( logger.statusText, contains('No custom devices found in "/.flutter_custom_devices.json"') ); } ); testUsingContext( 'custom-devices list command lists all devices', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final BufferLogger logger = BufferLogger.test(); CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: logger, )..add( CustomDeviceConfig.exampleUnix.copyWith(id: 'testid', label: 'testlabel', enabled: true) )..add( CustomDeviceConfig.exampleUnix.copyWith(id: 'testid2', label: 'testlabel2', enabled: false) ); final CommandRunner<void> runner = createCustomDevicesCommandRunner( logger: logger, fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'list']), completes ); expect( logger.statusText, contains('List of custom devices in "/.flutter_custom_devices.json":') ); expect( logger.statusText, contains('id: testid, label: testlabel, enabled: true') ); expect( logger.statusText, contains('id: testid2, label: testlabel2, enabled: false') ); } ); testUsingContext( 'custom-devices reset correctly backs up the config file', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final BufferLogger logger = BufferLogger.test(); CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: logger, )..add( CustomDeviceConfig.exampleUnix.copyWith(id: 'testid', label: 'testlabel', enabled: true) )..add( CustomDeviceConfig.exampleUnix.copyWith(id: 'testid2', label: 'testlabel2', enabled: false) ); final Uint8List contentsBefore = fs.file('.flutter_custom_devices.json').readAsBytesSync(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( logger: logger, fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'reset']), completes ); expect( logger.statusText, contains( 'Successfully resetted the custom devices config file and created a ' 'backup at "/.flutter_custom_devices.json.bak".' ) ); final Uint8List backupContents = fs.file('.flutter_custom_devices.json.bak').readAsBytesSync(); expect(contentsBefore, equals(backupContents)); expect( fs.file('.flutter_custom_devices.json').readAsStringSync(), anyOf(equals(defaultConfigLinux1), equals(defaultConfigLinux2)) ); } ); testUsingContext( "custom-devices reset outputs correct msg when config file didn't exist", () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final BufferLogger logger = BufferLogger.test(); final CommandRunner<void> runner = createCustomDevicesCommandRunner( logger: logger, fileSystem: fs, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'reset']), completes ); expect( logger.statusText, contains( 'Successfully resetted the custom devices config file.' ) ); expect(fs.file('.flutter_custom_devices.json.bak'), isNot(exists)); expect( fs.file('.flutter_custom_devices.json').readAsStringSync(), anyOf(equals(defaultConfigLinux1), equals(defaultConfigLinux2)) ); } ); }); group('windows', () { setUp(() { Cache.flutterRoot = windowsFlutterRoot; }); testUsingContext( 'custom-devices add command correctly adds ssh device config on windows', () async { final MemoryFileSystem fs = MemoryFileSystem.test(style: FileSystemStyle.windows); final CommandRunner<void> runner = createCustomDevicesCommandRunner( terminal: (Platform platform) => createFakeTerminalForAddingSshDevice( platform: platform, id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: 'y', hostname: 'testhostname', username: 'testuser', runDebug: 'testrundebug', usePortForwarding: 'y', screenshot: 'testscreenshot', apply: 'y', ), fileSystem: fs, platform: windowsPlatform, featureEnabled: true ); await expectLater( runner.run(const <String>['custom-devices', 'add', '--no-check']), completes ); final CustomDevicesConfig config = CustomDevicesConfig.test( fileSystem: fs, directory: fs.directory('/'), logger: BufferLogger.test() ); expect( config.devices, contains( CustomDeviceConfig( id: 'testid', label: 'testlabel', sdkNameAndVersion: 'testsdknameandversion', enabled: true, pingCommand: const <String>[ 'ping', '-n', '1', '-w', '500', 'testhostname' ], pingSuccessRegex: RegExp(r'[<=]\d+ms'), postBuildCommand: null, // ignore: avoid_redundant_argument_values installCommand: const <String>[ 'scp', '-r', '-o', 'BatchMode=yes', r'${localPath}', r'testuser@testhostname:/tmp/${appName}' ], uninstallCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', r'rm -rf "/tmp/${appName}"' ], runDebugCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testrundebug' ], forwardPortCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'testuser@testhostname', "echo 'Port forwarding success'; read" ], forwardPortSuccessRegex: RegExp('Port forwarding success'), screenshotCommand: const <String>[ 'ssh', '-o', 'BatchMode=yes', 'testuser@testhostname', 'testscreenshot' ] ) ) ); }, ); }); }