android_workflow_test.dart 20.7 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:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
7
import 'package:flutter_tools/src/android/android_studio.dart';
8
import 'package:flutter_tools/src/android/android_workflow.dart';
9
import 'package:flutter_tools/src/base/file_system.dart';
10
import 'package:flutter_tools/src/base/logger.dart';
11
import 'package:flutter_tools/src/base/os.dart';
12
import 'package:flutter_tools/src/base/platform.dart';
13 14
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/base/version.dart';
15
import 'package:flutter_tools/src/doctor_validator.dart';
16
import 'package:test/fake.dart';
17

18
import '../../src/common.dart';
19
import '../../src/fake_process_manager.dart';
20
import '../../src/fakes.dart';
21

22
void main() {
23 24 25 26 27
  late FakeAndroidSdk sdk;
  late Logger logger;
  late MemoryFileSystem fileSystem;
  late FakeProcessManager processManager;
  late FakeStdio stdio;
28 29

  setUp(() {
30
    sdk = FakeAndroidSdk();
31 32 33
    fileSystem = MemoryFileSystem.test();
    fileSystem.directory('/home/me').createSync(recursive: true);
    logger = BufferLogger.test();
34
    processManager = FakeProcessManager.empty();
35
    stdio = FakeStdio();
36 37
  });

38 39 40
  testWithoutContext('AndroidWorkflow handles a null AndroidSDK', () {
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
41
      androidSdk: null, // ignore: avoid_redundant_argument_values
42
      operatingSystemUtils: FakeOperatingSystemUtils(),
43 44 45 46 47 48 49
    );

    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

50
  testWithoutContext('AndroidWorkflow handles a null adb', () {
51 52
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = null;
53 54 55
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
56
      operatingSystemUtils: FakeOperatingSystemUtils(),
57 58 59 60 61 62 63
    );

    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

64 65
  // Android Studio is not currently supported on Linux Arm64 hosts.
  testWithoutContext('Not supported AndroidStudio on Linux Arm Hosts', () {
66 67
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = null;
68 69 70 71 72 73 74
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
      operatingSystemUtils: CustomFakeOperatingSystemUtils(hostPlatform: HostPlatform.linux_arm64),
    );

    expect(androidWorkflow.appliesToHostPlatform, false);
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

  testWithoutContext('AndroidWorkflow is disabled if feature is disabled', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = 'path/to/adb';
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(isAndroidEnabled: false),
      androidSdk: androidSdk,
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(androidWorkflow.appliesToHostPlatform, false);
    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

  testWithoutContext('AndroidWorkflow cannot list emulators if emulatorPath is null', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = 'path/to/adb';
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(androidWorkflow.appliesToHostPlatform, true);
    expect(androidWorkflow.canLaunchDevices, true);
    expect(androidWorkflow.canListDevices, true);
    expect(androidWorkflow.canListEmulators, false);
  });

  testWithoutContext('AndroidWorkflow can list emulators', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = 'path/to/adb';
    androidSdk.emulatorPath = 'path/to/emulator';
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(androidWorkflow.appliesToHostPlatform, true);
    expect(androidWorkflow.canLaunchDevices, true);
    expect(androidWorkflow.canListDevices, true);
    expect(androidWorkflow.canListEmulators, true);
124
  });
125

126
  testWithoutContext('licensesAccepted returns LicensesAccepted.unknown if cannot find sdkmanager', () async {
127 128
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
129 130 131 132 133 134 135 136
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
137
      androidStudio: FakeAndroidStudio(),
138 139
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
140
    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
141

142
    expect(licenseStatus, LicensesAccepted.unknown);
143 144 145
  });

  testWithoutContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async {
146 147
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
148 149 150 151 152 153 154 155
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
156
      androidStudio: FakeAndroidStudio(),
157 158
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
159
    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
160

161
    expect(licenseStatus, LicensesAccepted.unknown);
162 163 164
  });

  testWithoutContext('licensesAccepted handles garbage/no output', () async {
165 166 167 168 169 170 171
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: 'asdasassad',
    ));
172 173 174 175 176 177 178 179
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
180
      androidStudio: FakeAndroidStudio(),
181 182
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
183
    final LicensesAccepted result = await licenseValidator.licensesAccepted;
184

185
    expect(result, LicensesAccepted.unknown);
186 187 188
  });

  testWithoutContext('licensesAccepted works for all licenses accepted', () async {
189 190 191 192 193 194 195 196 197 198 199
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String output = '''
[=======================================] 100% Computing updates...
All SDK package licenses accepted.
''';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: output,
    ));
