xcodeproj_test.dart 34.7 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
// @dart = 2.8

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
import 'package:flutter_tools/src/base/logger.dart';
12
import 'package:flutter_tools/src/base/platform.dart';
13
import 'package:flutter_tools/src/base/version.dart';
14
import 'package:flutter_tools/src/build_info.dart';
15
import 'package:flutter_tools/src/ios/xcode_build_settings.dart';
16
import 'package:flutter_tools/src/ios/xcodeproj.dart';
17
import 'package:flutter_tools/src/project.dart';
18
import 'package:flutter_tools/src/reporting/reporting.dart';
19

20 21
import '../../src/common.dart';
import '../../src/context.dart';
22
import '../../src/fake_process_manager.dart';
23 24

const String xcodebuild = '/usr/bin/xcodebuild';
25 26

void main() {
27 28
  group('MockProcessManager', () {
    setUp(() {
29
      final FileSystem fileSystem = MemoryFileSystem.test();
30 31 32 33
      fileSystem.file(xcodebuild).createSync(recursive: true);
    });
  });

34 35 36 37 38 39 40
  const FakeCommand kWhichSysctlCommand = FakeCommand(
    command: <String>[
      'which',
      'sysctl',
    ],
  );

41 42 43 44 45 46 47 48
  const FakeCommand kARMCheckCommand = FakeCommand(
    command: <String>[
      'sysctl',
      'hw.optional.arm64',
    ],
    exitCode: 1,
  );

49
  FakeProcessManager fakeProcessManager;
50 51 52 53 54 55
  XcodeProjectInterpreter xcodeProjectInterpreter;
  FakePlatform platform;
  FileSystem fileSystem;
  BufferLogger logger;

  setUp(() {
56
    fakeProcessManager = FakeProcessManager.empty();
57
    platform = FakePlatform(operatingSystem: 'macos');
58
    fileSystem = MemoryFileSystem.test();
59
    fileSystem.file(xcodebuild).createSync(recursive: true);
60
    logger = BufferLogger.test();
61 62 63 64
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
65
      processManager: fakeProcessManager,
66
      usage: null,
67 68
    );
  });
69

70
  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
71
    fakeProcessManager.addCommands(const <FakeCommand>[
72
      kWhichSysctlCommand,
73 74 75 76 77 78 79 80 81
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
            "but active developer directory '/Library/Developer/CommandLineTools' "
            'is a command line tools instance',
        exitCode: 1,
      ),
    ]);
82

83
    expect(xcodeProjectInterpreter.versionText, isNull);
84
    expect(fakeProcessManager, hasNoRemainingExpectations);
85
  });
86

87 88 89 90 91 92 93 94 95 96 97 98 99
  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        exception: ProcessException(xcodebuild, <String>['-version']),
      ),
    ]);

    expect(xcodeProjectInterpreter.versionText, isNull);
  });

100
  testWithoutContext('xcodebuild versionText returns formatted version text', () {
101
    fakeProcessManager.addCommands(const <FakeCommand>[
102
      kWhichSysctlCommand,
103 104 105 106 107 108
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
      ),
    ]);
109

110
    expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
111
    expect(fakeProcessManager, hasNoRemainingExpectations);
112
  });
113

114
  testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () {
115
    fakeProcessManager.addCommands(const <FakeCommand>[
116
      kWhichSysctlCommand,
117 118 119 120 121 122
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
      ),
    ]);
123

124
    expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
125
    expect(fakeProcessManager, hasNoRemainingExpectations);
126
  });
127

128
  testWithoutContext('xcodebuild version parts can be parsed', () {
129
    fakeProcessManager.addCommands(const <FakeCommand>[
130
      kWhichSysctlCommand,
131 132 133 134 135 136
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 11.4.1\nBuild version 11N111s',
      ),
    ]);
137

138
    expect(xcodeProjectInterpreter.version, Version(11, 4, 1));
139
    expect(fakeProcessManager, hasNoRemainingExpectations);
140
  });
141

