xcodeproj_test.dart 25.7 KB
Newer Older
1 2 3
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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/build_info.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
13
import 'package:flutter_tools/src/project.dart';
14 15 16 17
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

18 19
import '../../src/common.dart';
import '../../src/context.dart';
20
import '../../src/mocks.dart' as mocks;
21
import '../../src/pubspec_schema.dart';
22 23

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

void main() {
26
  group('xcodebuild versioning', () {
27
    mocks.MockProcessManager mockProcessManager;
28 29 30 31 32
    XcodeProjectInterpreter xcodeProjectInterpreter;
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
33
      mockProcessManager = mocks.MockProcessManager();
34
      xcodeProjectInterpreter = XcodeProjectInterpreter();
35
      macOS = fakePlatform('macos');
36
      fs = MemoryFileSystem();
37 38 39 40 41 42 43 44 45 46 47 48 49
      fs.file(xcodebuild).createSync(recursive: true);
    });

    void testUsingOsxContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        ProcessManager: () => mockProcessManager,
        Platform: () => macOS,
        FileSystem: () => fs,
      });
    }

    testUsingOsxContext('versionText returns null when xcodebuild is not installed', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
50
          .thenThrow(const ProcessException(xcodebuild, <String>['-version']));
51 52 53 54 55
      expect(xcodeProjectInterpreter.versionText, isNull);
    });

    testUsingOsxContext('versionText returns null when xcodebuild is not fully installed', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
56
        ProcessResult(
57 58 59 60 61 62 63 64 65 66 67 68 69
          0,
          1,
          "xcode-select: error: tool 'xcodebuild' requires Xcode, "
          "but active developer directory '/Library/Developer/CommandLineTools' "
          'is a command line tools instance',
          '',
        ),
      );
      expect(xcodeProjectInterpreter.versionText, isNull);
    });

    testUsingOsxContext('versionText returns formatted version text', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
70
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
71 72 73 74 75
      expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
    });

    testUsingOsxContext('versionText handles Xcode version string with unexpected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
76
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
77 78 79 80 81
      expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
    });

    testUsingOsxContext('majorVersion returns major version', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
82
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
83 84 85 86 87
      expect(xcodeProjectInterpreter.majorVersion, 8);
    });

    testUsingOsxContext('majorVersion is null when version has unexpected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
88
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
89 90 91 92 93
      expect(xcodeProjectInterpreter.majorVersion, isNull);
    });

    testUsingOsxContext('minorVersion returns minor version', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
94
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
95 96 97 98 99
      expect(xcodeProjectInterpreter.minorVersion, 3);
    });

    testUsingOsxContext('minorVersion returns 0 when minor version is unspecified', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
100
          .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
101 102 103 104 105
      expect(xcodeProjectInterpreter.minorVersion, 0);
    });

    testUsingOsxContext('minorVersion is null when version has unexpected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
106
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
107 108 109 110 111 112 113
      expect(xcodeProjectInterpreter.minorVersion, isNull);
    });

    testUsingContext('isInstalled is false when not on MacOS', () {
      fs.file(xcodebuild).deleteSync();
      expect(xcodeProjectInterpreter.isInstalled, isFalse);
    }, overrides: <Type, Generator>{
114
      Platform: () => fakePlatform('notMacOS'),
115 116 117 118 119 120 121 122 123
    });

    testUsingOsxContext('isInstalled is false when xcodebuild does not exist', () {
      fs.file(xcodebuild).deleteSync();
      expect(xcodeProjectInterpreter.isInstalled, isFalse);
    });

    testUsingOsxContext('isInstalled is false when Xcode is not fully installed', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
124
        ProcessResult(
125 126 127 128 129 130 131 132 133 134 135 136 137
          0,
          1,
          "xcode-select: error: tool 'xcodebuild' requires Xcode, "
          "but active developer directory '/Library/Developer/CommandLineTools' "
          'is a command line tools instance',
          '',
        ),
      );
      expect(xcodeProjectInterpreter.isInstalled, isFalse);
    });

    testUsingOsxContext('isInstalled is false when version has unexpected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
138
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
139 140 141 142 143
      expect(xcodeProjectInterpreter.isInstalled, isFalse);
    });

    testUsingOsxContext('isInstalled is true when version has expected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
144
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
145 146
      expect(xcodeProjectInterpreter.isInstalled, isTrue);
    });
