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

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

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

void main() {
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
  group('MockProcessManager', () {
    mocks.MockProcessManager processManager;
    XcodeProjectInterpreter xcodeProjectInterpreter;
    FakePlatform platform;
    BufferLogger logger;

    setUp(() {
      processManager = mocks.MockProcessManager();
      platform = FakePlatform(operatingSystem: 'macos');
      final FileSystem fileSystem = MemoryFileSystem();
      fileSystem.file(xcodebuild).createSync(recursive: true);
      final AnsiTerminal terminal = MockAnsiTerminal();
      logger = BufferLogger.test(
        terminal: terminal
      );
      xcodeProjectInterpreter = XcodeProjectInterpreter(
        logger: logger,
        fileSystem: fileSystem,
        platform: platform,
        processManager: processManager,
        terminal: terminal,
        usage: null,
      );
    });

    // Work around https://github.com/flutter/flutter/issues/56415.
    testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
      when(processManager.runSync(<String>[xcodebuild, '-version']))
        .thenThrow(const ProcessException(xcodebuild, <String>['-version']));

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

    testWithoutContext('xcodebuild build settings flakes', () async {
      const Duration delay = Duration(seconds: 1);
      processManager.processFactory = mocks.flakyProcessFactory(
        flakes: 1,
        delay: delay + const Duration(seconds: 1),
      );
      platform.environment = const <String, String>{};

      expect(await xcodeProjectInterpreter.getBuildSettings(
71
        '', scheme: 'Runner', timeout: delay),
72 73 74 75 76 77 78 79 80
        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'));
    });
  });

  FakeProcessManager fakeProcessManager;
81 82 83 84 85 86 87
  XcodeProjectInterpreter xcodeProjectInterpreter;
  FakePlatform platform;
  FileSystem fileSystem;
  BufferLogger logger;
  AnsiTerminal terminal;

  setUp(() {
88
    fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]);
89
    platform = FakePlatform(operatingSystem: 'macos');
90 91 92
    fileSystem = MemoryFileSystem();
    fileSystem.file(xcodebuild).createSync(recursive: true);
    terminal = MockAnsiTerminal();
93
    logger = BufferLogger.test(
94 95 96 97 98 99
      terminal: terminal
    );
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
100
      processManager: fakeProcessManager,
101
      terminal: terminal,
102
      usage: null,
103 104
    );
  });
105

106
  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
107 108 109
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
110 111
        "but active developer directory '/Library/Developer/CommandLineTools' "
        'is a command line tools instance',
112 113
      exitCode: 1,
    ));
114

115
    expect(xcodeProjectInterpreter.versionText, isNull);
116
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
117
  });
118

119
  testWithoutContext('xcodebuild versionText returns formatted version text', () {
120 121 122 123
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
    ));
124

125
    expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
126
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
127
  });
128

129
  testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () {
130 131 132 133
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
    ));
134

135
    expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
136
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
137
  });
138

139
  testWithoutContext('xcodebuild majorVersion returns major version', () {
140 141 142 143
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode 11.4.1\nBuild version 11N111s',
    ));
144

145
    expect(xcodeProjectInterpreter.majorVersion, 11);
146
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
147
  });
148

149
  testWithoutContext('xcodebuild majorVersion is null when version has unexpected format', () {
150 151 152 153
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
    ));
154
    expect(xcodeProjectInterpreter.majorVersion, isNull);
155
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
156
  });
157

158
  testWithoutContext('xcodebuild minorVersion returns minor version', () {
159 160 161 162
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
    ));
163
    expect(xcodeProjectInterpreter.minorVersion, 3);
164
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
165
  });
166

167
  testWithoutContext('xcodebuild minorVersion returns 0 when minor version is unspecified', () {
168 169 170 171
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode 8\nBuild version 8E3004b',
    ));
172
    expect(xcodeProjectInterpreter.minorVersion, 0);
173
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
174
  });
175

176
  testWithoutContext('xcodebuild minorVersion is null when version has unexpected format', () {
177 178 179 180
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
    ));
181
    expect(xcodeProjectInterpreter.minorVersion, isNull);
182
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
183
  });
184

185
  testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () {
186
    final Platform platform = FakePlatform(operatingSystem: 'notMacOS');
187 188 189 190
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
191
      processManager: fakeProcessManager,
192
      terminal: terminal,
193
      usage: Usage.test(),
194 195 196 197
    );
    fileSystem.file(xcodebuild).deleteSync();

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
198
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
199 200 201 202 203 204
  });

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

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

  testWithoutContext('xcodebuild isInstalled is false when Xcode is not fully installed', () {
209 210 211
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
212 213
        "but active developer directory '/Library/Developer/CommandLineTools' "
        'is a command line tools instance',
214 215
      exitCode: 1,
    ));
216

217
    expect(xcodeProjectInterpreter.isInstalled, isFalse);
218
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
219
  });
220

221
  testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () {
222 223 224 225
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
    ));
226

227
    expect(xcodeProjectInterpreter.isInstalled, isFalse);
228
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
229
  });
230

231
  testWithoutContext('xcodebuild isInstalled is true when version has expected format', () {
232 233 234 235
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-version'],
      stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
    ));
236

237
    expect(xcodeProjectInterpreter.isInstalled, isTrue);
238
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
239
  });
240

241
  testWithoutContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
242
    platform.environment = const <String, String>{};
243

244 245 246 247 248
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[
        '/usr/bin/xcodebuild',
        '-project',
        '/',
249 250
        '-scheme',
        'Free',
251 252 253 254
        '-showBuildSettings'
      ],
      exitCode: 1,
    ));
255

256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
    expect(await xcodeProjectInterpreter.getBuildSettings('', scheme: 'Free'), const <String, String>{});
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
  });

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

    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[
        '/usr/bin/xcodebuild',
        '-project',
        '/',
        '-showBuildSettings'
      ],
      exitCode: 1,
    ));

    expect(await xcodeProjectInterpreter.getBuildSettings(''), const <String, String>{});
274
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
275 276 277
  });

  testWithoutContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
278
    platform.environment = const <String, String>{
279 280
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64'
281 282 283 284 285 286
    };
    fakeProcessManager.addCommand(FakeCommand(
      command: <String>[
        xcodebuild,
        '-project',
        fileSystem.path.separator,
287 288
        '-scheme',
        'Free',
289 290 291 292 293
        '-showBuildSettings',
        'CODE_SIGN_STYLE=Manual',
        'ARCHS=arm64'
      ],
    ));
294
    expect(await xcodeProjectInterpreter.getBuildSettings('', scheme: 'Free'), const <String, String>{});
295
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
296 297 298
  });

  testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
299
    platform.environment = const <String, String>{
300 301
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64'
302 303 304 305 306 307 308 309
    };

    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[
        xcodebuild,
        '-workspace',
        'workspace_path',
        '-scheme',
310
        'Free',
311 312 313 314 315 316 317
        '-quiet',
        'clean',
        'CODE_SIGN_STYLE=Manual',
        'ARCHS=arm64'
      ],
    ));

318
    await xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Free');
319
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
320 321 322 323
  });

  testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async {
    const String workingDirectory = '/';
324 325 326 327
    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-list'],
    ));

328 329 330 331
    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
332
      processManager: fakeProcessManager,
333
      terminal: terminal,
334
      usage: Usage.test(),
335 336 337
    );

    expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
338
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
339 340 341 342 343
  });

  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.';
344 345 346 347 348 349 350

    fakeProcessManager.addCommand(const FakeCommand(
      command: <String>[xcodebuild, '-list'],
      exitCode: 66,
      stderr: stderr,
    ));

351 352 353 354
    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
355
      processManager: fakeProcessManager,
356
      terminal: terminal,
357
      usage: Usage.test(),
358 359 360 361 362
    );

    expect(
        () async => await xcodeProjectInterpreter.getInfo(workingDirectory),
        throwsToolExit(message: stderr));
363
    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
364 365 366 367
  });

  testWithoutContext('Xcode project properties from default project can be parsed', () {
    const String output = '''
368 369 370 371 372 373 374 375 376 377 378 379 380 381
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

''';
382 383 384 385 386 387 388 389
    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 = '''
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
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

''';
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
    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');
423
  });
424

425
  testWithoutContext('expected scheme for flavored build is the title-cased flavor', () {
426 427 428
    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');
429 430
  });
  testWithoutContext('expected build configuration for flavored build is Mode-Flavor', () {
431 432 433
    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');
434 435 436 437 438 439 440 441
  });

  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');
442
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
  });

  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'],
    );

460 461 462 463 464
    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);
465 466 467 468 469 470 471 472 473
  });

  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'],
    );

474 475 476 477
    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');
478 479 480 481 482 483 484 485
  });

  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'],
    );
486 487 488
    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);
489 490
  });
 group('environmentVariablesAsXcodeBuildSettings', () {
491 492 493
    FakePlatform platform;

    setUp(() {
494
      platform = FakePlatform();
495 496
    });

497
    testWithoutContext('environment variables as Xcode build settings', () {
498
      platform.environment = const <String, String>{
499 500 501 502
        'Ignored': 'Bogus',
        'FLUTTER_NOT_XCODE': 'Bogus',
        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
        'FLUTTER_XCODE_ARCHS': 'arm64'
503
      };
504
      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform);
505 506 507 508
      expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
    });
  });

