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

import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
7
import 'package:flutter_tools/src/android/android_sdk.dart';
8
import 'package:flutter_tools/src/android/android_studio.dart';
9
import 'package:flutter_tools/src/base/file_system.dart';
10
import 'package:flutter_tools/src/base/logger.dart';
11 12
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build_apk.dart';
13
import 'package:flutter_tools/src/globals.dart' as globals;
14
import 'package:flutter_tools/src/project.dart';
15
import 'package:flutter_tools/src/reporting/reporting.dart';
16
import 'package:test/fake.dart';
17

18
import '../../src/android_common.dart';
19 20
import '../../src/common.dart';
import '../../src/context.dart';
21
import '../../src/fake_process_manager.dart';
22
import '../../src/test_flutter_command_runner.dart';
23 24 25 26

void main() {
  Cache.disableLocking();

27
  group('Usage', () {
28 29
    late Directory tempDir;
    late TestUsage testUsage;
30 31

    setUp(() {
32
      testUsage = TestUsage();
33
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
34 35 36 37 38 39 40 41 42
    });

    tearDown(() {
      tryToDelete(tempDir);
    });

    testUsingContext('indicate the default target platforms', () async {
      final String projectPath = await createProject(tempDir,
          arguments: <String>['--no-pub', '--template=app']);
43
      final BuildApkCommand command = await runBuildApkCommand(projectPath);
44

45
      expect((await command.usageValues).commandBuildApkTargetPlatform, 'android-arm,android-arm64,android-x64');
46 47 48

    }, overrides: <Type, Generator>{
      AndroidBuilder: () => FakeAndroidBuilder(),
49
    });
50 51 52 53 54

    testUsingContext('split per abi', () async {
      final String projectPath = await createProject(tempDir,
          arguments: <String>['--no-pub', '--template=app']);

55
      final BuildApkCommand commandWithFlag = await runBuildApkCommand(projectPath,
56
          arguments: <String>['--split-per-abi']);
57
      expect((await commandWithFlag.usageValues).commandBuildApkSplitPerAbi, true);
58

59
      final BuildApkCommand commandWithoutFlag = await runBuildApkCommand(projectPath);
60
      expect((await commandWithoutFlag.usageValues).commandBuildApkSplitPerAbi, false);
61 62 63

    }, overrides: <Type, Generator>{
      AndroidBuilder: () => FakeAndroidBuilder(),
64
    });
65 66 67 68 69

    testUsingContext('build type', () async {
      final String projectPath = await createProject(tempDir,
          arguments: <String>['--no-pub', '--template=app']);

70
      final BuildApkCommand commandDefault = await runBuildApkCommand(projectPath);
71
      expect((await commandDefault.usageValues).commandBuildApkBuildMode, 'release');
72

73
      final BuildApkCommand commandInRelease = await runBuildApkCommand(projectPath,
74
          arguments: <String>['--release']);
75
      expect((await commandInRelease.usageValues).commandBuildApkBuildMode, 'release');
76

77
      final BuildApkCommand commandInDebug = await runBuildApkCommand(projectPath,
78
          arguments: <String>['--debug']);
79
      expect((await commandInDebug.usageValues).commandBuildApkBuildMode, 'debug');
80

81
      final BuildApkCommand commandInProfile = await runBuildApkCommand(projectPath,
82
          arguments: <String>['--profile']);
83
      expect((await commandInProfile.usageValues).commandBuildApkBuildMode, 'profile');
84 85 86

    }, overrides: <Type, Generator>{
      AndroidBuilder: () => FakeAndroidBuilder(),
87
    });
88 89 90 91 92 93 94

    testUsingContext('logs success', () async {
      final String projectPath = await createProject(tempDir,
          arguments: <String>['--no-pub', '--template=app']);

      await runBuildApkCommand(projectPath);

95 96 97 98 99 100 101
      expect(testUsage.events, contains(
        const TestUsageEvent(
          'tool-command-result',
          'apk',
          label: 'success',
        ),
      ));
102 103 104
    },
    overrides: <Type, Generator>{
      AndroidBuilder: () => FakeAndroidBuilder(),
105
      Usage: () => testUsage,
106
    });
107
  });
108 109

  group('Gradle', () {
110 111 112 113 114
    late Directory tempDir;
    late FakeProcessManager processManager;
    late String gradlew;
    late AndroidSdk mockAndroidSdk;
    late TestUsage testUsage;
115 116

    setUp(() {
117
      testUsage = TestUsage();
118 119 120
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
      gradlew = globals.fs.path.join(tempDir.path, 'flutter_project', 'android',
          globals.platform.isWindows ? 'gradlew.bat' : 'gradlew');
121
      processManager = FakeProcessManager.empty();
122
      mockAndroidSdk = FakeAndroidSdk(globals.fs.directory('irrelevant'));
123 124 125 126 127 128
    });

    tearDown(() {
      tryToDelete(tempDir);
    });

129
    group('AndroidSdk', () {
130
      testUsingContext('throws throwsToolExit if AndroidSdk is null', () async {
131
        final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
132

133 134
        await expectLater(
          () => runBuildApkCommand(
135 136
            projectPath,
            arguments: <String>['--no-pub'],
137 138 139 140 141
          ),
          throwsToolExit(
            message: 'No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable',
          ),
        );
142 143 144 145
      },
      overrides: <Type, Generator>{
        AndroidSdk: () => null,
        FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
146 147
        ProcessManager: () => processManager,
        AndroidStudio: () => FakeAndroidStudio(),
148 149 150
      });
    });

Emmanuel Garcia's avatar
Emmanuel Garcia committed
151
    testUsingContext('shrinking is enabled by default on release mode', () async {
152
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
153 154
      processManager.addCommand(FakeCommand(
        command: <String>[
155 156
          gradlew,
          '-q',
157
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
158
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
159
          '-Pbase-application-name=android.app.Application',
160
          '-Pdart-obfuscation=false',
161
          '-Ptrack-widget-creation=true',
162
          '-Ptree-shake-icons=true',
163 164
          'assembleRelease',
        ],
165 166 167 168 169 170 171 172
        exitCode: 1,
      ));

      await expectLater(
        () => runBuildApkCommand(projectPath),
        throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
      );
      expect(processManager, hasNoRemainingExpectations);
173 174 175 176
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
177 178
      ProcessManager: () => processManager,
      AndroidStudio: () => FakeAndroidStudio(),
179
    });
180

181
    testUsingContext('--split-debug-info is enabled when an output directory is provided', () async {
182
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
183 184
      processManager.addCommand(FakeCommand(
        command: <String>[
185 186
          gradlew,
          '-q',
187
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
188
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
189
          '-Pbase-application-name=android.app.Application',
190
          '-Pdart-obfuscation=false',
191
          '-Psplit-debug-info=${tempDir.path}',
192
          '-Ptrack-widget-creation=true',
193
          '-Ptree-shake-icons=true',
194 195
          'assembleRelease',
        ],
196 197 198 199 200 201 202 203
        exitCode: 1,
      ));

      await expectLater(
        () => runBuildApkCommand(projectPath, arguments: <String>['--split-debug-info=${tempDir.path}']),
        throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
      );
      expect(processManager, hasNoRemainingExpectations);
204 205 206 207
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
208 209
      ProcessManager: () => processManager,
      AndroidStudio: () => FakeAndroidStudio(),
210 211
    });

212
    testUsingContext('--extra-front-end-options are provided to gradle project', () async {
213
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
214 215
      processManager.addCommand(FakeCommand(
        command: <String>[
216 217 218 219
          gradlew,
          '-q',
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
220
          '-Pbase-application-name=android.app.Application',
221
          '-Pdart-obfuscation=false',
222
          '-Pextra-front-end-options=foo,bar',
223
          '-Ptrack-widget-creation=true',
224
          '-Ptree-shake-icons=true',
225 226
          'assembleRelease',
        ],
227 228 229 230 231 232 233 234
        exitCode: 1,
      ));

      await expectLater(() => runBuildApkCommand(projectPath, arguments: <String>[
        '--extra-front-end-options=foo',
        '--extra-front-end-options=bar',
      ]), throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
       expect(processManager, hasNoRemainingExpectations);
235 236 237 238
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
239 240
      ProcessManager: () => processManager,
      AndroidStudio: () => FakeAndroidStudio(),
241 242
    });

Emmanuel Garcia's avatar
Emmanuel Garcia committed
243
    testUsingContext('shrinking is disabled when --no-shrink is passed', () async {
244
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
245 246
      processManager.addCommand(FakeCommand(
        command: <String>[
247 248
          gradlew,
          '-q',
249
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
250
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
251
          '-Pbase-application-name=android.app.Application',
252
          '-Pdart-obfuscation=false',
253
          '-Ptrack-widget-creation=true',
254
          '-Ptree-shake-icons=true',
255 256
          'assembleRelease',
        ],
257 258 259 260 261 262 263 264 265 266 267
        exitCode: 1,
      ));

      await expectLater(
        () => runBuildApkCommand(
          projectPath,
          arguments: <String>['--no-shrink'],
        ),
        throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
      );
      expect(processManager, hasNoRemainingExpectations);
268 269 270 271
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
272 273
      ProcessManager: () => processManager,
      AndroidStudio: () => FakeAndroidStudio(),
274
    });
275

Emmanuel Garcia's avatar
Emmanuel Garcia committed
276
    testUsingContext('guides the user when the shrinker fails', () async {
277
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
278 279 280 281 282
      const String r8StdoutWarning =
        "Execution failed for task ':app:transformClassesAndResourcesWithR8ForStageInternal'.\n"
        '> com.android.tools.r8.CompilationFailedException: Compilation failed to complete';
      processManager.addCommand(FakeCommand(
        command: <String>[
283 284
          gradlew,
          '-q',
285
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
286
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
287
          '-Pbase-application-name=android.app.Application',
288
          '-Pdart-obfuscation=false',
289
          '-Ptrack-widget-creation=true',
290
          '-Ptree-shake-icons=true',
291 292
          'assembleRelease',
        ],
293 294 295
        exitCode: 1,
        stdout: r8StdoutWarning,
      ));
296

297 298
      await expectLater(
        () => runBuildApkCommand(
299
          projectPath,
300 301
        ),
        throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
302 303
      );
      expect(
304 305 306 307 308
        testLogger.statusText, allOf(
          containsIgnoringWhitespace('The shrinker may have failed to optimize the Java bytecode.'),
          containsIgnoringWhitespace('To disable the shrinker, pass the `--no-shrink` flag to this command.'),
          containsIgnoringWhitespace('To learn more, see: https://developer.android.com/studio/build/shrink-code'),
        )
309
      );
310 311 312
      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
313
          'gradle',
314
          label: 'gradle-r8-failure',
315
          parameters: CustomDimensions(),
316 317
        ),
      ));
318
      expect(processManager, hasNoRemainingExpectations);
319 320 321 322
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
323
      ProcessManager: () => processManager,
324
      Usage: () => testUsage,
325
      AndroidStudio: () => FakeAndroidStudio(),
326
    });
327

328
    testUsingContext("reports when the app isn't using AndroidX", () async {
329
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
330 331 332 333 334 335
      // Simulate a non-androidx project.
      tempDir
        .childDirectory('flutter_project')
        .childDirectory('android')
        .childFile('gradle.properties')
        .writeAsStringSync('android.useAndroidX=false');
336 337
      processManager.addCommand(FakeCommand(
        command: <String>[
338 339
          gradlew,
          '-q',
340
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
341
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
342
          '-Pbase-application-name=android.app.Application',
343
          '-Pdart-obfuscation=false',
344
          '-Ptrack-widget-creation=true',
345
          '-Ptree-shake-icons=true',
346 347
          'assembleRelease',
        ],
348 349
      ));

350
      // The command throws a [ToolExit] because it expects an APK in the file system.
351
      await expectLater(() =>  runBuildApkCommand(projectPath), throwsToolExit());
352

353 354
      expect(
        testLogger.statusText,
355 356 357 358 359 360
        allOf(
          containsIgnoringWhitespace("Your app isn't using AndroidX"),
          containsIgnoringWhitespace(
            'To avoid potential build failures, you can quickly migrate your app by '
            'following the steps on https://goo.gl/CP92wY'
          ),
361
        ),
362
      );
363 364 365
      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
366
          'gradle',
367
          label: 'app-not-using-android-x',
368
          parameters: CustomDimensions(),
369 370
        ),
      ));
