diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 032377f5e2a..09af158d940 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 @@ class UserMessages { 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 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 8d7a0ebac83..51bb7e7a0d4 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -46,6 +46,8 @@ import 'version.dart'; 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 runInContext( @@ -100,6 +102,8 @@ Future runInContext( 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 50cf645077a..a73f7e9933e 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -35,6 +35,7 @@ import 'version.dart'; 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(); @@ -68,6 +69,9 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { 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 a4ddb7d15b9..1d080752bed 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 '../convert.dart'; import '../globals.dart'; import '../project.dart'; import 'msbuild_utils.dart'; +import 'visual_studio.dart'; /// Builds the Windows project using msbuild. Future buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {String target = 'lib/main.dart'}) async { @@ -31,9 +32,10 @@ Future buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S } 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 5a5d173521e..db841aed3ac 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 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([ - 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 00000000000..0253cbf185c --- /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(); + +/// 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 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 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 _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 { + // 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 _visualStudioDetails({Iterable requiredComponents}) { + final List requirementArguments = requiredComponents == null + ? [] + : ['-requires', ...requiredComponents]; + try { + final ProcessResult whereResult = processManager.runSync([ + _vswherePath, + '-format', 'json', + '-utf8', + '-latest', + ...?requirementArguments, + ]); + if (whereResult.exitCode == 0) { + final List> installations = json.decode(whereResult.stdout) + .cast>(); + 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 _cachedUsableVisualStudioDetails; + Map get _usableVisualStudioDetails { + _cachedUsableVisualStudioDetails ??= + _visualStudioDetails(requiredComponents: _requiredComponents().keys); + return _cachedUsableVisualStudioDetails; + } + + /// Returns the details dictionary of the latest version of Visual Studio, + /// regardless of components. + Map _cachedAnyVisualStudioDetails; + Map 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 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 00000000000..91c8047cece --- /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(); + +class VisualStudioValidator extends DoctorValidator { + const VisualStudioValidator() : super('Visual Studio - develop for Windows'); + + @override + Future validate() async { + final List messages = []; + 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 6f9d98d3e53..cc86816b7d8 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/io.dart'; 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 @@ void main() { 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 @@ void main() { 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(result.stdout).thenReturn(visualStudioPath); - when(mockProcessManager.run([ - 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 @@ void main() { }, overrides: { 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 ['build', 'windows'] ), throwsA(isInstanceOf())); }, overrides: { 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 @@ void main() { }, overrides: { 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 @@ void main() { 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 environment = { '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 00000000000..198ae583ddc --- /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 environment = {}; +} +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 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(result.stdout).thenReturn(response == null + ? json.encode(>[ + { + 'installationPath': visualStudioPath, + 'displayName': 'Visual Studio Community 2017', + 'installationVersion': '15.9.28307.665', + 'catalog': { + 'productDisplayVersion': '15.9.12', + }, + } + ]) + : response); + + final List requirementArguments = requiredComponents == null + ? [] + : ['-requires', ...requiredComponents]; + when(mockProcessManager.runSync([ + 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([ + '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', [])); + + visualStudio = VisualStudio(); + expect(visualStudio.isInstalled, false); + }, overrides: { + FileSystem: () => memoryFilesystem, + Platform: () => windowsPlatform, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('vcvarsPath returns null when vswhere is missing', () { + when(mockProcessManager.runSync(any)) + .thenThrow(const ProcessException('vswhere', [])); + + visualStudio = VisualStudio(); + expect(visualStudio.vcvarsPath, isNull); + }, overrides: { + FileSystem: () => memoryFilesystem, + Platform: () => windowsPlatform, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('isInstalled returns false when vswhere returns non-zero', () { + when(mockProcessManager.runSync(any)) + .thenThrow(const ProcessException('vswhere', [])); + 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: { + 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: { + 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: { + 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: { + 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: { + 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: { + 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: { + 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(>[ + { + 'installationPath': visualStudioPath, + 'displayName': 'Visual Studio Community 2019', + 'installationVersion': '16.1.1.1', + 'catalog': { + 'productDisplayVersion': '16.1', + }, + } + ]); + setMockVswhereResponse(null, incompleteVersionResponse); + + visualStudio = VisualStudio(); + expect(visualStudio.displayName, equals('Visual Studio Community 2017')); + expect(visualStudio.displayVersion, equals('15.9.12')); + }, overrides: { + 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 00000000000..5f514313429 --- /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: { + 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(['A', 'B']); + when(visualStudio.fullVersion).thenReturn('15.1'); + const VisualStudioValidator validator = VisualStudioValidator(); + final ValidationResult result = await validator.validate(); + expect(result.type, ValidationType.partial); + }, overrides: { + 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: { + VisualStudio: () => mockVisualStudio, + }); + }); +}