// 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'; 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>{ 'BUILT_PRODUCTS_DIR': buildDir.path, 'ENABLE_BITCODE': 'YES', '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=', '-dEnableBitcode=', '--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, 'ENABLE_BITCODE': 'YES', '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=', '-dEnableBitcode=', '--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 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, 'ENABLE_BITCODE': 'YES', 'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity, 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions, 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions, 'FLUTTER_ROOT': flutterRoot.path, 'INFOPLIST_PATH': 'Info.plist', 'SDKROOT': sdkRoot, '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()}', '-dIosArchs=$archs', '-dSdkRoot=$sdkRoot', '-dSplitDebugInfo=$splitDebugInfo', '-dTreeShakeIcons=$treeShake', '-dTrackWidgetCreation=$trackWidgetCreation', '-dDartObfuscation=$dartObfuscation', '-dEnableBitcode=true', '--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_observatory_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_observatory_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 _dartobservatory._tcp NSBonjourServices insertion.'), ); }); }); } 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 existsDir(String path) { return fileSystem.directory(path).existsSync(); } @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'); } }