flutter/packages/flutter_tools/test/general.shard/macos/application_package_test.dart
Andy Weiss c4ceea397a
[flutter_tools] Support zipped application bundles for macOS (#68854)
* [flutter_tools] Support zipped application bundles for macOS

It is not possible to directly produce a directory (.app) in some build systems
but rather it must be zip'ed before being passed to the tool for
running. This adds support for attempting to extract an application
bundle from a zip file if the bundle is not already a directory. This
uses very similar code from lib/src/application_package.dart which is
used for extracting an ipa for iOS.

This introduces tests for the macos/application_package.dart behavior which did not exist before. These tests cover the changes in the PR and some of the existing behavior, but do not cover everything in that file.
2020-11-02 08:58:33 -08:00

231 lines
8.6 KiB
Dart

// 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:convert';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/macos/application_package.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group('PrebuiltMacOSApp', () {
MockOperatingSystemUtils os;
final Map<Type, Generator> overrides = <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => MockPlistUtils(),
Platform: _kNoColorTerminalPlatform,
OperatingSystemUtils: () => os,
};
setUp(() {
os = MockOperatingSystemUtils();
});
testUsingContext('Error on non-existing file', () {
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('not_existing.app'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(
testLogger.errorText,
'File "not_existing.app" does not exist.\n',
);
}, overrides: overrides);
testUsingContext('Error on non-app-bundle folder', () {
globals.fs.directory('regular_folder').createSync();
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('regular_folder'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(testLogger.errorText,
'Folder "regular_folder" is not an app bundle.\n');
}, overrides: overrides);
testUsingContext('Error on no info.plist', () {
globals.fs.directory('bundle.app').createSync();
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('bundle.app'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(
testLogger.errorText,
'Invalid prebuilt macOS app. Does not contain Info.plist.\n',
);
}, overrides: overrides);
testUsingContext('Error on info.plist missing bundle identifier', () {
final String contentsDirectory =
globals.fs.path.join('bundle.app', 'Contents');
globals.fs.directory(contentsDirectory).createSync(recursive: true);
globals.fs
.file(globals.fs.path.join('bundle.app', 'Contents', 'Info.plist'))
.writeAsStringSync(badPlistData);
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('bundle.app'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(
testLogger.errorText,
contains(
'Invalid prebuilt macOS app. Info.plist does not contain bundle identifier\n'),
);
}, overrides: overrides);
testUsingContext('Error on info.plist missing executable', () {
final String contentsDirectory =
globals.fs.path.join('bundle.app', 'Contents');
globals.fs.directory(contentsDirectory).createSync(recursive: true);
globals.fs
.file(globals.fs.path.join('bundle.app', 'Contents', 'Info.plist'))
.writeAsStringSync(badPlistDataNoExecutable);
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('bundle.app'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(
testLogger.errorText,
contains(
'Invalid prebuilt macOS app. Info.plist does not contain bundle executable\n'),
);
}, overrides: overrides);
testUsingContext('Success with app bundle', () {
final String appDirectory =
globals.fs.path.join('bundle.app', 'Contents', 'MacOS');
globals.fs.directory(appDirectory).createSync(recursive: true);
globals.fs
.file(globals.fs.path.join('bundle.app', 'Contents', 'Info.plist'))
.writeAsStringSync(plistData);
globals.fs
.file(globals.fs.path.join(appDirectory, executableName))
.createSync();
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('bundle.app'))
as PrebuiltMacOSApp;
expect(testLogger.errorText, isEmpty);
expect(macosApp.bundleDir.path, 'bundle.app');
expect(macosApp.id, 'fooBundleId');
expect(macosApp.bundleName, 'bundle.app');
}, overrides: overrides);
testUsingContext('Bad zipped app, no payload dir', () {
globals.fs.file('app.zip').createSync();
when(os.unzip(globals.fs.file('app.zip'), any))
.thenAnswer((Invocation _) {});
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('app.zip'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(
testLogger.errorText,
'Archive "app.zip" does not contain a single app bundle.\n',
);
}, overrides: overrides);
testUsingContext('Bad zipped app, two app bundles', () {
globals.fs.file('app.zip').createSync();
when(os.unzip(any, any)).thenAnswer((Invocation invocation) {
final File zipFile = invocation.positionalArguments[0] as File;
if (zipFile.path != 'app.zip') {
return;
}
final Directory targetDirectory =
invocation.positionalArguments[1] as Directory;
final String bundlePath1 =
globals.fs.path.join(targetDirectory.path, 'bundle1.app');
final String bundlePath2 =
globals.fs.path.join(targetDirectory.path, 'bundle2.app');
globals.fs.directory(bundlePath1).createSync(recursive: true);
globals.fs.directory(bundlePath2).createSync(recursive: true);
});
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('app.zip'))
as PrebuiltMacOSApp;
expect(macosApp, isNull);
expect(testLogger.errorText,
'Archive "app.zip" does not contain a single app bundle.\n');
}, overrides: overrides);
testUsingContext('Success with zipped app', () {
globals.fs.file('app.zip').createSync();
when(os.unzip(any, any)).thenAnswer((Invocation invocation) {
final File zipFile = invocation.positionalArguments[0] as File;
if (zipFile.path != 'app.zip') {
return;
}
final Directory targetDirectory =
invocation.positionalArguments[1] as Directory;
final Directory bundleAppContentsDir = globals.fs.directory(globals
.fs.path
.join(targetDirectory.path, 'bundle.app', 'Contents'));
bundleAppContentsDir.createSync(recursive: true);
globals.fs
.file(globals.fs.path.join(bundleAppContentsDir.path, 'Info.plist'))
.writeAsStringSync(plistData);
globals.fs
.directory(globals.fs.path.join(bundleAppContentsDir.path, 'MacOS'))
.createSync();
globals.fs
.file(globals.fs.path
.join(bundleAppContentsDir.path, 'MacOS', executableName))
.createSync();
});
final PrebuiltMacOSApp macosApp =
MacOSApp.fromPrebuiltApp(globals.fs.file('app.zip'))
as PrebuiltMacOSApp;
expect(testLogger.errorText, isEmpty);
expect(macosApp.bundleDir.path, endsWith('bundle.app'));
expect(macosApp.id, 'fooBundleId');
expect(macosApp.bundleName, endsWith('bundle.app'));
}, overrides: overrides);
});
}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
final Generator _kNoColorTerminalPlatform =
() => FakePlatform(stdoutSupportsAnsi: false);
final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
Platform: _kNoColorTerminalPlatform,
};
class MockPlistUtils extends Mock implements PlistParser {
@override
Map<String, dynamic> parseFile(String plistFilePath) {
final File file = globals.fs.file(plistFilePath);
if (!file.existsSync()) {
return <String, dynamic>{};
}
return castStringKeyedMap(json.decode(file.readAsStringSync()));
}
}
// Contains no bundle identifier.
const String badPlistData = '''
{}
''';
// Contains no bundle executable.
const String badPlistDataNoExecutable = '''
{"CFBundleIdentifier": "fooBundleId"}
''';
const String executableName = 'foo';
const String plistData = '''
{"CFBundleIdentifier": "fooBundleId", "CFBundleExecutable": "$executableName"}
''';