project_test.dart 71.3 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
import 'package:file/file.dart';
import 'package:file/memory.dart';
7 8 9
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart' as gradle_utils;
10
import 'package:flutter_tools/src/android/java.dart';
11
import 'package:flutter_tools/src/base/file_system.dart';
12
import 'package:flutter_tools/src/base/logger.dart';
13
import 'package:flutter_tools/src/base/os.dart';
14
import 'package:flutter_tools/src/base/version.dart';
15
import 'package:flutter_tools/src/build_info.dart';
16
import 'package:flutter_tools/src/cache.dart';
17
import 'package:flutter_tools/src/convert.dart';
18
import 'package:flutter_tools/src/features.dart';
19
import 'package:flutter_tools/src/flutter_manifest.dart';
20
import 'package:flutter_tools/src/globals.dart' as globals;
21
import 'package:flutter_tools/src/ios/plist_parser.dart';
22
import 'package:flutter_tools/src/ios/xcodeproj.dart';
23
import 'package:flutter_tools/src/project.dart';
24
import 'package:meta/meta.dart';
25
import 'package:test/fake.dart';
26

27 28
import '../src/common.dart';
import '../src/context.dart';
29
import '../src/fakes.dart';
30 31

void main() {
32
  // TODO(zanderso): remove once FlutterProject is fully refactored.
33 34 35
  // this is safe since no tests have expectations on the test logger.
  final BufferLogger logger = BufferLogger.test();

36
  group('Project', () {
37
    group('construction', () {
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
      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(),
        );
      });

54
      _testInMemory('fails on invalid pubspec.yaml', () async {
55
        final Directory directory = globals.fs.directory('myproject');
56 57 58
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
59 60 61

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
62
          throwsToolExit(),
63 64 65
        );
      });

66
      _testInMemory('fails on pubspec.yaml parse failure', () async {
67
        final Directory directory = globals.fs.directory('myproject');
68 69 70 71 72 73
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(parseErrorPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
74
          throwsToolExit(),
75 76 77
        );
      });

78
      _testInMemory('fails on invalid example/pubspec.yaml', () async {
79
        final Directory directory = globals.fs.directory('myproject');
80 81 82
        directory.childDirectory('example').childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
83 84 85

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
86
          throwsToolExit(),
87 88 89
        );
      });

90
      _testInMemory('treats missing pubspec.yaml as empty', () async {
91
        final Directory directory = globals.fs.directory('myproject')
92
          ..createSync(recursive: true);
93
        expect(FlutterProject.fromDirectory(directory).manifest.isEmpty,
94 95 96 97
          true,
        );
      });

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

109 110 111 112 113 114 115 116 117 118 119
      _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'},
        );
      });

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

137
    group('ensure ready for platform-specific tooling', () {
138
      _testInMemory('does nothing, if project is not created', () async {
139
        final FlutterProject project = FlutterProject(
140
          globals.fs.directory('not_created'),
141 142
          FlutterManifest.empty(logger: logger),
          FlutterManifest.empty(logger: logger),
143
        );
144
        await project.regeneratePlatformSpecificTooling();
145
        expectNotExists(project.directory);
146
      });
147
      _testInMemory('does nothing in plugin or package root project', () async {
148
        final FlutterProject project = await aPluginProject();
149
        await project.regeneratePlatformSpecificTooling();
150
        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
151
        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
152
        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
153
        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
154
      });
155 156 157 158 159 160 161 162 163 164 165 166
      _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'));
      });
167
      _testInMemory('injects plugins for iOS', () async {
168
        final FlutterProject project = await someProject();
169
        await project.regeneratePlatformSpecificTooling();
170
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
171
      });
172
      _testInMemory('generates Xcode configuration for iOS', () async {
173
        final FlutterProject project = await someProject();
174
        await project.regeneratePlatformSpecificTooling();
175
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
176
      });
177
      _testInMemory('injects plugins for Android', () async {
178
        final FlutterProject project = await someProject();
179
        await project.regeneratePlatformSpecificTooling();
180
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
181
      });
182
      _testInMemory('updates local properties for Android', () async {
183
        final FlutterProject project = await someProject();
184
        await project.regeneratePlatformSpecificTooling();
185
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
186
      });
