Commit 5a21ef24 authored by Devon Carew's avatar Devon Carew

Merge pull request #1504 from devoncarew/flutter_logs

refactor flutter logs
parents 3abb8c4e 78e05884
...@@ -294,6 +294,7 @@ class AndroidDevice extends Device { ...@@ -294,6 +294,7 @@ class AndroidDevice extends Device {
String mainPath, String mainPath,
String route, String route,
bool checked: true, bool checked: true,
bool clearLogs: false,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}) { }) {
return flx.buildInTempDir( return flx.buildInTempDir(
...@@ -309,7 +310,7 @@ class AndroidDevice extends Device { ...@@ -309,7 +310,7 @@ class AndroidDevice extends Device {
checked: checked, checked: checked,
traceStartup: platformArgs['trace-startup'], traceStartup: platformArgs['trace-startup'],
route: route, route: route,
clearLogs: platformArgs['clear-logs'] clearLogs: clearLogs
)) { )) {
return true; return true;
} else { } else {
...@@ -334,26 +335,7 @@ class AndroidDevice extends Device { ...@@ -334,26 +335,7 @@ class AndroidDevice extends Device {
runSync(adbCommandForDevice(['logcat', '-c'])); runSync(adbCommandForDevice(['logcat', '-c']));
} }
Future<int> logs({bool clear: false}) async { DeviceLogReader createLogReader() => new _AdbLogReader(this);
if (!isConnected()) {
return 2;
}
if (clear) {
clearLogs();
}
return await runCommandAndStreamOutput(adbCommandForDevice([
'logcat',
'-v',
'tag', // Only log the tag and the message
'-s',
'flutter:V',
'ActivityManager:W',
'System.err:W',
'*:F',
]), prefix: 'android: ');
}
void startTracing(AndroidApk apk) { void startTracing(AndroidApk apk) {
runCheckedSync(adbCommandForDevice([ runCheckedSync(adbCommandForDevice([
...@@ -529,3 +511,41 @@ String getAdbPath() { ...@@ -529,3 +511,41 @@ String getAdbPath() {
return _defaultAdbPath; return _defaultAdbPath;
} }
} }
/// A log reader that logs from `adb logcat`. This will have the same output as
/// another copy of [_AdbLogReader], and the two instances will be equivalent.
class _AdbLogReader extends DeviceLogReader {
_AdbLogReader(this.device);
final AndroidDevice device;
String get name => 'Android';
Future<int> logs({bool clear: false}) async {
if (!device.isConnected())
return 2;
if (clear)
device.clearLogs();
return await runCommandAndStreamOutput(device.adbCommandForDevice(<String>[
'logcat',
'-v',
'tag', // Only log the tag and the message
'-s',
'flutter:V',
'ActivityManager:W',
'System.err:W',
'*:F',
]), prefix: '[Android] ');
}
// Intentionally constant; overridden because we've overridden the `operator ==` method below.
int get hashCode => name.hashCode;
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
return other is _AdbLogReader;
}
}
...@@ -8,12 +8,15 @@ import 'dart:io'; ...@@ -8,12 +8,15 @@ import 'dart:io';
import 'context.dart'; import 'context.dart';
typedef String StringConverter(String string);
/// This runs the command and streams stdout/stderr from the child process to /// This runs the command and streams stdout/stderr from the child process to
/// this process' stdout/stderr. /// this process' stdout/stderr.
Future<int> runCommandAndStreamOutput(List<String> cmd, { Future<int> runCommandAndStreamOutput(List<String> cmd, {
String workingDirectory,
String prefix: '', String prefix: '',
RegExp filter, RegExp filter,
String workingDirectory StringConverter mapFunction
}) async { }) async {
printTrace(cmd.join(' ')); printTrace(cmd.join(' '));
Process process = await Process.start( Process process = await Process.start(
...@@ -26,14 +29,20 @@ Future<int> runCommandAndStreamOutput(List<String> cmd, { ...@@ -26,14 +29,20 @@ Future<int> runCommandAndStreamOutput(List<String> cmd, {
.transform(const LineSplitter()) .transform(const LineSplitter())
.where((String line) => filter == null ? true : filter.hasMatch(line)) .where((String line) => filter == null ? true : filter.hasMatch(line))
.listen((String line) { .listen((String line) {
printStatus('$prefix$line'); if (mapFunction != null)
line = mapFunction(line);
if (line != null)
printStatus('$prefix$line');
}); });
process.stderr process.stderr
.transform(UTF8.decoder) .transform(UTF8.decoder)
.transform(const LineSplitter()) .transform(const LineSplitter())
.where((String line) => filter == null ? true : filter.hasMatch(line)) .where((String line) => filter == null ? true : filter.hasMatch(line))
.listen((String line) { .listen((String line) {
printError('$prefix$line'); if (mapFunction != null)
line = mapFunction(line);
if (line != null)
printError('$prefix$line');
}); });
return await process.exitCode; return await process.exitCode;
} }
...@@ -57,7 +66,7 @@ Future<Process> runDetached(List<String> cmd) { ...@@ -57,7 +66,7 @@ Future<Process> runDetached(List<String> cmd) {
/// Run cmd and return stdout. /// Run cmd and return stdout.
/// Throws an error if cmd exits with a non-zero value. /// Throws an error if cmd exits with a non-zero value.
String runCheckedSync(List<String> cmd, { String workingDirectory }) { String runCheckedSync(List<String> cmd, { String workingDirectory }) {
return _runWithLoggingSync(cmd, workingDirectory: workingDirectory, checked: true); return _runWithLoggingSync(cmd, workingDirectory: workingDirectory, checked: true, noisyErrors: true);
} }
/// Run cmd and return stdout. /// Run cmd and return stdout.
...@@ -73,6 +82,7 @@ String sdkBinaryName(String name) { ...@@ -73,6 +82,7 @@ String sdkBinaryName(String name) {
String _runWithLoggingSync(List<String> cmd, { String _runWithLoggingSync(List<String> cmd, {
bool checked: false, bool checked: false,
bool noisyErrors: false,
String workingDirectory String workingDirectory
}) { }) {
printTrace(cmd.join(' ')); printTrace(cmd.join(' '));
...@@ -82,8 +92,13 @@ String _runWithLoggingSync(List<String> cmd, { ...@@ -82,8 +92,13 @@ String _runWithLoggingSync(List<String> cmd, {
String errorDescription = 'Error code ${results.exitCode} ' String errorDescription = 'Error code ${results.exitCode} '
'returned when attempting to run command: ${cmd.join(' ')}'; 'returned when attempting to run command: ${cmd.join(' ')}';
printTrace(errorDescription); printTrace(errorDescription);
if (results.stderr.length > 0) if (results.stderr.length > 0) {
printTrace('Errors logged: ${results.stderr.trim()}'); if (noisyErrors) {
printError(results.stderr.trim());
} else {
printTrace('Errors logged: ${results.stderr.trim()}');
}
}
if (checked) if (checked)
throw errorDescription; throw errorDescription;
} }
......
...@@ -17,8 +17,6 @@ class IOSCommand extends FlutterCommand { ...@@ -17,8 +17,6 @@ class IOSCommand extends FlutterCommand {
final String name = "ios"; final String name = "ios";
final String description = "Commands for creating and updating Flutter iOS projects."; final String description = "Commands for creating and updating Flutter iOS projects.";
final bool requiresProjectRoot = true;
IOSCommand() { IOSCommand() {
argParser.addFlag('init', help: 'Initialize the Xcode project for building the iOS application'); argParser.addFlag('init', help: 'Initialize the Xcode project for building the iOS application');
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import '../base/context.dart';
import '../device.dart'; import '../device.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
...@@ -13,25 +14,52 @@ class LogsCommand extends FlutterCommand { ...@@ -13,25 +14,52 @@ class LogsCommand extends FlutterCommand {
LogsCommand() { LogsCommand() {
argParser.addFlag('clear', argParser.addFlag('clear',
negatable: false, negatable: false,
abbr: 'c', abbr: 'c',
help: 'Clear log history before reading from logs (Android only).'); help: 'Clear log history before reading from logs.'
);
} }
bool get requiresProjectRoot => false; bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async { Future<int> runInProject() async {
connectToDevices(); DeviceManager deviceManager = new DeviceManager();
List<Device> devices;
String deviceId = globalResults['device-id'];
if (deviceId != null) {
Device device = await deviceManager.getDeviceById(deviceId);
if (device == null) {
printError("No device found with id '$deviceId'.");
return 1;
}
devices = <Device>[device];
} else {
devices = await deviceManager.getDevices();
}
if (devices.isEmpty) {
printStatus('No connected devices.');
return 0;
}
bool clear = argResults['clear']; bool clear = argResults['clear'];
Iterable<Future<int>> results = devices.all.map( Set<DeviceLogReader> readers = new Set<DeviceLogReader>();
(Device device) => device.logs(clear: clear)); for (Device device in devices) {
readers.add(device.createLogReader());
}
printStatus('Logging for ${readers.join(', ')}...');
for (Future<int> result in results) List<int> results = await Future.wait(readers.map((DeviceLogReader reader) async {
await result; int result = await reader.logs(clear: clear);
if (result != 0)
printError('Error listening to $reader logs.');
return result;
}));
return 0; // If all readers failed, return an error.
return results.every((int result) => result != 0) ? 1 : 0;
} }
} }
...@@ -134,8 +134,6 @@ Future<int> startApp( ...@@ -134,8 +134,6 @@ Future<int> startApp(
if (traceStartup != null) if (traceStartup != null)
platformArgs['trace-startup'] = traceStartup; platformArgs['trace-startup'] = traceStartup;
if (clearLogs != null)
platformArgs['clear-logs'] = clearLogs;
printStatus('Starting ${_getDisplayPath(mainPath)} on ${device.name}...'); printStatus('Starting ${_getDisplayPath(mainPath)} on ${device.name}...');
...@@ -145,6 +143,7 @@ Future<int> startApp( ...@@ -145,6 +143,7 @@ Future<int> startApp(
mainPath: mainPath, mainPath: mainPath,
route: route, route: route,
checked: checked, checked: checked,
clearLogs: clearLogs,
platformArgs: platformArgs platformArgs: platformArgs
); );
......
...@@ -34,6 +34,19 @@ class DeviceManager { ...@@ -34,6 +34,19 @@ class DeviceManager {
Completer _initedCompleter = new Completer(); Completer _initedCompleter = new Completer();
/// Return the device with the matching ID; else, complete the Future with
/// `null`.
///
/// This does a case insentitive compare with `deviceId`.
Future<Device> getDeviceById(String deviceId) async {
deviceId = deviceId.toLowerCase();
List<Device> devices = await getDevices();
return devices.firstWhere(
(Device device) => device.id.toLowerCase() == deviceId,
orElse: () => null
);
}
Future<List<Device>> getDevices() async { Future<List<Device>> getDevices() async {
await _initedCompleter.future; await _initedCompleter.future;
...@@ -78,7 +91,7 @@ abstract class Device { ...@@ -78,7 +91,7 @@ abstract class Device {
TargetPlatform get platform; TargetPlatform get platform;
Future<int> logs({bool clear: false}); DeviceLogReader createLogReader();
/// Start an app package on the current device. /// Start an app package on the current device.
/// ///
...@@ -90,6 +103,7 @@ abstract class Device { ...@@ -90,6 +103,7 @@ abstract class Device {
String mainPath, String mainPath,
String route, String route,
bool checked: true, bool checked: true,
bool clearLogs: false,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}); });
...@@ -99,6 +113,21 @@ abstract class Device { ...@@ -99,6 +113,21 @@ abstract class Device {
String toString() => '$runtimeType $id'; String toString() => '$runtimeType $id';
} }
/// Read the log for a particular device. Subclasses must implement `hashCode`
/// and `operator ==` so that log readers that read from the same location can be
/// de-duped. For example, two Android devices will both try and log using
/// `adb logcat`; we don't want to display two identical log streams.
abstract class DeviceLogReader {
String get name;
Future<int> logs({ bool clear: false });
int get hashCode;
bool operator ==(dynamic other);
String toString() => name;
}
// TODO(devoncarew): Unify this with [DeviceManager]. // TODO(devoncarew): Unify this with [DeviceManager].
class DeviceStore { class DeviceStore {
final AndroidDevice android; final AndroidDevice android;
......
...@@ -184,9 +184,10 @@ class IOSDevice extends Device { ...@@ -184,9 +184,10 @@ class IOSDevice extends Device {
String mainPath, String mainPath,
String route, String route,
bool checked: true, bool checked: true,
bool clearLogs: false,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}) async { }) async {
// TODO: Use checked, mainPath, route // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
printTrace('Building ${app.name} for $id'); printTrace('Building ${app.name} for $id');
// Step 1: Install the precompiled application if necessary // Step 1: Install the precompiled application if necessary
...@@ -231,8 +232,7 @@ class IOSDevice extends Device { ...@@ -231,8 +232,7 @@ class IOSDevice extends Device {
return false; return false;
} }
Future<bool> pushFile( Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
ApplicationPackage app, String localFile, String targetFile) async {
if (Platform.isMacOS) { if (Platform.isMacOS) {
runSync([ runSync([
pusherPath, pusherPath,
...@@ -255,14 +255,7 @@ class IOSDevice extends Device { ...@@ -255,14 +255,7 @@ class IOSDevice extends Device {
@override @override
TargetPlatform get platform => TargetPlatform.iOS; TargetPlatform get platform => TargetPlatform.iOS;
/// Note that clear is not supported on iOS at this time. DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
Future<int> logs({bool clear: false}) async {
if (!isConnected()) {
return 2;
}
return await runCommandAndStreamOutput([loggerPath],
prefix: 'iOS: ', filter: new RegExp(r'(FlutterRunner|flutter.runner.Runner)'));
}
} }
class IOSSimulator extends Device { class IOSSimulator extends Device {
...@@ -335,10 +328,9 @@ class IOSSimulator extends Device { ...@@ -335,10 +328,9 @@ class IOSSimulator extends Device {
String _getSimulatorPath() { String _getSimulatorPath() {
String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id; String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id;
String homeDirectory = path.absolute(Platform.environment['HOME']);
if (deviceID == null) if (deviceID == null)
return null; return null;
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID); return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID);
} }
String _getSimulatorAppHomeDirectory(ApplicationPackage app) { String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
...@@ -438,11 +430,15 @@ class IOSSimulator extends Device { ...@@ -438,11 +430,15 @@ class IOSSimulator extends Device {
String mainPath, String mainPath,
String route, String route,
bool checked: true, bool checked: true,
bool clearLogs: false,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}) async { }) async {
// TODO: Use checked, mainPath, route // TODO(chinmaygarde): Use checked, mainPath, route.
printTrace('Building ${app.name} for $id'); printTrace('Building ${app.name} for $id');
if (clearLogs)
this.clearLogs();
// Step 1: Build the Xcode project // Step 1: Build the Xcode project
bool buildResult = await _buildIOSXcodeProject(app, false); bool buildResult = await _buildIOSXcodeProject(app, false);
if (!buildResult) { if (!buildResult) {
...@@ -506,34 +502,122 @@ class IOSSimulator extends Device { ...@@ -506,34 +502,122 @@ class IOSSimulator extends Device {
return false; return false;
} }
String get logFilePath {
return path.join(_homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
}
@override @override
TargetPlatform get platform => TargetPlatform.iOSSimulator; TargetPlatform get platform => TargetPlatform.iOSSimulator;
Future<int> logs({bool clear: false}) async { DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
if (!isConnected())
void clearLogs() {
File logFile = new File(logFilePath);
if (logFile.existsSync())
logFile.delete();
}
}
class _IOSDeviceLogReader extends DeviceLogReader {
_IOSDeviceLogReader(this.device);
final IOSDevice device;
String get name => device.name;
// TODO(devoncarew): Support [clear].
Future<int> logs({ bool clear: false }) async {
if (!device.isConnected())
return 2; return 2;
String homeDirectory = path.absolute(Platform.environment['HOME']); return await runCommandAndStreamOutput(
String simulatorDeviceID = _getRunningSimulatorInfo().id; [device.loggerPath],
String logFilePath = path.join( prefix: '[$name] ',
homeDirectory, 'Library', 'Logs', 'CoreSimulator', simulatorDeviceID, 'system.log' filter: new RegExp(r'(FlutterRunner|flutter.runner.Runner)')
); );
}
int get hashCode => name.hashCode;
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSDeviceLogReader)
return false;
return other.name == name;
}
}
class _IOSSimulatorLogReader extends DeviceLogReader {
_IOSSimulatorLogReader(this.device);
final IOSSimulator device;
String get name => device.name;
Future<int> logs({bool clear: false}) async {
if (!device.isConnected())
return 2;
if (clear) if (clear)
runSync(['rm', logFilePath]); device.clearLogs();
// TODO(devoncarew): The log message prefix could be shortened or removed. // Match the log prefix (in order to shorten it):
// Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
// TODO(devoncarew): This truncates multi-line messages like: RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
// Jan 29 01:31:43 devoncarew-macbookpro3 CoreSimulatorBridge[96656]: Requesting... { // Jan 31 19:23:28 --- last message repeated 1 time ---
// environment = { RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ (--- .* ---)$');
// };
// } // This filter matches many Flutter lines in the log:
return await runCommandAndStreamOutput( // new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
['tail', '-f', logFilePath], // a fair number, including ones that would be useful in diagnosing crashes.
prefix: 'iOS: ', // For now, we're not filtering the log file (but do clear it with each run).
filter: new RegExp(r'(FlutterRunner|flutter.runner.Runner)')
Future<int> result = runCommandAndStreamOutput(
<String>['tail', '-n', '+0', '-F', device.logFilePath],
prefix: '[$name] ',
mapFunction: (String string) {
Match match = mapRegex.matchAsPrefix(string);
if (match != null) {
// Filter out some messages that clearly aren't related to Flutter.
if (string.contains(': could not find icon for representation -> com.apple.'))
return null;
String category = match.group(1);
String content = match.group(2);
if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd')
return null;
return '$category: $content';
}
match = lastMessageRegex.matchAsPrefix(string);
if (match != null)
return match.group(1);
return string;
}
); );
// Track system.log crashes.
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
runCommandAndStreamOutput(
<String>['tail', '-F', '/private/var/log/system.log'],
prefix: '[$name] ',
filter: new RegExp(r' FlutterRunner\[\d+\] '),
mapFunction: (String string) {
Match match = mapRegex.matchAsPrefix(string);
return match == null ? string : '${match.group(1)}: ${match.group(2)}';
}
);
return result;
}
int get hashCode => device.logFilePath.hashCode;
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSSimulatorLogReader)
return false;
return other.device.logFilePath == device.logFilePath;
} }
} }
...@@ -547,6 +631,8 @@ class _IOSSimulatorInfo { ...@@ -547,6 +631,8 @@ class _IOSSimulatorInfo {
final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.'; final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.';
String get _homeDirectory => path.absolute(Platform.environment['HOME']);
bool _checkXcodeVersion() { bool _checkXcodeVersion() {
if (!Platform.isMacOS) if (!Platform.isMacOS)
return false; return false;
...@@ -573,15 +659,18 @@ Future<bool> _buildIOSXcodeProject(ApplicationPackage app, bool isDevice) async ...@@ -573,15 +659,18 @@ Future<bool> _buildIOSXcodeProject(ApplicationPackage app, bool isDevice) async
if (!_checkXcodeVersion()) if (!_checkXcodeVersion())
return false; return false;
List<String> command = [ List<String> commands = [
'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release' '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
]; ];
if (!isDevice) { if (!isDevice) {
command.addAll(['-sdk', 'iphonesimulator']); commands.addAll(['-sdk', 'iphonesimulator']);
} }
ProcessResult result = Process.runSync('/usr/bin/env', command, try {
workingDirectory: app.localPath); runCheckedSync(commands, workingDirectory: app.localPath);
return result.exitCode == 0; return true;
} catch (error) {
return false;
}
} }
...@@ -142,11 +142,16 @@ class FlutterCommandRunner extends CommandRunner { ...@@ -142,11 +142,16 @@ class FlutterCommandRunner extends CommandRunner {
String get _defaultFlutterRoot { String get _defaultFlutterRoot {
if (Platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) if (Platform.environment.containsKey(kFlutterRootEnvironmentVariableName))
return Platform.environment[kFlutterRootEnvironmentVariableName]; return Platform.environment[kFlutterRootEnvironmentVariableName];
String script = Platform.script.toFilePath();
if (path.basename(script) == kSnapshotFileName) try {
return path.dirname(path.dirname(path.dirname(script))); String script = Platform.script.toFilePath();
if (path.basename(script) == kFlutterToolsScriptFileName) if (path.basename(script) == kSnapshotFileName)
return path.dirname(path.dirname(path.dirname(path.dirname(script)))); return path.dirname(path.dirname(path.dirname(script)));
if (path.basename(script) == kFlutterToolsScriptFileName)
return path.dirname(path.dirname(path.dirname(path.dirname(script))));
} catch (error) {
printTrace('Unable to locate fluter root: $error');
}
return '.'; return '.';
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/commands/logs.dart'; import 'package:flutter_tools/src/commands/logs.dart';
import 'package:mockito/mockito.dart'; import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'src/mocks.dart'; import 'src/mocks.dart';
...@@ -13,18 +13,13 @@ main() => defineTests(); ...@@ -13,18 +13,13 @@ main() => defineTests();
defineTests() { defineTests() {
group('logs', () { group('logs', () {
test('returns 0 when no device is connected', () { test('fail with a bad device id', () {
LogsCommand command = new LogsCommand(); LogsCommand command = new LogsCommand();
applyMocksToCommand(command); applyMocksToCommand(command);
MockDeviceStore mockDevices = command.devices; CommandRunner runner = new FlutterCommandRunner()..addCommand(command);
runner.run(<String>['-d', 'abc123', 'logs']).then((int code) {
when(mockDevices.android.isConnected()).thenReturn(false); expect(code, equals(1));
when(mockDevices.iOS.isConnected()).thenReturn(false); });
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
CommandRunner runner = new CommandRunner('test_flutter', '')
..addCommand(command);
runner.run(['logs']).then((int code) => expect(code, equals(0)));
}); });
}); });
} }
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