371
      expect(processManager, hasNoRemainingExpectations);
372 373 374 375
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
376
      ProcessManager: () => processManager,
377
      Usage: () => testUsage,
378
      AndroidStudio: () => FakeAndroidStudio(),
379
    });
380 381

    testUsingContext('reports when the app is using AndroidX', () async {
382
      final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
383 384
      processManager.addCommand(FakeCommand(
        command: <String>[
385 386
          gradlew,
          '-q',
387
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
388
          '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
389
          '-Pbase-application-name=android.app.Application',
390
          '-Pdart-obfuscation=false',
391
          '-Ptrack-widget-creation=true',
392
          '-Ptree-shake-icons=true',
393 394
          'assembleRelease',
        ],
395 396
      ));

397
      // The command throws a [ToolExit] because it expects an APK in the file system.
398
      await expectLater(() => runBuildApkCommand(projectPath), throwsToolExit());
399 400

      expect(
401 402 403 404 405 406
        testLogger.statusText, allOf(
          isNot(contains("[!] Your app isn't using AndroidX")),
          isNot(contains(
            'To avoid potential build failures, you can quickly migrate your app by '
            'following the steps on https://goo.gl/CP92wY'
          ))
407 408
        ),
      );
409 410 411
      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
412
          'gradle',
413
          label: 'app-using-android-x',
414
          parameters: CustomDimensions(),
415 416
        ),
      ));
417
      expect(processManager, hasNoRemainingExpectations);
418 419 420 421
    },
    overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
422
      ProcessManager: () => processManager,
423
      Usage: () => testUsage,
424
      AndroidStudio: () => FakeAndroidStudio(),
425
    });
426 427 428 429
  });
}

Future<BuildApkCommand> runBuildApkCommand(
430
  String target, {
431
  List<String>? arguments,
432
}) async {
433
  final BuildApkCommand command = BuildApkCommand(logger: BufferLogger.test());
434 435 436 437
  final CommandRunner<void> runner = createTestCommandRunner(command);
  await runner.run(<String>[
    'apk',
    ...?arguments,
438
    '--no-pub',
439
    globals.fs.path.join(target, 'lib', 'main.dart'),
440 441
  ]);
  return command;
442
}
443

444 445 446 447 448 449 450
class FakeAndroidSdk extends Fake implements AndroidSdk {
  FakeAndroidSdk(this.directory);

  @override
  final Directory directory;
}

451 452 453
class FakeAndroidStudio extends Fake implements AndroidStudio {
  @override
  String get javaPath => 'java';
454
}