187 188 189
      _testInMemory('checkForDeprecation fails on invalid android app manifest file', () async {
        // This is not a valid Xml document
        const String invalidManifest = '<manifest></application>';
190
        final FlutterProject project = await someProject(androidManifestOverride: invalidManifest, includePubspec: true);
191 192 193 194 195 196

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

203
        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore);
204
        expect(testLogger.statusText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
205 206
      });
      _testInMemory('Android project not on v2 embedding exits', () async {
207
        final FlutterProject project = await someProject(includePubspec: true);
208 209 210 211 212 213 214 215
        // 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.')
        );
216
        expect(testLogger.statusText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
217
        expect(testLogger.statusText, contains('No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in '));
218
      });
219
      _testInMemory('Project not on v2 embedding does not warn if deprecation status is irrelevant', () async {
220
        final FlutterProject project = await someProject(includePubspec: true);
221 222 223 224
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

225 226
        // Default is "DeprecationBehavior.none"
        project.checkForDeprecation();
227 228
        expect(testLogger.statusText, isEmpty);
      });
229
      _testInMemory('Android project not on v2 embedding ignore continues', () async {
230
        final FlutterProject project = await someProject(includePubspec: true);
231 232 233 234 235
        // 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);
236
        expect(testLogger.statusText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
237
      });
238 239 240 241 242 243 244 245 246
      _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')));
      });
247 248 249 250
      _testInMemory('Android plugin project does not throw v1 embedding deprecation warning', () async {
        final FlutterProject project = await aPluginProject();

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

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

345
      testUsingContext('Version.json info is correct', () {
346 347 348 349
        final MemoryFileSystem fileSystem = MemoryFileSystem.test();
        final FlutterManifest manifest = FlutterManifest.createFromString('''
    name: test
    version: 1.0.0+3
350 351
    ''', logger: BufferLogger.test())!;
        final FlutterProject project = FlutterProject(fileSystem.systemTempDirectory, manifest, manifest);
352
        final Map<String, dynamic> versionInfo = jsonDecode(project.getVersionInfo()) as Map<String, dynamic>;
353 354 355
        expect(versionInfo['app_name'],'test');
        expect(versionInfo['version'],'1.0.0');
        expect(versionInfo['build_number'],'3');
356
        expect(versionInfo['package_name'],'test');
357
      });
358 359 360 361 362 363 364 365 366 367 368 369 370
      _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);
      });
371 372
    });

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

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

