blob: ebbcb4946ca89248d6f829fade1cefd3e36121b8 [file] [log] [blame]
// 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:fake_async/fake_async.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/ios/application_package.dart';
import 'package:flutter_tools/src/ios/core_devices.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/ios_deploy.dart';
import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/xcode_debug.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart' hide FakeXcodeProjectInterpreter;
import '../../src/fake_devices.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
List<String> _xattrArgs(FlutterProject flutterProject) {
return <String>[
'xattr',
'-r',
'-d',
'com.apple.FinderInfo',
flutterProject.directory.path,
];
}
const List<String> kRunReleaseArgs = <String>[
'xcrun',
'xcodebuild',
'-configuration',
'Release',
'-quiet',
'-workspace',
'Runner.xcworkspace',
'-scheme',
'Runner',
'BUILD_DIR=/build/ios',
'-sdk',
'iphoneos',
'-destination',
'id=123',
'ONLY_ACTIVE_ARCH=YES',
'ARCHS=arm64',
'-resultBundlePath', '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle',
'-resultBundleVersion', '3',
'FLUTTER_SUPPRESS_ANALYTICS=true',
'COMPILER_INDEX_STORE_ENABLE=NO',
];
const String kConcurrentBuildErrorMessage = '''
"/Developer/Xcode/DerivedData/foo/XCBuildData/build.db":
database is locked
Possibly there are two concurrent builds running in the same filesystem location.
''';
final FakePlatform macPlatform = FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{},
);
void main() {
late Artifacts artifacts;
late String iosDeployPath;
setUp(() {
artifacts = Artifacts.test();
iosDeployPath = artifacts.getHostArtifact(HostArtifact.iosDeploy).path;
});
group('IOSDevice.startApp succeeds in release mode', () {
late FileSystem fileSystem;
late FakeProcessManager processManager;
late BufferLogger logger;
late Xcode xcode;
late FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter;
late XcodeProjectInfo projectInfo;
setUp(() {
logger = BufferLogger.test();
fileSystem = MemoryFileSystem.test();
processManager = FakeProcessManager.empty();
projectInfo = XcodeProjectInfo(
<String>['Runner'],
<String>['Debug', 'Release'],
<String>['Runner'],
logger,
);
fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
fileSystem.file('foo/.packages')
..createSync(recursive: true)
..writeAsStringSync('\n');
});
testUsingContext('missing TARGET_BUILD_DIR', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
artifacts: artifacts,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(launchResult.started, false);
expect(logger.errorText, contains('Xcode build is missing expected TARGET_BUILD_DIR build setting'));
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(buildSettings: const <String, String>{
'WRAPPER_NAME': 'My Super Awesome App.app',
'DEVELOPMENT_TEAM': '3333CCCC33',
}, projectInfo: projectInfo),
Xcode: () => xcode,
});
testUsingContext('missing project info', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(launchResult.started, false);
expect(logger.errorText, contains('Xcode project not found'));
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
Xcode: () => xcode,
});
testUsingContext('with buildable app', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
artifacts: artifacts,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
processManager.addCommand(const FakeCommand(command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'build/ios/Release-iphoneos/My Super Awesome App.app',
'build/ios/iphoneos',
]));
processManager.addCommand(FakeCommand(
command: <String>[
iosDeployPath,
'--id',
'123',
'--bundle',
'build/ios/iphoneos/My Super Awesome App.app',
'--app_deltas',
'build/ios/app-delta',
'--no-wifi',
'--justlaunch',
'--args',
const <String>[
'--enable-dart-profiling',
].join(' '),
])
);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
testUsingContext('with concurrent build failures', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
artifacts: artifacts,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
// The first xcrun call should fail with a
// concurrent build exception.
processManager.addCommand(
const FakeCommand(
command: kRunReleaseArgs,
exitCode: 1,
stdout: kConcurrentBuildErrorMessage,
));
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
processManager.addCommand(FakeCommand(
command: <String>[
iosDeployPath,
'--id',
'123',
'--bundle',
'build/ios/iphoneos/My Super Awesome App.app',
'--no-wifi',
'--justlaunch',
'--args',
const <String>[
'--enable-dart-profiling',
'--disable-service-auth-codes',
].join(' '),
])
);
await FakeAsync().run((FakeAsync time) async {
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
time.elapse(const Duration(seconds: 2));
expect(logger.statusText,
contains('Xcode build failed due to concurrent builds, will retry in 2 seconds'));
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
});
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
}, skip: true); // TODO(zanderso): clean up with https://github.com/flutter/flutter/issues/60675
});
group('IOSDevice.startApp for CoreDevice', () {
late FileSystem fileSystem;
late FakeProcessManager processManager;
late BufferLogger logger;
late Xcode xcode;
late FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter;
late XcodeProjectInfo projectInfo;
setUp(() {
logger = BufferLogger.test();
fileSystem = MemoryFileSystem.test();
processManager = FakeProcessManager.empty();
projectInfo = XcodeProjectInfo(
<String>['Runner'],
<String>['Debug', 'Release'],
<String>['Runner'],
logger,
);
fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
fileSystem.file('foo/.packages')
..createSync(recursive: true)
..writeAsStringSync('\n');
});
group('in release mode', () {
testUsingContext('suceeds when install and launch succeed', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
testUsingContext('fails when install fails', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(
installSuccess: false,
),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, false);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
testUsingContext('fails when launch fails', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(
launchSuccess: false,
),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, false);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
testUsingContext('ensure arguments passed to launch', () async {
final FakeIOSCoreDeviceControl coreDeviceControl = FakeIOSCoreDeviceControl();
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: coreDeviceControl,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
expect(coreDeviceControl.argumentsUsedForLaunch, isNotNull);
expect(coreDeviceControl.argumentsUsedForLaunch, contains('--enable-dart-profiling'));
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
});
group('in debug mode', () {
testUsingContext('succeeds', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(
expectedProject: XcodeDebugProject(
scheme: 'Runner',
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
iosDevice.portForwarder = const NoOpDevicePortForwarder();
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
// Start writing messages to the log reader.
Timer.run(() {
deviceLogReader.addLine('Foo');
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
});
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
null,
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
);
expect(logger.errorText, isEmpty);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
group('with flavor', () {
setUp(() {
projectInfo = XcodeProjectInfo(
<String>['Runner'],
<String>['Debug', 'Release', 'Debug-free', 'Release-free'],
<String>['Runner', 'free'],
logger,
);
fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
});
testUsingContext('succeeds', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(
expectedProject: XcodeDebugProject(
scheme: 'free',
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
expectedSchemeFilePath: '/ios/Runner.xcodeproj/xcshareddata/xcschemes/free.xcscheme',
),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
iosDevice.portForwarder = const NoOpDevicePortForwarder();
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
// Start writing messages to the log reader.
Timer.run(() {
deviceLogReader.addLine('Foo');
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
});
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
'free',
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
);
expect(logger.errorText, isEmpty);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
});
testUsingContext('updates Generated.xcconfig before and after launch', () async {
final Completer<void> debugStartedCompleter = Completer<void>();
final Completer<void> debugEndedCompleter = Completer<void>();
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(
expectedProject: XcodeDebugProject(
scheme: 'Runner',
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
hostAppProjectName: 'Runner',
expectedConfigurationBuildDir: '/build/ios/iphoneos',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
debugStartedCompleter: debugStartedCompleter,
debugEndedCompleter: debugEndedCompleter,
),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
iosDevice.portForwarder = const NoOpDevicePortForwarder();
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
// Start writing messages to the log reader.
Timer.run(() {
deviceLogReader.addLine('Foo');
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
});
final Future<LaunchResult> futureLaunchResult = iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
null,
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
);
await debugStartedCompleter.future;
// Validate CoreDevice build settings were used
final File config = fileSystem.directory('ios').childFile('Flutter/Generated.xcconfig');
expect(config.existsSync(), isTrue);
String contents = config.readAsStringSync();
expect(contents, contains('CONFIGURATION_BUILD_DIR=/build/ios/iphoneos'));
debugEndedCompleter.complete();
await futureLaunchResult;
// Validate CoreDevice build settings were removed after launch
contents = config.readAsStringSync();
expect(contents.contains('CONFIGURATION_BUILD_DIR'), isFalse);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
testUsingContext('fails when Xcode project is not found', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl()
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
null,
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
);
expect(logger.errorText, contains('Xcode project not found'));
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, false);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
Xcode: () => xcode,
});
testUsingContext('fails when Xcode workspace is not found', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl()
);
setUpIOSProject(fileSystem, createWorkspace: false);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
null,
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
);
expect(logger.errorText, contains('Unable to get Xcode workspace'));
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, false);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
testUsingContext('fails when scheme is not found', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl()
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
iosDevice.portForwarder = const NoOpDevicePortForwarder();
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
// Start writing messages to the log reader.
Timer.run(() {
deviceLogReader.addLine('Foo');
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
});
expect(() async => iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
'Flavor',
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
});
});
}
void setUpIOSProject(FileSystem fileSystem, {bool createWorkspace = true}) {
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').writeAsStringSync('\n');
fileSystem.directory('ios').createSync();
if (createWorkspace) {
fileSystem.directory('ios/Runner.xcworkspace').createSync();
}
fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync(recursive: true);
// This is the expected output directory.
fileSystem.directory('build/ios/iphoneos/My Super Awesome App.app').createSync(recursive: true);
}
IOSDevice setUpIOSDevice({
String sdkVersion = '13.0.1',
FileSystem? fileSystem,
Logger? logger,
ProcessManager? processManager,
Artifacts? artifacts,
bool isCoreDevice = false,
IOSCoreDeviceControl? coreDeviceControl,
FakeXcodeDebug? xcodeDebug,
}) {
artifacts ??= Artifacts.test();
final Cache cache = Cache.test(
artifacts: <ArtifactSet>[
FakeDyldEnvironmentArtifact(),
],
processManager: FakeProcessManager.any(),
);
logger ??= BufferLogger.test();
return IOSDevice(
'123',
name: 'iPhone 1',
sdkVersion: sdkVersion,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
platform: macPlatform,
iProxy: IProxy.test(logger: logger, processManager: processManager ?? FakeProcessManager.any()),
logger: logger,
iosDeploy: IOSDeploy(
logger: logger,
platform: macPlatform,
processManager: processManager ?? FakeProcessManager.any(),
artifacts: artifacts,
cache: cache,
),
iMobileDevice: IMobileDevice(
logger: logger,
processManager: processManager ?? FakeProcessManager.any(),
artifacts: artifacts,
cache: cache,
),
coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
isCoreDevice: isCoreDevice,
);
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({
this.projectInfo,
this.buildSettings = const <String, String>{
'TARGET_BUILD_DIR': 'build/ios/Release-iphoneos',
'WRAPPER_NAME': 'My Super Awesome App.app',
'DEVELOPMENT_TEAM': '3333CCCC33',
},
});
final Map<String, String> buildSettings;
final XcodeProjectInfo? projectInfo;
@override
final bool isInstalled = true;
@override
final Version version = Version(1000, 0, 0);
@override
String get versionText => version.toString();
@override
List<String> xcrunCommand() => <String>['xcrun'];
@override
Future<XcodeProjectInfo?> getInfo(
String projectPath, {
String? projectFilename,
}) async => projectInfo;
@override
Future<Map<String, String>> getBuildSettings(
String projectPath, {
required XcodeProjectBuildContext buildContext,
Duration timeout = const Duration(minutes: 1),
}) async => buildSettings;
}
class FakeXcodeDebug extends Fake implements XcodeDebug {
FakeXcodeDebug({
this.debugSuccess = true,
this.expectedProject,
this.expectedDeviceId,
this.expectedLaunchArguments,
this.expectedSchemeFilePath = '/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme',
this.debugStartedCompleter,
this.debugEndedCompleter,
});
final bool debugSuccess;
final XcodeDebugProject? expectedProject;
final String? expectedDeviceId;
final List<String>? expectedLaunchArguments;
final Completer<void>? debugStartedCompleter;
final Completer<void>? debugEndedCompleter;
final String expectedSchemeFilePath;
@override
Future<bool> debugApp({
required XcodeDebugProject project,
required String deviceId,
required List<String> launchArguments,
}) async {
debugStartedCompleter?.complete();
if (expectedProject != null) {
expect(project.scheme, expectedProject!.scheme);
expect(project.xcodeWorkspace.path, expectedProject!.xcodeWorkspace.path);
expect(project.xcodeProject.path, expectedProject!.xcodeProject.path);
expect(project.isTemporaryProject, expectedProject!.isTemporaryProject);
}
if (expectedDeviceId != null) {
expect(deviceId, expectedDeviceId);
}
if (expectedLaunchArguments != null) {
expect(expectedLaunchArguments, launchArguments);
}
await debugEndedCompleter?.future;
return debugSuccess;
}
@override
void ensureXcodeDebuggerLaunchAction(File schemeFile) {
expect(schemeFile.path, expectedSchemeFilePath);
}
}
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
FakeIOSCoreDeviceControl({
this.installSuccess = true,
this.launchSuccess = true
});
final bool installSuccess;
final bool launchSuccess;
List<String>? _launchArguments;
List<String>? get argumentsUsedForLaunch => _launchArguments;
@override
Future<bool> installApp({
required String deviceId,
required String bundlePath,
}) async {
return installSuccess;
}
@override
Future<bool> launchApp({
required String deviceId,
required String bundleId,
List<String> launchArguments = const <String>[],
}) async {
_launchArguments = launchArguments;
return launchSuccess;
}
}