// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:args/command_runner.dart'; import 'package:fake_async/fake_async.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_studio_validator.dart'; import 'package:flutter_tools/src/android/android_workflow.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/doctor.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor_validator.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/vscode/vscode.dart'; import 'package:flutter_tools/src/vscode/vscode_validator.dart'; import 'package:flutter_tools/src/web/workflow.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; import '../../src/test_flutter_command_runner.dart'; void main() { late FakeFlutterVersion flutterVersion; late BufferLogger logger; late FakeProcessManager fakeProcessManager; setUp(() { flutterVersion = FakeFlutterVersion(); logger = BufferLogger.test(); fakeProcessManager = FakeProcessManager.empty(); }); 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)); }); group('doctor', () { 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 .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); expect(message.message, 'Flutter extension version 4.5.6'); expect(message.isError, isFalse); }); testUsingContext('No IDE Validator includes expected installation messages', () async { final ValidationResult result = await NoIdeValidator().validate(); expect(result.type, ValidationType.notAvailable); expect( result.messages.map((ValidationMessage vm) => vm.message), UserMessages().noIdeInstallationInfo, ); }); 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 .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); expect(message.message, 'Flutter extension version 4.5.6'); }); testUsingContext('vs code validator when extension missing', () async { final ValidationResult result = await VsCodeValidatorTestTargets.installedWithoutExtension.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 .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); 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); }); group('device validator', () { testWithoutContext('no devices', () async { final FakeDeviceManager deviceManager = FakeDeviceManager(); final DeviceValidator deviceValidator = DeviceValidator( deviceManager: deviceManager, userMessages: UserMessages(), ); final ValidationResult result = await deviceValidator.validate(); expect(result.type, ValidationType.notAvailable); expect(result.messages, const [ ValidationMessage.hint('No devices available'), ]); expect(result.statusInfo, isNull); }); testWithoutContext('diagnostic message', () async { final FakeDeviceManager deviceManager = FakeDeviceManager() ..diagnostics = ['Device locked']; final DeviceValidator deviceValidator = DeviceValidator( deviceManager: deviceManager, userMessages: UserMessages(), ); final ValidationResult result = await deviceValidator.validate(); expect(result.type, ValidationType.notAvailable); expect(result.messages, const [ ValidationMessage.hint('Device locked'), ]); expect(result.statusInfo, isNull); }); testWithoutContext('diagnostic message and devices', () async { final FakeDevice device = FakeDevice(); final FakeDeviceManager deviceManager = FakeDeviceManager() ..devices = [device] ..diagnostics = ['Device locked']; final DeviceValidator deviceValidator = DeviceValidator( deviceManager: deviceManager, userMessages: UserMessages(), ); final ValidationResult result = await deviceValidator.validate(); expect(result.type, ValidationType.installed); expect(result.messages, const [ ValidationMessage('name (mobile) • device-id • android • 1.2.3'), ValidationMessage.hint('Device locked'), ]); expect(result.statusInfo, '1 available'); }); }); }); group('doctor with overridden validators', () { testUsingContext('validate non-verbose output format for run without issues', () async { final Doctor doctor = Doctor(logger: logger); expect(await doctor.diagnose(verbose: false), isTrue); expect(logger.statusText, equals( '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: { AnsiTerminal: () => FakeTerminal(), DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), }); }); group('doctor usage params', () { late TestUsage testUsage; setUp(() { testUsage = TestUsage(); }); testUsingContext('contains installed', () async { final Doctor doctor = Doctor(logger: logger); await doctor.diagnose(verbose: false); expect(testUsage.events.length, 3); expect(testUsage.events, contains( const TestUsageEvent( 'doctor-result', 'PassingValidator', label: 'installed', ), )); }, overrides: { DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), Usage: () => testUsage, }); testUsingContext('contains installed and partial', () async { await FakePassingDoctor(logger).diagnose(verbose: false); expect(testUsage.events, unorderedEquals([ const TestUsageEvent( 'doctor-result', 'PassingValidator', label: 'installed', ), const TestUsageEvent( 'doctor-result', 'PassingValidator', label: 'installed', ), const TestUsageEvent( 'doctor-result', 'PartialValidatorWithHintsOnly', label: 'partial', ), const TestUsageEvent( 'doctor-result', 'PartialValidatorWithErrors', label: 'partial', ), ])); }, overrides: { Usage: () => testUsage, }); testUsingContext('contains installed, missing and partial', () async { await FakeDoctor(logger).diagnose(verbose: false); expect(testUsage.events, unorderedEquals([ const TestUsageEvent( 'doctor-result', 'PassingValidator', label: 'installed', ), const TestUsageEvent( 'doctor-result', 'MissingValidator', label: 'missing', ), const TestUsageEvent( 'doctor-result', 'NotAvailableValidator', label: 'notAvailable', ), const TestUsageEvent( 'doctor-result', 'PartialValidatorWithHintsOnly', label: 'partial', ), const TestUsageEvent( 'doctor-result', 'PartialValidatorWithErrors', label: 'partial', ), ])); }, overrides: { Usage: () => testUsage, }); testUsingContext('events for grouped validators are properly decomposed', () async { await FakeGroupedDoctor(logger).diagnose(verbose: false); expect(testUsage.events, unorderedEquals([ const TestUsageEvent( 'doctor-result', 'PassingGroupedValidator', label: 'installed', ), const TestUsageEvent( 'doctor-result', 'PassingGroupedValidator', label: 'installed', ), const TestUsageEvent( 'doctor-result', 'PassingGroupedValidator', label: 'installed', ), const TestUsageEvent( 'doctor-result', 'MissingGroupedValidator', label: 'missing', ), ])); }, overrides: { Usage: () => testUsage, }); testUsingContext('sending events can be skipped', () async { await FakePassingDoctor(logger).diagnose(verbose: false, sendEvent: false); expect(testUsage.events, isEmpty); }, overrides: { Usage: () => testUsage, }); }); group('doctor with fake validators', () { testUsingContext('validate non-verbose output format for run without issues', () async { expect(await FakeQuietDoctor(logger).diagnose(verbose: false), isTrue); expect(logger.statusText, equals( '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' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate non-verbose output format for run with crash', () async { expect(await FakeCrashingDoctor(logger).diagnose(verbose: false), isFalse); expect(logger.statusText, equals( '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' ' ✗ Bad state: 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: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate verbose output format contains trace for run with crash', () async { expect(await FakeCrashingDoctor(logger).diagnose(), isFalse); expect(logger.statusText, contains('#0 CrashingValidator.validate')); }); testUsingContext('validate tool exit when exceeding timeout', () async { FakeAsync().run((FakeAsync time) { final Doctor doctor = FakeAsyncStuckDoctor(logger); doctor.diagnose(verbose: false); time.elapse(Doctor.doctorDuration + const Duration(seconds: 1)); time.flushMicrotasks(); }); expect(logger.statusText, contains('Stuck validator that never completes exceeded maximum allowed duration of ')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate non-verbose output format for run with an async crash', () async { final Completer completer = Completer(); await FakeAsync().run((FakeAsync time) { unawaited(FakeAsyncCrashingDoctor(time, logger).diagnose(verbose: false).then((bool r) { expect(r, isFalse); completer.complete(null); })); time.elapse(const Duration(seconds: 1)); time.flushMicrotasks(); return completer.future; }); expect(logger.statusText, equals( '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' ' ✗ Bad state: 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: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate non-verbose output format when only one category fails', () async { expect(await FakeSinglePassingDoctor(logger).diagnose(verbose: false), isTrue); expect(logger.statusText, equals( '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' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate non-verbose output format for a passing run', () async { expect(await FakePassingDoctor(logger).diagnose(verbose: false), isTrue); expect(logger.statusText, equals( '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' ' ✗ An error message indicating partial installation\n' ' ! Maybe a hint will help the user\n' '[✓] Another Passing Validator (with statusInfo)\n' '\n' '! Doctor found issues in 2 categories.\n' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate non-verbose output format', () async { expect(await FakeDoctor(logger).diagnose(verbose: false), isFalse); expect(logger.statusText, equals( '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' '[!] Not Available Validator\n' ' ✗ A useful error message\n' ' ! A hint message\n' '[!] Partial Validator with only a Hint\n' ' ! There is a hint here\n' '[!] Partial Validator with Errors\n' ' ✗ An error message indicating partial installation\n' ' ! Maybe a hint will help the user\n' '\n' '! Doctor found issues in 4 categories.\n' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate verbose output format', () async { expect(await FakeDoctor(logger).diagnose(), isFalse); expect(logger.statusText, equals( '[✓] 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' '[!] Not Available Validator\n' ' ✗ A useful error message\n' ' • A message that is not an error\n' ' ! A hint message\n' '\n' '[!] 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' ' ✗ An error message indicating partial installation\n' ' ! Maybe a hint will help the user\n' ' • An extra message with some verbose details\n' '\n' '! Doctor found issues in 4 categories.\n' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); 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' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); }); group('doctor diagnosis wrapper', () { late TestUsage testUsage; late BufferLogger logger; setUp(() { testUsage = TestUsage(); logger = BufferLogger.test(); }); testUsingContext('PII separated, events only sent once', () async { final Doctor fakeDoctor = FakePiiDoctor(logger); final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor); const String expectedPiiText = '[✓] PII Validator\n' ' • Contains PII path/to/username\n' '\n' '• No issues found!\n'; const String expectedPiiStrippedText = '[✓] PII Validator\n' ' • Does not contain PII\n' '\n' '• No issues found!\n'; // Run each multiple times to make sure the logger buffer is being cleared, // and that events are only sent once. expect(await doctorText.text, expectedPiiText); expect(await doctorText.text, expectedPiiText); expect(await doctorText.piiStrippedText, expectedPiiStrippedText); expect(await doctorText.piiStrippedText, expectedPiiStrippedText); // Only one event sent. expect(testUsage.events, [ const TestUsageEvent( 'doctor-result', 'PiiValidator', label: 'installed', ), ]); }, overrides: { AnsiTerminal: () => FakeTerminal(), 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: { Usage: () => testUsage, }); }); testUsingContext('validate non-verbose output wrapping', () async { final BufferLogger wrapLogger = BufferLogger.test( outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30), ); expect(await FakeDoctor(wrapLogger).diagnose(verbose: false), isFalse); expect(wrapLogger.statusText, equals( '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: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate verbose output wrapping', () async { final BufferLogger wrapLogger = BufferLogger.test( outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 30), ); expect(await FakeDoctor(wrapLogger).diagnose(), isFalse); expect(wrapLogger.statusText, equals( '[✓] 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: { AnsiTerminal: () => FakeTerminal(), }); group('doctor with grouped validators', () { testUsingContext('validate diagnose combines validator output', () async { expect(await FakeGroupedDoctor(logger).diagnose(), isTrue); expect(logger.statusText, equals( '[✓] 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' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate merging assigns statusInfo and title', () async { // There are two subvalidators. Only the second contains statusInfo. expect(await FakeGroupedDoctorWithStatus(logger).diagnose(), isTrue); expect(logger.statusText, equals( '[✓] First validator title (A status message)\n' ' • A helpful message\n' ' • A different message\n' '\n' '• No issues found!\n' )); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); }); group('grouped validator merging results', () { final PassingGroupedValidator installed = PassingGroupedValidator('Category'); final PartialGroupedValidator partial = PartialGroupedValidator('Category'); final MissingGroupedValidator missing = MissingGroupedValidator('Category'); testUsingContext('validate installed + installed = installed', () async { expect(await FakeSmallGroupDoctor(logger, installed, installed).diagnose(), isTrue); expect(logger.statusText, startsWith('[✓]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate installed + partial = partial', () async { expect(await FakeSmallGroupDoctor(logger, installed, partial).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate installed + missing = partial', () async { expect(await FakeSmallGroupDoctor(logger, installed, missing).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate partial + installed = partial', () async { expect(await FakeSmallGroupDoctor(logger, partial, installed).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate partial + partial = partial', () async { expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate partial + missing = partial', () async { expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate missing + installed = partial', () async { expect(await FakeSmallGroupDoctor(logger, missing, installed).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate missing + partial = partial', () async { expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(), isTrue); expect(logger.statusText, startsWith('[!]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); testUsingContext('validate missing + missing = missing', () async { expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(), isFalse); expect(logger.statusText, startsWith('[✗]')); }, overrides: { AnsiTerminal: () => FakeTerminal(), }); }); testUsingContext('WebWorkflow is a part of validator workflows if enabled', () async { final List workflows = DoctorValidatorsProvider.test( featureFlags: TestFeatureFlags(isWebEnabled: true), platform: FakePlatform(), ).workflows; expect( workflows, contains(isA()), ); }, overrides: { FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => fakeProcessManager, }); testUsingContext('Fetches tags to get the right version', () async { Cache.disableLocking(); final DoctorCommand doctorCommand = DoctorCommand(); final CommandRunner commandRunner = createTestCommandRunner(doctorCommand); await commandRunner.run(['doctor']); expect(flutterVersion.didFetchTagsAndUpdate, true); Cache.enableLocking(); }, overrides: { ProcessManager: () => FakeProcessManager.any(), FileSystem: () => MemoryFileSystem.test(), FlutterVersion: () => flutterVersion, Doctor: () => NoOpDoctor(), }, initializeFlutterRoot: false); testUsingContext('If android workflow is disabled, AndroidStudio validator is not included', () { final DoctorValidatorsProvider provider = DoctorValidatorsProvider.test( featureFlags: TestFeatureFlags(isAndroidEnabled: false), ); expect(provider.validators, isNot(contains(isA()))); expect(provider.validators, isNot(contains(isA()))); }, overrides: { AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false), }); } class FakeAndroidWorkflow extends Fake implements AndroidWorkflow { FakeAndroidWorkflow({ this.canListDevices = true, this.appliesToHostPlatform = true, }); @override final bool canListDevices; @override final bool appliesToHostPlatform; } class NoOpDoctor implements Doctor { @override bool get canLaunchAnything => true; @override bool get canListAnything => true; @override Future checkRemoteArtifacts(String engineRevision) async => true; @override Future diagnose({ bool androidLicenses = false, bool verbose = true, bool showColor = true, AndroidLicenseValidator? androidLicenseValidator, bool showPii = true, List? startedValidatorTasks, bool sendEvent = true, }) async => true; @override List startValidatorTasks() => []; @override Future summary() async { } @override List get validators => []; @override List get workflows => []; } class PassingValidator extends DoctorValidator { PassingValidator(super.name); @override Future validate() async { const List messages = [ ValidationMessage('A helpful message'), ValidationMessage('A second, somewhat longer helpful message'), ]; return const ValidationResult(ValidationType.installed, messages, statusInfo: 'with statusInfo'); } } class PiiValidator extends DoctorValidator { PiiValidator() : super('PII Validator'); @override Future validate() async { const List messages = [ ValidationMessage('Contains PII path/to/username', piiStrippedMessage: 'Does not contain PII'), ]; return const ValidationResult(ValidationType.installed, messages); } } class MissingValidator extends DoctorValidator { MissingValidator() : super('Missing Validator'); @override Future validate() async { const List messages = [ ValidationMessage.error('A useful error message'), ValidationMessage('A message that is not an error'), ValidationMessage.hint('A hint message'), ]; return const ValidationResult(ValidationType.missing, messages); } } class NotAvailableValidator extends DoctorValidator { NotAvailableValidator() : super('Not Available Validator'); @override Future validate() async { const List messages = [ ValidationMessage.error('A useful error message'), ValidationMessage('A message that is not an error'), ValidationMessage.hint('A hint message'), ]; return const ValidationResult(ValidationType.notAvailable, messages); } } class StuckValidator extends DoctorValidator { StuckValidator() : super('Stuck validator that never completes'); @override Future validate() { final Completer completer = Completer(); // This future will never complete return completer.future; } } class PartialValidatorWithErrors extends DoctorValidator { PartialValidatorWithErrors() : super('Partial Validator with Errors'); @override Future validate() async { const List messages = [ 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'), ]; return const ValidationResult(ValidationType.partial, messages); } } class PartialValidatorWithHintsOnly extends DoctorValidator { PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint'); @override Future validate() async { const List messages = [ ValidationMessage.hint('There is a hint here'), ValidationMessage('But there is no error'), ]; return const ValidationResult(ValidationType.partial, messages); } } class CrashingValidator extends DoctorValidator { CrashingValidator() : super('Crashing validator'); @override Future validate() async { throw StateError('fatal error'); } } class AsyncCrashingValidator extends DoctorValidator { AsyncCrashingValidator(this._time) : super('Async crashing validator'); final FakeAsync _time; @override Future validate() { const Duration delay = Duration(seconds: 1); final Future result = Future.delayed( delay, () => throw StateError('fatal error'), ); _time.elapse(const Duration(seconds: 1)); _time.flushMicrotasks(); return result; } } /// A doctor that fails with a missing [ValidationResult]. class FakeDoctor extends Doctor { FakeDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PassingValidator('Passing Validator'), MissingValidator(), NotAvailableValidator(), PartialValidatorWithHintsOnly(), PartialValidatorWithErrors(), ]; } /// A doctor that should pass, but still has issues in some categories. class FakePassingDoctor extends Doctor { FakePassingDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PassingValidator('Passing Validator'), PartialValidatorWithHintsOnly(), PartialValidatorWithErrors(), PassingValidator('Another Passing Validator'), ]; } /// A doctor that should pass, but still has 1 issue to test the singular of /// categories. class FakeSinglePassingDoctor extends Doctor { FakeSinglePassingDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PartialValidatorWithHintsOnly(), ]; } /// A doctor that passes and has no issues anywhere. class FakeQuietDoctor extends Doctor { FakeQuietDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PassingValidator('Passing Validator'), PassingValidator('Another Passing Validator'), PassingValidator('Validators are fun'), PassingValidator('Four score and seven validators ago'), ]; } /// A doctor that passes and contains PII that can be hidden. class FakePiiDoctor extends Doctor { FakePiiDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PiiValidator(), ]; } /// A doctor with a validator that throws an exception. class FakeCrashingDoctor extends Doctor { FakeCrashingDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PassingValidator('Passing Validator'), PassingValidator('Another Passing Validator'), CrashingValidator(), PassingValidator('Validators are fun'), PassingValidator('Four score and seven validators ago'), ]; } /// A doctor with a validator that will never finish. class FakeAsyncStuckDoctor extends Doctor { FakeAsyncStuckDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ PassingValidator('Passing Validator'), PassingValidator('Another Passing Validator'), StuckValidator(), PassingValidator('Validators are fun'), PassingValidator('Four score and seven validators ago'), ]; } /// A doctor with a validator that throws an exception. class FakeAsyncCrashingDoctor extends Doctor { FakeAsyncCrashingDoctor(this._time, Logger logger) : super(logger: logger); final FakeAsync _time; @override late final List validators = [ PassingValidator('Passing Validator'), PassingValidator('Another Passing Validator'), AsyncCrashingValidator(_time), PassingValidator('Validators are fun'), PassingValidator('Four score and seven validators ago'), ]; } /// A DoctorValidatorsProvider that overrides the default validators without /// overriding the doctor. class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider { @override List get validators { return [ PassingValidator('Passing Validator'), PassingValidator('Another Passing Validator'), PassingValidator('Providing validators is fun'), ]; } @override List get workflows => []; } class PassingGroupedValidator extends DoctorValidator { PassingGroupedValidator(super.name); @override Future validate() async { const List messages = [ ValidationMessage('A helpful message'), ]; return const ValidationResult(ValidationType.installed, messages); } } class MissingGroupedValidator extends DoctorValidator { MissingGroupedValidator(super.name); @override Future validate() async { const List messages = [ ValidationMessage.error('A useful error message'), ]; return const ValidationResult(ValidationType.missing, messages); } } class PartialGroupedValidator extends DoctorValidator { PartialGroupedValidator(super.name); @override Future validate() async { const List messages = [ ValidationMessage.error('An error message for partial installation'), ]; return const ValidationResult(ValidationType.partial, messages); } } class PassingGroupedValidatorWithStatus extends DoctorValidator { PassingGroupedValidatorWithStatus(super.name); @override Future validate() async { const List messages = [ ValidationMessage('A different message'), ]; return const ValidationResult(ValidationType.installed, messages, statusInfo: 'A status message'); } } /// A doctor that has two groups of two validators each. class FakeGroupedDoctor extends Doctor { FakeGroupedDoctor(Logger logger) : super(logger: logger); @override late final List validators = [ GroupedValidator([ PassingGroupedValidator('Category 1'), PassingGroupedValidator('Category 1'), ]), GroupedValidator([ PassingGroupedValidator('Category 2'), MissingGroupedValidator('Category 2'), ]), ]; } class FakeGroupedDoctorWithStatus extends Doctor { FakeGroupedDoctorWithStatus(Logger logger) : super(logger: logger); @override late final List validators = [ GroupedValidator([ PassingGroupedValidator('First validator title'), PassingGroupedValidatorWithStatus('Second validator title'), ]), ]; } /// A doctor that takes any two validators. Used to check behavior when /// merging ValidationTypes (installed, missing, partial). class FakeSmallGroupDoctor extends Doctor { FakeSmallGroupDoctor(Logger logger, DoctorValidator val1, DoctorValidator val2) : validators = [GroupedValidator([val1, val2])], super(logger: logger); @override final List validators; } class VsCodeValidatorTestTargets extends VsCodeValidator { VsCodeValidatorTestTargets._(String installDirectory, String extensionDirectory, {String? edition}) : super(VsCode.fromDirectory(installDirectory, extensionDirectory, edition: edition, fileSystem: globals.fs)); static VsCodeValidatorTestTargets get installedWithExtension => VsCodeValidatorTestTargets._(validInstall, validExtensions); static VsCodeValidatorTestTargets get installedWithExtension64bit => VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition'); static VsCodeValidatorTestTargets get installedWithoutExtension => VsCodeValidatorTestTargets._(validInstall, missingExtensions); 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'); } class FakeDeviceManager extends Fake implements DeviceManager { List diagnostics = []; List devices = []; @override Future> getAllConnectedDevices() async => devices; @override Future> getDeviceDiagnostics() async => diagnostics; } // 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 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 get isLocalEmulator async => false; @override Future get targetPlatformDisplayName async => 'android'; @override Future get sdkNameAndVersion async => '1.2.3'; @override Future get targetPlatform => Future.value(TargetPlatform.android); } class FakeTerminal extends Fake implements AnsiTerminal { @override final bool supportsColor = false; }