Unverified Commit b7c714e8 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tool] Use a timeout for xcode showBuildSettings (#39280)

parent ce3c4672
...@@ -231,16 +231,88 @@ Future<RunResult> runAsync( ...@@ -231,16 +231,88 @@ Future<RunResult> runAsync(
String workingDirectory, String workingDirectory,
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
Map<String, String> environment, Map<String, String> environment,
Duration timeout,
int timeoutRetries = 0,
}) async { }) async {
_traceCommand(cmd, workingDirectory: workingDirectory); _traceCommand(cmd, workingDirectory: workingDirectory);
final ProcessResult results = await processManager.run(
cmd, // When there is no timeout, there's no need to kill a running process, so
workingDirectory: workingDirectory, // we can just use processManager.run().
environment: _environment(allowReentrantFlutter, environment), if (timeout == null) {
); final ProcessResult results = await processManager.run(
final RunResult runResults = RunResult(results, cmd); cmd,
printTrace(runResults.toString()); workingDirectory: workingDirectory,
return runResults; environment: _environment(allowReentrantFlutter, environment),
);
final RunResult runResults = RunResult(results, cmd);
printTrace(runResults.toString());
return runResults;
}
// When there is a timeout, we have to kill the running process, so we have
// to use processManager.start() through runCommand() above.
while (true) {
assert(timeoutRetries >= 0);
timeoutRetries = timeoutRetries - 1;
final Process process = await runCommand(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment,
);
final StringBuffer stdoutBuffer = StringBuffer();
final StringBuffer stderrBuffer = StringBuffer();
final Future<void> stdoutFuture = process.stdout
.transform<String>(const Utf8Decoder(reportErrors: false))
.listen(stdoutBuffer.write)
.asFuture<void>(null);
final Future<void> stderrFuture = process.stderr
.transform<String>(const Utf8Decoder(reportErrors: false))
.listen(stderrBuffer.write)
.asFuture<void>(null);
int exitCode;
exitCode = await process.exitCode.timeout(timeout, onTimeout: () {
return null;
});
String stdoutString;
String stderrString;
try {
await Future.wait<void>(<Future<void>>[stdoutFuture, stderrFuture]);
} catch (_) {
// Ignore errors on the process' stdout and stderr streams. Just capture
// whatever we got, and use the exit code
}
stdoutString = stdoutBuffer.toString();
stderrString = stderrBuffer.toString();
final ProcessResult result = ProcessResult(
process.pid, exitCode ?? -1, stdoutString, stderrString);
final RunResult runResult = RunResult(result, cmd);
// If the process did not timeout. We are done.
if (exitCode != null) {
printTrace(runResult.toString());
return runResult;
}
// The process timed out. Kill it.
processManager.killPid(process.pid);
// If we are out of timeoutRetries, throw a ProcessException.
if (timeoutRetries < 0) {
throw ProcessException(cmd[0], cmd.sublist(1),
'Process "${cmd[0]}" timed out: $runResult', exitCode);
}
// Log the timeout with a trace message in verbose mode.
printTrace('Process "${cmd[0]}" timed out. $timeoutRetries attempts left: $runResult');
}
// Unreachable.
} }
typedef RunResultChecker = bool Function(int); typedef RunResultChecker = bool Function(int);
...@@ -251,12 +323,16 @@ Future<RunResult> runCheckedAsync( ...@@ -251,12 +323,16 @@ Future<RunResult> runCheckedAsync(
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
Map<String, String> environment, Map<String, String> environment,
RunResultChecker whiteListFailures, RunResultChecker whiteListFailures,
Duration timeout,
int timeoutRetries = 0,
}) async { }) async {
final RunResult result = await runAsync( final RunResult result = await runAsync(
cmd, cmd,
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter, allowReentrantFlutter: allowReentrantFlutter,
environment: environment, environment: environment,
timeout: timeout,
timeoutRetries: timeoutRetries,
); );
if (result.exitCode != 0) { if (result.exitCode != 0) {
if (whiteListFailures == null || !whiteListFailures(result.exitCode)) { if (whiteListFailures == null || !whiteListFailures(result.exitCode)) {
......
...@@ -10,6 +10,7 @@ import '../artifacts.dart'; ...@@ -10,6 +10,7 @@ import '../artifacts.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
...@@ -249,6 +250,8 @@ class XcodeProjectInterpreter { ...@@ -249,6 +250,8 @@ class XcodeProjectInterpreter {
return _minorVersion; return _minorVersion;
} }
/// Synchronously retrieve xcode build settings. Prefer using the async
/// version below.
Map<String, String> getBuildSettings(String projectPath, String target) { Map<String, String> getBuildSettings(String projectPath, String target) {
try { try {
final String out = runCheckedSync(<String>[ final String out = runCheckedSync(<String>[
...@@ -266,6 +269,41 @@ class XcodeProjectInterpreter { ...@@ -266,6 +269,41 @@ class XcodeProjectInterpreter {
} }
} }
/// Asynchronously retrieve xcode build settings. This one is preferred for
/// new call-sites.
Future<Map<String, String>> getBuildSettingsAsync(
String projectPath, String target, {
Duration timeout = const Duration(minutes: 1),
}) async {
final Status status = Status.withSpinner(
timeout: timeoutConfiguration.fastOperation,
);
try {
// showBuildSettings is reported to ocassionally timeout. Here, we give it
// a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
// When there is a timeout, we retry once.
final RunResult result = await runCheckedAsync(<String>[
_executable,
'-project',
fs.path.absolute(projectPath),
'-target',
target,
'-showBuildSettings',
],
workingDirectory: projectPath,
timeout: timeout,
timeoutRetries: 1,
);
final String out = result.stdout.trim();
return parseXcodeBuildSettings(out);
} catch(error) {
printTrace('Unexpected failure to get the build settings: $error.');
return const <String, String>{};
} finally {
status.stop();
}
}
void cleanWorkspace(String workspacePath, String scheme) { void cleanWorkspace(String workspacePath, String scheme) {
runSync(<String>[ runSync(<String>[
_executable, _executable,
......
...@@ -192,7 +192,7 @@ class CocoaPods { ...@@ -192,7 +192,7 @@ class CocoaPods {
/// Ensures the given Xcode-based sub-project of a parent Flutter project /// Ensures the given Xcode-based sub-project of a parent Flutter project
/// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files
/// include pods configuration. /// include pods configuration.
void setupPodfile(XcodeBasedProject xcodeProject) { Future<void> setupPodfile(XcodeBasedProject xcodeProject) async {
if (!xcodeProjectInterpreter.isInstalled) { if (!xcodeProjectInterpreter.isInstalled) {
// Don't do anything for iOS when host platform doesn't support it. // Don't do anything for iOS when host platform doesn't support it.
return; return;
...@@ -202,27 +202,29 @@ class CocoaPods { ...@@ -202,27 +202,29 @@ class CocoaPods {
return; return;
} }
final File podfile = xcodeProject.podfile; final File podfile = xcodeProject.podfile;
if (!podfile.existsSync()) { if (podfile.existsSync()) {
String podfileTemplateName; addPodsDependencyToFlutterXcconfig(xcodeProject);
if (xcodeProject is MacOSProject) { return;
podfileTemplateName = 'Podfile-macos'; }
} else { String podfileTemplateName;
final bool isSwift = xcodeProjectInterpreter.getBuildSettings( if (xcodeProject is MacOSProject) {
runnerProject.path, podfileTemplateName = 'Podfile-macos';
'Runner', } else {
).containsKey('SWIFT_VERSION'); final bool isSwift = (await xcodeProjectInterpreter.getBuildSettingsAsync(
podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc'; runnerProject.path,
} 'Runner',
final File podfileTemplate = fs.file(fs.path.join( )).containsKey('SWIFT_VERSION');
Cache.flutterRoot, podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
'packages',
'flutter_tools',
'templates',
'cocoapods',
podfileTemplateName,
));
podfileTemplate.copySync(podfile.path);
} }
final File podfileTemplate = fs.file(fs.path.join(
Cache.flutterRoot,
'packages',
'flutter_tools',
'templates',
'cocoapods',
podfileTemplateName,
));
podfileTemplate.copySync(podfile.path);
addPodsDependencyToFlutterXcconfig(xcodeProject); addPodsDependencyToFlutterXcconfig(xcodeProject);
} }
......
...@@ -371,7 +371,7 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false}) ...@@ -371,7 +371,7 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false})
if (!project.isModule && (!checkProjects || subproject.existsSync())) { if (!project.isModule && (!checkProjects || subproject.existsSync())) {
final CocoaPods cocoaPods = CocoaPods(); final CocoaPods cocoaPods = CocoaPods();
if (plugins.isNotEmpty) { if (plugins.isNotEmpty) {
cocoaPods.setupPodfile(subproject); await cocoaPods.setupPodfile(subproject);
} }
/// The user may have a custom maintained Podfile that they're running `pod install` /// The user may have a custom maintained Podfile that they're running `pod install`
/// on themselves. /// on themselves.
......
...@@ -22,24 +22,29 @@ import 'ios/xcodeproj.dart' as xcode; ...@@ -22,24 +22,29 @@ import 'ios/xcodeproj.dart' as xcode;
import 'plugins.dart'; import 'plugins.dart';
import 'template.dart'; import 'template.dart';
FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? const FlutterProjectFactory(); FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory();
class FlutterProjectFactory { class FlutterProjectFactory {
const FlutterProjectFactory(); FlutterProjectFactory();
final Map<String, FlutterProject> _projects =
<String, FlutterProject>{};
/// Returns a [FlutterProject] view of the given directory or a ToolExit error, /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
/// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
FlutterProject fromDirectory(Directory directory) { FlutterProject fromDirectory(Directory directory) {
assert(directory != null); assert(directory != null);
final FlutterManifest manifest = FlutterProject._readManifest( return _projects.putIfAbsent(directory.path, /* ifAbsent */ () {
directory.childFile(bundle.defaultManifestPath).path, final FlutterManifest manifest = FlutterProject._readManifest(
); directory.childFile(bundle.defaultManifestPath).path,
final FlutterManifest exampleManifest = FlutterProject._readManifest( );
FlutterProject._exampleDirectory(directory) final FlutterManifest exampleManifest = FlutterProject._readManifest(
.childFile(bundle.defaultManifestPath) FlutterProject._exampleDirectory(directory)
.path, .childFile(bundle.defaultManifestPath)
); .path,
return FlutterProject(directory, manifest, exampleManifest); );
return FlutterProject(directory, manifest, exampleManifest);
});
} }
} }
...@@ -394,9 +399,14 @@ class IosProject implements XcodeBasedProject { ...@@ -394,9 +399,14 @@ class IosProject implements XcodeBasedProject {
Map<String, String> get buildSettings { Map<String, String> get buildSettings {
if (!xcode.xcodeProjectInterpreter.isInstalled) if (!xcode.xcodeProjectInterpreter.isInstalled)
return null; return null;
return xcode.xcodeProjectInterpreter.getBuildSettings(xcodeProject.path, _hostAppBundleName); _buildSettings ??=
xcode.xcodeProjectInterpreter.getBuildSettings(xcodeProject.path,
_hostAppBundleName);
return _buildSettings;
} }
Map<String, String> _buildSettings;
Future<void> ensureReadyForPlatformSpecificTooling() async { Future<void> ensureReadyForPlatformSpecificTooling() async {
_regenerateFromTemplateIfNeeded(); _regenerateFromTemplateIfNeeded();
if (!_flutterLibRoot.existsSync()) if (!_flutterLibRoot.existsSync())
......
...@@ -12,7 +12,9 @@ import 'package:process/process.dart'; ...@@ -12,7 +12,9 @@ import 'package:process/process.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/mocks.dart' show MockProcess, MockProcessManager; import '../../src/mocks.dart' show MockProcess,
MockProcessManager,
flakyProcessFactory;
void main() { void main() {
group('process exceptions', () { group('process exceptions', () {
...@@ -28,6 +30,7 @@ void main() { ...@@ -28,6 +30,7 @@ void main() {
expect(() async => await runCheckedAsync(<String>['false']), throwsA(isInstanceOf<ProcessException>())); expect(() async => await runCheckedAsync(<String>['false']), throwsA(isInstanceOf<ProcessException>()));
}, overrides: <Type, Generator>{ProcessManager: () => mockProcessManager}); }, overrides: <Type, Generator>{ProcessManager: () => mockProcessManager});
}); });
group('shutdownHooks', () { group('shutdownHooks', () {
testUsingContext('runInExpectedOrder', () async { testUsingContext('runInExpectedOrder', () async {
int i = 1; int i = 1;
...@@ -60,6 +63,7 @@ void main() { ...@@ -60,6 +63,7 @@ void main() {
expect(cleanup, 4); expect(cleanup, 4);
}); });
}); });
group('output formatting', () { group('output formatting', () {
MockProcessManager mockProcessManager; MockProcessManager mockProcessManager;
BufferLogger mockLogger; BufferLogger mockLogger;
...@@ -90,6 +94,49 @@ void main() { ...@@ -90,6 +94,49 @@ void main() {
Platform: () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false, Platform: () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false,
}); });
}); });
group('runAsync timeout and retry', () {
const Duration delay = Duration(seconds: 2);
MockProcessManager flakyProcessManager;
setUp(() {
// MockProcessManager has an implementation of start() that returns the
// result of processFactory.
flakyProcessManager = MockProcessManager();
flakyProcessManager.processFactory = flakyProcessFactory(1, delay: delay);
});
testUsingContext('flaky process fails without retry', () async {
final RunResult result = await runAsync(
<String>['dummy'],
timeout: delay + const Duration(seconds: 1),
);
expect(result.exitCode, -9);
}, overrides: <Type, Generator>{
ProcessManager: () => flakyProcessManager,
});
testUsingContext('flaky process succeeds with retry', () async {
final RunResult result = await runAsync(
<String>['dummy'],
timeout: delay - const Duration(milliseconds: 500),
timeoutRetries: 1,
);
expect(result.exitCode, 0);
}, overrides: <Type, Generator>{
ProcessManager: () => flakyProcessManager,
});
testUsingContext('flaky process generates ProcessException on timeout', () async {
expect(() async => await runAsync(
<String>['dummy'],
timeout: delay - const Duration(milliseconds: 500),
timeoutRetries: 0,
), throwsA(isInstanceOf<ProcessException>()));
}, overrides: <Type, Generator>{
ProcessManager: () => flakyProcessManager,
});
});
} }
class PlainMockProcessManager extends Mock implements ProcessManager {} class PlainMockProcessManager extends Mock implements ProcessManager {}
...@@ -17,19 +17,20 @@ import 'package:process/process.dart'; ...@@ -17,19 +17,20 @@ import 'package:process/process.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/mocks.dart' as mocks;
import '../../src/pubspec_schema.dart'; import '../../src/pubspec_schema.dart';
const String xcodebuild = '/usr/bin/xcodebuild'; const String xcodebuild = '/usr/bin/xcodebuild';
void main() { void main() {
group('xcodebuild versioning', () { group('xcodebuild versioning', () {
MockProcessManager mockProcessManager; mocks.MockProcessManager mockProcessManager;
XcodeProjectInterpreter xcodeProjectInterpreter; XcodeProjectInterpreter xcodeProjectInterpreter;
FakePlatform macOS; FakePlatform macOS;
FileSystem fs; FileSystem fs;
setUp(() { setUp(() {
mockProcessManager = MockProcessManager(); mockProcessManager = mocks.MockProcessManager();
xcodeProjectInterpreter = XcodeProjectInterpreter(); xcodeProjectInterpreter = XcodeProjectInterpreter();
macOS = fakePlatform('macos'); macOS = fakePlatform('macos');
fs = MemoryFileSystem(); fs = MemoryFileSystem();
...@@ -149,6 +150,23 @@ void main() { ...@@ -149,6 +150,23 @@ void main() {
.thenReturn(ProcessResult(0, 1, '', '')); .thenReturn(ProcessResult(0, 1, '', ''));
expect(xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{}); expect(xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
}); });
testUsingContext('build settings flakes', () async {
const Duration delay = Duration(seconds: 1);
mockProcessManager.processFactory =
mocks.flakyProcessFactory(1, delay: delay + const Duration(seconds: 1));
expect(await xcodeProjectInterpreter.getBuildSettingsAsync(
'', '', timeout: delay),
const <String, String>{});
// build settings times out and is killed once, then succeeds.
verify(mockProcessManager.killPid(any)).called(1);
// The verbose logs should tell us something timed out.
expect(testLogger.traceText, contains('timed out'));
}, overrides: <Type, Generator>{
Platform: () => macOS,
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
}); });
group('Xcode project properties', () { group('Xcode project properties', () {
test('properties from default project can be parsed', () { test('properties from default project can be parsed', () {
......
...@@ -164,7 +164,7 @@ void main() { ...@@ -164,7 +164,7 @@ void main() {
group('Setup Podfile', () { group('Setup Podfile', () {
testUsingContext('creates objective-c Podfile when not present', () async { testUsingContext('creates objective-c Podfile when not present', () async {
cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios); await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template'); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -173,12 +173,13 @@ void main() { ...@@ -173,12 +173,13 @@ void main() {
testUsingContext('creates swift Podfile if swift', () async { testUsingContext('creates swift Podfile if swift', () async {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{ when(mockXcodeProjectInterpreter.getBuildSettingsAsync(any, any))
.thenAnswer((_) async => <String, String>{
'SWIFT_VERSION': '4.0', 'SWIFT_VERSION': '4.0',
}); });
final FlutterProject project = FlutterProject.fromPath('project'); final FlutterProject project = FlutterProject.fromPath('project');
cocoaPodsUnderTest.setupPodfile(project.ios); await cocoaPodsUnderTest.setupPodfile(project.ios);
expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template'); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -188,7 +189,7 @@ void main() { ...@@ -188,7 +189,7 @@ void main() {
testUsingContext('creates macOS Podfile when not present', () async { testUsingContext('creates macOS Podfile when not present', () async {
projectUnderTest.macos.xcodeProject.createSync(recursive: true); projectUnderTest.macos.xcodeProject.createSync(recursive: true);
cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos); await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos);
expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template'); expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -199,7 +200,7 @@ void main() { ...@@ -199,7 +200,7 @@ void main() {
projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
final FlutterProject project = FlutterProject.fromPath('project'); final FlutterProject project = FlutterProject.fromPath('project');
cocoaPodsUnderTest.setupPodfile(project.ios); await cocoaPodsUnderTest.setupPodfile(project.ios);
expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile'); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -210,7 +211,7 @@ void main() { ...@@ -210,7 +211,7 @@ void main() {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
final FlutterProject project = FlutterProject.fromPath('project'); final FlutterProject project = FlutterProject.fromPath('project');
cocoaPodsUnderTest.setupPodfile(project.ios); await cocoaPodsUnderTest.setupPodfile(project.ios);
expect(projectUnderTest.ios.podfile.existsSync(), false); expect(projectUnderTest.ios.podfile.existsSync(), false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -228,7 +229,7 @@ void main() { ...@@ -228,7 +229,7 @@ void main() {
..writeAsStringSync('Existing release config'); ..writeAsStringSync('Existing release config');
final FlutterProject project = FlutterProject.fromPath('project'); final FlutterProject project = FlutterProject.fromPath('project');
cocoaPodsUnderTest.setupPodfile(project.ios); await cocoaPodsUnderTest.setupPodfile(project.ios);
final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync(); final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
expect(debugContents, contains( expect(debugContents, contains(
......
...@@ -244,9 +244,11 @@ void main() { ...@@ -244,9 +244,11 @@ void main() {
group('language', () { group('language', () {
MockXcodeProjectInterpreter mockXcodeProjectInterpreter; MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
MemoryFileSystem fs; MemoryFileSystem fs;
FlutterProjectFactory flutterProjectFactory;
setUp(() { setUp(() {
fs = MemoryFileSystem(); fs = MemoryFileSystem();
mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
flutterProjectFactory = FlutterProjectFactory();
}); });
testInMemory('default host app language', () async { testInMemory('default host app language', () async {
...@@ -273,6 +275,7 @@ apply plugin: 'kotlin-android' ...@@ -273,6 +275,7 @@ apply plugin: 'kotlin-android'
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => fs, FileSystem: () => fs,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
}); });
}); });
...@@ -280,10 +283,12 @@ apply plugin: 'kotlin-android' ...@@ -280,10 +283,12 @@ apply plugin: 'kotlin-android'
MemoryFileSystem fs; MemoryFileSystem fs;
MockPlistUtils mockPlistUtils; MockPlistUtils mockPlistUtils;
MockXcodeProjectInterpreter mockXcodeProjectInterpreter; MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
FlutterProjectFactory flutterProjectFactory;
setUp(() { setUp(() {
fs = MemoryFileSystem(); fs = MemoryFileSystem();
mockPlistUtils = MockPlistUtils(); mockPlistUtils = MockPlistUtils();
mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
flutterProjectFactory = FlutterProjectFactory();
}); });
void testWithMocks(String description, Future<void> testMethod()) { void testWithMocks(String description, Future<void> testMethod()) {
...@@ -291,6 +296,7 @@ apply plugin: 'kotlin-android' ...@@ -291,6 +296,7 @@ apply plugin: 'kotlin-android'
FileSystem: () => fs, FileSystem: () => fs,
PlistParser: () => mockPlistUtils, PlistParser: () => mockPlistUtils,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
}); });
} }
...@@ -425,9 +431,11 @@ apply plugin: 'kotlin-android' ...@@ -425,9 +431,11 @@ apply plugin: 'kotlin-android'
group('Regression test for invalid pubspec', () { group('Regression test for invalid pubspec', () {
Testbed testbed; Testbed testbed;
FlutterProjectFactory flutterProjectFactory;
setUp(() { setUp(() {
testbed = Testbed(); testbed = Testbed();
flutterProjectFactory = FlutterProjectFactory();
}); });
test('Handles asking for builders from an invalid pubspec', () => testbed.run(() { test('Handles asking for builders from an invalid pubspec', () => testbed.run(() {
...@@ -439,6 +447,8 @@ apply plugin: 'kotlin-android' ...@@ -439,6 +447,8 @@ apply plugin: 'kotlin-android'
final FlutterProject flutterProject = FlutterProject.current(); final FlutterProject flutterProject = FlutterProject.current();
expect(flutterProject.builders, null); expect(flutterProject.builders, null);
}, overrides: <Type, Generator>{
FlutterProjectFactory: () => flutterProjectFactory,
})); }));
test('Handles asking for builders from a trivial pubspec', () => testbed.run(() { test('Handles asking for builders from a trivial pubspec', () => testbed.run(() {
...@@ -451,6 +461,8 @@ name: foo_bar ...@@ -451,6 +461,8 @@ name: foo_bar
final FlutterProject flutterProject = FlutterProject.current(); final FlutterProject flutterProject = FlutterProject.current();
expect(flutterProject.builders, null); expect(flutterProject.builders, null);
}, overrides: <Type, Generator>{
FlutterProjectFactory: () => flutterProjectFactory,
})); }));
}); });
} }
...@@ -510,12 +522,16 @@ void testInMemory(String description, Future<void> testMethod()) { ...@@ -510,12 +522,16 @@ void testInMemory(String description, Future<void> testMethod()) {
.childDirectory('packages') .childDirectory('packages')
.childDirectory('flutter_tools') .childDirectory('flutter_tools')
.childDirectory('schema'), testFileSystem); .childDirectory('schema'), testFileSystem);
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory();
testUsingContext( testUsingContext(
description, description,
testMethod, testMethod,
overrides: <Type, Generator>{ overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
Cache: () => Cache(), Cache: () => Cache(),
FlutterProjectFactory: () => flutterProjectFactory,
}, },
); );
} }
......
...@@ -335,6 +335,15 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { ...@@ -335,6 +335,15 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {
return <String, String>{}; return <String, String>{};
} }
@override
Future<Map<String, String>> getBuildSettingsAsync(
String projectPath,
String target, {
Duration timeout = const Duration(minutes: 1),
}) async {
return <String, String>{};
}
@override @override
void cleanWorkspace(String workspacePath, String scheme) { void cleanWorkspace(String workspacePath, String scheme) {
} }
......
...@@ -182,6 +182,26 @@ class MockProcessManager extends Mock implements ProcessManager { ...@@ -182,6 +182,26 @@ class MockProcessManager extends Mock implements ProcessManager {
} }
} }
/// A function that generates a process factory that gives processes that fail
/// a given number of times before succeeding. The returned processes will
/// fail after a delay if one is supplied.
ProcessFactory flakyProcessFactory(int flakes, {Duration delay}) {
int flakesLeft = flakes;
return (List<String> command) {
if (flakesLeft == 0) {
return MockProcess(exitCode: Future<int>.value(0));
}
flakesLeft = flakesLeft - 1;
Future<int> exitFuture;
if (delay == null) {
exitFuture = Future<int>.value(-9);
} else {
exitFuture = Future<int>.delayed(delay, () => Future<int>.value(-9));
}
return MockProcess(exitCode: exitFuture);
};
}
/// A process that exits successfully with no output and ignores all input. /// A process that exits successfully with no output and ignores all input.
class MockProcess extends Mock implements Process { class MockProcess extends Mock implements Process {
MockProcess({ MockProcess({
......
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