android_workflow_test.dart 23.3 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/android/java.dart';
10
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:flutter_tools/src/base/logger.dart';
13
import 'package:flutter_tools/src/base/platform.dart';
14 15
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/base/version.dart';
16
import 'package:flutter_tools/src/doctor_validator.dart';
17
import 'package:test/fake.dart';
18

19
import '../../src/common.dart';
20
import '../../src/context.dart';
21
import '../../src/fake_process_manager.dart';
22
import '../../src/fakes.dart';
23

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

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

40 41 42
  testWithoutContext('AndroidWorkflow handles a null AndroidSDK', () {
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
43
      androidSdk: null,
44 45 46 47 48 49 50
    );

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

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

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

64 65
  // Android SDK is actually supported on Linux Arm64 hosts.
  testWithoutContext('Support for Android SDK on Linux Arm Hosts', () {
66 67
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = null;
68 69 70 71 72
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
    );

73 74 75 76
    expect(androidWorkflow.appliesToHostPlatform, isTrue);
    expect(androidWorkflow.canLaunchDevices, isFalse);
    expect(androidWorkflow.canListDevices, isFalse);
    expect(androidWorkflow.canListEmulators, isFalse);
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
  });

  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,
    );

    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,
    );

    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,
    );

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

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

138
    expect(licenseStatus, LicensesAccepted.unknown);
139 140 141
  });

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

157
    expect(licenseStatus, LicensesAccepted.unknown);
158 159 160
  });

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

181
    expect(result, LicensesAccepted.unknown);
182 183 184
  });

  testWithoutContext('licensesAccepted works for all licenses accepted', () async {
185 186 187 188 189 190 191 192 193 194 195
    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,
    ));
196

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

210
    expect(result, LicensesAccepted.all);
211 212
  });

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
  testWithoutContext('licensesAccepted sets environment for finding java', () async {
    final Java java = FakeJava();
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.addCommand(
      FakeCommand(
        command: <String>[sdk.sdkManagerPath!, '--licenses'],
        stdout: 'All SDK package licenses accepted.',
        environment: <String, String>{
          'JAVA_HOME': java.javaHome!,
          'PATH': fileSystem.path.join(java.javaHome!, 'bin'),
        }
      )
    );
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      java: java,
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
    );
    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;

    expect(licenseStatus, LicensesAccepted.all);
  });

242
  testWithoutContext('licensesAccepted works for some licenses accepted', () async {
243 244 245 246 247 248 249 250 251 252 253 254
    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,
    ));
255

256
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
257
      java: FakeJava(),
258 259 260 261 262 263 264
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
265
      androidStudio: FakeAndroidStudio(),
266
    );
267
    final LicensesAccepted result = await licenseValidator.licensesAccepted;
268

269
    expect(result, LicensesAccepted.some);
270 271 272
  });

  testWithoutContext('licensesAccepted works for no licenses accepted', () async {
273 274 275 276 277 278 279 280 281 282 283 284
    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,
    ));
285

286
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
287
      java: FakeJava(),
288 289 290 291 292 293 294
      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
    final LicensesAccepted result = await licenseValidator.licensesAccepted;
298

299
    expect(result, LicensesAccepted.none);
300 301 302
  });

  testWithoutContext('runLicenseManager succeeds for version >= 26', () async {
303 304 305 306 307 308
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.sdkManagerVersion = '26.0.0';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
309
      ],
310
    ));
311

312
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
313
      java: FakeJava(),
314 315 316 317 318 319 320
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
321
      androidStudio: FakeAndroidStudio(),
322
    );
323

324 325 326 327
    expect(await licenseValidator.runLicenseManager(), isTrue);
  });

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

331
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
332
      java: FakeJava(),
333 334 335 336 337 338 339
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
340
      androidStudio: FakeAndroidStudio(),
341 342 343 344
    );

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

346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
  testWithoutContext('runLicenseManager handles broken pipe without ArgumentError', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String exceptionMessage = 'Write failed (OS Error: Broken pipe, errno = 32), port = 0';
    const SocketException exception = SocketException(exceptionMessage);
    // By using a `Socket` generic parameter, the stdin.addStream will return a `Future<Socket>`
    // We are testing that our error handling properly handles futures of this type
    final ThrowingStdin<Socket> fakeStdin = ThrowingStdin<Socket>(exception);
    final FakeCommand licenseCommand = FakeCommand(
      command: <String>[sdk.sdkManagerPath!, '--licenses'],
      stdin: fakeStdin,
    );
    processManager.addCommand(licenseCommand);
    final BufferLogger logger = BufferLogger.test();

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
361
      java: FakeJava(),
362 363 364 365 366 367 368 369 370 371 372
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: logger,
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
    );

    await licenseValidator.runLicenseManager();
373
    expect(logger.traceText, contains(exceptionMessage));
374 375 376
    expect(processManager, hasNoRemainingExpectations);
  });

377
  testWithoutContext('runLicenseManager errors when sdkmanager fails to run', () async {
378 379
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
380

381
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
382
      java: FakeJava(),
383 384 385 386 387 388 389
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
390
      androidStudio: FakeAndroidStudio(),
391 392 393 394
    );

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

396 397 398 399 400 401 402 403 404 405 406 407 408
  testWithoutContext('runLicenseManager errors when sdkmanager exits non-zero', () async {
    const String sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.sdkManagerPath = sdkManagerPath;
    final BufferLogger logger = BufferLogger.test();
    processManager.addCommand(
      const FakeCommand(
        command: <String>[sdkManagerPath, '--licenses'],
        exitCode: 1,
        stderr: 'sdkmanager crash',
      ),
    );

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
409
      java: FakeJava(),
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: logger,
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
    );

    await expectLater(
      licenseValidator.runLicenseManager(),
      throwsToolExit(
        message: 'Android sdkmanager tool was found, but failed to run ($sdkManagerPath): "exited code 1"',
      ),
    );
    expect(processManager, hasNoRemainingExpectations);
    expect(logger.traceText, isEmpty);
    expect(stdio.writtenToStdout, isEmpty);
    expect(stdio.writtenToStderr, contains('sdkmanager crash'));
  });

432 433 434 435
  testWithoutContext('detects license-only SDK installation with cmdline-tools', () async {
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = false
436 437
      ..cmdlineToolsAvailable = true
      ..directory = fileSystem.directory('/foo/bar');
438
    final ValidationResult validationResult = await AndroidValidator(
439
      androidStudio: FakeAndroidStudio(),
440
      androidSdk: sdk,
441
      fileSystem: fileSystem,
442 443 444
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
445
      userMessages: UserMessages(),
446
    ).validate();
447

448
    expect(validationResult.type, ValidationType.partial);
449 450 451 452 453 454 455 456

    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));
457
  });
458

459
  testUsingContext('detects minimum required SDK and buildtools', () async {
460 461 462 463 464 465 466 467 468 469 470 471 472
    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
473
      ..cmdlineToolsAvailable = true
474
    // Test with invalid SDK and build tools
475 476 477 478
      ..directory = fileSystem.directory('/foo/bar')
      ..sdkManagerPath = '/foo/bar/sdkmanager'
      ..latestVersion = sdkVersion;

479
    final String errorMessage = UserMessages().androidSdkBuildToolsOutdated(
480 481
      kAndroidSdkMinVersion,
      kAndroidSdkBuildToolsMinVersion.toString(),
482
      FakePlatform(),
483 484
    );

485
    final AndroidValidator androidValidator = AndroidValidator(
486
      androidStudio: null,
487
      androidSdk: sdk,
488
      fileSystem: fileSystem,
489 490 491
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
492
      userMessages: UserMessages(),
493 494 495
    );

    ValidationResult validationResult = await androidValidator.validate();
496 497 498 499 500 501 502
    expect(validationResult.type, ValidationType.missing);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );

    // Test with valid SDK but invalid build tools
503 504
    sdkVersion.sdkLevel = 29;
    sdkVersion.buildToolsVersion = Version(28, 0, 2);
505

506
    validationResult = await androidValidator.validate();
507 508 509 510 511 512 513
    expect(validationResult.type, ValidationType.missing);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );

    // Test with valid SDK and valid build tools
514
    // Will still be partial because AndroidSdk.findJavaBinary is static :(
515 516
    sdkVersion.sdkLevel = kAndroidSdkMinVersion;
    sdkVersion.buildToolsVersion = kAndroidSdkBuildToolsMinVersion;
517

518
    validationResult = await androidValidator.validate();
519 520 521 522 523
    expect(validationResult.type, ValidationType.partial); // No Java binary
    expect(
      validationResult.messages.any((ValidationMessage message) => message.message == errorMessage),
      isFalse,
    );
524
  });
525

526 527 528 529
  testWithoutContext('detects missing cmdline tools', () async {
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
530 531
      ..cmdlineToolsAvailable = false
      ..directory = fileSystem.directory('/foo/bar');
532 533

    final AndroidValidator androidValidator = AndroidValidator(
534
      androidStudio: null,
535 536 537 538 539 540 541 542
      androidSdk: sdk,
      fileSystem: fileSystem,
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
      userMessages: UserMessages(),
    );

543 544
    final String errorMessage = UserMessages().androidMissingCmdTools;

545 546
    final ValidationResult validationResult = await androidValidator.validate();
    expect(validationResult.type, ValidationType.missing);
547 548 549 550 551 552 553 554

    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);
555 556
  });

557
  testUsingContext('detects minimum required java version', () async {
558 559 560 561 562 563 564 565 566 567 568
    // 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);
569 570

    // Mock a pass through scenario to reach _checkJavaVersion()
571 572 573
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
574
      ..cmdlineToolsAvailable = true
575 576 577 578
      ..directory = fileSystem.directory('/foo/bar')
      ..sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.latestVersion = sdkVersion;

579
    final String errorMessage = UserMessages().androidJavaMinimumVersion(javaVersionText);
580

581 582
    final ValidationResult validationResult = await AndroidValidator(
      androidSdk: sdk,
583
      androidStudio: null,
584
      fileSystem: fileSystem,
585
      logger: logger,
586
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me', AndroidSdk.javaHomeEnvironmentVariable: 'home/java'},
587
      processManager: processManager,
588
      userMessages: UserMessages(),
589
    ).validate();
590 591 592 593 594
    expect(validationResult.type, ValidationType.partial);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );
595 596 597 598 599 600
    expect(
      validationResult.messages.any(
        (ValidationMessage message) => message.message.contains('Unable to locate Android SDK')
      ),
      false,
    );
601
  });
602

603
  testWithoutContext('Mentions `flutter config --android-sdk if user has no AndroidSdk`', () async {
604
    final ValidationResult validationResult = await AndroidValidator(
605 606
      androidSdk: null,
      androidStudio: null,
607
      fileSystem: fileSystem,
608
      logger: logger,
609
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me', AndroidSdk.javaHomeEnvironmentVariable: 'home/java'},
610
      processManager: processManager,
611
      userMessages: UserMessages(),
612
    ).validate();
613

614 615
    expect(
      validationResult.messages.any(
616
        (ValidationMessage message) => message.message.contains('flutter config --android-sdk')
617 618 619 620
      ),
      true,
    );
  });
621
}
622

623 624
class FakeAndroidSdk extends Fake implements AndroidSdk {
  @override
625
  String? sdkManagerPath;
626 627

  @override
628
  String? sdkManagerVersion;
629 630

  @override
631
  String? adbPath;
632 633

  @override
634
  bool licensesAvailable = false;
635 636

  @override
637
  bool platformToolsAvailable = false;
638

639
  @override
640
  bool cmdlineToolsAvailable = false;
641

642
  @override
643
  Directory directory = MemoryFileSystem.test().directory('/foo/bar');
644 645

  @override
646
  AndroidSdkVersion? latestVersion;
647

648
  @override
649
  String? emulatorPath;
650

651 652 653 654 655 656
  @override
  List<String> validateSdkWellFormed() => <String>[];
}

class FakeAndroidSdkVersion extends Fake implements AndroidSdkVersion {
  @override
657
  int sdkLevel = 0;
658 659

  @override
660
  Version buildToolsVersion = Version(0, 0, 0);
661 662 663 664 665 666 667 668

  @override
  String get buildToolsVersionName => '';

  @override
  String get platformName => '';
}

669 670 671 672
class FakeAndroidStudio extends Fake implements AndroidStudio {
  @override
  String get javaPath => 'java';
}
673 674 675 676 677 678 679 680 681 682 683

class ThrowingStdin<T> extends Fake implements IOSink {
  ThrowingStdin(this.exception);

  final Exception exception;

  @override
  Future<dynamic> addStream(Stream<List<int>> stream) {
    return Future<T>.error(exception);
  }
}