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

5 6
// @dart = 2.8

7 8 9 10 11 12
// TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=20210723"
@Tags(<String>['no-shuffle'])

13 14
import 'dart:async';

15
import 'package:args/command_runner.dart';
16
import 'package:fake_async/fake_async.dart';
17
import 'package:file/memory.dart';
18
import 'package:flutter_tools/src/android/android_studio_validator.dart';
19
import 'package:flutter_tools/src/android/android_workflow.dart';
20
import 'package:flutter_tools/src/base/file_system.dart';
21
import 'package:flutter_tools/src/base/logger.dart';
22
import 'package:flutter_tools/src/base/platform.dart';
23
import 'package:flutter_tools/src/base/terminal.dart';
24
import 'package:flutter_tools/src/base/user_messages.dart';
25
import 'package:flutter_tools/src/build_info.dart';
26 27
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
28
import 'package:flutter_tools/src/device.dart';
29
import 'package:flutter_tools/src/doctor.dart';
30
import 'package:flutter_tools/src/doctor_validator.dart';
31
import 'package:flutter_tools/src/features.dart';
32
import 'package:flutter_tools/src/globals.dart' as globals;
33
import 'package:flutter_tools/src/reporting/reporting.dart';
34
import 'package:flutter_tools/src/version.dart';
35 36
import 'package:flutter_tools/src/vscode/vscode.dart';
import 'package:flutter_tools/src/vscode/vscode_validator.dart';
37
import 'package:flutter_tools/src/web/workflow.dart';
38
import 'package:test/fake.dart';
39

40 41
import '../../src/common.dart';
import '../../src/context.dart';
42
import '../../src/fakes.dart';
43
import '../../src/test_flutter_command_runner.dart';
44

45 46 47 48 49
final Platform macPlatform = FakePlatform(
  operatingSystem: 'macos',
  environment: <String, String>{'HOME': '/foo/bar'}
);

50
void main() {
51
  FakeFlutterVersion flutterVersion;
52
  BufferLogger logger;
53
  FakeProcessManager fakeProcessManager;
54 55

  setUp(() {
56
    flutterVersion = FakeFlutterVersion();
57
    logger = BufferLogger.test();
58
    fakeProcessManager = FakeProcessManager.empty();
59 60
  });

61 62 63 64 65 66 67 68 69 70
  testWithoutContext('ValidationMessage equality and hashCode includes contextUrl', () {
    const ValidationMessage messageA = ValidationMessage('ab', contextUrl: 'a');
    const ValidationMessage messageB = ValidationMessage('ab', contextUrl: 'b');

    expect(messageB, isNot(messageA));
    expect(messageB.hashCode, isNot(messageA.hashCode));
    expect(messageA, isNot(messageB));
    expect(messageA.hashCode, isNot(messageB.hashCode));
  });

71
  group('doctor', () {
72 73 74 75 76 77 78 79 80 81 82
    testUsingContext('vs code validator when both installed', () async {
      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithExtension.validate();
      expect(result.type, ValidationType.installed);
      expect(result.statusInfo, 'version 1.2.3');
      expect(result.messages, hasLength(2));

      ValidationMessage message = result.messages
          .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code '));
      expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}');

      message = result.messages
83 84
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, 'Flutter extension version 4.5.6');
85
      expect(message.isError, isFalse);
86
    });
87

88 89
    testUsingContext('No IDE Validator includes expected installation messages', () async {
      final ValidationResult result = await NoIdeValidator().validate();
90
      expect(result.type, ValidationType.notAvailable);
91 92 93 94 95

      expect(
        result.messages.map((ValidationMessage vm) => vm.message),
        UserMessages().noIdeInstallationInfo,
      );
96
    });
97

98 99 100 101 102 103 104 105 106 107 108 109
    testUsingContext('vs code validator when 64bit installed', () async {
      expect(VsCodeValidatorTestTargets.installedWithExtension64bit.title, 'VS Code, 64-bit edition');
      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithExtension64bit.validate();
      expect(result.type, ValidationType.installed);
      expect(result.statusInfo, 'version 1.2.3');
      expect(result.messages, hasLength(2));

      ValidationMessage message = result.messages
          .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code '));
      expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}');

      message = result.messages
110 111
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, 'Flutter extension version 4.5.6');
112
    });
113

114 115
    testUsingContext('vs code validator when extension missing', () async {
      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithoutExtension.validate();
116
      expect(result.type, ValidationType.installed);
117 118 119 120 121 122 123 124
      expect(result.statusInfo, 'version 1.2.3');
      expect(result.messages, hasLength(2));

      ValidationMessage message = result.messages
          .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code '));
      expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}');

      message = result.messages
125
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
126 127 128
      expect(message.message, startsWith('Flutter extension can be installed from'));
      expect(message.contextUrl, 'https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter');
      expect(message.isError, false);
129
    });
