[flutter_tool] Use a timeout for xcode showBuildSettings (#39280)
This commit is contained in:
parent
ce3c467292
commit
b7c714e84c
@ -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(
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|
||||||
final bool isSwift = xcodeProjectInterpreter.getBuildSettings(
|
|
||||||
runnerProject.path,
|
|
||||||
'Runner',
|
|
||||||
).containsKey('SWIFT_VERSION');
|
|
||||||
podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
|
|
||||||
}
|
|
||||||
final File podfileTemplate = fs.file(fs.path.join(
|
|
||||||
Cache.flutterRoot,
|
|
||||||
'packages',
|
|
||||||
'flutter_tools',
|
|
||||||
'templates',
|
|
||||||
'cocoapods',
|
|
||||||
podfileTemplateName,
|
|
||||||
));
|
|
||||||
podfileTemplate.copySync(podfile.path);
|
|
||||||
}
|
}
|
||||||
|
String podfileTemplateName;
|
||||||
|
if (xcodeProject is MacOSProject) {
|
||||||
|
podfileTemplateName = 'Podfile-macos';
|
||||||
|
} else {
|
||||||
|
final bool isSwift = (await xcodeProjectInterpreter.getBuildSettingsAsync(
|
||||||
|
runnerProject.path,
|
||||||
|
'Runner',
|
||||||
|
)).containsKey('SWIFT_VERSION');
|
||||||
|
podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
|
||||||
|
}
|
||||||
|
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})
|
|||||||
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;
|
|||||||
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 {
|
|||||||
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';
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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';
|
|||||||
|
|
||||||
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() {
|
|||||||
.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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
..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() {
|
|||||||
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'
|
|||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => fs,
|
FileSystem: () => fs,
|
||||||
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
|
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
|
||||||
|
FlutterProjectFactory: () => flutterProjectFactory,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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'
|
|||||||
FileSystem: () => fs,
|
FileSystem: () => fs,
|
||||||
PlistParser: () => mockPlistUtils,
|
PlistParser: () => mockPlistUtils,
|
||||||
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
|
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
|
||||||
|
FlutterProjectFactory: () => flutterProjectFactory,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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'
|
|||||||
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
|
|||||||
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()) {
|
|||||||
.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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user