| // 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/io.dart'; |
| |
| import '../../bin/xcode_backend.dart'; |
| import '../src/common.dart' hide Context; |
| import '../src/fake_process_manager.dart'; |
| |
| void main() { |
| late MemoryFileSystem fileSystem; |
| |
| setUp(() { |
| fileSystem = MemoryFileSystem(); |
| }); |
| |
| group('build', () { |
| test('exits with useful error message when build mode not set', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds') |
| ..createSync(recursive: true); |
| final Directory flutterRoot = fileSystem.directory('/path/to/flutter') |
| ..createSync(recursive: true); |
| final File pipe = fileSystem.file('/tmp/pipe') |
| ..createSync(recursive: true); |
| const String buildMode = 'Debug'; |
| final TestContext context = TestContext( |
| <String>['build'], |
| <String, String>{ |
| 'ACTION': 'build', |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'FLUTTER_ROOT': flutterRoot.path, |
| 'INFOPLIST_PATH': 'Info.plist', |
| }, |
| commands: <FakeCommand>[ |
| FakeCommand( |
| command: <String>[ |
| '${flutterRoot.path}/bin/flutter', |
| 'assemble', |
| '--no-version-check', |
| '--output=${buildDir.path}/', |
| '-dTargetPlatform=ios', |
| '-dTargetFile=lib/main.dart', |
| '-dBuildMode=${buildMode.toLowerCase()}', |
| '-dIosArchs=', |
| '-dSdkRoot=', |
| '-dSplitDebugInfo=', |
| '-dTreeShakeIcons=', |
| '-dTrackWidgetCreation=', |
| '-dDartObfuscation=', |
| '-dAction=build', |
| '-dFrontendServerStarterPath=', |
| '--ExtraGenSnapshotOptions=', |
| '--DartDefines=', |
| '--ExtraFrontEndOptions=', |
| 'debug_ios_bundle_flutter_assets', |
| ], |
| ), |
| ], |
| fileSystem: fileSystem, |
| scriptOutputStreamFile: pipe, |
| ); |
| expect( |
| () => context.run(), |
| throwsException, |
| ); |
| expect( |
| context.stderr, |
| contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'), |
| ); |
| }); |
| test('calls flutter assemble', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds') |
| ..createSync(recursive: true); |
| final Directory flutterRoot = fileSystem.directory('/path/to/flutter') |
| ..createSync(recursive: true); |
| final File pipe = fileSystem.file('/tmp/pipe') |
| ..createSync(recursive: true); |
| const String buildMode = 'Debug'; |
| final TestContext context = TestContext( |
| <String>['build'], |
| <String, String>{ |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'CONFIGURATION': buildMode, |
| 'FLUTTER_ROOT': flutterRoot.path, |
| 'INFOPLIST_PATH': 'Info.plist', |
| }, |
| commands: <FakeCommand>[ |
| FakeCommand( |
| command: <String>[ |
| '${flutterRoot.path}/bin/flutter', |
| 'assemble', |
| '--no-version-check', |
| '--output=${buildDir.path}/', |
| '-dTargetPlatform=ios', |
| '-dTargetFile=lib/main.dart', |
| '-dBuildMode=${buildMode.toLowerCase()}', |
| '-dIosArchs=', |
| '-dSdkRoot=', |
| '-dSplitDebugInfo=', |
| '-dTreeShakeIcons=', |
| '-dTrackWidgetCreation=', |
| '-dDartObfuscation=', |
| '-dAction=', |
| '-dFrontendServerStarterPath=', |
| '--ExtraGenSnapshotOptions=', |
| '--DartDefines=', |
| '--ExtraFrontEndOptions=', |
| 'debug_ios_bundle_flutter_assets', |
| ], |
| ), |
| ], |
| fileSystem: fileSystem, |
| scriptOutputStreamFile: pipe, |
| )..run(); |
| final List<String> streamedLines = pipe.readAsLinesSync(); |
| // Ensure after line splitting, the exact string 'done' appears |
| expect(streamedLines, contains('done')); |
| expect(streamedLines, contains(' └─Compiling, linking and signing...')); |
| expect( |
| context.stdout, |
| contains('built and packaged successfully.'), |
| ); |
| expect(context.stderr, isEmpty); |
| }); |
| |
| test('forwards all env variables to flutter assemble', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds') |
| ..createSync(recursive: true); |
| final Directory flutterRoot = fileSystem.directory('/path/to/flutter') |
| ..createSync(recursive: true); |
| const String archs = 'arm64'; |
| const String buildMode = 'Release'; |
| const String dartObfuscation = 'false'; |
| const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue'; |
| const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2'; |
| const String extraFrontEndOptions = '--some-option'; |
| const String extraGenSnapshotOptions = '--obfuscate'; |
| const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart'; |
| const String sdkRoot = '/path/to/sdk'; |
| const String splitDebugInfo = '/path/to/split/debug/info'; |
| const String trackWidgetCreation = 'true'; |
| const String treeShake = 'true'; |
| final TestContext context = TestContext( |
| <String>['build'], |
| <String, String>{ |
| 'ACTION': 'install', |
| 'ARCHS': archs, |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'CODE_SIGNING_REQUIRED': 'YES', |
| 'CONFIGURATION': buildMode, |
| 'DART_DEFINES': dartDefines, |
| 'DART_OBFUSCATION': dartObfuscation, |
| 'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity, |
| 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions, |
| 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions, |
| 'FLUTTER_ROOT': flutterRoot.path, |
| 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath, |
| 'INFOPLIST_PATH': 'Info.plist', |
| 'SDKROOT': sdkRoot, |
| 'FLAVOR': 'strawberry', |
| 'SPLIT_DEBUG_INFO': splitDebugInfo, |
| 'TRACK_WIDGET_CREATION': trackWidgetCreation, |
| 'TREE_SHAKE_ICONS': treeShake, |
| }, |
| commands: <FakeCommand>[ |
| FakeCommand( |
| command: <String>[ |
| '${flutterRoot.path}/bin/flutter', |
| 'assemble', |
| '--no-version-check', |
| '--output=${buildDir.path}/', |
| '-dTargetPlatform=ios', |
| '-dTargetFile=lib/main.dart', |
| '-dBuildMode=${buildMode.toLowerCase()}', |
| '-dFlavor=strawberry', |
| '-dIosArchs=$archs', |
| '-dSdkRoot=$sdkRoot', |
| '-dSplitDebugInfo=$splitDebugInfo', |
| '-dTreeShakeIcons=$treeShake', |
| '-dTrackWidgetCreation=$trackWidgetCreation', |
| '-dDartObfuscation=$dartObfuscation', |
| '-dAction=install', |
| '-dFrontendServerStarterPath=$frontendServerStarterPath', |
| '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions', |
| '--DartDefines=$dartDefines', |
| '--ExtraFrontEndOptions=$extraFrontEndOptions', |
| '-dCodesignIdentity=$expandedCodeSignIdentity', |
| 'release_ios_bundle_flutter_assets', |
| ], |
| ), |
| ], |
| fileSystem: fileSystem, |
| )..run(); |
| expect( |
| context.stdout, |
| contains('built and packaged successfully.'), |
| ); |
| expect(context.stderr, isEmpty); |
| }); |
| }); |
| |
| group('test_vm_service_bonjour_service', () { |
| test('handles when the Info.plist is missing', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds'); |
| buildDir.createSync(recursive: true); |
| final TestContext context = TestContext( |
| <String>['test_vm_service_bonjour_service'], |
| <String, String>{ |
| 'CONFIGURATION': 'Debug', |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'INFOPLIST_PATH': 'Info.plist', |
| }, |
| commands: <FakeCommand>[], |
| fileSystem: fileSystem, |
| )..run(); |
| expect( |
| context.stdout, |
| contains( |
| 'Info.plist does not exist. Skipping _dartVmService._tcp NSBonjourServices insertion.'), |
| ); |
| }); |
| }); |
| |
| group('prepare', () { |
| test('exits with useful error message when build mode not set', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds') |
| ..createSync(recursive: true); |
| final Directory flutterRoot = fileSystem.directory('/path/to/flutter') |
| ..createSync(recursive: true); |
| final File pipe = fileSystem.file('/tmp/pipe') |
| ..createSync(recursive: true); |
| const String buildMode = 'Debug'; |
| final TestContext context = TestContext( |
| <String>['prepare'], |
| <String, String>{ |
| 'ACTION': 'build', |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'FLUTTER_ROOT': flutterRoot.path, |
| 'INFOPLIST_PATH': 'Info.plist', |
| }, |
| commands: <FakeCommand>[ |
| FakeCommand( |
| command: <String>[ |
| '${flutterRoot.path}/bin/flutter', |
| 'assemble', |
| '--no-version-check', |
| '--output=${buildDir.path}/', |
| '-dTargetPlatform=ios', |
| '-dTargetFile=lib/main.dart', |
| '-dBuildMode=${buildMode.toLowerCase()}', |
| '-dIosArchs=', |
| '-dSdkRoot=', |
| '-dSplitDebugInfo=', |
| '-dTreeShakeIcons=', |
| '-dTrackWidgetCreation=', |
| '-dDartObfuscation=', |
| '-dAction=build', |
| '-dFrontendServerStarterPath=', |
| '--ExtraGenSnapshotOptions=', |
| '--DartDefines=', |
| '--ExtraFrontEndOptions=', |
| 'debug_unpack_ios', |
| ], |
| ), |
| ], |
| fileSystem: fileSystem, |
| scriptOutputStreamFile: pipe, |
| ); |
| expect( |
| () => context.run(), |
| throwsException, |
| ); |
| expect( |
| context.stderr, |
| contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'), |
| ); |
| }); |
| test('calls flutter assemble', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds') |
| ..createSync(recursive: true); |
| final Directory flutterRoot = fileSystem.directory('/path/to/flutter') |
| ..createSync(recursive: true); |
| final File pipe = fileSystem.file('/tmp/pipe') |
| ..createSync(recursive: true); |
| const String buildMode = 'Debug'; |
| final TestContext context = TestContext( |
| <String>['prepare'], |
| <String, String>{ |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'CONFIGURATION': buildMode, |
| 'FLUTTER_ROOT': flutterRoot.path, |
| 'INFOPLIST_PATH': 'Info.plist', |
| }, |
| commands: <FakeCommand>[ |
| FakeCommand( |
| command: <String>[ |
| '${flutterRoot.path}/bin/flutter', |
| 'assemble', |
| '--no-version-check', |
| '--output=${buildDir.path}/', |
| '-dTargetPlatform=ios', |
| '-dTargetFile=lib/main.dart', |
| '-dBuildMode=${buildMode.toLowerCase()}', |
| '-dIosArchs=', |
| '-dSdkRoot=', |
| '-dSplitDebugInfo=', |
| '-dTreeShakeIcons=', |
| '-dTrackWidgetCreation=', |
| '-dDartObfuscation=', |
| '-dAction=', |
| '-dFrontendServerStarterPath=', |
| '--ExtraGenSnapshotOptions=', |
| '--DartDefines=', |
| '--ExtraFrontEndOptions=', |
| 'debug_unpack_ios', |
| ], |
| ), |
| ], |
| fileSystem: fileSystem, |
| scriptOutputStreamFile: pipe, |
| )..run(); |
| expect(context.stderr, isEmpty); |
| }); |
| |
| test('forwards all env variables to flutter assemble', () { |
| final Directory buildDir = fileSystem.directory('/path/to/builds') |
| ..createSync(recursive: true); |
| final Directory flutterRoot = fileSystem.directory('/path/to/flutter') |
| ..createSync(recursive: true); |
| const String archs = 'arm64'; |
| const String buildMode = 'Release'; |
| const String dartObfuscation = 'false'; |
| const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue'; |
| const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2'; |
| const String extraFrontEndOptions = '--some-option'; |
| const String extraGenSnapshotOptions = '--obfuscate'; |
| const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart'; |
| const String sdkRoot = '/path/to/sdk'; |
| const String splitDebugInfo = '/path/to/split/debug/info'; |
| const String trackWidgetCreation = 'true'; |
| const String treeShake = 'true'; |
| final TestContext context = TestContext( |
| <String>['prepare'], |
| <String, String>{ |
| 'ACTION': 'install', |
| 'ARCHS': archs, |
| 'BUILT_PRODUCTS_DIR': buildDir.path, |
| 'CODE_SIGNING_REQUIRED': 'YES', |
| 'CONFIGURATION': buildMode, |
| 'DART_DEFINES': dartDefines, |
| 'DART_OBFUSCATION': dartObfuscation, |
| 'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity, |
| 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions, |
| 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions, |
| 'FLUTTER_ROOT': flutterRoot.path, |
| 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath, |
| 'INFOPLIST_PATH': 'Info.plist', |
| 'SDKROOT': sdkRoot, |
| 'FLAVOR': 'strawberry', |
| 'SPLIT_DEBUG_INFO': splitDebugInfo, |
| 'TRACK_WIDGET_CREATION': trackWidgetCreation, |
| 'TREE_SHAKE_ICONS': treeShake, |
| }, |
| commands: <FakeCommand>[ |
| FakeCommand( |
| command: <String>[ |
| '${flutterRoot.path}/bin/flutter', |
| 'assemble', |
| '--no-version-check', |
| '--output=${buildDir.path}/', |
| '-dTargetPlatform=ios', |
| '-dTargetFile=lib/main.dart', |
| '-dBuildMode=${buildMode.toLowerCase()}', |
| '-dFlavor=strawberry', |
| '-dIosArchs=$archs', |
| '-dSdkRoot=$sdkRoot', |
| '-dSplitDebugInfo=$splitDebugInfo', |
| '-dTreeShakeIcons=$treeShake', |
| '-dTrackWidgetCreation=$trackWidgetCreation', |
| '-dDartObfuscation=$dartObfuscation', |
| '-dAction=install', |
| '-dFrontendServerStarterPath=$frontendServerStarterPath', |
| '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions', |
| '--DartDefines=$dartDefines', |
| '--ExtraFrontEndOptions=$extraFrontEndOptions', |
| '-dCodesignIdentity=$expandedCodeSignIdentity', |
| 'release_unpack_ios', |
| ], |
| ), |
| ], |
| fileSystem: fileSystem, |
| )..run(); |
| expect(context.stderr, isEmpty); |
| }); |
| }); |
| } |
| |
| class TestContext extends Context { |
| TestContext( |
| List<String> arguments, |
| Map<String, String> environment, { |
| required this.fileSystem, |
| required List<FakeCommand> commands, |
| File? scriptOutputStreamFile, |
| }) : processManager = FakeProcessManager.list(commands), |
| super(arguments: arguments, environment: environment, scriptOutputStreamFile: scriptOutputStreamFile); |
| |
| final FileSystem fileSystem; |
| final FakeProcessManager processManager; |
| |
| String stdout = ''; |
| String stderr = ''; |
| |
| @override |
| bool existsFile(String path) { |
| return fileSystem.file(path).existsSync(); |
| } |
| |
| @override |
| ProcessResult runSync( |
| String bin, |
| List<String> args, { |
| bool verbose = false, |
| bool allowFail = false, |
| String? workingDirectory, |
| }) { |
| return processManager.runSync( |
| <dynamic>[bin, ...args], |
| workingDirectory: workingDirectory, |
| environment: environment, |
| ); |
| } |
| |
| @override |
| void echoError(String message) { |
| stderr += '$message\n'; |
| } |
| |
| @override |
| void echo(String message) { |
| stdout += message; |
| } |
| |
| @override |
| Never exitApp(int code) { |
| // This is an exception for the benefit of unit tests. |
| // The real implementation calls `exit(code)`. |
| throw Exception('App exited with code $code'); |
| } |
| } |