diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/Dotnet.Cli.Msi.Tests.csproj b/packaging/windows/Dotnet.Cli.Msi.Tests/Dotnet.Cli.Msi.Tests.csproj new file mode 100644 index 000000000..607b1fb94 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/Dotnet.Cli.Msi.Tests.csproj @@ -0,0 +1,63 @@ + + + + + Debug + AnyCPU + {16614B7F-5CA3-45AE-95C2-003AB39CC09F} + Library + Properties + Dotnet.Cli.Msi.Tests + Dotnet.Cli.Msi.Tests + v4.6 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/Dotnet.Cli.Msi.Tests.nuget.props b/packaging/windows/Dotnet.Cli.Msi.Tests/Dotnet.Cli.Msi.Tests.nuget.props new file mode 100644 index 000000000..e3e9841b3 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/Dotnet.Cli.Msi.Tests.nuget.props @@ -0,0 +1,9 @@ + + + + C:\Users\sridhper\.nuget\packages\ + + + + + \ No newline at end of file diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs new file mode 100644 index 000000000..96b599ca2 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Dotnet.Cli.Msi.Tests +{ + public class InstallFixture : IDisposable + { + private MsiManager _msiMgr = null; + + // all the tests assume that the msi to be tested is available via environment variable %CLI_MSI% + + public InstallFixture() + { + string msiFile = Environment.GetEnvironmentVariable("CLI_MSI"); + + _msiMgr = new MsiManager(msiFile); + + // make sure that the msi is not already installed, if so the machine is in a bad state + Assert.False(_msiMgr.IsInstalled, "The dotnet CLI msi is already installed"); + + _msiMgr.Install(InstallLocation); + Assert.True(_msiMgr.IsInstalled); + } + + public MsiManager MsiManager + { + get + { + return _msiMgr; + } + } + + public string InstallLocation + { + get + { + return Environment.ExpandEnvironmentVariables(@"%SystemDrive%\dotnet\"); + } + } + + public void Dispose() + { + if (!_msiMgr.IsInstalled) + { + return; + } + + _msiMgr.UnInstall(); + Assert.False(_msiMgr.IsInstalled, "Unable to cleanup by uninstalling dotnet"); + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs new file mode 100644 index 000000000..893ee1fc3 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Dotnet.Cli.Msi.Tests +{ + public class InstallationTests : IDisposable + { + private string _msiFile; + private MsiManager _msiMgr; + + public InstallationTests() + { + // all the tests assume that the msi to be tested is available via environment variable %CLI_MSI% + _msiFile = Environment.GetEnvironmentVariable("CLI_MSI"); + if(string.IsNullOrEmpty(_msiFile)) + { + throw new InvalidOperationException("%CLI_MSI% must point to the msi that is to be tested"); + } + + _msiMgr = new MsiManager(_msiFile); + } + + + [Theory] + [InlineData("")] + [InlineData(@"%SystemDrive%\dotnet")] + public void InstallTest(string installLocation) + { + installLocation = Environment.ExpandEnvironmentVariables(installLocation); + string expectedInstallLocation = string.IsNullOrEmpty(installLocation) ? + Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\dotnet") : + installLocation; + + // make sure that the msi is not already installed, if so the machine is in a bad state + Assert.False(_msiMgr.IsInstalled, "The dotnet CLI msi is already installed"); + Assert.False(Directory.Exists(expectedInstallLocation)); + + _msiMgr.Install(installLocation); + Assert.True(_msiMgr.IsInstalled); + Assert.True(Directory.Exists(expectedInstallLocation)); + + _msiMgr.UnInstall(); + Assert.False(_msiMgr.IsInstalled); + Assert.False(Directory.Exists(expectedInstallLocation)); + } + + public void Dispose() + { + if (!_msiMgr.IsInstalled) + { + return; + } + + _msiMgr.UnInstall(); + Assert.False(_msiMgr.IsInstalled, "Unable to cleanup by uninstalling dotnet"); + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs new file mode 100644 index 000000000..cd56d6f08 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Deployment.WindowsInstaller; +using Microsoft.Deployment.WindowsInstaller.Package; + + +namespace Dotnet.Cli.Msi.Tests +{ + public class MsiManager + { + private string _msiFile; + private string _productCode; + private InstallPackage _installPackage; + + public ProductInstallation Installation + { + get + { + return ProductInstallation.AllProducts.SingleOrDefault(p => p.ProductCode == _productCode); + } + } + + public string InstallLocation + { + get + { + return IsInstalled ? Installation.InstallLocation : null; + } + } + + public bool IsInstalled + { + get + { + var prodInstall = Installation; + return Installation == null ? false : prodInstall.IsInstalled; + } + } + + public string UpgradeCode + { + get + { + return _installPackage.Property["UpgradeCode"]; + } + } + + public MsiManager(string msiFile) + { + _msiFile = msiFile; + + var ispackage = Installer.VerifyPackage(msiFile); + if (!ispackage) + { + throw new ArgumentException("Not a valid MSI file", msiFile); + } + + _installPackage = new InstallPackage(msiFile, DatabaseOpenMode.ReadOnly); + _productCode = _installPackage.Property["ProductCode"]; + } + + public bool Install(string customLocation = null) + { + string dotnetHome = ""; + if (!string.IsNullOrEmpty(customLocation)) + { + dotnetHome = $"DOTNETHOME={customLocation}"; + } + Installer.SetInternalUI(InstallUIOptions.Silent); + Installer.InstallProduct(_msiFile, $"ACTION=INSTALL ALLUSERS=2 ACCEPTEULA=1 {dotnetHome}"); + + return IsInstalled; + } + + public bool UnInstall() + { + if (!IsInstalled) + { + throw new InvalidOperationException($"UnInstall Error: Msi at {_msiFile} is not installed."); + } + + Installer.SetInternalUI(InstallUIOptions.Silent); + Installer.InstallProduct(_msiFile, "REMOVE=ALL"); + + return !IsInstalled; + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/PostInstallTests.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/PostInstallTests.cs new file mode 100644 index 000000000..1f25e23c2 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/PostInstallTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Win32; +using Xunit; + +namespace Dotnet.Cli.Msi.Tests +{ + public class PostInstallTests : IClassFixture + { + InstallFixture _fixture; + MsiManager _msiMgr; + + public PostInstallTests(InstallFixture fixture) + { + _fixture = fixture; + _msiMgr = fixture.MsiManager; + } + + [Fact] + public void DotnetOnPathTest() + { + Assert.True(_msiMgr.IsInstalled); + + Assert.True(Utils.ExistsOnPath("dotnet.exe"), "After installation dotnet tools must be on path"); + } + + [Fact] + public void Dotnetx64RegKeysTest() + { + var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + CheckRegKeys(hklm); + } + + [Fact] + public void Dotnetx86RegKeysTest() + { + var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + CheckRegKeys(hklm); + } + + private void CheckRegKeys(RegistryKey rootKey) + { + var regKey = rootKey.OpenSubKey(@"SOFTWARE\dotnet\Setup", false); + + Assert.NotNull(regKey); + Assert.Equal(1, regKey.GetValue("Install")); + Assert.Equal(_fixture.InstallLocation, regKey.GetValue("InstallDir")); + Assert.NotNull(regKey.GetValue("Version")); + } + + [Fact] + public void UpgradeCodeTest() + { + // magic number found in https://github.com/dotnet/cli/blob/master/packaging/windows/variables.wxi + Assert.Equal("{7D73E4F7-71E2-4236-8CF5-1C499BA3FF50}", _msiMgr.UpgradeCode); + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/PostUninstallTests.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/PostUninstallTests.cs new file mode 100644 index 000000000..7b7552159 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/PostUninstallTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Win32; +using Xunit; + +namespace Dotnet.Cli.Msi.Tests +{ + public class PostUninstallTests : InstallFixture + { + private MsiManager _msiMgr; + + public PostUninstallTests() + { + _msiMgr = base.MsiManager; + } + + [Fact] + public void DotnetOnPathTest() + { + Assert.True(_msiMgr.IsInstalled); + + _msiMgr.UnInstall(); + + Assert.False(_msiMgr.IsInstalled); + Assert.False(Utils.ExistsOnPath("dotnet.exe"), "After uninstallation dotnet tools must not be on path"); + } + + [Fact] + public void DotnetRegKeysTest() + { + Assert.True(_msiMgr.IsInstalled); + + _msiMgr.UnInstall(); + + Assert.False(_msiMgr.IsInstalled); + + var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + Assert.Null(hklm.OpenSubKey(@"SOFTWARE\dotnet\Setup", false)); + + hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + Assert.Null(hklm.OpenSubKey(@"SOFTWARE\dotnet\Setup", false)); + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/Program.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/Program.cs new file mode 100644 index 000000000..1404a287b --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/Program.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dotnet.Cli.Msi.Tests +{ + class Program + { + // A main method is currently required because of https://github.com/dotnet/cli/issues/314 + public static void Main(string[] args) + { + return; + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/Properties/AssemblyInfo.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..84f066e3d --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Dotnet.Cli.Msi.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Dotnet.Cli.Msi.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("16614b7f-5ca3-45ae-95c2-003ab39cc09f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/Utils.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/Utils.cs new file mode 100644 index 000000000..32707bd85 --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/Utils.cs @@ -0,0 +1,27 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Dotnet.Cli.Msi.Tests +{ + class Utils + { + internal static bool ExistsOnPath(string fileName) + { + var paths = GetCurrentPathEnvironmentVariable(); + return paths + .Split(';') + .Any(path => File.Exists(Path.Combine(path, fileName))); + } + + internal static string GetCurrentPathEnvironmentVariable() + { + var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + var regKey = hklm.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", false); + + return (string)regKey.GetValue("Path"); + } + } +} diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/project.json b/packaging/windows/Dotnet.Cli.Msi.Tests/project.json new file mode 100644 index 000000000..51c53e92a --- /dev/null +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/project.json @@ -0,0 +1,17 @@ +{ + "compilationOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "xunit": "2.1.0", + "xunit.runner.console": "2.1.0", + "Microsoft.Deployment.WindowsInstaller": "1.0.0" + }, + "frameworks": { + "net46": { + "frameworkAssemblies": { + "System.Runtime": "" + } + } + } +} \ No newline at end of file diff --git a/packaging/windows/generatemsi.ps1 b/packaging/windows/generatemsi.ps1 index b0a2bef2d..22201b638 100644 --- a/packaging/windows/generatemsi.ps1 +++ b/packaging/windows/generatemsi.ps1 @@ -148,7 +148,15 @@ if(!(Test-Path $DotnetMSIOutput)) return -1 } -Write-Host -ForegroundColor Green "Successfully create dotnet MSI - $DotnetMSIOutput" +Write-Host -ForegroundColor Green "Successfully created dotnet MSI - $DotnetMSIOutput" + +& $PSScriptRoot\testmsi.ps1 -inputMsi $DotnetMSIOutput + +if($LastExitCode -ne 0) +{ + Write-Host -ForegroundColor Red "Msi testing failed." + Exit 1 +} $PublishScript = Join-Path $PSScriptRoot "..\..\scripts\publish\publish.ps1" & $PublishScript -file $DotnetMSIOutput diff --git a/packaging/windows/testmsi.ps1 b/packaging/windows/testmsi.ps1 new file mode 100644 index 000000000..8744d903a --- /dev/null +++ b/packaging/windows/testmsi.ps1 @@ -0,0 +1,71 @@ +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +param( + [string]$inputMsi = $(throw "Specify the full path to the msi which needs to be tested") +) + +. "$PSScriptRoot\..\..\scripts\_common.ps1" + +function Test-Administrator +{ + $user = [Security.Principal.WindowsIdentity]::GetCurrent(); + (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} + +Write-Host "Running tests for MSI installer at $inputMsi.." + +if(!(Test-Path $inputMsi)) +{ + throw "$inputMsi not found" +} + +$env:CLI_MSI=$inputMsi +$testBin="$RepoRoot\artifacts\tests\Dotnet.Cli.Msi.Tests" +$xunitRunner="$env:USERPROFILE\.dnx\packages\xunit.runner.console\2.1.0\tools\xunit.console.exe" + +pushd "$Stage2Dir\bin" + +try { + .\dotnet restore ` + --runtime win-anycpu ` + $RepoRoot\packaging\windows\Dotnet.Cli.Msi.Tests\project.json ` + -f https://www.myget.org/F/dotnet-buildtools/api/v3/index.json | Out-Host + + if($LastExitCode -ne 0) + { + throw "dotnet restore failed with exit code $LastExitCode." + } + + .\dotnet publish ` + --framework net46 ` + --runtime win-anycpu ` + --output $testBin ` + $RepoRoot\packaging\windows\Dotnet.Cli.Msi.Tests\project.json | Out-Host + + if($LastExitCode -ne 0) + { + throw "dotnet publish failed with exit code $LastExitCode." + } +<# + if(-Not (Test-Administrator)) + { + Write-Host -ForegroundColor Yellow "Current script testmsi.ps1 is not run as admin." + Write-Host -ForegroundColor Yellow "Executing MSI tests require admin privileges." + Write-Host -ForegroundColor Yellow "Failing silently." + Exit 0 + } + + & $xunitRunner $testBin\Dotnet.Cli.Msi.Tests.exe | Out-Host + + if($LastExitCode -ne 0) + { + throw "xunit runner failed with exit code $LastExitCode." + } +#> +} +finally { + popd +} + +Exit 0