xcodeproj_test.dart 31.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
4

5 6
import 'dart:async';

7
import 'package:file/memory.dart';
8
import 'package:flutter_tools/src/artifacts.dart';
9 10
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
11 12
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
13 14
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
15
import 'package:flutter_tools/src/project.dart';
16
import 'package:flutter_tools/src/reporting/reporting.dart';
17

18 19 20 21
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

22 23
import '../../src/common.dart';
import '../../src/context.dart';
24
import '../../src/mocks.dart' as mocks;
25
import '../../src/pubspec_schema.dart';
26 27

const String xcodebuild = '/usr/bin/xcodebuild';
28 29

void main() {
30 31 32 33 34 35 36 37 38
  mocks.MockProcessManager processManager;
  XcodeProjectInterpreter xcodeProjectInterpreter;
  FakePlatform platform;
  FileSystem fileSystem;
  BufferLogger logger;
  AnsiTerminal terminal;

  setUp(() {
    processManager = mocks.MockProcessManager();
39
    platform = FakePlatform(operatingSystem: 'macos');
40 41 42
    fileSystem = MemoryFileSystem();
    fileSystem.file(xcodebuild).createSync(recursive: true);
    terminal = MockAnsiTerminal();
43
    logger = BufferLogger.test(
44 45 46 47 48 49 50 51
      terminal: terminal
    );
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      terminal: terminal,
52
      usage: null,
53 54
    );
  });
55

56 57 58
  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenThrow(const ProcessException(xcodebuild, <String>['-version']));
59

60 61
    expect(xcodeProjectInterpreter.versionText, isNull);
  });
62

63 64 65 66 67 68 69 70 71 72 73
  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
    when(processManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
      ProcessResult(
        0,
        1,
        "xcode-select: error: tool 'xcodebuild' requires Xcode, "
        "but active developer directory '/Library/Developer/CommandLineTools' "
        'is a command line tools instance',
        '',
      ),
    );
74

75 76
    expect(xcodeProjectInterpreter.versionText, isNull);
  });
77

78 79 80
  testWithoutContext('xcodebuild versionText returns formatted version text', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
81

82 83
    expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
  });
84

85 86 87
  testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
88

89 90
    expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
  });
91

92 93
  testWithoutContext('xcodebuild majorVersion returns major version', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
94
        .thenReturn(ProcessResult(1, 0, 'Xcode 11.4.1\nBuild version 11N111s', ''));
95

96
    expect(xcodeProjectInterpreter.majorVersion, 11);
97
  });
98

99 100 101
  testWithoutContext('xcodebuild majorVersion is null when version has unexpected format', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
102

103 104
    expect(xcodeProjectInterpreter.majorVersion, isNull);
  });
105

106
  testWithoutContext('xcodebuild minorVersion returns minor version', () {
107 108
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
109

110 111
    expect(xcodeProjectInterpreter.minorVersion, 3);
  });
112

113 114 115
  testWithoutContext('xcodebuild minorVersion returns 0 when minor version is unspecified', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
116

117 118
    expect(xcodeProjectInterpreter.minorVersion, 0);
  });
119

120 121 122
  testWithoutContext('xcodebuild minorVersion is null when version has unexpected format', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
123

124 125
    expect(xcodeProjectInterpreter.minorVersion, isNull);
  });
126

127
  testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () {
128
    final Platform platform = FakePlatform(operatingSystem: 'notMacOS');
129 130 131 132 133 134
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      terminal: terminal,
135
      usage: Usage.test(),
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    );
    fileSystem.file(xcodebuild).deleteSync();

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
  });

  testWithoutContext('xcodebuild isInstalled is false when xcodebuild does not exist', () {
    fileSystem.file(xcodebuild).deleteSync();

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
  });

  testWithoutContext('xcodebuild isInstalled is false when Xcode is not fully installed', () {
    when(processManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
      ProcessResult(
        0,
        1,
        "xcode-select: error: tool 'xcodebuild' requires Xcode, "
        "but active developer directory '/Library/Developer/CommandLineTools' "
        'is a command line tools instance',
156
        '',
157 158
      ),
    );
159

160
    expect(xcodeProjectInterpreter.isInstalled, isFalse);
161
  });
162

163 164 165
  testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
166

167 168
    expect(xcodeProjectInterpreter.isInstalled, isFalse);
  });
169

170 171 172
  testWithoutContext('xcodebuild isInstalled is true when version has expected format', () {
    when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
173

174 175
    expect(xcodeProjectInterpreter.isInstalled, isTrue);
  });
176

177
  testWithoutContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
178
    platform.environment = Map<String, String>.unmodifiable(<String, String>{});
