Add 'doctor' support for Windows (#33872)
Moves the logic for finding vcvars64.bat to a new VisualStudio class
that encapsulates finding, and providing information about, VisualStudio
installations. Adds a validator for it, and runs it for Windows
workflows in doctor.
diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 032377f..09af158 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -215,6 +215,17 @@
String vsCodeLocation(String location) => 'VS Code at $location';
String vsCodeFlutterExtensionMissing(String url) => 'Flutter extension not installed; install from\n$url';
+ // Messages used in VisualStudioValidator
+ String visualStudioVersion(String name, String version) => '$name version $version';
+ String visualStudioLocation(String location) => 'Visual Studio at $location';
+ String visualStudioMissingComponents(String workload, List<String> components) =>
+ 'Visual Studio is missing necessary components. Please re-run the '
+ 'Visual Studio installer for the "$workload" workload, and include these components:\n'
+ ' ${components.join('\n ')}';
+ String get visualStudioMissing =>
+ 'Visual Studio not installed; this is necessary for Windows development.\n'
+ 'Download at https://visualstudio.microsoft.com/downloads/.';
+
// Messages used in FlutterCommand
String flutterElapsedTime(String name, String elapsedTime) => '"flutter $name" took $elapsedTime.';
String get flutterNoDevelopmentDevice =>
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 8d7a0eb..51bb7e7 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -46,6 +46,8 @@
import 'web/chrome.dart';
import 'web/compile.dart';
import 'web/workflow.dart';
+import 'windows/visual_studio.dart';
+import 'windows/visual_studio_validator.dart';
import 'windows/windows_workflow.dart';
Future<T> runInContext<T>(
@@ -100,6 +102,8 @@
TimeoutConfiguration: () => const TimeoutConfiguration(),
Usage: () => Usage(),
UserMessages: () => UserMessages(),
+ VisualStudio: () => VisualStudio(),
+ VisualStudioValidator: () => const VisualStudioValidator(),
WebCompiler: () => const WebCompiler(),
WebWorkflow: () => const WebWorkflow(),
WindowsWorkflow: () => const WindowsWorkflow(),
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 50cf645..a73f7e9 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -35,6 +35,7 @@
import 'vscode/vscode_validator.dart';
import 'web/web_validator.dart';
import 'web/workflow.dart';
+import 'windows/visual_studio_validator.dart';
import 'windows/windows_workflow.dart';
Doctor get doctor => context.get<Doctor>();
@@ -68,6 +69,9 @@
if (iosWorkflow.appliesToHostPlatform)
_validators.add(iosValidator);
+ if (windowsWorkflow.appliesToHostPlatform)
+ _validators.add(visualStudioValidator);
+
if (webWorkflow.appliesToHostPlatform)
_validators.add(const WebValidator());
diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart
index a4ddb7d..1d08075 100644
--- a/packages/flutter_tools/lib/src/windows/build_windows.dart
+++ b/packages/flutter_tools/lib/src/windows/build_windows.dart
@@ -14,6 +14,7 @@
import '../globals.dart';
import '../project.dart';
import 'msbuild_utils.dart';
+import 'visual_studio.dart';
/// Builds the Windows project using msbuild.
Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {String target = 'lib/main.dart'}) async {
@@ -31,9 +32,10 @@
}
writePropertySheet(windowsProject.generatedPropertySheetFile, environment);
- final String vcvarsScript = await findVcvars();
+ final String vcvarsScript = visualStudio.vcvarsPath;
if (vcvarsScript == null) {
- throwToolExit('Unable to build: could not find suitable toolchain.');
+ throwToolExit('Unable to find suitable Visual Studio toolchain. '
+ 'Please run `flutter doctor` for more details.');
}
final String buildScript = fs.path.join(
diff --git a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
index 5a5d173..db841ae 100644
--- a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
+++ b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
@@ -5,74 +5,6 @@
import 'package:xml/xml.dart' as xml;
import '../base/file_system.dart';
-import '../base/io.dart';
-import '../base/platform.dart';
-import '../base/process_manager.dart';
-import '../globals.dart';
-
-/// Returns the path to an installed vcvars64.bat script if found, or null.
-Future<String> findVcvars() async {
- final String vswherePath = fs.path.join(
- platform.environment['PROGRAMFILES(X86)'],
- 'Microsoft Visual Studio',
- 'Installer',
- 'vswhere.exe',
- );
- // The "Desktop development with C++" workload. This is a coarse check, since
- // it doesn't validate that the specific pieces are available, but should be
- // a reasonable first-pass approximation.
- // In the future, a set of more targetted checks will be used to provide
- // clear validation feedback (e.g., VS is installed, but missing component X).
- const String requiredComponent = 'Microsoft.VisualStudio.Workload.NativeDesktop';
-
- const String visualStudioInstallMessage =
- 'Ensure that you have Visual Studio 2017 or later installed, including '
- 'the "Desktop development with C++" workload.';
-
- if (!fs.file(vswherePath).existsSync()) {
- printError(
- 'Unable to locate Visual Studio: vswhere.exe not found\n'
- '$visualStudioInstallMessage',
- emphasis: true,
- );
- return null;
- }
-
- final ProcessResult whereResult = await processManager.run(<String>[
- vswherePath,
- '-latest',
- '-requires', requiredComponent,
- '-property', 'installationPath',
- ]);
- if (whereResult.exitCode != 0) {
- printError(
- 'Unable to locate Visual Studio:\n'
- '${whereResult.stdout}\n'
- '$visualStudioInstallMessage',
- emphasis: true,
- );
- return null;
- }
- final String visualStudioPath = whereResult.stdout.trim();
- if (visualStudioPath.isEmpty) {
- printError(
- 'No suitable Visual Studio found. $visualStudioInstallMessage\n',
- emphasis: true,
- );
- return null;
- }
- final String vcvarsPath =
- fs.path.join(visualStudioPath, 'VC', 'Auxiliary', 'Build', 'vcvars64.bat');
- if (!fs.file(vcvarsPath).existsSync()) {
- printError(
- 'vcvars64.bat does not exist at $vcvarsPath.\n',
- emphasis: true,
- );
- return null;
- }
-
- return vcvarsPath;
-}
/// Writes a property sheet (.props) file to expose all of the key/value
/// pairs in [variables] as enivornment variables.
diff --git a/packages/flutter_tools/lib/src/windows/visual_studio.dart b/packages/flutter_tools/lib/src/windows/visual_studio.dart
new file mode 100644
index 0000000..0253cbf
--- /dev/null
+++ b/packages/flutter_tools/lib/src/windows/visual_studio.dart
@@ -0,0 +1,186 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/platform.dart';
+import '../base/process_manager.dart';
+import '../convert.dart';
+
+VisualStudio get visualStudio => context.get<VisualStudio>();
+
+/// Encapsulates information about the installed copy of Visual Studio, if any.
+class VisualStudio {
+ /// True if a sufficiently recent version of Visual Studio is installed.
+ ///
+ /// Versions older than 2017 Update 2 won't be detected, so error messages to
+ /// users should take into account that [false] may mean that the user may
+ /// have an old version rather than no installation at all.
+ bool get isInstalled => _bestVisualStudioDetails != null;
+
+ /// True if there is a version of Visual Studio with all the components
+ /// necessary to build the project.
+ bool get hasNecessaryComponents => _usableVisualStudioDetails != null;
+
+ /// The name of the Visual Studio install.
+ ///
+ /// For instance: "Visual Studio Community 2017".
+ String get displayName => _bestVisualStudioDetails[_displayNameKey];
+
+ /// The user-friendly version number of the Visual Studio install.
+ ///
+ /// For instance: "15.4.0".
+ String get displayVersion =>
+ _bestVisualStudioDetails[_catalogKey][_catalogDisplayVersionKey];
+
+ /// The directory where Visual Studio is installed.
+ String get installLocation => _bestVisualStudioDetails[_installationPathKey];
+
+ /// The full version of the Visual Studio install.
+ ///
+ /// For instance: "15.4.27004.2002".
+ String get fullVersion => _bestVisualStudioDetails[_fullVersionKey];
+
+ /// The name of the recommended Visual Studio installer workload.
+ String get workloadDescription => 'Desktop development with C++';
+
+ /// The names of the components within the workload that must be installed.
+ ///
+ /// If there is an existing Visual Studio installation, the major version
+ /// should be provided here, as the descriptions of some componets differ
+ /// from version to version.
+ List<String> necessaryComponentDescriptions([int visualStudioMajorVersion]) {
+ return _requiredComponents(visualStudioMajorVersion).values.toList();
+ }
+
+ /// The path to vcvars64.bat, or null if no Visual Studio installation has
+ /// the components necessary to build.
+ String get vcvarsPath {
+ final Map<String, dynamic> details = _usableVisualStudioDetails;
+ if (details == null) {
+ return null;
+ }
+ return fs.path.join(
+ _usableVisualStudioDetails[_installationPathKey],
+ 'VC',
+ 'Auxiliary',
+ 'Build',
+ 'vcvars64.bat',
+ );
+ }
+
+ /// The path to vswhere.exe.
+ ///
+ /// vswhere should be installed for VS 2017 Update 2 and later; if it's not
+ /// present then there isn't a new enough installation of VS. This path is
+ /// not user-controllable, unlike the install location of Visual Studio
+ /// itself.
+ final String _vswherePath = fs.path.join(
+ platform.environment['PROGRAMFILES(X86)'],
+ 'Microsoft Visual Studio',
+ 'Installer',
+ 'vswhere.exe',
+ );
+
+ /// Components for use with vswhere requriements.
+ ///
+ /// Maps from component IDs to description in the installer UI.
+ /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
+ Map<String, String> _requiredComponents([int visualStudioMajorVersion]) {
+ // The description of the C++ toolchain required by the template. The
+ // component name is significantly different in different versions.
+ // Default to the latest known description, but use a specific string
+ // if a known older version is requested.
+ String cppToolchainDescription = 'MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.21)';
+ if (visualStudioMajorVersion == 15) {
+ cppToolchainDescription = 'VC++ 2017 version 15.9 v14.16 latest v141 tools';
+ }
+
+ return <String, String>{
+ // The MSBuild tool and related command-line toolchain.
+ 'Microsoft.Component.MSBuild': 'MSBuild',
+ // The C++ toolchain required by the template.
+ 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64': cppToolchainDescription,
+ // The Windows SDK version used by the template.
+ 'Microsoft.VisualStudio.Component.Windows10SDK.17763':
+ 'Windows 10 SDK (10.0.17763.0)',
+ };
+ }
+
+ // Keys in a VS details dictionary returned from vswhere.
+
+ /// The root directory of the Visual Studio installation.
+ static const String _installationPathKey = 'installationPath';
+
+ /// The user-friendly name of the installation.
+ static const String _displayNameKey = 'displayName';
+
+ /// The complete version.
+ static const String _fullVersionKey = 'installationVersion';
+
+ /// The 'catalog' entry containing more details.
+ static const String _catalogKey = 'catalog';
+
+ /// The user-friendly version.
+ ///
+ /// This key is under the 'catalog' entry.
+ static const String _catalogDisplayVersionKey = 'productDisplayVersion';
+
+ /// Returns the details dictionary for the newest version of Visual Studio
+ /// that includes all of [requiredComponents], if there is one.
+ Map<String, dynamic> _visualStudioDetails({Iterable<String> requiredComponents}) {
+ final List<String> requirementArguments = requiredComponents == null
+ ? <String>[]
+ : <String>['-requires', ...requiredComponents];
+ try {
+ final ProcessResult whereResult = processManager.runSync(<String>[
+ _vswherePath,
+ '-format', 'json',
+ '-utf8',
+ '-latest',
+ ...?requirementArguments,
+ ]);
+ if (whereResult.exitCode == 0) {
+ final List<Map<String, dynamic>> installations = json.decode(whereResult.stdout)
+ .cast<Map<String, dynamic>>();
+ if (installations.isNotEmpty) {
+ return installations[0];
+ }
+ }
+ } on ArgumentError {
+ // Thrown if vswhere doesnt' exist; ignore and return null below.
+ } on ProcessException {
+ // Ignored, return null below.
+ }
+ return null;
+ }
+
+ /// Returns the details dictionary for the latest version of Visual Studio
+ /// that has all required components.
+ Map<String, dynamic> _cachedUsableVisualStudioDetails;
+ Map<String, dynamic> get _usableVisualStudioDetails {
+ _cachedUsableVisualStudioDetails ??=
+ _visualStudioDetails(requiredComponents: _requiredComponents().keys);
+ return _cachedUsableVisualStudioDetails;
+ }
+
+ /// Returns the details dictionary of the latest version of Visual Studio,
+ /// regardless of components.
+ Map<String, dynamic> _cachedAnyVisualStudioDetails;
+ Map<String, dynamic> get _anyVisualStudioDetails {
+ _cachedAnyVisualStudioDetails ??= _visualStudioDetails();
+ return _cachedAnyVisualStudioDetails;
+ }
+
+ /// Returns the details dictionary of the best available version of Visual
+ /// Studio. If there's a version that has all the required components, that
+ /// will be returned, otherwise returs the lastest installed version (if any).
+ Map<String, dynamic> get _bestVisualStudioDetails {
+ if (_usableVisualStudioDetails != null) {
+ return _usableVisualStudioDetails;
+ }
+ return _anyVisualStudioDetails;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart b/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart
new file mode 100644
index 0000000..91c8047
--- /dev/null
+++ b/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart
@@ -0,0 +1,51 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/context.dart';
+import '../base/user_messages.dart';
+import '../doctor.dart';
+import 'visual_studio.dart';
+
+VisualStudioValidator get visualStudioValidator => context.get<VisualStudioValidator>();
+
+class VisualStudioValidator extends DoctorValidator {
+ const VisualStudioValidator() : super('Visual Studio - develop for Windows');
+
+ @override
+ Future<ValidationResult> validate() async {
+ final List<ValidationMessage> messages = <ValidationMessage>[];
+ ValidationType status = ValidationType.missing;
+ String versionInfo;
+
+ if (visualStudio.isInstalled) {
+ status = ValidationType.installed;
+
+ messages.add(ValidationMessage(
+ userMessages.visualStudioLocation(visualStudio.installLocation)
+ ));
+
+ messages.add(ValidationMessage(userMessages.visualStudioVersion(
+ visualStudio.displayName,
+ visualStudio.fullVersion,
+ )));
+
+ if (!visualStudio.hasNecessaryComponents) {
+ status = ValidationType.partial;
+ final int majorVersion = int.tryParse(visualStudio.fullVersion.split('.')[0]);
+ messages.add(ValidationMessage.error(
+ userMessages.visualStudioMissingComponents(
+ visualStudio.workloadDescription,
+ visualStudio.necessaryComponentDescriptions(majorVersion)
+ )
+ ));
+ }
+ versionInfo = '${visualStudio.displayName} ${visualStudio.displayVersion}';
+ } else {
+ status = ValidationType.missing;
+ messages.add(ValidationMessage.error(userMessages.visualStudioMissing));
+ }
+
+ return ValidationResult(status, messages, statusInfo: versionInfo);
+ }
+}
diff --git a/packages/flutter_tools/test/commands/build_windows_test.dart b/packages/flutter_tools/test/commands/build_windows_test.dart
index 6f9d98d..cc86816 100644
--- a/packages/flutter_tools/test/commands/build_windows_test.dart
+++ b/packages/flutter_tools/test/commands/build_windows_test.dart
@@ -9,6 +9,7 @@
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/windows/visual_studio.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:xml/xml.dart' as xml;
@@ -25,6 +26,7 @@
final MockPlatform windowsPlatform = MockPlatform()
..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
final MockPlatform notWindowsPlatform = MockPlatform();
+ final MockVisualStudio mockVisualStudio = MockVisualStudio();
const String solutionPath = r'C:\windows\Runner.sln';
const String visualStudioPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community';
const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat';
@@ -41,25 +43,6 @@
when(windowsPlatform.isWindows).thenReturn(true);
when(notWindowsPlatform.isWindows).thenReturn(false);
- // Sets up the mock environment so that lookup of vcvars64.bat will succeed.
- void enableVcvarsMocking() {
- const String vswherePath = r'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe';
- fs.file(vswherePath).createSync(recursive: true);
- fs.file(vcvarsPath).createSync(recursive: true);
-
- final MockProcessResult result = MockProcessResult();
- when(result.exitCode).thenReturn(0);
- when<String>(result.stdout).thenReturn(visualStudioPath);
- when(mockProcessManager.run(<String>[
- vswherePath,
- '-latest',
- '-requires', 'Microsoft.VisualStudio.Workload.NativeDesktop',
- '-property', 'installationPath',
- ])).thenAnswer((Invocation invocation) async {
- return result;
- });
- }
-
testUsingContext('Windows build fails when there is no vcvars64.bat', () async {
final BuildCommand command = BuildCommand();
applyMocksToCommand(command);
@@ -70,25 +53,27 @@
}, overrides: <Type, Generator>{
Platform: () => windowsPlatform,
FileSystem: () => memoryFilesystem,
+ VisualStudio: () => mockVisualStudio,
});
testUsingContext('Windows build fails when there is no windows project', () async {
final BuildCommand command = BuildCommand();
applyMocksToCommand(command);
- enableVcvarsMocking();
+ when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath);
expect(createTestCommandRunner(command).run(
const <String>['build', 'windows']
), throwsA(isInstanceOf<ToolExit>()));
}, overrides: <Type, Generator>{
Platform: () => windowsPlatform,
FileSystem: () => memoryFilesystem,
+ VisualStudio: () => mockVisualStudio,
});
testUsingContext('Windows build fails on non windows platform', () async {
final BuildCommand command = BuildCommand();
applyMocksToCommand(command);
fs.file(solutionPath).createSync(recursive: true);
- enableVcvarsMocking();
+ when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath);
fs.file('pubspec.yaml').createSync();
fs.file('.packages').createSync();
@@ -98,13 +83,14 @@
}, overrides: <Type, Generator>{
Platform: () => notWindowsPlatform,
FileSystem: () => memoryFilesystem,
+ VisualStudio: () => mockVisualStudio,
});
testUsingContext('Windows build invokes msbuild and writes generated files', () async {
final BuildCommand command = BuildCommand();
applyMocksToCommand(command);
fs.file(solutionPath).createSync(recursive: true);
- enableVcvarsMocking();
+ when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath);
fs.file('pubspec.yaml').createSync();
fs.file('.packages').createSync();
@@ -132,15 +118,16 @@
FileSystem: () => memoryFilesystem,
ProcessManager: () => mockProcessManager,
Platform: () => windowsPlatform,
+ VisualStudio: () => mockVisualStudio,
});
}
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {}
-class MockProcessResult extends Mock implements ProcessResult {}
class MockPlatform extends Mock implements Platform {
@override
Map<String, String> environment = <String, String>{
'FLUTTER_ROOT': r'C:\',
};
}
+class MockVisualStudio extends Mock implements VisualStudio {}
diff --git a/packages/flutter_tools/test/windows/visual_studio_test.dart b/packages/flutter_tools/test/windows/visual_studio_test.dart
new file mode 100644
index 0000000..198ae58
--- /dev/null
+++ b/packages/flutter_tools/test/windows/visual_studio_test.dart
@@ -0,0 +1,236 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/convert.dart';
+import 'package:flutter_tools/src/windows/visual_studio.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+class MockPlatform extends Mock implements Platform {
+ @override
+ Map<String, String> environment = <String, String>{};
+}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcessResult extends Mock implements ProcessResult {}
+
+void main() {
+ const String programFilesPath = r'C:\Program Files (x86)';
+ const String visualStudioPath = programFilesPath + r'\Microsoft Visual Studio\2017\Community';
+ const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat';
+ const String vswherePath = programFilesPath + r'\Microsoft Visual Studio\Installer\vswhere.exe';
+
+ final MockPlatform windowsPlatform = MockPlatform()
+ ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
+ MockProcessManager mockProcessManager;
+ final MemoryFileSystem memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows);
+
+ // Sets up the mock environment so that searching for Visual Studio with
+ // exactly the given required components will provide a result. By default it
+ // return a preset installation, but the response can be overridden.
+ void setMockVswhereResponse([List<String> requiredComponents, String response]) {
+ fs.file(vswherePath).createSync(recursive: true);
+ fs.file(vcvarsPath).createSync(recursive: true);
+
+ final MockProcessResult result = MockProcessResult();
+ when(result.exitCode).thenReturn(0);
+ when<String>(result.stdout).thenReturn(response == null
+ ? json.encode(<Map<String, dynamic>>[
+ <String, dynamic>{
+ 'installationPath': visualStudioPath,
+ 'displayName': 'Visual Studio Community 2017',
+ 'installationVersion': '15.9.28307.665',
+ 'catalog': <String, String>{
+ 'productDisplayVersion': '15.9.12',
+ },
+ }
+ ])
+ : response);
+
+ final List<String> requirementArguments = requiredComponents == null
+ ? <String>[]
+ : <String>['-requires', ...requiredComponents];
+ when(mockProcessManager.runSync(<String>[
+ vswherePath,
+ '-format', 'json',
+ '-utf8',
+ '-latest',
+ ...?requirementArguments,
+ ])).thenAnswer((Invocation invocation) {
+ return result;
+ });
+ }
+
+ // Sets whether or not a vswhere query without components will return an
+ // installation.
+ void setMockIncompleteVisualStudioExists(bool exists) {
+ setMockVswhereResponse(null, exists ? null : '[]');
+ }
+
+ // Sets whether or not a vswhere query with the required components will
+ // return an installation.
+ void setMockCompatibleVisualStudioExists(bool exists) {
+ setMockVswhereResponse(<String>[
+ 'Microsoft.Component.MSBuild',
+ 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
+ 'Microsoft.VisualStudio.Component.Windows10SDK.17763',
+ ], exists ? null : '[]');
+ }
+
+ group('Visual Studio', () {
+ VisualStudio visualStudio;
+
+ setUp(() {
+ mockProcessManager = MockProcessManager();
+ });
+
+ testUsingContext('isInstalled returns false when vswhere is missing', () {
+ when(mockProcessManager.runSync(any))
+ .thenThrow(const ProcessException('vswhere', <String>[]));
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.isInstalled, false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('vcvarsPath returns null when vswhere is missing', () {
+ when(mockProcessManager.runSync(any))
+ .thenThrow(const ProcessException('vswhere', <String>[]));
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.vcvarsPath, isNull);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('isInstalled returns false when vswhere returns non-zero', () {
+ when(mockProcessManager.runSync(any))
+ .thenThrow(const ProcessException('vswhere', <String>[]));
+ final MockProcessResult result = MockProcessResult();
+ when(result.exitCode).thenReturn(1);
+ when(mockProcessManager.runSync(any)).thenAnswer((Invocation invocation) {
+ return result;
+ });
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.isInstalled, false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('isInstalled returns true when VS is present but missing components', () {
+ setMockIncompleteVisualStudioExists(true);
+ setMockCompatibleVisualStudioExists(false);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.isInstalled, true);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('hasNecessaryComponents returns false when VS is present but missing components', () {
+ setMockIncompleteVisualStudioExists(true);
+ setMockCompatibleVisualStudioExists(false);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.hasNecessaryComponents, false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('vcvarsPath returns null when VS is present but missing components', () {
+ setMockIncompleteVisualStudioExists(true);
+ setMockCompatibleVisualStudioExists(false);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.vcvarsPath, isNull);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('VS metadata is available when VS is present, even if missing components', () {
+ setMockIncompleteVisualStudioExists(true);
+ setMockCompatibleVisualStudioExists(false);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.displayName, equals('Visual Studio Community 2017'));
+ expect(visualStudio.displayVersion, equals('15.9.12'));
+ expect(visualStudio.installLocation, equals(visualStudioPath));
+ expect(visualStudio.fullVersion, equals('15.9.28307.665'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+
+ testUsingContext('isInstalled returns true when VS is present but missing components', () {
+ setMockIncompleteVisualStudioExists(true);
+ setMockCompatibleVisualStudioExists(false);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.isInstalled, true);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('Everything returns good values when VS is present with all components', () {
+ setMockCompatibleVisualStudioExists(true);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.isInstalled, true);
+ expect(visualStudio.hasNecessaryComponents, true);
+ expect(visualStudio.vcvarsPath, equals(vcvarsPath));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('Metadata is for compatible version when latest is missing components', () {
+ setMockCompatibleVisualStudioExists(true);
+ // Return a different version for queries without the required packages.
+ final String incompleteVersionResponse = json.encode(<Map<String, dynamic>>[
+ <String, dynamic>{
+ 'installationPath': visualStudioPath,
+ 'displayName': 'Visual Studio Community 2019',
+ 'installationVersion': '16.1.1.1',
+ 'catalog': <String, String>{
+ 'productDisplayVersion': '16.1',
+ },
+ }
+ ]);
+ setMockVswhereResponse(null, incompleteVersionResponse);
+
+ visualStudio = VisualStudio();
+ expect(visualStudio.displayName, equals('Visual Studio Community 2017'));
+ expect(visualStudio.displayVersion, equals('15.9.12'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => memoryFilesystem,
+ Platform: () => windowsPlatform,
+ ProcessManager: () => mockProcessManager,
+ });
+ });
+}
diff --git a/packages/flutter_tools/test/windows/visual_studio_validator_test.dart b/packages/flutter_tools/test/windows/visual_studio_validator_test.dart
new file mode 100644
index 0000000..5f51431
--- /dev/null
+++ b/packages/flutter_tools/test/windows/visual_studio_validator_test.dart
@@ -0,0 +1,55 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/windows/visual_studio.dart';
+import 'package:flutter_tools/src/windows/visual_studio_validator.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+class MockVisualStudio extends Mock implements VisualStudio {}
+
+void main() {
+ group('Visual Studio validation', () {
+ MockVisualStudio mockVisualStudio;
+
+ setUp(() {
+ mockVisualStudio = MockVisualStudio();
+ });
+
+ testUsingContext('Emits missing status when Visual Studio is not installed', () async {
+ when(visualStudio.isInstalled).thenReturn(false);
+ const VisualStudioValidator validator = VisualStudioValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.missing);
+ }, overrides: <Type, Generator>{
+ VisualStudio: () => mockVisualStudio,
+ });
+
+ testUsingContext('Emits partial status when Visual Studio is installed without necessary components', () async {
+ when(visualStudio.isInstalled).thenReturn(true);
+ when(visualStudio.hasNecessaryComponents).thenReturn(false);
+ when(visualStudio.workloadDescription).thenReturn('Desktop development');
+ when(visualStudio.necessaryComponentDescriptions(any)).thenReturn(<String>['A', 'B']);
+ when(visualStudio.fullVersion).thenReturn('15.1');
+ const VisualStudioValidator validator = VisualStudioValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ VisualStudio: () => mockVisualStudio,
+ });
+
+ testUsingContext('Emits installed status when Visual Studio is installed with necessary components', () async {
+ when(visualStudio.isInstalled).thenReturn(true);
+ when(visualStudio.hasNecessaryComponents).thenReturn(true);
+ const VisualStudioValidator validator = VisualStudioValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.installed);
+ }, overrides: <Type, Generator>{
+ VisualStudio: () => mockVisualStudio,
+ });
+ });
+}