project_test.dart 69.6 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 660 661 662 663
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
    });

664
    group('language', () {
665 666 667
      late XcodeProjectInterpreter xcodeProjectInterpreter;
      late MemoryFileSystem fs;
      late FlutterProjectFactory flutterProjectFactory;
668
      setUp(() {
669
        fs = MemoryFileSystem.test();
670
        xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: FakeProcessManager.any());
671 672 673 674
        flutterProjectFactory = FlutterProjectFactory(
          logger: logger,
          fileSystem: fs,
        );
675 676
      });

677
      _testInMemory('default host app language', () async {
678 679 680 681
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

682
      testUsingContext('kotlin host app language', () async {
683 684 685 686
        final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
687
            return '''
688 689 690 691 692 693
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
694
        FileSystem: () => fs,
695
        ProcessManager: () => FakeProcessManager.any(),
696
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
697
        FlutterProjectFactory: () => flutterProjectFactory,
698 699 700
      });
    });

701
    group('With mocked context', () {
702 703 704 705
      late MemoryFileSystem fs;
      late FakePlistParser testPlistUtils;
      late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
      late FlutterProjectFactory flutterProjectFactory;
706
      setUp(() {
707
        fs = MemoryFileSystem.test();
708
        testPlistUtils = FakePlistParser();
709
        xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
710 711 712 713
        flutterProjectFactory = FlutterProjectFactory(
          fileSystem: fs,
          logger: logger,
        );
714 715
      });

716
      void testWithMocks(String description, Future<void> Function() testMethod) {
717 718
        testUsingContext(description, testMethod, overrides: <Type, Generator>{
          FileSystem: () => fs,
719
          ProcessManager: () => FakeProcessManager.any(),
720
          PlistParser: () => testPlistUtils,
721
          XcodeProjectInterpreter: () => xcodeProjectInterpreter,
722
          FlutterProjectFactory: () => flutterProjectFactory,
723 724 725
        });
      }

726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752
      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',
            ],
          );
753
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
754 755 756
            target: 'Runner',
            configuration: 'config',
          );
757 758 759
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;

760
          expect(
761
            json['associatedDomains'],
762 763 764 765 766 767 768
            unorderedEquals(
              <String>[
                'example.com',
                'example2.com',
              ],
            ),
          );
769 770
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
771
        });
772

773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798
        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',
            ],
          );
799

800
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
801 802 803
            target: 'Runner',
            configuration: 'config',
          );
804 805
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
806
          expect(
807
            json['associatedDomains'],
808 809 810 811 812 813 814
            unorderedEquals(
              <String>[
                'example.com',
                'example2.com',
              ],
            ),
          );
815 816
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
817
        });
818

819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
        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)');
834
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
835 836 837
            target: 'Runner',
            configuration: 'config',
          );
838 839 840 841 842
          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>[]));
843
        });
844 845
      });

846 847 848 849
      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);
850
        });
851

852 853 854 855 856 857 858 859 860 861 862 863
        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');
        });
864

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

869 870 871 872 873
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject');
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });
874

875 876 877 878 879 880
        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');
        });
881

882 883 884 885 886 887 888 889 890 891 892 893
        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');
        });
894

895 896 897 898 899 900 901 902 903 904 905 906 907
        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');
908 909
        });

910 911 912 913 914 915 916
        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);
917

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

921 922 923 924
        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);
925

926 927 928 929 930
          await expectToolExitLater(
            project.ios.productBundleIdentifier(null),
            contains('You must specify a --flavor option to select one of the available schemes.'),
          );
        });
931

932 933 934 935 936 937 938 939 940 941 942 943
        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');
        });
944

945 946 947 948 949
        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);
950

951 952 953 954 955
          await expectToolExitLater(
            project.ios.productBundleIdentifier(buildInfo),
            contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.'),
          );
        });
956

957 958 959 960 961 962 963
        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), '');
964
        });
965

966 967 968 969 970 971 972
        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');
973
        });
974

975 976 977 978 979 980 981
        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');
982
        });
983
      });
984 985
    });

986
    group('application bundle name', () {
987 988
      late MemoryFileSystem fs;
      late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
989
      setUp(() {
990
        fs = MemoryFileSystem.test();
991
        mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
992 993 994 995
      });

      testUsingContext('app product name defaults to Runner.app', () async {
        final FlutterProject project = await someProject();
996
        expect(await project.ios.hostAppBundleName(null), 'Runner.app');
997 998 999
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1000
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
1001 1002 1003 1004
      });

      testUsingContext('app product name xcodebuild settings', () async {
        final FlutterProject project = await someProject();
1005
        project.ios.xcodeProject.createSync();
1006 1007
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1008
          'FULL_PRODUCT_NAME': 'My App.app',
1009 1010
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
1011

1012
        expect(await project.ios.hostAppBundleName(null), 'My App.app');
1013 1014 1015
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1016
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
1017 1018 1019
      });
    });

1020
    group('organization names set', () {
1021
      _testInMemory('is empty, if project not created', () async {
1022
        final FlutterProject project = await someProject();
1023
        expect(await project.organizationNames, isEmpty);
1024
      });
1025
      _testInMemory('is empty, if no platform folders exist', () async {
1026
        final FlutterProject project = await someProject();
1027
        project.directory.createSync();
1028
        expect(await project.organizationNames, isEmpty);
1029
      });
1030
      _testInMemory('is populated from iOS bundle identifier', () async {
1031
        final FlutterProject project = await someProject();
1032
        addIosProjectFile(project.directory, projectFileContent: () {
1033
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
1034
        });
1035
        expect(await project.organizationNames, <String>['io.flutter']);
1036
      });
1037
      _testInMemory('is populated from Android application ID', () async {
1038
        final FlutterProject project = await someProject();
1039 1040 1041 1042
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
1043
        expect(await project.organizationNames, <String>['io.flutter']);
1044
      });
1045
      _testInMemory('is populated from iOS bundle identifier in plugin example', () async {
1046
        final FlutterProject project = await someProject();
1047
        addIosProjectFile(project.example.directory, projectFileContent: () {
1048
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
1049
        });
1050
        expect(await project.organizationNames, <String>['io.flutter']);
1051
      });
1052
      _testInMemory('is populated from Android application ID in plugin example', () async {
1053
        final FlutterProject project = await someProject();
1054 1055 1056 1057
        addAndroidGradleFile(project.example.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
1058
        expect(await project.organizationNames, <String>['io.flutter']);
1059
      });
1060
      _testInMemory('is populated from Android group in plugin', () async {
1061
        final FlutterProject project = await someProject();
1062
        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
1063
        expect(await project.organizationNames, <String>['io.flutter']);
1064
      });
1065
      _testInMemory('is singleton, if sources agree', () async {
1066
        final FlutterProject project = await someProject();
1067 1068 1069 1070 1071 1072 1073
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
1074
        expect(await project.organizationNames, <String>['io.flutter']);
1075
      });
1076
      _testInMemory('is non-singleton, if sources disagree', () async {
1077
        final FlutterProject project = await someProject();
1078 1079 1080 1081 1082 1083 1084
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.clutter.someproject');
          });
1085
        expect(
1086
          await project.organizationNames,
1087 1088 1089 1090 1091
          <String>['io.flutter', 'io.clutter'],
        );
      });
    });
  });
1092
  group('watch companion', () {
1093 1094 1095 1096
    late MemoryFileSystem fs;
    late FakePlistParser testPlistParser;
    late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
    late FlutterProjectFactory flutterProjectFactory;
1097 1098
    setUp(() {
      fs = MemoryFileSystem.test();
1099
      testPlistParser = FakePlistParser();
1100
      mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
1101 1102 1103 1104 1105 1106 1107 1108
      flutterProjectFactory = FlutterProjectFactory(
        fileSystem: fs,
        logger: logger,
      );
    });

    testUsingContext('cannot find bundle identifier', () async {
      final FlutterProject project = await someProject();
1109 1110 1111 1112 1113 1114 1115 1116 1117
      final XcodeProjectInfo projectInfo = XcodeProjectInfo(<String>['WatchTarget'], <String>[], <String>[], logger);
      expect(
        await project.ios.containsWatchCompanion(
          projectInfo: projectInfo,
          buildInfo: BuildInfo.debug,
          deviceId: '123',
        ),
        isFalse,
      );
1118 1119 1120
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
1121
      PlistParser: () => testPlistParser,
1122 1123 1124 1125 1126 1127
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      FlutterProjectFactory: () => flutterProjectFactory,
    });

    group('with bundle identifier', () {
      setUp(() {
1128 1129
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1130
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1131
        };
1132
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
1133 1134 1135 1136
      });

      testUsingContext('no Info.plist in target', () async {
        final FlutterProject project = await someProject();
1137 1138 1139 1140 1141 1142 1143 1144
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
1145 1146 1147
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1148
        PlistParser: () => testPlistParser,
1149 1150 1151 1152 1153 1154 1155 1156
        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);

1157 1158 1159 1160 1161 1162 1163 1164
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
1165 1166 1167
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1168
        PlistParser: () => testPlistParser,
1169 1170 1171 1172 1173 1174 1175 1176
        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);

1177
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someOTHERproject');
1178 1179 1180 1181 1182 1183 1184 1185
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
1186 1187 1188
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1189
        PlistParser: () => testPlistParser,
1190 1191 1192 1193
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

1194
      testUsingContext('has watch companion in plist', () async {
1195
        final FlutterProject project = await someProject();
1196
        project.ios.xcodeProject.createSync();
1197
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
1198
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someProject');
1199

1200 1201 1202 1203 1204 1205 1206 1207
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
1208 1209 1210
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
1211
        PlistParser: () => testPlistParser,
1212 1213 1214
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
1215

1216
      testUsingContext('has watch companion in plist with xcode variable', () async {
1217 1218
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
1219 1220 1221 1222 1223
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123',
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1224
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1225 1226 1227 1228
        };
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257
        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>{
1258
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 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
        };

        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>{
1298
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1299 1300 1301 1302 1303 1304 1305 1306
        };

        const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
          scheme: 'WatchScheme',
          deviceId: '123',
          isWatch: true,
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
1307
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318
          'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
        };

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
1319 1320 1321 1322 1323 1324 1325
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
1326 1327
    });
  });
1328 1329
}

1330
Future<FlutterProject> someProject({
1331
  String? androidManifestOverride,
1332
  bool includePubspec = false,
1333
}) async {
1334
  final Directory directory = globals.fs.directory('some_project');
1335 1336 1337 1338
  directory.childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1339 1340 1341 1342 1343
  if (includePubspec) {
    directory.childFile('pubspec.yaml')
    ..createSync(recursive: true)
    ..writeAsStringSync(validPubspec);
  }
1344
  directory.childDirectory('ios').createSync(recursive: true);
1345 1346 1347 1348 1349
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
1350
    .writeAsStringSync(androidManifestOverride ?? '<manifest></manifest>');
1351
  return FlutterProject.fromDirectory(directory);
1352 1353
}

1354
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
1355
  final Directory directory = globals.fs.directory('plugin_project');
1356
  directory.childDirectory('ios').createSync(recursive: true);
1357 1358
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
1359 1360 1361
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
1362 1363 1364 1365 1366 1367
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
1380 1381
      linux:
        pluginClass: MyPlugin
1382 1383
      macos:
        pluginClass: MyPlugin
1384 1385
      windows:
        pluginClass: MyPlugin
1386 1387 1388
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
1389
  return FlutterProject.fromDirectory(directory);
1390 1391
}

1392
Future<FlutterProject> aModuleProject() async {
1393
  final Directory directory = globals.fs.directory('module_project');
1394 1395 1396 1397 1398
  directory
    .childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1399
  directory.childFile('pubspec.yaml').writeAsStringSync('''
1400
name: my_module
1401
flutter:
1402
  module:
1403 1404 1405
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
1406
}
1407

1408 1409
/// Executes the [testMethod] in a context where the file system
/// is in memory.
1410
@isTest
1411 1412 1413 1414
void _testInMemory(
  String description,
  Future<void> Function() testMethod, {
  FileSystem? fileSystem,
1415
  Java? java,
1416 1417 1418 1419
  AndroidStudio? androidStudio,
  ProcessManager? processManager,
  AndroidSdk? androidSdk,
}) {
1420
  Cache.flutterRoot = getFlutterRoot();
1421 1422
  final FileSystem testFileSystem = fileSystem ?? getFileSystemForPlatform();
  testFileSystem.directory('.dart_tool').childFile('package_config.json')
1423 1424
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
1425 1426
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
1427
  final Logger logger = BufferLogger.test();
1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448
  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);
1449
  // Set up enough of the packages to satisfy the templating code.
1450 1451
  final File packagesFile = testFileSystem
      .directory(Cache.flutterRoot)
1452 1453
      .childDirectory('packages')
      .childDirectory('flutter_tools')
1454 1455
      .childDirectory('.dart_tool')
      .childFile('package_config.json');
1456 1457
  final Directory dummyTemplateImagesDirectory =
      testFileSystem.directory(Cache.flutterRoot).parent;
1458 1459
  dummyTemplateImagesDirectory.createSync(recursive: true);
  packagesFile.createSync(recursive: true);
1460 1461 1462 1463 1464 1465 1466
  packagesFile.writeAsStringSync(json.encode(<String, Object>{
    'configVersion': 2,
    'packages': <Object>[
      <String, Object>{
        'name': 'flutter_template_images',
        'rootUri': dummyTemplateImagesDirectory.uri.toString(),
        'packageUri': 'lib/',
1467
        'languageVersion': '2.6',
1468 1469 1470
      },
    ],
  }));
1471

1472 1473 1474 1475
  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
1476
      FileSystem: () => testFileSystem,
1477
      ProcessManager: () => processManager ?? FakeProcessManager.any(),
1478
      Java : () => java,
1479 1480 1481
      AndroidStudio: () => androidStudio ?? FakeAndroidStudio(),
      // Intentionlly null if not set. Some ios tests fail if this is a fake.
      AndroidSdk: () => androidSdk,
1482
      Cache: () => Cache(
1483 1484 1485 1486 1487 1488
            logger: globals.logger,
            fileSystem: testFileSystem,
            osUtils: globals.os,
            platform: globals.platform,
            artifacts: <ArtifactSet>[],
          ),
1489
      FlutterProjectFactory: () => FlutterProjectFactory(
1490 1491 1492
            fileSystem: testFileSystem,
            logger: globals.logger,
          ),
1493 1494 1495 1496
    },
  );
}

1497 1498 1499 1500 1501
/// 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);
1502
    for (final FileSystemEntity child in entity.listSync()) {
1503 1504 1505 1506 1507
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
1508
    throw Exception('Unsupported FileSystemEntity ${entity.runtimeType}');
1509 1510 1511
  }
}

