doctor_test.dart 43.3 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 8
import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
9
import 'package:flutter_tools/src/artifacts.dart';
10
import 'package:flutter_tools/src/base/common.dart';
11
import 'package:flutter_tools/src/base/file_system.dart';
12
import 'package:flutter_tools/src/base/io.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 18
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
19
import 'package:flutter_tools/src/doctor.dart';
20
import 'package:flutter_tools/src/features.dart';
21
import 'package:flutter_tools/src/globals.dart' as globals;
22
import 'package:flutter_tools/src/ios/plist_parser.dart';
23
import 'package:flutter_tools/src/reporting/reporting.dart';
24
import 'package:flutter_tools/src/version.dart';
25 26
import 'package:flutter_tools/src/vscode/vscode.dart';
import 'package:flutter_tools/src/vscode/vscode_validator.dart';
27 28 29 30
import 'package:flutter_tools/src/web/workflow.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:quiver/testing/async.dart';
31

32 33
import '../../src/common.dart';
import '../../src/context.dart';
34
import '../../src/testbed.dart';
35

36 37
final Generator _kNoColorOutputPlatform = () => FakePlatform(
  localeName: 'en_US.UTF-8',
38
  environment: <String, String>{},
39 40
  stdoutSupportsAnsi: false,
);
41

42
final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
43
  Platform: _kNoColorOutputPlatform,
44 45
};

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

51
void main() {
52
  MockProcessManager mockProcessManager;
53
  MockFlutterVersion mockFlutterVersion;
54
  BufferLogger logger;
55 56 57

  setUp(() {
    mockProcessManager = MockProcessManager();
58
    mockFlutterVersion = MockFlutterVersion();
59
    logger = BufferLogger.test();
60 61
  });

62
  group('doctor', () {
63 64 65 66 67 68 69 70
    MockPlistParser mockPlistParser;
    MemoryFileSystem fileSystem;

    setUp(() {
      mockPlistParser = MockPlistParser();
      fileSystem = MemoryFileSystem.test();
    });

71
    testUsingContext('intellij validator', () async {
72
      const String installPath = '/path/to/intelliJ';
73
      final ValidationResult result = await IntelliJValidatorTestTarget('Test', installPath).validate();
74
      expect(result.type, ValidationType.partial);
75
      expect(result.statusInfo, 'version test.test.test');
76
      expect(result.messages, hasLength(4));
77 78

      ValidationMessage message = result.messages
79 80 81 82
          .firstWhere((ValidationMessage m) => m.message.startsWith('IntelliJ '));
      expect(message.message, 'IntelliJ at $installPath');

      message = result.messages
83 84 85 86 87
          .firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
      expect(message.message, 'Dart plugin version 162.2485');

      message = result.messages
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
88 89
      expect(message.message, contains('Flutter plugin version 0.1.3'));
      expect(message.message, contains('recommended minimum version'));
90
    }, overrides: noColorTerminalOverride);
91

92
    testUsingContext('intellij plugins path checking on mac', () async {
93
      when(mockPlistParser.getValueFromFile(any, PlistParser.kCFBundleShortVersionStringKey)).thenReturn('2020.10');
94

95 96 97 98 99 100
      final Directory pluginsDirectory = fileSystem.directory('/foo/bar/Library/Application Support/JetBrains/TestID2020.10/plugins')
        ..createSync(recursive: true);
      final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/path/to/app');
      expect(validator.plistFile, '/path/to/app/Contents/Info.plist');
      expect(validator.pluginsPath, pluginsDirectory.path);
    }, overrides: <Type, Generator>{
101
      Platform: () => macPlatform,
102 103 104
      PlistParser: () => mockPlistParser,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
105 106 107 108
      FileSystemUtils: () => FileSystemUtils(
        fileSystem: fileSystem,
        platform: macPlatform,
      )
109
    });
110

111 112 113 114 115 116
    testUsingContext('legacy intellij plugins path checking on mac', () async {
      when(mockPlistParser.getValueFromFile(any, PlistParser.kCFBundleShortVersionStringKey)).thenReturn('2020.10');

      final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/foo');
      expect(validator.pluginsPath, '/foo/bar/Library/Application Support/TestID2020.10');
    }, overrides: <Type, Generator>{
117
      Platform: () => macPlatform,
118
      PlistParser: () => mockPlistParser,
119 120 121 122 123 124
      FileSystem: () => fileSystem,
      FileSystemUtils: () => FileSystemUtils(
        fileSystem: fileSystem,
        platform: macPlatform,
      ),
      ProcessManager: () => FakeProcessManager.any(),
125 126 127 128 129 130 131 132 133
    });

    testUsingContext('intellij plugins path checking on mac with override', () async {
      when(mockPlistParser.getValueFromFile(any, 'JetBrainsToolboxApp')).thenReturn('/path/to/JetBrainsToolboxApp');

      final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/foo');
      expect(validator.pluginsPath, '/path/to/JetBrainsToolboxApp.plugins');
    }, overrides: <Type, Generator>{
      PlistParser: () => mockPlistParser,
134 135 136 137 138 139 140
      Platform: () => macPlatform,
      FileSystem: () => fileSystem,
      FileSystemUtils: () => FileSystemUtils(
        fileSystem: fileSystem,
        platform: macPlatform,
      ),
      ProcessManager: () => FakeProcessManager.any(),
141 142
    });

143 144 145 146 147 148 149 150 151 152 153
    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
154 155
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, 'Flutter extension version 4.5.6');
156
      expect(message.isError, isFalse);
157
    }, overrides: noColorTerminalOverride);
