Unverified Commit d6806392 authored by Kirill Pertsev's avatar Kirill Pertsev Committed by GitHub

Implements --machine flag for `devices` command (#50581)

parent 74a1b9b3
......@@ -6,13 +6,21 @@ import 'dart:async';
import '../base/common.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../device.dart';
import '../doctor.dart';
import '../globals.dart' as globals;
import '../runner/flutter_command.dart';
class DevicesCommand extends FlutterCommand {
DevicesCommand() {
argParser.addFlag('machine',
negatable: false,
help: 'Output device information in machine readable structured JSON format',
);
argParser.addOption(
'timeout',
abbr: 't',
......@@ -73,6 +81,8 @@ class DevicesCommand extends FlutterCommand {
globals.printStatus('• $diagnostic', hangingIndent: 2);
}
}
} else if (boolArg('machine')) {
await printDevicesAsJson(devices);
} else {
globals.printStatus('${devices.length} connected ${pluralize('device', devices.length)}:\n');
await Device.printDevices(devices);
......@@ -80,4 +90,13 @@ class DevicesCommand extends FlutterCommand {
return FlutterCommandResult.success();
}
Future<void> printDevicesAsJson(List<Device> devices) async {
globals.printStatus(
const JsonEncoder.withIndent(' ').convert(
await Future.wait(devices.map((Device d) => d.toJson()))
)
);
}
}
......@@ -538,6 +538,28 @@ abstract class Device {
await descriptions(devices).forEach(globals.printStatus);
}
/// Convert the Device object to a JSON representation suitable for serialization.
Future<Map<String, Object>> toJson() async {
final bool isLocalEmu = await isLocalEmulator;
return <String, Object>{
'name': name,
'id': id,
'isSupported': isSupported(),
'targetPlatform': getNameForTargetPlatform(await targetPlatform),
'emulator': isLocalEmu,
'sdk': await sdkNameAndVersion,
'capabilities': <String, Object>{
'hotReload': supportsHotReload,
'hotRestart': supportsHotRestart,
'screenshot': supportsScreenshot,
'fastStart': supportsFastStart,
'flutterExit': supportsFlutterExit,
'hardwareRendering': isLocalEmu && await supportsHardwareRendering,
'startPaused': supportsStartPaused,
}
};
}
/// Clean up resources allocated by device
///
/// For example log readers or port forwarders.
......
......@@ -15,6 +15,7 @@ import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
void main() {
group('devices', () {
......@@ -36,6 +37,53 @@ void main() {
DeviceManager: () => DeviceManager(),
ProcessManager: () => MockProcessManager(),
});
testUsingContext('Outputs parsable JSON with --machine flag', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices', '--machine']);
expect(
json.decode(testLogger.statusText),
<Map<String,Object>>[
<String, Object>{
'name': 'ephemeral',
'id': 'ephemeral',
'isSupported': true,
'targetPlatform': 'android-arm',
'emulator': true,
'sdk': 'Test SDK (1.2.3)',
'capabilities': <String, Object>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': true,
'startPaused': true
}
},
<String,Object>{
'name': 'webby',
'id': 'webby',
'isSupported': true,
'targetPlatform': 'web-javascript',
'emulator': true,
'sdk': 'Web SDK (1.2.4)',
'capabilities': <String, Object>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': false,
'startPaused': true
}
}
]
);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(),
ProcessManager: () => MockProcessManager(),
});
});
}
......@@ -66,3 +114,16 @@ class MockProcessManager extends Mock implements ProcessManager {
return ProcessResult(0, 0, '', '');
}
}
class _FakeDeviceManager extends DeviceManager {
_FakeDeviceManager();
@override
Future<List<Device>> getAllConnectedDevices() =>
Future<List<Device>>.value(fakeDevices.map((FakeDeviceJsonData d) => d.dev).toList());
@override
Future<List<Device>> refreshAllConnectedDevices({Duration timeout}) =>
getAllConnectedDevices();
}
// 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:convert';
import 'package:file/file.dart';
import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/web/chrome.dart';
import 'package:flutter_tools/src/commands/devices.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/base/context.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group('devices', () {
Directory configDir;
Config config;
tearDown(() {
if (configDir != null) {
tryToDelete(configDir);
configDir = null;
}
});
setUpAll(() {
configDir ??= globals.fs.systemTempDirectory.createTempSync(
'flutter_config_dir_test.',
);
config = Config.test(
Config.kFlutterSettings,
directory: configDir,
logger: globals.logger,
)..setValue('enable-web', true);
});
// Test assumes no devices connected.
// Should return only `web-server` device
testUsingContext('Test the --machine flag', () async {
final BufferLogger logger = context.get<Logger>() as BufferLogger;
final DevicesCommand command = DevicesCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['devices', '--machine']);
expect(
json.decode(logger.statusText),
<Map<String,Object>>[
<String, Object>{
'name': 'Web Server',
'id': 'web-server',
'isSupported': true,
'targetPlatform': 'web-javascript',
'emulator': false,
'sdk': 'Flutter Tools',
'capabilities': <String, Object>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': false,
'startPaused': true
}
}
]
);
},
overrides: <Type, Generator>{
DeviceManager: () => DeviceManager(),
Config: () => config,
ChromeLauncher: () => _DisabledChromeLauncher(),
});
});
}
// Without ChromeLauncher DeviceManager constructor fails with noSuchMethodError
// trying to call canFindChrome on null
// Also, Chrome may have different versions on different machines and
// JSON will not match, because the `sdk` field of the Device contains version number
// Mock the launcher to make it appear that we don't have Chrome.
class _DisabledChromeLauncher implements ChromeLauncher {
@override
bool canFindChrome() => false;
@override
Future<Chrome> launch(String url, {bool headless = false, int debugPort, bool skipCheck = false, Directory cacheDir})
=> Future<Chrome>.error('Chrome disabled');
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ import 'package:mockito/mockito.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fake_devices.dart';
import '../src/mocks.dart';
void main() {
......@@ -24,9 +25,9 @@ void main() {
});
testUsingContext('getDeviceById', () async {
final _MockDevice device1 = _MockDevice('Nexus 5', '0553790d0a4e726f');
final _MockDevice device2 = _MockDevice('Nexus 5X', '01abfc49119c410e');
final _MockDevice device3 = _MockDevice('iPod touch', '82564b38861a9a5');
final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f');
final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e');
final FakeDevice device3 = FakeDevice('iPod touch', '82564b38861a9a5');
final List<Device> devices = <Device>[device1, device2, device3];
final DeviceManager deviceManager = TestDeviceManager(devices);
......@@ -42,42 +43,42 @@ void main() {
});
testUsingContext('getAllConnectedDevices caches', () async {
final _MockDevice device1 = _MockDevice('Nexus 5', '0553790d0a4e726f');
final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f');
final TestDeviceManager deviceManager = TestDeviceManager(<Device>[device1]);
expect(await deviceManager.getAllConnectedDevices(), <Device>[device1]);
final _MockDevice device2 = _MockDevice('Nexus 5X', '01abfc49119c410e');
final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e');
deviceManager.resetDevices(<Device>[device2]);
expect(await deviceManager.getAllConnectedDevices(), <Device>[device1]);
});
testUsingContext('refreshAllConnectedDevices does not cache', () async {
final _MockDevice device1 = _MockDevice('Nexus 5', '0553790d0a4e726f');
final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f');
final TestDeviceManager deviceManager = TestDeviceManager(<Device>[device1]);
expect(await deviceManager.refreshAllConnectedDevices(), <Device>[device1]);
final _MockDevice device2 = _MockDevice('Nexus 5X', '01abfc49119c410e');
final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e');
deviceManager.resetDevices(<Device>[device2]);
expect(await deviceManager.refreshAllConnectedDevices(), <Device>[device2]);
});
});
group('Filter devices', () {
_MockDevice ephemeral;
_MockDevice nonEphemeralOne;
_MockDevice nonEphemeralTwo;
_MockDevice unsupported;
_MockDevice webDevice;
_MockDevice fuchsiaDevice;
FakeDevice ephemeral;
FakeDevice nonEphemeralOne;
FakeDevice nonEphemeralTwo;
FakeDevice unsupported;
FakeDevice webDevice;
FakeDevice fuchsiaDevice;
setUp(() {
ephemeral = _MockDevice('ephemeral', 'ephemeral', true);
nonEphemeralOne = _MockDevice('nonEphemeralOne', 'nonEphemeralOne', false);
nonEphemeralTwo = _MockDevice('nonEphemeralTwo', 'nonEphemeralTwo', false);
unsupported = _MockDevice('unsupported', 'unsupported', true, false);
webDevice = _MockDevice('webby', 'webby')
ephemeral = FakeDevice('ephemeral', 'ephemeral', true);
nonEphemeralOne = FakeDevice('nonEphemeralOne', 'nonEphemeralOne', false);
nonEphemeralTwo = FakeDevice('nonEphemeralTwo', 'nonEphemeralTwo', false);
unsupported = FakeDevice('unsupported', 'unsupported', true, false);
webDevice = FakeDevice('webby', 'webby')
..targetPlatform = Future<TargetPlatform>.value(TargetPlatform.web_javascript);
fuchsiaDevice = _MockDevice('fuchsiay', 'fuchsiay')
fuchsiaDevice = FakeDevice('fuchsiay', 'fuchsiay')
..targetPlatform = Future<TargetPlatform>.value(TargetPlatform.fuchsia_x64);
});
......@@ -182,6 +183,17 @@ void main() {
});
});
});
group('JSON encode devices', () {
testUsingContext('Consistency of JSON representation', () async {
expect(
// This tests that fakeDevices is a list of tuples where "second" is the
// correct JSON representation of the "first". Actual values are irrelevant
await Future.wait(fakeDevices.map((FakeDeviceJsonData d) => d.dev.toJson())),
fakeDevices.map((FakeDeviceJsonData d) => d.json)
);
});
});
}
class TestDeviceManager extends DeviceManager {
......@@ -208,27 +220,4 @@ class TestDeviceManager extends DeviceManager {
}
}
class _MockDevice extends Device {
_MockDevice(this.name, String id, [bool ephemeral = true, this._isSupported = true]) : super(
id,
platformType: PlatformType.web,
category: Category.mobile,
ephemeral: ephemeral,
);
final bool _isSupported;
@override
final String name;
@override
Future<TargetPlatform> targetPlatform = Future<TargetPlatform>.value(TargetPlatform.android_arm);
@override
void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
@override
bool isSupportedForProject(FlutterProject flutterProject) => _isSupported;
}
class MockProcess extends Mock implements Process {}
// 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 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/project.dart';
/// A list of fake devices to test JSON serialization
/// (`Device.toJson()` and `--machine` flag for `devices` command)
List<FakeDeviceJsonData> fakeDevices = <FakeDeviceJsonData>[
FakeDeviceJsonData(
FakeDevice('ephemeral', 'ephemeral', true),
<String, Object>{
'name': 'ephemeral',
'id': 'ephemeral',
'isSupported': true,
'targetPlatform': 'android-arm',
'emulator': true,
'sdk': 'Test SDK (1.2.3)',
'capabilities': <String, Object>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': true,
'startPaused': true
}
}
),
FakeDeviceJsonData(
FakeDevice('webby', 'webby')
..targetPlatform = Future<TargetPlatform>.value(TargetPlatform.web_javascript)
..sdkNameAndVersion = Future<String>.value('Web SDK (1.2.4)'),
<String,Object>{
'name': 'webby',
'id': 'webby',
'isSupported': true,
'targetPlatform': 'web-javascript',
'emulator': true,
'sdk': 'Web SDK (1.2.4)',
'capabilities': <String, Object>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': false,
'startPaused': true
}
}
),
];
/// Fake device to test `devices` command
class FakeDevice extends Device {
FakeDevice(this.name, String id, [bool ephemeral = true, this._isSupported = true]) : super(
id,
platformType: PlatformType.web,
category: Category.mobile,
ephemeral: ephemeral,
);
final bool _isSupported;
@override
final String name;
@override
Future<TargetPlatform> targetPlatform = Future<TargetPlatform>.value(TargetPlatform.android_arm);
@override
void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
@override
bool isSupportedForProject(FlutterProject flutterProject) => _isSupported;
@override
bool isSupported() => _isSupported;
@override
Future<bool> isLocalEmulator = Future<bool>.value(true);
@override
Future<String> sdkNameAndVersion = Future<String>.value('Test SDK (1.2.3)');
}
/// Combines fake device with its canonical JSON representation
class FakeDeviceJsonData {
FakeDeviceJsonData(this.dev, this.json);
final FakeDevice dev;
final Map<String, Object> json;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment