Add validator execution times to flutter doctor --verbose (#158124)

Should help provide more information for `flutter doctor` timeouts like
we've seen in https://github.com/flutter/flutter/issues/157513
This commit is contained in:
Ben Konyi 2025-01-13 16:01:44 -05:00 committed by GitHub
parent 5f06c091b9
commit b3e65358d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 804 additions and 545 deletions

View File

@ -52,7 +52,7 @@ class AndroidStudioValidator extends DoctorValidator {
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
ValidationType type = ValidationType.missing; ValidationType type = ValidationType.missing;
@ -125,7 +125,7 @@ class NoAndroidStudioValidator extends DoctorValidator {
final UserMessages _userMessages; final UserMessages _userMessages;
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
final String? cfgAndroidStudio = _config.getValue('android-studio-dir') as String?; final String? cfgAndroidStudio = _config.getValue('android-studio-dir') as String?;

View File

@ -134,7 +134,7 @@ class AndroidValidator extends DoctorValidator {
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
final AndroidSdk? androidSdk = _androidSdk; final AndroidSdk? androidSdk = _androidSdk;
if (androidSdk == null) { if (androidSdk == null) {
@ -267,7 +267,7 @@ class AndroidLicenseValidator extends DoctorValidator {
String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...'; String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
// Match pre-existing early termination behavior // Match pre-existing early termination behavior

View File

@ -63,9 +63,11 @@ abstract class DoctorValidatorsProvider {
} }
/// The singleton instance, pulled from the [AppContext]. /// The singleton instance, pulled from the [AppContext].
static DoctorValidatorsProvider get _instance => context.get<DoctorValidatorsProvider>()!; static DoctorValidatorsProvider get _instance =>
context.get<DoctorValidatorsProvider>()!;
static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider( static final DoctorValidatorsProvider defaultInstance =
_DefaultDoctorValidatorsProvider(
logger: globals.logger, logger: globals.logger,
platform: globals.platform, platform: globals.platform,
featureFlags: featureFlags, featureFlags: featureFlags,
@ -93,7 +95,8 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
featureFlags: featureFlags, featureFlags: featureFlags,
); );
late final WebWorkflow webWorkflow = WebWorkflow(platform: platform, featureFlags: featureFlags); late final WebWorkflow webWorkflow =
WebWorkflow(platform: platform, featureFlags: featureFlags);
late final MacOSWorkflow macOSWorkflow = MacOSWorkflow( late final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
platform: platform, platform: platform,
@ -126,15 +129,16 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
processManager: globals.processManager, processManager: globals.processManager,
logger: _logger, logger: _logger,
), ),
...VsCodeValidator.installedValidators(globals.fs, platform, globals.processManager), ...VsCodeValidator.installedValidators(
globals.fs, platform, globals.processManager),
]; ];
final ProxyValidator proxyValidator = ProxyValidator(platform: platform); final ProxyValidator proxyValidator = ProxyValidator(platform: platform);
_validators = <DoctorValidator>[ _validators = <DoctorValidator>[
FlutterValidator( FlutterValidator(
fileSystem: globals.fs, fileSystem: globals.fs,
platform: globals.platform, platform: globals.platform,
flutterVersion: flutterVersion: () => globals.flutterVersion
() => globals.flutterVersion.fetchTagsAndGetVersion(clock: globals.systemClock), .fetchTagsAndGetVersion(clock: globals.systemClock),
devToolsVersion: () => globals.cache.devToolsVersion, devToolsVersion: () => globals.cache.devToolsVersion,
processManager: globals.processManager, processManager: globals.processManager,
userMessages: globals.userMessages, userMessages: globals.userMessages,
@ -152,8 +156,10 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
), ),
), ),
if (androidWorkflow!.appliesToHostPlatform) if (androidWorkflow!.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]), GroupedValidator(
if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform) <DoctorValidator>[androidValidator!, androidLicenseValidator!]),
if (globals.iosWorkflow!.appliesToHostPlatform ||
macOSWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[ GroupedValidator(<DoctorValidator>[
XcodeValidator( XcodeValidator(
xcode: globals.xcode!, xcode: globals.xcode!,
@ -183,7 +189,9 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
if (ideValidators.isNotEmpty) ...ideValidators else NoIdeValidator(), if (ideValidators.isNotEmpty) ...ideValidators else NoIdeValidator(),
if (proxyValidator.shouldShow) proxyValidator, if (proxyValidator.shouldShow) proxyValidator,
if (globals.deviceManager?.canListAnything ?? false) if (globals.deviceManager?.canListAnything ?? false)
DeviceValidator(deviceManager: globals.deviceManager, userMessages: globals.userMessages), DeviceValidator(
deviceManager: globals.deviceManager,
userMessages: globals.userMessages),
HttpHostValidator( HttpHostValidator(
platform: globals.platform, platform: globals.platform,
featureFlags: featureFlags, featureFlags: featureFlags,
@ -208,7 +216,10 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
} }
class Doctor { class Doctor {
Doctor({required Logger logger, required SystemClock clock, Analytics? analytics}) Doctor(
{required Logger logger,
required SystemClock clock,
Analytics? analytics})
: _logger = logger, : _logger = logger,
_clock = clock, _clock = clock,
_analytics = analytics ?? globals.analytics; _analytics = analytics ?? globals.analytics;
@ -233,7 +244,8 @@ class Doctor {
// onError callback to it and translate errors into ValidationResults. // onError callback to it and translate errors into ValidationResults.
asyncGuard<ValidationResult>( asyncGuard<ValidationResult>(
() { () {
final Completer<ValidationResult> timeoutCompleter = Completer<ValidationResult>(); final Completer<ValidationResult> timeoutCompleter =
Completer<ValidationResult>();
final Timer timer = Timer(doctorDuration, () { final Timer timer = Timer(doctorDuration, () {
timeoutCompleter.completeError( timeoutCompleter.completeError(
Exception( Exception(
@ -241,7 +253,8 @@ class Doctor {
), ),
); );
}); });
final Future<ValidationResult> validatorFuture = validator.validate(); final Future<ValidationResult> validatorFuture =
validator.validate();
return Future.any<ValidationResult>(<Future<ValidationResult>>[ return Future.any<ValidationResult>(<Future<ValidationResult>>[
validatorFuture, validatorFuture,
// This future can only complete with an error // This future can only complete with an error
@ -277,7 +290,8 @@ class Doctor {
final StringBuffer lineBuffer = StringBuffer(); final StringBuffer lineBuffer = StringBuffer();
ValidationResult result; ValidationResult result;
try { try {
result = await asyncGuard<ValidationResult>(() => validator.validate()); result =
await asyncGuard<ValidationResult>(() => validator.validateImpl());
} on Exception catch (exception) { } on Exception catch (exception) {
// We're generating a summary, so drop the stack trace. // We're generating a summary, so drop the stack trace.
result = ValidationResult.crash(exception); result = ValidationResult.crash(exception);
@ -290,7 +304,8 @@ class Doctor {
case ValidationType.missing: case ValidationType.missing:
lineBuffer.write('is not installed.'); lineBuffer.write('is not installed.');
case ValidationType.partial: case ValidationType.partial:
lineBuffer.write('is partially installed; more components are available.'); lineBuffer
.write('is partially installed; more components are available.');
case ValidationType.notAvailable: case ValidationType.notAvailable:
lineBuffer.write('is not available.'); lineBuffer.write('is not available.');
case ValidationType.success: case ValidationType.success:
@ -318,7 +333,8 @@ class Doctor {
if (sawACrash) { if (sawACrash) {
buffer.writeln(); buffer.writeln();
buffer.writeln('Run "flutter doctor" for information about why a doctor check crashed.'); buffer.writeln(
'Run "flutter doctor" for information about why a doctor check crashed.');
} }
if (missingComponent) { if (missingComponent) {
@ -332,7 +348,8 @@ class Doctor {
} }
Future<bool> checkRemoteArtifacts(String engineRevision) async { Future<bool> checkRemoteArtifacts(String engineRevision) async {
return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision); return globals.cache
.areRemoteArtifactsAvailable(engineVersion: engineRevision);
} }
/// Maximum allowed duration for an entire validator to take. /// Maximum allowed duration for an entire validator to take.
@ -360,7 +377,8 @@ class Doctor {
} }
if (!verbose) { if (!verbose) {
_logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):'); _logger.printStatus(
'Doctor summary (to see all details, run flutter doctor -v):');
} }
bool doctorResult = true; bool doctorResult = true;
int issues = 0; int issues = 0;
@ -369,7 +387,8 @@ class Doctor {
// were sent for each doctor validator and its result // were sent for each doctor validator and its result
final int analyticsTimestamp = _clock.now().millisecondsSinceEpoch; final int analyticsTimestamp = _clock.now().millisecondsSinceEpoch;
for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) { for (final ValidatorTask validatorTask
in startedValidatorTasks ?? startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator; final DoctorValidator validator = validatorTask.validator;
final Status status = _logger.startSpinner( final Status status = _logger.startSpinner(
timeout: validator.slowWarningDuration, timeout: validator.slowWarningDuration,
@ -437,29 +456,41 @@ class Doctor {
DoctorResultEvent(validator: validator, result: result).send(); DoctorResultEvent(validator: validator, result: result).send();
} }
final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox; final String executionDuration = () {
final Duration? executionTime = result.executionTime;
if (!verbose || executionTime == null) {
return '';
}
final String formatted = executionTime.inSeconds < 2
? getElapsedAsMilliseconds(executionTime)
: getElapsedAsSeconds(executionTime);
return ' [$formatted]';
}();
final String leadingBox =
showColor ? result.coloredLeadingBox : result.leadingBox;
if (result.statusInfo != null) { if (result.statusInfo != null) {
_logger.printStatus( _logger.printStatus(
'$leadingBox ${validator.title} (${result.statusInfo})', '$leadingBox ${validator.title} (${result.statusInfo})$executionDuration',
hangingIndent: result.leadingBox.length + 1, hangingIndent: result.leadingBox.length + 1);
);
} else { } else {
_logger.printStatus( _logger.printStatus('$leadingBox ${validator.title}$executionDuration',
'$leadingBox ${validator.title}', hangingIndent: result.leadingBox.length + 1);
hangingIndent: result.leadingBox.length + 1,
);
} }
for (final ValidationMessage message in result.messages) { for (final ValidationMessage message in result.messages) {
if (!message.isInformation || verbose) { if (!message.isInformation || verbose) {
int hangingIndent = 2; int hangingIndent = 2;
int indent = 4; int indent = 4;
final String indicator = showColor ? message.coloredIndicator : message.indicator; final String indicator =
showColor ? message.coloredIndicator : message.indicator;
for (final String line for (final String line
in '$indicator ${showPii ? message.message : message.piiStrippedMessage}'.split( in '$indicator ${showPii ? message.message : message.piiStrippedMessage}'
.split(
'\n', '\n',
)) { )) {
_logger.printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true); _logger.printStatus(line,
hangingIndent: hangingIndent, indent: indent, emphasis: true);
// Only do hanging indent for the first line. // Only do hanging indent for the first line.
hangingIndent = 0; hangingIndent = 0;
indent = 6; indent = 6;
@ -501,7 +532,8 @@ class Doctor {
return doctorResult; return doctorResult;
} }
bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices); bool get canListAnything =>
workflows.any((Workflow workflow) => workflow.canListDevices);
bool get canLaunchAnything { bool get canLaunchAnything {
if (FlutterTesterDevices.showFlutterTesterDevice) { if (FlutterTesterDevices.showFlutterTesterDevice) {
@ -549,7 +581,7 @@ class FlutterValidator extends DoctorValidator {
final OperatingSystemUtils _operatingSystemUtils; final OperatingSystemUtils _operatingSystemUtils;
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
String? versionChannel; String? versionChannel;
String? frameworkVersion; String? frameworkVersion;
@ -561,7 +593,8 @@ class FlutterValidator extends DoctorValidator {
frameworkVersion = version.frameworkVersion; frameworkVersion = version.frameworkVersion;
final String flutterRoot = _flutterRoot(); final String flutterRoot = _flutterRoot();
messages.add(_getFlutterVersionMessage(frameworkVersion, versionChannel, flutterRoot)); messages.add(_getFlutterVersionMessage(
frameworkVersion, versionChannel, flutterRoot));
_validateRequiredBinaries(flutterRoot).forEach(messages.add); _validateRequiredBinaries(flutterRoot).forEach(messages.add);
messages.add(_getFlutterUpstreamMessage(version)); messages.add(_getFlutterUpstreamMessage(version));
@ -577,16 +610,21 @@ class FlutterValidator extends DoctorValidator {
), ),
), ),
); );
messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort))); messages.add(ValidationMessage(
messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion))); _userMessages.engineRevision(version.engineRevisionShort)));
messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion()))); messages.add(ValidationMessage(
_userMessages.dartRevision(version.dartSdkVersion)));
messages.add(
ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
final String? pubUrl = _platform.environment[kPubDevOverride]; final String? pubUrl = _platform.environment[kPubDevOverride];
if (pubUrl != null) { if (pubUrl != null) {
messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl))); messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
} }
final String? storageBaseUrl = _platform.environment[kFlutterStorageBaseUrl]; final String? storageBaseUrl =
_platform.environment[kFlutterStorageBaseUrl];
if (storageBaseUrl != null) { if (storageBaseUrl != null) {
messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl))); messages.add(
ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
} }
} on VersionCheckError catch (e) { } on VersionCheckError catch (e) {
messages.add(ValidationMessage.error(e.message)); messages.add(ValidationMessage.error(e.message));
@ -595,8 +633,10 @@ class FlutterValidator extends DoctorValidator {
// Check that the binaries we downloaded for this platform actually run on it. // Check that the binaries we downloaded for this platform actually run on it.
// If the binaries are not downloaded (because android is not enabled), then do // If the binaries are not downloaded (because android is not enabled), then do
// not run this check. // not run this check.
final String genSnapshotPath = _artifacts.getArtifactPath(Artifact.genSnapshot); final String genSnapshotPath =
if (_fileSystem.file(genSnapshotPath).existsSync() && !_genSnapshotRuns(genSnapshotPath)) { _artifacts.getArtifactPath(Artifact.genSnapshot);
if (_fileSystem.file(genSnapshotPath).existsSync() &&
!_genSnapshotRuns(genSnapshotPath)) {
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
buffer.writeln(_userMessages.flutterBinariesDoNotRun); buffer.writeln(_userMessages.flutterBinariesDoNotRun);
if (_platform.isLinux) { if (_platform.isLinux) {
@ -606,7 +646,8 @@ class FlutterValidator extends DoctorValidator {
buffer.writeln( buffer.writeln(
'Flutter requires the Rosetta translation environment on ARM Macs. Try running:', 'Flutter requires the Rosetta translation environment on ARM Macs. Try running:',
); );
buffer.writeln(' sudo softwareupdate --install-rosetta --agree-to-license'); buffer.writeln(
' sudo softwareupdate --install-rosetta --agree-to-license');
} }
messages.add(ValidationMessage.error(buffer.toString())); messages.add(ValidationMessage.error(buffer.toString()));
} }
@ -619,7 +660,8 @@ class FlutterValidator extends DoctorValidator {
// in that case, make it clear that it is fine to continue, but freshness check/upgrades // in that case, make it clear that it is fine to continue, but freshness check/upgrades
// won't be supported. // won't be supported.
valid = ValidationType.partial; valid = ValidationType.partial;
messages.add(ValidationMessage(_userMessages.flutterValidatorErrorIntentional)); messages.add(
ValidationMessage(_userMessages.flutterValidatorErrorIntentional));
} }
return ValidationResult( return ValidationResult(
@ -653,17 +695,21 @@ class FlutterValidator extends DoctorValidator {
return ValidationMessage(flutterVersionMessage); return ValidationMessage(flutterVersionMessage);
} }
if (versionChannel == kUserBranch) { if (versionChannel == kUserBranch) {
flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}'; flutterVersionMessage =
'$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}';
} }
if (frameworkVersion == '0.0.0-unknown') { if (frameworkVersion == '0.0.0-unknown') {
flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownVersion}'; flutterVersionMessage =
'$flutterVersionMessage\n${_userMessages.flutterUnknownVersion}';
} }
return ValidationMessage.hint(flutterVersionMessage); return ValidationMessage.hint(flutterVersionMessage);
} }
List<ValidationMessage> _validateRequiredBinaries(String flutterRoot) { List<ValidationMessage> _validateRequiredBinaries(String flutterRoot) {
final ValidationMessage? flutterWarning = _validateSdkBinary('flutter', flutterRoot); final ValidationMessage? flutterWarning =
final ValidationMessage? dartWarning = _validateSdkBinary('dart', flutterRoot); _validateSdkBinary('flutter', flutterRoot);
final ValidationMessage? dartWarning =
_validateSdkBinary('dart', flutterRoot);
return <ValidationMessage>[ return <ValidationMessage>[
if (flutterWarning != null) flutterWarning, if (flutterWarning != null) flutterWarning,
if (dartWarning != null) dartWarning, if (dartWarning != null) dartWarning,
@ -684,8 +730,7 @@ class FlutterValidator extends DoctorValidator {
} }
final String resolvedFlutterPath = flutterBin.resolveSymbolicLinksSync(); final String resolvedFlutterPath = flutterBin.resolveSymbolicLinksSync();
if (!_filePathContainsDirPath(flutterRoot, resolvedFlutterPath)) { if (!_filePathContainsDirPath(flutterRoot, resolvedFlutterPath)) {
final String hint = final String hint = 'Warning: `$binary` on your path resolves to '
'Warning: `$binary` on your path resolves to '
'$resolvedFlutterPath, which is not inside your current Flutter ' '$resolvedFlutterPath, which is not inside your current Flutter '
'SDK checkout at $flutterRoot. Consider adding $flutterBinDir to ' 'SDK checkout at $flutterRoot. Consider adding $flutterBinDir to '
'the front of your path.'; 'the front of your path.';
@ -697,9 +742,8 @@ class FlutterValidator extends DoctorValidator {
bool _filePathContainsDirPath(String directory, String file) { bool _filePathContainsDirPath(String directory, String file) {
// calling .canonicalize() will normalize for alphabetic case and path // calling .canonicalize() will normalize for alphabetic case and path
// separators // separators
return _fileSystem.path return _fileSystem.path.canonicalize(file).startsWith(
.canonicalize(file) _fileSystem.path.canonicalize(directory) + _fileSystem.path.separator);
.startsWith(_fileSystem.path.canonicalize(directory) + _fileSystem.path.separator);
} }
ValidationMessage _getFlutterUpstreamMessage(FlutterVersion version) { ValidationMessage _getFlutterUpstreamMessage(FlutterVersion version) {
@ -710,11 +754,14 @@ class FlutterValidator extends DoctorValidator {
// VersionUpstreamValidator can produce an error if repositoryUrl is null // VersionUpstreamValidator can produce an error if repositoryUrl is null
if (upstreamValidationError != null) { if (upstreamValidationError != null) {
final String errorMessage = upstreamValidationError.message; final String errorMessage = upstreamValidationError.message;
if (errorMessage.contains('could not determine the remote upstream which is being tracked')) { if (errorMessage.contains(
return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUnknown); 'could not determine the remote upstream which is being tracked')) {
return ValidationMessage.hint(
_userMessages.flutterUpstreamRepositoryUnknown);
} }
// At this point, repositoryUrl must not be null // At this point, repositoryUrl must not be null
if (errorMessage.contains('Flutter SDK is tracking a non-standard remote')) { if (errorMessage
.contains('Flutter SDK is tracking a non-standard remote')) {
return ValidationMessage.hint( return ValidationMessage.hint(
_userMessages.flutterUpstreamRepositoryUrlNonStandard(repositoryUrl!), _userMessages.flutterUpstreamRepositoryUrlNonStandard(repositoryUrl!),
); );
@ -727,13 +774,15 @@ class FlutterValidator extends DoctorValidator {
); );
} }
} }
return ValidationMessage(_userMessages.flutterUpstreamRepositoryUrl(repositoryUrl!)); return ValidationMessage(
_userMessages.flutterUpstreamRepositoryUrl(repositoryUrl!));
} }
bool _genSnapshotRuns(String genSnapshotPath) { bool _genSnapshotRuns(String genSnapshotPath) {
const int kExpectedExitCode = 255; const int kExpectedExitCode = 255;
try { try {
return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode; return _processManager.runSync(<String>[genSnapshotPath]).exitCode ==
kExpectedExitCode;
} on Exception { } on Exception {
return false; return false;
} }
@ -754,24 +803,26 @@ class DeviceValidator extends DoctorValidator {
String get slowWarning => 'Scanning for devices is taking a long time...'; String get slowWarning => 'Scanning for devices is taking a long time...';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<Device> devices = await _deviceManager.refreshAllDevices( final List<Device> devices = await _deviceManager.refreshAllDevices(
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout, timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
); );
List<ValidationMessage> installedMessages = <ValidationMessage>[]; List<ValidationMessage> installedMessages = <ValidationMessage>[];
if (devices.isNotEmpty) { if (devices.isNotEmpty) {
installedMessages = installedMessages = (await Device.descriptions(
(await Device.descriptions(
devices, devices,
)).map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList(); ))
.map<ValidationMessage>((String msg) => ValidationMessage(msg))
.toList();
} }
List<ValidationMessage> diagnosticMessages = <ValidationMessage>[]; List<ValidationMessage> diagnosticMessages = <ValidationMessage>[];
final List<String> diagnostics = await _deviceManager.getDeviceDiagnostics(); final List<String> diagnostics =
await _deviceManager.getDeviceDiagnostics();
if (diagnostics.isNotEmpty) { if (diagnostics.isNotEmpty) {
diagnosticMessages = diagnosticMessages = diagnostics
diagnostics .map<ValidationMessage>(
.map<ValidationMessage>((String message) => ValidationMessage.hint(message)) (String message) => ValidationMessage.hint(message))
.toList(); .toList();
} else if (devices.isEmpty) { } else if (devices.isEmpty) {
diagnosticMessages = <ValidationMessage>[ diagnosticMessages = <ValidationMessage>[
@ -800,8 +851,10 @@ class DeviceValidator extends DoctorValidator {
/// Wrapper for doctor to run multiple times with PII and without, running the validators only once. /// Wrapper for doctor to run multiple times with PII and without, running the validators only once.
class DoctorText { class DoctorText {
DoctorText(BufferLogger logger, {SystemClock? clock, @visibleForTesting Doctor? doctor}) DoctorText(BufferLogger logger,
: _doctor = doctor ?? Doctor(logger: logger, clock: clock ?? globals.systemClock), {SystemClock? clock, @visibleForTesting Doctor? doctor})
: _doctor = doctor ??
Doctor(logger: logger, clock: clock ?? globals.systemClock),
_logger = logger; _logger = logger;
final BufferLogger _logger; final BufferLogger _logger;
@ -812,7 +865,8 @@ class DoctorText {
late final Future<String> piiStrippedText = _runDiagnosis(false); late final Future<String> piiStrippedText = _runDiagnosis(false);
// Start the validator tasks only once. // Start the validator tasks only once.
late final List<ValidatorTask> _validatorTasks = _doctor.startValidatorTasks(); late final List<ValidatorTask> _validatorTasks =
_doctor.startValidatorTasks();
Future<String> _runDiagnosis(bool showPii) async { Future<String> _runDiagnosis(bool showPii) async {
try { try {

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'base/async_guard.dart'; import 'base/async_guard.dart';
@ -36,7 +38,7 @@ enum ValidationType { crash, missing, partial, notAvailable, success }
enum ValidationMessageType { error, hint, information } enum ValidationMessageType { error, hint, information }
abstract class DoctorValidator { abstract class DoctorValidator {
const DoctorValidator(this.title); DoctorValidator(this.title);
/// This is displayed in the CLI. /// This is displayed in the CLI.
final String title; final String title;
@ -48,7 +50,19 @@ abstract class DoctorValidator {
/// Duration before the spinner should display [slowWarning]. /// Duration before the spinner should display [slowWarning].
Duration get slowWarningDuration => _slowWarningDuration; Duration get slowWarningDuration => _slowWarningDuration;
Future<ValidationResult> validate(); /// Performs validation by invoking [validateImpl].
///
/// Tracks time taken to execute the validation step.
Future<ValidationResult> validate() async {
final Stopwatch stopwatch = Stopwatch()..start();
final ValidationResult result = await validateImpl();
stopwatch.stop();
result._executionTime = stopwatch.elapsed;
return result;
}
/// Validation implementation.
Future<ValidationResult> validateImpl();
} }
/// A validator that runs other [DoctorValidator]s and combines their output /// A validator that runs other [DoctorValidator]s and combines their output
@ -74,10 +88,11 @@ class GroupedValidator extends DoctorValidator {
String _currentSlowWarning = 'Initializing...'; String _currentSlowWarning = 'Initializing...';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidatorTask> tasks = <ValidatorTask>[ final List<ValidatorTask> tasks = <ValidatorTask>[
for (final DoctorValidator validator in subValidators) for (final DoctorValidator validator in subValidators)
ValidatorTask(validator, asyncGuard<ValidationResult>(() => validator.validate())), ValidatorTask(validator,
asyncGuard<ValidationResult>(() => validator.validate())),
]; ];
final List<ValidationResult> results = <ValidationResult>[]; final List<ValidationResult> results = <ValidationResult>[];
@ -123,14 +138,15 @@ class GroupedValidator extends DoctorValidator {
} }
} }
@immutable
class ValidationResult { class ValidationResult {
/// [ValidationResult.type] should only equal [ValidationResult.success] /// [ValidationResult.type] should only equal [ValidationResult.success]
/// if no [messages] are hints or errors. /// if no [messages] are hints or errors.
const ValidationResult(this.type, this.messages, {this.statusInfo}); ValidationResult(this.type, this.messages, {this.statusInfo});
factory ValidationResult.crash(Object error, [StackTrace? stackTrace]) { factory ValidationResult.crash(Object error, [StackTrace? stackTrace]) {
return ValidationResult(ValidationType.crash, <ValidationMessage>[ return ValidationResult(
ValidationType.crash,
<ValidationMessage>[
const ValidationMessage.error( const ValidationMessage.error(
'Due to an error, the doctor check did not complete. ' 'Due to an error, the doctor check did not complete. '
'If the error message below is not helpful, ' 'If the error message below is not helpful, '
@ -140,7 +156,8 @@ class ValidationResult {
if (stackTrace != null) if (stackTrace != null)
// Stacktrace is informational. Printed in verbose mode only. // Stacktrace is informational. Printed in verbose mode only.
ValidationMessage('$stackTrace'), ValidationMessage('$stackTrace'),
], statusInfo: 'the doctor check crashed'); ],
statusInfo: 'the doctor check crashed');
} }
final ValidationType type; final ValidationType type;
@ -155,11 +172,19 @@ class ValidationResult {
ValidationType.notAvailable || ValidationType.partial => '[!]', ValidationType.notAvailable || ValidationType.partial => '[!]',
}; };
/// The time taken to perform the validation, set by [DoctorValidator.validate].
Duration? get executionTime => _executionTime;
Duration? _executionTime;
String get coloredLeadingBox { String get coloredLeadingBox {
return globals.terminal.color(leadingBox, switch (type) { return globals.terminal.color(
leadingBox,
switch (type) {
ValidationType.success => TerminalColor.green, ValidationType.success => TerminalColor.green,
ValidationType.crash || ValidationType.missing => TerminalColor.red, ValidationType.crash || ValidationType.missing => TerminalColor.red,
ValidationType.notAvailable || ValidationType.partial => TerminalColor.yellow, ValidationType.notAvailable ||
ValidationType.partial =>
TerminalColor.yellow,
}); });
} }
@ -192,7 +217,8 @@ class ValidationMessage {
/// ///
/// The [contextUrl] may be supplied to link to external resources. This /// The [contextUrl] may be supplied to link to external resources. This
/// is displayed after the informative message in verbose modes. /// is displayed after the informative message in verbose modes.
const ValidationMessage(this.message, {this.contextUrl, String? piiStrippedMessage}) const ValidationMessage(this.message,
{this.contextUrl, String? piiStrippedMessage})
: type = ValidationMessageType.information, : type = ValidationMessageType.information,
piiStrippedMessage = piiStrippedMessage ?? message; piiStrippedMessage = piiStrippedMessage ?? message;
@ -229,7 +255,9 @@ class ValidationMessage {
}; };
String get coloredIndicator { String get coloredIndicator {
return globals.terminal.color(indicator, switch (type) { return globals.terminal.color(
indicator,
switch (type) {
ValidationMessageType.error => TerminalColor.red, ValidationMessageType.error => TerminalColor.red,
ValidationMessageType.hint => TerminalColor.yellow, ValidationMessageType.hint => TerminalColor.yellow,
ValidationMessageType.information => TerminalColor.green, ValidationMessageType.information => TerminalColor.green,
@ -255,7 +283,7 @@ class NoIdeValidator extends DoctorValidator {
NoIdeValidator() : super('Flutter IDE Support'); NoIdeValidator() : super('Flutter IDE Support');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
return ValidationResult( return ValidationResult(
// Info hint to user they do not have a supported IDE installed // Info hint to user they do not have a supported IDE installed
ValidationType.notAvailable, ValidationType.notAvailable,
@ -273,5 +301,5 @@ class ValidatorWithResult extends DoctorValidator {
final ValidationResult result; final ValidationResult result;
@override @override
Future<ValidationResult> validate() async => result; Future<ValidationResult> validateImpl() async => result;
} }

View File

@ -18,7 +18,8 @@ const String kMaven = 'https://maven.google.com/';
const String kPubDev = 'https://pub.dev/'; const String kPubDev = 'https://pub.dev/';
// Overridable environment variables. // Overridable environment variables.
const String kPubDevOverride = 'PUB_HOSTED_URL'; // https://dart.dev/tools/pub/environment-variables const String kPubDevOverride =
'PUB_HOSTED_URL'; // https://dart.dev/tools/pub/environment-variables
// Validator that checks all provided hosts are reachable and responsive // Validator that checks all provided hosts are reachable and responsive
class HttpHostValidator extends DoctorValidator { class HttpHostValidator extends DoctorValidator {
@ -82,7 +83,7 @@ class HttpHostValidator extends DoctorValidator {
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<String?> availabilityResults = <String?>[]; final List<String?> availabilityResults = <String?>[];
final List<Uri> requiredHosts = <Uri>[]; final List<Uri> requiredHosts = <Uri>[];
@ -100,7 +101,8 @@ class HttpHostValidator extends DoctorValidator {
requiredHosts.add(Uri.parse(kPubDev)); requiredHosts.add(Uri.parse(kPubDev));
} }
if (_platform.environment.containsKey(kFlutterStorageBaseUrl)) { if (_platform.environment.containsKey(kFlutterStorageBaseUrl)) {
final Uri? url = _parseUrl(_platform.environment[kFlutterStorageBaseUrl]!); final Uri? url =
_parseUrl(_platform.environment[kFlutterStorageBaseUrl]!);
if (url == null) { if (url == null) {
availabilityResults.add( availabilityResults.add(
'Environment variable $kFlutterStorageBaseUrl does not specify a valid URL: "${_platform.environment[kFlutterStorageBaseUrl]}"\n' 'Environment variable $kFlutterStorageBaseUrl does not specify a valid URL: "${_platform.environment[kFlutterStorageBaseUrl]}"\n'
@ -141,9 +143,12 @@ class HttpHostValidator extends DoctorValidator {
if (failures == 0) { if (failures == 0) {
assert(successes > 0); assert(successes > 0);
assert(messages.isEmpty); assert(messages.isEmpty);
return const ValidationResult(ValidationType.success, <ValidationMessage>[ return ValidationResult(
ValidationMessage('All expected network resources are available.'), ValidationType.success,
]); const <ValidationMessage>[
ValidationMessage('All expected network resources are available.')
],
);
} }
assert(messages.isNotEmpty); assert(messages.isNotEmpty);
return ValidationResult( return ValidationResult(

View File

@ -91,7 +91,7 @@ abstract class IntelliJValidator extends DoctorValidator {
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
if (pluginsPath == null) { if (pluginsPath == null) {

View File

@ -54,7 +54,7 @@ class LinuxDoctorValidator extends DoctorValidator {
final List<String> _requiredGtkLibraries = <String>['gtk+-3.0', 'glib-2.0', 'gio-2.0']; final List<String> _requiredGtkLibraries = <String>['gtk+-3.0', 'glib-2.0', 'gio-2.0'];
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
ValidationType validationType = ValidationType.success; ValidationType validationType = ValidationType.success;
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];

View File

@ -20,7 +20,7 @@ class CocoaPodsValidator extends DoctorValidator {
final UserMessages _userMessages; final UserMessages _userMessages;
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
final CocoaPodsStatus cocoaPodsStatus = await _cocoaPods.evaluateCocoaPodsInstallation; final CocoaPodsStatus cocoaPodsStatus = await _cocoaPods.evaluateCocoaPodsInstallation;

View File

@ -32,7 +32,7 @@ class XcodeValidator extends DoctorValidator {
final UserMessages _userMessages; final UserMessages _userMessages;
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
ValidationType xcodeStatus = ValidationType.missing; ValidationType xcodeStatus = ValidationType.missing;
String? xcodeVersionInfo; String? xcodeVersionInfo;

View File

@ -30,9 +30,10 @@ class ProxyValidator extends DoctorValidator {
''; '';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
if (_httpProxy.isEmpty) { if (_httpProxy.isEmpty) {
return const ValidationResult(ValidationType.success, <ValidationMessage>[]); return ValidationResult(
ValidationType.success, const <ValidationMessage>[]);
} }
final List<ValidationMessage> messages = <ValidationMessage>[ final List<ValidationMessage> messages = <ValidationMessage>[
@ -49,13 +50,16 @@ class ProxyValidator extends DoctorValidator {
], ],
]; ];
final bool hasIssues = messages.any((ValidationMessage msg) => msg.isHint || msg.isError); final bool hasIssues =
messages.any((ValidationMessage msg) => msg.isHint || msg.isError);
return ValidationResult(hasIssues ? ValidationType.partial : ValidationType.success, messages); return ValidationResult(
hasIssues ? ValidationType.partial : ValidationType.success, messages);
} }
Future<List<String>> _getLoopbackAddresses() async { Future<List<String>> _getLoopbackAddresses() async {
final List<NetworkInterface> networkInterfaces = await listNetworkInterfaces( final List<NetworkInterface> networkInterfaces =
await listNetworkInterfaces(
includeLinkLocal: true, includeLinkLocal: true,
includeLoopback: true, includeLoopback: true,
); );
@ -63,7 +67,8 @@ class ProxyValidator extends DoctorValidator {
return <String>[ return <String>[
'localhost', 'localhost',
for (final NetworkInterface networkInterface in networkInterfaces) for (final NetworkInterface networkInterface in networkInterfaces)
for (final InternetAddress internetAddress in networkInterface.addresses) for (final InternetAddress internetAddress
in networkInterface.addresses)
if (internetAddress.isLoopback) internetAddress.address, if (internetAddress.isLoopback) internetAddress.address,
]; ];
} }

View File

@ -27,16 +27,17 @@ class VsCodeValidator extends DoctorValidator {
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> validationMessages = List<ValidationMessage>.from( final List<ValidationMessage> validationMessages =
_vsCode.validationMessages, List<ValidationMessage>.from(_vsCode.validationMessages);
);
final String vsCodeVersionText = final String vsCodeVersionText = _vsCode.version == null
_vsCode.version == null ? 'version unknown' : 'version ${_vsCode.version}'; ? 'version unknown'
: 'version ${_vsCode.version}';
if (_vsCode.version == null) { if (_vsCode.version == null) {
validationMessages.add(const ValidationMessage.error('Unable to determine VS Code version.')); validationMessages.add(const ValidationMessage.error(
'Unable to determine VS Code version.'));
} }
return ValidationResult( return ValidationResult(

View File

@ -8,14 +8,14 @@ import 'chrome.dart';
/// A validator for Chromium-based browsers. /// A validator for Chromium-based browsers.
abstract class ChromiumValidator extends DoctorValidator { abstract class ChromiumValidator extends DoctorValidator {
const ChromiumValidator(super.title); ChromiumValidator(super.title);
Platform get _platform; Platform get _platform;
ChromiumLauncher get _chromiumLauncher; ChromiumLauncher get _chromiumLauncher;
String get _name; String get _name;
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final bool canRunChromium = _chromiumLauncher.canFindExecutable(); final bool canRunChromium = _chromiumLauncher.canFindExecutable();
final String chromiumSearchLocation = _chromiumLauncher.findExecutable(); final String chromiumSearchLocation = _chromiumLauncher.findExecutable();
final List<ValidationMessage> messages = <ValidationMessage>[ final List<ValidationMessage> messages = <ValidationMessage>[
@ -45,8 +45,10 @@ abstract class ChromiumValidator extends DoctorValidator {
/// A validator that checks whether Chrome is installed and can run. /// A validator that checks whether Chrome is installed and can run.
class ChromeValidator extends ChromiumValidator { class ChromeValidator extends ChromiumValidator {
const ChromeValidator({required Platform platform, required ChromiumLauncher chromiumLauncher}) ChromeValidator({
: _platform = platform, required Platform platform,
required ChromiumLauncher chromiumLauncher,
}) : _platform = platform,
_chromiumLauncher = chromiumLauncher, _chromiumLauncher = chromiumLauncher,
super('Chrome - develop for the web'); super('Chrome - develop for the web');
@ -62,8 +64,10 @@ class ChromeValidator extends ChromiumValidator {
/// A validator that checks whether Edge is installed and can run. /// A validator that checks whether Edge is installed and can run.
class EdgeValidator extends ChromiumValidator { class EdgeValidator extends ChromiumValidator {
const EdgeValidator({required Platform platform, required ChromiumLauncher chromiumLauncher}) EdgeValidator({
: _platform = platform, required Platform platform,
required ChromiumLauncher chromiumLauncher,
}) : _platform = platform,
_chromiumLauncher = chromiumLauncher, _chromiumLauncher = chromiumLauncher,
super('Edge - develop for the web'); super('Edge - develop for the web');

View File

@ -10,7 +10,7 @@ import 'visual_studio.dart';
VisualStudioValidator? get visualStudioValidator => context.get<VisualStudioValidator>(); VisualStudioValidator? get visualStudioValidator => context.get<VisualStudioValidator>();
class VisualStudioValidator extends DoctorValidator { class VisualStudioValidator extends DoctorValidator {
const VisualStudioValidator({ VisualStudioValidator({
required VisualStudio visualStudio, required VisualStudio visualStudio,
required UserMessages userMessages, required UserMessages userMessages,
}) : _visualStudio = visualStudio, }) : _visualStudio = visualStudio,
@ -21,7 +21,7 @@ class VisualStudioValidator extends DoctorValidator {
final UserMessages _userMessages; final UserMessages _userMessages;
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
ValidationType status = ValidationType.missing; ValidationType status = ValidationType.missing;
String? versionInfo; String? versionInfo;

View File

@ -14,7 +14,8 @@ const List<String> kUnsupportedVersions = <String>['6', '7', '8'];
/// Regex pattern for identifying line from systeminfo stdout with windows version /// Regex pattern for identifying line from systeminfo stdout with windows version
/// (ie. 10.0.22631.4037) /// (ie. 10.0.22631.4037)
const String kWindowsOSVersionSemVerPattern = r'([0-9]+)\.([0-9]+)\.([0-9]+)\.?([0-9\.]+)?'; const String kWindowsOSVersionSemVerPattern =
r'([0-9]+)\.([0-9]+)\.([0-9]+)\.?([0-9\.]+)?';
/// Regex pattern for identifying a running instance of the Topaz OFD process. /// Regex pattern for identifying a running instance of the Topaz OFD process.
/// This is a known process that interferes with the build toolchain. /// This is a known process that interferes with the build toolchain.
@ -23,7 +24,7 @@ const String kCoreProcessPattern = r'Topaz\s+OFD\\Warsaw\\core\.exe';
/// Validator for supported Windows host machine operating system version. /// Validator for supported Windows host machine operating system version.
class WindowsVersionValidator extends DoctorValidator { class WindowsVersionValidator extends DoctorValidator {
const WindowsVersionValidator({ WindowsVersionValidator({
required OperatingSystemUtils operatingSystemUtils, required OperatingSystemUtils operatingSystemUtils,
required ProcessLister processLister, required ProcessLister processLister,
required WindowsVersionExtractor versionExtractor, required WindowsVersionExtractor versionExtractor,
@ -41,35 +42,47 @@ class WindowsVersionValidator extends DoctorValidator {
Future<ValidationResult> _topazScan() async { Future<ValidationResult> _topazScan() async {
if (!_processLister.canRunPowershell()) { if (!_processLister.canRunPowershell()) {
return const ValidationResult(ValidationType.missing, <ValidationMessage>[ return ValidationResult(
ValidationType.missing,
const <ValidationMessage>[
ValidationMessage.hint( ValidationMessage.hint(
'Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH', 'Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH'),
), ],
]); );
} }
final ProcessResult getProcessesResult = await _processLister.getProcessesWithPath(); final ProcessResult getProcessesResult =
await _processLister.getProcessesWithPath();
if (getProcessesResult.exitCode != 0) { if (getProcessesResult.exitCode != 0) {
return const ValidationResult(ValidationType.missing, <ValidationMessage>[ return ValidationResult(
ValidationType.missing,
const <ValidationMessage>[
ValidationMessage.hint('Get-Process failed to complete'), ValidationMessage.hint('Get-Process failed to complete'),
]); ],
);
} }
final RegExp topazRegex = RegExp(kCoreProcessPattern, caseSensitive: false, multiLine: true); final RegExp topazRegex =
RegExp(kCoreProcessPattern, caseSensitive: false, multiLine: true);
final String processes = getProcessesResult.stdout as String; final String processes = getProcessesResult.stdout as String;
final bool topazFound = topazRegex.hasMatch(processes); final bool topazFound = topazRegex.hasMatch(processes);
if (topazFound) { if (topazFound) {
return const ValidationResult(ValidationType.missing, <ValidationMessage>[ return ValidationResult(
ValidationType.missing,
const <ValidationMessage>[
ValidationMessage.hint( ValidationMessage.hint(
'The Topaz OFD Security Module was detected on your machine. ' 'The Topaz OFD Security Module was detected on your machine. '
'You may need to disable it to build Flutter applications.', 'You may need to disable it to build Flutter applications.',
), ),
]); ],
);
} }
return const ValidationResult(ValidationType.success, <ValidationMessage>[]); return ValidationResult(
ValidationType.success, const <ValidationMessage>[]);
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
final RegExp regex = RegExp(kWindowsOSVersionSemVerPattern, multiLine: true); final RegExp regex =
RegExp(kWindowsOSVersionSemVerPattern, multiLine: true);
final String commandResult = _operatingSystemUtils.name; final String commandResult = _operatingSystemUtils.name;
final Iterable<RegExpMatch> matches = regex.allMatches(commandResult); final Iterable<RegExpMatch> matches = regex.allMatches(commandResult);
@ -78,13 +91,15 @@ class WindowsVersionValidator extends DoctorValidator {
ValidationType windowsVersionStatus; ValidationType windowsVersionStatus;
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
String statusInfo; String statusInfo;
if (matches.length == 1 && !kUnsupportedVersions.contains(matches.elementAt(0).group(1))) { if (matches.length == 1 &&
!kUnsupportedVersions.contains(matches.elementAt(0).group(1))) {
windowsVersionStatus = ValidationType.success; windowsVersionStatus = ValidationType.success;
final WindowsVersionExtractionResult details = await _versionExtractor.getDetails(); final WindowsVersionExtractionResult details =
await _versionExtractor.getDetails();
String? caption = details.caption; String? caption = details.caption;
if (caption == null || caption.isEmpty) { if (caption == null || caption.isEmpty) {
final bool isWindows11 = final bool isWindows11 = int.parse(matches.elementAt(0).group(3)!) >
int.parse(matches.elementAt(0).group(3)!) > _lowestWindows11BuildNumber; _lowestWindows11BuildNumber;
if (isWindows11) { if (isWindows11) {
caption = 'Windows 11 or higher'; caption = 'Windows 11 or higher';
} else { } else {
@ -99,7 +114,9 @@ class WindowsVersionValidator extends DoctorValidator {
// Check if the Topaz OFD security module is running, and warn the user if it is. // Check if the Topaz OFD security module is running, and warn the user if it is.
// See https://github.com/flutter/flutter/issues/121366 // See https://github.com/flutter/flutter/issues/121366
final List<ValidationResult> subResults = <ValidationResult>[await _topazScan()]; final List<ValidationResult> subResults = <ValidationResult>[
await _topazScan()
];
for (final ValidationResult subResult in subResults) { for (final ValidationResult subResult in subResults) {
if (subResult.type != ValidationType.success) { if (subResult.type != ValidationType.success) {
statusInfo = 'Problem detected with Windows installation'; statusInfo = 'Problem detected with Windows installation';
@ -109,10 +126,12 @@ class WindowsVersionValidator extends DoctorValidator {
} }
} else { } else {
windowsVersionStatus = ValidationType.missing; windowsVersionStatus = ValidationType.missing;
statusInfo = 'Unable to determine Windows version (command `ver` returned $commandResult)'; statusInfo =
'Unable to determine Windows version (command `ver` returned $commandResult)';
} }
return ValidationResult(windowsVersionStatus, messages, statusInfo: statusInfo); return ValidationResult(windowsVersionStatus, messages,
statusInfo: statusInfo);
} }
} }
@ -147,7 +166,8 @@ class ProcessLister {
/// the edition and the processor architecture), releaseId and displayVersion and are returned via the /// the edition and the processor architecture), releaseId and displayVersion and are returned via the
/// [WindowsVersionExtractionResult] class. /// [WindowsVersionExtractionResult] class.
class WindowsVersionExtractor { class WindowsVersionExtractor {
WindowsVersionExtractor({required ProcessManager processManager, required Logger logger}) WindowsVersionExtractor(
{required ProcessManager processManager, required Logger logger})
: _logger = logger, : _logger = logger,
_processManager = processManager; _processManager = processManager;
@ -169,12 +189,16 @@ class WindowsVersionExtractor {
if (output != null) { if (output != null) {
final List<String> parts = output.split('\n'); final List<String> parts = output.split('\n');
if (parts.length >= 2) { if (parts.length >= 2) {
caption = parts[1].replaceAll('Microsoft Windows', '').replaceAll(' ', ' ').trim(); caption = parts[1]
.replaceAll('Microsoft Windows', '')
.replaceAll(' ', ' ')
.trim();
} }
} }
} }
} on ProcessException catch (e) { } on ProcessException catch (e) {
_logger.printTrace('Failed to get Caption and OSArchitecture from WMI: $e'); _logger
.printTrace('Failed to get Caption and OSArchitecture from WMI: $e');
} }
try { try {
@ -190,9 +214,13 @@ class WindowsVersionExtractor {
final String? output = osDetails.stdout as String?; final String? output = osDetails.stdout as String?;
if (output != null) { if (output != null) {
final Map<String, String> data = Map<String, String>.fromEntries( final Map<String, String> data = Map<String, String>.fromEntries(
output.split('\n').where((String line) => line.contains('REG_SZ')).map((String line) { output
.split('\n')
.where((String line) => line.contains('REG_SZ'))
.map((String line) {
final List<String> parts = line.split('REG_SZ'); final List<String> parts = line.split('REG_SZ');
return MapEntry<String, String>(parts.first.trim(), parts.last.trim()); return MapEntry<String, String>(
parts.first.trim(), parts.last.trim());
}), }),
); );
releaseId = data['ReleaseId']; releaseId = data['ReleaseId'];
@ -200,7 +228,8 @@ class WindowsVersionExtractor {
} }
} }
} on ProcessException catch (e) { } on ProcessException catch (e) {
_logger.printTrace('Failed to get ReleaseId and DisplayVersion from registry: $e'); _logger.printTrace(
'Failed to get ReleaseId and DisplayVersion from registry: $e');
} }
return WindowsVersionExtractionResult( return WindowsVersionExtractionResult(
@ -221,7 +250,8 @@ final class WindowsVersionExtractionResult {
}); });
factory WindowsVersionExtractionResult.empty() { factory WindowsVersionExtractionResult.empty() {
return WindowsVersionExtractionResult(caption: null, releaseId: null, displayVersion: null); return WindowsVersionExtractionResult(
caption: null, releaseId: null, displayVersion: null);
} }
final String? caption; final String? caption;

View File

@ -44,7 +44,8 @@ void main() {
fs = MemoryFileSystem.test(); fs = MemoryFileSystem.test();
}); });
testWithoutContext('ValidationMessage equality and hashCode includes contextUrl', () { testWithoutContext(
'ValidationMessage equality and hashCode includes contextUrl', () {
const ValidationMessage messageA = ValidationMessage('ab', contextUrl: 'a'); const ValidationMessage messageA = ValidationMessage('ab', contextUrl: 'a');
const ValidationMessage messageB = ValidationMessage('ab', contextUrl: 'b'); const ValidationMessage messageB = ValidationMessage('ab', contextUrl: 'b');
@ -65,7 +66,8 @@ void main() {
ValidationMessage message = result.messages.firstWhere( ValidationMessage message = result.messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('VS Code '), (ValidationMessage m) => m.message.startsWith('VS Code '),
); );
expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}'); expect(message.message,
'VS Code at ${VsCodeValidatorTestTargets.validInstall}');
message = result.messages.firstWhere( message = result.messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '), (ValidationMessage m) => m.message.startsWith('Flutter '),
@ -74,7 +76,8 @@ void main() {
expect(message.isError, isFalse); expect(message.isError, isFalse);
}); });
testUsingContext('No IDE Validator includes expected installation messages', () async { testUsingContext('No IDE Validator includes expected installation messages',
() async {
final ValidationResult result = await NoIdeValidator().validate(); final ValidationResult result = await NoIdeValidator().validate();
expect(result.type, ValidationType.notAvailable); expect(result.type, ValidationType.notAvailable);
@ -89,8 +92,9 @@ void main() {
VsCodeValidatorTestTargets.installedWithExtension64bit.title, VsCodeValidatorTestTargets.installedWithExtension64bit.title,
'VS Code, 64-bit edition', 'VS Code, 64-bit edition',
); );
final ValidationResult result = final ValidationResult result = await VsCodeValidatorTestTargets
await VsCodeValidatorTestTargets.installedWithExtension64bit.validate(); .installedWithExtension64bit
.validate();
expect(result.type, ValidationType.success); expect(result.type, ValidationType.success);
expect(result.statusInfo, 'version 1.2.3'); expect(result.statusInfo, 'version 1.2.3');
expect(result.messages, hasLength(2)); expect(result.messages, hasLength(2));
@ -98,7 +102,8 @@ void main() {
ValidationMessage message = result.messages.firstWhere( ValidationMessage message = result.messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('VS Code '), (ValidationMessage m) => m.message.startsWith('VS Code '),
); );
expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}'); expect(message.message,
'VS Code at ${VsCodeValidatorTestTargets.validInstall}');
message = result.messages.firstWhere( message = result.messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '), (ValidationMessage m) => m.message.startsWith('Flutter '),
@ -116,12 +121,14 @@ void main() {
ValidationMessage message = result.messages.firstWhere( ValidationMessage message = result.messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('VS Code '), (ValidationMessage m) => m.message.startsWith('VS Code '),
); );
expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}'); expect(message.message,
'VS Code at ${VsCodeValidatorTestTargets.validInstall}');
message = result.messages.firstWhere( message = result.messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '), (ValidationMessage m) => m.message.startsWith('Flutter '),
); );
expect(message.message, startsWith('Flutter extension can be installed from')); expect(message.message,
startsWith('Flutter extension can be installed from'));
expect( expect(
message.contextUrl, message.contextUrl,
'https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter', 'https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter',
@ -145,8 +152,8 @@ void main() {
}); });
testWithoutContext('diagnostic message', () async { testWithoutContext('diagnostic message', () async {
final FakeDeviceManager deviceManager = final FakeDeviceManager deviceManager = FakeDeviceManager()
FakeDeviceManager()..diagnostics = <String>['Device locked']; ..diagnostics = <String>['Device locked'];
final DeviceValidator deviceValidator = DeviceValidator( final DeviceValidator deviceValidator = DeviceValidator(
deviceManager: deviceManager, deviceManager: deviceManager,
@ -154,14 +161,14 @@ void main() {
); );
final ValidationResult result = await deviceValidator.validate(); final ValidationResult result = await deviceValidator.validate();
expect(result.type, ValidationType.notAvailable); expect(result.type, ValidationType.notAvailable);
expect(result.messages, const <ValidationMessage>[ValidationMessage.hint('Device locked')]); expect(result.messages,
const <ValidationMessage>[ValidationMessage.hint('Device locked')]);
expect(result.statusInfo, isNull); expect(result.statusInfo, isNull);
}); });
testWithoutContext('diagnostic message and devices', () async { testWithoutContext('diagnostic message and devices', () async {
final FakeDevice device = FakeDevice(); final FakeDevice device = FakeDevice();
final FakeDeviceManager deviceManager = final FakeDeviceManager deviceManager = FakeDeviceManager()
FakeDeviceManager()
..devices = <Device>[device] ..devices = <Device>[device]
..diagnostics = <String>['Device locked']; ..diagnostics = <String>['Device locked'];
@ -184,7 +191,8 @@ void main() {
testUsingContext( testUsingContext(
'validate non-verbose output format for run without issues', 'validate non-verbose output format for run without issues',
() async { () async {
final Doctor doctor = Doctor(logger: logger, clock: const SystemClock()); final Doctor doctor =
Doctor(logger: logger, clock: const SystemClock());
expect(await doctor.diagnose(verbose: false), isTrue); expect(await doctor.diagnose(verbose: false), isTrue);
expect( expect(
logger.statusText, logger.statusText,
@ -215,13 +223,15 @@ void main() {
testUsingContext( testUsingContext(
'contains installed', 'contains installed',
() async { () async {
final Doctor doctor = Doctor(logger: logger, clock: const SystemClock()); final Doctor doctor =
Doctor(logger: logger, clock: const SystemClock());
await doctor.diagnose(verbose: false); await doctor.diagnose(verbose: false);
expect(testUsage.events.length, 3); expect(testUsage.events.length, 3);
expect( expect(
testUsage.events, testUsage.events,
contains(const TestUsageEvent('doctor-result', 'PassingValidator', label: 'installed')), contains(const TestUsageEvent('doctor-result', 'PassingValidator',
label: 'installed')),
); );
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
@ -236,10 +246,14 @@ void main() {
expect( expect(
testUsage.events, testUsage.events,
unorderedEquals(<TestUsageEvent>[ unorderedEquals(<TestUsageEvent>[
const TestUsageEvent('doctor-result', 'PassingValidator', label: 'installed'), const TestUsageEvent('doctor-result', 'PassingValidator',
const TestUsageEvent('doctor-result', 'PassingValidator', label: 'installed'), label: 'installed'),
const TestUsageEvent('doctor-result', 'PartialValidatorWithHintsOnly', label: 'partial'), const TestUsageEvent('doctor-result', 'PassingValidator',
const TestUsageEvent('doctor-result', 'PartialValidatorWithErrors', label: 'partial'), label: 'installed'),
const TestUsageEvent('doctor-result', 'PartialValidatorWithHintsOnly',
label: 'partial'),
const TestUsageEvent('doctor-result', 'PartialValidatorWithErrors',
label: 'partial'),
]), ]),
); );
}, overrides: <Type, Generator>{Usage: () => testUsage}); }, overrides: <Type, Generator>{Usage: () => testUsage});
@ -250,11 +264,16 @@ void main() {
expect( expect(
testUsage.events, testUsage.events,
unorderedEquals(<TestUsageEvent>[ unorderedEquals(<TestUsageEvent>[
const TestUsageEvent('doctor-result', 'PassingValidator', label: 'installed'), const TestUsageEvent('doctor-result', 'PassingValidator',
const TestUsageEvent('doctor-result', 'MissingValidator', label: 'missing'), label: 'installed'),
const TestUsageEvent('doctor-result', 'NotAvailableValidator', label: 'notAvailable'), const TestUsageEvent('doctor-result', 'MissingValidator',
const TestUsageEvent('doctor-result', 'PartialValidatorWithHintsOnly', label: 'partial'), label: 'missing'),
const TestUsageEvent('doctor-result', 'PartialValidatorWithErrors', label: 'partial'), const TestUsageEvent('doctor-result', 'NotAvailableValidator',
label: 'notAvailable'),
const TestUsageEvent('doctor-result', 'PartialValidatorWithHintsOnly',
label: 'partial'),
const TestUsageEvent('doctor-result', 'PartialValidatorWithErrors',
label: 'partial'),
]), ]),
); );
}, overrides: <Type, Generator>{Usage: () => testUsage}); }, overrides: <Type, Generator>{Usage: () => testUsage});
@ -267,10 +286,14 @@ void main() {
expect( expect(
testUsage.events, testUsage.events,
unorderedEquals(<TestUsageEvent>[ unorderedEquals(<TestUsageEvent>[
const TestUsageEvent('doctor-result', 'PassingGroupedValidator', label: 'installed'), const TestUsageEvent('doctor-result', 'PassingGroupedValidator',
const TestUsageEvent('doctor-result', 'PassingGroupedValidator', label: 'installed'), label: 'installed'),
const TestUsageEvent('doctor-result', 'PassingGroupedValidator', label: 'installed'), const TestUsageEvent('doctor-result', 'PassingGroupedValidator',
const TestUsageEvent('doctor-result', 'MissingGroupedValidator', label: 'missing'), label: 'installed'),
const TestUsageEvent('doctor-result', 'PassingGroupedValidator',
label: 'installed'),
const TestUsageEvent('doctor-result', 'MissingGroupedValidator',
label: 'missing'),
]), ]),
); );
}, },
@ -278,7 +301,8 @@ void main() {
); );
testUsingContext('sending events can be skipped', () async { testUsingContext('sending events can be skipped', () async {
await FakePassingDoctor(logger).diagnose(verbose: false, sendEvent: false); await FakePassingDoctor(logger)
.diagnose(verbose: false, sendEvent: false);
expect(testUsage.events, isEmpty); expect(testUsage.events, isEmpty);
}, overrides: <Type, Generator>{Usage: () => testUsage}); }, overrides: <Type, Generator>{Usage: () => testUsage});
@ -308,7 +332,8 @@ void main() {
testUsingContext( testUsingContext(
'validate non-verbose output format for run with crash', 'validate non-verbose output format for run with crash',
() async { () async {
expect(await FakeCrashingDoctor(logger).diagnose(verbose: false), isFalse); expect(
await FakeCrashingDoctor(logger).diagnose(verbose: false), isFalse);
expect( expect(
logger.statusText, logger.statusText,
equals( equals(
@ -329,7 +354,9 @@ void main() {
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
); );
testUsingContext('validate verbose output format contains trace for run with crash', () async { testUsingContext(
'validate verbose output format contains trace for run with crash',
() async {
expect(await FakeCrashingDoctor(logger).diagnose(), isFalse); expect(await FakeCrashingDoctor(logger).diagnose(), isFalse);
expect(logger.statusText, contains('#0 CrashingValidator.validate')); expect(logger.statusText, contains('#0 CrashingValidator.validate'));
}); });
@ -346,7 +373,8 @@ void main() {
expect( expect(
logger.statusText, logger.statusText,
contains('Stuck validator that never completes exceeded maximum allowed duration of '), contains(
'Stuck validator that never completes exceeded maximum allowed duration of '),
); );
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -358,7 +386,9 @@ void main() {
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
await FakeAsync().run((FakeAsync time) { await FakeAsync().run((FakeAsync time) {
unawaited( unawaited(
FakeAsyncCrashingDoctor(time, logger).diagnose(verbose: false).then((bool r) { FakeAsyncCrashingDoctor(time, logger)
.diagnose(verbose: false)
.then((bool r) {
expect(r, isFalse); expect(r, isFalse);
completer.complete(); completer.complete();
}), }),
@ -390,7 +420,8 @@ void main() {
testUsingContext( testUsingContext(
'validate non-verbose output format when only one category fails', 'validate non-verbose output format when only one category fails',
() async { () async {
expect(await FakeSinglePassingDoctor(logger).diagnose(verbose: false), isTrue); expect(await FakeSinglePassingDoctor(logger).diagnose(verbose: false),
isTrue);
expect( expect(
logger.statusText, logger.statusText,
equals( equals(
@ -408,7 +439,8 @@ void main() {
testUsingContext( testUsingContext(
'validate non-verbose output format for a passing run', 'validate non-verbose output format for a passing run',
() async { () async {
expect(await FakePassingDoctor(logger).diagnose(verbose: false), isTrue); expect(
await FakePassingDoctor(logger).diagnose(verbose: false), isTrue);
expect( expect(
logger.statusText, logger.statusText,
equals( equals(
@ -460,59 +492,54 @@ void main() {
expect(await FakeDoctor(logger).diagnose(), isFalse); expect(await FakeDoctor(logger).diagnose(), isFalse);
expect( expect(
logger.statusText, logger.statusText,
equals( equals('[✓] Passing Validator (with statusInfo) [0ms]\n'
'[✓] Passing Validator (with statusInfo)\n'
' • A helpful message\n' ' • A helpful message\n'
' • A second, somewhat longer helpful message\n' ' • A second, somewhat longer helpful message\n'
'\n' '\n'
'[✗] Missing Validator\n' '[✗] Missing Validator [0ms]\n'
' ✗ A useful error message\n' ' ✗ A useful error message\n'
' • A message that is not an error\n' ' • A message that is not an error\n'
' ! A hint message\n' ' ! A hint message\n'
'\n' '\n'
'[!] Not Available Validator\n' '[!] Not Available Validator [0ms]\n'
' ✗ A useful error message\n' ' ✗ A useful error message\n'
' • A message that is not an error\n' ' • A message that is not an error\n'
' ! A hint message\n' ' ! A hint message\n'
'\n' '\n'
'[!] Partial Validator with only a Hint\n' '[!] Partial Validator with only a Hint [0ms]\n'
' ! There is a hint here\n' ' ! There is a hint here\n'
' • But there is no error\n' ' • But there is no error\n'
'\n' '\n'
'[!] Partial Validator with Errors\n' '[!] Partial Validator with Errors [0ms]\n'
' ✗ An error message indicating partial installation\n' ' ✗ An error message indicating partial installation\n'
' ! Maybe a hint will help the user\n' ' ! Maybe a hint will help the user\n'
' • An extra message with some verbose details\n' ' • An extra message with some verbose details\n'
'\n' '\n'
'! Doctor found issues in 4 categories.\n', '! Doctor found issues in 4 categories.\n'));
), }, overrides: <Type, Generator>{
); AnsiTerminal: () => FakeTerminal(),
}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}); });
testUsingContext('validate PII can be hidden', () async { testUsingContext('validate PII can be hidden', () async {
expect(await FakePiiDoctor(logger).diagnose(showPii: false), isTrue); expect(await FakePiiDoctor(logger).diagnose(showPii: false), isTrue);
expect( expect(
logger.statusText, logger.statusText,
equals( equals('[✓] PII Validator [0ms]\n'
'[✓] PII Validator\n'
' • Does not contain PII\n' ' • Does not contain PII\n'
'\n' '\n'
'• No issues found!\n', '• No issues found!\n'));
),
);
logger.clear(); logger.clear();
// PII shown. // PII shown.
expect(await FakePiiDoctor(logger).diagnose(), isTrue); expect(await FakePiiDoctor(logger).diagnose(), isTrue);
expect( expect(
logger.statusText, logger.statusText,
equals( equals('[✓] PII Validator [0ms]\n'
'[✓] PII Validator\n'
' • Contains PII path/to/username\n' ' • Contains PII path/to/username\n'
'\n' '\n'
'• No issues found!\n', '• No issues found!\n'));
), }, overrides: <Type, Generator>{
); AnsiTerminal: () => FakeTerminal(),
}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}); });
}); });
group('doctor diagnosis wrapper', () { group('doctor diagnosis wrapper', () {
@ -529,13 +556,11 @@ void main() {
() async { () async {
final Doctor fakeDoctor = FakePiiDoctor(logger); final Doctor fakeDoctor = FakePiiDoctor(logger);
final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor); final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor);
const String expectedPiiText = const String expectedPiiText = '[✓] PII Validator [0ms]\n'
'[✓] PII Validator\n'
' • Contains PII path/to/username\n' ' • Contains PII path/to/username\n'
'\n' '\n'
'• No issues found!\n'; '• No issues found!\n';
const String expectedPiiStrippedText = const String expectedPiiStrippedText = '[✓] PII Validator [0ms]\n'
'[✓] PII Validator\n'
' • Does not contain PII\n' ' • Does not contain PII\n'
'\n' '\n'
'• No issues found!\n'; '• No issues found!\n';
@ -550,10 +575,14 @@ void main() {
// Only one event sent. // Only one event sent.
expect(testUsage.events, <TestUsageEvent>[ expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent('doctor-result', 'PiiValidator', label: 'installed'), const TestUsageEvent('doctor-result', 'PiiValidator',
label: 'installed'),
]); ]);
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal(), Usage: () => testUsage}, overrides: <Type, Generator>{
AnsiTerminal: () => FakeTerminal(),
Usage: () => testUsage
},
); );
testUsingContext( testUsingContext(
@ -618,30 +647,31 @@ void main() {
wrapLogger.statusText, wrapLogger.statusText,
equals( equals(
'[✓] Passing Validator (with\n' '[✓] Passing Validator (with\n'
' statusInfo)\n' ' statusInfo) [0ms]\n'
' • A helpful message\n' ' • A helpful message\n'
' • A second, somewhat\n' ' • A second, somewhat\n'
' longer helpful message\n' ' longer helpful message\n'
'\n' '\n'
'[✗] Missing Validator\n' '[✗] Missing Validator [0ms]\n'
' ✗ A useful error message\n' ' ✗ A useful error message\n'
' • A message that is not an\n' ' • A message that is not an\n'
' error\n' ' error\n'
' ! A hint message\n' ' ! A hint message\n'
'\n' '\n'
'[!] Not Available Validator\n' '[!] Not Available Validator\n'
' [0ms]\n'
' ✗ A useful error message\n' ' ✗ A useful error message\n'
' • A message that is not an\n' ' • A message that is not an\n'
' error\n' ' error\n'
' ! A hint message\n' ' ! A hint message\n'
'\n' '\n'
'[!] Partial Validator with\n' '[!] Partial Validator with\n'
' only a Hint\n' ' only a Hint [0ms]\n'
' ! There is a hint here\n' ' ! There is a hint here\n'
' • But there is no error\n' ' • But there is no error\n'
'\n' '\n'
'[!] Partial Validator with\n' '[!] Partial Validator with\n'
' Errors\n' ' Errors [0ms]\n'
' ✗ An error message\n' ' ✗ An error message\n'
' indicating partial\n' ' indicating partial\n'
' installation\n' ' installation\n'
@ -657,57 +687,50 @@ void main() {
}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}); }, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()});
group('doctor with grouped validators', () { group('doctor with grouped validators', () {
testUsingContext( testUsingContext('validate diagnose combines validator output', () async {
'validate diagnose combines validator output',
() async {
expect(await FakeGroupedDoctor(logger).diagnose(), isTrue); expect(await FakeGroupedDoctor(logger).diagnose(), isTrue);
expect( expect(
logger.statusText, logger.statusText,
equals( equals('[✓] Category 1 [0ms]\n'
'[✓] Category 1\n'
' • A helpful message\n' ' • A helpful message\n'
' • A helpful message\n' ' • A helpful message\n'
'\n' '\n'
'[!] Category 2\n' '[!] Category 2 [0ms]\n'
' • A helpful message\n' ' • A helpful message\n'
' ✗ A useful error message\n' ' ✗ A useful error message\n'
'\n' '\n'
'! Doctor found issues in 1 category.\n', '! Doctor found issues in 1 category.\n'));
), }, overrides: <Type, Generator>{
); AnsiTerminal: () => FakeTerminal(),
}, });
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
testUsingContext( testUsingContext('validate merging assigns statusInfo and title', () async {
'validate merging assigns statusInfo and title',
() async {
// There are two subvalidators. Only the second contains statusInfo. // There are two subvalidators. Only the second contains statusInfo.
expect(await FakeGroupedDoctorWithStatus(logger).diagnose(), isTrue); expect(await FakeGroupedDoctorWithStatus(logger).diagnose(), isTrue);
expect( expect(
logger.statusText, logger.statusText,
equals( equals('[✓] First validator title (A status message) [0ms]\n'
'[✓] First validator title (A status message)\n'
' • A helpful message\n' ' • A helpful message\n'
' • A different message\n' ' • A different message\n'
'\n' '\n'
'• No issues found!\n', '• No issues found!\n'));
), }, overrides: <Type, Generator>{
); AnsiTerminal: () => FakeTerminal(),
}, });
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
);
}); });
group('grouped validator merging results', () { group('grouped validator merging results', () {
final PassingGroupedValidator installed = PassingGroupedValidator('Category'); final PassingGroupedValidator installed =
PassingGroupedValidator('Category');
final PartialGroupedValidator partial = PartialGroupedValidator('Category'); final PartialGroupedValidator partial = PartialGroupedValidator('Category');
final MissingGroupedValidator missing = MissingGroupedValidator('Category'); final MissingGroupedValidator missing = MissingGroupedValidator('Category');
testUsingContext( testUsingContext(
'validate installed + installed = installed', 'validate installed + installed = installed',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, installed, installed).diagnose(), isTrue); expect(
await FakeSmallGroupDoctor(logger, installed, installed).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[✓]')); expect(logger.statusText, startsWith('[✓]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -716,7 +739,9 @@ void main() {
testUsingContext( testUsingContext(
'validate installed + partial = partial', 'validate installed + partial = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, installed, partial).diagnose(), isTrue); expect(
await FakeSmallGroupDoctor(logger, installed, partial).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -725,7 +750,9 @@ void main() {
testUsingContext( testUsingContext(
'validate installed + missing = partial', 'validate installed + missing = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, installed, missing).diagnose(), isTrue); expect(
await FakeSmallGroupDoctor(logger, installed, missing).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -734,7 +761,9 @@ void main() {
testUsingContext( testUsingContext(
'validate partial + installed = partial', 'validate partial + installed = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, partial, installed).diagnose(), isTrue); expect(
await FakeSmallGroupDoctor(logger, partial, installed).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -743,7 +772,8 @@ void main() {
testUsingContext( testUsingContext(
'validate partial + partial = partial', 'validate partial + partial = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(), isTrue); expect(await FakeSmallGroupDoctor(logger, partial, partial).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -752,7 +782,8 @@ void main() {
testUsingContext( testUsingContext(
'validate partial + missing = partial', 'validate partial + missing = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(), isTrue); expect(await FakeSmallGroupDoctor(logger, partial, missing).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -761,7 +792,9 @@ void main() {
testUsingContext( testUsingContext(
'validate missing + installed = partial', 'validate missing + installed = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, missing, installed).diagnose(), isTrue); expect(
await FakeSmallGroupDoctor(logger, missing, installed).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -770,7 +803,8 @@ void main() {
testUsingContext( testUsingContext(
'validate missing + partial = partial', 'validate missing + partial = partial',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(), isTrue); expect(await FakeSmallGroupDoctor(logger, missing, partial).diagnose(),
isTrue);
expect(logger.statusText, startsWith('[!]')); expect(logger.statusText, startsWith('[!]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -779,7 +813,8 @@ void main() {
testUsingContext( testUsingContext(
'validate missing + missing = missing', 'validate missing + missing = missing',
() async { () async {
expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(), isFalse); expect(await FakeSmallGroupDoctor(logger, missing, missing).diagnose(),
isFalse);
expect(logger.statusText, startsWith('[✗]')); expect(logger.statusText, startsWith('[✗]'));
}, },
overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()}, overrides: <Type, Generator>{AnsiTerminal: () => FakeTerminal()},
@ -789,8 +824,7 @@ void main() {
testUsingContext( testUsingContext(
'WebWorkflow is a part of validator workflows if enabled', 'WebWorkflow is a part of validator workflows if enabled',
() async { () async {
final List<Workflow> workflows = final List<Workflow> workflows = DoctorValidatorsProvider.test(
DoctorValidatorsProvider.test(
featureFlags: TestFeatureFlags(isWebEnabled: true), featureFlags: TestFeatureFlags(isWebEnabled: true),
platform: FakePlatform(), platform: FakePlatform(),
).workflows; ).workflows;
@ -805,14 +839,16 @@ void main() {
testUsingContext( testUsingContext(
'CustomDevicesWorkflow is a part of validator workflows if enabled', 'CustomDevicesWorkflow is a part of validator workflows if enabled',
() async { () async {
final List<Workflow> workflows = final List<Workflow> workflows = DoctorValidatorsProvider.test(
DoctorValidatorsProvider.test(
featureFlags: TestFeatureFlags(areCustomDevicesEnabled: true), featureFlags: TestFeatureFlags(areCustomDevicesEnabled: true),
platform: FakePlatform(), platform: FakePlatform(),
).workflows; ).workflows;
expect(workflows, contains(isA<CustomDeviceWorkflow>())); expect(workflows, contains(isA<CustomDeviceWorkflow>()));
}, },
overrides: <Type, Generator>{FileSystem: () => fs, ProcessManager: () => fakeProcessManager}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => fakeProcessManager
},
); );
group('FlutterValidator', () { group('FlutterValidator', () {
@ -835,17 +871,21 @@ void main() {
final Directory devtoolsDir = fs.directory( final Directory devtoolsDir = fs.directory(
'/path/to/flutter/bin/cache/dart-sdk/bin/resources/devtools', '/path/to/flutter/bin/cache/dart-sdk/bin/resources/devtools',
)..createSync(recursive: true); )..createSync(recursive: true);
fs.directory('/path/to/flutter/bin/cache/artifacts').createSync(recursive: true); fs
devtoolsDir.childFile('version.json').writeAsStringSync('{"version": "123"}'); .directory('/path/to/flutter/bin/cache/artifacts')
.createSync(recursive: true);
devtoolsDir
.childFile('version.json')
.writeAsStringSync('{"version": "123"}');
fakeProcessManager.addCommands(const <FakeCommand>[ fakeProcessManager.addCommands(const <FakeCommand>[
FakeCommand(command: <String>['which', 'java']), FakeCommand(command: <String>['which', 'java']),
]); ]);
final List<DoctorValidator> validators = final List<DoctorValidator> validators = DoctorValidatorsProvider.test(
DoctorValidatorsProvider.test(
featureFlags: featureFlags, featureFlags: featureFlags,
platform: FakePlatform(), platform: FakePlatform(),
).validators; ).validators;
final FlutterValidator flutterValidator = validators.whereType<FlutterValidator>().first; final FlutterValidator flutterValidator =
validators.whereType<FlutterValidator>().first;
final ValidationResult result = await flutterValidator.validate(); final ValidationResult result = await flutterValidator.validate();
expect( expect(
result.messages.map((ValidationMessage msg) => msg.message), result.messages.map((ValidationMessage msg) => msg.message),
@ -853,8 +893,7 @@ void main() {
); );
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Cache: Cache: () => Cache.test(
() => Cache.test(
rootOverride: fs.directory('/path/to/flutter'), rootOverride: fs.directory('/path/to/flutter'),
fileSystem: fs, fileSystem: fs,
processManager: fakeProcessManager, processManager: fakeProcessManager,
@ -873,8 +912,10 @@ void main() {
final DoctorValidatorsProvider provider = DoctorValidatorsProvider.test( final DoctorValidatorsProvider provider = DoctorValidatorsProvider.test(
featureFlags: TestFeatureFlags(isAndroidEnabled: false), featureFlags: TestFeatureFlags(isAndroidEnabled: false),
); );
expect(provider.validators, isNot(contains(isA<AndroidStudioValidator>()))); expect(
expect(provider.validators, isNot(contains(isA<NoAndroidStudioValidator>()))); provider.validators, isNot(contains(isA<AndroidStudioValidator>())));
expect(provider.validators,
isNot(contains(isA<NoAndroidStudioValidator>())));
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false), AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false),
@ -925,13 +966,16 @@ void main() {
); );
expect(fakeAnalytics.sentEvents, contains(eventToFind)); expect(fakeAnalytics.sentEvents, contains(eventToFind));
}, },
overrides: <Type, Generator>{DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider()}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider()
},
); );
testUsingContext( testUsingContext(
'contains installed and partial', 'contains installed and partial',
() async { () async {
await FakePassingDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false); await FakePassingDoctor(logger, clock: fakeSystemClock)
.diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(4)); expect(fakeAnalytics.sentEvents, hasLength(4));
expect( expect(
@ -975,7 +1019,8 @@ void main() {
testUsingContext( testUsingContext(
'contains installed, missing and partial', 'contains installed, missing and partial',
() async { () async {
await FakeDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false); await FakeDoctor(logger, clock: fakeSystemClock)
.diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(5)); expect(fakeAnalytics.sentEvents, hasLength(5));
expect( expect(
@ -1024,7 +1069,8 @@ void main() {
testUsingContext( testUsingContext(
'events for grouped validators are properly decomposed', 'events for grouped validators are properly decomposed',
() async { () async {
await FakeGroupedDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false); await FakeGroupedDoctor(logger, clock: fakeSystemClock)
.diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(4)); expect(fakeAnalytics.sentEvents, hasLength(4));
expect( expect(
@ -1066,14 +1112,18 @@ void main() {
testUsingContext( testUsingContext(
'grouped validator subresult and subvalidators different lengths', 'grouped validator subresult and subvalidators different lengths',
() async { () async {
final FakeGroupedDoctorWithCrash fakeDoctor = FakeGroupedDoctorWithCrash( final FakeGroupedDoctorWithCrash fakeDoctor =
FakeGroupedDoctorWithCrash(
logger, logger,
clock: fakeSystemClock, clock: fakeSystemClock,
); );
await fakeDoctor.diagnose(verbose: false); await fakeDoctor.diagnose(verbose: false);
expect(fakeDoctor.validators, hasLength(1)); expect(fakeDoctor.validators, hasLength(1));
expect(fakeDoctor.validators.first.runtimeType == FakeGroupedValidatorWithCrash, true); expect(
fakeDoctor.validators.first.runtimeType ==
FakeGroupedValidatorWithCrash,
true);
expect(fakeAnalytics.sentEvents, hasLength(0)); expect(fakeAnalytics.sentEvents, hasLength(0));
// Attempt to send a random event to ensure that the // Attempt to send a random event to ensure that the
@ -1088,14 +1138,16 @@ void main() {
); );
testUsingContext('sending events can be skipped', () async { testUsingContext('sending events can be skipped', () async {
await FakePassingDoctor(logger).diagnose(verbose: false, sendEvent: false); await FakePassingDoctor(logger)
.diagnose(verbose: false, sendEvent: false);
expect(fakeAnalytics.sentEvents, isEmpty); expect(fakeAnalytics.sentEvents, isEmpty);
}, overrides: <Type, Generator>{Analytics: () => fakeAnalytics}); }, overrides: <Type, Generator>{Analytics: () => fakeAnalytics});
}); });
} }
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow { class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
FakeAndroidWorkflow({this.canListDevices = true, this.appliesToHostPlatform = true}); FakeAndroidWorkflow(
{this.canListDevices = true, this.appliesToHostPlatform = true});
@override @override
final bool canListDevices; final bool canListDevices;
@ -1108,12 +1160,13 @@ class PassingValidator extends DoctorValidator {
PassingValidator(super.title); PassingValidator(super.title);
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage('A helpful message'), ValidationMessage('A helpful message'),
ValidationMessage('A second, somewhat longer helpful message'), ValidationMessage('A second, somewhat longer helpful message'),
]; ];
return const ValidationResult(ValidationType.success, messages, statusInfo: 'with statusInfo'); return ZeroExecutionTimeValidationResult(ValidationType.success, messages,
statusInfo: 'with statusInfo');
} }
} }
@ -1121,14 +1174,14 @@ class PiiValidator extends DoctorValidator {
PiiValidator() : super('PII Validator'); PiiValidator() : super('PII Validator');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage( ValidationMessage(
'Contains PII path/to/username', 'Contains PII path/to/username',
piiStrippedMessage: 'Does not contain PII', piiStrippedMessage: 'Does not contain PII',
), ),
]; ];
return const ValidationResult(ValidationType.success, messages); return ZeroExecutionTimeValidationResult(ValidationType.success, messages);
} }
} }
@ -1136,13 +1189,13 @@ class MissingValidator extends DoctorValidator {
MissingValidator() : super('Missing Validator'); MissingValidator() : super('Missing Validator');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('A useful error message'), ValidationMessage.error('A useful error message'),
ValidationMessage('A message that is not an error'), ValidationMessage('A message that is not an error'),
ValidationMessage.hint('A hint message'), ValidationMessage.hint('A hint message'),
]; ];
return const ValidationResult(ValidationType.missing, messages); return ZeroExecutionTimeValidationResult(ValidationType.missing, messages);
} }
} }
@ -1150,13 +1203,14 @@ class NotAvailableValidator extends DoctorValidator {
NotAvailableValidator() : super('Not Available Validator'); NotAvailableValidator() : super('Not Available Validator');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('A useful error message'), ValidationMessage.error('A useful error message'),
ValidationMessage('A message that is not an error'), ValidationMessage('A message that is not an error'),
ValidationMessage.hint('A hint message'), ValidationMessage.hint('A hint message'),
]; ];
return const ValidationResult(ValidationType.notAvailable, messages); return ZeroExecutionTimeValidationResult(
ValidationType.notAvailable, messages);
} }
} }
@ -1164,7 +1218,7 @@ class StuckValidator extends DoctorValidator {
StuckValidator() : super('Stuck validator that never completes'); StuckValidator() : super('Stuck validator that never completes');
@override @override
Future<ValidationResult> validate() { Future<ValidationResult> validateImpl() {
final Completer<ValidationResult> completer = Completer<ValidationResult>(); final Completer<ValidationResult> completer = Completer<ValidationResult>();
// This future will never complete // This future will never complete
@ -1176,13 +1230,14 @@ class PartialValidatorWithErrors extends DoctorValidator {
PartialValidatorWithErrors() : super('Partial Validator with Errors'); PartialValidatorWithErrors() : super('Partial Validator with Errors');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('An error message indicating partial installation'), ValidationMessage.error(
'An error message indicating partial installation'),
ValidationMessage.hint('Maybe a hint will help the user'), ValidationMessage.hint('Maybe a hint will help the user'),
ValidationMessage('An extra message with some verbose details'), ValidationMessage('An extra message with some verbose details'),
]; ];
return const ValidationResult(ValidationType.partial, messages); return ZeroExecutionTimeValidationResult(ValidationType.partial, messages);
} }
} }
@ -1190,12 +1245,12 @@ class PartialValidatorWithHintsOnly extends DoctorValidator {
PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint'); PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.hint('There is a hint here'), ValidationMessage.hint('There is a hint here'),
ValidationMessage('But there is no error'), ValidationMessage('But there is no error'),
]; ];
return const ValidationResult(ValidationType.partial, messages); return ZeroExecutionTimeValidationResult(ValidationType.partial, messages);
} }
} }
@ -1203,7 +1258,7 @@ class CrashingValidator extends DoctorValidator {
CrashingValidator() : super('Crashing validator'); CrashingValidator() : super('Crashing validator');
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
throw StateError('fatal error'); throw StateError('fatal error');
} }
} }
@ -1214,7 +1269,7 @@ class AsyncCrashingValidator extends DoctorValidator {
final FakeAsync _time; final FakeAsync _time;
@override @override
Future<ValidationResult> validate() { Future<ValidationResult> validateImpl() {
const Duration delay = Duration(seconds: 1); const Duration delay = Duration(seconds: 1);
final Future<ValidationResult> result = Future<ValidationResult>.delayed( final Future<ValidationResult> result = Future<ValidationResult>.delayed(
delay, delay,
@ -1228,7 +1283,8 @@ class AsyncCrashingValidator extends DoctorValidator {
/// A doctor that fails with a missing [ValidationResult]. /// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor { class FakeDoctor extends Doctor {
FakeDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakeDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1242,7 +1298,8 @@ class FakeDoctor extends Doctor {
/// A doctor that should pass, but still has issues in some categories. /// A doctor that should pass, but still has issues in some categories.
class FakePassingDoctor extends Doctor { class FakePassingDoctor extends Doctor {
FakePassingDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakePassingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1260,12 +1317,15 @@ class FakeSinglePassingDoctor extends Doctor {
: super(logger: logger); : super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[PartialValidatorWithHintsOnly()]; late final List<DoctorValidator> validators = <DoctorValidator>[
PartialValidatorWithHintsOnly()
];
} }
/// A doctor that passes and has no issues anywhere. /// A doctor that passes and has no issues anywhere.
class FakeQuietDoctor extends Doctor { class FakeQuietDoctor extends Doctor {
FakeQuietDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakeQuietDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1278,15 +1338,19 @@ class FakeQuietDoctor extends Doctor {
/// A doctor that passes and contains PII that can be hidden. /// A doctor that passes and contains PII that can be hidden.
class FakePiiDoctor extends Doctor { class FakePiiDoctor extends Doctor {
FakePiiDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakePiiDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[PiiValidator()]; late final List<DoctorValidator> validators = <DoctorValidator>[
PiiValidator()
];
} }
/// A doctor with a validator that throws an exception. /// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor { class FakeCrashingDoctor extends Doctor {
FakeCrashingDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakeCrashingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1300,7 +1364,8 @@ class FakeCrashingDoctor extends Doctor {
/// A doctor with a validator that will never finish. /// A doctor with a validator that will never finish.
class FakeAsyncStuckDoctor extends Doctor { class FakeAsyncStuckDoctor extends Doctor {
FakeAsyncStuckDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakeAsyncStuckDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1314,7 +1379,8 @@ class FakeAsyncStuckDoctor extends Doctor {
/// A doctor with a validator that throws an exception. /// A doctor with a validator that throws an exception.
class FakeAsyncCrashingDoctor extends Doctor { class FakeAsyncCrashingDoctor extends Doctor {
FakeAsyncCrashingDoctor(this._time, Logger logger, {super.clock = const SystemClock()}) FakeAsyncCrashingDoctor(this._time, Logger logger,
{super.clock = const SystemClock()})
: super(logger: logger); : super(logger: logger);
final FakeAsync _time; final FakeAsync _time;
@ -1347,11 +1413,11 @@ class PassingGroupedValidator extends DoctorValidator {
PassingGroupedValidator(super.title); PassingGroupedValidator(super.title);
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage('A helpful message'), ValidationMessage('A helpful message'),
]; ];
return const ValidationResult(ValidationType.success, messages); return ZeroExecutionTimeValidationResult(ValidationType.success, messages);
} }
} }
@ -1359,11 +1425,11 @@ class MissingGroupedValidator extends DoctorValidator {
MissingGroupedValidator(super.title); MissingGroupedValidator(super.title);
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('A useful error message'), ValidationMessage.error('A useful error message'),
]; ];
return const ValidationResult(ValidationType.missing, messages); return ZeroExecutionTimeValidationResult(ValidationType.missing, messages);
} }
} }
@ -1371,11 +1437,11 @@ class PartialGroupedValidator extends DoctorValidator {
PartialGroupedValidator(super.title); PartialGroupedValidator(super.title);
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage.error('An error message for partial installation'), ValidationMessage.error('An error message for partial installation'),
]; ];
return const ValidationResult(ValidationType.partial, messages); return ZeroExecutionTimeValidationResult(ValidationType.partial, messages);
} }
} }
@ -1383,17 +1449,19 @@ class PassingGroupedValidatorWithStatus extends DoctorValidator {
PassingGroupedValidatorWithStatus(super.title); PassingGroupedValidatorWithStatus(super.title);
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
const List<ValidationMessage> messages = <ValidationMessage>[ const List<ValidationMessage> messages = <ValidationMessage>[
ValidationMessage('A different message'), ValidationMessage('A different message'),
]; ];
return const ValidationResult(ValidationType.success, messages, statusInfo: 'A status message'); return ZeroExecutionTimeValidationResult(ValidationType.success, messages,
statusInfo: 'A status message');
} }
} }
/// A doctor that has two groups of two validators each. /// A doctor that has two groups of two validators each.
class FakeGroupedDoctor extends Doctor { class FakeGroupedDoctor extends Doctor {
FakeGroupedDoctor(Logger logger, {super.clock = const SystemClock()}) : super(logger: logger); FakeGroupedDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1438,7 +1506,8 @@ class FakeGroupedValidatorWithCrash extends GroupedValidator {
} }
class FakeGroupedDoctorWithStatus extends Doctor { class FakeGroupedDoctorWithStatus extends Doctor {
FakeGroupedDoctorWithStatus(Logger logger, {super.clock = const SystemClock()}) FakeGroupedDoctorWithStatus(Logger logger,
{super.clock = const SystemClock()})
: super(logger: logger); : super(logger: logger);
@override @override
@ -1485,12 +1554,14 @@ class VsCodeValidatorTestTargets extends VsCodeValidator {
VsCodeValidatorTestTargets._(validInstall, validExtensions); VsCodeValidatorTestTargets._(validInstall, validExtensions);
static VsCodeValidatorTestTargets get installedWithExtension64bit => static VsCodeValidatorTestTargets get installedWithExtension64bit =>
VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition'); VsCodeValidatorTestTargets._(validInstall, validExtensions,
edition: '64-bit edition');
static VsCodeValidatorTestTargets get installedWithoutExtension => static VsCodeValidatorTestTargets get installedWithoutExtension =>
VsCodeValidatorTestTargets._(validInstall, missingExtensions); VsCodeValidatorTestTargets._(validInstall, missingExtensions);
static final String validInstall = globals.fs.path.join('test', 'data', 'vscode', 'application'); static final String validInstall =
globals.fs.path.join('test', 'data', 'vscode', 'application');
static final String validExtensions = globals.fs.path.join( static final String validExtensions = globals.fs.path.join(
'test', 'test',
'data', 'data',
@ -1510,13 +1581,15 @@ class FakeDeviceManager extends Fake implements DeviceManager {
List<Device> devices = <Device>[]; List<Device> devices = <Device>[];
@override @override
Future<List<Device>> getAllDevices({DeviceDiscoveryFilter? filter}) async => devices; Future<List<Device>> getAllDevices({DeviceDiscoveryFilter? filter}) async =>
devices;
@override @override
Future<List<Device>> refreshAllDevices({ Future<List<Device>> refreshAllDevices({
Duration? timeout, Duration? timeout,
DeviceDiscoveryFilter? filter, DeviceDiscoveryFilter? filter,
}) async => devices; }) async =>
devices;
@override @override
Future<List<String>> getDeviceDiagnostics() async => diagnostics; Future<List<String>> getDeviceDiagnostics() async => diagnostics;
@ -1548,7 +1621,8 @@ class FakeDevice extends Fake implements Device {
Future<String> get sdkNameAndVersion async => '1.2.3'; Future<String> get sdkNameAndVersion async => '1.2.3';
@override @override
Future<TargetPlatform> get targetPlatform => Future<TargetPlatform>.value(TargetPlatform.android); Future<TargetPlatform> get targetPlatform =>
Future<TargetPlatform>.value(TargetPlatform.android);
} }
class FakeTerminal extends Fake implements AnsiTerminal { class FakeTerminal extends Fake implements AnsiTerminal {
@ -1558,3 +1632,11 @@ class FakeTerminal extends Fake implements AnsiTerminal {
@override @override
bool get isCliAnimationEnabled => supportsColor; bool get isCliAnimationEnabled => supportsColor;
} }
class ZeroExecutionTimeValidationResult extends ValidationResult {
ZeroExecutionTimeValidationResult(super.type, super.messages,
{super.statusInfo});
@override
Duration? get executionTime => Duration.zero;
}

View File

@ -162,7 +162,7 @@ class FakeDoctorValidator extends DoctorValidator {
FakeDoctorValidator(super.title); FakeDoctorValidator(super.title);
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validateImpl() async {
return ValidationResult.crash(Object()); return ValidationResult.crash(Object());
} }
} }

View File

@ -14,15 +14,20 @@ import '../src/context.dart';
import '../src/fake_process_manager.dart'; import '../src/fake_process_manager.dart';
/// Fake [_WindowsUtils] to use for testing /// Fake [_WindowsUtils] to use for testing
class FakeValidOperatingSystemUtils extends Fake implements OperatingSystemUtils { class FakeValidOperatingSystemUtils extends Fake
FakeValidOperatingSystemUtils([this.name = 'Microsoft Windows [Version 11.0.22621.963]']); implements OperatingSystemUtils {
FakeValidOperatingSystemUtils(
[this.name = 'Microsoft Windows [Version 11.0.22621.963]']);
@override @override
final String name; final String name;
} }
class FakeProcessLister extends Fake implements ProcessLister { class FakeProcessLister extends Fake implements ProcessLister {
FakeProcessLister({required this.result, this.exitCode = 0, this.powershellAvailable = true}); FakeProcessLister(
{required this.result,
this.exitCode = 0,
this.powershellAvailable = true});
final String result; final String result;
final int exitCode; final int exitCode;
final bool powershellAvailable; final bool powershellAvailable;
@ -37,11 +42,13 @@ class FakeProcessLister extends Fake implements ProcessLister {
} }
FakeProcessLister ofdRunning() { FakeProcessLister ofdRunning() {
return FakeProcessLister(result: r'Path: "C:\Program Files\Topaz OFD\Warsaw\core.exe"'); return FakeProcessLister(
result: r'Path: "C:\Program Files\Topaz OFD\Warsaw\core.exe"');
} }
FakeProcessLister ofdNotRunning() { FakeProcessLister ofdNotRunning() {
return FakeProcessLister(result: r'Path: "C:\Program Files\Google\Chrome\Application\chrome.exe'); return FakeProcessLister(
result: r'Path: "C:\Program Files\Google\Chrome\Application\chrome.exe');
} }
FakeProcessLister failure() { FakeProcessLister failure() {
@ -57,38 +64,45 @@ FakeProcessLister powershellUnavailable() {
/// The expected validation result object for /// The expected validation result object for
/// a passing windows version test /// a passing windows version test
const ValidationResult validWindows11ValidationResult = ValidationResult( ValidationResult validWindows11ValidationResult = ValidationResult(
ValidationType.success, ValidationType.success,
<ValidationMessage>[], const <ValidationMessage>[],
statusInfo: '11 Pro 64-bit, 23H2, 2009', statusInfo: '11 Pro 64-bit, 23H2, 2009',
); );
/// The expected validation result object for /// The expected validation result object for
/// a passing windows version test /// a passing windows version test
const ValidationResult invalidWindowsValidationResult = ValidationResult( ValidationResult invalidWindowsValidationResult = ValidationResult(
ValidationType.missing, ValidationType.missing,
<ValidationMessage>[], const <ValidationMessage>[],
statusInfo: 'Unable to confirm if installed Windows version is 10 or greater', statusInfo: 'Unable to confirm if installed Windows version is 10 or greater',
); );
const ValidationResult ofdFoundRunning = ValidationResult ofdFoundRunning = ValidationResult(
ValidationResult(ValidationType.partial, <ValidationMessage>[ ValidationType.partial,
const <ValidationMessage>[
ValidationMessage.hint( ValidationMessage.hint(
'The Topaz OFD Security Module was detected on your machine. ' 'The Topaz OFD Security Module was detected on your machine. '
'You may need to disable it to build Flutter applications.', 'You may need to disable it to build Flutter applications.',
), ),
], statusInfo: 'Problem detected with Windows installation'); ],
statusInfo: 'Problem detected with Windows installation',
);
const ValidationResult powershellUnavailableResult = ValidationResult powershellUnavailableResult = ValidationResult(
ValidationResult(ValidationType.partial, <ValidationMessage>[
ValidationMessage.hint(
'Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH',
),
], statusInfo: 'Problem detected with Windows installation');
const ValidationResult getProcessFailed = ValidationResult(
ValidationType.partial, ValidationType.partial,
<ValidationMessage>[ValidationMessage.hint('Get-Process failed to complete')], const <ValidationMessage>[
ValidationMessage.hint(
'Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH'),
],
statusInfo: 'Problem detected with Windows installation',
);
ValidationResult getProcessFailed = ValidationResult(
ValidationType.partial,
const <ValidationMessage>[
ValidationMessage.hint('Get-Process failed to complete'),
],
statusInfo: 'Problem detected with Windows installation', statusInfo: 'Problem detected with Windows installation',
); );
@ -111,8 +125,10 @@ class FakeVersionExtractor extends Fake implements WindowsVersionExtractor {
} }
void main() { void main() {
testWithoutContext('Successfully running windows version check on windows 10', () async { testWithoutContext('Successfully running windows version check on windows 10',
final WindowsVersionValidator windowsVersionValidator = WindowsVersionValidator( () async {
final WindowsVersionValidator windowsVersionValidator =
WindowsVersionValidator(
operatingSystemUtils: FakeValidOperatingSystemUtils(), operatingSystemUtils: FakeValidOperatingSystemUtils(),
processLister: ofdNotRunning(), processLister: ofdNotRunning(),
versionExtractor: FakeVersionExtractor.win11ProX64(), versionExtractor: FakeVersionExtractor.win11ProX64(),
@ -132,8 +148,11 @@ void main() {
); );
}); });
testWithoutContext('Successfully running windows version check on windows 10 for BR', () async { testWithoutContext(
final WindowsVersionValidator windowsVersionValidator = WindowsVersionValidator( 'Successfully running windows version check on windows 10 for BR',
() async {
final WindowsVersionValidator windowsVersionValidator =
WindowsVersionValidator(
operatingSystemUtils: FakeValidOperatingSystemUtils( operatingSystemUtils: FakeValidOperatingSystemUtils(
'Microsoft Windows [versão 10.0.22621.1105]', 'Microsoft Windows [versão 10.0.22621.1105]',
), ),
@ -156,7 +175,8 @@ void main() {
}); });
testWithoutContext('Identifying a windows version before 10', () async { testWithoutContext('Identifying a windows version before 10', () async {
final WindowsVersionValidator windowsVersionValidator = WindowsVersionValidator( final WindowsVersionValidator windowsVersionValidator =
WindowsVersionValidator(
operatingSystemUtils: FakeValidOperatingSystemUtils( operatingSystemUtils: FakeValidOperatingSystemUtils(
'Microsoft Windows [Version 8.0.22621.1105]', 'Microsoft Windows [Version 8.0.22621.1105]',
), ),
@ -185,13 +205,16 @@ OS Version: .0.19044 N/A Build 19044
OS : 10.0.22621 Build 22621 OS : 10.0.22621 Build 22621
'''; ''';
final RegExp regex = RegExp(kWindowsOSVersionSemVerPattern, multiLine: true); final RegExp regex =
RegExp(kWindowsOSVersionSemVerPattern, multiLine: true);
final Iterable<RegExpMatch> matches = regex.allMatches(testStr); final Iterable<RegExpMatch> matches = regex.allMatches(testStr);
expect(matches.length, 5, reason: 'There should be only 5 matches for the pattern provided'); expect(matches.length, 5,
reason: 'There should be only 5 matches for the pattern provided');
}); });
testWithoutContext('Successfully checks for Topaz OFD when it is running', () async { testWithoutContext('Successfully checks for Topaz OFD when it is running',
() async {
final WindowsVersionValidator validator = WindowsVersionValidator( final WindowsVersionValidator validator = WindowsVersionValidator(
operatingSystemUtils: FakeValidOperatingSystemUtils(), operatingSystemUtils: FakeValidOperatingSystemUtils(),
processLister: ofdRunning(), processLister: ofdRunning(),
@ -253,7 +276,8 @@ OS 版本: 10.0.22621 暂缺 Build 22621
final WindowsVersionValidator validator = WindowsVersionValidator( final WindowsVersionValidator validator = WindowsVersionValidator(
operatingSystemUtils: FakeValidOperatingSystemUtils(), operatingSystemUtils: FakeValidOperatingSystemUtils(),
processLister: failure(), processLister: failure(),
versionExtractor: FakeVersionExtractor(mockData: WindowsVersionExtractionResult.empty()), versionExtractor: FakeVersionExtractor(
mockData: WindowsVersionExtractionResult.empty()),
); );
final ValidationResult result = await validator.validate(); final ValidationResult result = await validator.validate();
expect( expect(
@ -278,14 +302,20 @@ OS 版本: 10.0.22621 暂缺 Build 22621
); );
}); });
testWithoutContext('getProcessesWithPath successfully runs with powershell', () async { testWithoutContext('getProcessesWithPath successfully runs with powershell',
() async {
final ProcessLister processLister = ProcessLister( final ProcessLister processLister = ProcessLister(
FakeProcessManager.list(<FakeCommand>[ FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ProcessLister.powershell, '-command', 'Get-Process | Format-List Path'], command: <String>[
ProcessLister.powershell,
'-command',
'Get-Process | Format-List Path'
],
stdout: ProcessLister.powershell, stdout: ProcessLister.powershell,
), ),
])..excludedExecutables.add(ProcessLister.pwsh), ])
..excludedExecutables.add(ProcessLister.pwsh),
); );
try { try {
@ -303,10 +333,15 @@ OS 版本: 10.0.22621 暂缺 Build 22621
final ProcessLister processLister = ProcessLister( final ProcessLister processLister = ProcessLister(
FakeProcessManager.list(<FakeCommand>[ FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ProcessLister.pwsh, '-command', 'Get-Process | Format-List Path'], command: <String>[
ProcessLister.pwsh,
'-command',
'Get-Process | Format-List Path'
],
stdout: ProcessLister.pwsh, stdout: ProcessLister.pwsh,
), ),
])..excludedExecutables.add(ProcessLister.powershell), ])
..excludedExecutables.add(ProcessLister.powershell),
); );
try { try {
@ -324,7 +359,8 @@ OS 版本: 10.0.22621 暂缺 Build 22621
() async { () async {
final ProcessLister processLister = ProcessLister( final ProcessLister processLister = ProcessLister(
FakeProcessManager.empty() FakeProcessManager.empty()
..excludedExecutables.addAll(<String>[ProcessLister.powershell, ProcessLister.pwsh]), ..excludedExecutables
.addAll(<String>[ProcessLister.powershell, ProcessLister.pwsh]),
); );
try { try {
@ -342,7 +378,8 @@ OS 版本: 10.0.22621 暂缺 Build 22621
testWithoutContext( testWithoutContext(
'Parses Caption, OSArchitecture, releaseId, and CurrentVersion from the OS', 'Parses Caption, OSArchitecture, releaseId, and CurrentVersion from the OS',
() async { () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager =
FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <Pattern>['wmic', 'os', 'get', 'Caption,OSArchitecture'], command: <Pattern>['wmic', 'os', 'get', 'Caption,OSArchitecture'],
stdout: ''' stdout: '''
@ -402,7 +439,8 @@ End of search: 22 match(es) found.
}, },
); );
testWithoutContext('Differentiates Windows 11 from 10 when wmic call fails', () async { testWithoutContext('Differentiates Windows 11 from 10 when wmic call fails',
() async {
const String windows10RegQueryOutput = r''' const String windows10RegQueryOutput = r'''
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
SystemRoot REG_SZ C:\WINDOWS SystemRoot REG_SZ C:\WINDOWS
@ -430,8 +468,14 @@ HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
End of search: 21 match(es) found. End of search: 21 match(es) found.
'''; ''';
const List<String> wmicCommand = <String>['wmic', 'os', 'get', 'Caption,OSArchitecture']; const List<String> wmicCommand = <String>[
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ 'wmic',
'os',
'get',
'Caption,OSArchitecture'
];
final FakeProcessManager processManager =
FakeProcessManager.list(<FakeCommand>[
FakeCommand( FakeCommand(
command: wmicCommand, command: wmicCommand,
exception: ProcessException(wmicCommand[0], wmicCommand.sublist(1)), exception: ProcessException(wmicCommand[0], wmicCommand.sublist(1)),
@ -462,7 +506,12 @@ End of search: 21 match(es) found.
}); });
testWithoutContext('Handles reg call failing', () async { testWithoutContext('Handles reg call failing', () async {
const List<String> wmicCommand = <String>['wmic', 'os', 'get', 'Caption,OSArchitecture']; const List<String> wmicCommand = <String>[
'wmic',
'os',
'get',
'Caption,OSArchitecture'
];
const List<String> regCommand = <String>[ const List<String> regCommand = <String>[
'reg', 'reg',
'query', 'query',
@ -470,7 +519,8 @@ End of search: 21 match(es) found.
'/t', '/t',
'REG_SZ', 'REG_SZ',
]; ];
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager =
FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: wmicCommand, command: wmicCommand,
stdout: r''' stdout: r'''