Unverified Commit 2717eb64 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter tools] rewrite launch non-prebuilt app tests (#53351)

parent c5800fe1
......@@ -3,11 +3,12 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
......@@ -15,26 +16,15 @@ import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/ios_deploy.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:quiver/testing/async.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/mocks.dart';
void main() {
......@@ -59,7 +49,6 @@ void main() {
mockCache = MockCache();
const MapEntry<String, String> dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', '/path/to/libs');
when(mockCache.dyLdLibEntry).thenReturn(dyLdLibEntry);
mockFileSystem = MockFileSystem();
logger = BufferLogger.test();
iosDeploy = IOSDeploy(
artifacts: mockArtifacts,
......@@ -231,7 +220,6 @@ void main() {
forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3);
mockArtifacts = MockArtifacts();
mockCache = MockCache();
mockFileSystem = MockFileSystem();
iosDeploy = IOSDeploy(
artifacts: mockArtifacts,
cache: mockCache,
......@@ -268,271 +256,12 @@ void main() {
verify(mockProcess3.kill());
});
});
group('startApp', () {
MockIOSApp mockApp;
MockArtifacts mockArtifacts;
MockCache mockCache;
MockFileSystem mockFileSystem;
MockPlatform mockPlatform;
MockProcessManager mockProcessManager;
FakeDeviceLogReader mockLogReader;
MockPortForwarder mockPortForwarder;
MockIMobileDevice mockIMobileDevice;
MockIOSDeploy mockIosDeploy;
Directory tempDir;
Directory projectDir;
const int devicePort = 499;
const int hostPort = 42;
const String installerPath = '/path/to/ideviceinstaller';
const String iosDeployPath = '/path/to/iosdeploy';
const String iproxyPath = '/path/to/iproxy';
const MapEntry<String, String> libraryEntry = MapEntry<String, String>(
'DYLD_LIBRARY_PATH',
'/path/to/libraries',
);
final Map<String, String> env = Map<String, String>.fromEntries(
<MapEntry<String, String>>[libraryEntry]
);
setUp(() {
Cache.disableLocking();
mockApp = MockIOSApp();
mockArtifacts = MockArtifacts();
mockCache = MockCache();
when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
mockFileSystem = MockFileSystem();
mockPlatform = MockPlatform();
when(mockPlatform.isMacOS).thenReturn(true);
mockProcessManager = MockProcessManager();
mockLogReader = FakeDeviceLogReader();
mockPortForwarder = MockPortForwarder();
mockIMobileDevice = MockIMobileDevice();
mockIosDeploy = MockIOSDeploy();
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
projectDir = tempDir.childDirectory('flutter_project');
when(
mockArtifacts.getArtifactPath(
Artifact.ideviceinstaller,
platform: anyNamed('platform'),
),
).thenReturn(installerPath);
when(
mockArtifacts.getArtifactPath(
Artifact.iosDeploy,
platform: anyNamed('platform'),
),
).thenReturn(iosDeployPath);
when(
mockArtifacts.getArtifactPath(
Artifact.iproxy,
platform: anyNamed('platform'),
),
).thenReturn(iproxyPath);
when(mockPortForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
.thenAnswer((_) async => hostPort);
when(mockPortForwarder.forwardedPorts)
.thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
when(mockPortForwarder.unforward(any))
.thenAnswer((_) async => null);
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
when(mockFileSystem.currentDirectory)
.thenReturn(memoryFileSystem.currentDirectory);
const String bundlePath = '/path/to/bundle';
final List<String> installArgs = <String>[installerPath, '-i', bundlePath];
when(mockApp.deviceBundlePath).thenReturn(bundlePath);
final MockDirectory directory = MockDirectory();
when(mockFileSystem.directory(bundlePath)).thenReturn(directory);
when(directory.existsSync()).thenReturn(true);
when(mockProcessManager.run(
installArgs,
workingDirectory: anyNamed('workingDirectory'),
environment: env,
)).thenAnswer(
(_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
);
});
tearDown(() {
mockLogReader.dispose();
tryToDelete(tempDir);
Cache.enableLocking();
});
void testNonPrebuilt(
String name, {
@required bool showBuildSettingsFlakes,
void Function() additionalSetup,
void Function() additionalExpectations,
}) {
testUsingContext('non-prebuilt succeeds in debug mode $name', () async {
final Directory targetBuildDir =
projectDir.childDirectory('build/ios/iphoneos/Debug-arm64');
// The -showBuildSettings calls have a timeout and so go through
// globals.processManager.start().
mockProcessManager.processFactory = flakyProcessFactory(
flakes: showBuildSettingsFlakes ? 1 : 0,
delay: const Duration(seconds: 62),
filter: (List<String> args) => args.contains('-showBuildSettings'),
stdout:
() => Stream<String>
.fromIterable(
<String>['TARGET_BUILD_DIR = ${targetBuildDir.path}\n'])
.transform(utf8.encoder),
);
// Make all other subcommands succeed.
when(mockProcessManager.run(
any,
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'),
)).thenAnswer((Invocation inv) {
return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
});
when(mockProcessManager.run(
argThat(contains('find-identity')),
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(
1, // pid
0, // exitCode
'''
1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
3 valid identities found''',
'',
)));
// Deploy works.
when(mockIosDeploy.runApp(
deviceId: anyNamed('deviceId'),
bundlePath: anyNamed('bundlePath'),
launchArguments: anyNamed('launchArguments'),
)).thenAnswer((_) => Future<int>.value(0));
// Create a dummy project to avoid mocking out the whole directory
// structure expected by device.startApp().
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'create',
'--no-pub',
projectDir.path,
]);
if (additionalSetup != null) {
additionalSetup();
}
final IOSApp app = await AbsoluteBuildableIOSApp.fromProject(
FlutterProject.fromDirectory(projectDir).ios);
final IOSDevice device = IOSDevice(
'123',
name: 'iPhone 1',
sdkVersion: '13.3',
artifacts: mockArtifacts,
fileSystem: globals.fs,
logger: testLogger,
platform: macPlatform,
iosDeploy: mockIosDeploy,
iMobileDevice: iMobileDevice,
cpuArchitecture: DarwinArch.arm64,
);
// Pre-create the expected build products.
targetBuildDir.createSync(recursive: true);
projectDir.childDirectory('build/ios/iphoneos/Runner.app').createSync(recursive: true);
final Completer<LaunchResult> completer = Completer<LaunchResult>();
FakeAsync().run((FakeAsync time) {
device.startApp(
app,
prebuiltApplication: false,
debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
platformArgs: <String, dynamic>{},
).then((LaunchResult result) {
completer.complete(result);
});
time.flushMicrotasks();
time.elapse(const Duration(seconds: 65));
});
final LaunchResult launchResult = await completer.future;
expect(launchResult.started, isTrue);
expect(launchResult.hasObservatory, isFalse);
expect(await device.stopApp(mockApp), isFalse);
if (additionalExpectations != null) {
additionalExpectations();
}
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeIosDoctorProvider(),
IMobileDevice: () => mockIMobileDevice,
Platform: () => macPlatform,
ProcessManager: () => mockProcessManager,
});
}
testNonPrebuilt('flaky: false', showBuildSettingsFlakes: false);
testNonPrebuilt('flaky: true', showBuildSettingsFlakes: true);
testNonPrebuilt('with concurrent build failiure',
showBuildSettingsFlakes: false,
additionalSetup: () {
int callCount = 0;
when(mockProcessManager.run(
argThat(allOf(
contains('xcodebuild'),
contains('-configuration'),
contains('Debug'),
)),
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'),
)).thenAnswer((Invocation inv) {
// Succeed after 2 calls.
if (++callCount > 2) {
return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
}
// Otherwise fail with the Xcode concurrent error.
return Future<ProcessResult>.value(ProcessResult(
0,
1,
'''
"/Developer/Xcode/DerivedData/foo/XCBuildData/build.db":
database is locked
Possibly there are two concurrent builds running in the same filesystem location.
''',
'',
));
});
},
additionalExpectations: () {
expect(testLogger.statusText, contains('will retry in 2 seconds'));
expect(testLogger.statusText, contains('will retry in 4 seconds'));
expect(testLogger.statusText, contains('Xcode build done.'));
},
);
});
});
group('pollingGetDevices', () {
MockXcdevice mockXcdevice;
MockArtifacts mockArtifacts;
MockCache mockCache;
MockFileSystem mockFileSystem;
FakeProcessManager fakeProcessManager;
Logger logger;
IOSDeploy iosDeploy;
......@@ -544,7 +273,6 @@ void main() {
mockArtifacts = MockArtifacts();
mockCache = MockCache();
logger = BufferLogger.test();
mockFileSystem = MockFileSystem();
mockIosWorkflow = MockIOSWorkflow();
fakeProcessManager = FakeProcessManager.any();
iosDeploy = IOSDeploy(
......@@ -596,7 +324,7 @@ void main() {
iMobileDevice: iMobileDevice,
logger: logger,
platform: macPlatform,
fileSystem: mockFileSystem,
fileSystem: MemoryFileSystem.test(),
);
when(mockXcdevice.getAvailableTetheredIOSDevices())
.thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device]));
......@@ -646,48 +374,10 @@ void main() {
});
}
class AbsoluteBuildableIOSApp extends BuildableIOSApp {
AbsoluteBuildableIOSApp(IosProject project, String projectBundleId) :
super(project, projectBundleId);
static Future<AbsoluteBuildableIOSApp> fromProject(IosProject project) async {
final String projectBundleId = await project.productBundleIdentifier;
return AbsoluteBuildableIOSApp(project, projectBundleId);
}
@override
String get deviceBundlePath =>
globals.fs.path.join(project.parent.directory.path, 'build', 'ios', 'iphoneos', name);
}
class FakeIosDoctorProvider implements DoctorValidatorsProvider {
List<Workflow> _workflows;
@override
List<DoctorValidator> get validators => <DoctorValidator>[];
@override
List<Workflow> get workflows {
if (_workflows == null) {
_workflows = <Workflow>[];
if (globals.iosWorkflow.appliesToHostPlatform) {
_workflows.add(globals.iosWorkflow);
}
}
return _workflows;
}
}
class MockIOSApp extends Mock implements IOSApp {}
class MockArtifacts extends Mock implements Artifacts {}
class MockCache extends Mock implements Cache {}
class MockDirectory extends Mock implements Directory {}
class MockFile extends Mock implements File {}
class MockFileSystem extends Mock implements FileSystem {}
class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockIOSDeploy extends Mock implements IOSDeploy {}
class MockIOSWorkflow extends Mock implements IOSWorkflow {}
class MockPlatform extends Mock implements Platform {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
class MockXcdevice extends Mock implements XCDevice {}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.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/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/ios_deploy.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:quiver/testing/async.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
const List<String> kRunReleaseArgs = <String>[
'/usr/bin/env',
'xcrun',
'xcodebuild',
'-configuration',
'Release',
'-quiet',
'-workspace',
'Runner.xcworkspace',
'-scheme',
'Runner',
'BUILD_DIR=/build/ios',
'-sdk',
'iphoneos',
'ONLY_ACTIVE_ARCH=YES',
'ARCHS=arm64',
'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() {
FileSystem fileSystem;
FakeProcessManager processManager;
BufferLogger logger;
setUp(() {
logger = BufferLogger.test();
fileSystem = MemoryFileSystem.test();
processManager = FakeProcessManager.list(<FakeCommand>[]);
});
testUsingContext('IOSDevice.startApp succeeds in release mode with buildable app', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter');
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
processManager.addCommand(const FakeCommand(command: <String>[...kRunReleaseArgs, '-showBuildSettings']));
processManager.addCommand(FakeCommand(
command: <String>[
'ios-deploy',
'--id',
'123',
'--bundle',
'build/ios/iphoneos/Runner.app',
'--no-wifi',
'--justlaunch',
'--args',
const <String>[
'--enable-dart-profiling',
'--enable-service-port-fallback',
'--disable-service-auth-codes',
'--observatory-port=53781',
].join(' ')
])
);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(launchResult.started, true);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
});
testUsingContext('IOSDevice.startApp succeeds in release mode with buildable '
'app with flaky buildSettings call', () async {
LaunchResult launchResult;
FakeAsync().run((FakeAsync time) {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter');
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
// The first showBuildSettings call should timeout.
processManager.addCommand(
const FakeCommand(
command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
duration: Duration(minutes: 5), // this is longer than the timeout of 1 minute.
));
// The second call succeedes and is made after the first times out.
processManager.addCommand(
const FakeCommand(
command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
exitCode: 0,
));
processManager.addCommand(FakeCommand(
command: <String>[
'ios-deploy',
'--id',
'123',
'--bundle',
'build/ios/iphoneos/Runner.app',
'--no-wifi',
'--justlaunch',
'--args',
const <String>[
'--enable-dart-profiling',
'--enable-service-port-fallback',
'--disable-service-auth-codes',
'--observatory-port=53781',
].join(' ')
])
);
iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
).then((LaunchResult result) {
launchResult = result;
});
// Elapse duration for process timeout.
time.flushMicrotasks();
time.elapse(const Duration(minutes: 1));
// Elapse duration for overall process timer.
time.flushMicrotasks();
time.elapse(const Duration(minutes: 5));
time.flushTimers();
});
expect(launchResult?.started, true);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
});
testUsingContext('IOSDevice.startApp succeeds in release mode with buildable '
'app with concurrent build failure', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter');
// 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(
const FakeCommand(
command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
exitCode: 0,
));
processManager.addCommand(FakeCommand(
command: <String>[
'ios-deploy',
'--id',
'123',
'--bundle',
'build/ios/iphoneos/Runner.app',
'--no-wifi',
'--justlaunch',
'--args',
const <String>[
'--enable-dart-profiling',
'--enable-service-port-fallback',
'--disable-service-auth-codes',
'--observatory-port=53781',
].join(' ')
])
);
final LaunchResult launchResult = await iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
platformArgs: <String, Object>{},
);
expect(logger.statusText,
contains('Xcode build failed due to concurrent builds, will retry in 2 seconds'));
expect(launchResult.started, true);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
});
}
void setUpIOSProject(FileSystem fileSystem) {
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').writeAsStringSync('\n');
fileSystem.directory('ios').createSync();
fileSystem.directory('ios/Runner.xcworkspace').createSync();
fileSystem.directory('ios/Runner.xcodeproj').createSync();
fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync();
// This is the expected output directory.
fileSystem.directory('build/ios/iphoneos/Runner.app').createSync(recursive: true);
}
IOSDevice setUpIOSDevice({
String sdkVersion = '13.0.1',
FileSystem fileSystem,
Logger logger,
ProcessManager processManager,
}) {
const MapEntry<String, String> dyldLibraryEntry = MapEntry<String, String>(
'DYLD_LIBRARY_PATH',
'/path/to/libraries',
);
final MockCache cache = MockCache();
final MockArtifacts artifacts = MockArtifacts();
logger ??= BufferLogger.test();
when(cache.dyLdLibEntry).thenReturn(dyldLibraryEntry);
when(artifacts.getArtifactPath(Artifact.iosDeploy, platform: anyNamed('platform')))
.thenReturn('ios-deploy');
return IOSDevice('123',
name: 'iPhone 1',
sdkVersion: sdkVersion,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
platform: macPlatform,
artifacts: artifacts,
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,
),
cpuArchitecture: DarwinArch.arm64,
);
}
class MockArtifacts extends Mock implements Artifacts {}
class MockCache extends Mock implements Cache {}
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