1512 1513 1514 1515 1516 1517 1518 1519
void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

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

1520
void addIosProjectFile(Directory directory, {required String Function() projectFileContent}) {
1521 1522 1523 1524 1525
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
1526
    ..writeAsStringSync(projectFileContent());
1527 1528
}

1529
void addAndroidGradleFile(Directory directory, { required String Function() gradleFileContent }) {
1530 1531 1532 1533
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile('build.gradle')
1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567
    ..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,
  );
1568 1569 1570 1571 1572 1573 1574 1575
}

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

1576 1577 1578 1579 1580
String get validPubspec => '''
name: hello
flutter:
''';

1581 1582 1583 1584 1585 1586 1587 1588 1589 1590
String get validPubspecWithDependencies => '''
name: hello
flutter:

dependencies:
  plugin_a:
  plugin_b:
''';


1591 1592 1593 1594 1595 1596
String get invalidPubspec => '''
name: hello
flutter:
  invalid:
''';

1597 1598 1599 1600 1601 1602 1603 1604
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

1605
String projectFileWithBundleId(String id, {String? qualifier}) {
1606 1607 1608 1609 1610
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
1611
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
1623
    compileSdkVersion 33
1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639

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

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

apply plugin: 'com.android.library'

android {
1640
    compileSdkVersion 33
1641 1642 1643
}
''';
}
1644 1645 1646 1647 1648 1649 1650 1651 1652 1653

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

1655
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
1656
  final Map<XcodeProjectBuildContext, Map<String, String>> buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
1657
  late XcodeProjectInfo xcodeProjectInfo;
1658 1659 1660

  @override
  Future<Map<String, String>> getBuildSettings(String projectPath, {
1661
    XcodeProjectBuildContext? buildContext,
1662 1663
    Duration timeout = const Duration(minutes: 1),
  }) async {
1664 1665 1666 1667
    if (buildSettingsByBuildContext[buildContext] == null) {
      return <String, String>{};
    }
    return buildSettingsByBuildContext[buildContext]!;
1668 1669 1670
  }

  @override
1671
  Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
1672 1673 1674
    return xcodeProjectInfo;
  }

1675 1676 1677
  @override
  bool get isInstalled => true;
}
1678 1679

class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
1680
  FakeAndroidSdkWithDir(this._directory);
1681 1682 1683 1684 1685 1686

  final Directory _directory;

  @override
  Directory get directory => _directory;
}