158

159 160 161 162 163 164 165 166 167 168 169 170
    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
171 172
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, 'Flutter extension version 4.5.6');
173
    }, overrides: noColorTerminalOverride);
174

175 176 177 178 179 180 181 182 183 184 185
    testUsingContext('vs code validator when extension missing', () async {
      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithoutExtension.validate();
      expect(result.type, ValidationType.partial);
      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
186 187
          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
      expect(message.message, startsWith('Flutter extension not installed'));
188
      expect(message.isError, isTrue);
189
    }, overrides: noColorTerminalOverride);
190
  });
191

Chris Bracken's avatar
Chris Bracken committed
192
  group('doctor with overridden validators', () {
193
    testUsingContext('validate non-verbose output format for run without issues', () async {
194
      final Doctor doctor = Doctor(logger: logger);
195
      expect(await doctor.diagnose(verbose: false), isTrue);
196
      expect(logger.statusText, equals(
197 198 199 200 201 202 203 204
              '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>{
205 206
      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
      Platform: _kNoColorOutputPlatform,
207 208 209
    });
  });

210 211 212 213 214 215 216 217 218
  group('doctor usage params', () {
    Usage mockUsage;

    setUp(() {
      mockUsage = MockUsage();
      when(mockUsage.isFirstRun).thenReturn(true);
    });

    testUsingContext('contains installed', () async {
219
      final Doctor doctor = Doctor(logger: logger);
220 221 222
      await doctor.diagnose(verbose: false);

      expect(
223
        verify(mockUsage.sendEvent(
224 225 226
          'doctor-result',
          'PassingValidator',
          label: captureAnyNamed('label'),
227
        )).captured,
228 229 230 231 232 233 234 235 236
        <dynamic>['installed', 'installed', 'installed'],
      );
    }, overrides: <Type, Generator>{
      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
      Platform: _kNoColorOutputPlatform,
      Usage: () => mockUsage,
    });

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

      expect(
240
        verify(mockUsage.sendEvent(
241 242 243
          'doctor-result',
          'PassingValidator',
          label: captureAnyNamed('label'),
244
        )).captured,
245 246 247
        <dynamic>['installed', 'installed'],
      );
      expect(
248
        verify(mockUsage.sendEvent(
249 250 251
          'doctor-result',
          'PartialValidatorWithHintsOnly',
          label: captureAnyNamed('label'),
252
        )).captured,
253 254 255
        <dynamic>['partial'],
      );
      expect(
256
        verify(mockUsage.sendEvent(
257 258 259
          'doctor-result',
          'PartialValidatorWithErrors',
          label: captureAnyNamed('label'),
260
        )).captured,
261 262 263 264 265 266 267 268
        <dynamic>['partial'],
      );
    }, overrides: <Type, Generator>{
      Platform: _kNoColorOutputPlatform,
      Usage: () => mockUsage,
    });

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

      expect(
272
        verify(mockUsage.sendEvent(
273 274 275
          'doctor-result',
          'PassingValidator',
          label: captureAnyNamed('label'),
276
        )).captured,
277 278 279
        <dynamic>['installed'],
      );
      expect(
280
        verify(mockUsage.sendEvent(
281 282 283
          'doctor-result',
          'MissingValidator',
          label: captureAnyNamed('label'),
284
        )).captured,
285 286 287
        <dynamic>['missing'],
      );
      expect(
288
        verify(mockUsage.sendEvent(
289 290 291
          'doctor-result',
          'NotAvailableValidator',
          label: captureAnyNamed('label'),
292
        )).captured,
293 294 295
        <dynamic>['notAvailable'],
      );
      expect(
296
        verify(mockUsage.sendEvent(
297 298 299
          'doctor-result',
          'PartialValidatorWithHintsOnly',
          label: captureAnyNamed('label'),
300
        )).captured,
301 302 303
        <dynamic>['partial'],
      );
      expect(
304
        verify(mockUsage.sendEvent(
305 306 307
          'doctor-result',
          'PartialValidatorWithErrors',
          label: captureAnyNamed('label'),
308
        )).captured,
309 310 311 312 313 314
        <dynamic>['partial'],
      );
    }, overrides: <Type, Generator>{
      Platform: _kNoColorOutputPlatform,
      Usage: () => mockUsage,
    });
315 316

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

      expect(
320
        verify(mockUsage.sendEvent(
321 322 323
          'doctor-result',
          'PassingGroupedValidator',
          label: captureAnyNamed('label'),
324
        )).captured,
325 326 327
        <dynamic>['installed', 'installed', 'installed'],
      );
      expect(
328
        verify(mockUsage.sendEvent(
329 330 331
          'doctor-result',
          'MissingGroupedValidator',
          label: captureAnyNamed('label'),
332
        )).captured,
333 334 335 336 337 338
        <dynamic>['missing'],
      );
    }, overrides: <Type, Generator>{
      Platform: _kNoColorOutputPlatform,
      Usage: () => mockUsage,
    });
339
  });
340

341
  group('doctor with fake validators', () {
342 343 344 345 346 347 348 349 350 351
    MockArtifacts mockArtifacts;
    const String genSnapshotPath = '/path/to/gen_snapshot';
    FileSystem memoryFileSystem;

    setUp(() {
      memoryFileSystem = MemoryFileSystem.test();
      mockArtifacts = MockArtifacts();
      when(mockArtifacts.getArtifactPath(Artifact.genSnapshot)).thenReturn(genSnapshotPath);
    });

352
    testUsingContext('validate non-verbose output format for run without issues', () async {
353 354
      expect(await FakeQuietDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
355 356 357 358 359 360 361 362
              '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'
      ));
363
    }, overrides: noColorTerminalOverride);
364

365
    testUsingContext('validate non-verbose output format for run with crash', () async {
366 367
      expect(await FakeCrashingDoctor(logger).diagnose(verbose: false), isFalse);
      expect(logger.statusText, equals(
368 369 370 371 372 373 374 375 376 377 378 379 380 381
              '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'
              '    ✗ fatal error\n'
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
    }, overrides: noColorTerminalOverride);

382
    testUsingContext('validate verbose output format contains trace for run with crash', () async {
383 384
      expect(await FakeCrashingDoctor(logger).diagnose(verbose: true), isFalse);
      expect(logger.statusText, contains('#0      CrashingValidator.validate'));
385 386
    }, overrides: noColorTerminalOverride);

387 388 389 390

    testUsingContext('validate non-verbose output format for run with an async crash', () async {
      final Completer<void> completer = Completer<void>();
      await FakeAsync().run((FakeAsync time) {
391
        unawaited(FakeAsyncCrashingDoctor(time, logger).diagnose(verbose: false).then((bool r) {
392 393 394 395 396 397 398
          expect(r, isFalse);
          completer.complete(null);
        }));
        time.elapse(const Duration(seconds: 1));
        time.flushMicrotasks();
        return completer.future;
      });
399
      expect(logger.statusText, equals(
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
              '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'
              '    ✗ fatal error\n'
              '[✓] Validators are fun (with statusInfo)\n'
              '[✓] Four score and seven validators ago (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 1 category.\n'
      ));
    }, overrides: noColorTerminalOverride);


415
    testUsingContext('validate non-verbose output format when only one category fails', () async {
416 417
      expect(await FakeSinglePassingDoctor(logger).diagnose(verbose: false), isTrue);
      expect(logger.statusText, equals(
418 419 420 421 422 423
              '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'
      ));
424
    }, overrides: noColorTerminalOverride);
425 426

    testUsingContext('validate non-verbose output format for a passing run', () async {
427 428
      expect(await FakePassingDoctor(logger).diagnose(verbose: false), isTrue);
      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'
              '[!] Partial Validator with only a Hint\n'
              '    ! There is a hint here\n'
              '[!] Partial Validator with Errors\n'
434
              '    ✗ An error message indicating partial installation\n'
435 436 437 438 439
              '    ! Maybe a hint will help the user\n'
              '[✓] Another Passing Validator (with statusInfo)\n'
              '\n'
              '! Doctor found issues in 2 categories.\n'
      ));
440
    }, overrides: noColorTerminalOverride);
441 442

    testUsingContext('validate non-verbose output format', () async {
443 444
      expect(await FakeDoctor(logger).diagnose(verbose: false), isFalse);
      expect(logger.statusText, equals(
445 446 447 448 449
              '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'
450 451 452
              '[!] Not Available Validator\n'
              '    ✗ A useful error message\n'
              '    ! A hint message\n'
453 454 455
              '[!] Partial Validator with only a Hint\n'
              '    ! There is a hint here\n'
              '[!] Partial Validator with Errors\n'
456
              '    ✗ An error message indicating partial installation\n'
457 458
              '    ! Maybe a hint will help the user\n'
              '\n'
459
              '! Doctor found issues in 4 categories.\n'
460
      ));
461
    }, overrides: noColorTerminalOverride);
462 463

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

    testUsingContext('gen_snapshot does not work', () async {
494
      memoryFileSystem.file(genSnapshotPath).createSync(recursive: true);
495
      when(mockProcessManager.runSync(
496
        <String>[genSnapshotPath],
497 498 499 500
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment'),
      )).thenReturn(ProcessResult(101, 1, '', ''));

501 502
      expect(await FlutterValidatorDoctor(logger).diagnose(verbose: false), isTrue);
      final List<String> statusLines = logger.statusText.split('\n');
503
      for (final String msg in userMessages.flutterBinariesDoNotRun.split('\n')) {
504 505
        expect(statusLines, contains(contains(msg)));
      }
506
      if (globals.platform.isLinux) {
507
        for (final String msg in userMessages.flutterBinariesLinuxRepairCommands.split('\n')) {
508 509 510 511
          expect(statusLines, contains(contains(msg)));
        }
      }
    }, overrides: <Type, Generator>{
512 513
      Artifacts: () => mockArtifacts,
      FileSystem: () => memoryFileSystem,
514 515 516 517
      OutputPreferences: () => OutputPreferences(wrapText: false),
      ProcessManager: () => mockProcessManager,
      Platform: _kNoColorOutputPlatform,
    });
518 519

    testUsingContext('gen_snapshot binary not available', () async {
520
      expect(await FlutterValidatorDoctor(logger).diagnose(verbose: false), isTrue);
521 522
      // gen_snapshot is downloaded on demand, and the doctor should not
      // fail if the gen_snapshot binary is not present.
523
      expect(logger.statusText, contains('No issues found!'));
524
    }, overrides: <Type, Generator>{
525
      Artifacts: () => mockArtifacts,
526 527 528
      FileSystem: () => MemoryFileSystem(),
      ProcessManager: () => FakeProcessManager.any(),
    });
529 530

    testUsingContext('version checking does not work', () async {
531
      memoryFileSystem.file(genSnapshotPath).createSync(recursive: true);
532 533 534 535 536 537 538
      final VersionCheckError versionCheckError = VersionCheckError('version error');

      when(mockFlutterVersion.channel).thenReturn('unknown');
      when(mockFlutterVersion.frameworkVersion).thenReturn('0.0.0');
      when(mockFlutterVersion.frameworkDate).thenThrow(versionCheckError);

      when(mockProcessManager.runSync(
539
        <String>[genSnapshotPath],
540 541 542 543
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment'),
      )).thenReturn(ProcessResult(101, 255, '', ''));

544
      expect(await FlutterValidatorDoctor(logger).diagnose(verbose: false), isTrue);
545

546
      expect(logger.statusText, equals(
547
        'Doctor summary (to see all details, run flutter doctor -v):\n'
548
          '[!] Flutter (Channel unknown, 0.0.0, on fake OS name and version, locale en_US.UTF-8)\n'
549 550 551 552
          '    ✗ version error\n\n'
          '! Doctor found issues in 1 category.\n'
      ));
    }, overrides: <Type, Generator>{
553 554
      Artifacts: () => mockArtifacts,
      FileSystem: () => memoryFileSystem,
555 556 557 558 559
      OutputPreferences: () => OutputPreferences(wrapText: false),
      ProcessManager: () => mockProcessManager,
      Platform: _kNoColorOutputPlatform,
      FlutterVersion: () => mockFlutterVersion,
    });
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577

    testUsingContext('shows mirrors', () async {
      (globals.platform as FakePlatform).environment = <String, String>{
        'PUB_HOSTED_URL': 'https://example.com/pub',
        'FLUTTER_STORAGE_BASE_URL': 'https://example.com/flutter',
      };

      expect(await FlutterValidatorDoctor(logger).diagnose(verbose: true), isTrue);
      expect(logger.statusText, contains('Pub download mirror https://example.com/pub'));
      expect(logger.statusText, contains('Flutter download mirror https://example.com/flutter'));
    }, overrides: <Type, Generator>{
      Artifacts: () => mockArtifacts,
      FileSystem: () => memoryFileSystem,
      OutputPreferences: () => OutputPreferences(wrapText: false),
      ProcessManager: () => mockProcessManager,
      Platform: _kNoColorOutputPlatform,
      FlutterVersion: () => mockFlutterVersion,
    });
578
  });
