blob: 027147a5cae0c1ec25a2adfaa444035e2169a2b3 [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 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/macos/migrations/flutter_application_migration.dart';
import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart';
import 'package:flutter_tools/src/macos/migrations/nsapplicationmain_deprecation_migration.dart';
import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
void main() {
group('remove link and embed migration', () {
late TestUsage testUsage;
late FakeAnalytics fakeAnalytics;
late MemoryFileSystem memoryFileSystem;
late BufferLogger testLogger;
late FakeMacOSProject macOSProject;
late File xcodeProjectInfoFile;
setUp(() {
testUsage = TestUsage();
memoryFileSystem = MemoryFileSystem.test();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: memoryFileSystem,
fakeFlutterVersion: FakeFlutterVersion(),
);
xcodeProjectInfoFile = memoryFileSystem.file('project.pbxproj');
testLogger = BufferLogger.test();
macOSProject = FakeMacOSProject();
macOSProject.xcodeProjectInfoFile = xcodeProjectInfoFile;
});
testWithoutContext('skipped if files are missing', () async {
final RemoveMacOSFrameworkLinkAndEmbeddingMigration macosProjectMigration =
RemoveMacOSFrameworkLinkAndEmbeddingMigration(
macOSProject,
testLogger,
testUsage,
fakeAnalytics,
);
await macosProjectMigration.migrate();
expect(testUsage.events, isEmpty);
expect(fakeAnalytics.sentEvents, isEmpty);
expect(xcodeProjectInfoFile.existsSync(), isFalse);
expect(
testLogger.traceText,
contains(
'Xcode project not found, skipping framework link and embedding migration'));
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if nothing to upgrade', () async {
const String contents = 'Nothing to upgrade';
xcodeProjectInfoFile.writeAsStringSync(contents);
final DateTime projectLastModified =
xcodeProjectInfoFile.lastModifiedSync();
final RemoveMacOSFrameworkLinkAndEmbeddingMigration macosProjectMigration =
RemoveMacOSFrameworkLinkAndEmbeddingMigration(
macOSProject,
testLogger,
testUsage,
fakeAnalytics,
);
await macosProjectMigration.migrate();
expect(testUsage.events, isEmpty);
expect(fakeAnalytics.sentEvents, isEmpty);
expect(xcodeProjectInfoFile.lastModifiedSync(), projectLastModified);
expect(xcodeProjectInfoFile.readAsStringSync(), contents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skips migrating script with embed', () async {
const String contents = r'''
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
''';
xcodeProjectInfoFile.writeAsStringSync(contents);
final RemoveMacOSFrameworkLinkAndEmbeddingMigration macosProjectMigration =
RemoveMacOSFrameworkLinkAndEmbeddingMigration(
macOSProject,
testLogger,
testUsage,
fakeAnalytics,
);
await macosProjectMigration.migrate();
expect(xcodeProjectInfoFile.readAsStringSync(), contents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('Xcode project is migrated', () async {
xcodeProjectInfoFile.writeAsStringSync(r'''
prefix D73912F022F37F9E000D13A0
D73912F222F3801D000D13A0 suffix
D73912EF22F37F9E000D13A0
keep this 1
33D1A10422148B71006C7A3E spaces
33D1A10522148B93006C7A3E
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n";
keep this 2
''');
final RemoveMacOSFrameworkLinkAndEmbeddingMigration macosProjectMigration =
RemoveMacOSFrameworkLinkAndEmbeddingMigration(
macOSProject,
testLogger,
testUsage,
fakeAnalytics,
);
await macosProjectMigration.migrate();
expect(testUsage.events, isEmpty);
expect(fakeAnalytics.sentEvents, isEmpty);
expect(xcodeProjectInfoFile.readAsStringSync(), r'''
keep this 1
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
keep this 2
''');
expect(testLogger.statusText, contains('Upgrading project.pbxproj'));
});
testWithoutContext('migration fails with leftover App.framework reference', () {
xcodeProjectInfoFile.writeAsStringSync('''
D73912F022F37F9bogus /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912F022F37F9bogus /* App.framework */; };
''');
final RemoveMacOSFrameworkLinkAndEmbeddingMigration macosProjectMigration =
RemoveMacOSFrameworkLinkAndEmbeddingMigration(
macOSProject,
testLogger,
testUsage,
fakeAnalytics,
);
expect(macosProjectMigration.migrate,
throwsToolExit(message: 'Your Xcode project requires migration'));
expect(testUsage.events, contains(
const TestUsageEvent('macos-migration', 'remove-frameworks', label: 'failure'),
));
expect(fakeAnalytics.sentEvents, contains(
Event.appleUsageEvent(
workflow: 'macos-migration',
parameter: 'remove-frameworks',
result: 'failure',
)
));
});
testWithoutContext(
'migration fails with leftover FlutterMacOS.framework reference', () {
xcodeProjectInfoFile.writeAsStringSync('''
33D1A10522148B93bogus /* FlutterMacOS.framework in Bundle Framework */,
''');
final RemoveMacOSFrameworkLinkAndEmbeddingMigration macosProjectMigration =
RemoveMacOSFrameworkLinkAndEmbeddingMigration(
macOSProject,
testLogger,
testUsage,
fakeAnalytics,
);
expect(macosProjectMigration.migrate,
throwsToolExit(message: 'Your Xcode project requires migration'));
expect(testUsage.events, contains(
const TestUsageEvent('macos-migration', 'remove-frameworks', label: 'failure'),
));
expect(fakeAnalytics.sentEvents, contains(
Event.appleUsageEvent(
workflow: 'macos-migration',
parameter: 'remove-frameworks',
result: 'failure',
)
));
});
});
group('update deployment target version', () {
late MemoryFileSystem memoryFileSystem;
late BufferLogger testLogger;
late FakeMacOSProject project;
late File xcodeProjectInfoFile;
late File podfile;
setUp(() {
memoryFileSystem = MemoryFileSystem();
testLogger = BufferLogger.test();
project = FakeMacOSProject();
xcodeProjectInfoFile = memoryFileSystem.file('project.pbxproj');
project.xcodeProjectInfoFile = xcodeProjectInfoFile;
podfile = memoryFileSystem.file('Podfile');
project.podfile = podfile;
});
testWithoutContext('skipped if files are missing', () async {
final MacOSDeploymentTargetMigration macOSProjectMigration = MacOSDeploymentTargetMigration(
project,
testLogger,
);
await macOSProjectMigration.migrate();
expect(xcodeProjectInfoFile.existsSync(), isFalse);
expect(podfile.existsSync(), isFalse);
expect(testLogger.traceText, contains('Xcode project not found, skipping macOS deployment target version migration'));
expect(testLogger.traceText, contains('Podfile not found, skipping global platform macOS version migration'));
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if nothing to upgrade', () async {
const String xcodeProjectInfoFileContents = 'MACOSX_DEPLOYMENT_TARGET = 10.14;';
xcodeProjectInfoFile.writeAsStringSync(xcodeProjectInfoFileContents);
final DateTime projectLastModified = xcodeProjectInfoFile.lastModifiedSync();
const String podfileFileContents = "# platform :osx, '10.14'";
podfile.writeAsStringSync(podfileFileContents);
final DateTime podfileLastModified = podfile.lastModifiedSync();
final MacOSDeploymentTargetMigration macOSProjectMigration = MacOSDeploymentTargetMigration(
project,
testLogger,
);
await macOSProjectMigration.migrate();
expect(xcodeProjectInfoFile.lastModifiedSync(), projectLastModified);
expect(xcodeProjectInfoFile.readAsStringSync(), xcodeProjectInfoFileContents);
expect(podfile.lastModifiedSync(), podfileLastModified);
expect(podfile.readAsStringSync(), podfileFileContents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('Xcode project is migrated from 10.11 to 10.14', () async {
xcodeProjectInfoFile.writeAsStringSync('''
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = YES;
''');
podfile.writeAsStringSync('''
# platform :osx, '10.11'
platform :osx, '10.11'
''');
final MacOSDeploymentTargetMigration macOSProjectMigration = MacOSDeploymentTargetMigration(
project,
testLogger,
);
await macOSProjectMigration.migrate();
expect(xcodeProjectInfoFile.readAsStringSync(), '''
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
''');
expect(podfile.readAsStringSync(), '''
# platform :osx, '10.14'
platform :osx, '10.14'
''');
// Only print once even though 2 lines were changed.
expect('Updating minimum macOS deployment target to 10.14'.allMatches(testLogger.statusText).length, 1);
});
testWithoutContext('Xcode project is migrated from 10.13 to 10.14', () async {
xcodeProjectInfoFile.writeAsStringSync('''
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = YES;
''');
podfile.writeAsStringSync('''
# platform :osx, '10.13'
platform :osx, '10.13'
''');
final MacOSDeploymentTargetMigration macOSProjectMigration = MacOSDeploymentTargetMigration(
project,
testLogger,
);
await macOSProjectMigration.migrate();
expect(xcodeProjectInfoFile.readAsStringSync(), '''
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
''');
expect(podfile.readAsStringSync(), '''
# platform :osx, '10.14'
platform :osx, '10.14'
''');
// Only print once even though 2 lines were changed.
expect('Updating minimum macOS deployment target to 10.14'.allMatches(testLogger.statusText).length, 1);
});
});
group('update NSPrincipalClass from FlutterApplication to NSApplication', () {
late MemoryFileSystem memoryFileSystem;
late BufferLogger testLogger;
late FakeMacOSProject project;
late File infoPlistFile;
late FakePlistParser fakePlistParser;
late FlutterProjectFactory flutterProjectFactory;
setUp(() {
memoryFileSystem = MemoryFileSystem();
fakePlistParser = FakePlistParser();
testLogger = BufferLogger.test();
project = FakeMacOSProject();
infoPlistFile = memoryFileSystem.file('Info.plist');
project.defaultHostInfoPlist = infoPlistFile;
flutterProjectFactory = FlutterProjectFactory(
fileSystem: memoryFileSystem,
logger: testLogger,
);
});
void testWithMocks(String description, Future<void> Function() testMethod) {
testUsingContext(description, testMethod, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => fakePlistParser,
FlutterProjectFactory: () => flutterProjectFactory,
});
}
testWithMocks('skipped if files are missing', () async {
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
project,
testLogger,
);
await macOSProjectMigration.migrate();
expect(infoPlistFile.existsSync(), isFalse);
expect(testLogger.traceText, isEmpty);
expect(testLogger.statusText, isEmpty);
});
testWithMocks('skipped if no NSPrincipalClass key exists to upgrade', () async {
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
project,
testLogger,
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
await macOSProjectMigration.migrate();
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
expect(testLogger.statusText, isEmpty);
});
testWithMocks('skipped if already de-upgraded (or never migrated)', () async {
fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'NSApplication');
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
project,
testLogger,
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
await macOSProjectMigration.migrate();
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
expect(testLogger.statusText, isEmpty);
});
testWithMocks('Info.plist migrated to use NSApplication', () async {
fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'FlutterApplication');
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
project,
testLogger,
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
await macOSProjectMigration.migrate();
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
// Only print once.
expect('Updating ${infoPlistFile.basename} to use NSApplication instead of FlutterApplication.'.allMatches(testLogger.statusText).length, 1);
});
testWithMocks('Skip if NSPrincipalClass is not NSApplication', () async {
const String differentApp = 'DIFFERENTApplication';
fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, differentApp);
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
project,
testLogger,
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
await macOSProjectMigration.migrate();
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
expect(testLogger.traceText, isEmpty);
});
});
group('migrate @NSApplicationMain attribute to @main', () {
late MemoryFileSystem memoryFileSystem;
late BufferLogger testLogger;
late FakeMacOSProject project;
late File appDelegateFile;
setUp(() {
memoryFileSystem = MemoryFileSystem();
testLogger = BufferLogger.test();
project = FakeMacOSProject();
appDelegateFile = memoryFileSystem.file('AppDelegate.swift');
project.appDelegateSwift = appDelegateFile;
});
testWithoutContext('skipped if files are missing', () async {
final NSApplicationMainDeprecationMigration migration = NSApplicationMainDeprecationMigration(
project,
testLogger,
);
await migration.migrate();
expect(appDelegateFile.existsSync(), isFalse);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if nothing to upgrade', () async {
const String appDelegateContents = '''
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
''';
appDelegateFile.writeAsStringSync(appDelegateContents);
final DateTime lastModified = appDelegateFile.lastModifiedSync();
final NSApplicationMainDeprecationMigration migration = NSApplicationMainDeprecationMigration(
project,
testLogger,
);
await migration.migrate();
expect(appDelegateFile.lastModifiedSync(), lastModified);
expect(appDelegateFile.readAsStringSync(), appDelegateContents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('updates AppDelegate.swift', () async {
appDelegateFile.writeAsStringSync('''
import Cocoa
import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
''');
final NSApplicationMainDeprecationMigration migration = NSApplicationMainDeprecationMigration(
project,
testLogger,
);
await migration.migrate();
expect(appDelegateFile.readAsStringSync(), '''
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
''');
expect(testLogger.warningText, contains('uses the deprecated @NSApplicationMain attribute, updating'));
});
});
}
class FakeMacOSProject extends Fake implements MacOSProject {
@override
File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile');
@override
File defaultHostInfoPlist = MemoryFileSystem.test().file('InfoplistFile');
@override
File podfile = MemoryFileSystem.test().file('Podfile');
@override
File appDelegateSwift = MemoryFileSystem.test().file('AppDelegate.swift');
}