• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:file/file.dart';
8import 'package:file/memory.dart';
9import 'package:flutter_tools/src/base/common.dart';
10import 'package:flutter_tools/src/base/context.dart';
11import 'package:flutter_tools/src/base/file_system.dart';
12import 'package:flutter_tools/src/base/platform.dart';
13import 'package:flutter_tools/src/cache.dart';
14import 'package:flutter_tools/src/flutter_manifest.dart';
15import 'package:flutter_tools/src/ios/plist_parser.dart';
16import 'package:flutter_tools/src/ios/xcodeproj.dart';
17import 'package:flutter_tools/src/project.dart';
18import 'package:meta/meta.dart';
19import 'package:mockito/mockito.dart';
20
21import '../src/common.dart';
22import '../src/context.dart';
23import '../src/testbed.dart';
24
25void main() {
26  group('Project', () {
27    group('construction', () {
28      testInMemory('fails on null directory', () async {
29        expect(
30          () => FlutterProject.fromDirectory(null),
31          throwsA(isInstanceOf<AssertionError>()),
32        );
33      });
34
35      testInMemory('fails on invalid pubspec.yaml', () async {
36        final Directory directory = fs.directory('myproject');
37        directory.childFile('pubspec.yaml')
38          ..createSync(recursive: true)
39          ..writeAsStringSync(invalidPubspec);
40
41        expect(
42          () => FlutterProject.fromDirectory(directory),
43          throwsA(isInstanceOf<ToolExit>()),
44        );
45      });
46
47      testInMemory('fails on pubspec.yaml parse failure', () async {
48        final Directory directory = fs.directory('myproject');
49        directory.childFile('pubspec.yaml')
50          ..createSync(recursive: true)
51          ..writeAsStringSync(parseErrorPubspec);
52
53        expect(
54          () => FlutterProject.fromDirectory(directory),
55          throwsA(isInstanceOf<ToolExit>()),
56        );
57      });
58
59      testInMemory('fails on invalid example/pubspec.yaml', () async {
60        final Directory directory = fs.directory('myproject');
61        directory.childDirectory('example').childFile('pubspec.yaml')
62          ..createSync(recursive: true)
63          ..writeAsStringSync(invalidPubspec);
64
65        expect(
66          () => FlutterProject.fromDirectory(directory),
67          throwsA(isInstanceOf<ToolExit>()),
68        );
69      });
70
71      testInMemory('treats missing pubspec.yaml as empty', () async {
72        final Directory directory = fs.directory('myproject')
73          ..createSync(recursive: true);
74        expect((FlutterProject.fromDirectory(directory)).manifest.isEmpty,
75          true,
76        );
77      });
78
79      testInMemory('reads valid pubspec.yaml', () async {
80        final Directory directory = fs.directory('myproject');
81        directory.childFile('pubspec.yaml')
82          ..createSync(recursive: true)
83          ..writeAsStringSync(validPubspec);
84        expect(
85          FlutterProject.fromDirectory(directory).manifest.appName,
86          'hello',
87        );
88      });
89
90      testInMemory('sets up location', () async {
91        final Directory directory = fs.directory('myproject');
92        expect(
93          FlutterProject.fromDirectory(directory).directory.absolute.path,
94          directory.absolute.path,
95        );
96        expect(
97          FlutterProject.fromPath(directory.path).directory.absolute.path,
98          directory.absolute.path,
99        );
100        expect(
101          FlutterProject.current().directory.absolute.path,
102          fs.currentDirectory.absolute.path,
103        );
104      });
105    });
106
107    group('editable Android host app', () {
108      testInMemory('fails on non-module', () async {
109        final FlutterProject project = await someProject();
110        await expectLater(
111          project.android.makeHostAppEditable(),
112          throwsA(isInstanceOf<AssertionError>()),
113        );
114      });
115      testInMemory('exits on already editable module', () async {
116        final FlutterProject project = await aModuleProject();
117        await project.android.makeHostAppEditable();
118        return expectToolExitLater(project.android.makeHostAppEditable(), contains('already editable'));
119      });
120      testInMemory('creates android/app folder in place of .android/app', () async {
121        final FlutterProject project = await aModuleProject();
122        await project.android.makeHostAppEditable();
123        expectNotExists(project.directory.childDirectory('.android').childDirectory('app'));
124        expect(
125          project.directory.childDirectory('.android').childFile('settings.gradle').readAsStringSync(),
126          isNot(contains("include ':app'")),
127        );
128        expectExists(project.directory.childDirectory('android').childDirectory('app'));
129        expectExists(project.directory.childDirectory('android').childFile('local.properties'));
130        expect(
131          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
132          contains("include ':app'"),
133        );
134      });
135      testInMemory('retains .android/Flutter folder and references it', () async {
136        final FlutterProject project = await aModuleProject();
137        await project.android.makeHostAppEditable();
138        expectExists(project.directory.childDirectory('.android').childDirectory('Flutter'));
139        expect(
140          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
141          contains('new File(settingsDir.parentFile, \'.android/include_flutter.groovy\')'),
142        );
143      });
144      testInMemory('can be redone after deletion', () async {
145        final FlutterProject project = await aModuleProject();
146        await project.android.makeHostAppEditable();
147        project.directory.childDirectory('android').deleteSync(recursive: true);
148        await project.android.makeHostAppEditable();
149        expectExists(project.directory.childDirectory('android').childDirectory('app'));
150      });
151    });
152
153    group('ensure ready for platform-specific tooling', () {
154      testInMemory('does nothing, if project is not created', () async {
155        final FlutterProject project = FlutterProject(
156          fs.directory('not_created'),
157          FlutterManifest.empty(),
158          FlutterManifest.empty(),
159        );
160        await project.ensureReadyForPlatformSpecificTooling();
161        expectNotExists(project.directory);
162      });
163      testInMemory('does nothing in plugin or package root project', () async {
164        final FlutterProject project = await aPluginProject();
165        await project.ensureReadyForPlatformSpecificTooling();
166        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
167        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
168        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
169        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
170      });
171      testInMemory('injects plugins for iOS', () async {
172        final FlutterProject project = await someProject();
173        await project.ensureReadyForPlatformSpecificTooling();
174        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
175      });
176      testInMemory('generates Xcode configuration for iOS', () async {
177        final FlutterProject project = await someProject();
178        await project.ensureReadyForPlatformSpecificTooling();
179        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
180      });
181      testInMemory('injects plugins for Android', () async {
182        final FlutterProject project = await someProject();
183        await project.ensureReadyForPlatformSpecificTooling();
184        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
185      });
186      testInMemory('updates local properties for Android', () async {
187        final FlutterProject project = await someProject();
188        await project.ensureReadyForPlatformSpecificTooling();
189        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
190      });
191      testInMemory('creates Android library in module', () async {
192        final FlutterProject project = await aModuleProject();
193        await project.ensureReadyForPlatformSpecificTooling();
194        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
195        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
196        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
197      });
198      testInMemory('creates iOS pod in module', () async {
199        final FlutterProject project = await aModuleProject();
200        await project.ensureReadyForPlatformSpecificTooling();
201        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
202        expectExists(flutter.childFile('podhelper.rb'));
203        expectExists(flutter.childFile('flutter_export_environment.sh'));
204        expectExists(flutter.childFile('${project.manifest.appName}.podspec'));
205        expectExists(flutter.childFile('Generated.xcconfig'));
206        final Directory pluginRegistrantClasses = flutter
207            .childDirectory('FlutterPluginRegistrant')
208            .childDirectory('Classes');
209        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
210        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
211      });
212    });
213
214    group('module status', () {
215      testInMemory('is known for module', () async {
216        final FlutterProject project = await aModuleProject();
217        expect(project.isModule, isTrue);
218        expect(project.android.isModule, isTrue);
219        expect(project.ios.isModule, isTrue);
220        expect(project.android.hostAppGradleRoot.basename, '.android');
221        expect(project.ios.hostAppRoot.basename, '.ios');
222      });
223      testInMemory('is known for non-module', () async {
224        final FlutterProject project = await someProject();
225        expect(project.isModule, isFalse);
226        expect(project.android.isModule, isFalse);
227        expect(project.ios.isModule, isFalse);
228        expect(project.android.hostAppGradleRoot.basename, 'android');
229        expect(project.ios.hostAppRoot.basename, 'ios');
230      });
231    });
232
233    group('example', () {
234      testInMemory('exists for plugin', () async {
235        final FlutterProject project = await aPluginProject();
236        expect(project.hasExampleApp, isTrue);
237      });
238      testInMemory('does not exist for non-plugin', () async {
239        final FlutterProject project = await someProject();
240        expect(project.hasExampleApp, isFalse);
241      });
242    });
243
244    group('language', () {
245      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
246      MemoryFileSystem fs;
247      setUp(() {
248        fs = MemoryFileSystem();
249        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
250      });
251
252      testInMemory('default host app language', () async {
253        final FlutterProject project = await someProject();
254        expect(project.ios.isSwift, isFalse);
255        expect(project.android.isKotlin, isFalse);
256      });
257
258      testUsingContext('swift and kotlin host app language', () async {
259        final FlutterProject project = await someProject();
260
261        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
262          'SWIFT_VERSION': '4.0',
263        });
264        addAndroidGradleFile(project.directory,
265          gradleFileContent: () {
266      return '''
267apply plugin: 'com.android.application'
268apply plugin: 'kotlin-android'
269''';
270        });
271        expect(project.ios.isSwift, isTrue);
272        expect(project.android.isKotlin, isTrue);
273      }, overrides: <Type, Generator>{
274          FileSystem: () => fs,
275          XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
276      });
277    });
278
279    group('product bundle identifier', () {
280      MemoryFileSystem fs;
281      MockPlistUtils mockPlistUtils;
282      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
283      setUp(() {
284        fs = MemoryFileSystem();
285        mockPlistUtils = MockPlistUtils();
286        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
287      });
288
289      void testWithMocks(String description, Future<void> testMethod()) {
290        testUsingContext(description, testMethod, overrides: <Type, Generator>{
291          FileSystem: () => fs,
292          PlistParser: () => mockPlistUtils,
293          XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
294        });
295      }
296
297      testWithMocks('null, if no pbxproj or plist entries', () async {
298        final FlutterProject project = await someProject();
299        expect(project.ios.productBundleIdentifier, isNull);
300      });
301      testWithMocks('from pbxproj file, if no plist', () async {
302        final FlutterProject project = await someProject();
303        addIosProjectFile(project.directory, projectFileContent: () {
304          return projectFileWithBundleId('io.flutter.someProject');
305        });
306        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
307      });
308      testWithMocks('from plist, if no variables', () async {
309        final FlutterProject project = await someProject();
310        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject');
311        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
312      });
313      testWithMocks('from pbxproj and plist, if default variable', () async {
314        final FlutterProject project = await someProject();
315        addIosProjectFile(project.directory, projectFileContent: () {
316          return projectFileWithBundleId('io.flutter.someProject');
317        });
318        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
319        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
320      });
321      testWithMocks('from pbxproj and plist, by substitution', () async {
322        final FlutterProject project = await someProject();
323        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
324          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
325          'SUFFIX': 'suffix',
326        });
327        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER).\$(SUFFIX)');
328        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject.suffix');
329      });
330      testWithMocks('empty surrounded by quotes', () async {
331        final FlutterProject project = await someProject();
332        addIosProjectFile(project.directory, projectFileContent: () {
333          return projectFileWithBundleId('', qualifier: '"');
334        });
335        expect(project.ios.productBundleIdentifier, '');
336      });
337      testWithMocks('surrounded by double quotes', () async {
338        final FlutterProject project = await someProject();
339        addIosProjectFile(project.directory, projectFileContent: () {
340          return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
341        });
342        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
343      });
344      testWithMocks('surrounded by single quotes', () async {
345        final FlutterProject project = await someProject();
346        addIosProjectFile(project.directory, projectFileContent: () {
347          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
348        });
349        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
350      });
351    });
352
353    group('organization names set', () {
354      testInMemory('is empty, if project not created', () async {
355        final FlutterProject project = await someProject();
356        expect(project.organizationNames, isEmpty);
357      });
358      testInMemory('is empty, if no platform folders exist', () async {
359        final FlutterProject project = await someProject();
360        project.directory.createSync();
361        expect(project.organizationNames, isEmpty);
362      });
363      testInMemory('is populated from iOS bundle identifier', () async {
364        final FlutterProject project = await someProject();
365        addIosProjectFile(project.directory, projectFileContent: () {
366          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
367        });
368        expect(project.organizationNames, <String>['io.flutter']);
369      });
370      testInMemory('is populated from Android application ID', () async {
371        final FlutterProject project = await someProject();
372        addAndroidGradleFile(project.directory,
373          gradleFileContent: () {
374            return gradleFileWithApplicationId('io.flutter.someproject');
375          });
376        expect(project.organizationNames, <String>['io.flutter']);
377      });
378      testInMemory('is populated from iOS bundle identifier in plugin example', () async {
379        final FlutterProject project = await someProject();
380        addIosProjectFile(project.example.directory, projectFileContent: () {
381          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
382        });
383        expect(project.organizationNames, <String>['io.flutter']);
384      });
385      testInMemory('is populated from Android application ID in plugin example', () async {
386        final FlutterProject project = await someProject();
387        addAndroidGradleFile(project.example.directory,
388          gradleFileContent: () {
389            return gradleFileWithApplicationId('io.flutter.someproject');
390          });
391        expect(project.organizationNames, <String>['io.flutter']);
392      });
393      testInMemory('is populated from Android group in plugin', () async {
394        final FlutterProject project = await someProject();
395        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
396        expect(project.organizationNames, <String>['io.flutter']);
397      });
398      testInMemory('is singleton, if sources agree', () async {
399        final FlutterProject project = await someProject();
400        addIosProjectFile(project.directory, projectFileContent: () {
401          return projectFileWithBundleId('io.flutter.someProject');
402        });
403        addAndroidGradleFile(project.directory,
404          gradleFileContent: () {
405            return gradleFileWithApplicationId('io.flutter.someproject');
406          });
407        expect(project.organizationNames, <String>['io.flutter']);
408      });
409      testInMemory('is non-singleton, if sources disagree', () async {
410        final FlutterProject project = await someProject();
411        addIosProjectFile(project.directory, projectFileContent: () {
412          return projectFileWithBundleId('io.flutter.someProject');
413        });
414        addAndroidGradleFile(project.directory,
415          gradleFileContent: () {
416            return gradleFileWithApplicationId('io.clutter.someproject');
417          });
418        expect(
419          project.organizationNames,
420          <String>['io.flutter', 'io.clutter'],
421        );
422      });
423    });
424  });
425
426  group('Regression test for invalid pubspec', () {
427    Testbed testbed;
428
429    setUp(() {
430      testbed = Testbed();
431    });
432
433    test('Handles asking for builders from an invalid pubspec', () => testbed.run(() {
434      fs.file('pubspec.yaml')
435        ..createSync()
436        ..writeAsStringSync(r'''
437# Hello, World
438''');
439      final FlutterProject flutterProject = FlutterProject.current();
440
441      expect(flutterProject.builders, null);
442    }));
443
444    test('Handles asking for builders from a trivial pubspec', () => testbed.run(() {
445      fs.file('pubspec.yaml')
446        ..createSync()
447        ..writeAsStringSync(r'''
448# Hello, World
449name: foo_bar
450''');
451      final FlutterProject flutterProject = FlutterProject.current();
452
453      expect(flutterProject.builders, null);
454    }));
455  });
456}
457
458Future<FlutterProject> someProject() async {
459  final Directory directory = fs.directory('some_project');
460  directory.childFile('.packages').createSync(recursive: true);
461  directory.childDirectory('ios').createSync(recursive: true);
462  directory.childDirectory('android').createSync(recursive: true);
463  return FlutterProject.fromDirectory(directory);
464}
465
466Future<FlutterProject> aPluginProject() async {
467  final Directory directory = fs.directory('plugin_project');
468  directory.childDirectory('ios').createSync(recursive: true);
469  directory.childDirectory('android').createSync(recursive: true);
470  directory.childDirectory('example').createSync(recursive: true);
471  directory.childFile('pubspec.yaml').writeAsStringSync('''
472name: my_plugin
473flutter:
474  plugin:
475    androidPackage: com.example
476    pluginClass: MyPlugin
477    iosPrefix: FLT
478''');
479  return FlutterProject.fromDirectory(directory);
480}
481
482Future<FlutterProject> aModuleProject() async {
483  final Directory directory = fs.directory('module_project');
484  directory.childFile('.packages').createSync(recursive: true);
485  directory.childFile('pubspec.yaml').writeAsStringSync('''
486name: my_module
487flutter:
488  module:
489    androidPackage: com.example
490''');
491  return FlutterProject.fromDirectory(directory);
492}
493
494/// Executes the [testMethod] in a context where the file system
495/// is in memory.
496@isTest
497void testInMemory(String description, Future<void> testMethod()) {
498  Cache.flutterRoot = getFlutterRoot();
499  final FileSystem testFileSystem = MemoryFileSystem(
500    style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
501  );
502  // Transfer needed parts of the Flutter installation folder
503  // to the in-memory file system used during testing.
504  transfer(Cache().getArtifactDirectory('gradle_wrapper'), testFileSystem);
505  transfer(fs.directory(Cache.flutterRoot)
506      .childDirectory('packages')
507      .childDirectory('flutter_tools')
508      .childDirectory('templates'), testFileSystem);
509  transfer(fs.directory(Cache.flutterRoot)
510      .childDirectory('packages')
511      .childDirectory('flutter_tools')
512      .childDirectory('schema'), testFileSystem);
513  testUsingContext(
514    description,
515    testMethod,
516    overrides: <Type, Generator>{
517      FileSystem: () => testFileSystem,
518      Cache: () => Cache(),
519    },
520  );
521}
522
523/// Transfers files and folders from the local file system's Flutter
524/// installation to an (in-memory) file system used for testing.
525void transfer(FileSystemEntity entity, FileSystem target) {
526  if (entity is Directory) {
527    target.directory(entity.absolute.path).createSync(recursive: true);
528    for (FileSystemEntity child in entity.listSync()) {
529      transfer(child, target);
530    }
531  } else if (entity is File) {
532    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
533  } else {
534    throw 'Unsupported FileSystemEntity ${entity.runtimeType}';
535  }
536}
537
538void expectExists(FileSystemEntity entity) {
539  expect(entity.existsSync(), isTrue);
540}
541
542void expectNotExists(FileSystemEntity entity) {
543  expect(entity.existsSync(), isFalse);
544}
545
546void addIosProjectFile(Directory directory, {String projectFileContent()}) {
547  directory
548      .childDirectory('ios')
549      .childDirectory('Runner.xcodeproj')
550      .childFile('project.pbxproj')
551        ..createSync(recursive: true)
552    ..writeAsStringSync(projectFileContent());
553}
554
555void addAndroidGradleFile(Directory directory, { String gradleFileContent() }) {
556  directory
557      .childDirectory('android')
558      .childDirectory('app')
559      .childFile('build.gradle')
560        ..createSync(recursive: true)
561        ..writeAsStringSync(gradleFileContent());
562}
563
564void addAndroidWithGroup(Directory directory, String id) {
565  directory.childDirectory('android').childFile('build.gradle')
566    ..createSync(recursive: true)
567    ..writeAsStringSync(gradleFileWithGroupId(id));
568}
569
570String get validPubspec => '''
571name: hello
572flutter:
573''';
574
575String get invalidPubspec => '''
576name: hello
577flutter:
578  invalid:
579''';
580
581String get parseErrorPubspec => '''
582name: hello
583# Whitespace is important.
584flutter:
585    something:
586  something_else:
587''';
588
589String projectFileWithBundleId(String id, {String qualifier}) {
590  return '''
59197C147061CF9000F007C117D /* Debug */ = {
592  isa = XCBuildConfiguration;
593  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
594  buildSettings = {
595    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
596    PRODUCT_NAME = "\$(TARGET_NAME)";
597  };
598  name = Debug;
599};
600''';
601}
602
603String gradleFileWithApplicationId(String id) {
604  return '''
605apply plugin: 'com.android.application'
606android {
607    compileSdkVersion 28
608
609    defaultConfig {
610        applicationId '$id'
611    }
612}
613''';
614}
615
616String gradleFileWithGroupId(String id) {
617  return '''
618group '$id'
619version '1.0-SNAPSHOT'
620
621apply plugin: 'com.android.library'
622
623android {
624    compileSdkVersion 28
625}
626''';
627}
628
629File androidPluginRegistrant(Directory parent) {
630  return parent.childDirectory('src')
631    .childDirectory('main')
632    .childDirectory('java')
633    .childDirectory('io')
634    .childDirectory('flutter')
635    .childDirectory('plugins')
636    .childFile('GeneratedPluginRegistrant.java');
637}
638
639class MockPlistUtils extends Mock implements PlistParser {}
640
641class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
642  @override
643  bool get isInstalled => true;
644}
645