doctor_test.dart 44.1 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
import 'dart:async';

7
import 'package:args/command_runner.dart';
8
import 'package:fake_async/fake_async.dart';
9
import 'package:file/memory.dart';
10
import 'package:flutter_tools/src/android/android_studio_validator.dart';
11
import 'package:flutter_tools/src/android/android_workflow.dart';
12
import 'package:flutter_tools/src/base/file_system.dart';
13
import 'package:flutter_tools/src/base/logger.dart';
14
import 'package:flutter_tools/src/base/platform.dart';
15
import 'package:flutter_tools/src/base/terminal.dart';
16
import 'package:flutter_tools/src/base/user_messages.dart';
17
import 'package:flutter_tools/src/build_info.dart';
18 19
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
20
import 'package:flutter_tools/src/custom_devices/custom_device_workflow.dart';
21
import 'package:flutter_tools/src/device.dart';
22
import 'package:flutter_tools/src/doctor.dart';
23
import 'package:flutter_tools/src/doctor_validator.dart';
24
import 'package:flutter_tools/src/globals.dart' as globals;
25
import 'package:flutter_tools/src/reporting/reporting.dart';
26
import 'package:flutter_tools/src/version.dart';
27 28
import 'package:flutter_tools/src/vscode/vscode.dart';
import 'package:flutter_tools/src/vscode/vscode_validator.dart';
29
import 'package:flutter_tools/src/web/workflow.dart';
30
import 'package:test/fake.dart';
31

32 33
import '../../src/common.dart';
import '../../src/context.dart';
34
import '../../src/fakes.dart';
35
import '../../src/test_flutter_command_runner.dart';
36 37

void main() {
38 39 40
  late FakeFlutterVersion flutterVersion;
  late BufferLogger logger;
  late FakeProcessManager fakeProcessManager;
41 42

  setUp(() {
43
    flutterVersion = FakeFlutterVersion();
44
    logger = BufferLogger.test();
45
    fakeProcessManager = FakeProcessManager.empty();
46 47
  });

48 49 50 51 52 53 54 55 56 57
  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));
  });

58
  group('doctor', () {
59 60 61 62 63 64 65 66 67 68 69
    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
70 71
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, 'Flutter extension version 4.5.6');
72
      expect(message.isError, isFalse);
73
    });
74

75 76
    testUsingContext('No IDE Validator includes expected installation messages', () async {
      final ValidationResult result = await NoIdeValidator().validate();
77
      expect(result.type, ValidationType.notAvailable);
78 79 80 81 82

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

85 86 87 88 89 90 91 92 93 94 95 96
    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
97 98
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, 'Flutter extension version 4.5.6');
99
    });
100

101 102
    testUsingContext('vs code validator when extension missing', () async {
      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithoutExtension.validate();
103
      expect(result.type, ValidationType.installed);
104 105 106 107 108 109 110 111
      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
112
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
113 114 115
      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);
116
    });
117 118 119

    group('device validator', () {
      testWithoutContext('no devices', () async {
120
        final FakeDeviceManager deviceManager = FakeDeviceManager();
121
        final DeviceValidator deviceValidator = DeviceValidator(
122
          deviceManager: deviceManager,
123 124 125 126 127 128 129 130 131 132 133
          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 {
134 135
        final FakeDeviceManager deviceManager = FakeDeviceManager()
          ..diagnostics = <String>['Device locked'];
136 137

        final DeviceValidator deviceValidator = DeviceValidator(
138
          deviceManager: deviceManager,
139 140 141 142 143 144 145 146 147 148 149
          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 {
150 151 152 153
        final FakeDevice device = FakeDevice();
        final FakeDeviceManager deviceManager = FakeDeviceManager()
          ..devices = <Device>[device]
          ..diagnostics = <String>['Device locked'];
154 155

        final DeviceValidator deviceValidator = DeviceValidator(
156
          deviceManager: deviceManager,
157 158 159 160 161
          userMessages: UserMessages(),
        );
        final ValidationResult result = await deviceValidator.validate();
        expect(result.type, ValidationType.installed);
        expect(result.messages, const <ValidationMessage>[
162
          ValidationMessage('name (mobile) • device-id • android • 1.2.3'),
163 164 165 166 167
          ValidationMessage.hint('Device locked'),
        ]);
        expect(result.statusInfo, '1 available');
      });
    });
168
  });
169

Chris Bracken's avatar
Chris Bracken committed
170
  group('doctor with overridden validators', () {
171
    testUsingContext('validate non-verbose output format for run without issues', () async {
172
      final Doctor doctor = Doctor(logger: logger);
173
      expect(await doctor.diagnose(verbose: false), isTrue);
174
      expect(logger.statusText, equals(
175 176 177 178 179 180 181 182
              '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>{
183
      AnsiTerminal: () => FakeTerminal(),
184
      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
185 186 187
    });
  });

188
  group('doctor usage params', () {
189
    late TestUsage testUsage;
190 191

    setUp(() {
192
      testUsage = TestUsage();
193 194 195
    });

    testUsingContext('contains installed', () async {
196
      final Doctor doctor = Doctor(logger: logger);
197 198
      await doctor.diagnose(verbose: false);

199 200 201
      expect(testUsage.events.length, 3);
      expect(testUsage.events, contains(
        const TestUsageEvent(
202 203
          'doctor-result',
          'PassingValidator',
204 205 206
          label: 'installed',
        ),
      ));
207 208
    }, overrides: <Type, Generator>{
      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
209
      Usage: () => testUsage,
210 211 212
    });

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

215 216
      expect(testUsage.events, unorderedEquals(<TestUsageEvent>[
        const TestUsageEvent(
217 218
          'doctor-result',
          'PassingValidator',
219 220 221 222 223 224 225 226
          label: 'installed',
        ),
        const TestUsageEvent(
          'doctor-result',
          'PassingValidator',
          label: 'installed',
        ),
        const TestUsageEvent(
227 228
          'doctor-result',
          'PartialValidatorWithHintsOnly',
229 230 231
          label: 'partial',
        ),
        const TestUsageEvent(
232 233
          'doctor-result',
          'PartialValidatorWithErrors',
234 235 236
          label: 'partial',
        ),
      ]));
237
    }, overrides: <Type, Generator>{
238
      Usage: () => testUsage,
239 240 241
    });

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

244 245
      expect(testUsage.events, unorderedEquals(<TestUsageEvent>[
        const TestUsageEvent(
246 247
          'doctor-result',
          'PassingValidator',
248 249 250
          label: 'installed',
        ),
        const TestUsageEvent(
251 252
          'doctor-result',
          'MissingValidator',
253 254 255
          label: 'missing',
        ),
        const TestUsageEvent(
256 257
          'doctor-result',
          'NotAvailableValidator',
258 259 260
          label: 'notAvailable',
        ),
        const TestUsageEvent(
261 262
          'doctor-result',
          'PartialValidatorWithHintsOnly',
263 264 265
          label: 'partial',
        ),
        const TestUsageEvent(
266 267
          'doctor-result',
          'PartialValidatorWithErrors',
268 269 270
          label: 'partial',
        ),
      ]));
271
    }, overrides: <Type, Generator>{
272
      Usage: () => testUsage,
273
    });
274 275

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

278 279
      expect(testUsage.events, unorderedEquals(<TestUsageEvent>[
        const TestUsageEvent(
280 281
          'doctor-result',
          'PassingGroupedValidator',
282 283 284 285 286 287 288 289 290 291 292 293 294
          label: 'installed',
        ),
        const TestUsageEvent(
          'doctor-result',
          'PassingGroupedValidator',
          label: 'installed',
        ),
        const TestUsageEvent(
          'doctor-result',
          'PassingGroupedValidator',
          label: 'installed',
        ),
        const TestUsageEvent(
295 296
          'doctor-result',
          'MissingGroupedValidator',
297 298 299
          label: 'missing',
        ),
      ]));
300
    }, overrides: <Type, Generator>{
301
      Usage: () => testUsage,
302
    });
303 304 305 306 307 308 309 310

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

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

313
  group('doctor with fake validators', () {
314
    testUsingContext('validate non-verbose output format for run without issues', () async {
315 316
      expect(await FakeQuietDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
317 318 319 320 321 322 323 324
              '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'
      ));
325 326
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
327
    });
328

329
    testUsingContext('validate non-verbose output format for run with crash', () async {
330 331
      expect(await FakeCrashingDoctor(logger).diagnose(verbose: false), isFalse);
      expect(logger.statusText, equals(
332 333 334 335 336 337
              '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'
338
              '    ✗ Bad state: fatal error\n'
339 340 341 342 343
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
344 345
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
346
    });
347

348
    testUsingContext('validate verbose output format contains trace for run with crash', () async {
349
      expect(await FakeCrashingDoctor(logger).diagnose(), isFalse);
350
      expect(logger.statusText, contains('#0      CrashingValidator.validate'));
351
    });
352

353 354 355 356
    testUsingContext('validate tool exit when exceeding timeout', () async {
      FakeAsync().run<void>((FakeAsync time) {
        final Doctor doctor = FakeAsyncStuckDoctor(logger);
        doctor.diagnose(verbose: false);
357
        time.elapse(const Duration(minutes: 5));
358 359 360 361
        time.flushMicrotasks();
      });

      expect(logger.statusText, contains('Stuck validator that never completes exceeded maximum allowed duration of '));
362 363
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
364
    });
365 366 367 368

    testUsingContext('validate non-verbose output format for run with an async crash', () async {
      final Completer<void> completer = Completer<void>();
      await FakeAsync().run((FakeAsync time) {
369
        unawaited(FakeAsyncCrashingDoctor(time, logger).diagnose(verbose: false).then((bool r) {
370 371 372 373 374 375 376
          expect(r, isFalse);
          completer.complete(null);
        }));
        time.elapse(const Duration(seconds: 1));
        time.flushMicrotasks();
        return completer.future;
      });
377
      expect(logger.statusText, equals(
378 379 380 381 382 383
              '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'
384
              '    ✗ Bad state: fatal error\n'
385 386 387 388 389
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
390 391
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
392
    });
393 394


395
    testUsingContext('validate non-verbose output format when only one category fails', () async {
396 397
      expect(await FakeSinglePassingDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
398 399 400 401 402 403
              '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'
      ));
404 405
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
406
    });
407 408

    testUsingContext('validate non-verbose output format for a passing run', () async {
409 410
      expect(await FakePassingDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
411 412 413 414 415
              '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'
416
              '    ✗ An error message indicating partial installation\n'
417 418 419 420 421
              '    ! Maybe a hint will help the user\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 2 categories.\n'
      ));
422 423
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
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
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
447
    });
448 449

    testUsingContext('validate verbose output format', () async {
450
      expect(await FakeDoctor(logger).diagnose(), isFalse);
451
      expect(logger.statusText, equals(
452 453 454 455 456 457 458 459 460
              '[✓] 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'
461 462 463 464 465
              '[!] Not Available Validator\n'
              '    ✗ A useful error message\n'
              '    • A message that is not an error\n'
              '    ! A hint message\n'
              '\n'
466 467 468 469 470
              '[!] 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'
471
              '    ✗ An error message indicating partial installation\n'
472 473 474
              '    ! Maybe a hint will help the user\n'
              '    • An extra message with some verbose details\n'
              '\n'
475
              '! Doctor found issues in 4 categories.\n'
476
      ));
477 478
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
479
    });
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497

    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'
      ));
498 499
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
500 501 502 503
    });
  });

  group('doctor diagnosis wrapper', () {
504 505
    late TestUsage testUsage;
    late BufferLogger logger;
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

    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>{
542
      AnsiTerminal: () => FakeTerminal(),
543 544 545 546 547 548 549 550 551 552 553 554
      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,
    });
555
  });
556

557
  testUsingContext('validate non-verbose output wrapping', () async {
558 559 560 561 562
    final BufferLogger wrapLogger = BufferLogger.test(
      outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30),
    );
    expect(await FakeDoctor(wrapLogger).diagnose(verbose: false), isFalse);
    expect(wrapLogger.statusText, equals(
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
        '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'
    ));
588 589
  }, overrides: <Type, Generator>{
    AnsiTerminal: () => FakeTerminal(),
590 591 592
  });

  testUsingContext('validate verbose output wrapping', () async {
593 594 595
    final BufferLogger wrapLogger = BufferLogger.test(
      outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30),
    );
596
    expect(await FakeDoctor(wrapLogger).diagnose(), isFalse);
597
    expect(wrapLogger.statusText, equals(
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 627 628 629 630 631 632 633
        '[✓] 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'
    ));
634 635
  }, overrides: <Type, Generator>{
    AnsiTerminal: () => FakeTerminal(),
636 637
  });

638 639
  group('doctor with grouped validators', () {
    testUsingContext('validate diagnose combines validator output', () async {
640 641
      expect(await FakeGroupedDoctor(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
642 643 644 645 646 647 648 649 650 651
              '[✓] 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'
      ));
652 653
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
654
    });
655

656 657
    testUsingContext('validate merging assigns statusInfo and title', () async {
      // There are two subvalidators. Only the second contains statusInfo.
658 659
      expect(await FakeGroupedDoctorWithStatus(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
660 661 662
              '[✓] First validator title (A status message)\n'
              '    • A helpful message\n'
              '    • A different message\n'
663
              '\n'
664
              '• No issues found!\n'
665
      ));
666 667
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
668
    });
669 670
  });

671 672 673 674
  group('grouped validator merging results', () {
    final PassingGroupedValidator installed = PassingGroupedValidator('Category');
    final PartialGroupedValidator partial = PartialGroupedValidator('Category');
    final MissingGroupedValidator missing = MissingGroupedValidator('Category');
675 676

    testUsingContext('validate installed + installed = installed', () async {
677 678
      expect(await FakeSmallGroupDoctor(logger, installed, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[✓]'));
679 680
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
681
    });
682 683

    testUsingContext('validate installed + partial = partial', () async {
684 685
      expect(await FakeSmallGroupDoctor(logger, installed, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
686 687
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
688
    });
689 690

    testUsingContext('validate installed + missing = partial', () async {
691 692
      expect(await FakeSmallGroupDoctor(logger, installed, missing).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
693 694
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
695
    });
696 697

    testUsingContext('validate partial + installed = partial', () async {
698 699
      expect(await FakeSmallGroupDoctor(logger, partial, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
700 701
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
702
    });
703 704

    testUsingContext('validate partial + partial = partial', () async {
705 706
      expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
707 708
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
709
    });
710 711

    testUsingContext('validate partial + missing = partial', () async {
712 713
      expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
714 715
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
716
    });
717 718

    testUsingContext('validate missing + installed = partial', () async {
719 720
      expect(await FakeSmallGroupDoctor(logger, missing, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
721 722
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
723
    });
724 725

    testUsingContext('validate missing + partial = partial', () async {
726 727
      expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
728 729
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
730
    });
731 732

    testUsingContext('validate missing + missing = missing', () async {
733 734
      expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(), isFalse);
      expect(logger.statusText, startsWith('[✗]'));
735 736
    }, overrides: <Type, Generator>{
      AnsiTerminal: () => FakeTerminal(),
737
    });
738
  });
739 740

  testUsingContext('WebWorkflow is a part of validator workflows if enabled', () async {
741 742 743 744 745 746 747 748
    final List<Workflow> workflows = DoctorValidatorsProvider.test(
      featureFlags: TestFeatureFlags(isWebEnabled: true),
      platform: FakePlatform(),
    ).workflows;
    expect(
      workflows,
      contains(isA<WebWorkflow>()),
    );
749
  }, overrides: <Type, Generator>{
750
    FileSystem: () => MemoryFileSystem.test(),
751
    ProcessManager: () => fakeProcessManager,
752
  });
753

754 755 756 757 758 759 760 761 762 763 764 765 766 767
  testUsingContext('CustomDevicesWorkflow is a part of validator workflows if enabled', () async {
    final List<Workflow> workflows = DoctorValidatorsProvider.test(
      featureFlags: TestFeatureFlags(areCustomDevicesEnabled: true),
      platform: FakePlatform(),
    ).workflows;
    expect(
      workflows,
      contains(isA<CustomDeviceWorkflow>()),
    );
  }, overrides: <Type, Generator>{
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => fakeProcessManager,
  });

768 769 770 771 772 773 774 775
  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']);

776
    expect(flutterVersion.didFetchTagsAndUpdate, true);
777 778 779 780
    Cache.enableLocking();
  }, overrides: <Type, Generator>{
    ProcessManager: () => FakeProcessManager.any(),
    FileSystem: () => MemoryFileSystem.test(),
781
    FlutterVersion: () => flutterVersion,
782 783
    Doctor: () => NoOpDoctor(),
  }, initializeFlutterRoot: false);
784 785

  testUsingContext('If android workflow is disabled, AndroidStudio validator is not included', () {
786 787 788 789 790
    final DoctorValidatorsProvider provider = DoctorValidatorsProvider.test(
      featureFlags: TestFeatureFlags(isAndroidEnabled: false),
    );
    expect(provider.validators, isNot(contains(isA<AndroidStudioValidator>())));
    expect(provider.validators, isNot(contains(isA<NoAndroidStudioValidator>())));
791
  }, overrides: <Type, Generator>{
792
    AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false),
793
  });
794 795
}

796 797 798 799 800 801 802 803 804 805 806 807 808 809
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
  FakeAndroidWorkflow({
    this.canListDevices = true,
    this.appliesToHostPlatform = true,
  });

  @override
  final bool canListDevices;

  @override
  final bool appliesToHostPlatform;
}


810 811 812 813 814 815 816 817 818 819 820
class NoOpDoctor implements Doctor {
  @override
  bool get canLaunchAnything => true;

  @override
  bool get canListAnything => true;

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

  @override
821 822 823 824
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
825
    AndroidLicenseValidator? androidLicenseValidator,
826
    bool showPii = true,
827
    List<ValidatorTask>? startedValidatorTasks,
828
    bool sendEvent = true,
829
  }) async => true;
830 831 832 833 834

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

  @override
835
  Future<void> summary() async { }
836 837 838 839 840 841

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

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

844
class PassingValidator extends DoctorValidator {
845
  PassingValidator(super.name);
846 847 848

  @override
  Future<ValidationResult> validate() async {
849
    const List<ValidationMessage> messages = <ValidationMessage>[
850 851 852
      ValidationMessage('A helpful message'),
      ValidationMessage('A second, somewhat longer helpful message'),
    ];
853
    return const ValidationResult(ValidationType.installed, messages, statusInfo: 'with statusInfo');
854 855 856
  }
}

857 858 859 860 861 862 863 864 865 866 867 868
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);
  }
}

869
class MissingValidator extends DoctorValidator {
870
  MissingValidator() : super('Missing Validator');
871 872 873

  @override
  Future<ValidationResult> validate() async {
874
    const List<ValidationMessage> messages = <ValidationMessage>[
875 876 877 878
      ValidationMessage.error('A useful error message'),
      ValidationMessage('A message that is not an error'),
      ValidationMessage.hint('A hint message'),
    ];
879
    return const ValidationResult(ValidationType.missing, messages);
880 881 882
  }
}

883
class NotAvailableValidator extends DoctorValidator {
884
  NotAvailableValidator() : super('Not Available Validator');
885 886 887

  @override
  Future<ValidationResult> validate() async {
888
    const List<ValidationMessage> messages = <ValidationMessage>[
889 890 891 892
      ValidationMessage.error('A useful error message'),
      ValidationMessage('A message that is not an error'),
      ValidationMessage.hint('A hint message'),
    ];
893
    return const ValidationResult(ValidationType.notAvailable, messages);
894 895 896
  }
}

897 898 899 900 901 902 903 904 905 906 907 908
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;
  }
}

909 910 911 912 913
class PartialValidatorWithErrors extends DoctorValidator {
  PartialValidatorWithErrors() : super('Partial Validator with Errors');

  @override
  Future<ValidationResult> validate() async {
914
    const List<ValidationMessage> messages = <ValidationMessage>[
915 916 917 918
      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'),
    ];
919
    return const ValidationResult(ValidationType.partial, messages);
920 921 922 923 924 925 926 927
  }
}

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

  @override
  Future<ValidationResult> validate() async {
928
    const List<ValidationMessage> messages = <ValidationMessage>[
929 930 931
      ValidationMessage.hint('There is a hint here'),
      ValidationMessage('But there is no error'),
    ];
932
    return const ValidationResult(ValidationType.partial, messages);
933 934 935
  }
}

936 937 938 939 940
class CrashingValidator extends DoctorValidator {
  CrashingValidator() : super('Crashing validator');

  @override
  Future<ValidationResult> validate() async {
941
    throw StateError('fatal error');
942 943 944
  }
}

945 946 947 948 949 950 951 952
class AsyncCrashingValidator extends DoctorValidator {
  AsyncCrashingValidator(this._time) : super('Async crashing validator');

  final FakeAsync _time;

  @override
  Future<ValidationResult> validate() {
    const Duration delay = Duration(seconds: 1);
953 954 955 956
    final Future<ValidationResult> result = Future<ValidationResult>.delayed(
      delay,
      () => throw StateError('fatal error'),
    );
957 958 959 960 961 962
    _time.elapse(const Duration(seconds: 1));
    _time.flushMicrotasks();
    return result;
  }
}

963 964
/// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor {
965 966
  FakeDoctor(Logger logger) : super(logger: logger);

967
  @override
968 969 970 971 972 973 974
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PassingValidator('Passing Validator'),
    MissingValidator(),
    NotAvailableValidator(),
    PartialValidatorWithHintsOnly(),
    PartialValidatorWithErrors(),
  ];
975 976 977 978
}

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

981
  @override
982 983 984 985 986 987
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PassingValidator('Passing Validator'),
    PartialValidatorWithHintsOnly(),
    PartialValidatorWithErrors(),
    PassingValidator('Another Passing Validator'),
  ];
988 989 990 991 992
}

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

995
  @override
996 997 998
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PartialValidatorWithHintsOnly(),
  ];
999 1000 1001 1002
}

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

1005
  @override
1006 1007 1008 1009 1010 1011
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PassingValidator('Passing Validator'),
    PassingValidator('Another Passing Validator'),
    PassingValidator('Validators are fun'),
    PassingValidator('Four score and seven validators ago'),
  ];
1012 1013
}

1014 1015 1016 1017 1018
/// A doctor that passes and contains PII that can be hidden.
class FakePiiDoctor extends Doctor {
  FakePiiDoctor(Logger logger) : super(logger: logger);

  @override
1019 1020 1021
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PiiValidator(),
  ];
1022 1023
}

1024 1025
/// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor {
1026 1027
  FakeCrashingDoctor(Logger logger) : super(logger: logger);

1028
  @override
1029 1030 1031 1032 1033 1034 1035
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PassingValidator('Passing Validator'),
    PassingValidator('Another Passing Validator'),
    CrashingValidator(),
    PassingValidator('Validators are fun'),
    PassingValidator('Four score and seven validators ago'),
  ];
1036 1037
}

1038 1039 1040 1041 1042
/// A doctor with a validator that will never finish.
class FakeAsyncStuckDoctor extends Doctor {
  FakeAsyncStuckDoctor(Logger logger) : super(logger: logger);

  @override
1043 1044 1045 1046 1047 1048 1049
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PassingValidator('Passing Validator'),
    PassingValidator('Another Passing Validator'),
    StuckValidator(),
    PassingValidator('Validators are fun'),
    PassingValidator('Four score and seven validators ago'),
  ];
1050 1051
}

1052 1053
/// A doctor with a validator that throws an exception.
class FakeAsyncCrashingDoctor extends Doctor {
1054
  FakeAsyncCrashingDoctor(this._time, Logger logger) : super(logger: logger);
1055 1056 1057 1058

  final FakeAsync _time;

  @override
1059 1060 1061 1062 1063 1064 1065
  late final List<DoctorValidator> validators = <DoctorValidator>[
    PassingValidator('Passing Validator'),
    PassingValidator('Another Passing Validator'),
    AsyncCrashingValidator(_time),
    PassingValidator('Validators are fun'),
    PassingValidator('Four score and seven validators ago'),
  ];
1066 1067
}

1068 1069 1070 1071 1072 1073
/// A DoctorValidatorsProvider that overrides the default validators without
/// overriding the doctor.
class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
  @override
  List<DoctorValidator> get validators {
    return <DoctorValidator>[
1074 1075
      PassingValidator('Passing Validator'),
      PassingValidator('Another Passing Validator'),
1076
      PassingValidator('Providing validators is fun'),
1077 1078
    ];
  }
1079 1080 1081 1082 1083 1084

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

class PassingGroupedValidator extends DoctorValidator {
1085
  PassingGroupedValidator(super.name);
1086 1087 1088

  @override
  Future<ValidationResult> validate() async {
1089
    const List<ValidationMessage> messages = <ValidationMessage>[
1090 1091
      ValidationMessage('A helpful message'),
    ];
1092
    return const ValidationResult(ValidationType.installed, messages);
1093 1094 1095 1096
  }
}

class MissingGroupedValidator extends DoctorValidator {
1097
  MissingGroupedValidator(super.name);
1098 1099 1100

  @override
  Future<ValidationResult> validate() async {
1101
    const List<ValidationMessage> messages = <ValidationMessage>[
1102 1103
      ValidationMessage.error('A useful error message'),
    ];
1104
    return const ValidationResult(ValidationType.missing, messages);
1105 1106 1107 1108
  }
}

class PartialGroupedValidator extends DoctorValidator {
1109
  PartialGroupedValidator(super.name);
1110 1111 1112

  @override
  Future<ValidationResult> validate() async {
1113
    const List<ValidationMessage> messages = <ValidationMessage>[
1114 1115
      ValidationMessage.error('An error message for partial installation'),
    ];
1116
    return const ValidationResult(ValidationType.partial, messages);
1117 1118 1119
  }
}

1120
class PassingGroupedValidatorWithStatus extends DoctorValidator {
1121
  PassingGroupedValidatorWithStatus(super.name);
1122 1123 1124

  @override
  Future<ValidationResult> validate() async {
1125
    const List<ValidationMessage> messages = <ValidationMessage>[
1126 1127
      ValidationMessage('A different message'),
    ];
1128
    return const ValidationResult(ValidationType.installed, messages, statusInfo: 'A status message');
1129 1130 1131 1132
  }
}

/// A doctor that has two groups of two validators each.
1133
class FakeGroupedDoctor extends Doctor {
1134 1135
  FakeGroupedDoctor(Logger logger) : super(logger: logger);

1136
  @override
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
  late final List<DoctorValidator> validators = <DoctorValidator>[
    GroupedValidator(<DoctorValidator>[
      PassingGroupedValidator('Category 1'),
      PassingGroupedValidator('Category 1'),
    ]),
    GroupedValidator(<DoctorValidator>[
      PassingGroupedValidator('Category 2'),
      MissingGroupedValidator('Category 2'),
    ]),
  ];
1147 1148
}

1149
class FakeGroupedDoctorWithStatus extends Doctor {
1150 1151
  FakeGroupedDoctorWithStatus(Logger logger) : super(logger: logger);

1152
  @override
1153 1154 1155 1156 1157 1158
  late final List<DoctorValidator> validators = <DoctorValidator>[
    GroupedValidator(<DoctorValidator>[
      PassingGroupedValidator('First validator title'),
      PassingGroupedValidatorWithStatus('Second validator title'),
    ]),
  ];
1159 1160
}

1161 1162 1163
/// A doctor that takes any two validators. Used to check behavior when
/// merging ValidationTypes (installed, missing, partial).
class FakeSmallGroupDoctor extends Doctor {
1164 1165 1166
  FakeSmallGroupDoctor(Logger logger, DoctorValidator val1, DoctorValidator val2)
    : validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])],
      super(logger: logger);
1167

1168
  @override
1169
  final List<DoctorValidator> validators;
1170 1171
}

1172
class VsCodeValidatorTestTargets extends VsCodeValidator {
1173
  VsCodeValidatorTestTargets._(String installDirectory, String extensionDirectory, {String? edition})
1174
    : super(VsCode.fromDirectory(installDirectory, extensionDirectory, edition: edition, fileSystem: globals.fs));
1175 1176

  static VsCodeValidatorTestTargets get installedWithExtension =>
1177
      VsCodeValidatorTestTargets._(validInstall, validExtensions);
1178

1179
  static VsCodeValidatorTestTargets get installedWithExtension64bit =>
1180
      VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition');
1181

1182
  static VsCodeValidatorTestTargets get installedWithoutExtension =>
1183
      VsCodeValidatorTestTargets._(validInstall, missingExtensions);
1184

1185 1186 1187
  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');
1188
}
1189

1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200
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;
}

1201 1202 1203
// 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
1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227
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);
1228
}
1229 1230 1231 1232 1233

class FakeTerminal extends Fake implements AnsiTerminal {
  @override
  final bool supportsColor = false;
}