579

580
  testUsingContext('validate non-verbose output wrapping', () async {
581 582 583 584 585
    final BufferLogger wrapLogger = BufferLogger.test(
      outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30),
    );
    expect(await FakeDoctor(wrapLogger).diagnose(verbose: false), isFalse);
    expect(wrapLogger.statusText, equals(
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
        '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'
        ''
    ));
  }, overrides: <Type, Generator>{
613
    Platform: _kNoColorOutputPlatform,
614 615 616
  });

  testUsingContext('validate verbose output wrapping', () async {
617 618 619 620 621
    final BufferLogger wrapLogger = BufferLogger.test(
      outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30),
    );
    expect(await FakeDoctor(wrapLogger).diagnose(verbose: true), isFalse);
    expect(wrapLogger.statusText, equals(
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
        '[✓] 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'
        ''
    ));
  }, overrides: <Type, Generator>{
660
    Platform: _kNoColorOutputPlatform,
661 662 663
  });


664 665
  group('doctor with grouped validators', () {
    testUsingContext('validate diagnose combines validator output', () async {
666 667
      expect(await FakeGroupedDoctor(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
668 669 670 671 672 673 674 675 676 677
              '[✓] 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'
      ));
678
    }, overrides: noColorTerminalOverride);
679

680 681
    testUsingContext('validate merging assigns statusInfo and title', () async {
      // There are two subvalidators. Only the second contains statusInfo.
682 683
      expect(await FakeGroupedDoctorWithStatus(logger).diagnose(), isTrue);
      expect(logger.statusText, equals(
684 685 686
              '[✓] First validator title (A status message)\n'
              '    • A helpful message\n'
              '    • A different message\n'
687
              '\n'
688
              '• No issues found!\n'
689
      ));
690
    }, overrides: noColorTerminalOverride);
691 692 693
  });


694 695 696 697
  group('grouped validator merging results', () {
    final PassingGroupedValidator installed = PassingGroupedValidator('Category');
    final PartialGroupedValidator partial = PartialGroupedValidator('Category');
    final MissingGroupedValidator missing = MissingGroupedValidator('Category');
698 699

    testUsingContext('validate installed + installed = installed', () async {
700 701
      expect(await FakeSmallGroupDoctor(logger, installed, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[✓]'));
702
    }, overrides: noColorTerminalOverride);
703 704

    testUsingContext('validate installed + partial = partial', () async {
705 706
      expect(await FakeSmallGroupDoctor(logger, installed, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
707
    }, overrides: noColorTerminalOverride);
708 709

    testUsingContext('validate installed + missing = partial', () async {
710 711
      expect(await FakeSmallGroupDoctor(logger, installed, missing).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
712
    }, overrides: noColorTerminalOverride);
713 714

    testUsingContext('validate partial + installed = partial', () async {
715 716
      expect(await FakeSmallGroupDoctor(logger, partial, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
717
    }, overrides: noColorTerminalOverride);
718 719

    testUsingContext('validate partial + partial = partial', () async {
720 721
      expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
722
    }, overrides: noColorTerminalOverride);
723 724

    testUsingContext('validate partial + missing = partial', () async {
725 726
      expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
727
    }, overrides: noColorTerminalOverride);
728 729

    testUsingContext('validate missing + installed = partial', () async {
730 731
      expect(await FakeSmallGroupDoctor(logger, missing, installed).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
732
    }, overrides: noColorTerminalOverride);
733 734

    testUsingContext('validate missing + partial = partial', () async {
735 736
      expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(), isTrue);
      expect(logger.statusText, startsWith('[!]'));
737
    }, overrides: noColorTerminalOverride);
738 739

    testUsingContext('validate missing + missing = missing', () async {
740 741
      expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(), isFalse);
      expect(logger.statusText, startsWith('[✗]'));
742
    }, overrides: noColorTerminalOverride);
743
  });
744 745

  testUsingContext('WebWorkflow is a part of validator workflows if enabled', () async {
746
    when(globals.processManager.canRun(any)).thenReturn(true);
747

748 749
    expect(DoctorValidatorsProvider.defaultInstance.workflows,
      contains(isA<WebWorkflow>()));
750 751
  }, overrides: <Type, Generator>{
    FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
752
    FileSystem: () => MemoryFileSystem.test(),
753 754
    ProcessManager: () => MockProcessManager(),
  });
755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798

  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']);

    verify(mockFlutterVersion.fetchTagsAndUpdate()).called(1);

    Cache.enableLocking();
  }, overrides: <Type, Generator>{
    ProcessManager: () => FakeProcessManager.any(),
    FileSystem: () => MemoryFileSystem.test(),
    FlutterVersion: () => mockFlutterVersion,
    Doctor: () => NoOpDoctor(),
  }, initializeFlutterRoot: false);
}

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

  @override
  bool get canListAnything => true;

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

  @override
  Future<bool> diagnose({ bool androidLicenses = false, bool verbose = true, bool showColor = true }) async => true;

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

  @override
  Future<void> summary() => null;

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

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

801 802
class MockUsage extends Mock implements Usage {}

803
class IntelliJValidatorTestTarget extends IntelliJValidator {
804
  IntelliJValidatorTestTarget(String title, String installPath) : super(title, installPath);
805 806

  @override
807
  String get pluginsPath => globals.fs.path.join('test', 'data', 'intellij', 'plugins');
808 809 810 811

  @override
  String get version => 'test.test.test';
}
812 813 814 815 816 817

class PassingValidator extends DoctorValidator {
  PassingValidator(String name) : super(name);

  @override
  Future<ValidationResult> validate() async {
818
    const List<ValidationMessage> messages = <ValidationMessage>[
819 820 821
      ValidationMessage('A helpful message'),
      ValidationMessage('A second, somewhat longer helpful message'),
    ];
822
    return const ValidationResult(ValidationType.installed, messages, statusInfo: 'with statusInfo');
823 824 825 826
  }
}

class MissingValidator extends DoctorValidator {
827
  MissingValidator() : super('Missing Validator');
828 829 830

  @override
  Future<ValidationResult> validate() async {
831
    const List<ValidationMessage> messages = <ValidationMessage>[
832 833 834 835
      ValidationMessage.error('A useful error message'),
      ValidationMessage('A message that is not an error'),
      ValidationMessage.hint('A hint message'),
    ];
836
    return const ValidationResult(ValidationType.missing, messages);
837 838 839
  }
}

840
class NotAvailableValidator extends DoctorValidator {
841
  NotAvailableValidator() : super('Not Available Validator');
842 843 844

  @override
  Future<ValidationResult> validate() async {
845
    const List<ValidationMessage> messages = <ValidationMessage>[
846 847 848 849
      ValidationMessage.error('A useful error message'),
      ValidationMessage('A message that is not an error'),
      ValidationMessage.hint('A hint message'),
    ];
850
    return const ValidationResult(ValidationType.notAvailable, messages);
851 852 853
  }
}

854 855 856 857 858
class PartialValidatorWithErrors extends DoctorValidator {
  PartialValidatorWithErrors() : super('Partial Validator with Errors');

  @override
  Future<ValidationResult> validate() async {
859
    const List<ValidationMessage> messages = <ValidationMessage>[
860 861 862 863
      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'),
    ];
864
    return const ValidationResult(ValidationType.partial, messages);
865 866 867 868 869 870 871 872
  }
}

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

  @override
  Future<ValidationResult> validate() async {
873
    const List<ValidationMessage> messages = <ValidationMessage>[
874 875 876
      ValidationMessage.hint('There is a hint here'),
      ValidationMessage('But there is no error'),
    ];
877
    return const ValidationResult(ValidationType.partial, messages);
878 879 880
  }
}

881 882 883 884 885 886 887 888 889
class CrashingValidator extends DoctorValidator {
  CrashingValidator() : super('Crashing validator');

  @override
  Future<ValidationResult> validate() async {
    throw 'fatal error';
  }
}

890 891 892 893 894 895 896 897
class AsyncCrashingValidator extends DoctorValidator {
  AsyncCrashingValidator(this._time) : super('Async crashing validator');

  final FakeAsync _time;

  @override
  Future<ValidationResult> validate() {
    const Duration delay = Duration(seconds: 1);
898 899 900 901
    final Future<ValidationResult> result = Future<ValidationResult>.delayed(delay)
      .then((_) {
        throw 'fatal error';
      });
902 903 904 905 906 907
    _time.elapse(const Duration(seconds: 1));
    _time.flushMicrotasks();
    return result;
  }
}

908 909
/// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor {
910 911
  FakeDoctor(Logger logger) : super(logger: logger);

912 913 914 915
  List<DoctorValidator> _validators;

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

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

930 931 932
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
933 934 935 936 937 938
    return _validators ??= <DoctorValidator>[
      PassingValidator('Passing Validator'),
      PartialValidatorWithHintsOnly(),
      PartialValidatorWithErrors(),
      PassingValidator('Another Passing Validator'),
    ];
939 940 941 942 943 944
  }
}

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