200

201 202 203 204 205 206 207 208
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
209
      androidStudio: FakeAndroidStudio(),
210 211
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
212
    final LicensesAccepted result = await licenseValidator.licensesAccepted;
213

214
    expect(result, LicensesAccepted.all);
215 216 217
  });

  testWithoutContext('licensesAccepted works for some licenses accepted', () async {
218 219 220 221 222 223 224 225 226 227 228 229
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String output = '''
[=======================================] 100% Computing updates...
2 of 5 SDK package licenses not accepted.
Review licenses that have not been accepted (y/N)?
''';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: output,
    ));
230

231 232 233 234 235 236 237 238
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
239
      androidStudio: FakeAndroidStudio(),
240 241
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
242
    final LicensesAccepted result = await licenseValidator.licensesAccepted;
243

244
    expect(result, LicensesAccepted.some);
245 246 247
  });

  testWithoutContext('licensesAccepted works for no licenses accepted', () async {
248 249 250 251 252 253 254 255 256 257 258 259
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String output = '''
[=======================================] 100% Computing updates...
5 of 5 SDK package licenses not accepted.
Review licenses that have not been accepted (y/N)?
''';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: output,
    ));
260

261 262 263 264 265 266 267 268
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
269
      androidStudio: FakeAndroidStudio(),
270 271
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
272
    final LicensesAccepted result = await licenseValidator.licensesAccepted;
273

274
    expect(result, LicensesAccepted.none);
275 276 277
  });

  testWithoutContext('runLicenseManager succeeds for version >= 26', () async {
278 279 280 281 282 283
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.sdkManagerVersion = '26.0.0';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
284
      ],
285
    ));
286

287 288 289 290 291 292 293 294
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
295
      androidStudio: FakeAndroidStudio(),
296 297
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
298

299 300 301 302
    expect(await licenseValidator.runLicenseManager(), isTrue);
  });

  testWithoutContext('runLicenseManager errors when sdkmanager is not found', () async {
303 304
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
305

306 307 308 309 310 311 312 313
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
314
      androidStudio: FakeAndroidStudio(),
315 316 317 318 319
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(licenseValidator.runLicenseManager(), throwsToolExit());
  });
320

321
  testWithoutContext('runLicenseManager errors when sdkmanager fails to run', () async {
322 323
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
324

325 326 327 328 329 330 331 332
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
333
      androidStudio: FakeAndroidStudio(),
334 335 336 337 338
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(licenseValidator.runLicenseManager(), throwsToolExit());
  });
339

340 341 342 343
  testWithoutContext('detects license-only SDK installation with cmdline-tools', () async {
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = false
344 345
      ..cmdlineToolsAvailable = true
      ..directory = fileSystem.directory('/foo/bar');
346
    final ValidationResult validationResult = await AndroidValidator(
347
      androidStudio: FakeAndroidStudio(),
348
      androidSdk: sdk,
349
      fileSystem: fileSystem,
350 351 352
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
353
      userMessages: UserMessages(),
354
    ).validate();
355

356
    expect(validationResult.type, ValidationType.partial);
357 358 359 360 361 362 363 364

    final ValidationMessage sdkMessage = validationResult.messages.first;
    expect(sdkMessage.type, ValidationMessageType.information);
    expect(sdkMessage.message, 'Android SDK at /foo/bar');

    final ValidationMessage licenseMessage = validationResult.messages.last;
    expect(licenseMessage.type, ValidationMessageType.hint);
    expect(licenseMessage.message, UserMessages().androidSdkLicenseOnly(kAndroidHome));
365
  });
366

367
  testWithoutContext('detects minimum required SDK and buildtools', () async {
368 369 370 371 372 373 374 375 376 377 378 379 380
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'which',
        'java',
      ], exitCode: 1,
    ));
    final FakeAndroidSdkVersion sdkVersion = FakeAndroidSdkVersion()
      ..sdkLevel = 28
      ..buildToolsVersion = Version(26, 0, 3);

    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
381
      ..cmdlineToolsAvailable = true
382
    // Test with invalid SDK and build tools
383 384 385 386
      ..directory = fileSystem.directory('/foo/bar')
      ..sdkManagerPath = '/foo/bar/sdkmanager'
      ..latestVersion = sdkVersion;

387
    final String errorMessage = UserMessages().androidSdkBuildToolsOutdated(
388 389
      kAndroidSdkMinVersion,
      kAndroidSdkBuildToolsMinVersion.toString(),
390
      FakePlatform(),
391 392
    );

393
    final AndroidValidator androidValidator = AndroidValidator(
394
      androidStudio: null, // ignore: avoid_redundant_argument_values
395
      androidSdk: sdk,
396
      fileSystem: fileSystem,
397 398 399
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
400
      userMessages: UserMessages(),
401 402 403
    );

    ValidationResult validationResult = await androidValidator.validate();
404 405 406 407 408 409 410
    expect(validationResult.type, ValidationType.missing);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );

    // Test with valid SDK but invalid build tools
411 412
    sdkVersion.sdkLevel = 29;
    sdkVersion.buildToolsVersion = Version(28, 0, 2);
413

414
    validationResult = await androidValidator.validate();
415 416 417 418 419 420 421
    expect(validationResult.type, ValidationType.missing);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );

    // Test with valid SDK and valid build tools
422
    // Will still be partial because AndroidSdk.findJavaBinary is static :(
423 424
    sdkVersion.sdkLevel = kAndroidSdkMinVersion;
    sdkVersion.buildToolsVersion = kAndroidSdkBuildToolsMinVersion;
425

426
    validationResult = await androidValidator.validate();
427 428 429 430 431
    expect(validationResult.type, ValidationType.partial); // No Java binary
    expect(
      validationResult.messages.any((ValidationMessage message) => message.message == errorMessage),
      isFalse,
    );
432
  });
433

434 435 436 437
  testWithoutContext('detects missing cmdline tools', () async {
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
438 439
      ..cmdlineToolsAvailable = false
      ..directory = fileSystem.directory('/foo/bar');
440 441

    final AndroidValidator androidValidator = AndroidValidator(
442
      androidStudio: null, // ignore: avoid_redundant_argument_values
443 444 445 446 447 448 449 450
      androidSdk: sdk,
      fileSystem: fileSystem,
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
      userMessages: UserMessages(),
    );

451 452
    final String errorMessage = UserMessages().androidMissingCmdTools;

453 454
    final ValidationResult validationResult = await androidValidator.validate();
    expect(validationResult.type, ValidationType.missing);
455 456 457 458 459 460 461 462

    final ValidationMessage sdkMessage = validationResult.messages.first;
    expect(sdkMessage.type, ValidationMessageType.information);
    expect(sdkMessage.message, 'Android SDK at /foo/bar');

    final ValidationMessage cmdlineMessage = validationResult.messages.last;
    expect(cmdlineMessage.type, ValidationMessageType.error);
    expect(cmdlineMessage.message, errorMessage);
463 464
  });

465
  testWithoutContext('detects minimum required java version', () async {
466 467 468 469 470 471 472 473 474 475 476
    // Test with older version of JDK
    const String javaVersionText = 'openjdk version "1.7.0_212"';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'home/java/bin/java',
        '-version',
      ], stderr: javaVersionText,
    ));
    final FakeAndroidSdkVersion sdkVersion = FakeAndroidSdkVersion()
      ..sdkLevel = 29
      ..buildToolsVersion = Version(28, 0, 3);
477 478

    // Mock a pass through scenario to reach _checkJavaVersion()
479 480 481
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
482
      ..cmdlineToolsAvailable = true
483 484 485 486
      ..directory = fileSystem.directory('/foo/bar')
      ..sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.latestVersion = sdkVersion;

487
    final String errorMessage = UserMessages().androidJavaMinimumVersion(javaVersionText);
488

489 490
    final ValidationResult validationResult = await AndroidValidator(
      androidSdk: sdk,
491
      androidStudio: null, // ignore: avoid_redundant_argument_values
492
      fileSystem: fileSystem,
493 494 495
      logger: logger,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me', 'JAVA_HOME': 'home/java'},
      processManager: processManager,
496
      userMessages: UserMessages(),
497
    ).validate();
498 499 500 501 502
    expect(validationResult.type, ValidationType.partial);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );
503 504 505 506 507 508
    expect(
      validationResult.messages.any(
        (ValidationMessage message) => message.message.contains('Unable to locate Android SDK')
      ),
      false,
    );
509
  });
510

511
  testWithoutContext('Mentions `flutter config --android-sdk if user has no AndroidSdk`', () async {
512
    final ValidationResult validationResult = await AndroidValidator(
513 514
      androidSdk: null, // ignore: avoid_redundant_argument_values
      androidStudio: null, // ignore: avoid_redundant_argument_values
515
      fileSystem: fileSystem,
516 517 518
      logger: logger,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me', 'JAVA_HOME': 'home/java'},
      processManager: processManager,
519
      userMessages: UserMessages(),
520
    ).validate();
521

522 523
    expect(
      validationResult.messages.any(
524
        (ValidationMessage message) => message.message.contains('flutter config --android-sdk')
525 526 527 528
      ),
      true,
    );
  });
529
}
530

531 532
class FakeAndroidSdk extends Fake implements AndroidSdk {
  @override
533
  String? sdkManagerPath;
534 535

  @override
536
  String? sdkManagerVersion;
537 538

  @override
539
  String? adbPath;
540 541

  @override
542
  bool licensesAvailable = false;
543 544

  @override
545
  bool platformToolsAvailable = false;
546

547
  @override
548
  bool cmdlineToolsAvailable = false;
549

550
  @override
551
  Directory directory = MemoryFileSystem.test().directory('/foo/bar');
552 553

  @override
554
  AndroidSdkVersion? latestVersion;
555

556
  @override
557
  String? emulatorPath;
558

559 560 561 562 563 564 565 566 567
  @override
  List<String> validateSdkWellFormed() => <String>[];

  @override
  Map<String, String> get sdkManagerEnv => <String, String>{};
}

class FakeAndroidSdkVersion extends Fake implements AndroidSdkVersion {
  @override
568
  int sdkLevel = 0;
569 570

  @override
571
  Version buildToolsVersion = Version(0, 0, 0);
572 573 574 575 576 577 578 579

  @override
  String get buildToolsVersionName => '';

  @override
  String get platformName => '';
}

580 581 582 583 584 585 586 587 588 589 590 591 592
class CustomFakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
  CustomFakeOperatingSystemUtils({
    HostPlatform hostPlatform = HostPlatform.linux_x64
  })  : _hostPlatform = hostPlatform;

  final HostPlatform _hostPlatform;

  @override
  String get name => 'Linux';

  @override
  HostPlatform get hostPlatform => _hostPlatform;
}
593 594 595 596 597

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