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

5 6 7
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
8
import 'package:flutter_tools/src/base/logger.dart';
9
import 'package:flutter_tools/src/base/os.dart';
10
import 'package:flutter_tools/src/build_info.dart';
11
import 'package:flutter_tools/src/cache.dart';
12
import 'package:flutter_tools/src/convert.dart';
13
import 'package:flutter_tools/src/features.dart';
14
import 'package:flutter_tools/src/flutter_manifest.dart';
15
import 'package:flutter_tools/src/globals.dart' as globals;
16
import 'package:flutter_tools/src/ios/plist_parser.dart';
17
import 'package:flutter_tools/src/ios/xcodeproj.dart';
18
import 'package:flutter_tools/src/project.dart';
19
import 'package:meta/meta.dart';
20
import 'package:test/fake.dart';
21

22 23
import '../src/common.dart';
import '../src/context.dart';
24
import '../src/fakes.dart';
25 26

void main() {
27
  // TODO(zanderso): remove once FlutterProject is fully refactored.
28 29 30
  // this is safe since no tests have expectations on the test logger.
  final BufferLogger logger = BufferLogger.test();

31
  group('Project', () {
32
    group('construction', () {
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
      testWithoutContext('invalid utf8 throws a tool exit', () {
        final FileSystem fileSystem = MemoryFileSystem.test();
        final FlutterProjectFactory projectFactory = FlutterProjectFactory(
          fileSystem: fileSystem,
          logger: BufferLogger.test(),
        );
        fileSystem.file('pubspec.yaml').writeAsBytesSync(<int>[0xFFFE]);

        /// Technically this should throw a FileSystemException but this is
        /// currently a bug in package:file.
        expect(
          () => projectFactory.fromDirectory(fileSystem.currentDirectory),
          throwsToolExit(),
        );
      });

49
      _testInMemory('fails on invalid pubspec.yaml', () async {
50
        final Directory directory = globals.fs.directory('myproject');
51 52 53
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
54 55 56

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
57
          throwsToolExit(),
58 59 60
        );
      });

61
      _testInMemory('fails on pubspec.yaml parse failure', () async {
62
        final Directory directory = globals.fs.directory('myproject');
63 64 65 66 67 68
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(parseErrorPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
69
          throwsToolExit(),
70 71 72
        );
      });

73
      _testInMemory('fails on invalid example/pubspec.yaml', () async {
74
        final Directory directory = globals.fs.directory('myproject');
75 76 77
        directory.childDirectory('example').childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
78 79 80

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
81
          throwsToolExit(),
82 83 84
        );
      });

85
      _testInMemory('treats missing pubspec.yaml as empty', () async {
86
        final Directory directory = globals.fs.directory('myproject')
87
          ..createSync(recursive: true);
88
        expect(FlutterProject.fromDirectory(directory).manifest.isEmpty,
89 90 91 92
          true,
        );
      });

93
      _testInMemory('reads valid pubspec.yaml', () async {
94
        final Directory directory = globals.fs.directory('myproject');
95 96 97 98
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspec);
        expect(
99
          FlutterProject.fromDirectory(directory).manifest.appName,
100 101 102 103
          'hello',
        );
      });

104 105 106 107 108 109 110 111 112 113 114
      _testInMemory('reads dependencies from pubspec.yaml', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspecWithDependencies);
        expect(
          FlutterProject.fromDirectory(directory).manifest.dependencies,
          <String>{'plugin_a', 'plugin_b'},
        );
      });

115
      _testInMemory('sets up location', () async {
116
        final Directory directory = globals.fs.directory('myproject');
117
        expect(
118
          FlutterProject.fromDirectory(directory).directory.absolute.path,
119 120 121
          directory.absolute.path,
        );
        expect(
122
          FlutterProject.fromDirectoryTest(directory).directory.absolute.path,
123 124 125
          directory.absolute.path,
        );
        expect(
126
          FlutterProject.current().directory.absolute.path,
127
          globals.fs.currentDirectory.absolute.path,
128 129
        );
      });
130
    });
131

132
    group('ensure ready for platform-specific tooling', () {
133
      _testInMemory('does nothing, if project is not created', () async {
134
        final FlutterProject project = FlutterProject(
135
          globals.fs.directory('not_created'),
136 137
          FlutterManifest.empty(logger: logger),
          FlutterManifest.empty(logger: logger),
138
        );
139
        await project.regeneratePlatformSpecificTooling();
140
        expectNotExists(project.directory);
141
      });
142
      _testInMemory('does nothing in plugin or package root project', () async {
143
        final FlutterProject project = await aPluginProject();
144
        await project.regeneratePlatformSpecificTooling();
145
        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
146
        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
147
        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
148
        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
149
      });
150 151 152 153 154 155 156 157 158 159 160 161
      _testInMemory('works if there is an "example" folder', () async {
        final FlutterProject project = await someProject();
        // The presence of an "example" folder used to be used as an indicator
        // that a project was a plugin, but shouldn't be as this creates false
        // positives.
        project.directory.childDirectory('example').createSync();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
162
      _testInMemory('injects plugins for iOS', () async {
163
        final FlutterProject project = await someProject();
164
        await project.regeneratePlatformSpecificTooling();
165
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
166
      });
167
      _testInMemory('generates Xcode configuration for iOS', () async {
168
        final FlutterProject project = await someProject();
169
        await project.regeneratePlatformSpecificTooling();
170
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
171
      });
172
      _testInMemory('injects plugins for Android', () async {
173
        final FlutterProject project = await someProject();
174
        await project.regeneratePlatformSpecificTooling();
175
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
176
      });
177
      _testInMemory('updates local properties for Android', () async {
178
        final FlutterProject project = await someProject();
179
        await project.regeneratePlatformSpecificTooling();
180
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
181
      });
182 183 184
      _testInMemory('checkForDeprecation fails on invalid android app manifest file', () async {
        // This is not a valid Xml document
        const String invalidManifest = '<manifest></application>';
185
        final FlutterProject project = await someProject(androidManifestOverride: invalidManifest, includePubspec: true);
186 187 188 189 190 191

        expect(
          () => project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore),
          throwsToolExit(message: 'Please ensure that the android manifest is a valid XML document and try again.'),
        );
      });
192
      _testInMemory('Android project not on v2 embedding shows a warning', () async {
193
        final FlutterProject project = await someProject(includePubspec: true);
194 195 196 197
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

198
        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore);
199
        expect(testLogger.statusText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
200 201
      });
      _testInMemory('Android project not on v2 embedding exits', () async {
202
        final FlutterProject project = await someProject(includePubspec: true);
203 204 205 206 207 208 209 210
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

        await expectToolExitLater(
          Future<dynamic>.sync(() => project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.exit)),
          contains('Build failed due to use of deprecated Android v1 embedding.')
        );
211
        expect(testLogger.statusText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
212
        expect(testLogger.statusText, contains('No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in '));
213
      });
214
      _testInMemory('Project not on v2 embedding does not warn if deprecation status is irrelevant', () async {
215
        final FlutterProject project = await someProject(includePubspec: true);
216 217 218 219
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

220 221
        // Default is "DeprecationBehavior.none"
        project.checkForDeprecation();
222 223
        expect(testLogger.statusText, isEmpty);
      });
224
      _testInMemory('Android project not on v2 embedding ignore continues', () async {
225
        final FlutterProject project = await someProject(includePubspec: true);
226 227 228 229 230
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore);
231
        expect(testLogger.statusText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
232
      });
233 234 235 236 237 238 239 240 241
      _testInMemory('Android project no pubspec continues', () async {
        final FlutterProject project = await someProject();
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore);
        expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects')));
      });
242 243 244 245
      _testInMemory('Android plugin project does not throw v1 embedding deprecation warning', () async {
        final FlutterProject project = await aPluginProject();

        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.exit);
246
        expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects')));
247 248
        expect(testLogger.statusText, isNot(contains('No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in ')));
      });
249 250 251 252 253
      _testInMemory('Android plugin without example app does not show a warning', () async {
        final FlutterProject project = await aPluginProject();
        project.example.directory.deleteSync();

        await project.regeneratePlatformSpecificTooling();
254
        expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects')));
255
      });
256 257
      _testInMemory('updates local properties for Android', () async {
        final FlutterProject project = await someProject();
258
        await project.regeneratePlatformSpecificTooling();
259 260
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
261 262 263
      testUsingContext('injects plugins for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
264
        await project.regeneratePlatformSpecificTooling();
265
        expectExists(project.macos.pluginRegistrantImplementation);
266
      }, overrides: <Type, Generator>{
267
        FileSystem: () => MemoryFileSystem.test(),
268
        ProcessManager: () => FakeProcessManager.any(),
269
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
270 271 272 273
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
274 275 276 277
      });
      testUsingContext('generates Xcode configuration for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
278
        await project.regeneratePlatformSpecificTooling();
279 280
        expectExists(project.macos.generatedXcodePropertiesFile);
      }, overrides: <Type, Generator>{
281
        FileSystem: () => MemoryFileSystem.test(),
282
        ProcessManager: () => FakeProcessManager.any(),
283
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
284 285 286 287
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
288 289 290
      });
      testUsingContext('injects plugins for Linux', () async {
        final FlutterProject project = await someProject();
291
        project.linux.cmakeFile.createSync(recursive: true);
292
        await project.regeneratePlatformSpecificTooling();
293 294 295
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
296
        FileSystem: () => MemoryFileSystem.test(),
297
        ProcessManager: () => FakeProcessManager.any(),
298
        FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
299 300 301 302
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
303 304 305
      });
      testUsingContext('injects plugins for Windows', () async {
        final FlutterProject project = await someProject();
306
        project.windows.cmakeFile.createSync(recursive: true);
307
        await project.regeneratePlatformSpecificTooling();
308 309 310
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
311
        FileSystem: () => MemoryFileSystem.test(),
312
        ProcessManager: () => FakeProcessManager.any(),
313
        FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
314 315 316 317
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
318
      });
319
      _testInMemory('creates Android library in module', () async {
320
        final FlutterProject project = await aModuleProject();
321
        await project.regeneratePlatformSpecificTooling();
322 323 324
        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
325
      });
326
      _testInMemory('creates iOS pod in module', () async {
327
        final FlutterProject project = await aModuleProject();
328
        await project.regeneratePlatformSpecificTooling();
329
        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
330
        expectExists(flutter.childFile('podhelper.rb'));
331
        expectExists(flutter.childFile('flutter_export_environment.sh'));
332 333 334 335 336 337
        expectExists(flutter.childFile('Generated.xcconfig'));
        final Directory pluginRegistrantClasses = flutter
            .childDirectory('FlutterPluginRegistrant')
            .childDirectory('Classes');
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
338
      });
339

340
      testUsingContext('Version.json info is correct', () {
341 342 343 344
        final MemoryFileSystem fileSystem = MemoryFileSystem.test();
        final FlutterManifest manifest = FlutterManifest.createFromString('''
    name: test
    version: 1.0.0+3
345 346
    ''', logger: BufferLogger.test())!;
        final FlutterProject project = FlutterProject(fileSystem.systemTempDirectory, manifest, manifest);
347
        final Map<String, dynamic> versionInfo = jsonDecode(project.getVersionInfo()) as Map<String, dynamic>;
348 349 350
        expect(versionInfo['app_name'],'test');
        expect(versionInfo['version'],'1.0.0');
        expect(versionInfo['build_number'],'3');
351
        expect(versionInfo['package_name'],'test');
352
      });
353 354 355 356 357 358 359 360 361 362 363 364 365
      _testInMemory('gets xcworkspace directory', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.hostAppRoot.childFile('._Runner.xcworkspace').createSync(recursive: true);
        project.ios.hostAppRoot.childFile('Runner.xcworkspace').createSync(recursive: true);

        expect(project.ios.xcodeWorkspace?.basename, 'Runner.xcworkspace');
      });
      _testInMemory('no xcworkspace directory found', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        expect(project.ios.xcodeWorkspace?.basename, null);
      });