947 948 949
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
950 951 952
    return _validators ??= <DoctorValidator>[
      PartialValidatorWithHintsOnly(),
    ];
953 954 955 956 957
  }
}

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

960 961 962
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
963 964 965 966 967 968
    return _validators ??= <DoctorValidator>[
      PassingValidator('Passing Validator'),
      PassingValidator('Another Passing Validator'),
      PassingValidator('Validators are fun'),
      PassingValidator('Four score and seven validators ago'),
    ];
969
  }
970 971
}

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

976 977 978 979 980 981 982 983 984 985 986 987 988 989 990
  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;
  }
}

991 992
/// A doctor with a validator that throws an exception.
class FakeAsyncCrashingDoctor extends Doctor {
993
  FakeAsyncCrashingDoctor(this._time, Logger logger) : super(logger: logger);
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011

  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;
  }
}

1012 1013 1014 1015 1016 1017
/// A DoctorValidatorsProvider that overrides the default validators without
/// overriding the doctor.
class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
  @override
  List<DoctorValidator> get validators {
    return <DoctorValidator>[
1018 1019
      PassingValidator('Passing Validator'),
      PassingValidator('Another Passing Validator'),
1020
      PassingValidator('Providing validators is fun'),
1021 1022
    ];
  }
1023 1024 1025 1026 1027 1028

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