147 148

    testUsingOsxContext('build settings is empty when xcodebuild failed to get the build settings', () {
149 150 151 152
      when(mockProcessManager.runSync(
               argThat(contains(xcodebuild)),
               workingDirectory: anyNamed('workingDirectory'),
               environment: anyNamed('environment')))
153 154 155
          .thenReturn(ProcessResult(0, 1, '', ''));
      expect(xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
    });
156 157 158

    testUsingContext('build settings flakes', () async {
      const Duration delay = Duration(seconds: 1);
159 160 161 162
      mockProcessManager.processFactory = mocks.flakyProcessFactory(
        flakes: 1,
        delay: delay + const Duration(seconds: 1),
      );
163 164 165 166 167 168 169 170 171 172 173 174
      expect(await xcodeProjectInterpreter.getBuildSettingsAsync(
                 '', '', timeout: delay),
             const <String, String>{});
      // build settings times out and is killed once, then succeeds.
      verify(mockProcessManager.killPid(any)).called(1);
      // The verbose logs should tell us something timed out.
      expect(testLogger.traceText, contains('timed out'));
    }, overrides: <Type, Generator>{
      Platform: () => macOS,
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
    });
175
  });
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 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

  group('xcodebuild -list', () {
    mocks.MockProcessManager mockProcessManager;
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
      mockProcessManager = mocks.MockProcessManager();
      macOS = fakePlatform('macos');
      fs = MemoryFileSystem();
      fs.file(xcodebuild).createSync(recursive: true);
    });

    void testUsingOsxContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        ProcessManager: () => mockProcessManager,
        Platform: () => macOS,
        FileSystem: () => fs,
      });
    }

    testUsingOsxContext('getInfo returns something when xcodebuild -list succeeds', () async {
      const String workingDirectory = '/';
      when(mockProcessManager.run(<String>[xcodebuild, '-list'],
          environment: anyNamed('environment'),
          workingDirectory: workingDirectory)).thenAnswer((_) {
        return Future<ProcessResult>.value(ProcessResult(1, 0, '', ''));
      });
      final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter();
      expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
    });

    testUsingOsxContext('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(mockProcessManager.run(<String>[xcodebuild, '-list'],
          environment: anyNamed('environment'),
          workingDirectory: workingDirectory)).thenAnswer((_) {
        return Future<ProcessResult>.value(ProcessResult(1, 66, '', stderr));
      });
      final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter();
      expect(
          () async => await xcodeProjectInterpreter.getInfo(workingDirectory),
          throwsToolExit(message: stderr));
    });
  });

223 224
  group('Xcode project properties', () {
    test('properties from default project can be parsed', () {
225
      const String output = '''
226 227 228 229 230 231 232 233 234 235 236 237 238 239
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

''';
240
      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
241 242 243 244 245
      expect(info.targets, <String>['Runner']);
      expect(info.schemes, <String>['Runner']);
      expect(info.buildConfigurations, <String>['Debug', 'Release']);
    });
    test('properties from project with custom schemes can be parsed', () {
246
      const String output = '''
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
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

''';
264
      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
265 266 267 268 269 270 271 272 273 274 275
      expect(info.targets, <String>['Runner']);
      expect(info.schemes, <String>['Free', 'Paid']);
      expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']);
    });
    test('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');
    });
    test('expected build configuration for non-flavored build is derived from BuildMode', () {
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
276
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
277 278 279 280 281 282 283 284 285
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
    });
    test('expected scheme for flavored build is the title-cased flavor', () {
      expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello')), 'Hello');
      expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO')), 'HELLO');
      expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello');
    });
    test('expected build configuration for flavored build is Mode-Flavor', () {
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello'), 'Hello'), 'Debug-Hello');
286
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO'), 'Hello'), 'Profile-Hello');
287 288 289
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello');
    });
    test('scheme for default project is Runner', () {
290
      final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);
291 292 293 294 295 296
      expect(info.schemeFor(BuildInfo.debug), 'Runner');
      expect(info.schemeFor(BuildInfo.profile), 'Runner');
      expect(info.schemeFor(BuildInfo.release), 'Runner');
      expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
    });
    test('build configuration for default project is matched against BuildMode', () {
297
      final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner']);
298
      expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
299
      expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
300 301 302
      expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
    });
    test('scheme for project with custom schemes is matched against flavor', () {
303
      final XcodeProjectInfo info = XcodeProjectInfo(
304 305 306 307 308 309 310 311 312 313 314
        <String>['Runner'],
        <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
        <String>['Free', 'Paid'],
      );
      expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free')), 'Free');
      expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free')), 'Free');
      expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid')), 'Paid');
      expect(info.schemeFor(const BuildInfo(BuildMode.debug, null)), isNull);
      expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
    });
    test('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
315
      final XcodeProjectInfo info = XcodeProjectInfo(
316
        <String>['Runner'],
317
        <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
318 319 320 321
        <String>['Free', 'Paid'],
      );
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free'), 'Free'), 'debug (free)');
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid'), 'Paid'), 'Debug paid');
322
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE'), 'Free'), 'profile - Free');
323 324 325
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid');
    });
    test('build configuration for project with inconsistent naming is null', () {
326
      final XcodeProjectInfo info = XcodeProjectInfo(
327 328 329 330 331 332 333 334 335
        <String>['Runner'],
        <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
        <String>['Free', 'Paid'],
      );
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free'), 'Free'), null);
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null);
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null);
    });
  });