409
    group('java gradle agp compatibility', () {
410
      Future<FlutterProject?> configureGradleAgpForTest({
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
        required String gradleV,
        required String agpV,
      }) async {
        final FlutterProject project = await someProject();
        addRootGradleFile(project.directory, gradleFileContent: () {
          return '''
dependencies {
    classpath 'com.android.tools.build:gradle:$agpV'
}
''';
        });
        addGradleWrapperFile(project.directory, gradleV);
        return project;
      }

      // Tests in this group that use overrides and _testInMemory should
      // be placed in their own group to avoid test pollution. This is
      // especially important for filesystem.
      group('_', () {
        final FakeProcessManager processManager;
431
        final Java java;
432 433 434
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
435
        java = FakeJava(version: Version(17, 0, 2));
436 437 438
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
439
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
440 441 442 443 444 445
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'flamingo values are compatible',
          () async {
446
            final FlutterProject? project = await configureGradleAgpForTest(
447 448 449 450 451 452 453
              gradleV: '8.0',
              agpV: '7.4.2',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isTrue);
          },
454
          java: java,
455 456 457 458 459 460 461
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        final FakeProcessManager processManager;
462
        final Java java;
463 464 465
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
466
        java = FakeJava(version: const Version.withText(1, 8, 0, '1.8.0_242'));
467 468 469
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
470
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
471 472 473 474 475 476
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'java 8 era values are compatible',
          () async {
477
            final FlutterProject? project = await configureGradleAgpForTest(
478 479 480 481 482 483 484
              gradleV: '6.7.1',
              agpV: '4.2.0',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isTrue);
          },
485
          java: java,
486 487 488 489 490 491 492 493
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });

      group('_', () {
        final FakeProcessManager processManager;
494
        final Java java;
495 496 497 498
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        processManager = FakeProcessManager.empty();
499
        java = FakeJava(version: Version(11, 0, 14));
500 501
        androidStudio = FakeAndroidStudio();
        androidSdk =
502
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
503 504 505 506 507 508
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'electric eel era values are compatible',
          () async {
509
            final FlutterProject? project = await configureGradleAgpForTest(
510 511 512 513 514 515 516
              gradleV: '7.3.3',
              agpV: '7.2.0',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isTrue);
          },
517
          java: java,
518 519 520 521 522 523
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
524 525 526 527
        const String javaV = '17.0.2';
        const String gradleV = '6.7.3';
        const String agpV = '7.2.0';

528
        final FakeProcessManager processManager;
529
        final Java java;
530 531 532 533
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        processManager = FakeProcessManager.empty();
534
        java = FakeJava(version: Version.parse(javaV));
535 536
        androidStudio = FakeAndroidStudio();
        androidSdk =
537
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
538 539 540 541 542 543
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'incompatible everything',
          () async {
544 545

            final FlutterProject? project = await configureGradleAgpForTest(
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
              gradleV: gradleV,
              agpV: agpV,
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url and gradle and agp versions.
            expect(value.description,
                contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
            expect(value.description, contains(RegExp(gradleV)));
            expect(value.description, contains(RegExp(agpV)));
            // On gradle/agp error print help url and java and gradle versions.
            expect(value.description,
                contains(RegExp(AndroidProject.javaGradleCompatUrl)));
            expect(value.description, contains(RegExp(javaV)));
            expect(value.description, contains(RegExp(gradleV)));
          },
568
          java: java,
569 570 571 572 573 574
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
575 576 577 578
        const String javaV = '17.0.2';
        const String gradleV = '6.7.3';
        const String agpV = '4.2.0';

579
        final FakeProcessManager processManager;
580
        final Java java;
581 582 583 584
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        processManager = FakeProcessManager.empty();
585
        java = FakeJava(version: Version(17, 0, 2));
586 587
        androidStudio = FakeAndroidStudio();
        androidSdk =
588
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
589 590 591 592 593 594
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'incompatible java/gradle only',
          () async {
595
            final FlutterProject? project = await configureGradleAgpForTest(
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
              gradleV: gradleV,
              agpV: agpV,
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string.
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url and java and gradle versions.
            expect(value.description,
                contains(RegExp(AndroidProject.javaGradleCompatUrl)));
            expect(value.description, contains(RegExp(javaV)));
            expect(value.description, contains(RegExp(gradleV)));
          },
613
          java: java,
614 615 616 617 618 619 620
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        final FakeProcessManager processManager;
621
        final Java java;
622 623 624
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
625
        java = FakeJava(version: Version(11, 0, 2));
626 627 628
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
629
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
630 631 632 633 634 635 636 637
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'incompatible gradle/agp only',
          () async {
            const String gradleV = '7.0.3';
            const String agpV = '7.1.0';
638
            final FlutterProject? project = await configureGradleAgpForTest(
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
              gradleV: gradleV,
              agpV: agpV,
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string.
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url and gradle and agp versions.
            expect(value.description,
                contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
            expect(value.description, contains(RegExp(gradleV)));
            expect(value.description, contains(RegExp(agpV)));
          },
656
          java: java,
657 658 659
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
        );
      });
      group('_', () {
        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        java = FakeJava(version: Version(11, 0, 2));
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'null agp only',
          () async {
            const String gradleV = '7.0.3';
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: gradleV,
              agpV: '',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string.
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url null value for agp.
            expect(value.description,
                contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
            expect(value.description, contains(RegExp(gradleV)));
            expect(value.description, contains(RegExp('null')));
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
702 703 704 705
        );
      });
    });

706
    group('language', () {
707 708 709
      late XcodeProjectInterpreter xcodeProjectInterpreter;
      late MemoryFileSystem fs;
      late FlutterProjectFactory flutterProjectFactory;
710
      setUp(() {
711
        fs = MemoryFileSystem.test();
712
        xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: FakeProcessManager.any());
713 714 715 716
        flutterProjectFactory = FlutterProjectFactory(
          logger: logger,
          fileSystem: fs,
        );
717 718
      });

719
      _testInMemory('default host app language', () async {
720 721 722 723
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

724
      testUsingContext('kotlin host app language', () async {
725 726 727 728
        final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
729
            return '''
730 731 732 733 734 735
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
736
        FileSystem: () => fs,
737
        ProcessManager: () => FakeProcessManager.any(),
738
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
739
        FlutterProjectFactory: () => flutterProjectFactory,
740 741 742
      });
    });

743
    group('With mocked context', () {
744 745 746 747
      late MemoryFileSystem fs;
      late FakePlistParser testPlistUtils;
      late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
      late FlutterProjectFactory flutterProjectFactory;
748
      setUp(() {
749
        fs = MemoryFileSystem.test();
750
        testPlistUtils = FakePlistParser();
751
        xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
752 753 754 755
        flutterProjectFactory = FlutterProjectFactory(
          fileSystem: fs,
          logger: logger,
        );
756 757
      });

758
      void testWithMocks(String description, Future<void> Function() testMethod) {
759 760
        testUsingContext(description, testMethod, overrides: <Type, Generator>{
          FileSystem: () => fs,
761
          ProcessManager: () => FakeProcessManager.any(),
762
          PlistParser: () => testPlistUtils,
763
          XcodeProjectInterpreter: () => xcodeProjectInterpreter,
764
          FlutterProjectFactory: () => flutterProjectFactory,
765 766 767
        });
      }

768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
      group('universal link', () {
        testWithMocks('build with flavor', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          const String entitlementFilePath = 'myEntitlement.Entitlement';
          project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);

          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
            target: 'Runner',
            configuration: 'config',
          );
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            IosProject.kTeamIdKey: 'ABC',
            IosProject.kEntitlementFilePathKey: entitlementFilePath,
            'SUFFIX': 'suffix',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
          testPlistUtils.setProperty(
            PlistParser.kAssociatedDomainsKey,
            <String>[
              'applinks:example.com',
              'applinks:example2.com',
            ],
          );
795
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
796 797 798
            target: 'Runner',
            configuration: 'config',
          );
799 800 801
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;

802
          expect(
803
            json['associatedDomains'],
804 805 806 807 808 809 810
            unorderedEquals(
              <String>[
                'example.com',
                'example2.com',
              ],
            ),
          );
811 812
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
813
        });
814

815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840
        testWithMocks('can handle entitlement file in nested directory structure.', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          const String entitlementFilePath = 'nested/somewhere/myEntitlement.Entitlement';
          project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);

          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
            target: 'Runner',
            configuration: 'config',
          );
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            IosProject.kTeamIdKey: 'ABC',
            IosProject.kEntitlementFilePathKey: entitlementFilePath,
            'SUFFIX': 'suffix',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
          testPlistUtils.setProperty(
            PlistParser.kAssociatedDomainsKey,
            <String>[
              'applinks:example.com',
              'applinks:example2.com',
            ],
          );
841

842
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
843 844 845
            target: 'Runner',
            configuration: 'config',
          );
846 847
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
848
          expect(
849
            json['associatedDomains'],
850 851 852 853 854 855 856
            unorderedEquals(
              <String>[
                'example.com',
                'example2.com',
              ],
            ),
          );
857 858
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
859
        });
860

861 862 863 864 865 866 867 868 869 870 871 872 873 874 875
        testWithMocks('return empty when no entitlement', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);

          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
            target: 'Runner',
            configuration: 'config',
          );
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            IosProject.kTeamIdKey: 'ABC',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER)');
876
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
877 878 879
            target: 'Runner',
            configuration: 'config',
          );
880 881 882 883 884
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject');
          expect(json['associatedDomains'], unorderedEquals(<String>[]));
885
        });
886 887
      });

888 889 890 891
      group('product bundle identifier', () {
        testWithMocks('null, if no build settings or plist entries', () async {
          final FlutterProject project = await someProject();
          expect(await project.ios.productBundleIdentifier(null), isNull);
892
        });
893

894 895 896 897 898 899 900 901 902 903 904 905
        testWithMocks('from build settings, if no plist', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] =
          <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);

          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });
906

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

911 912 913 914 915
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject');
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });
916

917 918 919 920 921 922
        testWithMocks('from plist, if no variables', () async {
          final FlutterProject project = await someProject();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });
923

924 925 926 927 928 929 930 931 932 933 934 935
        testWithMocks('from build settings and plist, if default variable', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });
936

937 938 939 940 941 942 943 944 945 946 947 948 949
        testWithMocks('from build settings and plist, by substitution', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            'SUFFIX': 'suffix',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');

          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
950 951
        });

952 953 954 955 956 957 958
        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);
959

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

963 964 965 966
        testWithMocks('fails with no flavor and defined schemes', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
967

968 969 970 971 972
          await expectToolExitLater(
            project.ios.productBundleIdentifier(null),
            contains('You must specify a --flavor option to select one of the available schemes.'),
          );
        });
973

974 975 976 977 978 979 980 981 982 983 984 985
        testWithMocks('handles case insensitive flavor', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
          const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);

          expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
        });
986

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

993 994 995 996 997
          await expectToolExitLater(
            project.ios.productBundleIdentifier(buildInfo),
            contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.'),
          );
        });
998

999 1000 1001 1002 1003 1004 1005
        testWithMocks('empty surrounded by quotes', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('', qualifier: '"');
          });
          expect(await project.ios.productBundleIdentifier(null), '');
1006
        });
1007

1008 1009 1010 1011 1012 1013 1014
        testWithMocks('surrounded by double quotes', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
1015
        });
1016

1017 1018 1019 1020 1021 1022 1023
        testWithMocks('surrounded by single quotes', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
1024
        });
1025
      });
1026 1027
    });

1028
    group('application bundle name', () {
1029 1030
      late MemoryFileSystem fs;
      late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
1031
      setUp(() {
1032
        fs = MemoryFileSystem.test();
1033
        mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
1034 1035 1036 1037
      });

      testUsingContext('app product name defaults to Runner.app', () async {
        final FlutterProject project = await someProject();
1038
        expect(await project.ios.hostAppBundleName(null), 'Runner.app');
1039 1040 1041
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1042
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
1043 1044 1045 1046
      });

      testUsingContext('app product name xcodebuild settings', () async {
        final FlutterProject project = await someProject();
1047
        project.ios.xcodeProject.createSync();
1048 1049
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1050
          'FULL_PRODUCT_NAME': 'My App.app',
1051 1052
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
1053

1054
        expect(await project.ios.hostAppBundleName(null), 'My App.app');
1055 1056 1057
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1058
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
1059 1060 1061
      });
    });

1062
    group('organization names set', () {
1063
      _testInMemory('is empty, if project not created', () async {
1064
        final FlutterProject project = await someProject();
1065
        expect(await project.organizationNames, isEmpty);
1066
      });
1067
      _testInMemory('is empty, if no platform folders exist', () async {
1068
        final FlutterProject project = await someProject();
1069
        project.directory.createSync();
1070
        expect(await project.organizationNames, isEmpty);
1071
      });
1072
      _testInMemory('is populated from iOS bundle identifier', () async {
1073
        final FlutterProject project = await someProject();
1074
        addIosProjectFile(project.directory, projectFileContent: () {
1075
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
1076
        });
1077
        expect(await project.organizationNames, <String>['io.flutter']);
1078
      });
1079
      _testInMemory('is populated from Android application ID', () async {
1080
        final FlutterProject project = await someProject();
1081 1082 1083 1084
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
1085
        expect(await project.organizationNames, <String>['io.flutter']);
1086
      });
1087
      _testInMemory('is populated from iOS bundle identifier in plugin example', () async {
1088
        final FlutterProject project = await someProject();
1089
        addIosProjectFile(project.example.directory, projectFileContent: () {
1090
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
1091
        });
1092
        expect(await project.organizationNames, <String>['io.flutter']);
1093
      });
1094
      _testInMemory('is populated from Android application ID in plugin example', () async {
1095
        final FlutterProject project = await someProject();
1096 1097 1098 1099
        addAndroidGradleFile(project.example.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
1100
        expect(await project.organizationNames, <String>['io.flutter']);
1101
      });
1102
      _testInMemory('is populated from Android group in plugin', () async {
1103
        final FlutterProject project = await someProject();
1104
        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
1105
        expect(await project.organizationNames, <String>['io.flutter']);
1106
      });
1107
      _testInMemory('is singleton, if sources agree', () async {
1108
        final FlutterProject project = await someProject();
1109 1110 1111 1112 1113 1114 1115
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
1116
        expect(await project.organizationNames, <String>['io.flutter']);
1117
      });
1118
      _testInMemory('is non-singleton, if sources disagree', () async {
1119
        final FlutterProject project = await someProject();
1120 1121 1122 1123 1124 1125 1126
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.clutter.someproject');
          });
1127
        expect(
1128
          await project.organizationNames,
1129 1130 1131 1132 1133
          <String>['io.flutter', 'io.clutter'],
        );
      });
    });
  });
1134
  group('watch companion', () {
1135 1136 1137 1138
    late MemoryFileSystem fs;
    late FakePlistParser testPlistParser;
    late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
    late FlutterProjectFactory flutterProjectFactory;
1139 1140
    setUp(() {
      fs = MemoryFileSystem.test();
1141
      testPlistParser = FakePlistParser();
1142
      mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
1143 1144 1145 1146 1147 1148 1149 1150
      flutterProjectFactory = FlutterProjectFactory(
        fileSystem: fs,
        logger: logger,
      );
    });

    testUsingContext('cannot find bundle identifier', () async {
      final FlutterProject project = await someProject();
1151 1152 1153 1154 1155 1156 1157 1158 1159
      final XcodeProjectInfo projectInfo = XcodeProjectInfo(<String>['WatchTarget'], <String>[], <String>[], logger);
      expect(
        await project.ios.containsWatchCompanion(
          projectInfo: projectInfo,
          buildInfo: BuildInfo.debug,
          deviceId: '123',
        ),
        isFalse,
      );
1160 1161 1162
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
1163
      PlistParser: () => testPlistParser,
1164 1165 1166 1167 1168 1169
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      FlutterProjectFactory: () => flutterProjectFactory,
    });

    group('with bundle identifier', () {
      setUp(() {
1170 1171
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1172
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1173
        };
1174
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
1175 1176 1177 1178
      });

      testUsingContext('no Info.plist in target', () async {
        final FlutterProject project = await someProject();
1179 1180 1181 1182 1183 1184 1185 1186
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
1187 1188 1189
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1190
        PlistParser: () => testPlistParser,
1191 1192 1193 1194 1195 1196 1197 1198
        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);

1199 1200 1201 1202 1203 1204 1205 1206
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
1207 1208 1209
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1210
        PlistParser: () => testPlistParser,
1211 1212 1213 1214 1215 1216 1217 1218
        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);

1219
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someOTHERproject');
1220 1221 1222 1223 1224 1225 1226 1227
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
1228 1229 1230
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1231
        PlistParser: () => testPlistParser,
1232 1233 1234 1235
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

1236
      testUsingContext('has watch companion in plist', () async {
1237
        final FlutterProject project = await someProject();
1238
        project.ios.xcodeProject.createSync();
1239
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
1240
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someProject');
1241

1242 1243 1244 1245 1246 1247 1248 1249
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
1250 1251 1252
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1253
        PlistParser: () => testPlistParser,
1254 1255 1256
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
1257

1258
      testUsingContext('has watch companion in plist with xcode variable', () async {
1259 1260
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
1261 1262 1263 1264 1265
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123',
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1266
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1267 1268 1269 1270
        };
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299
        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>{
1300
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339
        };

        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>{
1340
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1341 1342 1343 1344 1345 1346 1347 1348
        };

        const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
          scheme: 'WatchScheme',
          deviceId: '123',
          isWatch: true,
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
1349
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360
          'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
        };

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
1361 1362 1363 1364 1365 1366 1367
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
1368 1369
    });
  });
1370 1371
}

1372
Future<FlutterProject> someProject({
1373
  String? androidManifestOverride,
1374
  bool includePubspec = false,
1375
}) async {
1376
  final Directory directory = globals.fs.directory('some_project');
1377 1378 1379 1380
  directory.childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1381 1382 1383 1384 1385
  if (includePubspec) {
    directory.childFile('pubspec.yaml')
    ..createSync(recursive: true)
    ..writeAsStringSync(validPubspec);
  }
1386
  directory.childDirectory('ios').createSync(recursive: true);
1387 1388 1389 1390 1391
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
1392
    .writeAsStringSync(androidManifestOverride ?? '<manifest></manifest>');
1393
  return FlutterProject.fromDirectory(directory);
1394 1395
}

1396
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
1397
  final Directory directory = globals.fs.directory('plugin_project');
1398
  directory.childDirectory('ios').createSync(recursive: true);
1399 1400
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
1401 1402 1403
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
1404 1405 1406 1407 1408 1409
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
1422 1423
      linux:
        pluginClass: MyPlugin
1424 1425
      macos:
        pluginClass: MyPlugin
1426 1427
      windows:
        pluginClass: MyPlugin
1428 1429 1430
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
1431
  return FlutterProject.fromDirectory(directory);
1432 1433
}

1434
Future<FlutterProject> aModuleProject() async {
1435
  final Directory directory = globals.fs.directory('module_project');
1436 1437 1438 1439 1440
  directory
    .childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1441
  directory.childFile('pubspec.yaml').writeAsStringSync('''
1442
name: my_module
1443
flutter:
1444
  module:
1445 1446 1447
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
1448
}
1449

1450 1451
/// Executes the [testMethod] in a context where the file system
/// is in memory.
1452
@isTest
1453 1454 1455 1456
void _testInMemory(
  String description,
  Future<void> Function() testMethod, {
  FileSystem? fileSystem,
1457
  Java? java,
1458 1459 1460 1461
  AndroidStudio? androidStudio,
  ProcessManager? processManager,
  AndroidSdk? androidSdk,
}) {
1462
  Cache.flutterRoot = getFlutterRoot();
1463 1464
  final FileSystem testFileSystem = fileSystem ?? getFileSystemForPlatform();
  testFileSystem.directory('.dart_tool').childFile('package_config.json')
1465 1466
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1467 1468
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
1469
  final Logger logger = BufferLogger.test();
1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490
  transfer(
      Cache(
        fileSystem: globals.fs,
        logger: logger,
        artifacts: <ArtifactSet>[],
        osUtils: OperatingSystemUtils(
          fileSystem: globals.fs,
          logger: logger,
          platform: globals.platform,
          processManager: globals.processManager,
        ),
        platform: globals.platform,
      ).getArtifactDirectory('gradle_wrapper'),
      testFileSystem);
  transfer(
      globals.fs
          .directory(Cache.flutterRoot)
          .childDirectory('packages')
          .childDirectory('flutter_tools')
          .childDirectory('templates'),
      testFileSystem);
1491
  // Set up enough of the packages to satisfy the templating code.
1492 1493
  final File packagesFile = testFileSystem
      .directory(Cache.flutterRoot)
1494 1495
      .childDirectory('packages')
      .childDirectory('flutter_tools')
1496 1497
      .childDirectory('.dart_tool')
      .childFile('package_config.json');
1498 1499
  final Directory dummyTemplateImagesDirectory =
      testFileSystem.directory(Cache.flutterRoot).parent;
1500 1501
  dummyTemplateImagesDirectory.createSync(recursive: true);
  packagesFile.createSync(recursive: true);
1502 1503 1504 1505 1506 1507 1508
  packagesFile.writeAsStringSync(json.encode(<String, Object>{
    'configVersion': 2,
    'packages': <Object>[
      <String, Object>{
        'name': 'flutter_template_images',
        'rootUri': dummyTemplateImagesDirectory.uri.toString(),
        'packageUri': 'lib/',
1509
        'languageVersion': '2.6',
1510 1511 1512
      },
    ],
  }));
1513

1514 1515 1516 1517
  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
1518
      FileSystem: () => testFileSystem,
1519
      ProcessManager: () => processManager ?? FakeProcessManager.any(),
1520
      Java : () => java,
1521 1522 1523
      AndroidStudio: () => androidStudio ?? FakeAndroidStudio(),
      // Intentionlly null if not set. Some ios tests fail if this is a fake.
      AndroidSdk: () => androidSdk,
1524
      Cache: () => Cache(
1525 1526 1527 1528 1529 1530
            logger: globals.logger,
            fileSystem: testFileSystem,
            osUtils: globals.os,
            platform: globals.platform,
            artifacts: <ArtifactSet>[],
          ),
1531
      FlutterProjectFactory: () => FlutterProjectFactory(
1532 1533 1534
            fileSystem: testFileSystem,
            logger: globals.logger,
          ),
1535 1536 1537 1538
    },
  );
}

1539 1540 1541 1542 1543
/// 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);
1544
    for (final FileSystemEntity child in entity.listSync()) {
1545 1546 1547 1548 1549
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
1550
    throw Exception('Unsupported FileSystemEntity ${entity.runtimeType}');
1551 1552 1553
  }
}

1554 1555 1556 1557 1558 1559 1560 1561
void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

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

1562
void addIosProjectFile(Directory directory, {required String Function() projectFileContent}) {
1563 1564 1565 1566 1567
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
1568
    ..writeAsStringSync(projectFileContent());
1569 1570
}

1571
void addAndroidGradleFile(Directory directory, { required String Function() gradleFileContent }) {
1572 1573 1574 1575
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile('build.gradle')
1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileContent());
}

void addRootGradleFile(Directory directory,
    {required String Function() gradleFileContent}) {
  directory.childDirectory('android').childFile('build.gradle')
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileContent());
}

void addGradleWrapperFile(Directory directory, String gradleVersion) {
  directory
      .childDirectory('android')
      .childDirectory(gradle_utils.gradleDirectoryName)
      .childDirectory(gradle_utils.gradleWrapperDirectoryName)
      .childFile(gradle_utils.gradleWrapperPropertiesFilename)
    ..createSync(recursive: true)
    // ignore: unnecessary_string_escapes
    ..writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
''');
}

FileSystem getFileSystemForPlatform() {
  return MemoryFileSystem(
    style: globals.platform.isWindows
        ? FileSystemStyle.windows
        : FileSystemStyle.posix,
  );
1610 1611 1612 1613 1614 1615 1616 1617
}

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

1618 1619 1620 1621 1622
String get validPubspec => '''
name: hello
flutter:
''';

1623 1624 1625 1626 1627 1628 1629 1630 1631 1632
String get validPubspecWithDependencies => '''
name: hello
flutter:

dependencies:
  plugin_a:
  plugin_b:
''';


1633 1634 1635 1636 1637 1638
String get invalidPubspec => '''
name: hello
flutter:
  invalid:
''';

1639 1640 1641 1642 1643 1644 1645 1646
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

1647
String projectFileWithBundleId(String id, {String? qualifier}) {
1648 1649 1650 1651 1652
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
1653
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
1665
    compileSdk 34
1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681

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

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

apply plugin: 'com.android.library'

android {
1682
    compileSdk 34
1683 1684 1685
}
''';
}
1686 1687 1688 1689 1690 1691 1692 1693 1694 1695

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

1697
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
1698
  final Map<XcodeProjectBuildContext, Map<String, String>> buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
1699
  late XcodeProjectInfo xcodeProjectInfo;
1700 1701 1702

  @override
  Future<Map<String, String>> getBuildSettings(String projectPath, {
1703
    XcodeProjectBuildContext? buildContext,
1704 1705
    Duration timeout = const Duration(minutes: 1),
  }) async {
1706 1707 1708 1709
    if (buildSettingsByBuildContext[buildContext] == null) {
      return <String, String>{};
    }
    return buildSettingsByBuildContext[buildContext]!;
1710 1711 1712
  }

  @override
1713
  Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
1714 1715 1716
    return xcodeProjectInfo;
  }

1717 1718 1719
  @override
  bool get isInstalled => true;
}
1720 1721

class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
1722
  FakeAndroidSdkWithDir(this._directory);
1723 1724 1725 1726 1727 1728

  final Directory _directory;

  @override
  Directory get directory => _directory;
}