130 131 132

    group('device validator', () {
      testWithoutContext('no devices', () async {
133
        final FakeDeviceManager deviceManager = FakeDeviceManager();
134
        final DeviceValidator deviceValidator = DeviceValidator(
135
          deviceManager: deviceManager,
136 137 138 139 140 141 142 143 144 145 146
          userMessages: UserMessages(),
        );
        final ValidationResult result = await deviceValidator.validate();
        expect(result.type, ValidationType.notAvailable);
        expect(result.messages, const <ValidationMessage>[
          ValidationMessage.hint('No devices available'),
        ]);
        expect(result.statusInfo, isNull);
      });

      testWithoutContext('diagnostic message', () async {
147 148
        final FakeDeviceManager deviceManager = FakeDeviceManager()
          ..diagnostics = <String>['Device locked'];
149 150

        final DeviceValidator deviceValidator = DeviceValidator(
151
          deviceManager: deviceManager,
152 153 154 155 156 157 158 159 160 161 162
          userMessages: UserMessages(),
        );
        final ValidationResult result = await deviceValidator.validate();
        expect(result.type, ValidationType.notAvailable);
        expect(result.messages, const <ValidationMessage>[
          ValidationMessage.hint('Device locked'),
        ]);
        expect(result.statusInfo, isNull);
      });

      testWithoutContext('diagnostic message and devices', () async {
163 164 165 166
        final FakeDevice device = FakeDevice();
        final FakeDeviceManager deviceManager = FakeDeviceManager()
          ..devices = <Device>[device]
          ..diagnostics = <String>['Device locked'];
167 168

        final DeviceValidator deviceValidator = DeviceValidator(
169
          deviceManager: deviceManager,
170 171 172 173 174
          userMessages: UserMessages(),
        );
        final ValidationResult result = await deviceValidator.validate();
        expect(result.type, ValidationType.installed);
        expect(result.messages, const <ValidationMessage>[
175
          ValidationMessage('name (mobile) • device-id • android • 1.2.3'),
176 177 178 179 180
          ValidationMessage.hint('Device locked'),
        ]);
        expect(result.statusInfo, '1 available');
      });
    });
181
  });
182

Chris Bracken's avatar
Chris Bracken committed
183
  group('doctor with overridden validators', () {
184
    testUsingContext('validate non-verbose output format for run without issues', () async {
185
      final Doctor doctor = Doctor(logger: logger);
186
      expect(await doctor.diagnose(verbose: false), isTrue);
187
      expect(logger.statusText, equals(
188 189 190 191 192 193 194 195
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[✓] Passing Validator (with statusInfo)\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '[✓] Providing validators is fun (with statusInfo)\n'
              '\n'
              '• No issues found!\n'
      ));
    }, overrides: <Type, Generator>{
196
      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
197 198 199
    });
  });

200
  group('doctor usage params', () {
201
    TestUsage testUsage;
202 203

    setUp(() {
204
      testUsage = TestUsage();
205 206 207
    });

    testUsingContext('contains installed', () async {
208
      final Doctor doctor = Doctor(logger: logger);
209 210
      await doctor.diagnose(verbose: false);

211 212 213
      expect(testUsage.events.length, 3);
      expect(testUsage.events, contains(
        const TestUsageEvent(
214 215
          'doctor-result',
          'PassingValidator',
216 217 218
          label: 'installed',
        ),
      ));
219 220
    }, overrides: <Type, Generator>{
      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
221
      Usage: () => testUsage,
222 223 224
    });

    testUsingContext('contains installed and partial', () async {
225
      await FakePassingDoctor(logger).diagnose(verbose: false);
226

227 228
      expect(testUsage.events, unorderedEquals(<TestUsageEvent>[
        const TestUsageEvent(
229 230
          'doctor-result',
          'PassingValidator',
231 232 233 234 235 236 237 238
          label: 'installed',
        ),
        const TestUsageEvent(
          'doctor-result',
          'PassingValidator',
          label: 'installed',
        ),
        const TestUsageEvent(
239 240
          'doctor-result',
          'PartialValidatorWithHintsOnly',
241 242 243
          label: 'partial',
        ),
        const TestUsageEvent(
244 245
          'doctor-result',
          'PartialValidatorWithErrors',
246 247 248
          label: 'partial',
        ),
      ]));
249
    }, overrides: <Type, Generator>{
250
      Usage: () => testUsage,
251 252 253
    });

    testUsingContext('contains installed, missing and partial', () async {
254
      await FakeDoctor(logger).diagnose(verbose: false);
255

256 257
      expect(testUsage.events, unorderedEquals(<TestUsageEvent>[
        const TestUsageEvent(
258 259
          'doctor-result',
          'PassingValidator',
260 261 262
          label: 'installed',
        ),
        const TestUsageEvent(
263 264
          'doctor-result',
          'MissingValidator',
265 266 267
          label: 'missing',
        ),
        const TestUsageEvent(
268 269
          'doctor-result',
          'NotAvailableValidator',
270 271 272
          label: 'notAvailable',
        ),
        const TestUsageEvent(
273 274
          'doctor-result',
          'PartialValidatorWithHintsOnly',
275 276 277
          label: 'partial',
        ),
        const TestUsageEvent(
278 279
          'doctor-result',
          'PartialValidatorWithErrors',
280 281 282
          label: 'partial',
        ),
      ]));
283
    }, overrides: <Type, Generator>{
284
      Usage: () => testUsage,
285
    });
286 287

    testUsingContext('events for grouped validators are properly decomposed', () async {
288
      await FakeGroupedDoctor(logger).diagnose(verbose: false);
289

290 291
      expect(testUsage.events, unorderedEquals(<TestUsageEvent>[
        const TestUsageEvent(
292 293
          'doctor-result',
          'PassingGroupedValidator',
294 295 296 297 298 299 300 301 302 303 304 305 306
          label: 'installed',
        ),
        const TestUsageEvent(
          'doctor-result',
          'PassingGroupedValidator',
          label: 'installed',
        ),
        const TestUsageEvent(
          'doctor-result',
          'PassingGroupedValidator',
          label: 'installed',
        ),
        const TestUsageEvent(
307 308
          'doctor-result',
          'MissingGroupedValidator',
309 310 311
          label: 'missing',
        ),
      ]));
312
    }, overrides: <Type, Generator>{
313
      Usage: () => testUsage,
314
    });
315 316 317 318 319 320 321 322

    testUsingContext('sending events can be skipped', () async {
      await FakePassingDoctor(logger).diagnose(verbose: false, sendEvent: false);

      expect(testUsage.events, isEmpty);
    }, overrides: <Type, Generator>{
      Usage: () => testUsage,
    });
323
  });
324

325
  group('doctor with fake validators', () {
326
    testUsingContext('validate non-verbose output format for run without issues', () async {
327 328
      expect(await FakeQuietDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
329 330 331 332 333 334 335 336
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[✓] Passing Validator (with statusInfo)\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '• No issues found!\n'
      ));
337
    });
338

339
    testUsingContext('validate non-verbose output format for run with crash', () async {
340 341
      expect(await FakeCrashingDoctor(logger).diagnose(verbose: false), isFalse);
      expect(logger.statusText, equals(
342 343 344 345 346 347
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[✓] Passing Validator (with statusInfo)\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '[☠] Crashing validator (the doctor check crashed)\n'
              '    ✗ Due to an error, the doctor check did not complete. If the error message below is not helpful, '
              'please let us know about this issue at https://github.com/flutter/flutter/issues.\n'
348
              '    ✗ Bad state: fatal error\n'
349 350 351 352 353
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
354
    });
355

356
    testUsingContext('validate verbose output format contains trace for run with crash', () async {
357
      expect(await FakeCrashingDoctor(logger).diagnose(), isFalse);
358
      expect(logger.statusText, contains('#0      CrashingValidator.validate'));
359
    });
360

361 362 363 364 365 366 367 368 369 370
    testUsingContext('validate tool exit when exceeding timeout', () async {
      FakeAsync().run<void>((FakeAsync time) {
        final Doctor doctor = FakeAsyncStuckDoctor(logger);
        doctor.diagnose(verbose: false);
        time.elapse(Doctor.doctorDuration + const Duration(seconds: 1));
        time.flushMicrotasks();
      });

      expect(logger.statusText, contains('Stuck validator that never completes exceeded maximum allowed duration of '));
    });
371 372 373 374

    testUsingContext('validate non-verbose output format for run with an async crash', () async {
      final Completer<void> completer = Completer<void>();
      await FakeAsync().run((FakeAsync time) {
375
        unawaited(FakeAsyncCrashingDoctor(time, logger).diagnose(verbose: false).then((bool r) {
376 377 378 379 380 381 382
          expect(r, isFalse);
          completer.complete(null);
        }));
        time.elapse(const Duration(seconds: 1));
        time.flushMicrotasks();
        return completer.future;
      });
383
      expect(logger.statusText, equals(
384 385 386 387 388 389
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[✓] Passing Validator (with statusInfo)\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '[☠] Async crashing validator (the doctor check crashed)\n'
              '    ✗ Due to an error, the doctor check did not complete. If the error message below is not helpful, '
              'please let us know about this issue at https://github.com/flutter/flutter/issues.\n'
390
              '    ✗ Bad state: fatal error\n'
391 392 393 394 395
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
396
    });
397 398


399
    testUsingContext('validate non-verbose output format when only one category fails', () async {
400 401
      expect(await FakeSinglePassingDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
402 403 404 405 406 407
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[!] Partial Validator with only a Hint\n'
              '    ! There is a hint here\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
408
    });
409 410

    testUsingContext('validate non-verbose output format for a passing run', () async {
411 412
      expect(await FakePassingDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
413 414 415 416 417
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[✓] Passing Validator (with statusInfo)\n'
              '[!] Partial Validator with only a Hint\n'
              '    ! There is a hint here\n'
              '[!] Partial Validator with Errors\n'
418
              '    ✗ An error message indicating partial installation\n'
419 420 421 422 423
              '    ! Maybe a hint will help the user\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 2 categories.\n'
      ));
424
    });
425 426

    testUsingContext('validate non-verbose output format', () async {
427 428
      expect(await FakeDoctor(logger).diagnose(verbose: false), isFalse);
      expect(logger.statusText, equals(
429 430 431 432 433
              'Doctor summary (to see all details, run flutter doctor -v):\n'
              '[✓] Passing Validator (with statusInfo)\n'
              '[✗] Missing Validator\n'
              '    ✗ A useful error message\n'
              '    ! A hint message\n'
434 435 436
              '[!] Not Available Validator\n'
              '    ✗ A useful error message\n'
              '    ! A hint message\n'
437 438 439
              '[!] Partial Validator with only a Hint\n'
              '    ! There is a hint here\n'
              '[!] Partial Validator with Errors\n'
440
              '    ✗ An error message indicating partial installation\n'
441 442
              '    ! Maybe a hint will help the user\n'
              '\n'
443
              '! Doctor found issues in 4 categories.\n'
444
      ));
445
    });
446 447

    testUsingContext('validate verbose output format', () async {
448
      expect(await FakeDoctor(logger).diagnose(), isFalse);
449
      expect(logger.statusText, equals(
450 451 452 453 454 455 456 457 458
              '[✓] Passing Validator (with statusInfo)\n'
              '    • A helpful message\n'
              '    • A second, somewhat longer helpful message\n'
              '\n'
              '[✗] Missing Validator\n'
              '    ✗ A useful error message\n'
              '    • A message that is not an error\n'
              '    ! A hint message\n'
              '\n'
459 460 461 462 463
              '[!] Not Available Validator\n'
              '    ✗ A useful error message\n'
              '    • A message that is not an error\n'
              '    ! A hint message\n'
              '\n'
464 465 466 467 468
              '[!] Partial Validator with only a Hint\n'
              '    ! There is a hint here\n'
              '    • But there is no error\n'
              '\n'
              '[!] Partial Validator with Errors\n'
469
              '    ✗ An error message indicating partial installation\n'
470 471 472
              '    ! Maybe a hint will help the user\n'
              '    • An extra message with some verbose details\n'
              '\n'
473
              '! Doctor found issues in 4 categories.\n'
474
      ));
475
    });
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547

    testUsingContext('validate PII can be hidden', () async {
      expect(await FakePiiDoctor(logger).diagnose(showPii: false), isTrue);
      expect(logger.statusText, equals(
        '[✓] PII Validator\n'
        '    • Does not contain PII\n'
        '\n'
        '• No issues found!\n'
      ));
      logger.clear();
      // PII shown.
      expect(await FakePiiDoctor(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
          '[✓] PII Validator\n'
              '    • Contains PII path/to/username\n'
              '\n'
              '• No issues found!\n'
      ));
    });
  });

  group('doctor diagnosis wrapper', () {
    TestUsage testUsage;
    BufferLogger logger;

    setUp(() {
      testUsage = TestUsage();
      logger = BufferLogger.test();
    });

    testUsingContext('PII separated, events only sent once', () async {
      final Doctor fakeDoctor = FakePiiDoctor(logger);
      final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor);
      const String expectedPiiText = '[✓] PII Validator\n'
          '    • Contains PII path/to/username\n'
          '\n'
          '• No issues found!\n';
      const String expectedPiiStrippedText =
          '[✓] PII Validator\n'
          '    • Does not contain PII\n'
          '\n'
          '• No issues found!\n';

      // Run each multiple times to make sure the logger buffer is being cleared,
      // and that events are only sent once.
      expect(await doctorText.text, expectedPiiText);
      expect(await doctorText.text, expectedPiiText);

      expect(await doctorText.piiStrippedText, expectedPiiStrippedText);
      expect(await doctorText.piiStrippedText, expectedPiiStrippedText);

      // Only one event sent.
      expect(testUsage.events, <TestUsageEvent>[
        const TestUsageEvent(
          'doctor-result',
          'PiiValidator',
          label: 'installed',
        ),
      ]);
    }, overrides: <Type, Generator>{
      Usage: () => testUsage,
    });

    testUsingContext('without PII has same text and PII-stripped text', () async {
      final Doctor fakeDoctor = FakePassingDoctor(logger);
      final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor);
      final String piiText = await doctorText.text;
      expect(piiText, isNotEmpty);
      expect(piiText, await doctorText.piiStrippedText);
    }, overrides: <Type, Generator>{
      Usage: () => testUsage,
    });
548
  });
549

550
  testUsingContext('validate non-verbose output wrapping', () async {
551 552 553 554 555
    final BufferLogger wrapLogger = BufferLogger.test(
      outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30),
    );
    expect(await FakeDoctor(wrapLogger).diagnose(verbose: false), isFalse);
    expect(wrapLogger.statusText, equals(
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
        'Doctor summary (to see all\n'
        'details, run flutter doctor\n'
        '-v):\n'
        '[✓] Passing Validator (with\n'
        '    statusInfo)\n'
        '[✗] Missing Validator\n'
        '    ✗ A useful error message\n'
        '    ! A hint message\n'
        '[!] Not Available Validator\n'
        '    ✗ A useful error message\n'
        '    ! A hint message\n'
        '[!] Partial Validator with\n'
        '    only a Hint\n'
        '    ! There is a hint here\n'
        '[!] Partial Validator with\n'
        '    Errors\n'
        '    ✗ An error message\n'
        '      indicating partial\n'
        '      installation\n'
        '    ! Maybe a hint will help\n'
        '      the user\n'
        '\n'
        '! Doctor found issues in 4\n'
        '  categories.\n'
    ));
  });

  testUsingContext('validate verbose output wrapping', () async {
584 585 586
    final BufferLogger wrapLogger = BufferLogger.test(
      outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30),
    );
587
    expect(await FakeDoctor(wrapLogger).diagnose(), isFalse);
588
    expect(wrapLogger.statusText, equals(
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
        '[✓] Passing Validator (with\n'
        '    statusInfo)\n'
        '    • A helpful message\n'
        '    • A second, somewhat\n'
        '      longer helpful message\n'
        '\n'
        '[✗] Missing Validator\n'
        '    ✗ A useful error message\n'
        '    • A message that is not an\n'
        '      error\n'
        '    ! A hint message\n'
        '\n'
        '[!] Not Available Validator\n'
        '    ✗ A useful error message\n'
        '    • A message that is not an\n'
        '      error\n'
        '    ! A hint message\n'
        '\n'
        '[!] Partial Validator with\n'
        '    only a Hint\n'
        '    ! There is a hint here\n'
        '    • But there is no error\n'
        '\n'
        '[!] Partial Validator with\n'
        '    Errors\n'
        '    ✗ An error message\n'
        '      indicating partial\n'
        '      installation\n'
        '    ! Maybe a hint will help\n'
        '      the user\n'
        '    • An extra message with\n'
        '      some verbose details\n'
        '\n'
        '! Doctor found issues in 4\n'
        '  categories.\n'
    ));
  });

627 628
  group('doctor with grouped validators', () {
    testUsingContext('validate diagnose combines validator output', () async {
629 630
      expect(await FakeGroupedDoctor(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
631 632 633 634 635 636 637 638 639 640
              '[✓] Category 1\n'
              '    • A helpful message\n'
              '    • A helpful message\n'
              '\n'
              '[!] Category 2\n'
              '    • A helpful message\n'
              '    ✗ A useful error message\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
641
    });
642

643 644
    testUsingContext('validate merging assigns statusInfo and title', () async {
      // There are two subvalidators. Only the second contains statusInfo.
645 646
      expect(await FakeGroupedDoctorWithStatus(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
647 648 649
              '[✓] First validator title (A status message)\n'
              '    • A helpful message\n'
              '    • A different message\n'
650
              '\n'
651
              '• No issues found!\n'
652
      ));
653
    });
654 655
  });

656 657 658 659
  group('grouped validator merging results', () {
    final PassingGroupedValidator installed = PassingGroupedValidator('Category');
    final PartialGroupedValidator partial = PartialGroupedValidator('Category');
    final MissingGroupedValidator missing = MissingGroupedValidator('Category');
660 661

    testUsingContext('validate installed + installed = installed', () async {
662 663
      expect(await FakeSmallGroupDoctor(logger, installed, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[✓]'));
664
    });
665 666

    testUsingContext('validate installed + partial = partial', () async {
667 668
      expect(await FakeSmallGroupDoctor(logger, installed, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
669
    });
670 671

    testUsingContext('validate installed + missing = partial', () async {
672 673
      expect(await FakeSmallGroupDoctor(logger, installed, missing).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
674
    });
675 676

    testUsingContext('validate partial + installed = partial', () async {
677 678
      expect(await FakeSmallGroupDoctor(logger, partial, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
679
    });
680 681

    testUsingContext('validate partial + partial = partial', () async {
682 683
      expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
684
    });
685 686

    testUsingContext('validate partial + missing = partial', () async {
687 688
      expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
689
    });
690 691

    testUsingContext('validate missing + installed = partial', () async {
692 693
      expect(await FakeSmallGroupDoctor(logger, missing, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
694
    });
695 696

    testUsingContext('validate missing + partial = partial', () async {
697 698
      expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
699
    });
700 701

    testUsingContext('validate missing + missing = missing', () async {
702 703
      expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(), isFalse);
      expect(logger.statusText, startsWith('[✗]'));
704
    });
705
  });
706 707

  testUsingContext('WebWorkflow is a part of validator workflows if enabled', () async {
708 709
    expect(DoctorValidatorsProvider.defaultInstance.workflows,
      contains(isA<WebWorkflow>()));
710 711
  }, overrides: <Type, Generator>{
    FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
712
    FileSystem: () => MemoryFileSystem.test(),
713
    ProcessManager: () => fakeProcessManager,
714
  });
715 716 717 718 719 720 721 722 723

  testUsingContext('Fetches tags to get the right version', () async {
    Cache.disableLocking();

    final DoctorCommand doctorCommand = DoctorCommand();
    final CommandRunner<void> commandRunner = createTestCommandRunner(doctorCommand);

    await commandRunner.run(<String>['doctor']);

724
    expect(flutterVersion.didFetchTagsAndUpdate, true);
725 726 727 728
    Cache.enableLocking();
  }, overrides: <Type, Generator>{
    ProcessManager: () => FakeProcessManager.any(),
    FileSystem: () => MemoryFileSystem.test(),
729
    FlutterVersion: () => flutterVersion,
730 731
    Doctor: () => NoOpDoctor(),
  }, initializeFlutterRoot: false);
732 733 734 735 736 737 738

  testUsingContext('If android workflow is disabled, AndroidStudio validator is not included', () {
    expect(DoctorValidatorsProvider.defaultInstance.validators, isNot(contains(isA<AndroidStudioValidator>())));
    expect(DoctorValidatorsProvider.defaultInstance.validators, isNot(contains(isA<NoAndroidStudioValidator>())));
  }, overrides: <Type, Generator>{
    FeatureFlags: () => TestFeatureFlags(isAndroidEnabled: false),
  });
739 740 741 742 743 744 745 746 747 748 749 750 751
}

class NoOpDoctor implements Doctor {
  @override
  bool get canLaunchAnything => true;

  @override
  bool get canListAnything => true;

  @override
  Future<bool> checkRemoteArtifacts(String engineRevision) async => true;

  @override
752 753 754 755 756
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
    AndroidLicenseValidator androidLicenseValidator,
757 758 759
    bool showPii = true,
    List<ValidatorTask> startedValidatorTasks,
    bool sendEvent = true,
760
  }) async => true;
761 762 763 764 765

  @override
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[];

  @override
766
  Future<void> summary() async { }
767 768 769 770 771 772

  @override
  List<DoctorValidator> get validators => <DoctorValidator>[];

  @override
  List<Workflow> get workflows => <Workflow>[];
773 774
}

775 776 777 778 779
class PassingValidator extends DoctorValidator {
  PassingValidator(String name) : super(name);

  @override
  Future<ValidationResult> validate() async {
780
    const List<ValidationMessage> messages = <ValidationMessage>[
781 782 783
      ValidationMessage('A helpful message'),
      ValidationMessage('A second, somewhat longer helpful message'),
    ];
784
    return const ValidationResult(ValidationType.installed, messages, statusInfo: 'with statusInfo');
785 786 787
  }
}

788 789 790 791 792 793 794 795 796 797 798 799
class PiiValidator extends DoctorValidator {
  PiiValidator() : super('PII Validator');

  @override
  Future<ValidationResult> validate() async {
    const List<ValidationMessage> messages = <ValidationMessage>[
      ValidationMessage('Contains PII path/to/username', piiStrippedMessage: 'Does not contain PII'),
    ];
    return const ValidationResult(ValidationType.installed, messages);
  }
}

800
class MissingValidator extends DoctorValidator {
801
  MissingValidator() : super('Missing Validator');
802 803 804

  @override
  Future<ValidationResult> validate() async {
805
    const List<ValidationMessage> messages = <ValidationMessage>[
806 807 808 809
      ValidationMessage.error('A useful error message'),
      ValidationMessage('A message that is not an error'),
      ValidationMessage.hint('A hint message'),
    ];
810
    return const ValidationResult(ValidationType.missing, messages);
811 812 813
  }
}

814
class NotAvailableValidator extends DoctorValidator {
815
  NotAvailableValidator() : super('Not Available Validator');
816 817 818

  @override
  Future<ValidationResult> validate() async {
819
    const List<ValidationMessage> messages = <ValidationMessage>[
820 821 822 823
      ValidationMessage.error('A useful error message'),
      ValidationMessage('A message that is not an error'),
      ValidationMessage.hint('A hint message'),
    ];
824
    return const ValidationResult(ValidationType.notAvailable, messages);
825 826 827
  }
}

828 829 830 831 832 833 834 835 836 837 838 839
class StuckValidator extends DoctorValidator {
  StuckValidator() : super('Stuck validator that never completes');

  @override
  Future<ValidationResult> validate() {
    final Completer<ValidationResult> completer = Completer<ValidationResult>();

    // This future will never complete
    return completer.future;
  }
}

840 841 842 843 844
class PartialValidatorWithErrors extends DoctorValidator {
  PartialValidatorWithErrors() : super('Partial Validator with Errors');

  @override
  Future<ValidationResult> validate() async {
845
    const List<ValidationMessage> messages = <ValidationMessage>[
846 847 848 849
      ValidationMessage.error('An error message indicating partial installation'),
      ValidationMessage.hint('Maybe a hint will help the user'),
      ValidationMessage('An extra message with some verbose details'),
    ];
850
    return const ValidationResult(ValidationType.partial, messages);
851 852 853 854 855 856 857 858
  }
}

class PartialValidatorWithHintsOnly extends DoctorValidator {
  PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint');

  @override
  Future<ValidationResult> validate() async {
859
    const List<ValidationMessage> messages = <ValidationMessage>[
860 861 862
      ValidationMessage.hint('There is a hint here'),
      ValidationMessage('But there is no error'),
    ];
863
    return const ValidationResult(ValidationType.partial, messages);
864 865 866
  }
}

867 868 869 870 871
class CrashingValidator extends DoctorValidator {
  CrashingValidator() : super('Crashing validator');

  @override
  Future<ValidationResult> validate() async {
872
    throw StateError('fatal error');
873 874 875
  }
}

876 877 878 879 880 881 882 883
class AsyncCrashingValidator extends DoctorValidator {
  AsyncCrashingValidator(this._time) : super('Async crashing validator');

  final FakeAsync _time;

  @override
  Future<ValidationResult> validate() {
    const Duration delay = Duration(seconds: 1);
884 885
    final Future<ValidationResult> result = Future<ValidationResult>.delayed(delay)
      .then((_) {
886
        throw StateError('fatal error');
887
      });
888 889 890 891 892 893
    _time.elapse(const Duration(seconds: 1));
    _time.flushMicrotasks();
    return result;
  }
}

894 895
/// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor {
896 897
  FakeDoctor(Logger logger) : super(logger: logger);

898 899 900 901
  List<DoctorValidator> _validators;

  @override
  List<DoctorValidator> get validators {
902 903 904 905 906 907 908
    return _validators ??= <DoctorValidator>[
      PassingValidator('Passing Validator'),
      MissingValidator(),
      NotAvailableValidator(),
      PartialValidatorWithHintsOnly(),
      PartialValidatorWithErrors(),
    ];
909 910 911 912 913
  }
}

/// A doctor that should pass, but still has issues in some categories.
class FakePassingDoctor extends Doctor {
914 915
  FakePassingDoctor(Logger logger) : super(logger: logger);

916 917 918
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
919 920 921 922 923 924
    return _validators ??= <DoctorValidator>[
      PassingValidator('Passing Validator'),
      PartialValidatorWithHintsOnly(),
      PartialValidatorWithErrors(),
      PassingValidator('Another Passing Validator'),
    ];
925 926 927 928 929 930
  }
}

/// A doctor that should pass, but still has 1 issue to test the singular of
/// categories.
class FakeSinglePassingDoctor extends Doctor {
931 932
  FakeSinglePassingDoctor(Logger logger) : super(logger: logger);

933 934 935
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
936 937 938
    return _validators ??= <DoctorValidator>[
      PartialValidatorWithHintsOnly(),
    ];
939 940 941 942 943
  }
}

/// A doctor that passes and has no issues anywhere.
class FakeQuietDoctor extends Doctor {
944 945
  FakeQuietDoctor(Logger logger) : super(logger: logger);

946 947 948
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
949 950 951 952 953 954
    return _validators ??= <DoctorValidator>[
      PassingValidator('Passing Validator'),
      PassingValidator('Another Passing Validator'),
      PassingValidator('Validators are fun'),
      PassingValidator('Four score and seven validators ago'),
    ];
955
  }
956 957
}

958 959 960 961 962 963 964 965 966 967 968 969 970
/// A doctor that passes and contains PII that can be hidden.
class FakePiiDoctor extends Doctor {
  FakePiiDoctor(Logger logger) : super(logger: logger);

  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
    return _validators ??= <DoctorValidator>[
      PiiValidator(),
    ];
  }
}

971 972
/// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor {
973 974
  FakeCrashingDoctor(Logger logger) : super(logger: logger);

975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
    if (_validators == null) {
      _validators = <DoctorValidator>[];
      _validators.add(PassingValidator('Passing Validator'));
      _validators.add(PassingValidator('Another Passing Validator'));
      _validators.add(CrashingValidator());
      _validators.add(PassingValidator('Validators are fun'));
      _validators.add(PassingValidator('Four score and seven validators ago'));
    }
    return _validators;
  }
}

990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
/// A doctor with a validator that will never finish.
class FakeAsyncStuckDoctor extends Doctor {
  FakeAsyncStuckDoctor(Logger logger) : super(logger: logger);

  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
    if (_validators == null) {
      _validators = <DoctorValidator>[];
      _validators.add(PassingValidator('Passing Validator'));
      _validators.add(PassingValidator('Another Passing Validator'));
      _validators.add(StuckValidator());
      _validators.add(PassingValidator('Validators are fun'));
      _validators.add(PassingValidator('Four score and seven validators ago'));
    }
    return _validators;
  }
}

1009 1010
/// A doctor with a validator that throws an exception.
class FakeAsyncCrashingDoctor extends Doctor {
1011
  FakeAsyncCrashingDoctor(this._time, Logger logger) : super(logger: logger);
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029

  final FakeAsync _time;

  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
    if (_validators == null) {
      _validators = <DoctorValidator>[];
      _validators.add(PassingValidator('Passing Validator'));
      _validators.add(PassingValidator('Another Passing Validator'));
      _validators.add(AsyncCrashingValidator(_time));
      _validators.add(PassingValidator('Validators are fun'));
      _validators.add(PassingValidator('Four score and seven validators ago'));
    }
    return _validators;
  }
}

1030 1031 1032 1033 1034 1035
/// A DoctorValidatorsProvider that overrides the default validators without
/// overriding the doctor.
class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
  @override
  List<DoctorValidator> get validators {
    return <DoctorValidator>[
1036 1037
      PassingValidator('Passing Validator'),
      PassingValidator('Another Passing Validator'),
1038
      PassingValidator('Providing validators is fun'),
1039 1040
    ];
  }