336 337 338 339 340 341 342 343

  group('updateGeneratedXcodeProperties', () {
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
344 345 346
      fs = MemoryFileSystem();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
347 348 349 350 351 352 353 354 355 356 357 358 359
      macOS = fakePlatform('macos');
      fs.file(xcodebuild).createSync(recursive: true);
    });

    void testUsingOsxContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        ProcessManager: () => mockProcessManager,
        Platform: () => macOS,
        FileSystem: () => fs,
      });
    }

360
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
361 362
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
363
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
364

365
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
366
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
367 368
      await updateGeneratedXcodeProperties(
        project: project,
369 370 371 372 373 374 375 376
        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=armv7'), isTrue);
377 378 379 380 381 382

      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=armv7'), isTrue);
383 384
    });

385
    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
386 387
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
388
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
389
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true);
390
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
391 392
      await updateGeneratedXcodeProperties(
        project: project,
393 394 395 396 397 398 399 400
        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('TRACK_WIDGET_CREATION=true'), isTrue);
401 402 403 404 405 406

      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('TRACK_WIDGET_CREATION=true'), isTrue);
407 408 409
    });

    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
410 411
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
412
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
413
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
414
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
415 416
      await updateGeneratedXcodeProperties(
        project: project,
417 418 419 420 421 422 423 424
        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('TRACK_WIDGET_CREATION=true'), isFalse);
425 426 427 428 429 430

      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('TRACK_WIDGET_CREATION=true'), isFalse);
431 432
    });

433
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
434 435
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
436
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
437
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
438

439
      final FlutterProject project = FlutterProject.fromPath('path/to/project');
440 441
      await updateGeneratedXcodeProperties(
        project: project,
442 443 444 445 446 447 448 449 450
        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);
    });
451 452 453 454 455 456 457 458 459 460 461

    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({
462
      String manifestString,
463 464 465 466
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
467 468
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
469 470 471 472 473 474 475
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios'));

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

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

478
      await updateGeneratedXcodeProperties(
479
        project: FlutterProject.fromPath('path/to/project'),
480 481 482
        buildInfo: buildInfo,
      );

483
      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
484 485
      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
486
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), isNotNull);
487 488
    }

489
    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
490 491 492 493 494 495 496 497 498
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

499
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
500
      await checkBuildVersion(
501
        manifestString: manifest,
502 503 504 505 506 507
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

508
    testUsingOsxContext('extract build name from pubspec.yaml', () async {
509 510 511 512 513 514 515 516
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
517
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
518
      await checkBuildVersion(
519
        manifestString: manifest,
520 521
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
522
        expectedBuildNumber: '1.0.0',
523 524 525
      );
    });

526
    testUsingOsxContext('allow build info to override build name', () async {
527 528 529 530 531 532 533 534
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
535
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2');
536
      await checkBuildVersion(
537
        manifestString: manifest,
538 539 540 541 542 543
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
    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:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2');
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1.0.2',
      );
    });

562
    testUsingOsxContext('allow build info to override build number', () async {
563 564 565 566 567 568 569 570
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
571
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
572
      await checkBuildVersion(
573
        manifestString: manifest,
574 575 576 577 578 579
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

580
    testUsingOsxContext('allow build info to override build name and number', () async {
581 582 583 584 585 586 587 588
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
589
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
590
      await checkBuildVersion(
591
        manifestString: manifest,
592 593 594 595 596 597
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

598
    testUsingOsxContext('allow build info to override build name and set number', () async {
599 600 601 602 603 604 605 606
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
607
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
608
      await checkBuildVersion(
609
        manifestString: manifest,
610 611 612 613 614 615
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

616
    testUsingOsxContext('allow build info to set build name and number', () async {
617 618 619 620 621 622 623
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
624
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
625
      await checkBuildVersion(
626
        manifestString: manifest,
627 628 629 630 631 632
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });
  });
633
}
634 635

Platform fakePlatform(String name) {
636
  return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
637 638
}

639
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
640 641
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter { }