flutter/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart
Andrew Kolos db9591cfbf
delete Usage in doctor tests (#162646)
Tiny delete op. Toward https://github.com/flutter/flutter/issues/150575

<details>

<summary> Pre-launch checklist </summary> 


- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

</details>


<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-02-04 18:32:06 +00:00

1518 lines
53 KiB
Dart

// 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: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/time.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/custom_devices/custom_device_workflow.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/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 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
void main() {
late BufferLogger logger;
late FakeProcessManager fakeProcessManager;
late MemoryFileSystem fs;
setUp(() {
logger = BufferLogger.test();
fakeProcessManager = FakeProcessManager.empty();
fs = MemoryFileSystem.test();
});
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.success);
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.success);
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.success);
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>[
ValidationMessage.hint('No devices available'),
]);
expect(result.statusInfo, isNull);
});
testWithoutContext('diagnostic message', () async {
final FakeDeviceManager deviceManager =
FakeDeviceManager()..diagnostics = <String>['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>[ValidationMessage.hint('Device locked')]);
expect(result.statusInfo, isNull);
});
testWithoutContext('diagnostic message and devices', () async {
final FakeDevice device = FakeDevice();
final FakeDeviceManager deviceManager =
FakeDeviceManager()
..devices = <Device>[device]
..diagnostics = <String>['Device locked'];
final DeviceValidator deviceValidator = DeviceValidator(
deviceManager: deviceManager,
userMessages: UserMessages(),
);
final ValidationResult result = await deviceValidator.validate();
expect(result.type, ValidationType.success);
expect(result.messages, const <ValidationMessage>[
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, clock: const SystemClock());
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: <Type, Generator>{
AnsiTerminal: () => FakeTerminal(),
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
},
);
});
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: <Type, Generator>{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: <Type, Generator>{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<void>((FakeAsync time) {
final Doctor doctor = FakeAsyncStuckDoctor(logger);
doctor.diagnose(verbose: false);
time.elapse(const Duration(minutes: 5));
time.flushMicrotasks();
});
expect(
logger.statusText,
contains('Stuck validator that never completes exceeded maximum allowed duration of '),
);
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate non-verbose output format for run with an async crash',
() async {
final Completer<void> completer = Completer<void>();
await FakeAsync().run((FakeAsync time) {
unawaited(
FakeAsyncCrashingDoctor(time, logger).diagnose(verbose: false).then((bool r) {
expect(r, isFalse);
completer.complete();
}),
);
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: <Type, Generator>{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: <Type, Generator>{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: <Type, Generator>{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: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext('validate verbose output format', () async {
expect(await FakeDoctor(logger).diagnose(), isFalse);
expect(
logger.statusText,
equals(
'[✓] Passing Validator (with statusInfo) [0ms]\n'
' • A helpful message\n'
' • A second, somewhat longer helpful message\n'
'\n'
'[✗] Missing Validator [0ms]\n'
' ✗ A useful error message\n'
' • A message that is not an error\n'
' ! A hint message\n'
'\n'
'[!] Not Available Validator [0ms]\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 [0ms]\n'
' ! There is a hint here\n'
' • But there is no error\n'
'\n'
'[!] Partial Validator with Errors [0ms]\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: <Type, Generator>{AnsiTerminal: () => FakeTerminal()});
testUsingContext('validate PII can be hidden', () async {
expect(await FakePiiDoctor(logger).diagnose(showPii: false), isTrue);
expect(
logger.statusText,
equals(
'[✓] PII Validator [0ms]\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 [0ms]\n'
' • Contains PII path/to/username\n'
'\n'
'• No issues found!\n',
),
);
}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()});
});
group('doctor diagnosis wrapper', () {
late FakeAnalytics analytics;
late BufferLogger logger;
setUp(() {
analytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(),
);
logger = BufferLogger.test();
});
testUsingContext(
'PII separated, events only sent once',
() async {
final DateTime fakeDate = DateTime(1995, 3, 3);
final SystemClock systemClock = SystemClock.fixed(fakeDate);
final Doctor fakeDoctor = FakePiiDoctor(logger, clock: systemClock);
final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor);
const String expectedPiiText =
'[✓] PII Validator [0ms]\n'
' • Contains PII path/to/username\n'
'\n'
'• No issues found!\n';
const String expectedPiiStrippedText =
'[✓] PII Validator [0ms]\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(analytics.sentEvents, <Event>[
Event.doctorValidatorResult(
validatorName: 'PII Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: systemClock.now().millisecondsSinceEpoch,
),
]);
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal(), Analytics: () => analytics},
);
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);
});
});
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: <Type, Generator>{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) [0ms]\n'
' • A helpful message\n'
' • A second, somewhat\n'
' longer helpful message\n'
'\n'
'[✗] Missing Validator [0ms]\n'
' ✗ A useful error message\n'
' • A message that is not an\n'
' error\n'
' ! A hint message\n'
'\n'
'[!] Not Available Validator\n'
' [0ms]\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 [0ms]\n'
' ! There is a hint here\n'
' • But there is no error\n'
'\n'
'[!] Partial Validator with\n'
' Errors [0ms]\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>{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 [0ms]\n'
' • A helpful message\n'
' • A helpful message\n'
'\n'
'[!] Category 2 [0ms]\n'
' • A helpful message\n'
' ✗ A useful error message\n'
'\n'
'! Doctor found issues in 1 category.\n',
),
);
},
overrides: <Type, Generator>{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) [0ms]\n'
' • A helpful message\n'
' • A different message\n'
'\n'
'• No issues found!\n',
),
);
},
overrides: <Type, Generator>{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: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate installed + partial = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, installed, partial).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate installed + missing = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, installed, missing).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate partial + installed = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, partial, installed).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate partial + partial = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate partial + missing = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate missing + installed = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, missing, installed).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate missing + partial = partial',
() async {
expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(), isTrue);
expect(logger.statusText, startsWith('[!]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext(
'validate missing + missing = missing',
() async {
expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(), isFalse);
expect(logger.statusText, startsWith('[✗]'));
},
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
});
testUsingContext(
'WebWorkflow is a part of validator workflows if enabled',
() async {
final List<Workflow> workflows =
DoctorValidatorsProvider.test(
featureFlags: TestFeatureFlags(isWebEnabled: true),
platform: FakePlatform(),
).workflows;
expect(workflows, contains(isA<WebWorkflow>()));
},
overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => fakeProcessManager,
},
);
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: () => fs, ProcessManager: () => fakeProcessManager},
);
group('FlutterValidator', () {
late FakeFlutterVersion initialVersion;
late FakeFlutterVersion secondVersion;
late TestFeatureFlags featureFlags;
setUp(() {
secondVersion = FakeFlutterVersion(frameworkRevisionShort: '222');
initialVersion = FakeFlutterVersion(
frameworkRevisionShort: '111',
nextFlutterVersion: secondVersion,
);
featureFlags = TestFeatureFlags();
});
testUsingContext(
'FlutterValidator fetches tags and gets fresh version',
() async {
final Directory devtoolsDir = fs.directory(
'/path/to/flutter/bin/cache/dart-sdk/bin/resources/devtools',
)..createSync(recursive: true);
fs.directory('/path/to/flutter/bin/cache/artifacts').createSync(recursive: true);
devtoolsDir.childFile('version.json').writeAsStringSync('{"version": "123"}');
fakeProcessManager.addCommands(const <FakeCommand>[
FakeCommand(command: <String>['which', 'java']),
]);
final List<DoctorValidator> validators =
DoctorValidatorsProvider.test(
featureFlags: featureFlags,
platform: FakePlatform(),
).validators;
final FlutterValidator flutterValidator = validators.whereType<FlutterValidator>().first;
final ValidationResult result = await flutterValidator.validate();
expect(
result.messages.map((ValidationMessage msg) => msg.message),
contains(contains('Framework revision 222')),
);
},
overrides: <Type, Generator>{
Cache:
() => Cache.test(
rootOverride: fs.directory('/path/to/flutter'),
fileSystem: fs,
processManager: fakeProcessManager,
),
FileSystem: () => fs,
FlutterVersion: () => initialVersion,
Platform: () => FakePlatform(),
ProcessManager: () => fakeProcessManager,
TestFeatureFlags: () => featureFlags,
},
);
});
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<AndroidStudioValidator>())));
expect(provider.validators, isNot(contains(isA<NoAndroidStudioValidator>())));
},
overrides: <Type, Generator>{
AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false),
},
);
group('Doctor events with unified_analytics', () {
late FakeAnalytics fakeAnalytics;
final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion();
final DateTime fakeDate = DateTime(1995, 3, 3);
final SystemClock fakeSystemClock = SystemClock.fixed(fakeDate);
setUp(() {
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fakeFlutterVersion: fakeFlutterVersion,
fs: fs,
);
});
testUsingContext(
'ensure fake is being used and initialized',
() {
expect(fakeAnalytics.sentEvents.length, 0);
expect(fakeAnalytics.okToSend, true);
},
overrides: <Type, Generator>{Analytics: () => fakeAnalytics},
);
testUsingContext(
'contains installed',
() async {
final Doctor doctor = Doctor(
logger: logger,
clock: fakeSystemClock,
analytics: fakeAnalytics,
);
await doctor.diagnose(verbose: false);
expect(fakeAnalytics.sentEvents.length, 3);
// The event that should have been fired off during the doctor invocation
final Event eventToFind = Event.doctorValidatorResult(
validatorName: 'Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: DateTime(1995, 3, 3).millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
);
expect(fakeAnalytics.sentEvents, contains(eventToFind));
},
overrides: <Type, Generator>{DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider()},
);
testUsingContext(
'contains installed and partial',
() async {
await FakePassingDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(4));
expect(
fakeAnalytics.sentEvents,
unorderedEquals(<Event>[
Event.doctorValidatorResult(
validatorName: 'Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with only a Hint',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with Errors',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Another Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
),
]),
);
},
overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Analytics: () => fakeAnalytics,
},
);
testUsingContext(
'contains installed, missing and partial',
() async {
await FakeDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(5));
expect(
fakeAnalytics.sentEvents,
unorderedEquals(<Event>[
Event.doctorValidatorResult(
validatorName: 'Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
),
Event.doctorValidatorResult(
validatorName: 'Missing Validator',
result: 'missing',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Not Available Validator',
result: 'notAvailable',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with only a Hint',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with Errors',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
]),
);
},
overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Analytics: () => fakeAnalytics,
},
);
testUsingContext(
'events for grouped validators are properly decomposed',
() async {
await FakeGroupedDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(4));
expect(
fakeAnalytics.sentEvents,
unorderedEquals(<Event>[
Event.doctorValidatorResult(
validatorName: 'Category 1',
result: 'installed',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Category 1',
result: 'installed',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Category 2',
result: 'installed',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Category 2',
result: 'missing',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
]),
);
},
overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Analytics: () => fakeAnalytics,
},
);
testUsingContext(
'grouped validator subresult and subvalidators different lengths',
() async {
final FakeGroupedDoctorWithCrash fakeDoctor = FakeGroupedDoctorWithCrash(
logger,
clock: fakeSystemClock,
);
await fakeDoctor.diagnose(verbose: false);
expect(fakeDoctor.validators, hasLength(1));
expect(fakeDoctor.validators.first.runtimeType == FakeGroupedValidatorWithCrash, true);
expect(fakeAnalytics.sentEvents, hasLength(0));
// Attempt to send a random event to ensure that the
// analytics package is still working, despite not sending
// above (as expected)
final Event testEvent = Event.analyticsCollectionEnabled(status: true);
fakeAnalytics.send(testEvent);
expect(fakeAnalytics.sentEvents, hasLength(1));
expect(fakeAnalytics.sentEvents, contains(testEvent));
},
overrides: <Type, Generator>{Analytics: () => fakeAnalytics},
);
testUsingContext('sending events can be skipped', () async {
await FakePassingDoctor(logger).diagnose(verbose: false, sendEvent: false);
expect(fakeAnalytics.sentEvents, isEmpty);
}, overrides: <Type, Generator>{Analytics: () => fakeAnalytics});
});
}
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
FakeAndroidWorkflow({this.canListDevices = true, this.appliesToHostPlatform = true});
@override
final bool canListDevices;
@override
final bool appliesToHostPlatform;
}
class PassingValidator extends DoctorValidator {
PassingValidator(super.title);
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage('A helpful message'),
ValidationMessage('A second, somewhat longer helpful message'),
];
return ZeroExecutionTimeValidationResult(
ValidationType.success,
messages,
statusInfo: 'with statusInfo',
);
}
}
class PiiValidator extends DoctorValidator {
PiiValidator() : super('PII Validator');
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage(
'Contains PII path/to/username',
piiStrippedMessage: 'Does not contain PII',
),
];
return ZeroExecutionTimeValidationResult(ValidationType.success, messages);
}
}
class MissingValidator extends DoctorValidator {
MissingValidator() : super('Missing Validator');
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('A useful error message'),
ValidationMessage('A message that is not an error'),
ValidationMessage.hint('A hint message'),
];
return ZeroExecutionTimeValidationResult(ValidationType.missing, messages);
}
}
class NotAvailableValidator extends DoctorValidator {
NotAvailableValidator() : super('Not Available Validator');
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('A useful error message'),
ValidationMessage('A message that is not an error'),
ValidationMessage.hint('A hint message'),
];
return ZeroExecutionTimeValidationResult(ValidationType.notAvailable, messages);
}
}
class StuckValidator extends DoctorValidator {
StuckValidator() : super('Stuck validator that never completes');
@override
Future<ValidationResult> validateImpl() {
final Completer<ValidationResult> completer = Completer<ValidationResult>();
// This future will never complete
return completer.future;
}
}
class PartialValidatorWithErrors extends DoctorValidator {
PartialValidatorWithErrors() : super('Partial Validator with Errors');
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
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 ZeroExecutionTimeValidationResult(ValidationType.partial, messages);
}
}
class PartialValidatorWithHintsOnly extends DoctorValidator {
PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint');
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.hint('There is a hint here'),
ValidationMessage('But there is no error'),
];
return ZeroExecutionTimeValidationResult(ValidationType.partial, messages);
}
}
class CrashingValidator extends DoctorValidator {
CrashingValidator() : super('Crashing validator');
@override
Future<ValidationResult> validateImpl() async {
throw StateError('fatal error');
}
}
class AsyncCrashingValidator extends DoctorValidator {
AsyncCrashingValidator(this._time) : super('Async crashing validator');
final FakeAsync _time;
@override
Future<ValidationResult> validateImpl() {
const Duration delay = Duration(seconds: 1);
final Future<ValidationResult> result = Future<ValidationResult>.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.clock = const SystemClock()}) : super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
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.clock = const SystemClock()}) : super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
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.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[PartialValidatorWithHintsOnly()];
}
/// A doctor that passes and has no issues anywhere.
class FakeQuietDoctor extends Doctor {
FakeQuietDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
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.clock = const SystemClock()}) : super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[PiiValidator()];
}
/// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor {
FakeCrashingDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger);
@override
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'),
];
}
/// A doctor with a validator that will never finish.
class FakeAsyncStuckDoctor extends Doctor {
FakeAsyncStuckDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger);
@override
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'),
];
}
/// A doctor with a validator that throws an exception.
class FakeAsyncCrashingDoctor extends Doctor {
FakeAsyncCrashingDoctor(this._time, Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
final FakeAsync _time;
@override
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'),
];
}
/// A DoctorValidatorsProvider that overrides the default validators without
/// overriding the doctor.
class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
@override
List<DoctorValidator> get validators => <DoctorValidator>[
PassingValidator('Passing Validator'),
PassingValidator('Another Passing Validator'),
PassingValidator('Providing validators is fun'),
];
@override
List<Workflow> get workflows => <Workflow>[];
}
class PassingGroupedValidator extends DoctorValidator {
PassingGroupedValidator(super.title);
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage('A helpful message'),
];
return ZeroExecutionTimeValidationResult(ValidationType.success, messages);
}
}
class MissingGroupedValidator extends DoctorValidator {
MissingGroupedValidator(super.title);
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('A useful error message'),
];
return ZeroExecutionTimeValidationResult(ValidationType.missing, messages);
}
}
class PartialGroupedValidator extends DoctorValidator {
PartialGroupedValidator(super.title);
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('An error message for partial installation'),
];
return ZeroExecutionTimeValidationResult(ValidationType.partial, messages);
}
}
class PassingGroupedValidatorWithStatus extends DoctorValidator {
PassingGroupedValidatorWithStatus(super.title);
@override
Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage('A different message'),
];
return ZeroExecutionTimeValidationResult(
ValidationType.success,
messages,
statusInfo: 'A status message',
);
}
}
/// A doctor that has two groups of two validators each.
class FakeGroupedDoctor extends Doctor {
FakeGroupedDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
FakeGroupedValidator(<DoctorValidator>[
PassingGroupedValidator('Category 1'),
PassingGroupedValidator('Category 1'),
]),
FakeGroupedValidator(<DoctorValidator>[
PassingGroupedValidator('Category 2'),
MissingGroupedValidator('Category 2'),
]),
];
}
/// Fake grouped doctor that is intended to be used with [FakeGroupedValidatorWithCrash].
class FakeGroupedDoctorWithCrash extends Doctor {
FakeGroupedDoctorWithCrash(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
FakeGroupedValidatorWithCrash(<DoctorValidator>[
PassingGroupedValidator('Category 1'),
PassingGroupedValidator('Category 1'),
]),
];
}
/// This extended grouped validator will have a list of sub validators
/// provided in the constructor, but it will have no [subResults] in the
/// list which simulates what happens if a validator crashes.
///
/// Usually, the grouped validators have 2 lists, a [subValidators] and
/// a [subResults] list, and if nothing crashes, those 2 lists will have the
/// same length. This fake is simulating what happens when the validators
/// crash and results in no results getting returned.
class FakeGroupedValidatorWithCrash extends GroupedValidator {
FakeGroupedValidatorWithCrash(super.subValidators);
@override
List<ValidationResult> get subResults => <ValidationResult>[];
}
class FakeGroupedValidator extends GroupedValidator {
FakeGroupedValidator(super.subValidator);
@override
Future<ValidationResult> validateImpl() async {
final ValidationResult result = await super.validateImpl();
return ZeroExecutionTimeValidationResult(
result.type,
result.messages,
statusInfo: result.statusInfo,
);
}
}
class FakeGroupedDoctorWithStatus extends Doctor {
FakeGroupedDoctorWithStatus(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
FakeGroupedValidator(<DoctorValidator>[
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, {
super.clock = const SystemClock(),
}) : validators = <DoctorValidator>[
FakeGroupedValidator(<DoctorValidator>[val1, val2]),
],
super(logger: logger);
@override
final List<DoctorValidator> 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<String> diagnostics = <String>[];
List<Device> devices = <Device>[];
@override
Future<List<Device>> getAllDevices({DeviceDiscoveryFilter? filter}) async => devices;
@override
Future<List<Device>> refreshAllDevices({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async => devices;
@override
Future<List<String>> getDeviceDiagnostics() async => diagnostics;
}
class FakeDevice extends Fake implements Device {
@override
String get name => 'name';
@override
String get displayName => 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);
}
class FakeTerminal extends Fake implements AnsiTerminal {
@override
final bool supportsColor = false;
@override
bool get isCliAnimationEnabled => supportsColor;
}
class ZeroExecutionTimeValidationResult extends ValidationResult {
ZeroExecutionTimeValidationResult(super.type, super.messages, {super.statusInfo});
@override
Duration? get executionTime => Duration.zero;
}