509
  group('updateGeneratedXcodeProperties', () {
510
    MockLocalEngineArtifacts mockArtifacts;
511 512 513 514
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
515
      fs = MemoryFileSystem();
516
      mockArtifacts = MockLocalEngineArtifacts();
517
      macOS = FakePlatform(operatingSystem: 'macos');
518
      fs.file(xcodebuild).createSync(recursive: true);
519 520
    });

521
    void testUsingOsxContext(String description, dynamic testMethod()) {
522
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
523
        Artifacts: () => mockArtifacts,
524 525
        Platform: () => macOS,
        FileSystem: () => fs,
526
        ProcessManager: () => FakeProcessManager.any(),
527 528 529
      });
    }

530
    testUsingOsxContext('sets OTHER_LDFLAGS for iOS', () async {
531
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
532
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn(fs.path.join('engine', 'Flutter.framework'));
533
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555

      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 {
556
      when(mockArtifacts.getArtifactPath(Artifact.flutterMacOSFramework,
557
          platform: TargetPlatform.darwin_x64, mode: anyNamed('mode'))).thenReturn(fs.path.join('engine', 'FlutterMacOS.framework'));
558
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580

      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);
    });

581
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
582
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
583
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
584
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
585

586
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
587
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
588 589
      await updateGeneratedXcodeProperties(
        project: project,
590 591 592
        buildInfo: buildInfo,
      );

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

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

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

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

606
    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
607
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
608
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
609
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
610
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, treeShakeIcons: false);
611
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
612 613
      await updateGeneratedXcodeProperties(
        project: project,
614 615 616
        buildInfo: buildInfo,
      );

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

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

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

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
628 629 630
    });

    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
631
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
632
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
633
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
634
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
635
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
636 637
      await updateGeneratedXcodeProperties(
        project: project,
638 639 640
        buildInfo: buildInfo,
      );

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

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

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

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

654
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
655
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
656
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
657
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
658
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
659

660
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
661 662
      await updateGeneratedXcodeProperties(
        project: project,
663 664 665
        buildInfo: buildInfo,
      );

666
      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
667 668 669 670 671
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=arm64'), isTrue);
    });
672 673 674 675 676 677 678 679 680 681 682

    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({
683
      String manifestString,
684 685 686 687
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
688
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
689
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
690
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios'));
691

692
      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
693 694 695 696
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifestString);

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

699
      await updateGeneratedXcodeProperties(
700
        project: FlutterProject.fromPath('path/to/project'),
701 702 703
        buildInfo: buildInfo,
      );

704
      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
705 706
      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
707
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), isNotNull);
708 709
    }

710
    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
711 712 713 714 715 716 717 718 719
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

720
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
721
      await checkBuildVersion(
722
        manifestString: manifest,
723 724 725 726 727 728
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

729
    testUsingOsxContext('extract build name from pubspec.yaml', () async {
730 731 732 733 734 735 736 737
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
738
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
739
      await checkBuildVersion(
740
        manifestString: manifest,
741 742
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
743
        expectedBuildNumber: '1.0.0',
744 745 746
      );
    });

747
    testUsingOsxContext('allow build info to override build name', () async {
748 749 750 751 752 753 754 755
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
756
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
757
      await checkBuildVersion(
758
        manifestString: manifest,
759 760 761 762 763 764
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

765 766 767 768 769 770 771 772 773
    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:
''';
774
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
775 776 777 778 779 780 781 782
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1.0.2',
      );
    });

783
    testUsingOsxContext('allow build info to override build number', () async {
784 785 786 787 788 789 790 791
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
792
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3', treeShakeIcons: false);
793
      await checkBuildVersion(
794
        manifestString: manifest,
795 796 797 798 799 800
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

801
    testUsingOsxContext('allow build info to override build name and number', () async {
802 803 804 805 806 807 808 809
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
810
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
811
      await checkBuildVersion(
812
        manifestString: manifest,
813 814 815 816 817 818
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

819
    testUsingOsxContext('allow build info to override build name and set number', () async {
820 821 822 823 824 825 826 827
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
828
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
829
      await checkBuildVersion(
830
        manifestString: manifest,
831 832 833 834 835 836
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

837
    testUsingOsxContext('allow build info to set build name and number', () async {
838 839 840 841 842 843 844
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
845
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
846
      await checkBuildVersion(
847
        manifestString: manifest,
848 849 850 851 852
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });
853 854 855 856 857 858 859 860 861

    testUsingOsxContext('default build name and number when version is missing', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
862
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
863 864 865 866 867 868 869
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });
870
  });
871
}
872

873
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
874 875 876 877 878
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class MockAnsiTerminal extends Mock implements AnsiTerminal {
  @override
  bool get supportsColor => false;
}