class PassingGroupedValidator extends DoctorValidator {
1029
  PassingGroupedValidator(String name) : super(name);
1030 1031 1032

  @override
  Future<ValidationResult> validate() async {
1033
    const List<ValidationMessage> messages = <ValidationMessage>[
1034 1035
      ValidationMessage('A helpful message'),
    ];
1036
    return const ValidationResult(ValidationType.installed, messages);
1037 1038 1039 1040
  }
}

class MissingGroupedValidator extends DoctorValidator {
1041
  MissingGroupedValidator(String name) : super(name);
1042 1043 1044

  @override
  Future<ValidationResult> validate() async {
1045
    const List<ValidationMessage> messages = <ValidationMessage>[
1046 1047
      ValidationMessage.error('A useful error message'),
    ];
1048
    return const ValidationResult(ValidationType.missing, messages);
1049 1050 1051 1052
  }
}

class PartialGroupedValidator extends DoctorValidator {
1053
  PartialGroupedValidator(String name) : super(name);
1054 1055 1056

  @override
  Future<ValidationResult> validate() async {
1057
    const List<ValidationMessage> messages = <ValidationMessage>[
1058 1059
      ValidationMessage.error('An error message for partial installation'),
    ];
1060
    return const ValidationResult(ValidationType.partial, messages);
1061 1062 1063
  }
}