366 367
    });

368
    group('module status', () {
369
      _testInMemory('is known for module', () async {
370 371 372 373
        final FlutterProject project = await aModuleProject();
        expect(project.isModule, isTrue);
        expect(project.android.isModule, isTrue);
        expect(project.ios.isModule, isTrue);
374
        expect(project.android.hostAppGradleRoot.basename, '.android');
375
        expect(project.ios.hostAppRoot.basename, '.ios');
376
      });
377
      _testInMemory('is known for non-module', () async {
378
        final FlutterProject project = await someProject();
379 380 381
        expect(project.isModule, isFalse);
        expect(project.android.isModule, isFalse);
        expect(project.ios.isModule, isFalse);
382
        expect(project.android.hostAppGradleRoot.basename, 'android');
383
        expect(project.ios.hostAppRoot.basename, 'ios');
384
      });
385
    });
386 387

    group('example', () {
388
      _testInMemory('exists for plugin in legacy format', () async {
389
        final FlutterProject project = await aPluginProject();
390
        expect(project.isPlugin, isTrue);
391 392
        expect(project.hasExampleApp, isTrue);
      });
393
      _testInMemory('exists for plugin in multi-platform format', () async {
394 395 396
        final FlutterProject project = await aPluginProject(legacy: false);
        expect(project.hasExampleApp, isTrue);
      });
397
      _testInMemory('does not exist for non-plugin', () async {
398
        final FlutterProject project = await someProject();
399
        expect(project.isPlugin, isFalse);
400 401 402 403
        expect(project.hasExampleApp, isFalse);
      });
    });