179 180 181 182 183 184 185
    when(processManager.runSync(
              argThat(contains(xcodebuild)),
              workingDirectory: anyNamed('workingDirectory'),
              environment: anyNamed('environment')))
        .thenReturn(ProcessResult(0, 1, '', ''));

    expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
186 187
  });

188 189 190 191 192 193
  testWithoutContext('xcodebuild build settings flakes', () async {
    const Duration delay = Duration(seconds: 1);
    processManager.processFactory = mocks.flakyProcessFactory(
      flakes: 1,
      delay: delay + const Duration(seconds: 1),
    );
194
    platform.environment = Map<String, String>.unmodifiable(<String, String>{});
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230

    expect(await xcodeProjectInterpreter.getBuildSettings(
                '', '', timeout: delay),
            const <String, String>{});
    // build settings times out and is killed once, then succeeds.
    verify(processManager.killPid(any)).called(1);
    // The verbose logs should tell us something timed out.
    expect(logger.traceText, contains('timed out'));
  });

  testWithoutContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
    platform.environment = Map<String, String>.unmodifiable(<String, String>{
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64'
    });
    when(processManager.runSync(<String>[
      xcodebuild,
      '-project',
      platform.pathSeparator,
      '-target',
      '',
      '-showBuildSettings',
      'CODE_SIGN_STYLE=Manual',
      'ARCHS=arm64'
    ],
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment')))
      .thenReturn(ProcessResult(1, 0, '', ''));
    expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
  });

  testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
    platform.environment = Map<String, String>.unmodifiable(<String, String>{
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64'
    });
231
    when(processManager.run(
232 233
      any,
      workingDirectory: anyNamed('workingDirectory')))
234 235 236
      .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', '')));
    await xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner');
    final List<dynamic> captured = verify(processManager.run(
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
      captureAny,
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'))).captured;

    expect(captured.first, <String>[
      xcodebuild,
      '-workspace',
      'workspace_path',
      '-scheme',
      'Runner',
      '-quiet',
      'clean',
      'CODE_SIGN_STYLE=Manual',
      'ARCHS=arm64'
    ]);
  });

  testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async {
    const String workingDirectory = '/';
    when(processManager.run(
      <String>[xcodebuild, '-list'],
      environment: anyNamed('environment'),
      workingDirectory: workingDirectory),
    ).thenAnswer((_) {
      return Future<ProcessResult>.value(ProcessResult(1, 0, '', ''));
    });
    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      terminal: terminal,
269
      usage: Usage.test(),
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    );

    expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
  });

  testWithoutContext('xcodebuild -list getInfo throws a tool exit when it is unable to find a project', () async {
    const String workingDirectory = '/';
    const String stderr = 'Useful Xcode failure message about missing project.';
    when(processManager.run(
      <String>[xcodebuild, '-list'],
      environment: anyNamed('environment'),
      workingDirectory: workingDirectory),
    ).thenAnswer((_) {
      return Future<ProcessResult>.value(ProcessResult(1, 66, '', stderr));
    });
    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      terminal: terminal,
291
      usage: Usage.test(),
292 293 294 295 296 297 298 299 300
    );

    expect(
        () async => await xcodeProjectInterpreter.getInfo(workingDirectory),
        throwsToolExit(message: stderr));
  });

  testWithoutContext('Xcode project properties from default project can be parsed', () {
    const String output = '''
301 302 303 304 305 306 307 308 309 310 311 312 313 314
Information about project "Runner":
    Targets:
        Runner

    Build Configurations:
        Debug
        Release

    If no build configuration is specified and -scheme is not passed then "Release" is used.

    Schemes:
        Runner

''';
315 316 317 318 319 320 321 322
    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
    expect(info.targets, <String>['Runner']);
    expect(info.schemes, <String>['Runner']);
    expect(info.buildConfigurations, <String>['Debug', 'Release']);
  });

  testWithoutContext('Xcode project properties from project with custom schemes can be parsed', () {
    const String output = '''
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
Information about project "Runner":
    Targets:
        Runner

    Build Configurations:
        Debug (Free)
        Debug (Paid)
        Release (Free)
        Release (Paid)

    If no build configuration is specified and -scheme is not passed then "Release (Free)" is used.

    Schemes:
        Free
        Paid

''';
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
    expect(info.targets, <String>['Runner']);
    expect(info.schemes, <String>['Free', 'Paid']);
    expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']);
  });

  testWithoutContext('expected scheme for non-flavored build is Runner', () {
    expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner');
    expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner');
    expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner');
  });

  testWithoutContext('expected build configuration for non-flavored build is derived from BuildMode', () {
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
356
  });
357

358
  testWithoutContext('expected scheme for flavored build is the title-cased flavor', () {
359 360 361
    expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello', treeShakeIcons: false)), 'Hello');
    expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO', treeShakeIcons: false)), 'HELLO');
    expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello', treeShakeIcons: false)), 'Hello');
362 363
  });
  testWithoutContext('expected build configuration for flavored build is Mode-Flavor', () {
364 365 366
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello', treeShakeIcons: false), 'Hello'), 'Debug-Hello');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO', treeShakeIcons: false), 'Hello'), 'Profile-Hello');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello', treeShakeIcons: false), 'Hello'), 'Release-Hello');
367 368 369 370 371 372 373 374
  });

  testWithoutContext('scheme for default project is Runner', () {
    final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);

    expect(info.schemeFor(BuildInfo.debug), 'Runner');
    expect(info.schemeFor(BuildInfo.profile), 'Runner');
    expect(info.schemeFor(BuildInfo.release), 'Runner');
375
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
  });

  testWithoutContext('build configuration for default project is matched against BuildMode', () {
    final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner']);

    expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
    expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
    expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
  });

  testWithoutContext('scheme for project with custom schemes is matched against flavor', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>['Runner'],
      <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
      <String>['Free', 'Paid'],
    );

393 394 395 396 397
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false)), 'Free');
    expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free', treeShakeIcons: false)), 'Free');
    expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid', treeShakeIcons: false)), 'Paid');
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)), isNull);
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
398 399 400 401 402 403 404 405 406
  });

  testWithoutContext('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>['Runner'],
      <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
      <String>['Free', 'Paid'],
    );

407 408 409 410
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false), 'Free'), 'debug (free)');
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid', treeShakeIcons: false), 'Paid'), 'Debug paid');
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE', treeShakeIcons: false), 'Free'), 'profile - Free');
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid', treeShakeIcons: false), 'Paid'), 'Release-Paid');
411 412 413 414 415 416 417 418
  });

  testWithoutContext('build configuration for project with inconsistent naming is null', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>['Runner'],
      <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
      <String>['Free', 'Paid'],
    );
419 420 421
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free', treeShakeIcons: false), 'Free'), null);
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free', treeShakeIcons: false), 'Free'), null);
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid', treeShakeIcons: false), 'Paid'), null);
422 423
  });
 group('environmentVariablesAsXcodeBuildSettings', () {
424 425 426
    FakePlatform platform;

    setUp(() {
427
      platform = FakePlatform();
428 429
    });

430
    testWithoutContext('environment variables as Xcode build settings', () {
431 432 433 434 435 436
      platform.environment = Map<String, String>.unmodifiable(<String, String>{
        'Ignored': 'Bogus',
        'FLUTTER_NOT_XCODE': 'Bogus',
        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
        'FLUTTER_XCODE_ARCHS': 'arm64'
      });
437
      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform);
438 439 440 441
      expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
    });
  });

442
  group('updateGeneratedXcodeProperties', () {
443
    MockLocalEngineArtifacts mockArtifacts;
444 445 446 447 448
    MockProcessManager mockProcessManager;
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
449
      fs = MemoryFileSystem();
450
      mockArtifacts = MockLocalEngineArtifacts();
451
      mockProcessManager = MockProcessManager();
452
      macOS = FakePlatform(operatingSystem: 'macos');
453
      fs.file(xcodebuild).createSync(recursive: true);
454 455
    });

456
    void testUsingOsxContext(String description, dynamic testMethod()) {
457
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
458
        Artifacts: () => mockArtifacts,
459 460
        Platform: () => macOS,
        FileSystem: () => fs,
461
        ProcessManager: () => mockProcessManager,
462 463 464
      });
    }

465
    testUsingOsxContext('sets OTHER_LDFLAGS for iOS', () async {
466
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
467
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn(fs.path.join('engine', 'Flutter.framework'));
468
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490

      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('OTHER_LDFLAGS=\$(inherited) -framework Flutter'), isTrue);

      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('OTHER_LDFLAGS=\$(inherited) -framework Flutter'), isTrue);
    });

    testUsingOsxContext('do not set OTHER_LDFLAGS for macOS', () async {
491
      when(mockArtifacts.getArtifactPath(Artifact.flutterMacOSFramework,
492
          platform: TargetPlatform.darwin_x64, mode: anyNamed('mode'))).thenReturn(fs.path.join('engine', 'FlutterMacOS.framework'));
493
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515

      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
        useMacOSConfig: true,
      );

      final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('OTHER_LDFLAGS'), isFalse);

      final File buildPhaseScript = fs.file('path/to/project/macos/Flutter/ephemeral/flutter_export_environment.sh');
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('OTHER_LDFLAGS'), isFalse);
    });

516
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
517
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
518
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
519
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
520

521
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
522
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
523 524
      await updateGeneratedXcodeProperties(
        project: project,
525 526 527
        buildInfo: buildInfo,
      );

528
      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
529 530 531 532
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=armv7'), isTrue);
533

534
      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
535 536 537 538
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('ARCHS=armv7'), isTrue);
539 540
    });

541
    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
542
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
543
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
544
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
545
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, treeShakeIcons: false);
546
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
547 548
      await updateGeneratedXcodeProperties(
        project: project,
549 550 551
        buildInfo: buildInfo,
      );

552
      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
553 554 555 556
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
557

558
      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
559 560 561 562
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
563 564 565
    });

    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
566
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
567
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
568
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
569
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
570
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
571 572
      await updateGeneratedXcodeProperties(
        project: project,
573 574 575
        buildInfo: buildInfo,
      );

576
      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
577 578 579 580
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isFalse);
581

582
      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
583 584 585 586
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('TRACK_WIDGET_CREATION=true'), isFalse);
587 588
    });

589
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
590
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
591
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
592
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
593
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
594

595
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
596 597
      await updateGeneratedXcodeProperties(
        project: project,
598 599 600
        buildInfo: buildInfo,
      );

601
      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
602 603 604 605 606
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=arm64'), isTrue);
    });
607 608 609 610 611 612 613 614 615 616 617

    String propertyFor(String key, File file) {
      final List<String> properties = file
          .readAsLinesSync()
          .where((String line) => line.startsWith('$key='))
          .map((String line) => line.split('=')[1])
          .toList();
      return properties.isEmpty ? null : properties.first;
    }

    Future<void> checkBuildVersion({
618
      String manifestString,
619 620 621 622
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
623
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
624
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
625
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios'));
626

627
      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
628 629 630 631
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifestString);

      // write schemaData otherwise pubspec.yaml file can't be loaded
632
      writeEmptySchemaFile(fs);
633

634
      await updateGeneratedXcodeProperties(
635
        project: FlutterProject.fromPath('path/to/project'),
636 637 638
        buildInfo: buildInfo,
      );

639
      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
640 641
      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
642
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), isNotNull);
643 644
    }

645
    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
646 647 648 649 650 651 652 653 654
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

655
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
656
      await checkBuildVersion(
657
        manifestString: manifest,
658 659 660 661 662 663
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

664
    testUsingOsxContext('extract build name from pubspec.yaml', () async {
665 666 667 668 669 670 671 672
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
673
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
674
      await checkBuildVersion(
675
        manifestString: manifest,
676 677
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
678
        expectedBuildNumber: '1.0.0',
679 680 681
      );
    });

682
    testUsingOsxContext('allow build info to override build name', () async {
683 684 685 686 687 688 689 690
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
691
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
692
      await checkBuildVersion(
693
        manifestString: manifest,
694 695 696 697 698 699
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

700 701 702 703 704 705 706 707 708
    testUsingOsxContext('allow build info to override build name with build number fallback', () async {
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
709
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
710 711 712 713 714 715 716 717
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1.0.2',
      );
    });

718
    testUsingOsxContext('allow build info to override build number', () async {
719 720 721 722 723 724 725 726
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
727
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3', treeShakeIcons: false);
728
      await checkBuildVersion(
729
        manifestString: manifest,
730 731 732 733 734 735
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

736
    testUsingOsxContext('allow build info to override build name and number', () async {
737 738 739 740 741 742 743 744
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
745
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
746
      await checkBuildVersion(
747
        manifestString: manifest,
748 749 750 751 752 753
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

754
    testUsingOsxContext('allow build info to override build name and set number', () async {
755 756 757 758 759 760 761 762
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
763
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
764
      await checkBuildVersion(
765
        manifestString: manifest,
766 767 768 769 770 771
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

772
    testUsingOsxContext('allow build info to set build name and number', () async {
773 774 775 776 777 778 779
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
780
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
781
      await checkBuildVersion(
782
        manifestString: manifest,
783 784 785 786 787
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });
788 789 790 791 792 793 794 795 796

    testUsingOsxContext('default build name and number when version is missing', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
797
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
798 799 800 801 802 803 804
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });
805
  });
806
}
807

808
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
809
class MockProcessManager extends Mock implements ProcessManager {}
810 811 812 813 814
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class MockAnsiTerminal extends Mock implements AnsiTerminal {
  @override
  bool get supportsColor => false;
}