142
  testWithoutContext('xcodebuild minor and patch version default to 0', () {
143
    fakeProcessManager.addCommands(const <FakeCommand>[
144
      kWhichSysctlCommand,
145 146 147 148 149 150
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 11\nBuild version 11N111s',
      ),
    ]);
151

152
    expect(xcodeProjectInterpreter.version, Version(11, 0, 0));
153
    expect(fakeProcessManager, hasNoRemainingExpectations);
154
  });
155

156
  testWithoutContext('xcodebuild version parts is null when version has unexpected format', () {
157
    fakeProcessManager.addCommands(const <FakeCommand>[
158
      kWhichSysctlCommand,
159 160 161 162 163 164
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
      ),
    ]);
165
    expect(xcodeProjectInterpreter.version, isNull);
166
    expect(fakeProcessManager, hasNoRemainingExpectations);
167
  });
168

169
  testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () {
170
    final Platform platform = FakePlatform(operatingSystem: 'notMacOS');
171 172 173 174
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
175
      processManager: fakeProcessManager,
176
      usage: TestUsage(),
177 178 179 180
    );
    fileSystem.file(xcodebuild).deleteSync();

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
181
    expect(fakeProcessManager, hasNoRemainingExpectations);
182 183 184 185 186 187
  });

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

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
188
    expect(fakeProcessManager, hasNoRemainingExpectations);
189 190
  });

191 192 193
  testWithoutContext(
      'xcodebuild isInstalled is false when Xcode is not fully installed', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
194
      kWhichSysctlCommand,
195 196 197 198 199 200 201 202 203
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
            "but active developer directory '/Library/Developer/CommandLineTools' "
            'is a command line tools instance',
        exitCode: 1,
      ),
    ]);
204

205
    expect(xcodeProjectInterpreter.isInstalled, isFalse);
206
    expect(fakeProcessManager, hasNoRemainingExpectations);
207
  });
208

209
  testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () {
210
    fakeProcessManager.addCommands(const <FakeCommand>[
211
      kWhichSysctlCommand,
212 213 214 215 216 217
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
      ),
    ]);
218

219
    expect(xcodeProjectInterpreter.isInstalled, isFalse);
220
    expect(fakeProcessManager, hasNoRemainingExpectations);
221
  });
222

223
  testWithoutContext('xcodebuild isInstalled is true when version has expected format', () {
224
    fakeProcessManager.addCommands(const <FakeCommand>[
225
      kWhichSysctlCommand,
226 227 228 229 230 231
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
      ),
    ]);
232

233
    expect(xcodeProjectInterpreter.isInstalled, isTrue);
234
    expect(fakeProcessManager, hasNoRemainingExpectations);
235
  });
236

237 238
  testWithoutContext('xcrun runs natively on arm64', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
239
      kWhichSysctlCommand,
240 241 242 243 244 245 246 247 248 249 250 251 252 253
      FakeCommand(
        command: <String>[
          'sysctl',
          'hw.optional.arm64',
        ],
        stdout: 'hw.optional.arm64: 1',
      ),
    ]);

    expect(xcodeProjectInterpreter.xcrunCommand(), <String>[
      '/usr/bin/arch',
      '-arm64e',
      'xcrun',
    ]);
254
    expect(fakeProcessManager, hasNoRemainingExpectations);
255 256
  });

257
  testUsingContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
258
    platform.environment = const <String, String>{};
259

260
    fakeProcessManager.addCommands(<FakeCommand>[
261
      kWhichSysctlCommand,
262
      const FakeCommand(
263 264 265 266 267 268 269 270 271 272 273 274 275 276
        command: <String>[
          'sysctl',
          'hw.optional.arm64',
        ],
        exitCode: 1,
      ),
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-scheme',
          'Free',
277 278
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
279 280 281 282
        ],
        exitCode: 1,
      ),
    ]);
283

284 285 286
    expect(
        await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(scheme: 'Free')),
        const <String, String>{});
287
    expect(fakeProcessManager, hasNoRemainingExpectations);
288 289 290
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
291 292
  });

293
  testUsingContext('build settings passes in the simulator SDK', () async {
294 295
    platform.environment = const <String, String>{};

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      kARMCheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-sdk',
          'iphonesimulator',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
        ],
        exitCode: 1,
      ),
    ]);

    expect(
      await xcodeProjectInterpreter.getBuildSettings(
        '',
        buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator),
      ),
      const <String, String>{},
    );
321
    expect(fakeProcessManager, hasNoRemainingExpectations);
322 323 324 325 326 327 328 329 330
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('build settings accepts an empty scheme', () async {
    platform.environment = const <String, String>{};

    fakeProcessManager.addCommands(<FakeCommand>[
331
      kWhichSysctlCommand,
332 333 334 335 336 337 338
      kARMCheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
339 340
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
341 342 343 344
        ],
        exitCode: 1,
      ),
    ]);
345

346 347
    expect(await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext()),
        const <String, String>{});
348
    expect(fakeProcessManager, hasNoRemainingExpectations);
349 350 351
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
352 353
  });

354
  testUsingContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
355
    platform.environment = const <String, String>{
356 357
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64'
358
    };
359
    fakeProcessManager.addCommands(<FakeCommand>[
360
      kWhichSysctlCommand,
361 362 363 364 365 366 367 368 369 370
      kARMCheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          fileSystem.path.separator,
          '-scheme',
          'Free',
          '-showBuildSettings',
371
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
372 373 374 375 376
          'CODE_SIGN_STYLE=Manual',
          'ARCHS=arm64'
        ],
      ),
    ]);
377 378 379
    expect(
        await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(scheme: 'Free')),
        const <String, String>{});
380
    expect(fakeProcessManager, hasNoRemainingExpectations);
381 382 383
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
384 385 386
  });

  testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
387
    platform.environment = const <String, String>{
388 389
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64'
390 391
    };

392
    fakeProcessManager.addCommands(const <FakeCommand>[
393
      kWhichSysctlCommand,
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
      kARMCheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-workspace',
          'workspace_path',
          '-scheme',
          'Free',
          '-quiet',
          'clean',
          'CODE_SIGN_STYLE=Manual',
          'ARCHS=arm64'
        ],
      ),
    ]);
410

411
    await xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Free');
412
    expect(fakeProcessManager, hasNoRemainingExpectations);
413 414 415 416
  });

  testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async {
    const String workingDirectory = '/';
417
    fakeProcessManager.addCommands(const <FakeCommand>[
418
      kWhichSysctlCommand,
419 420 421 422 423
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-list'],
      ),
    ]);
424

425 426 427 428
    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
429
      processManager: fakeProcessManager,
430
      usage: TestUsage(),
431 432 433
    );

    expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
434
    expect(fakeProcessManager, hasNoRemainingExpectations);
435 436 437 438 439
  });

  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.';
440

441
    fakeProcessManager.addCommands(const <FakeCommand>[
442
      kWhichSysctlCommand,
443 444 445 446 447 448 449
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-list'],
        exitCode: 66,
        stderr: stderr,
      ),
    ]);
450

451 452 453 454
    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
455
      processManager: fakeProcessManager,
456
      usage: TestUsage(),
457 458
    );

459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
    expect(() => xcodeProjectInterpreter.getInfo(workingDirectory), throwsToolExit(message: stderr));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild -list getInfo throws a tool exit when project is corrupted', () async {
    const String workingDirectory = '/';
    const String stderr = 'Useful Xcode failure message about corrupted project.';

    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kARMCheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-list'],
        exitCode: 74,
        stderr: stderr,
      ),
    ]);

    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: fakeProcessManager,
      usage: TestUsage(),
    );

    expect(() => xcodeProjectInterpreter.getInfo(workingDirectory), throwsToolExit(message: stderr));
    expect(fakeProcessManager, hasNoRemainingExpectations);
487 488 489 490
  });

  testWithoutContext('Xcode project properties from default project can be parsed', () {
    const String output = '''
491 492 493 494 495 496 497 498 499 500 501 502 503 504
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

''';
505
    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output, logger);
506 507 508 509 510 511 512
    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 = '''
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
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

''';
530
    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output, logger);
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
    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');
546
  });
547

548
  testWithoutContext('expected scheme for flavored build is the title-cased flavor', () {
549 550 551
    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');
552 553
  });
  testWithoutContext('expected build configuration for flavored build is Mode-Flavor', () {
554 555 556
    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');
557 558 559
  });

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

    expect(info.schemeFor(BuildInfo.debug), 'Runner');
    expect(info.schemeFor(BuildInfo.profile), 'Runner');
    expect(info.schemeFor(BuildInfo.release), 'Runner');
565
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
566 567 568
  });

  testWithoutContext('build configuration for default project is matched against BuildMode', () {
569
    final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner'], logger);
570 571 572 573 574 575 576 577 578 579 580

    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'],
581
      logger,
582 583
    );

584 585 586
    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');
587
    expect(info.schemeFor(BuildInfo.debug), isNull);
588
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
589 590
  });

591 592 593 594 595 596 597 598 599
  testWithoutContext('reports default scheme error and exit', () {
    final XcodeProjectInfo defaultInfo = XcodeProjectInfo(
      <String>[],
      <String>[],
      <String>['Runner'],
      logger,
    );

    expect(
600
      defaultInfo.reportFlavorNotFoundAndExit,
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
      throwsToolExit(
        message: 'The Xcode project does not define custom schemes. You cannot use the --flavor option.'
      ),
    );
  });

  testWithoutContext('reports custom scheme error and exit', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>[],
      <String>[],
      <String>['Free', 'Paid'],
      logger,
    );

    expect(
616
      info.reportFlavorNotFoundAndExit,
617 618 619 620 621 622
      throwsToolExit(
        message: 'You must specify a --flavor option to select one of the available schemes.'
      ),
    );
  });

623 624 625 626 627
  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'],
628
      logger,
629 630
    );

631 632 633 634
    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');
635 636 637 638 639 640 641
  });

  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'],
642
      logger,
643
    );
644 645 646
    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);
647 648
  });
 group('environmentVariablesAsXcodeBuildSettings', () {
649 650 651
    FakePlatform platform;

    setUp(() {
652
      platform = FakePlatform();
653 654
    });

655
    testWithoutContext('environment variables as Xcode build settings', () {
656
      platform.environment = const <String, String>{
657 658 659 660
        'Ignored': 'Bogus',
        'FLUTTER_NOT_XCODE': 'Bogus',
        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
        'FLUTTER_XCODE_ARCHS': 'arm64'
661
      };
662
      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform);
663 664 665 666
      expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
    });
  });

667
  group('updateGeneratedXcodeProperties', () {
668
    Artifacts localArtifacts;
669 670 671 672
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
673
      fs = MemoryFileSystem.test();
674
      localArtifacts = Artifacts.test(localEngine: 'out/ios_profile_arm');
675
      macOS = FakePlatform(operatingSystem: 'macos');
676
      fs.file(xcodebuild).createSync(recursive: true);
677 678
    });

679
    void testUsingOsxContext(String description, dynamic Function() testMethod) {
680
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
681
        Artifacts: () => localArtifacts,
682 683
        Platform: () => macOS,
        FileSystem: () => fs,
684
        ProcessManager: () => FakeProcessManager.any(),
685 686 687
      });
    }

688
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
689
      const BuildInfo buildInfo = BuildInfo.debug;
690
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
691 692
      await updateGeneratedXcodeProperties(
        project: project,
693 694 695
        buildInfo: buildInfo,
      );

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

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=armv7'), isTrue);
701
      expect(contents.contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=arm64 i386'), isTrue);
702

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

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('ARCHS=armv7'), isTrue);
708
      expect(buildPhaseScriptContents.contains('EXCLUDED_ARCHS'), isFalse);
709 710
    });

711
    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
712
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, treeShakeIcons: false);
713
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
714 715
      await updateGeneratedXcodeProperties(
        project: project,
716 717 718
        buildInfo: buildInfo,
      );

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

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

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

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
730 731 732
    });

    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
733
      const BuildInfo buildInfo = BuildInfo.debug;
734
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
735 736
      await updateGeneratedXcodeProperties(
        project: project,
737 738 739
        buildInfo: buildInfo,
      );

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

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

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

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

753 754
    group('sim local engine', () {
      Artifacts localArtifacts;
755

756 757 758
      setUp(() {
        localArtifacts = Artifacts.test(localEngine: 'out/ios_debug_sim_unopt');
      });
759

760 761 762 763 764 765 766
      testUsingContext('sets ARCHS=x86_64 when sim local engine is set', () async {
        const BuildInfo buildInfo = BuildInfo.debug;
        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
        await updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        );
767

768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
        expect(config.existsSync(), isTrue);

        final String contents = config.readAsStringSync();
        expect(contents.contains('ARCHS=x86_64'), 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('ARCHS=x86_64'), isTrue);
      }, overrides: <Type, Generator>{
        Artifacts: () => localArtifacts,
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
      });
    });

    group('armv7 local engine', () {
      Artifacts localArtifacts;

      setUp(() {
        localArtifacts = Artifacts.test(localEngine: 'out/ios_profile');
      });

      testUsingContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
        const BuildInfo buildInfo = BuildInfo.debug;

        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('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('ARCHS=arm64'), isTrue);
      }, overrides: <Type, Generator>{
        Artifacts: () => localArtifacts,
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
      });
814
    });
815 816 817 818 819 820 821 822 823 824 825

    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({
826
      String manifestString,
827 828 829 830
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
831
      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
832 833 834
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifestString);

835
      await updateGeneratedXcodeProperties(
836
        project: FlutterProject.fromDirectoryTest(fs.directory('path/to/project')),
837 838 839
        buildInfo: buildInfo,
      );

840
      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
841 842
      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
843
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), isNotNull);
844 845
    }

846
    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
847 848 849 850 851 852 853 854 855
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

856
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
857
      await checkBuildVersion(
858
        manifestString: manifest,
859 860 861 862 863 864
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

865
    testUsingOsxContext('extract build name from pubspec.yaml', () async {
866 867 868 869 870 871 872 873
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
874
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
875
      await checkBuildVersion(
876
        manifestString: manifest,
877 878
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
879
        expectedBuildNumber: '1.0.0',
880 881 882
      );
    });

883
    testUsingOsxContext('allow build info to override build name', () async {
884 885 886 887 888 889 890 891
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
892
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
893
      await checkBuildVersion(
894
        manifestString: manifest,
895 896 897 898 899 900
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

901 902 903 904 905 906 907 908 909
    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:
''';
910
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
911 912 913 914 915 916 917 918
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1.0.2',
      );
    });

919
    testUsingOsxContext('allow build info to override build number', () async {
920 921 922 923 924 925 926 927
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
928
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3', treeShakeIcons: false);
929
      await checkBuildVersion(
930
        manifestString: manifest,
931 932 933 934 935 936
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

937
    testUsingOsxContext('allow build info to override build name and number', () async {
938 939 940 941 942 943 944 945
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
946
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
947
      await checkBuildVersion(
948
        manifestString: manifest,
949 950 951 952 953 954
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

955
    testUsingOsxContext('allow build info to override build name and set number', () async {
956 957 958 959 960 961 962 963
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
964
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
965
      await checkBuildVersion(
966
        manifestString: manifest,
967 968 969 970 971 972
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

973
    testUsingOsxContext('allow build info to set build name and number', () async {
974 975 976 977 978 979 980
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
981
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
982
      await checkBuildVersion(
983
        manifestString: manifest,
984 985 986 987 988
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });
989 990 991 992 993 994 995 996 997

    testUsingOsxContext('default build name and number when version is missing', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
998
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
999 1000 1001 1002 1003 1004 1005
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });
1006
  });
1007
}