404
    group('language', () {
405 406 407
      late XcodeProjectInterpreter xcodeProjectInterpreter;
      late MemoryFileSystem fs;
      late FlutterProjectFactory flutterProjectFactory;
408
      setUp(() {
409
        fs = MemoryFileSystem.test();
410
        xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: FakeProcessManager.any());
411 412 413 414
        flutterProjectFactory = FlutterProjectFactory(
          logger: logger,
          fileSystem: fs,
        );
415 416
      });

417
      _testInMemory('default host app language', () async {
418 419 420 421
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

422
      testUsingContext('kotlin host app language', () async {
423 424 425 426
        final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
427
            return '''
428 429 430 431 432 433
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
434
        FileSystem: () => fs,
435
        ProcessManager: () => FakeProcessManager.any(),
436
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
437
        FlutterProjectFactory: () => flutterProjectFactory,
438 439 440
      });
    });

441
    group('product bundle identifier', () {
442 443 444 445
      late MemoryFileSystem fs;
      late FakePlistParser testPlistUtils;
      late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
      late FlutterProjectFactory flutterProjectFactory;
446
      setUp(() {
447
        fs = MemoryFileSystem.test();
448
        testPlistUtils = FakePlistParser();
449
        xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
450 451 452 453
        flutterProjectFactory = FlutterProjectFactory(
          fileSystem: fs,
          logger: logger,
        );
454 455
      });

456
      void testWithMocks(String description, Future<void> Function() testMethod) {
457 458
        testUsingContext(description, testMethod, overrides: <Type, Generator>{
          FileSystem: () => fs,
459
          ProcessManager: () => FakeProcessManager.any(),
460
          PlistParser: () => testPlistUtils,
461
          XcodeProjectInterpreter: () => xcodeProjectInterpreter,
462
          FlutterProjectFactory: () => flutterProjectFactory,
463 464 465
        });
      }

466
      testWithMocks('null, if no build settings or plist entries', () async {
467
        final FlutterProject project = await someProject();
468
        expect(await project.ios.productBundleIdentifier(null), isNull);
469
      });
470 471 472

      testWithMocks('from build settings, if no plist', () async {
        final FlutterProject project = await someProject();
473
        project.ios.xcodeProject.createSync();
474 475
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
476 477
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
478
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
479

480
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
481 482 483
      });

      testWithMocks('from project file, if no plist or build settings', () async {
484
        final FlutterProject project = await someProject();
485 486
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);

487 488 489
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
490
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
491
      });
492

493 494
      testWithMocks('from plist, if no variables', () async {
        final FlutterProject project = await someProject();
495
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
496
        testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
497
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
498
      });
499 500

      testWithMocks('from build settings and plist, if default variable', () async {
501
        final FlutterProject project = await someProject();
502
        project.ios.xcodeProject.createSync();
503 504
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
505 506
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
507
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
508
        testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
509

510
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
511
      });
512 513

      testWithMocks('from build settings and plist, by substitution', () async {
514
        final FlutterProject project = await someProject();
515
        project.ios.xcodeProject.createSync();
516
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
517 518
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
519 520 521
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
          'SUFFIX': 'suffix',
        };
522
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
523
        testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
524

525
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
526
      });
527

528 529 530 531 532 533 534 535 536 537 538
      testWithMocks('Always pass parsing org on ios project with flavors', () async {
        final FlutterProject project = await someProject();
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
        });
        project.ios.xcodeProject.createSync();
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);

        expect(await project.organizationNames, <String>[]);
      });

539 540
      testWithMocks('fails with no flavor and defined schemes', () async {
        final FlutterProject project = await someProject();
541
        project.ios.xcodeProject.createSync();
542
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
543

544 545 546 547 548 549 550 551
        await expectToolExitLater(
          project.ios.productBundleIdentifier(null),
          contains('You must specify a --flavor option to select one of the available schemes.')
        );
      });

      testWithMocks('handles case insensitive flavor', () async {
        final FlutterProject project = await someProject();
552
        project.ios.xcodeProject.createSync();
553 554
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
        xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
555 556
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
557
        xcodeProjectInterpreter.xcodeProjectInfo =XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
558
        const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
559

560 561 562 563 564
        expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
      });

      testWithMocks('fails with flavor and default schemes', () async {
        final FlutterProject project = await someProject();
565
        project.ios.xcodeProject.createSync();
566
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
567
        const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
568

569 570 571 572 573 574
        await expectToolExitLater(
          project.ios.productBundleIdentifier(buildInfo),
          contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.')
        );
      });

575 576
      testWithMocks('empty surrounded by quotes', () async {
        final FlutterProject project = await someProject();
577
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
578 579 580
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('', qualifier: '"');
        });
581
        expect(await project.ios.productBundleIdentifier(null), '');
582
      });
583

584 585
      testWithMocks('surrounded by double quotes', () async {
        final FlutterProject project = await someProject();
586
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
587 588 589
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
        });
590
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
591
      });
592

593 594
      testWithMocks('surrounded by single quotes', () async {
        final FlutterProject project = await someProject();
595
        xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
596
        addIosProjectFile(project.directory, projectFileContent: () {
597
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
598
        });
599
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
600
      });
601 602
    });

603
    group('application bundle name', () {
604 605
      late MemoryFileSystem fs;
      late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
606
      setUp(() {
607
        fs = MemoryFileSystem.test();
608
        mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
609 610 611 612
      });

      testUsingContext('app product name defaults to Runner.app', () async {
        final FlutterProject project = await someProject();
613
        expect(await project.ios.hostAppBundleName(null), 'Runner.app');
614 615 616
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
617
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
618 619 620 621
      });

      testUsingContext('app product name xcodebuild settings', () async {
        final FlutterProject project = await someProject();
622
        project.ios.xcodeProject.createSync();
623 624
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
625
          'FULL_PRODUCT_NAME': 'My App.app',
626 627
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
628

629
        expect(await project.ios.hostAppBundleName(null), 'My App.app');
630 631 632
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
633
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
634 635 636
      });
    });

637
    group('organization names set', () {
638
      _testInMemory('is empty, if project not created', () async {
639
        final FlutterProject project = await someProject();
640
        expect(await project.organizationNames, isEmpty);
641
      });
642
      _testInMemory('is empty, if no platform folders exist', () async {
643
        final FlutterProject project = await someProject();
644
        project.directory.createSync();
645
        expect(await project.organizationNames, isEmpty);
646
      });
647
      _testInMemory('is populated from iOS bundle identifier', () async {
648
        final FlutterProject project = await someProject();
649
        addIosProjectFile(project.directory, projectFileContent: () {
650
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
651
        });
652
        expect(await project.organizationNames, <String>['io.flutter']);
653
      });
654
      _testInMemory('is populated from Android application ID', () async {
655
        final FlutterProject project = await someProject();
656 657 658 659
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
660
        expect(await project.organizationNames, <String>['io.flutter']);
661
      });
662
      _testInMemory('is populated from iOS bundle identifier in plugin example', () async {
663
        final FlutterProject project = await someProject();
664
        addIosProjectFile(project.example.directory, projectFileContent: () {
665
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
666
        });
667
        expect(await project.organizationNames, <String>['io.flutter']);
668
      });
669
      _testInMemory('is populated from Android application ID in plugin example', () async {
670
        final FlutterProject project = await someProject();
671 672 673 674
        addAndroidGradleFile(project.example.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
675
        expect(await project.organizationNames, <String>['io.flutter']);
676
      });
677
      _testInMemory('is populated from Android group in plugin', () async {
678
        final FlutterProject project = await someProject();
679
        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
680
        expect(await project.organizationNames, <String>['io.flutter']);
681
      });
682
      _testInMemory('is singleton, if sources agree', () async {
683
        final FlutterProject project = await someProject();
684 685 686 687 688 689 690
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
691
        expect(await project.organizationNames, <String>['io.flutter']);
692
      });
693
      _testInMemory('is non-singleton, if sources disagree', () async {
694
        final FlutterProject project = await someProject();
695 696 697 698 699 700 701
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.clutter.someproject');
          });
702
        expect(
703
          await project.organizationNames,
704 705 706 707 708
          <String>['io.flutter', 'io.clutter'],
        );
      });
    });
  });
709
  group('watch companion', () {
710 711 712 713
    late MemoryFileSystem fs;
    late FakePlistParser testPlistParser;
    late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
    late FlutterProjectFactory flutterProjectFactory;
714 715
    setUp(() {
      fs = MemoryFileSystem.test();
716
      testPlistParser = FakePlistParser();
717
      mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
718 719 720 721 722 723 724 725
      flutterProjectFactory = FlutterProjectFactory(
        fileSystem: fs,
        logger: logger,
      );
    });

    testUsingContext('cannot find bundle identifier', () async {
      final FlutterProject project = await someProject();
726 727 728 729 730 731 732 733 734
      final XcodeProjectInfo projectInfo = XcodeProjectInfo(<String>['WatchTarget'], <String>[], <String>[], logger);
      expect(
        await project.ios.containsWatchCompanion(
          projectInfo: projectInfo,
          buildInfo: BuildInfo.debug,
          deviceId: '123',
        ),
        isFalse,
      );
735 736 737
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
738
      PlistParser: () => testPlistParser,
739 740 741 742 743 744
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      FlutterProjectFactory: () => flutterProjectFactory,
    });

    group('with bundle identifier', () {
      setUp(() {
745 746
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
747 748
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
749
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
750 751 752 753
      });

      testUsingContext('no Info.plist in target', () async {
        final FlutterProject project = await someProject();
754 755 756 757 758 759 760 761
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
762 763 764
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
765
        PlistParser: () => testPlistParser,
766 767 768 769 770 771 772 773
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('Info.plist in target does not contain WKCompanionAppBundleIdentifier', () async {
        final FlutterProject project = await someProject();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);

774 775 776 777 778 779 780 781
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
782 783 784
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
785
        PlistParser: () => testPlistParser,
786 787 788 789 790 791 792 793
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('target WKCompanionAppBundleIdentifier is not project bundle identifier', () async {
        final FlutterProject project = await someProject();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);

794
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someOTHERproject');
795 796 797 798 799 800 801 802
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
803 804 805
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
806
        PlistParser: () => testPlistParser,
807 808 809 810
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

811
      testUsingContext('has watch companion in plist', () async {
812
        final FlutterProject project = await someProject();
813
        project.ios.xcodeProject.createSync();
814
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
815
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someProject');
816

817 818 819 820 821 822 823 824
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
825 826 827
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
828
        PlistParser: () => testPlistParser,
829 830 831
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
832

833
      testUsingContext('has watch companion in plist with xcode variable', () async {
834 835
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
836 837 838 839 840
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123',
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
841 842 843 844 845
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion in other scheme build settings', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.xcodeProjectInfoFile.writeAsStringSync('''
        Build settings for action build and target "WatchTarget":
            INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.flutter.someProject
''');

        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123',
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };

        const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
          scheme: 'WatchScheme',
          deviceId: '123',
          isWatch: true,
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
          'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': 'io.flutter.someProject',
        };

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion in other scheme build settings with xcode variable', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.xcodeProjectInfoFile.writeAsStringSync(r'''
        Build settings for action build and target "WatchTarget":
            INFOPLIST_KEY_WKCompanionAppBundleIdentifier = $(PRODUCT_BUNDLE_IDENTIFIER)
''');
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123'
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };

        const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
          scheme: 'WatchScheme',
          deviceId: '123',
          isWatch: true,
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
          'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
        };

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
936 937 938 939 940 941 942
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
943 944
    });
  });
945 946
}

947
Future<FlutterProject> someProject({
948
  String? androidManifestOverride,
949
  bool includePubspec = false,
950
}) async {
951
  final Directory directory = globals.fs.directory('some_project');
952 953 954 955
  directory.childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
956 957 958 959 960
  if (includePubspec) {
    directory.childFile('pubspec.yaml')
    ..createSync(recursive: true)
    ..writeAsStringSync(validPubspec);
  }
961
  directory.childDirectory('ios').createSync(recursive: true);
962 963 964 965 966
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
967
    .writeAsStringSync(androidManifestOverride ?? '<manifest></manifest>');
968
  return FlutterProject.fromDirectory(directory);
969 970
}

971
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
972
  final Directory directory = globals.fs.directory('plugin_project');
973
  directory.childDirectory('ios').createSync(recursive: true);
974 975
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
976 977 978
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
979 980 981 982 983 984
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
985 986 987 988 989 990 991 992 993 994 995 996
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
997 998
      linux:
        pluginClass: MyPlugin
999 1000
      macos:
        pluginClass: MyPlugin
1001 1002
      windows:
        pluginClass: MyPlugin
1003 1004 1005
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
1006
  return FlutterProject.fromDirectory(directory);
1007 1008
}

1009
Future<FlutterProject> aModuleProject() async {
1010
  final Directory directory = globals.fs.directory('module_project');
1011 1012 1013 1014 1015
  directory
    .childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1016
  directory.childFile('pubspec.yaml').writeAsStringSync('''
1017
name: my_module
1018
flutter:
1019
  module:
1020 1021 1022
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
1023
}
1024

1025 1026
/// Executes the [testMethod] in a context where the file system
/// is in memory.
1027
@isTest
1028
void _testInMemory(String description, Future<void> Function() testMethod) {
1029
  Cache.flutterRoot = getFlutterRoot();
1030
  final FileSystem testFileSystem = MemoryFileSystem(
1031
    style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
1032
  );
1033 1034 1035 1036 1037
  testFileSystem
    .directory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1038 1039
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
1040
  final Logger logger = BufferLogger.test();
1041 1042
  transfer(Cache(
    fileSystem: globals.fs,
1043
    logger: logger,
1044
    artifacts: <ArtifactSet>[],
1045 1046 1047 1048 1049 1050
    osUtils: OperatingSystemUtils(
      fileSystem: globals.fs,
      logger: logger,
      platform: globals.platform,
      processManager: globals.processManager,
    ),
1051 1052
    platform: globals.platform,
  ).getArtifactDirectory('gradle_wrapper'), testFileSystem);
1053
  transfer(globals.fs.directory(Cache.flutterRoot)
1054 1055
      .childDirectory('packages')
      .childDirectory('flutter_tools')
1056
      .childDirectory('templates'), testFileSystem);
1057 1058 1059 1060
  // Set up enough of the packages to satisfy the templating code.
  final File packagesFile = testFileSystem.directory(Cache.flutterRoot)
      .childDirectory('packages')
      .childDirectory('flutter_tools')
1061 1062
      .childDirectory('.dart_tool')
      .childFile('package_config.json');
1063 1064 1065
  final Directory dummyTemplateImagesDirectory = testFileSystem.directory(Cache.flutterRoot).parent;
  dummyTemplateImagesDirectory.createSync(recursive: true);
  packagesFile.createSync(recursive: true);
1066 1067 1068 1069 1070 1071 1072
  packagesFile.writeAsStringSync(json.encode(<String, Object>{
    'configVersion': 2,
    'packages': <Object>[
      <String, Object>{
        'name': 'flutter_template_images',
        'rootUri': dummyTemplateImagesDirectory.uri.toString(),
        'packageUri': 'lib/',
1073
        'languageVersion': '2.6',
1074 1075 1076
      },
    ],
  }));
1077

1078 1079 1080 1081
  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
1082
      FileSystem: () => testFileSystem,
1083
      ProcessManager: () => FakeProcessManager.any(),
1084 1085
      Cache: () => Cache(
        logger: globals.logger,
1086
        fileSystem: testFileSystem,
1087 1088
        osUtils: globals.os,
        platform: globals.platform,
1089
        artifacts: <ArtifactSet>[],
1090
      ),
1091 1092
      FlutterProjectFactory: () => FlutterProjectFactory(
        fileSystem: testFileSystem,
1093
        logger: globals.logger,
1094
      ),
1095 1096 1097 1098
    },
  );
}

1099 1100 1101 1102 1103
/// Transfers files and folders from the local file system's Flutter
/// installation to an (in-memory) file system used for testing.
void transfer(FileSystemEntity entity, FileSystem target) {
  if (entity is Directory) {
    target.directory(entity.absolute.path).createSync(recursive: true);
1104
    for (final FileSystemEntity child in entity.listSync()) {
1105 1106 1107 1108 1109
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
1110
    throw Exception('Unsupported FileSystemEntity ${entity.runtimeType}');
1111 1112 1113
  }
}

1114 1115 1116 1117 1118 1119 1120 1121
void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

void expectNotExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isFalse);
}

1122
void addIosProjectFile(Directory directory, {required String Function() projectFileContent}) {
1123 1124 1125 1126 1127
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
1128
    ..writeAsStringSync(projectFileContent());
1129 1130
}

1131
void addAndroidGradleFile(Directory directory, { required String Function() gradleFileContent }) {
1132 1133 1134 1135 1136
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile('build.gradle')
        ..createSync(recursive: true)
1137
        ..writeAsStringSync(gradleFileContent());
1138 1139 1140 1141 1142 1143 1144 1145
}

void addAndroidWithGroup(Directory directory, String id) {
  directory.childDirectory('android').childFile('build.gradle')
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileWithGroupId(id));
}

1146 1147 1148 1149 1150
String get validPubspec => '''
name: hello
flutter:
''';

1151 1152 1153 1154 1155 1156 1157 1158 1159 1160
String get validPubspecWithDependencies => '''
name: hello
flutter:

dependencies:
  plugin_a:
  plugin_b:
''';


1161 1162 1163 1164 1165 1166
String get invalidPubspec => '''
name: hello
flutter:
  invalid:
''';

1167 1168 1169 1170 1171 1172 1173 1174
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

1175
String projectFileWithBundleId(String id, {String? qualifier}) {
1176 1177 1178 1179 1180
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
1181
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
1193
    compileSdkVersion 31
1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209

    defaultConfig {
        applicationId '$id'
    }
}
''';
}

String gradleFileWithGroupId(String id) {
  return '''
group '$id'
version '1.0-SNAPSHOT'

apply plugin: 'com.android.library'

android {
1210
    compileSdkVersion 31
1211 1212 1213
}
''';
}
1214 1215 1216 1217 1218 1219 1220 1221 1222 1223

File androidPluginRegistrant(Directory parent) {
  return parent.childDirectory('src')
    .childDirectory('main')
    .childDirectory('java')
    .childDirectory('io')
    .childDirectory('flutter')
    .childDirectory('plugins')
    .childFile('GeneratedPluginRegistrant.java');
}
1224

1225
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
1226
  final Map<XcodeProjectBuildContext, Map<String, String>> buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
1227
  late XcodeProjectInfo xcodeProjectInfo;
1228 1229 1230

  @override
  Future<Map<String, String>> getBuildSettings(String projectPath, {
1231
    XcodeProjectBuildContext? buildContext,
1232 1233
    Duration timeout = const Duration(minutes: 1),
  }) async {
1234 1235 1236 1237
    if (buildSettingsByBuildContext[buildContext] == null) {
      return <String, String>{};
    }
    return buildSettingsByBuildContext[buildContext]!;
1238 1239 1240
  }

  @override
1241
  Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
1242 1243 1244
    return xcodeProjectInfo;
  }

1245 1246 1247
  @override
  bool get isInstalled => true;
}