1041 1042 1043 1044 1045 1046

  @override
  List<Workflow> get workflows => <Workflow>[];
}

class PassingGroupedValidator extends DoctorValidator {
1047
  PassingGroupedValidator(String name) : super(name);
1048 1049 1050

  @override
  Future<ValidationResult> validate() async {
1051
    const List<ValidationMessage> messages = <ValidationMessage>[
1052 1053
      ValidationMessage('A helpful message'),
    ];
1054
    return const ValidationResult(ValidationType.installed, messages);
1055 1056 1057 1058
  }
}

class MissingGroupedValidator extends DoctorValidator {
1059
  MissingGroupedValidator(String name) : super(name);
1060 1061 1062

  @override
  Future<ValidationResult> validate() async {
1063
    const List<ValidationMessage> messages = <ValidationMessage>[
1064 1065
      ValidationMessage.error('A useful error message'),
    ];
1066
    return const ValidationResult(ValidationType.missing, messages);
1067 1068 1069 1070
  }
}

class PartialGroupedValidator extends DoctorValidator {
1071
  PartialGroupedValidator(String name) : super(name);
1072 1073 1074

  @override
  Future<ValidationResult> validate() async {
1075
    const List<ValidationMessage> messages = <ValidationMessage>[
1076 1077
      ValidationMessage.error('An error message for partial installation'),
    ];
1078
    return const ValidationResult(ValidationType.partial, messages);
1079 1080 1081
  }
}

