gradle_test.dart 18 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:file/memory.dart';
8
import 'package:flutter_tools/src/android/gradle.dart';
9
import 'package:flutter_tools/src/artifacts.dart';
10 11
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_info.dart';
12
import 'package:flutter_tools/src/cache.dart';
13
import 'package:flutter_tools/src/ios/xcodeproj.dart';
14
import 'package:flutter_tools/src/project.dart';
15 16 17
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
18

19
import '../src/common.dart';
20
import '../src/context.dart';
21
import '../src/pubspec_schema.dart';
22

23
void main() {
24
  Cache.flutterRoot = getFlutterRoot();
25
  group('gradle build', () {
26
    test('do not crash if there is no Android SDK', () async {
27 28 29 30 31 32 33 34 35
      Exception shouldBeToolExit;
      try {
        // We'd like to always set androidSdk to null and test updateLocalProperties. But that's
        // currently impossible as the test is not hermetic. Luckily, our bots don't have Android
        // SDKs yet so androidSdk should be null by default.
        //
        // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
        // will be null and our expectation would fail. That would remind us to make these tests
        // hermetic before adding Android SDKs to the bots.
36
        updateLocalProperties(project: FlutterProject.current());
37 38 39 40 41 42 43
      } on Exception catch (e) {
        shouldBeToolExit = e;
      }
      // Ensure that we throw a meaningful ToolExit instead of a general crash.
      expect(shouldBeToolExit, isToolExit);
    });

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
    test('androidXFailureRegex should match lines with likely AndroidX errors', () {
      final List<String> nonMatchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        '',
      ];
      final List<String> matchingLines = <String>[
        'AAPT: error: resource android:attr/fontVariationSettings not found.',
        'AAPT: error: resource android:attr/ttcIndex not found.',
        'error: package android.support.annotation does not exist',
        'import android.support.annotation.NonNull;',
        'import androidx.annotation.NonNull;',
        'Daemon:  AAPT2 aapt2-3.2.1-4818971-linux Daemon #0',
      ];
      for (String m in nonMatchingLines) {
        expect(androidXFailureRegex.hasMatch(m), isFalse);
      }
      for (String m in matchingLines) {
        expect(androidXFailureRegex.hasMatch(m), isTrue);
      }
    });

    test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
      final List<String> nonMatchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        'Generic plugin AndroidX text',
        '',
      ];
      final List<String> matchingLines = <String>[
        '*********************************************************************************************************************************',
        "WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.",
        'See https://goo.gl/CP92wY for more information on the problem and how to fix it.',
        'This warning prints for all Android build failures. The real root cause of the error may be unrelated.',
      ];
      for (String m in nonMatchingLines) {
        expect(androidXPluginWarningRegex.hasMatch(m), isFalse);
      }
      for (String m in matchingLines) {
        expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
      }
    });

    test('ndkMessageFilter should only match lines without the error message', () {
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
      final List<String> nonMatchingLines = <String>[
        'NDK is missing a "platforms" directory.',
        'If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to /usr/local/company/home/username/Android/Sdk/ndk-bundle.',
        'If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.',
      ];
      final List<String> matchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        '',
        'Something NDK related mentioning ANDROID_NDK_HOME',
      ];
      for (String m in nonMatchingLines) {
        expect(ndkMessageFilter.hasMatch(m), isFalse);
      }
      for (String m in matchingLines) {
        expect(ndkMessageFilter.hasMatch(m), isTrue);
      }
    });
  });

108
  group('gradle project', () {
109
    GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks);
110

111 112 113 114 115
    test('should extract build directory from app properties', () {
      final GradleProject project = projectFrom('''
someProperty: someValue
buildDir: /Users/some/apps/hello/build/app
someOtherProperty: someOtherValue
116
      ''', '');
117 118 119 120
      expect(
        fs.path.normalize(project.apkDirectory.path),
        fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'),
      );
121 122
    });
    test('should extract default build variants from app properties', () {
123 124 125 126 127 128 129 130
      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
someTask
assemble
assembleAndroidTest
assembleDebug
assembleProfile
assembleRelease
someOtherTask
131 132 133 134 135
      ''');
      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
      expect(project.productFlavors, isEmpty);
    });
    test('should extract custom build variants from app properties', () {
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
someTask
assemble
assembleAndroidTest
assembleDebug
assembleFree
assembleFreeAndroidTest
assembleFreeDebug
assembleFreeProfile
assembleFreeRelease
assemblePaid
assemblePaidAndroidTest
assemblePaidDebug
assemblePaidProfile
assemblePaidRelease
assembleProfile
assembleRelease
someOtherTask
154 155 156 157 158
      ''');
      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
      expect(project.productFlavors, <String>['free', 'paid']);
    });
    test('should provide apk file name for default build types', () {
159
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
160 161 162 163 164 165
      expect(project.apkFileFor(BuildInfo.debug), 'app-debug.apk');
      expect(project.apkFileFor(BuildInfo.profile), 'app-profile.apk');
      expect(project.apkFileFor(BuildInfo.release), 'app-release.apk');
      expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('should provide apk file name for flavored build types', () {
166
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
167 168 169 170
      expect(project.apkFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app-free-debug.apk');
      expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app-paid-release.apk');
      expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
171 172 173 174 175 176 177 178 179 180 181 182 183
    test('should provide bundle file name for default build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
      expect(project.bundleFileFor(BuildInfo.debug), 'app.aab');
      expect(project.bundleFileFor(BuildInfo.profile), 'app.aab');
      expect(project.bundleFileFor(BuildInfo.release), 'app.aab');
      expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
    });
    test('should provide bundle file name for flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
      expect(project.bundleFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app.aab');
      expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app.aab');
      expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
    });
184
    test('should provide assemble task name for default build types', () {
185
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
186 187 188 189 190 191
      expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
      expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
      expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('should provide assemble task name for flavored build types', () {
192
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
193 194 195
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
196
    });
197
    test('should respect format of the flavored build types', () {
198
      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
199 200
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug');
    });
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
    test('bundle should provide assemble task name for default build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
      expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug');
      expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile');
      expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('bundle should provide assemble task name for flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('bundle should respect format of the flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
    });
218
  });
219 220

  group('Gradle local.properties', () {
221 222 223 224
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform android;
    FileSystem fs;
225 226

    setUp(() {
227 228 229
      fs = MemoryFileSystem();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
230
      android = fakePlatform('android');
231 232
    });

233 234 235 236 237 238 239
    void testUsingAndroidContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        ProcessManager: () => mockProcessManager,
        Platform: () => android,
        FileSystem: () => fs,
      });
240 241 242
    }

    String propertyFor(String key, File file) {
243
      final Iterable<String> result = file.readAsLinesSync()
244
          .where((String line) => line.startsWith('$key='))
245 246
          .map((String line) => line.split('=')[1]);
      return result.isEmpty ? null : result.first;
247 248 249 250 251 252 253 254
    }

    Future<void> checkBuildVersion({
      String manifest,
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
255 256
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.android_arm, mode: anyNamed('mode'))).thenReturn('engine');
257
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'android_arm'));
258

259 260 261
      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifest);
262

263
      // write schemaData otherwise pubspec.yaml file can't be loaded
264
      writeEmptySchemaFile(fs);
265

266
      updateLocalProperties(
267
        project: FlutterProject.fromPath('path/to/project'),
268 269 270 271 272 273 274
        buildInfo: buildInfo,
        requireAndroidSdk: false,
      );

      final File localPropertiesFile = fs.file('path/to/project/android/local.properties');
      expect(propertyFor('flutter.versionName', localPropertiesFile), expectedBuildName);
      expect(propertyFor('flutter.versionCode', localPropertiesFile), expectedBuildNumber);
275 276
    }

277
    testUsingAndroidContext('extract build name and number from pubspec.yaml', () async {
278 279 280 281 282 283 284 285 286
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

287
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
288 289 290 291 292 293 294 295
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

296
    testUsingAndroidContext('extract build name from pubspec.yaml', () async {
297 298 299 300 301 302 303 304
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
305
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
306 307 308 309 310 311 312 313
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: null,
      );
    });

314
    testUsingAndroidContext('allow build info to override build name', () async {
315 316 317 318 319 320 321 322
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
323
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2');
324 325 326 327 328 329 330 331
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

332
    testUsingAndroidContext('allow build info to override build number', () async {
333 334 335 336 337 338 339 340
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
341
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
342 343 344 345 346 347 348 349
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

350
    testUsingAndroidContext('allow build info to override build name and number', () async {
351 352 353 354 355 356 357 358
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
359
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
360 361 362 363 364 365 366 367
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

368
    testUsingAndroidContext('allow build info to override build name and set number', () async {
369 370 371 372 373 374 375 376
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
377
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
378 379 380 381 382 383 384 385
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

386
    testUsingAndroidContext('allow build info to set build name and number', () async {
387 388 389 390 391 392 393
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
394
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
395 396 397 398 399 400 401
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418

    testUsingAndroidContext('allow build info to unset build name and number', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null),
        expectedBuildName: null,
        expectedBuildNumber: null,
      );
      await checkBuildVersion(
        manifest: manifest,
419
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3'),
420 421 422 423 424
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
      await checkBuildVersion(
        manifest: manifest,
425
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: '4'),
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
        expectedBuildName: '1.0.3',
        expectedBuildNumber: '4',
      );
      // Values don't get unset.
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: null,
        expectedBuildName: '1.0.3',
        expectedBuildNumber: '4',
      );
      // Values get unset.
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null),
        expectedBuildName: null,
        expectedBuildNumber: null,
      );
    });
444
  });
445
}
446 447

Platform fakePlatform(String name) {
448
  return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
449 450 451 452 453
}

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