commit 11b19469932e7511c4e1b1d30945804276d5fc4b
parent b95c17a80c1162fdddcad75189d548685d2ffbec
Author: JohnMcPMS <johnmcp@microsoft.com>
Date: Thu, 30 Nov 2023 14:45:35 -0800
Add a script to bootstrap running Pester tests (#3899)
Adds a script that can bootstrap from nothing to run the Pester tests.
Also updates the existing test scripts to enable this with the goal of
not changing their current behavior. Publishes the test scripts so that
one can download the artifacts and run tests against whatever set of
modules and client is interesting.
Diffstat:
9 files changed, 256 insertions(+), 14 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
@@ -29,6 +29,7 @@ ashpatil
Ashwini
ASwitch
ASYNCRTIMP
+ata
Atest
ATL
AUrl
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
@@ -278,6 +278,17 @@ jobs:
condition: succeededOrFailed()
- task: CopyFiles@2
+ displayName: 'Copy test scripts to artifacts'
+ inputs:
+ Contents: |
+ $(Build.SourcesDirectory)\src\PowerShell\scripts\Execute-WinGetTests.ps1
+ $(Build.SourcesDirectory)\src\PowerShell\tests\**
+ $(Build.SourcesDirectory)\src\LocalhostWebServer\Run-LocalhostWebServer.ps1
+ TargetFolder: '$(artifactsDir)\E2ETests\Scripts'
+ flattenFolders: true
+ condition: succeededOrFailed()
+
+ - task: CopyFiles@2
displayName: 'Copy Files: WinGetUtilInterop.UnitTests'
inputs:
SourceFolder: '$(Build.SourcesDirectory)\src\WinGetUtilInterop.UnitTests\bin\$(BuildConfiguration)\net6.0'
diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Init-TestRepository.ps1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Init-TestRepository.ps1
@@ -57,7 +57,7 @@ Write-Progress -Activity $Local:progressActivity
$Local:allItems | ForEach-Object -Process {
$Local:modulePath = $_.FullName
Write-Verbose "Publishing $Local:modulePath"
- Publish-Module -Path $Local:modulePath -Repository $RepositoryName
+ Publish-Module -Path $Local:modulePath -Repository $RepositoryName -Force
$Local:modulesPublished += 1
Write-Progress -Activity $Local:progressActivity -PercentComplete (($Local:modulesPublished * 100) / $Local:allItems.Count)
}
diff --git a/src/LocalhostWebServer/Program.cs b/src/LocalhostWebServer/Program.cs
@@ -16,7 +16,11 @@ namespace LocalhostWebServer
using Microsoft.WinGetSourceCreator;
public class Program
- {
+ {
+ const string CertificateProviderString = "Microsoft.PowerShell.Security\\Certificate::";
+ const string StoreLocationCurrentUser = "CurrentUser";
+ const string StoreLocationLocalMachine = "LocalMachine";
+
static void Main(string[] args)
{
IConfiguration config = new ConfigurationBuilder()
@@ -40,6 +44,53 @@ namespace LocalhostWebServer
Directory.CreateDirectory(Startup.StaticFileRoot);
+ if (Startup.CertPath.StartsWith(CertificateProviderString))
+ {
+ string certPath = Startup.CertPath.Substring(CertificateProviderString.Length);
+ string[] pathParts = certPath.Split('\\');
+
+ if (pathParts.Length != 3)
+ {
+ throw new InvalidDataException($"Don't know how to handle: {Startup.CertPath}");
+ }
+
+ StoreLocation storeLocation = StoreLocation.CurrentUser;
+ if (pathParts[0] == StoreLocationCurrentUser)
+ {
+ // The default
+ }
+ else if (pathParts[0] == StoreLocationLocalMachine)
+ {
+ storeLocation = StoreLocation.LocalMachine;
+ }
+ else
+ {
+ throw new InvalidDataException($"Unknown store scope: {Startup.CertPath}");
+ }
+
+ X509Store x509Store = new X509Store(pathParts[1], storeLocation);
+ x509Store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
+ X509Certificate2Collection collection = x509Store.Certificates;
+
+ if (collection.Count == 0)
+ {
+ throw new InvalidDataException($"Found {collection.Count} certificates in store '{pathParts[0]}' [{storeLocation}] \\ '{pathParts[1]}': {Startup.CertPath}");
+ }
+
+ X509Certificate2Collection results = collection.Find(X509FindType.FindByThumbprint, pathParts[2], true);
+
+ if (results.Count != 1)
+ {
+ throw new InvalidDataException($"Found {results.Count} matches for '{pathParts[2]}': {Startup.CertPath}");
+ }
+
+ ServerCertificate = results[0];
+ }
+ else
+ {
+ ServerCertificate = new X509Certificate2(Startup.CertPath, Startup.CertPassword);
+ }
+
if (!string.IsNullOrEmpty(Startup.OutCertFile))
{
string parent = Path.GetDirectoryName(Startup.OutCertFile);
@@ -48,8 +99,7 @@ namespace LocalhostWebServer
Directory.CreateDirectory(parent);
}
- X509Certificate2 serverCertificate = new X509Certificate2(Startup.CertPath, Startup.CertPassword, X509KeyStorageFlags.EphemeralKeySet);
- File.WriteAllBytes(Startup.OutCertFile, serverCertificate.Export(X509ContentType.Cert));
+ File.WriteAllBytes(Startup.OutCertFile, ServerCertificate.Export(X509ContentType.Cert));
}
if (!string.IsNullOrEmpty(Startup.LocalSourceJson))
@@ -73,11 +123,13 @@ namespace LocalhostWebServer
{
opt.ListenAnyIP(Startup.Port, listOpt =>
{
- listOpt.UseHttps(Startup.CertPath, Startup.CertPassword);
+ listOpt.UseHttps(ServerCertificate);
});
});
webBuilder.UseContentRoot(Startup.StaticFileRoot);
webBuilder.UseStartup<Startup>();
});
+
+ public static X509Certificate2 ServerCertificate { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 b/src/LocalhostWebServer/Run-LocalhostWebServer.ps1
@@ -27,7 +27,7 @@ param(
[Parameter(Mandatory=$true)]
[string]$CertPath,
- [Parameter(Mandatory=$true)]
+ [Parameter()]
[string]$CertPassword,
[Parameter()]
@@ -46,6 +46,8 @@ if (-not [System.String]::IsNullOrEmpty($sourceCert))
& certutil.exe -addstore -f "TRUSTEDPEOPLE" $sourceCert
}
-cd $BuildRoot
+Push-Location $BuildRoot
Start-Process -FilePath "LocalhostWebServer.exe" -ArgumentList "StaticFileRoot=$StaticFileRoot CertPath=$CertPath CertPassword=$CertPassword OutCertFile=$OutCertFile LocalSourceJson=$LocalSourceJson"
+
+Pop-Location+
\ No newline at end of file
diff --git a/src/PowerShell/scripts/Execute-WinGetTests.ps1 b/src/PowerShell/scripts/Execute-WinGetTests.ps1
@@ -0,0 +1,128 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+[CmdletBinding()]
+param(
+ # The version of the client PS module to use.
+ [string]$ClientModuleVersion,
+
+ # The version of the configuration PS module to use.
+ [string]$ConfigurationModuleVersion,
+
+ # The version of the client to use. Use 'existing' to skip updating.
+ [string]$ClientVersion,
+
+ # The version of Powershell to use. Use 'existing' to skip updating.
+ [string]$PwshVersion,
+
+ # The path to the binaries to run the web server.
+ [string]$LocalhostWebServerPath = (Join-Path $PSScriptRoot "..\LocalhostWebServer"),
+
+ # The path to the files to be hosted by the web server.
+ [string]$HostedFilePath = (Join-Path $PSScriptRoot "..\TestLocalIndex"),
+
+ # The path to the certificate that signed the source.msix package.
+ [string]$SourceCertPath = (Join-Path $PSScriptRoot "..\TestData\AppInstallerTest.cer"),
+
+ # The path to the configuration test data.
+ [string]$ConfigurationTestDataPath = (Join-Path $PSScriptRoot "..\TestData\Configuration"),
+
+ # The path to pwsh.exe
+ [string]$PwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe',
+
+ # Switches to prevent installing various runtimes required for the tests
+ [switch]$SkipVCRuntime,
+ [switch]$SkipDotNetRuntime,
+ [switch]$SkipAspNetRuntime,
+
+ # The path to write results to.
+ [string]$ResultsPath = (Join-Path ([System.IO.Path]::GetTempPath()) (New-Guid))
+)
+
+# Ensure we can connect to PS Gallery
+Install-PackageProvider -Name NuGet -Force | Out-Null
+
+# Get the client module
+if ([System.String]::IsNullOrEmpty($ClientModuleVersion))
+{
+ Install-Module Microsoft.WinGet.Client -Force
+}
+else
+{
+ Install-Module Microsoft.WinGet.Client -RequiredVersion $ClientModuleVersion -Force
+}
+
+# Get the client
+if ([System.String]::IsNullOrEmpty($ClientVersion))
+{
+ Repair-WingetPackageManager -Latest
+}
+elseif ($ClientVersion -eq "existing")
+{
+ # Use version already present
+}
+else
+{
+ Repair-WingetPackageManager -Version $ClientVersion
+}
+
+# Get pwsh
+if ([System.String]::IsNullOrEmpty($PwshVersion))
+{
+ winget install Microsoft.PowerShell -s winget
+}
+elseif ($PwshVersion -eq "existing")
+{
+ # Use version already present
+}
+else
+{
+ winget install Microsoft.PowerShell -s winget -v $PwshVersion
+}
+
+# Get VC Runtime
+if (-not $SkipVCRuntime)
+{
+ winget install 'Microsoft.VCRedist.2015+.x64' -s winget
+}
+
+# Install .NET 6 for the local web server
+if (-not $SkipDotNetRuntime)
+{
+ winget install Microsoft.DotNet.Runtime.6 -s winget
+}
+
+if (-not $SkipAspNetRuntime)
+{
+ winget install Microsoft.DotNet.AspNetCore.6 -s winget
+}
+
+# Generate a new TLS certificate and trust it
+$TLSCertificate = New-SelfSignedCertificate -CertStoreLocation "Cert:\LocalMachine\My" -DnsName "localhost"
+$CertTempPath = Join-Path $env:TEMP New-Guid
+Export-Certificate -Cert $TLSCertificate -FilePath $CertTempPath
+Import-Certificate -FilePath $CertTempPath -CertStoreLocation "Cert:\LocalMachine\Root"
+
+# Start local host web server
+.\Run-LocalhostWebServer.ps1 -BuildRoot $LocalhostWebServerPath -StaticFileRoot $HostedFilePath -CertPath $TLSCertificate.PSPath -SourceCert $SourceCertPath
+
+# Get the configuration module using pwsh since AllowPrerelease doesn't working in Windows PowerShell baseline
+if ([System.String]::IsNullOrEmpty($ConfigurationModuleVersion))
+{
+ & $PwshPath -Command "Install-Module Microsoft.WinGet.Configuration -Force -AllowPrerelease"
+}
+else
+{
+ & $PwshPath -Command "Install-Module Microsoft.WinGet.Configuration -RequiredVersion $ConfigurationModuleVersion -Force -AllowPrerelease"
+}
+
+# Create local PS repo
+$InitRepositoryPath = Join-Path $ConfigurationTestDataPath Init-TestRepository.ps1
+& $PwshPath -ExecutionPolicy Unrestricted -Command "$InitRepositoryPath -Force"
+
+# Run tests
+& $PwshPath -ExecutionPolicy Unrestricted -Command ".\RunTests.ps1 -TargetProduction -ConfigurationTestDataPath $ConfigurationTestDataPath -outputPath $ResultsPath"
+
+# Terminate the local web server
+Get-Process LocalhostWebServer -ErrorAction Ignore | Stop-Process -ErrorAction Ignore
+
+Write-Host "Results: $ResultsPath"
diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1
@@ -7,9 +7,23 @@
The tests require the localhost web server to be running and serving the test data.
'Invoke-Pester' should be called in an admin PowerShell window.
#>
+[CmdletBinding()]
+param(
+ # Whether to use production or developement targets.
+ [switch]$TargetProduction
+)
BeforeAll {
- $settingsFilePath = (ConvertFrom-Json (wingetdev.exe settings export)).userSettingsFile
+ if ($TargetProduction)
+ {
+ $wingetExeName = "winget.exe"
+ }
+ else
+ {
+ $wingetExeName = "wingetdev.exe"
+ }
+
+ $settingsFilePath = (ConvertFrom-Json (& $wingetExeName settings export)).userSettingsFile
$deviceGroupPolicyRoot = "HKLM:\Software\Policies\Microsoft\Windows"
$wingetPolicyKeyName = "AppInstaller"
@@ -42,7 +56,7 @@ BeforeAll {
# Source Remove requires admin privileges, this will only execute successfully in an elevated PowerShell.
# This is a workaround to an issue where the server takes longer than expected to terminate when
# running from PowerShell. This can cause other E2E tests to fail when attempting to reset the test source.
- Start-Process -FilePath "wingetdev" -ArgumentList "source remove TestSource"
+ Start-Process -FilePath $wingetExeName -ArgumentList "source remove TestSource"
}
}
}
diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1
@@ -7,6 +7,11 @@
'Invoke-Pester' should be called in an admin PowerShell window.
Requires local test repo to be setup.
#>
+[CmdletBinding()]
+param(
+ # The location of the test data
+ [string]$ConfigurationTestDataPath
+)
BeforeAll {
$env:POWERSHELL_TELEMETRY_OPTOUT = "true"
@@ -47,9 +52,14 @@ BeforeAll {
}
}
+ if ([System.String]::IsNullOrEmpty($ConfigurationTestDataPath))
+ {
+ $ConfigurationTestDataPath = (Join-Path $PSScriptRoot "..\..\AppInstallerCLIE2ETests\TestData\Configuration\")
+ }
+
function GetConfigTestDataPath()
{
- return Join-Path $PSScriptRoot "..\..\AppInstallerCLIE2ETests\TestData\Configuration\"
+ return $ConfigurationTestDataPath
}
function DeleteConfigTxtFiles()
diff --git a/src/PowerShell/tests/RunTests.ps1 b/src/PowerShell/tests/RunTests.ps1
@@ -4,7 +4,9 @@
param(
[string]$testModulesPath,
[string]$outputPath,
- [string]$packageLayoutPath
+ [string]$packageLayoutPath,
+ [switch]$TargetProduction,
+ [string]$ConfigurationTestDataPath
)
# This updates pester not always necessary but worth noting
@@ -34,17 +36,38 @@ if (-not [System.String]::IsNullOrEmpty($packageLayoutPath))
Add-AppxPackage -Register $local:packageManifestPath
# Configure crash dump and log file settings
- $local:settingsExport = ConvertFrom-Json (wingetdev.exe settings export)
+ if ($TargetProduction)
+ {
+ $local:wingetExeName = "winget.exe"
+ }
+ else
+ {
+ $local:wingetExeName = "wingetdev.exe"
+ }
+
+ $local:settingsExport = ConvertFrom-Json (& $local:wingetExeName settings export)
$local:settingsFilePath = $local:settingsExport.userSettingsFile
$local:settingsFileContent = ConvertTo-Json @{ debugging= @{ enableSelfInitiatedMinidump=$true ; keepAllLogFiles=$true } }
Set-Content -Path $local:settingsFilePath -Value $local:settingsFileContent
}
-Invoke-Pester -Script $PSScriptRoot\Microsoft.WinGet.Client.Tests.ps1 -OutputFile $outputPath\Tests-WinGetClient.XML -OutputFormat NUnitXML
+$clientConfig = New-PesterConfiguration
+$clientConfig.TestResult.OutputFormat = "NUnitXML"
+$clientConfig.TestResult.OutputPath = "$outputPath\Tests-WinGetClient.XML"
+$clientConfig.TestResult.Enabled = $true
+$clientConfig.Run.Container = New-PesterContainer -Path "$PSScriptRoot\Microsoft.WinGet.Client.Tests.ps1" -Data @{ TargetProduction = $TargetProduction }
+
+Invoke-Pester -Configuration $clientConfig
if ($PSEdition -eq "Core")
{
- Invoke-Pester -Script $PSScriptRoot\Microsoft.WinGet.Configuration.Tests.ps1 -OutputFile $outputPath\Tests-WinGetConfiguration.XML -OutputFormat NUnitXML
+ $configConfig = New-PesterConfiguration
+ $configConfig.TestResult.OutputFormat = "NUnitXML"
+ $configConfig.TestResult.OutputPath = "$outputPath\Tests-WinGetConfiguration.XML"
+ $configConfig.TestResult.Enabled = $true
+ $configConfig.Run.Container = New-PesterContainer -Path "$PSScriptRoot\Microsoft.WinGet.Configuration.Tests.ps1" -Data @{ ConfigurationTestDataPath = $ConfigurationTestDataPath }
+
+ Invoke-Pester -Configuration $configConfig
}