1082 1083 1084 1085 1086
class PassingGroupedValidatorWithStatus extends DoctorValidator {
  PassingGroupedValidatorWithStatus(String name) : super(name);

  @override
  Future<ValidationResult> validate() async {
1087
    const List<ValidationMessage> messages = <ValidationMessage>[
1088 1089
      ValidationMessage('A different message'),
    ];
1090
    return const ValidationResult(ValidationType.installed, messages, statusInfo: 'A status message');
1091 1092 1093 1094
  }
}

/// A doctor that has two groups of two validators each.
1095
class FakeGroupedDoctor extends Doctor {
1096 1097
  FakeGroupedDoctor(Logger logger) : super(logger: logger);

1098 1099 1100
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
1101 1102
    return _validators ??= <DoctorValidator>[
      GroupedValidator(<DoctorValidator>[
1103
        PassingGroupedValidator('Category 1'),
1104
        PassingGroupedValidator('Category 1'),
1105 1106
      ]),
      GroupedValidator(<DoctorValidator>[
1107
        PassingGroupedValidator('Category 2'),
1108
        MissingGroupedValidator('Category 2'),
1109 1110
      ]),
    ];
1111 1112 1113
  }
}

1114
class FakeGroupedDoctorWithStatus extends Doctor {
1115 1116
  FakeGroupedDoctorWithStatus(Logger logger) : super(logger: logger);

1117 1118 1119
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
1120
    return _validators ??= <DoctorValidator>[
1121 1122 1123
      GroupedValidator(<DoctorValidator>[
        PassingGroupedValidator('First validator title'),
        PassingGroupedValidatorWithStatus('Second validator title'),
1124 1125
      ]),
    ];
1126 1127 1128
  }
}

