diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9af239..0a3e4c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,13 +53,28 @@ It's also recommended that, if your machine supports it, you install [Docker for ```batch REM You only need to build once unless updating the Dockerfile or files it copies. -test\docker\build +docker\build REM This will automatically map build output. Defaults to Debug configuration. Pass -? for options. -test\docker\test +docker\test ``` -You can also run `test\docker\run.cmd` to start an interactive shell for exploratory testing. +For a faster development process, you can run `docker\run -detach`, copy the container ID printed to the window, then subsequently run `docker\test -on ` replacing `` with the container ID you copied previously. You can make changes to the test data and even rebuild the module and run this command again as frequently as you need. This is especially handy for quick turn around when debugging and fixing a problem. + +To stop the container, run `docker stop `. If you did not pass `-keep` when you started the container it will be removed automatically. + +### Debugging + +You can also run `docker\run.cmd` to start an interactive shell for exploratory testing. If no other commands are passed when starting the container, the Visual Studio Remote Debugger will launch by default. Remote debugging services are discoverable on your private network. + +1. Click *Debug -> Attach to Process* +2. Change *Transport* to "Remote (no authentication)" +3. Click *Find* +4. Click *Select* on the container (host name will be the container name) +5. Select "powershell" under *Available Processes* +6. Click *Attach* + +If you know the host name or IP address (depending on your network configuration for the container), you can type it into the *Qualifier* directory along with port 4020, e.g. "172.22.0.1:4020". ## Pull Requests diff --git a/README.md b/README.md index 996670a..f0d88ab 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To install for all users, pass `AllUsers` instead of `CurrentUser`, or just leav You can also download the ZIP package from the [Releases][releases] page on this project site and extract to a directory named _VSSetup_ under a directory in your `$env:PSMODULEPATH`. ```powershell -Expand-Archive VSSetup.zip "${env:USERPROFILE}\Documents\WindowsPowerShell\Modules\VSSetup" +Expand-Archive VSSetup.zip "$([Environment]::GetFolderPath("MyDocuments"))\WindowsPowerShell\Modules\VSSetup" ``` ## Using diff --git a/test/docker/Dockerfile b/docker/Dockerfile similarity index 60% rename from test/docker/Dockerfile rename to docker/Dockerfile index c023bdb..80305fd 100644 --- a/test/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,11 +4,24 @@ # Require .NET Framework FROM microsoft/windowsservercore -# Download and register current query APIs +# Download and install Remote Debugger SHELL ["powershell.exe", "-ExecutionPolicy", "Bypass", "-Command"] -ENV API_VERSION="1.5.125-rc" RUN $ErrorActionPreference = 'Stop' ; \ + $VerbosePreference = 'Continue' ; \ New-Item -Path C:\Downloads -Type Directory | Out-Null ; \ + Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/?LinkId=615470&clcid=0x409' -OutFile C:\Downloads\rtools_setup_x64.exe ; \ + Start-Process -Wait -FilePath C:\Downloads\rtools_setup_x64.exe -ArgumentList '-q' + +# Configure Remote Debugger +EXPOSE 3702 4020 4021 +RUN $ErrorActionPreference = 'Stop' ; \ + $VerbosePreference = 'Continue' ; \ + Start-Process -Wait -FilePath 'C:\Program Files\Microsoft Visual Studio 14.0\Common7\IDE\Remote Debugger\x64\msvsmon.exe' -ArgumentList '/prepcomputer', '/private', '/quiet' + +# Download and register current query APIs +ENV API_VERSION="1.8.24" +RUN $ErrorActionPreference = 'Stop' ; \ + $VerbosePreference = 'Continue' ; \ Invoke-WebRequest -Uri "https://www.nuget.org/api/v2/package/Microsoft.VisualStudio.Setup.Configuration.Native/${env:API_VERSION}" -OutFile C:\Downloads\Microsoft.VisualStudio.Setup.Configuration.Native.zip ; \ Expand-Archive -Path C:\Downloads\Microsoft.VisualStudio.Setup.Configuration.Native.zip -DestinationPath C:\Downloads\Microsoft.VisualStudio.Setup.Configuration.Native ; \ C:\Windows\System32\regsvr32.exe /s C:\Downloads\Microsoft.VisualStudio.Setup.Configuration.Native\tools\x64\Microsoft.VisualStudio.Setup.Configuration.Native.dll ; \ @@ -16,7 +29,10 @@ RUN $ErrorActionPreference = 'Stop' ; \ # Install latest version of Pester for integration testing RUN $ErrorActionPreference = 'Stop' ; \ + $VerbosePreference = 'Continue' ; \ Install-PackageProvider -Name nuget -MinimumVersion 2.8.5.201 -Force ; \ Install-Module -Name Pester -Scope CurrentUser -SkipPublisherCheck -Force +# Start Remote Debugger if no other command is passed to PowerShell ENTRYPOINT ["powershell.exe", "-ExecutionPolicy", "Unrestricted"] +CMD ["-NoExit", "-Command", "& 'C:\\Program Files\\Microsoft Visual Studio 14.0\\Common7\\IDE\\Remote Debugger\\x64\\msvsmon.exe' /silent /noauth /anyuser"] diff --git a/test/docker/Instances/1/state.json b/docker/Instances/1/state.json similarity index 100% rename from test/docker/Instances/1/state.json rename to docker/Instances/1/state.json diff --git a/test/docker/Instances/2/state.json b/docker/Instances/2/state.json similarity index 92% rename from test/docker/Instances/2/state.json rename to docker/Instances/2/state.json index 8cbb07b..cb89d26 100644 --- a/test/docker/Instances/2/state.json +++ b/docker/Instances/2/state.json @@ -73,6 +73,13 @@ "logFilePath": "C:\\TEMP\\dd_setup_201601180315_003_Microsoft.VisualStudio.Workload.Office_errors.log", "description": "Failed to install Microsoft.VisualStudio.Workload.Office" } + ], + "skippedPackages": [ + { + "id": "Microsoft.VisualStudio.Component.Sharepoint.Tools", + "version": "15.0.260009.0", + "type": "Component" + } ] } -} \ No newline at end of file +} diff --git a/test/docker/Instances/3/reboot.sem b/docker/Instances/3/reboot.sem similarity index 100% rename from test/docker/Instances/3/reboot.sem rename to docker/Instances/3/reboot.sem diff --git a/test/docker/Instances/3/state.json b/docker/Instances/3/state.json similarity index 100% rename from test/docker/Instances/3/state.json rename to docker/Instances/3/state.json diff --git a/test/docker/Instances/4/state.json b/docker/Instances/4/state.json similarity index 100% rename from test/docker/Instances/4/state.json rename to docker/Instances/4/state.json diff --git a/test/docker/Tests/GetInstance.Tests.ps1 b/docker/Tests/GetInstance.Tests.ps1 similarity index 78% rename from test/docker/Tests/GetInstance.Tests.ps1 rename to docker/Tests/GetInstance.Tests.ps1 index 17f8f8e..8e07294 100644 --- a/test/docker/Tests/GetInstance.Tests.ps1 +++ b/docker/Tests/GetInstance.Tests.ps1 @@ -62,6 +62,13 @@ Describe 'Get-VSSetupInstance' { $instance.InstanceId | Should Be 1 $instance.InstallationVersion | Should Be '15.0.26116.0' } + + It 'Does not contain errors' { + $instance = Get-VSSetupInstance 'C:\VS\Community' + + $instance.State -band 'NoErrors' | Should Be 'NoErrors' + $instance.Errors | Should Be $null + } } Context 'Contains custom properties' { @@ -86,4 +93,23 @@ Describe 'Get-VSSetupInstance' { $instance.Properties.Count | Should Be 0 } } + + Context 'Contains errors' { + $instance = Get-VSSetupInstance C:\VS\Enterprise + + It 'Contains errors' { + $instance.State -band 'NoErrors' | Should Be 0 + $instance.Errors | Should Not Be $null + } + + It 'Contains failed packages' { + $instance.Errors.FailedPackages.Count | Should Be 1 + $instance.Errors.FailedPackages[0].Id | Should Be 'Microsoft.VisualStudio.Workload.Office' + } + + It 'Contains skipped packages' { + $instance.Errors.SkippedPackages.Count | Should Be 1 + $instance.Errors.SkippedPackages[0].Id | Should Be 'Microsoft.VisualStudio.Component.Sharepoint.Tools' + } + } } diff --git a/test/docker/Tests/SelectInstance.Tests.ps1 b/docker/Tests/SelectInstance.Tests.ps1 similarity index 100% rename from test/docker/Tests/SelectInstance.Tests.ps1 rename to docker/Tests/SelectInstance.Tests.ps1 diff --git a/test/docker/build.cmd b/docker/build.cmd similarity index 100% rename from test/docker/build.cmd rename to docker/build.cmd diff --git a/test/docker/run.cmd b/docker/run.cmd similarity index 58% rename from test/docker/run.cmd rename to docker/run.cmd index 40d1710..54451cc 100644 --- a/test/docker/run.cmd +++ b/docker/run.cmd @@ -8,10 +8,11 @@ setlocal if "%script%"=="" set script=%~nx0 set projectDir=%~dp0 -set solutionDir=%projectDir:~0,-12% +set solutionDir=%projectDir:~0,-7% set configuration=Debug set name=vssetup/test +set mode=-it :parse if "%1"=="" goto :parse_end @@ -20,6 +21,10 @@ if /i "%1"=="-name" set name=%2& shift& shift& goto :parse if /i "%1"=="/name" set name=%2& shift& shift& goto :parse if /i "%1"=="-configuration" set configuration=%2& shift& shift& goto :parse if /i "%1"=="/configuration" set configuration=%2& shift& shift& goto :parse +if /i "%1"=="-detach" set mode=-d& shift& goto :parse +if /i "%1"=="/detach" set mode=-d& shift& goto :parse +if /i "%1"=="-on" set id=%2& shift& shift& goto :parse +if /i "%1"=="/on" set id=%2& shift& shift& goto :parse if /i "%1"=="-network" set params=%params% --network "%2"& shift& shift& goto :parse if /i "%1"=="/network" set params=%params% --network "%2"& shift& shift& goto :parse if /i "%1"=="-keep" set keep=1& shift& goto :parse @@ -38,19 +43,26 @@ goto :help if "%keep%"=="" set params=%params% --rm set outputPath=%solutionDir%src\VSSetup.PowerShell\bin\%configuration% -set volumes=-v "%projectDir%Instances:C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances" +set volumes=-v "%outputPath%:C:\Users\ContainerAdministrator\Documents\WindowsPowerShell\Modules\VSSetup:ro" +set volumes=%volumes% -v "%projectDir%Instances:C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances:ro" set volumes=%volumes% -v C:\VS\Community set volumes=%volumes% -v C:\VS\Professional set volumes=%volumes% -v C:\VS\Enterprise set volumes=%volumes% -v C:\BuildTools set volumes=%volumes% -v "%projectDir%Tests:C:\Tests" -set volumes=%volumes% -v "%outputPath%:C:\Users\ContainerAdministrator\Documents\WindowsPowerShell\Modules\VSSetup" -@echo on -docker run -it %volumes%%params% %name% %args% +if "%id%"=="" ( + REM Uses the ENTRYPOINT declaration in the Dockerfile + set cmd=docker run %mode% %volumes%%params% %name% %args% +) else ( + REM Keep in sync with the ENTRYPOINT in the Dockerfile + set cmd=docker exec %mode% %id% powershell.exe -ExecutionPolicy Unrestricted %args% +) + +echo %cmd% +call %cmd% @if errorlevel 1 exit /b %ERRORLEVEL% -@echo off echo. goto :EOF @@ -63,11 +75,13 @@ echo. echo %usage% echo. echo Options: -echo -name Image name. Defaults to vssetup/test. -echo -configuration The build configuration to map. Defaults to Debug. -echo -network External network name. Defaults to discovered transparent network. -echo -keep Do not delete the container after exiting. -echo -? Displays this help message. +echo -name value Image name. Defaults to vssetup/test. +echo -configuration value The build configuration to map. Defaults to Debug. +echo -detach Detach from the container and show the ID. +echo -on value Run command on specified container ID. +echo -network value External network name. Defaults to discovered transparent network. +echo -keep Do not delete the container after exiting. +echo -? Displays this help message. echo. if "%noargs%"=="" ( echo Arguments: diff --git a/test/docker/test.cmd b/docker/test.cmd similarity index 100% rename from test/docker/test.cmd rename to docker/test.cmd diff --git a/src/VSSetup.PowerShell/Errors.cs b/src/VSSetup.PowerShell/Errors.cs new file mode 100644 index 0000000..fe50089 --- /dev/null +++ b/src/VSSetup.PowerShell/Errors.cs @@ -0,0 +1,38 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using Configuration; + + /// + /// Represents errors that occurred during the last install operation. + /// + public class Errors + { + private readonly IList failedPackages; + private readonly IList skippedPackages; + + internal Errors(ISetupErrorState errors) + { + Validate.NotNull(errors, nameof(errors)); + + FailedPackages = Utilities.TrySetCollection(ref failedPackages, nameof(FailedPackages), errors.GetFailedPackages, PackageReferenceFactory.Create); + SkippedPackages = Utilities.TrySetCollection(ref skippedPackages, nameof(SkippedPackages), errors.GetSkippedPackages, PackageReferenceFactory.Create); + } + + /// + /// Gets a collection of references to packages that failed to install. + /// + public ReadOnlyCollection FailedPackages { get; } + + /// + /// Gets a collection of references to packages skipped because other packages in their parent workload or components failed. + /// + public ReadOnlyCollection SkippedPackages { get; } + } +} diff --git a/src/VSSetup.PowerShell/FailedPackageReference.cs b/src/VSSetup.PowerShell/FailedPackageReference.cs new file mode 100644 index 0000000..56afce3 --- /dev/null +++ b/src/VSSetup.PowerShell/FailedPackageReference.cs @@ -0,0 +1,26 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using Configuration; + + /// + /// Represents a failed package reference. + /// + public class FailedPackageReference : PackageReference + { + /// + /// Initializes a new instance of the class. + /// + /// The to adapt. + /// is null. + internal FailedPackageReference(ISetupFailedPackageReference reference) + : base(reference) + { + Validate.NotNull(reference, nameof(reference)); + } + } +} diff --git a/src/VSSetup.PowerShell/Instance.cs b/src/VSSetup.PowerShell/Instance.cs index e7b362b..498ca19 100644 --- a/src/VSSetup.PowerShell/Instance.cs +++ b/src/VSSetup.PowerShell/Instance.cs @@ -12,7 +12,6 @@ namespace Microsoft.VisualStudio.Setup using System.Globalization; using System.Linq; using System.Reflection; - using System.Runtime.InteropServices; using Configuration; /// @@ -58,75 +57,101 @@ namespace Microsoft.VisualStudio.Setup // The instance ID is required, but then try to set other properties to release the COM object and its resources ASAP. InstanceId = instance.GetInstanceId(); - TrySet(ref installationName, nameof(InstallationName), instance.GetInstallationName); - TrySet(ref installationPath, nameof(InstallationPath), instance.GetInstallationPath); - TrySet(ref installationVersion, nameof(InstallationVersion), () => - { - Version version; - - var versionString = instance.GetInstallationVersion(); - if (Version.TryParse(versionString, out version)) + Utilities.TrySet(ref installationName, nameof(InstallationName), instance.GetInstallationName, OnError); + Utilities.TrySet(ref installationPath, nameof(InstallationPath), instance.GetInstallationPath, OnError); + Utilities.TrySet( + ref installationVersion, + nameof(InstallationVersion), + () => { - return version.Normalize(); - } + Version version; - return null; - }); + var versionString = instance.GetInstallationVersion(); + if (Version.TryParse(versionString, out version)) + { + return version.Normalize(); + } - TrySet(ref installDate, nameof(InstallDate), () => - { - var ft = instance.GetInstallDate(); - var l = ((long)ft.dwHighDateTime << 32) + ft.dwLowDateTime; + return null; + }, + OnError); - return DateTime.FromFileTime(l); - }); + Utilities.TrySet( + ref installDate, + nameof(InstallDate), + () => + { + var ft = instance.GetInstallDate(); + var l = ((long)ft.dwHighDateTime << 32) + ft.dwLowDateTime; - TrySet(ref state, nameof(State), instance.GetState); + return DateTime.FromFileTime(l); + }, + OnError); + + Utilities.TrySet(ref state, nameof(State), instance.GetState, OnError); var lcid = CultureInfo.CurrentUICulture.LCID; - TrySet(ref displayName, nameof(DisplayName), () => - { - return instance.GetDisplayName(lcid); - }); - - TrySet(ref description, nameof(Description), () => - { - return instance.GetDescription(lcid); - }); - - TrySet(ref productPath, nameof(ProductPath), () => - { - var path = instance.GetProductPath(); - return instance.ResolvePath(path); - }); - - TrySet(ref product, nameof(Product), () => - { - var reference = instance.GetProduct(); - if (reference != null) + Utilities.TrySet( + ref displayName, + nameof(DisplayName), + () => { - return new PackageReference(reference); - } + return instance.GetDisplayName(lcid); + }, + OnError); - return null; - }); + Utilities.TrySet( + ref description, + nameof(Description), + () => + { + return instance.GetDescription(lcid); + }, + OnError); - TrySet(ref packages, nameof(Packages), () => + Utilities.TrySet( + ref productPath, + nameof(ProductPath), + () => + { + var path = instance.GetProductPath(); + return instance.ResolvePath(path); + }, + OnError); + + Utilities.TrySet( + ref product, + nameof(Product), + () => + { + var reference = instance.GetProduct(); + if (reference != null) + { + return new PackageReference(reference); + } + + return null; + }, + OnError); + + Packages = Utilities.TrySetCollection(ref packages, nameof(Packages), instance.GetPackages, PackageReferenceFactory.Create, OnError); + + var errors = instance.GetErrors(); + if (errors != null) { - return new List(GetPackages(instance)); - }); - - if (packages != null && packages.Any()) - { - Packages = new ReadOnlyCollection(packages); + Errors = new Errors(errors); } - TrySet(ref properties, nameof(Properties), () => - { - var properties = instance.GetProperties(); - return properties?.GetNames() - .ToDictionary(name => name.ToPascalCase(), name => properties.GetValue(name), StringComparer.OrdinalIgnoreCase); - }); + Utilities.TrySet( + ref properties, + nameof(Properties), + () => + { + var properties = instance.GetProperties(); + return properties?.GetNames() + .ToDictionary(name => name.ToPascalCase(), name => properties.GetValue(name), StringComparer.OrdinalIgnoreCase); + }, + OnError); if (properties != null) { @@ -138,9 +163,9 @@ namespace Microsoft.VisualStudio.Setup Properties = ReadOnlyDictionary.Empty; } - TrySet(ref enginePath, nameof(EnginePath), instance.GetEnginePath); - TrySet(ref isComplete, nameof(IsComplete), instance.IsComplete); - TrySet(ref isLaunchable, nameof(IsLaunchable), instance.IsLaunchable); + Utilities.TrySet(ref enginePath, nameof(EnginePath), instance.GetEnginePath, OnError); + Utilities.TrySet(ref isComplete, nameof(IsComplete), instance.IsComplete, OnError); + Utilities.TrySet(ref isLaunchable, nameof(IsLaunchable), instance.IsLaunchable, OnError); // Get all properties of the instance not explicitly declared. var store = (ISetupPropertyStore)instance; @@ -209,6 +234,11 @@ namespace Microsoft.VisualStudio.Setup /// public IDictionary Properties { get; } + /// + /// Gets errors that occurred during the last install (if any). + /// + public Errors Errors { get; } + /// /// Gets the path to the engine that installed the instance. /// @@ -229,31 +259,9 @@ namespace Microsoft.VisualStudio.Setup /// internal IDictionary AdditionalProperties { get; } - private static IEnumerable GetPackages(ISetupInstance2 instance) + private void OnError(string propertyName) { - var references = instance.GetPackages(); - if (references != null) - { - foreach (var reference in instance.GetPackages()) - { - if (reference != null) - { - yield return new PackageReference(reference); - } - } - } - } - - private void TrySet(ref T property, string propertyName, Func action) - { - try - { - property = action.Invoke(); - } - catch (COMException ex) when (ex.ErrorCode == NativeMethods.E_NOTFOUND) - { - Trace.WriteLine($@"Instance: property ""{propertyName}"" not found on instance ""{InstanceId}""."); - } + Trace.WriteLine($@"Instance: property ""{propertyName}"" not found on instance ""{InstanceId}""."); } } } diff --git a/src/VSSetup.PowerShell/PackageReference.cs b/src/VSSetup.PowerShell/PackageReference.cs index 77fc37e..6e368c5 100644 --- a/src/VSSetup.PowerShell/PackageReference.cs +++ b/src/VSSetup.PowerShell/PackageReference.cs @@ -6,8 +6,6 @@ namespace Microsoft.VisualStudio.Setup { using System; - using System.Diagnostics; - using System.Runtime.InteropServices; using Configuration; /// @@ -34,7 +32,7 @@ namespace Microsoft.VisualStudio.Setup // The package reference ID is required, but then try to set other properties to release the COM object and its resources ASAP. Id = reference.GetId(); - TrySet(ref version, nameof(Version), () => + Utilities.TrySet(ref version, nameof(Version), () => { Version version; @@ -47,11 +45,11 @@ namespace Microsoft.VisualStudio.Setup return null; }); - TrySet(ref chip, nameof(Chip), reference.GetChip); - TrySet(ref branch, nameof(Branch), reference.GetBranch); - TrySet(ref type, nameof(Type), reference.GetType); - TrySet(ref isExtension, nameof(IsExtension), reference.GetIsExtension); - TrySet(ref uniqueId, nameof(UniqueId), reference.GetUniqueId); + Utilities.TrySet(ref chip, nameof(Chip), reference.GetChip); + Utilities.TrySet(ref branch, nameof(Branch), reference.GetBranch); + Utilities.TrySet(ref type, nameof(Type), reference.GetType); + Utilities.TrySet(ref isExtension, nameof(IsExtension), reference.GetIsExtension); + Utilities.TrySet(ref uniqueId, nameof(UniqueId), reference.GetUniqueId); } /// @@ -97,17 +95,5 @@ namespace Microsoft.VisualStudio.Setup { return uniqueId ?? base.ToString(); } - - private void TrySet(ref T property, string propertyName, Func action) - { - try - { - property = action.Invoke(); - } - catch (COMException ex) when (ex.ErrorCode == NativeMethods.E_NOTFOUND) - { - Trace.WriteLine($@"Instance: property ""{propertyName}"" not found on package reference ""{Id}""."); - } - } } } diff --git a/src/VSSetup.PowerShell/PackageReferenceFactory.cs b/src/VSSetup.PowerShell/PackageReferenceFactory.cs new file mode 100644 index 0000000..4e2e246 --- /dev/null +++ b/src/VSSetup.PowerShell/PackageReferenceFactory.cs @@ -0,0 +1,41 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using Configuration; + + /// + /// Creates or derivative classes. + /// + internal static class PackageReferenceFactory + { + /// + /// Creates a new from an . + /// + /// The to wrap. + /// A that wraps the . + /// is null. + public static PackageReference Create(ISetupPackageReference package) + { + Validate.NotNull(package, nameof(package)); + + return new PackageReference(package); + } + + /// + /// Creates a new from an . + /// + /// The to wrap. + /// A that wraps the . + /// is null. + public static FailedPackageReference Create(ISetupFailedPackageReference package) + { + Validate.NotNull(package, nameof(package)); + + return new FailedPackageReference(package); + } + } +} diff --git a/src/VSSetup.PowerShell/Utilities.cs b/src/VSSetup.PowerShell/Utilities.cs new file mode 100644 index 0000000..b1a3efb --- /dev/null +++ b/src/VSSetup.PowerShell/Utilities.cs @@ -0,0 +1,131 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Runtime.InteropServices; + using Configuration; + + /// + /// Utility methods. + /// + internal static class Utilities + { + /// + /// Gets an empty . + /// + /// The type of element. + /// An empty . + public static ReadOnlyCollection EmptyReadOnlyCollection() + { + return EmptyReadOnlyCollectionContainer.Instance; + } + + /// + /// Gets an of adapted packages from type to type . + /// + /// The type of the package reference to adapt. + /// The adapted type of the package reference. + /// A method that gets an of package reference of type . + /// A method that creates an adapted reference of type . + /// An of adapted packages. This enumeration may yield zero results. + /// or is null. + public static IEnumerable GetAdaptedPackages(Func> action, Func creator) + where T : ISetupPackageReference + where R : PackageReference + { + Validate.NotNull(action, nameof(action)); + Validate.NotNull(creator, nameof(creator)); + + return YieldAdaptedPackages(action, creator); + } + + /// + /// Sets the given if the does not throw a for 0x80070490. + /// + /// The type of the property to set. + /// A reference to the property to set. + /// The name of the property for diagnostic purposes. + /// A method that returns the value of the property to set. + /// Optional error handler that accepts the name of the property. + /// is an empty string. + /// or is null. + public static void TrySet(ref T property, string propertyName, Func action, Action error = null) + { + Validate.NotNullOrEmpty(propertyName, nameof(propertyName)); + Validate.NotNull(action, nameof(action)); + + try + { + property = action.Invoke(); + } + catch (COMException ex) when (ex.ErrorCode == NativeMethods.E_NOTFOUND) + { + error?.Invoke(propertyName); + } + } + + /// + /// Sets the given package reference collection if the does not throw a for 0x80070490. + /// + /// The type of the package reference to adapt. + /// The adapted type of the package reference. + /// A reference to the property to set. + /// The name of the property for diagnostic purposes. + /// A method that returns the value of the property to set. + /// A method that creates the adapted reference type. + /// Optional error handler that accepts the name of the property. + /// A containing the adapted package references. This collection may be empty. + /// is an empty string. + /// One or more parameters is null. + public static ReadOnlyCollection TrySetCollection( + ref IList property, + string propertyName, + Func> action, + Func creator, + Action error = null) + where T : ISetupPackageReference + where R : PackageReference + { + Validate.NotNullOrEmpty(propertyName, nameof(propertyName)); + Validate.NotNull(action, nameof(action)); + Validate.NotNull(creator, nameof(creator)); + + var packages = GetAdaptedPackages(action, creator); + TrySet(ref property, propertyName, packages.ToList, error); + + if (property != null && property.Any()) + { + return new ReadOnlyCollection(property); + } + + return EmptyReadOnlyCollection(); + } + + private static IEnumerable YieldAdaptedPackages(Func> action, Func creator) + { + var references = action?.Invoke(); + if (references != null) + { + foreach (var reference in references) + { + if (reference != null) + { + yield return creator(reference); + } + } + } + } + + private class EmptyReadOnlyCollectionContainer + { + public static readonly ReadOnlyCollection Instance = new ReadOnlyCollection(new T[0]); + } + } +} diff --git a/src/VSSetup.PowerShell/VSSetup.PowerShell.csproj b/src/VSSetup.PowerShell/VSSetup.PowerShell.csproj index b7ee305..9c60d8d 100644 --- a/src/VSSetup.PowerShell/VSSetup.PowerShell.csproj +++ b/src/VSSetup.PowerShell/VSSetup.PowerShell.csproj @@ -35,7 +35,7 @@ - ..\..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.5.125-rc\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll + ..\..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.8.24\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll True @@ -47,10 +47,13 @@ + + + @@ -64,6 +67,7 @@ Resources.resx + diff --git a/src/VSSetup.PowerShell/packages.config b/src/VSSetup.PowerShell/packages.config index 0ed54f7..ad4e24f 100644 --- a/src/VSSetup.PowerShell/packages.config +++ b/src/VSSetup.PowerShell/packages.config @@ -1,7 +1,7 @@  - + diff --git a/test/VSSetup.PowerShell.Test/ErrorsTests.cs b/test/VSSetup.PowerShell.Test/ErrorsTests.cs new file mode 100644 index 0000000..fef7f70 --- /dev/null +++ b/test/VSSetup.PowerShell.Test/ErrorsTests.cs @@ -0,0 +1,86 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using System; + using System.Runtime.InteropServices; + using Configuration; + using Moq; + using Xunit; + + public class ErrorsTests + { + [Fact] + public void New_Errors_Null_Throws() + { + Assert.Throws("errors", () => new Errors(null)); + } + + [Fact] + public void New_Missing_FailedPackages() + { + var errors = new Mock(); + errors.Setup(x => x.GetFailedPackages()).Throws(new COMException("Not found", NativeMethods.E_NOTFOUND)); + + var sut = new Errors(errors.Object); + + Assert.Empty(sut.FailedPackages); + } + + [Fact] + public void New_FailedPackages() + { + var a = new Mock(); + a.As().Setup(x => x.GetId()).Returns("a"); + + var b = new Mock(); + b.As().Setup(x => x.GetId()).Returns("b"); + + var errors = new Mock(); + errors.Setup(x => x.GetFailedPackages()).Returns(new[] { a.Object, b.Object }); + + var sut = new Errors(errors.Object); + + Assert.NotNull(sut.FailedPackages); + Assert.Collection( + sut.FailedPackages, + x => Assert.Equal("a", x.Id), + x => Assert.Equal("b", x.Id)); + } + + [Fact] + public void New_Missing_SkippedPackages() + { + var errors = new Mock(); + errors.Setup(x => x.GetSkippedPackages()).Throws(new COMException("Not found", NativeMethods.E_NOTFOUND)); + + var sut = new Errors(errors.Object); + + Assert.Empty(sut.SkippedPackages); + } + + [Fact] + public void New_SkippedPackages() + { + var skippedPackages = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + var errors = new Mock(); + errors.Setup(x => x.GetSkippedPackages()).Returns(skippedPackages); + + var sut = new Errors(errors.Object); + + Assert.NotNull(sut.SkippedPackages); + Assert.Collection( + sut.SkippedPackages, + x => Assert.Equal("a", x.Id), + x => Assert.Equal("b", x.Id)); + } + } +} diff --git a/test/VSSetup.PowerShell.Test/FailedPackageReferenceTests.cs b/test/VSSetup.PowerShell.Test/FailedPackageReferenceTests.cs new file mode 100644 index 0000000..5f9a958 --- /dev/null +++ b/test/VSSetup.PowerShell.Test/FailedPackageReferenceTests.cs @@ -0,0 +1,31 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using System; + using Configuration; + using Moq; + using Xunit; + + public class FailedPackageReferenceTests + { + [Fact] + public void New_Reference_Null_Throws() + { + Assert.Throws("reference", () => new FailedPackageReference(null)); + } + + [Fact] + public void New_Valid() + { + var reference = new Mock(); + reference.As().Setup(x => x.GetId()).Returns("a"); + + var sut = new FailedPackageReference(reference.Object); + Assert.Equal("a", sut.Id); + } + } +} diff --git a/test/VSSetup.PowerShell.Test/InstanceTests.cs b/test/VSSetup.PowerShell.Test/InstanceTests.cs index 36c04bd..3b4681c 100644 --- a/test/VSSetup.PowerShell.Test/InstanceTests.cs +++ b/test/VSSetup.PowerShell.Test/InstanceTests.cs @@ -106,7 +106,7 @@ namespace Microsoft.VisualStudio.Setup instance.Setup(x => x.GetInstanceId()).Returns("test"); var sut = new Instance(instance.Object); - Assert.Null(sut.Packages); + Assert.Empty(sut.Packages); } [Fact] @@ -116,7 +116,7 @@ namespace Microsoft.VisualStudio.Setup instance.Setup(x => x.GetPackages()).Returns(Enumerable.Empty().ToArray()); var sut = new Instance(instance.Object); - Assert.Null(sut.Packages); + Assert.Empty(sut.Packages); } [Fact] @@ -152,5 +152,63 @@ namespace Microsoft.VisualStudio.Setup Assert.Equal(2, sut.AdditionalProperties["B"]); Assert.Equal(2, sut.AdditionalProperties["b"]); } + + [Fact] + public void New_No_Errors() + { + instance.Setup(x => x.GetInstanceId()).Returns("test"); + + var sut = new Instance(instance.Object); + + Assert.Null(sut.Errors); + } + + [Fact] + public void New_FailedPackages() + { + var a = new Mock(); + a.As().Setup(x => x.GetId()).Returns("a"); + + var b = new Mock(); + b.As().Setup(x => x.GetId()).Returns("b"); + + var errors = new Mock(); + errors.Setup(x => x.GetFailedPackages()).Returns(new[] { a.Object, b.Object }); + + instance.Setup(x => x.GetInstanceId()).Returns("test"); + instance.Setup(x => x.GetErrors()).Returns(errors.Object); + + var sut = new Instance(instance.Object); + + Assert.NotNull(sut.Errors); + Assert.Collection( + sut.Errors.FailedPackages, + x => Assert.Equal("a", x.Id), + x => Assert.Equal("b", x.Id)); + } + + [Fact] + public void New_SkippedPackages() + { + var skippedPackages = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + var errors = new Mock(); + errors.Setup(x => x.GetSkippedPackages()).Returns(skippedPackages); + + instance.Setup(x => x.GetInstanceId()).Returns("test"); + instance.Setup(x => x.GetErrors()).Returns(errors.Object); + + var sut = new Instance(instance.Object); + + Assert.NotNull(sut.Errors); + Assert.Collection( + sut.Errors.SkippedPackages, + x => Assert.Equal("a", x.Id), + x => Assert.Equal("b", x.Id)); + } } } diff --git a/test/VSSetup.PowerShell.Test/PackageReferenceFactoryTests.cs b/test/VSSetup.PowerShell.Test/PackageReferenceFactoryTests.cs new file mode 100644 index 0000000..1703b3a --- /dev/null +++ b/test/VSSetup.PowerShell.Test/PackageReferenceFactoryTests.cs @@ -0,0 +1,47 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using System; + using Configuration; + using Moq; + using Xunit; + + public class PackageReferenceFactoryTests + { + [Fact] + public void Create_PackageReference_Null_Throws() + { + Assert.Throws("package", () => PackageReferenceFactory.Create((ISetupPackageReference)null)); + } + + [Fact] + public void Create_PackageReference() + { + var reference = Mock.Of(x => x.GetId() == "a"); + var actual = PackageReferenceFactory.Create(reference); + + Assert.IsType(actual); + } + + [Fact] + public void Create_FailedPackageReference_Null_Throws() + { + Assert.Throws("package", () => PackageReferenceFactory.Create((ISetupFailedPackageReference)null)); + } + + [Fact] + public void Create_FailedPackageReference() + { + var reference = new Mock(); + reference.As().Setup(x => x.GetId()).Returns("a"); + + var actual = PackageReferenceFactory.Create(reference.Object); + + Assert.IsType(actual); + } + } +} diff --git a/test/VSSetup.PowerShell.Test/UtilitiesTests.cs b/test/VSSetup.PowerShell.Test/UtilitiesTests.cs new file mode 100644 index 0000000..430e0f0 --- /dev/null +++ b/test/VSSetup.PowerShell.Test/UtilitiesTests.cs @@ -0,0 +1,233 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +namespace Microsoft.VisualStudio.Setup +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Runtime.InteropServices; + using Configuration; + using Moq; + using Xunit; + + public class UtilitiesTests + { + [Fact] + public void EmptyReadOnlyCollection_Empty() + { + Assert.Empty(Utilities.EmptyReadOnlyCollection()); + } + + [Fact] + public void EmptyReadOnlyCollection_Singleton() + { + Assert.Same(Utilities.EmptyReadOnlyCollection(), Utilities.EmptyReadOnlyCollection()); + } + + [Fact] + public void GetAdaptedProperties_Action_Null_Throws() + { + Assert.Throws("action", () => Utilities.GetAdaptedPackages(null, null)); + } + + [Fact] + public void GetAdaptedProperties_Creator_Null_Throws() + { + var references = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + Assert.Throws("creator", () => Utilities.GetAdaptedPackages(() => references, null)); + } + + [Fact] + public void GetAdaptedProperties_Empty() + { + var references = new ISetupPackageReference[0]; + Func creator = reference => PackageReferenceFactory.Create(reference); + + var packages = Utilities.GetAdaptedPackages(() => references, creator).ToList(); + + Assert.Empty(packages); + } + + [Fact] + public void GetAdaptedProperties() + { + var references = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + Func creator = reference => PackageReferenceFactory.Create(reference); + + var packages = Utilities.GetAdaptedPackages(() => references, creator).ToList(); + + Assert.Collection( + packages, + x => Assert.Equal("a", x.Id), + x => Assert.Equal("b", x.Id)); + } + + [Fact] + public void TrySet_PropertyName_Null_Throws() + { + string property = null; + Assert.Throws("propertyName", () => Utilities.TrySet(ref property, null, null)); + } + + [Fact] + public void TrySet_PropertyName_Empty_Throws() + { + string property = null; + Assert.Throws("propertyName", () => Utilities.TrySet(ref property, string.Empty, null)); + } + + [Fact] + public void TrySet_Action_Null_Throws() + { + string property = null; + Assert.Throws("action", () => Utilities.TrySet(ref property, nameof(property), null)); + } + + [Fact] + public void TrySet_COMException_E_NOTFOUND() + { + string property = null; + Utilities.TrySet(ref property, nameof(property), () => { throw new COMException("Not found", NativeMethods.E_NOTFOUND); }); + + Assert.Null(property); + } + + [Fact] + public void TrySet_COMException_REGDB_E_CLASSNOTREG() + { + string property = null; + var ex = Assert.Throws(() => Utilities.TrySet(ref property, nameof(property), () => { throw new COMException("Not registered", NativeMethods.REGDB_E_CLASSNOTREG); })); + + Assert.Equal(NativeMethods.REGDB_E_CLASSNOTREG, ex.ErrorCode); + } + + [Fact] + public void TrySet() + { + string property = null; + Utilities.TrySet(ref property, nameof(property), () => { return "test"; }); + + Assert.Equal("test", property); + } + + [Fact] + public void TrySet_Error_Callback() + { + string property = null; + var called = false; + + Utilities.TrySet( + ref property, + nameof(property), + () => { throw new COMException("Not found", NativeMethods.E_NOTFOUND); }, + propertyName => called = true); + + Assert.Null(property); + Assert.True(called); + } + + [Fact] + public void TrySetCollection_PropertyName_Null_Throws() + { + IList property = null; + Assert.Throws("propertyName", () => Utilities.TrySetCollection(ref property, null, null, null)); + } + + [Fact] + public void TrySetCollection_PropertyName_Empty_Throws() + { + IList property = null; + Assert.Throws("propertyName", () => Utilities.TrySetCollection(ref property, string.Empty, null, null)); + } + + [Fact] + public void TrySetCollection_Action_Null_Throws() + { + IList property = null; + Assert.Throws("action", () => Utilities.TrySetCollection(ref property, nameof(property), null, null)); + } + + [Fact] + public void TrySetCollection_Creator_Null_Throws() + { + IList property = null; + var references = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + Assert.Throws("creator", () => Utilities.TrySetCollection(ref property, nameof(property), () => references, null)); + } + + [Fact] + public void TrySetCollection_Empty() + { + IList property = null; + var references = new ISetupPackageReference[0]; + Func creator = reference => PackageReferenceFactory.Create(reference); + + var packages = Utilities.TrySetCollection(ref property, nameof(property), () => references, creator); + + Assert.Empty(packages); + } + + [Fact] + public void TrySetCollection() + { + IList property = null; + var references = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + Func creator = reference => PackageReferenceFactory.Create(reference); + + var packages = Utilities.TrySetCollection(ref property, nameof(property), () => references, creator); + + Assert.Collection( + packages, + x => Assert.Equal("a", x.Id), + x => Assert.Equal("b", x.Id)); + } + + [Fact] + public void TrySetCollection_Error_Callback() + { + IList property = null; + var called = false; + var references = new[] + { + Mock.Of(x => x.GetId() == "a"), + Mock.Of(x => x.GetId() == "b"), + }; + + Func creator = reference => PackageReferenceFactory.Create(reference); + + var packages = Utilities.TrySetCollection( + ref property, + nameof(property), + () => { throw new COMException("Not found", NativeMethods.E_NOTFOUND); }, + creator, + propertyName => called = true); + + Assert.Null(property); + Assert.Empty(packages); + Assert.True(called); + } + } +} diff --git a/test/VSSetup.PowerShell.Test/VSSetup.PowerShell.Test.csproj b/test/VSSetup.PowerShell.Test/VSSetup.PowerShell.Test.csproj index 9a66002..373a382 100644 --- a/test/VSSetup.PowerShell.Test/VSSetup.PowerShell.Test.csproj +++ b/test/VSSetup.PowerShell.Test/VSSetup.PowerShell.Test.csproj @@ -46,7 +46,7 @@ True - ..\..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.5.125-rc\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll + ..\..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.8.24\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll False @@ -78,13 +78,17 @@ + + + + diff --git a/test/VSSetup.PowerShell.Test/packages.config b/test/VSSetup.PowerShell.Test/packages.config index ea6a833..432561f 100644 --- a/test/VSSetup.PowerShell.Test/packages.config +++ b/test/VSSetup.PowerShell.Test/packages.config @@ -2,7 +2,7 @@ - + diff --git a/version.json b/version.json index 407e66d..e7faea7 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.0.0-rc", + "version": "1.0.0", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/tags/v\\d\\.\\d"