1064 1065 1066 1067 1068
class PassingGroupedValidatorWithStatus extends DoctorValidator {
  PassingGroupedValidatorWithStatus(String name) : super(name);

  @override
  Future<ValidationResult> validate() async {
1069
    const List<ValidationMessage> messages = <ValidationMessage>[
1070 1071
      ValidationMessage('A different message'),
    ];
1072
    return const ValidationResult(ValidationType.installed, messages, statusInfo: 'A status message');
1073 1074 1075 1076
  }
}

/// A doctor that has two groups of two validators each.
1077
class FakeGroupedDoctor extends Doctor {
1078 1079
  FakeGroupedDoctor(Logger logger) : super(logger: logger);

1080 1081 1082
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
1083 1084
    return _validators ??= <DoctorValidator>[
      GroupedValidator(<DoctorValidator>[
1085
        PassingGroupedValidator('Category 1'),
1086
        PassingGroupedValidator('Category 1'),
1087 1088
      ]),
      GroupedValidator(<DoctorValidator>[
1089
        PassingGroupedValidator('Category 2'),
1090
        MissingGroupedValidator('Category 2'),
1091 1092
      ]),
    ];
1093 1094 1095
  }
}

1096
class FakeGroupedDoctorWithStatus extends Doctor {
1097 1098
  FakeGroupedDoctorWithStatus(Logger logger) : super(logger: logger);

1099 1100 1101
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
1102
    return _validators ??= <DoctorValidator>[
1103 1104 1105
      GroupedValidator(<DoctorValidator>[
        PassingGroupedValidator('First validator title'),
        PassingGroupedValidatorWithStatus('Second validator title'),
1106 1107
      ]),
    ];
1108 1109 1110
  }
}

1111
class FlutterValidatorDoctor extends Doctor {
1112 1113
  FlutterValidatorDoctor(Logger logger) : super(logger: logger);

1114 1115 1116
  List<DoctorValidator> _validators;
  @override
  List<DoctorValidator> get validators {
1117 1118 1119
    return _validators ??= <DoctorValidator>[
      FlutterValidator(),
    ];
1120 1121 1122
  }
}

1123 1124 1125
/// A doctor that takes any two validators. Used to check behavior when
/// merging ValidationTypes (installed, missing, partial).
class FakeSmallGroupDoctor extends Doctor {
1126
  FakeSmallGroupDoctor(Logger logger, DoctorValidator val1, DoctorValidator val2) : super(logger: logger) {
1127
    _validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])];
1128
  }
1129 1130 1131

  List<DoctorValidator> _validators;

1132 1133
  @override
  List<DoctorValidator> get validators => _validators;
1134 1135
}

1136
class VsCodeValidatorTestTargets extends VsCodeValidator {
1137
  VsCodeValidatorTestTargets._(String installDirectory, String extensionDirectory, {String edition})
1138
    : super(VsCode.fromDirectory(installDirectory, extensionDirectory, edition: edition));
1139 1140

  static VsCodeValidatorTestTargets get installedWithExtension =>
1141
      VsCodeValidatorTestTargets._(validInstall, validExtensions);
1142

1143
  static VsCodeValidatorTestTargets get installedWithExtension64bit =>
1144
      VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition');
1145

1146
  static VsCodeValidatorTestTargets get installedWithoutExtension =>
1147
      VsCodeValidatorTestTargets._(validInstall, missingExtensions);
1148

1149 1150 1151
  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');
1152
}
1153 1154

class MockProcessManager extends Mock implements ProcessManager {}
1155
class MockArtifacts extends Mock implements Artifacts {}
1156
class MockPlistParser extends Mock implements PlistParser {}