1129 1130 1131
/// A doctor that takes any two validators. Used to check behavior when
/// merging ValidationTypes (installed, missing, partial).
class FakeSmallGroupDoctor extends Doctor {
1132
  FakeSmallGroupDoctor(Logger logger, DoctorValidator val1, DoctorValidator val2) : super(logger: logger) {
1133
    _validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])];
1134
  }
1135 1136 1137

  List<DoctorValidator> _validators;

1138 1139
  @override
  List<DoctorValidator> get validators => _validators;
1140 1141
}

1142
class VsCodeValidatorTestTargets extends VsCodeValidator {
1143
  VsCodeValidatorTestTargets._(String installDirectory, String extensionDirectory, {String edition})
1144
    : super(VsCode.fromDirectory(installDirectory, extensionDirectory, edition: edition, fileSystem: globals.fs));
1145 1146

  static VsCodeValidatorTestTargets get installedWithExtension =>
1147
      VsCodeValidatorTestTargets._(validInstall, validExtensions);
1148

1149
  static VsCodeValidatorTestTargets get installedWithExtension64bit =>
1150
      VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition');
1151

1152
  static VsCodeValidatorTestTargets get installedWithoutExtension =>
1153
      VsCodeValidatorTestTargets._(validInstall, missingExtensions);
1154

1155 1156 1157
  static final String validInstall = globals.fs.path.join('test', 'data', 'vscode', 'application');
  static final String validExtensions = globals.fs.path.join('test', 'data', 'vscode', 'extensions');
  static final String missingExtensions = globals.fs.path.join('test', 'data', 'vscode', 'notExtensions');
1158
}
1159

1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
class FakeDeviceManager extends Fake implements DeviceManager {
  List<String> diagnostics = <String>[];
  List<Device> devices = <Device>[];

  @override
  Future<List<Device>> getAllConnectedDevices() async => devices;

  @override
  Future<List<String>> getDeviceDiagnostics() async => diagnostics;
}

1171 1172 1173
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197
class FakeDevice extends Fake implements Device {
  @override
  String get name => 'name';

  @override
  String get id => 'device-id';

  @override
  Category get category => Category.mobile;

  @override
  bool isSupported() => true;

  @override
  Future<bool> get isLocalEmulator async => false;

  @override
  Future<String> get targetPlatformDisplayName async => 'android';

  @override
  Future<String> get sdkNameAndVersion async => '1.2.3';

  @override
  Future<TargetPlatform> get targetPlatform =>  Future<TargetPlatform>.value(TargetPlatform.android);
1198
}