1156 lines
49 KiB
PowerShell
1156 lines
49 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
$runningOnWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
|
|
[System.Runtime.InteropServices.OSPlatform]::Windows
|
|
)
|
|
|
|
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
|
$srcRoot = Join-Path $repoRoot 'src'
|
|
$configuration = if ($env:CONFIGURATION) { $env:CONFIGURATION } else { 'Release' }
|
|
$defaultManifest = if ($runningOnWindows) {
|
|
Join-Path $PSScriptRoot 'cli-e2e.manifest.windows.json'
|
|
}
|
|
else {
|
|
Join-Path $PSScriptRoot 'cli-e2e.manifest.linux.json'
|
|
}
|
|
$manifestPath = if ($env:UNIGETUI_CLI_E2E_MANIFEST) {
|
|
$env:UNIGETUI_CLI_E2E_MANIFEST
|
|
}
|
|
else {
|
|
$defaultManifest
|
|
}
|
|
|
|
if (-not (Test-Path $manifestPath)) {
|
|
throw "CLI E2E manifest not found at $manifestPath"
|
|
}
|
|
|
|
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json -AsHashtable
|
|
if ($null -eq $manifest) {
|
|
throw "Could not parse CLI E2E manifest at $manifestPath"
|
|
}
|
|
|
|
$artifactRoot = if ($env:UNIGETUI_CLI_E2E_ARTIFACTS) {
|
|
$env:UNIGETUI_CLI_E2E_ARTIFACTS
|
|
}
|
|
else {
|
|
Join-Path ([System.IO.Path]::GetTempPath()) ("unigetui-cli-e2e-" + [Guid]::NewGuid().ToString('N'))
|
|
}
|
|
if (Test-Path $artifactRoot) {
|
|
Remove-Item -Recurse -Force $artifactRoot
|
|
}
|
|
New-Item -ItemType Directory -Path $artifactRoot -Force | Out-Null
|
|
$artifactRoot = (Resolve-Path $artifactRoot).Path
|
|
|
|
$daemonRoot = $artifactRoot
|
|
$downloadRoot = Join-Path $daemonRoot 'downloads'
|
|
$coveragePath = Join-Path $daemonRoot 'coverage.json'
|
|
$inventoryPath = Join-Path $daemonRoot 'environment.json'
|
|
$daemonStdOutLog = Join-Path $daemonRoot 'headless-daemon.stdout.log'
|
|
$daemonStdErrLog = Join-Path $daemonRoot 'headless-daemon.stderr.log'
|
|
$preserveArtifacts = $true
|
|
|
|
$localDataRoot = if ($runningOnWindows) {
|
|
Join-Path $daemonRoot 'AppData\Local'
|
|
}
|
|
else {
|
|
Join-Path $daemonRoot '.local\share'
|
|
}
|
|
$roamingDataRoot = if ($runningOnWindows) {
|
|
Join-Path $daemonRoot 'AppData\Roaming'
|
|
}
|
|
else {
|
|
$null
|
|
}
|
|
$dotnetHomeRoot = Join-Path $daemonRoot '.dotnet-home'
|
|
$npmGlobalRoot = Join-Path $daemonRoot 'npm-global'
|
|
|
|
New-Item -ItemType Directory -Path $downloadRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $localDataRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $dotnetHomeRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $npmGlobalRoot -Force | Out-Null
|
|
if (-not [string]::IsNullOrWhiteSpace($roamingDataRoot)) {
|
|
New-Item -ItemType Directory -Path $roamingDataRoot -Force | Out-Null
|
|
}
|
|
|
|
$env:DOTNET_CLI_HOME = $dotnetHomeRoot
|
|
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
|
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
|
$env:UNIGETUI_GITHUB_TOKEN_NAMESPACE = "cli-e2e-$([Guid]::NewGuid().ToString('N'))"
|
|
$env:UNIGETUI_AVALONIA_DEVTOOLS = 'disabled'
|
|
$env:npm_config_prefix = $npmGlobalRoot
|
|
|
|
if (-not $runningOnWindows) {
|
|
$env:HOME = $daemonRoot
|
|
$env:USERPROFILE = $daemonRoot
|
|
$env:XDG_DATA_HOME = $localDataRoot
|
|
}
|
|
|
|
$coverage = [ordered]@{
|
|
manifest = $manifest.name
|
|
tested = @()
|
|
excluded = @($manifest.excludedCommands)
|
|
}
|
|
|
|
function Add-Tested {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $Command,
|
|
[string] $Mode = 'success'
|
|
)
|
|
|
|
$script:coverage.tested += [ordered]@{
|
|
command = $Command
|
|
mode = $Mode
|
|
}
|
|
}
|
|
|
|
function Write-Stage {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $Name
|
|
)
|
|
|
|
Write-Host "== $Name =="
|
|
}
|
|
|
|
function Find-BuiltArtifact {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $ProjectDirectory,
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $FileName
|
|
)
|
|
|
|
$outputRoot = Join-Path $ProjectDirectory 'bin'
|
|
if (-not (Test-Path $outputRoot)) {
|
|
return $null
|
|
}
|
|
|
|
return Get-ChildItem -Path $outputRoot -Recurse -File -Filter $FileName |
|
|
Sort-Object @{
|
|
Expression = { if ($_.FullName -like "*\bin\*\$configuration\*") { 0 } else { 1 } }
|
|
}, @{
|
|
Expression = { $_.FullName }
|
|
} |
|
|
Select-Object -First 1 -ExpandProperty FullName
|
|
}
|
|
|
|
function Get-ManifestManagerByRole {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $Role
|
|
)
|
|
|
|
$matches = @($manifest.packageManagers | Where-Object { @($_.roles) -contains $Role })
|
|
if ($matches.Count -eq 0) {
|
|
return $null
|
|
}
|
|
|
|
return $matches[0]
|
|
}
|
|
|
|
function Get-PackageArguments {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $Fixture,
|
|
[switch] $IncludeVersion,
|
|
[switch] $IncludeSource
|
|
)
|
|
|
|
$arguments = @(
|
|
'--manager', [string]$Fixture.manager,
|
|
'--id', [string]$Fixture.packageId
|
|
)
|
|
if ($IncludeVersion -and $Fixture.ContainsKey('installVersion')) {
|
|
$arguments += @('--version', [string]$Fixture.installVersion)
|
|
}
|
|
if ($IncludeSource -and $Fixture.ContainsKey('sourceName')) {
|
|
$arguments += @('--source', [string]$Fixture.sourceName)
|
|
}
|
|
if ($Fixture.ContainsKey('scope')) {
|
|
$arguments += @('--scope', [string]$Fixture.scope)
|
|
}
|
|
return $arguments
|
|
}
|
|
|
|
function Find-PackageMatch {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[object[]] $Packages,
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $Fixture,
|
|
[string] $Version
|
|
)
|
|
|
|
return @($Packages | Where-Object {
|
|
$_.id -eq $Fixture.packageId -and (
|
|
-not $PSBoundParameters.ContainsKey('Version') -or $_.version -eq $Version
|
|
)
|
|
})[0]
|
|
}
|
|
|
|
function Resolve-QueueOutputPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $QueueFixture
|
|
)
|
|
|
|
$safeManager = ([string]$QueueFixture.manager) -replace '[^A-Za-z0-9._-]', '_'
|
|
$safePackage = ([string]$QueueFixture.packageId) -replace '[^A-Za-z0-9._-]', '_'
|
|
$targetDirectory = Join-Path $downloadRoot "$safeManager-$safePackage"
|
|
New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null
|
|
return $targetDirectory
|
|
}
|
|
|
|
function Get-DaemonCommand {
|
|
$daemonProject = Join-Path $srcRoot ([string]$manifest.daemon.project)
|
|
if (-not (Test-Path $daemonProject)) {
|
|
throw "Daemon project not found at $daemonProject"
|
|
}
|
|
|
|
switch ([string]$manifest.daemon.kind) {
|
|
'windows-exe' {
|
|
$daemonExe = if ($env:UNIGETUI_DAEMON_EXE) {
|
|
$env:UNIGETUI_DAEMON_EXE
|
|
}
|
|
else {
|
|
Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName "$($manifest.daemon.assemblyName).exe"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($daemonExe) -or -not (Test-Path $daemonExe)) {
|
|
throw "Windows headless executable was not found. Expected $($manifest.daemon.assemblyName).exe under $(Split-Path $daemonProject -Parent)\bin\$configuration"
|
|
}
|
|
|
|
return @{
|
|
FilePath = (Resolve-Path $daemonExe).Path
|
|
WorkingDirectory = Split-Path (Resolve-Path $daemonExe).Path -Parent
|
|
}
|
|
}
|
|
'avalonia-dll' {
|
|
$daemonDll = if ($env:UNIGETUI_DAEMON_DLL) {
|
|
$env:UNIGETUI_DAEMON_DLL
|
|
}
|
|
else {
|
|
Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName "$($manifest.daemon.assemblyName).dll"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($daemonDll) -or -not (Test-Path $daemonDll)) {
|
|
throw "Avalonia headless daemon DLL was not found. Expected $($manifest.daemon.assemblyName).dll under $(Split-Path $daemonProject -Parent)\bin\$configuration"
|
|
}
|
|
|
|
$resolvedDll = (Resolve-Path $daemonDll).Path
|
|
return @{
|
|
FilePath = 'dotnet'
|
|
WorkingDirectory = Split-Path $resolvedDll -Parent
|
|
PrefixArguments = @($resolvedDll)
|
|
}
|
|
}
|
|
'avalonia-native' {
|
|
$fileName = if ($runningOnWindows) { "$($manifest.daemon.assemblyName).exe" } else { $manifest.daemon.assemblyName }
|
|
$daemonNative = if ($env:UNIGETUI_DAEMON_EXE) {
|
|
$env:UNIGETUI_DAEMON_EXE
|
|
}
|
|
else {
|
|
Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName $fileName
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($daemonNative) -or -not (Test-Path $daemonNative)) {
|
|
throw "NativeAOT headless executable was not found. Expected $fileName under $(Split-Path $daemonProject -Parent)\bin\$configuration"
|
|
}
|
|
|
|
return @{
|
|
FilePath = (Resolve-Path $daemonNative).Path
|
|
WorkingDirectory = Split-Path (Resolve-Path $daemonNative).Path -Parent
|
|
}
|
|
}
|
|
default {
|
|
throw "Unsupported daemon kind $($manifest.daemon.kind)"
|
|
}
|
|
}
|
|
}
|
|
|
|
$pipeName = "UniGetUI.CI.$([Guid]::NewGuid().ToString('N'))"
|
|
$transportArgs = @('--transport', 'named-pipe', '--pipe-name', $pipeName)
|
|
$daemonExtraArgs = @('--headless', '--ipc-api-transport', 'named-pipe', '--ipc-api-pipe-name', $pipeName)
|
|
$daemonStartupTimeoutSeconds = if ($runningOnWindows) { 300 } else { 120 }
|
|
$daemonCommand = Get-DaemonCommand
|
|
$cliCommand = $daemonCommand
|
|
$process = $null
|
|
$gracefulShutdown = $false
|
|
|
|
function Get-DaemonLog {
|
|
$stdout = if (Test-Path $daemonStdOutLog) { Get-Content $daemonStdOutLog -Raw } else { '' }
|
|
$stderr = if (Test-Path $daemonStdErrLog) { Get-Content $daemonStdErrLog -Raw } else { '' }
|
|
return ($stdout, $stderr -join [Environment]::NewLine).Trim()
|
|
}
|
|
|
|
function Stop-Daemon {
|
|
if ($null -ne $script:process -and -not $script:process.HasExited) {
|
|
Stop-Process -Id $script:process.Id
|
|
}
|
|
}
|
|
|
|
function Invoke-CliRaw {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]] $Arguments
|
|
)
|
|
|
|
$commandArguments = @()
|
|
if ($cliCommand.ContainsKey('PrefixArguments')) {
|
|
$commandArguments += $cliCommand.PrefixArguments
|
|
}
|
|
$commandArguments += $transportArgs + $Arguments
|
|
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
|
$startInfo.FileName = $cliCommand.FilePath
|
|
$startInfo.WorkingDirectory = $cliCommand.WorkingDirectory
|
|
$startInfo.UseShellExecute = $false
|
|
$startInfo.RedirectStandardOutput = $true
|
|
$startInfo.RedirectStandardError = $true
|
|
foreach ($argument in $commandArguments) {
|
|
[void]$startInfo.ArgumentList.Add([string]$argument)
|
|
}
|
|
|
|
$commandProcess = [System.Diagnostics.Process]::Start($startInfo)
|
|
if ($null -eq $commandProcess) {
|
|
throw "Failed to start CLI command: $($cliCommand.FilePath) $($commandArguments -join ' ')"
|
|
}
|
|
|
|
$stdout = $commandProcess.StandardOutput.ReadToEnd()
|
|
$stderr = $commandProcess.StandardError.ReadToEnd()
|
|
$commandProcess.WaitForExit()
|
|
$text = (@($stdout, $stderr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
|
|
$_.TrimEnd()
|
|
}) -join [Environment]::NewLine
|
|
return @{
|
|
ExitCode = $commandProcess.ExitCode
|
|
Text = $text
|
|
}
|
|
}
|
|
|
|
function Invoke-CliJson {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]] $Arguments
|
|
)
|
|
|
|
$result = Invoke-CliRaw -Arguments $Arguments
|
|
if ($result.ExitCode -ne 0) {
|
|
throw "CLI command failed ($($result.ExitCode)): $($Arguments -join ' ')`n$($result.Text)"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($result.Text)) {
|
|
throw "CLI command returned empty output: $($Arguments -join ' ')"
|
|
}
|
|
return $result.Text | ConvertFrom-Json
|
|
}
|
|
|
|
function Invoke-CliFailure {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]] $Arguments
|
|
)
|
|
|
|
$result = Invoke-CliRaw -Arguments $Arguments
|
|
if ($result.ExitCode -eq 0) {
|
|
throw "CLI command unexpectedly succeeded: $($Arguments -join ' ')`n$($result.Text)"
|
|
}
|
|
return $result
|
|
}
|
|
|
|
function Wait-ForCliCondition {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]] $Arguments,
|
|
[Parameter(Mandatory = $true)]
|
|
[scriptblock] $Condition,
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $FailureMessage,
|
|
[int] $TimeoutSeconds = 120,
|
|
[int] $DelaySeconds = 2
|
|
)
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
$lastResponse = $null
|
|
$lastError = $null
|
|
|
|
do {
|
|
try {
|
|
$lastResponse = Invoke-CliJson -Arguments $Arguments
|
|
$lastError = $null
|
|
if (& $Condition $lastResponse) {
|
|
return $lastResponse
|
|
}
|
|
}
|
|
catch {
|
|
$lastError = $_.Exception.Message
|
|
}
|
|
|
|
Start-Sleep -Seconds $DelaySeconds
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
if ($null -ne $lastResponse) {
|
|
throw "$FailureMessage`nLast payload: $($lastResponse | ConvertTo-Json -Depth 10)"
|
|
}
|
|
|
|
throw "$FailureMessage`nLast error: $lastError"
|
|
}
|
|
|
|
function Wait-ForInstalledPackage {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $Fixture,
|
|
[string] $ExpectedVersion,
|
|
[int] $TimeoutSeconds = 180
|
|
)
|
|
|
|
return Wait-ForCliCondition `
|
|
-Arguments @('package', 'installed', '--manager', [string]$Fixture.manager) `
|
|
-Condition {
|
|
param($response)
|
|
@($response.packages | Where-Object {
|
|
$_.id -eq $Fixture.packageId -and (
|
|
[string]::IsNullOrWhiteSpace($ExpectedVersion) -or $_.version -eq $ExpectedVersion
|
|
)
|
|
}).Count -gt 0
|
|
} `
|
|
-FailureMessage "package installed did not report $($Fixture.packageId) for manager $($Fixture.manager)" `
|
|
-TimeoutSeconds $TimeoutSeconds `
|
|
-DelaySeconds 3
|
|
}
|
|
|
|
function Wait-ForPackageRemoval {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $Fixture,
|
|
[int] $TimeoutSeconds = 180
|
|
)
|
|
|
|
return Wait-ForCliCondition `
|
|
-Arguments @('package', 'installed', '--manager', [string]$Fixture.manager) `
|
|
-Condition {
|
|
param($response)
|
|
@($response.packages | Where-Object { $_.id -eq $Fixture.packageId }).Count -eq 0
|
|
} `
|
|
-FailureMessage "$($Fixture.packageId) still appears in package installed for manager $($Fixture.manager)" `
|
|
-TimeoutSeconds $TimeoutSeconds `
|
|
-DelaySeconds 3
|
|
}
|
|
|
|
function Wait-ForPackageUpdateVisibility {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $Fixture,
|
|
[int] $TimeoutSeconds = 120
|
|
)
|
|
|
|
return Wait-ForCliCondition `
|
|
-Arguments @('package', 'updates', '--manager', [string]$Fixture.manager) `
|
|
-Condition {
|
|
param($response)
|
|
@($response.updates | Where-Object { $_.id -eq $Fixture.packageId }).Count -gt 0
|
|
} `
|
|
-FailureMessage "package updates did not report $($Fixture.packageId) for manager $($Fixture.manager)" `
|
|
-TimeoutSeconds $TimeoutSeconds `
|
|
-DelaySeconds 3
|
|
}
|
|
|
|
function Get-LatestFixtureVersion {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable] $Fixture,
|
|
[Parameter(Mandatory = $true)]
|
|
[object] $SearchResponse,
|
|
[Parameter(Mandatory = $true)]
|
|
[object] $VersionResponse
|
|
)
|
|
|
|
$searchMatch = @($SearchResponse.packages | Where-Object { $_.id -eq $Fixture.packageId })[0]
|
|
if ($null -eq $searchMatch) {
|
|
throw "package search did not return $($Fixture.packageId) for manager $($Fixture.manager)"
|
|
}
|
|
|
|
$candidateVersions = @()
|
|
if (-not [string]::IsNullOrWhiteSpace($searchMatch.version)) {
|
|
$candidateVersions += [string]$searchMatch.version
|
|
}
|
|
$candidateVersions += @($VersionResponse.versions)
|
|
|
|
$latestVersion = @($candidateVersions | Where-Object {
|
|
-not [string]::IsNullOrWhiteSpace($_) -and $_ -ne $Fixture.installVersion
|
|
})[0]
|
|
|
|
if ([string]::IsNullOrWhiteSpace($latestVersion)) {
|
|
throw "Could not resolve a newer version for $($Fixture.packageId) on manager $($Fixture.manager)"
|
|
}
|
|
|
|
return [string]$latestVersion
|
|
}
|
|
|
|
function Assert-JsonCommandSucceeded {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[object] $Response,
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $Label
|
|
)
|
|
|
|
if ($Response.status -ne 'success') {
|
|
throw "$Label failed: $($Response | ConvertTo-Json -Depth 10)"
|
|
}
|
|
}
|
|
|
|
function Write-EnvironmentInventory {
|
|
$inventory = [ordered]@{
|
|
manifest = $manifest.name
|
|
os = if ($runningOnWindows) { 'windows' } else { 'linux' }
|
|
dotnet = (& dotnet --version)
|
|
python = ''
|
|
pip = ''
|
|
npm = ''
|
|
}
|
|
|
|
try {
|
|
$inventory.python = (& python --version 2>&1 | Out-String).Trim()
|
|
}
|
|
catch {
|
|
$inventory.python = $_.Exception.Message
|
|
}
|
|
|
|
try {
|
|
$inventory.pip = (& python -m pip --version 2>&1 | Out-String).Trim()
|
|
}
|
|
catch {
|
|
$inventory.pip = $_.Exception.Message
|
|
}
|
|
|
|
try {
|
|
$inventory.npm = (& npm --version 2>&1 | Out-String).Trim()
|
|
}
|
|
catch {
|
|
$inventory.npm = $_.Exception.Message
|
|
}
|
|
|
|
Set-Content -Path $inventoryPath -Value ($inventory | ConvertTo-Json -Depth 8) -Encoding UTF8
|
|
}
|
|
|
|
Write-EnvironmentInventory
|
|
|
|
try {
|
|
$daemonArguments = @()
|
|
if ($daemonCommand.ContainsKey('PrefixArguments')) {
|
|
$daemonArguments += $daemonCommand.PrefixArguments
|
|
}
|
|
$daemonArguments += $daemonExtraArgs
|
|
|
|
$process = Start-Process `
|
|
-FilePath $daemonCommand.FilePath `
|
|
-ArgumentList $daemonArguments `
|
|
-WorkingDirectory $daemonCommand.WorkingDirectory `
|
|
-RedirectStandardOutput $daemonStdOutLog `
|
|
-RedirectStandardError $daemonStdErrLog `
|
|
-PassThru
|
|
|
|
$status = Wait-ForCliCondition `
|
|
-Arguments @('status') `
|
|
-Condition { param($response) $response.running -and $response.transport -eq 'named-pipe' } `
|
|
-FailureMessage 'Headless daemon never became ready over named-pipe IPC.' `
|
|
-TimeoutSeconds $daemonStartupTimeoutSeconds `
|
|
-DelaySeconds 2
|
|
|
|
Write-Stage 'Status and headless transport'
|
|
if ($status.namedPipeName -ne $pipeName) {
|
|
throw "status did not report the expected named pipe name. Expected $pipeName, got $($status.namedPipeName)"
|
|
}
|
|
if (-not $runningOnWindows) {
|
|
$expectedSocketPath = "/tmp/$pipeName"
|
|
if ($status.namedPipePath -ne $expectedSocketPath) {
|
|
throw "status did not report the expected Unix socket path. Expected $expectedSocketPath, got $($status.namedPipePath)"
|
|
}
|
|
}
|
|
Add-Tested 'status'
|
|
|
|
$version = Invoke-CliJson -Arguments @('version')
|
|
if ($version.build -le 0) {
|
|
throw "version did not return a positive build number"
|
|
}
|
|
Add-Tested 'version'
|
|
|
|
$appState = Invoke-CliJson -Arguments @('app', 'status')
|
|
if (-not $appState.app.headless -or $appState.app.windowAvailable -or $appState.app.canNavigate -or -not $appState.app.canQuit) {
|
|
throw "app status did not report the expected headless state: $($appState | ConvertTo-Json -Depth 8)"
|
|
}
|
|
Add-Tested 'app status'
|
|
|
|
Invoke-CliFailure -Arguments @('app', 'show') | Out-Null
|
|
Add-Tested 'app show' 'expected-failure'
|
|
|
|
Invoke-CliFailure -Arguments @('app', 'navigate', '--page', 'settings') | Out-Null
|
|
Add-Tested 'app navigate' 'expected-failure'
|
|
|
|
$bundleFixture = Get-ManifestManagerByRole -Role 'bundle'
|
|
Invoke-CliFailure -Arguments @('package', 'show', '--id', [string]$bundleFixture.packageId, '--source', [string]$bundleFixture.sourceName) | Out-Null
|
|
Add-Tested 'package show' 'expected-failure'
|
|
|
|
if ($runningOnWindows -and $manifest.transport.verifyNoTcpListener) {
|
|
$connections = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -eq $process.Id }
|
|
if (@($connections).Count -ne 0) {
|
|
throw "Headless named-pipe session opened a TCP listener unexpectedly: $(@($connections | Select-Object -ExpandProperty LocalPort) -join ', ')"
|
|
}
|
|
}
|
|
|
|
Write-Stage 'Manager inspection'
|
|
$managers = Invoke-CliJson -Arguments @('manager', 'list')
|
|
foreach ($fixture in @($manifest.packageManagers)) {
|
|
if (@($managers.managers | Where-Object { $_.name -eq $fixture.manager }).Count -eq 0) {
|
|
throw "manager list did not report $($fixture.manager)"
|
|
}
|
|
}
|
|
Add-Tested 'manager list'
|
|
|
|
foreach ($fixture in @($manifest.packageManagers)) {
|
|
$maintenance = Invoke-CliJson -Arguments @('manager', 'maintenance', '--manager', [string]$fixture.manager)
|
|
if ($maintenance.maintenance.manager -ne $fixture.manager) {
|
|
throw "manager maintenance did not return the expected manager payload for $($fixture.manager)"
|
|
}
|
|
if (@($maintenance.maintenance.supportedActions | Where-Object { $_ -eq 'reload' }).Count -eq 0) {
|
|
throw "manager maintenance did not expose reload for $($fixture.manager)"
|
|
}
|
|
|
|
$reloadManager = Invoke-CliJson -Arguments @('manager', 'reload', '--manager', [string]$fixture.manager)
|
|
if ($reloadManager.operationStatus -ne 'completed') {
|
|
throw "manager reload did not complete successfully for $($fixture.manager): $($reloadManager | ConvertTo-Json -Depth 8)"
|
|
}
|
|
}
|
|
Add-Tested 'manager maintenance'
|
|
Add-Tested 'manager reload'
|
|
|
|
$toggleManagerFixture = Get-ManifestManagerByRole -Role 'toggle-manager'
|
|
$disableManager = Invoke-CliJson -Arguments @('manager', 'disable', '--manager', [string]$toggleManagerFixture.manager)
|
|
if ($disableManager.manager.enabled) {
|
|
throw "manager disable did not disable $($toggleManagerFixture.manager)"
|
|
}
|
|
$enableManager = Invoke-CliJson -Arguments @('manager', 'enable', '--manager', [string]$toggleManagerFixture.manager)
|
|
if (-not $enableManager.manager.enabled) {
|
|
throw "manager enable did not re-enable $($toggleManagerFixture.manager)"
|
|
}
|
|
Add-Tested 'manager enable'
|
|
Add-Tested 'manager disable'
|
|
|
|
$disableNotifications = Invoke-CliJson -Arguments @('manager', 'notifications', 'disable', '--manager', [string]$bundleFixture.manager)
|
|
if (-not $disableNotifications.manager.notificationsSuppressed) {
|
|
throw "manager notifications disable did not suppress notifications for $($bundleFixture.manager)"
|
|
}
|
|
$enableNotifications = Invoke-CliJson -Arguments @('manager', 'notifications', 'enable', '--manager', [string]$bundleFixture.manager)
|
|
if ($enableNotifications.manager.notificationsSuppressed) {
|
|
throw "manager notifications enable did not re-enable notifications for $($bundleFixture.manager)"
|
|
}
|
|
Add-Tested 'manager notifications disable'
|
|
Add-Tested 'manager notifications enable'
|
|
|
|
foreach ($fixture in @($manifest.packageManagers)) {
|
|
$sources = Invoke-CliJson -Arguments @('source', 'list', '--manager', [string]$fixture.manager)
|
|
if (@($sources.sources | Where-Object { $_.name -eq $fixture.sourceName }).Count -eq 0) {
|
|
throw "source list did not report $($fixture.sourceName) for $($fixture.manager)"
|
|
}
|
|
}
|
|
Add-Tested 'source list'
|
|
|
|
$sourceFixture = Get-ManifestManagerByRole -Role 'source'
|
|
if ($null -ne $sourceFixture) {
|
|
Write-Stage 'Source add/remove'
|
|
$sourceDirectory = Join-Path $daemonRoot 'LocalFeed'
|
|
New-Item -ItemType Directory -Path $sourceDirectory -Force | Out-Null
|
|
$sourceUri = ([System.Uri](Resolve-Path $sourceDirectory).Path).AbsoluteUri
|
|
$customSourceName = 'ci-local-feed'
|
|
$addedSource = Invoke-CliJson -Arguments @(
|
|
'source', 'add',
|
|
'--manager', [string]$sourceFixture.manager,
|
|
'--name', $customSourceName,
|
|
'--url', $sourceUri
|
|
)
|
|
Assert-JsonCommandSucceeded -Response $addedSource -Label 'source add'
|
|
$sourcesAfterAdd = Invoke-CliJson -Arguments @('source', 'list', '--manager', [string]$sourceFixture.manager)
|
|
if (@($sourcesAfterAdd.sources | Where-Object { $_.name -eq $customSourceName -and $_.url -eq $sourceUri }).Count -eq 0) {
|
|
throw "source add did not expose the configured custom source"
|
|
}
|
|
$removedSource = Invoke-CliJson -Arguments @(
|
|
'source', 'remove',
|
|
'--manager', [string]$sourceFixture.manager,
|
|
'--name', $customSourceName,
|
|
'--url', $sourceUri
|
|
)
|
|
Assert-JsonCommandSucceeded -Response $removedSource -Label 'source remove'
|
|
$sourcesAfterRemove = Invoke-CliJson -Arguments @('source', 'list', '--manager', [string]$sourceFixture.manager)
|
|
if (@($sourcesAfterRemove.sources | Where-Object { $_.name -eq $customSourceName }).Count -ne 0) {
|
|
throw "source remove did not remove the configured custom source"
|
|
}
|
|
Add-Tested 'source add'
|
|
Add-Tested 'source remove'
|
|
}
|
|
|
|
Write-Stage 'Settings and secure settings'
|
|
$settings = Invoke-CliJson -Arguments @('settings', 'list')
|
|
if (@($settings.settings | Where-Object { $_.name -eq 'FreshValue' }).Count -eq 0) {
|
|
throw "settings list did not report FreshValue"
|
|
}
|
|
Add-Tested 'settings list'
|
|
|
|
$setFreshValue = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'FreshValue', '--value', 'cli-smoke')
|
|
if ($setFreshValue.setting.stringValue -ne 'cli-smoke') {
|
|
throw "settings set did not persist FreshValue"
|
|
}
|
|
$getFreshValue = Invoke-CliJson -Arguments @('settings', 'get', '--key', 'FreshValue')
|
|
if ($getFreshValue.setting.stringValue -ne 'cli-smoke') {
|
|
throw "settings get did not return FreshValue"
|
|
}
|
|
Add-Tested 'settings set'
|
|
Add-Tested 'settings get'
|
|
|
|
$setFreshBool = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'FreshBoolSetting', '--enabled', 'true')
|
|
if (-not $setFreshBool.setting.boolValue) {
|
|
throw "settings set did not enable FreshBoolSetting"
|
|
}
|
|
|
|
$secureList = Invoke-CliJson -Arguments @('settings', 'secure', 'list')
|
|
if (@($secureList.settings | Where-Object { $_.key -eq $manifest.secureSettings.toggleKey }).Count -eq 0) {
|
|
throw "settings secure list did not report $($manifest.secureSettings.toggleKey)"
|
|
}
|
|
$secureGet = Invoke-CliJson -Arguments @('settings', 'secure', 'get', '--key', [string]$manifest.secureSettings.toggleKey)
|
|
if ($secureGet.setting.key -ne $manifest.secureSettings.toggleKey) {
|
|
throw "settings secure get did not return the requested key"
|
|
}
|
|
Add-Tested 'settings secure list'
|
|
Add-Tested 'settings secure get'
|
|
|
|
if ($manifest.secureSettings.allowSet) {
|
|
$secureSetOn = Invoke-CliJson -Arguments @(
|
|
'settings', 'secure', 'set',
|
|
'--key', [string]$manifest.secureSettings.toggleKey,
|
|
'--enabled', 'true'
|
|
)
|
|
if (-not $secureSetOn.setting.enabled) {
|
|
throw "settings secure set did not enable $($manifest.secureSettings.toggleKey)"
|
|
}
|
|
Add-Tested 'settings secure set'
|
|
|
|
$maintenanceWithCustomPaths = Invoke-CliJson -Arguments @('manager', 'maintenance', '--manager', [string]$manifest.secureSettings.managerForExecutableOverride)
|
|
if (-not $maintenanceWithCustomPaths.maintenance.customExecutablePathsAllowed) {
|
|
throw "manager maintenance did not reflect enabled custom executable paths"
|
|
}
|
|
|
|
$setExecutable = Invoke-CliJson -Arguments @(
|
|
'manager', 'set-executable',
|
|
'--manager', [string]$manifest.secureSettings.managerForExecutableOverride,
|
|
'--path', [string]$maintenanceWithCustomPaths.maintenance.effectiveExecutablePath
|
|
)
|
|
if ($setExecutable.maintenance.configuredExecutablePath -ne $maintenanceWithCustomPaths.maintenance.effectiveExecutablePath) {
|
|
throw "manager set-executable did not persist the configured executable path"
|
|
}
|
|
$clearExecutable = Invoke-CliJson -Arguments @(
|
|
'manager', 'clear-executable',
|
|
'--manager', [string]$manifest.secureSettings.managerForExecutableOverride
|
|
)
|
|
if (-not [string]::IsNullOrWhiteSpace($clearExecutable.maintenance.configuredExecutablePath)) {
|
|
throw "manager clear-executable did not clear the custom executable path"
|
|
}
|
|
Add-Tested 'manager set-executable'
|
|
Add-Tested 'manager clear-executable'
|
|
|
|
$secureSetOff = Invoke-CliJson -Arguments @(
|
|
'settings', 'secure', 'set',
|
|
'--key', [string]$manifest.secureSettings.toggleKey,
|
|
'--enabled', 'false'
|
|
)
|
|
if ($secureSetOff.setting.enabled) {
|
|
throw "settings secure set did not disable $($manifest.secureSettings.toggleKey)"
|
|
}
|
|
}
|
|
|
|
Write-Stage 'Shortcut and backup'
|
|
$syntheticShortcut = Join-Path $daemonRoot 'SyntheticShortcut.lnk'
|
|
New-Item -ItemType File -Path $syntheticShortcut | Out-Null
|
|
|
|
$keepShortcut = Invoke-CliJson -Arguments @('shortcut', 'set', '--path', $syntheticShortcut, '--status', 'keep')
|
|
if ($keepShortcut.shortcut.status -ne 'keep') {
|
|
throw "shortcut set did not persist keep"
|
|
}
|
|
$shortcuts = Invoke-CliJson -Arguments @('shortcut', 'list')
|
|
if (@($shortcuts.shortcuts | Where-Object { $_.path -eq $syntheticShortcut -and $_.status -eq 'keep' -and $_.existsOnDisk }).Count -eq 0) {
|
|
throw "shortcut list did not report the kept shortcut"
|
|
}
|
|
$deleteShortcut = Invoke-CliJson -Arguments @('shortcut', 'set', '--path', $syntheticShortcut, '--status', 'delete')
|
|
if ($deleteShortcut.shortcut.status -ne 'delete' -or (Test-Path $syntheticShortcut)) {
|
|
throw "shortcut set --status delete did not delete the shortcut"
|
|
}
|
|
$resetShortcut = Invoke-CliJson -Arguments @('shortcut', 'reset', '--path', $syntheticShortcut)
|
|
if ($resetShortcut.shortcut.status -ne 'unknown') {
|
|
throw "shortcut reset did not clear the shortcut verdict"
|
|
}
|
|
$resetAllShortcuts = Invoke-CliJson -Arguments @('shortcut', 'reset-all')
|
|
Assert-JsonCommandSucceeded -Response $resetAllShortcuts -Label 'shortcut reset-all'
|
|
Add-Tested 'shortcut list'
|
|
Add-Tested 'shortcut set'
|
|
Add-Tested 'shortcut reset'
|
|
Add-Tested 'shortcut reset-all'
|
|
|
|
$appLog = Invoke-CliJson -Arguments @('log', 'app', '--level', '5')
|
|
if (@($appLog.entries).Count -eq 0) {
|
|
throw "log app returned no entries"
|
|
}
|
|
Add-Tested 'log app'
|
|
|
|
$backupStatus = Invoke-CliJson -Arguments @('backup', 'status')
|
|
if ([string]::IsNullOrWhiteSpace($backupStatus.backup.backupDirectory)) {
|
|
throw "backup status did not report the backup directory"
|
|
}
|
|
Add-Tested 'backup status'
|
|
|
|
$backupDirectory = Join-Path $daemonRoot 'backups'
|
|
$setBackupDirectory = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'ChangeBackupOutputDirectory', '--value', $backupDirectory)
|
|
$setBackupFileName = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'ChangeBackupFileName', '--value', 'cli-e2e-backup')
|
|
$disableBackupTimestamping = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'EnableBackupTimestamping', '--enabled', 'false')
|
|
if ($setBackupDirectory.setting.stringValue -ne $backupDirectory -or $setBackupFileName.setting.stringValue -ne 'cli-e2e-backup' -or $disableBackupTimestamping.setting.boolValue) {
|
|
throw "backup settings did not persist correctly"
|
|
}
|
|
|
|
$localBackup = Invoke-CliJson -Arguments @('backup', 'local', 'create')
|
|
Assert-JsonCommandSucceeded -Response $localBackup -Label 'backup local create'
|
|
if (-not (Test-Path $localBackup.path)) {
|
|
throw "backup local create did not write the reported backup path"
|
|
}
|
|
Add-Tested 'backup local create'
|
|
|
|
Write-Stage 'Package discovery'
|
|
$fixtureState = @{}
|
|
foreach ($fixture in @($manifest.packageManagers)) {
|
|
$search = Invoke-CliJson -Arguments @('package', 'search', '--manager', [string]$fixture.manager, '--query', [string]$fixture.query, '--max-results', '20')
|
|
$details = Invoke-CliJson -Arguments (@('package', 'details') + (Get-PackageArguments -Fixture $fixture -IncludeSource))
|
|
$versions = Invoke-CliJson -Arguments (@('package', 'versions') + (Get-PackageArguments -Fixture $fixture -IncludeSource))
|
|
if (@($versions.versions | Where-Object { $_ -eq $fixture.installVersion }).Count -eq 0) {
|
|
throw "package versions did not include $($fixture.installVersion) for $($fixture.packageId) on $($fixture.manager)"
|
|
}
|
|
if ($details.package.id -ne $fixture.packageId) {
|
|
throw "package details did not return $($fixture.packageId) for $($fixture.manager)"
|
|
}
|
|
$latestVersion = Get-LatestFixtureVersion -Fixture $fixture -SearchResponse $search -VersionResponse $versions
|
|
$downloadDirectory = Join-Path $downloadRoot (([string]$fixture.manager -replace '[^A-Za-z0-9._-]', '_') + '-sync')
|
|
New-Item -ItemType Directory -Path $downloadDirectory -Force | Out-Null
|
|
$download = Invoke-CliJson -Arguments (@('package', 'download') + (Get-PackageArguments -Fixture $fixture -IncludeSource) + @('--output', $downloadDirectory))
|
|
Assert-JsonCommandSucceeded -Response $download -Label "package download $($fixture.packageId)"
|
|
if ([string]::IsNullOrWhiteSpace($download.outputPath) -or -not (Test-Path $download.outputPath)) {
|
|
throw "package download did not create an artifact for $($fixture.packageId) on $($fixture.manager)"
|
|
}
|
|
|
|
$fixtureState[[string]$fixture.manager] = [ordered]@{
|
|
fixture = $fixture
|
|
latestVersion = $latestVersion
|
|
}
|
|
}
|
|
Add-Tested 'package search'
|
|
Add-Tested 'package details'
|
|
Add-Tested 'package versions'
|
|
Add-Tested 'package download'
|
|
|
|
Write-Stage 'Bundle roundtrip and bundle install'
|
|
$resetBundle = Invoke-CliJson -Arguments @('bundle', 'reset')
|
|
Assert-JsonCommandSucceeded -Response $resetBundle -Label 'bundle reset'
|
|
$bundleAfterReset = Invoke-CliJson -Arguments @('bundle', 'get')
|
|
if ($bundleAfterReset.bundle.packageCount -ne 0) {
|
|
throw "bundle get did not return an empty bundle after reset"
|
|
}
|
|
$addBundlePackage = Invoke-CliJson -Arguments (@('bundle', 'add') + (Get-PackageArguments -Fixture $bundleFixture -IncludeVersion -IncludeSource) + @('--selection', 'search'))
|
|
if ($addBundlePackage.package.id -ne $bundleFixture.packageId) {
|
|
throw "bundle add did not add $($bundleFixture.packageId)"
|
|
}
|
|
$bundle = Invoke-CliJson -Arguments @('bundle', 'get')
|
|
if (@($bundle.bundle.packages | Where-Object { $_.id -eq $bundleFixture.packageId -and $_.selectedVersion -eq $bundleFixture.installVersion }).Count -eq 0) {
|
|
throw "bundle get did not report the selected bundle package"
|
|
}
|
|
$exportedBundle = Invoke-CliJson -Arguments @('bundle', 'export')
|
|
if ([string]::IsNullOrWhiteSpace($exportedBundle.content)) {
|
|
throw "bundle export returned no content"
|
|
}
|
|
$bundleRoundtripPath = Join-Path $daemonRoot 'BundleRoundtrip.json'
|
|
Set-Content -Path $bundleRoundtripPath -Value $exportedBundle.content -Encoding UTF8
|
|
$removeBundlePackage = Invoke-CliJson -Arguments (@('bundle', 'remove') + (Get-PackageArguments -Fixture $bundleFixture -IncludeSource))
|
|
if ($removeBundlePackage.removedCount -lt 1) {
|
|
throw "bundle remove did not remove $($bundleFixture.packageId)"
|
|
}
|
|
$importBundle = Invoke-CliJson -Arguments @('bundle', 'import', '--path', $bundleRoundtripPath)
|
|
Assert-JsonCommandSucceeded -Response $importBundle -Label 'bundle import'
|
|
$bundleInstall = Invoke-CliJson -Arguments @('bundle', 'install')
|
|
if ($bundleInstall.status -ne 'success' -or @($bundleInstall.results | Where-Object { $_.package.id -eq $bundleFixture.packageId }).Count -eq 0) {
|
|
throw "bundle install did not report a successful package result: $($bundleInstall | ConvertTo-Json -Depth 10)"
|
|
}
|
|
Wait-ForInstalledPackage -Fixture $bundleFixture | Out-Null
|
|
Add-Tested 'bundle get'
|
|
Add-Tested 'bundle reset'
|
|
Add-Tested 'bundle add'
|
|
Add-Tested 'bundle remove'
|
|
Add-Tested 'bundle export'
|
|
Add-Tested 'bundle import'
|
|
Add-Tested 'bundle install'
|
|
|
|
Write-Stage 'Operation queue control'
|
|
$queuedOperationIds = @()
|
|
foreach ($queueFixture in @($manifest.queueOperations)) {
|
|
$queueDownload = Invoke-CliJson -Arguments @(
|
|
'package', 'download',
|
|
'--manager', [string]$queueFixture.manager,
|
|
'--id', [string]$queueFixture.packageId,
|
|
'--source', [string]$queueFixture.sourceName,
|
|
'--output', (Resolve-QueueOutputPath -QueueFixture $queueFixture),
|
|
'--wait', 'false'
|
|
)
|
|
if ($queueDownload.status -ne 'success' -or $queueDownload.completed -or [string]::IsNullOrWhiteSpace($queueDownload.operationId)) {
|
|
throw "package download --wait false did not return an in-progress operation payload for $($queueFixture.packageId): $($queueDownload | ConvertTo-Json -Depth 10)"
|
|
}
|
|
$queuedOperationIds += [string]$queueDownload.operationId
|
|
}
|
|
|
|
$queuedOperations = Wait-ForCliCondition `
|
|
-Arguments @('operation', 'list') `
|
|
-Condition {
|
|
param($response)
|
|
$targeted = @($response.operations | Where-Object { $queuedOperationIds -contains $_.id })
|
|
$targeted.Count -eq $queuedOperationIds.Count
|
|
} `
|
|
-FailureMessage 'operation list never reported the queued download operations.' `
|
|
-TimeoutSeconds 180 `
|
|
-DelaySeconds 2
|
|
|
|
$queuedOperation = @($queuedOperations.operations | Where-Object {
|
|
$queuedOperationIds -contains $_.id
|
|
})[0]
|
|
|
|
$operationDetails = Invoke-CliJson -Arguments @('operation', 'get', '--id', $queuedOperation.id)
|
|
if ($operationDetails.operation.id -ne $queuedOperation.id) {
|
|
throw "operation get did not return the requested queued operation id"
|
|
}
|
|
$operationOutput = Invoke-CliJson -Arguments @('operation', 'output', '--id', $queuedOperation.id, '--tail', '10')
|
|
if ($operationOutput.output.operationId -ne $queuedOperation.id) {
|
|
throw "operation output did not return the requested queued operation id"
|
|
}
|
|
|
|
foreach ($operationId in $queuedOperationIds) {
|
|
$waitedOperation = Invoke-CliJson -Arguments @('operation', 'wait', '--id', $operationId, '--timeout', '300', '--delay', '1')
|
|
if ($waitedOperation.operation.status -ne 'succeeded') {
|
|
throw "operation wait did not report success for operation ${operationId}: $($waitedOperation | ConvertTo-Json -Depth 10)"
|
|
}
|
|
}
|
|
|
|
foreach ($operationId in $queuedOperationIds) {
|
|
$completedOutput = Invoke-CliJson -Arguments @('operation', 'output', '--id', $operationId)
|
|
if ($completedOutput.output.lineCount -lt 0) {
|
|
throw "operation output reported an invalid line count for operation $operationId"
|
|
}
|
|
$forget = Invoke-CliJson -Arguments @('operation', 'forget', '--id', $operationId)
|
|
Assert-JsonCommandSucceeded -Response $forget -Label "operation forget $operationId"
|
|
}
|
|
$operationsAfterForget = Invoke-CliJson -Arguments @('operation', 'list')
|
|
if (@($operationsAfterForget.operations | Where-Object { $queuedOperationIds -contains $_.id }).Count -ne 0) {
|
|
throw "operation forget did not remove all queued download operations"
|
|
}
|
|
Add-Tested 'operation list'
|
|
Add-Tested 'operation get'
|
|
Add-Tested 'operation output'
|
|
Add-Tested 'operation wait'
|
|
Add-Tested 'operation forget'
|
|
|
|
Write-Stage 'Package lifecycle and updates'
|
|
$specificUpdateFixture = @($manifest.packageManagers | Where-Object { @($_.roles) -contains 'specific-update' })[0]
|
|
if ($null -eq $specificUpdateFixture) {
|
|
throw 'The CLI E2E manifest must define a package manager fixture with the specific-update role.'
|
|
}
|
|
$directInstallFixtures = @($manifest.packageManagers | Where-Object {
|
|
$null -ne $specificUpdateFixture -and [string]$_.manager -eq [string]$specificUpdateFixture.manager
|
|
})
|
|
$otherInstallFixtures = @($manifest.packageManagers | Where-Object {
|
|
[string]$_.manager -ne [string]$bundleFixture.manager -and
|
|
[string]$_.manager -ne [string]$specificUpdateFixture.manager
|
|
})
|
|
$directInstallFixtures += $otherInstallFixtures
|
|
$bundleLatestVersion = [string]$fixtureState[$bundleFixture.manager].latestVersion
|
|
|
|
foreach ($installFixture in $directInstallFixtures) {
|
|
$installResult = Invoke-CliJson -Arguments (@('package', 'install') + (Get-PackageArguments -Fixture $installFixture -IncludeVersion -IncludeSource))
|
|
Assert-JsonCommandSucceeded -Response $installResult -Label "package install $($installFixture.manager)"
|
|
Wait-ForInstalledPackage -Fixture $installFixture -ExpectedVersion $installFixture.installVersion | Out-Null
|
|
}
|
|
Add-Tested 'package install'
|
|
Add-Tested 'package installed'
|
|
|
|
$updateDiscoveryFixtures = @($manifest.packageManagers | Where-Object { @($_.roles) -contains 'update-discovery' })
|
|
foreach ($updateDiscoveryFixture in $updateDiscoveryFixtures) {
|
|
$reloadManager = Invoke-CliJson -Arguments @('manager', 'reload', '--manager', [string]$updateDiscoveryFixture.manager)
|
|
if ($reloadManager.operationStatus -ne 'completed') {
|
|
throw "manager reload before package updates did not complete successfully for $($updateDiscoveryFixture.manager): $($reloadManager | ConvertTo-Json -Depth 8)"
|
|
}
|
|
}
|
|
foreach ($updateDiscoveryFixture in $updateDiscoveryFixtures) {
|
|
Wait-ForPackageUpdateVisibility -Fixture $updateDiscoveryFixture | Out-Null
|
|
}
|
|
$allUpdates = Invoke-CliJson -Arguments @('package', 'updates')
|
|
if ($null -eq $allUpdates.updates) {
|
|
throw "package updates did not return an updates payload"
|
|
}
|
|
Add-Tested 'package updates'
|
|
|
|
$ignoredAdd = Invoke-CliJson -Arguments @('package', 'ignored', 'add', '--manager', [string]$bundleFixture.manager, '--id', [string]$bundleFixture.packageId)
|
|
Assert-JsonCommandSucceeded -Response $ignoredAdd -Label 'package ignored add'
|
|
$ignoredList = Invoke-CliJson -Arguments @('package', 'ignored', 'list')
|
|
if (@($ignoredList.ignoredUpdates | Where-Object { $_.packageId -eq $bundleFixture.packageId }).Count -eq 0) {
|
|
throw "package ignored list did not report the ignored dotnet-tool fixture"
|
|
}
|
|
$ignoredRemove = Invoke-CliJson -Arguments @('package', 'ignored', 'remove', '--manager', [string]$bundleFixture.manager, '--id', [string]$bundleFixture.packageId)
|
|
Assert-JsonCommandSucceeded -Response $ignoredRemove -Label 'package ignored remove'
|
|
Add-Tested 'package ignored list'
|
|
Add-Tested 'package ignored add'
|
|
Add-Tested 'package ignored remove'
|
|
|
|
$specificUpdate = Invoke-CliJson -Arguments (@(
|
|
'package', 'update'
|
|
) + (Get-PackageArguments -Fixture $specificUpdateFixture -IncludeSource) + @(
|
|
'--version', [string]$fixtureState[$specificUpdateFixture.manager].latestVersion
|
|
))
|
|
Assert-JsonCommandSucceeded -Response $specificUpdate -Label "package update $($specificUpdateFixture.manager)"
|
|
Wait-ForInstalledPackage -Fixture $specificUpdateFixture -ExpectedVersion ([string]$fixtureState[$specificUpdateFixture.manager].latestVersion) | Out-Null
|
|
Add-Tested 'package update'
|
|
|
|
$updateManager = Invoke-CliFailure -Arguments @('package', 'update-manager', '--manager', [string]$bundleFixture.manager)
|
|
if ($updateManager.Text -notmatch 'cannot update manager packages') {
|
|
throw "package update-manager did not report the expected headless limitation: $($updateManager.Text)"
|
|
}
|
|
Add-Tested 'package update-manager' 'expected-failure'
|
|
|
|
$updateAll = Invoke-CliFailure -Arguments @('package', 'update-all')
|
|
if ($updateAll.Text -notmatch 'cannot update all packages') {
|
|
throw "package update-all did not report the expected headless limitation: $($updateAll.Text)"
|
|
}
|
|
Add-Tested 'package update-all' 'expected-failure'
|
|
|
|
$reinstall = Invoke-CliJson -Arguments (@('package', 'reinstall') + (Get-PackageArguments -Fixture $bundleFixture -IncludeSource))
|
|
Assert-JsonCommandSucceeded -Response $reinstall -Label 'package reinstall'
|
|
Wait-ForInstalledPackage -Fixture $bundleFixture -ExpectedVersion $bundleLatestVersion | Out-Null
|
|
Add-Tested 'package reinstall'
|
|
|
|
$repair = Invoke-CliJson -Arguments (@('package', 'repair') + (Get-PackageArguments -Fixture $bundleFixture -IncludeSource))
|
|
Assert-JsonCommandSucceeded -Response $repair -Label 'package repair'
|
|
Wait-ForInstalledPackage -Fixture $bundleFixture -ExpectedVersion $bundleLatestVersion | Out-Null
|
|
Add-Tested 'package repair'
|
|
|
|
$installedAll = Invoke-CliJson -Arguments @('package', 'installed')
|
|
foreach ($fixture in @($manifest.packageManagers)) {
|
|
if (@($installedAll.packages | Where-Object { $_.id -eq $fixture.packageId }).Count -eq 0) {
|
|
throw "package installed did not report $($fixture.packageId) after lifecycle operations"
|
|
}
|
|
}
|
|
|
|
Write-Stage 'Logs'
|
|
$operationHistory = Invoke-CliJson -Arguments @('log', 'operations')
|
|
if ($null -eq $operationHistory.history) {
|
|
throw "log operations did not return a history payload"
|
|
}
|
|
$managerLog = Wait-ForCliCondition `
|
|
-Arguments @('log', 'manager', '--manager', [string]$bundleFixture.manager, '--verbose') `
|
|
-Condition {
|
|
param($response)
|
|
@(
|
|
$response.managers |
|
|
Where-Object {
|
|
$_.name -eq $bundleFixture.manager -and
|
|
@($_.tasks | Where-Object { @($_.lines | Where-Object { $_ -match $bundleFixture.packageId }).Count -gt 0 }).Count -gt 0
|
|
}
|
|
).Count -gt 0
|
|
} `
|
|
-FailureMessage "log manager did not capture package activity for $($bundleFixture.packageId)" `
|
|
-TimeoutSeconds 180 `
|
|
-DelaySeconds 3
|
|
Add-Tested 'log operations'
|
|
Add-Tested 'log manager'
|
|
|
|
Write-Stage 'Package uninstall'
|
|
$uninstallValidationFixtures = @($manifest.packageManagers)
|
|
if ($null -ne $manifest.PSObject.Properties['uninstallValidationManagers']) {
|
|
$uninstallValidationManagerNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($managerName in @($manifest.uninstallValidationManagers)) {
|
|
if (-not [string]::IsNullOrWhiteSpace([string]$managerName)) {
|
|
[void]$uninstallValidationManagerNames.Add([string]$managerName)
|
|
}
|
|
}
|
|
|
|
$uninstallValidationFixtures = @($manifest.packageManagers | Where-Object {
|
|
$uninstallValidationManagerNames.Contains([string]$_.manager)
|
|
})
|
|
}
|
|
foreach ($fixture in $uninstallValidationFixtures) {
|
|
$uninstall = Invoke-CliJson -Arguments (@('package', 'uninstall') + (Get-PackageArguments -Fixture $fixture -IncludeSource))
|
|
Assert-JsonCommandSucceeded -Response $uninstall -Label "package uninstall $($fixture.packageId)"
|
|
if (-not [string]::IsNullOrWhiteSpace($uninstall.operationId)) {
|
|
$waitedUninstall = Invoke-CliJson -Arguments @('operation', 'wait', '--id', [string]$uninstall.operationId, '--timeout', '300', '--delay', '1')
|
|
if ($waitedUninstall.operation.status -ne 'succeeded') {
|
|
throw "package uninstall did not complete successfully for $($fixture.packageId): $($waitedUninstall | ConvertTo-Json -Depth 10)"
|
|
}
|
|
}
|
|
$reloadManager = Invoke-CliJson -Arguments @('manager', 'reload', '--manager', [string]$fixture.manager)
|
|
if ($reloadManager.operationStatus -ne 'completed') {
|
|
throw "manager reload after uninstall did not complete successfully for $($fixture.manager): $($reloadManager | ConvertTo-Json -Depth 8)"
|
|
}
|
|
Wait-ForPackageRemoval -Fixture $fixture | Out-Null
|
|
}
|
|
if ($uninstallValidationFixtures.Count -gt 0) {
|
|
Add-Tested 'package uninstall'
|
|
}
|
|
|
|
Write-Stage 'Settings reset and shutdown'
|
|
$clearFreshValue = Invoke-CliJson -Arguments @('settings', 'clear', '--key', 'FreshValue')
|
|
if ($clearFreshValue.setting.isSet) {
|
|
throw "settings clear did not clear FreshValue"
|
|
}
|
|
$disableFreshBool = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'FreshBoolSetting', '--enabled', 'false')
|
|
if ($disableFreshBool.setting.boolValue) {
|
|
throw "settings set did not disable FreshBoolSetting"
|
|
}
|
|
Add-Tested 'settings clear'
|
|
|
|
$resetSettings = Invoke-CliJson -Arguments @('settings', 'reset')
|
|
Assert-JsonCommandSucceeded -Response $resetSettings -Label 'settings reset'
|
|
Add-Tested 'settings reset'
|
|
|
|
$postResetStatus = Invoke-CliJson -Arguments @('status')
|
|
if (-not $postResetStatus.running) {
|
|
throw "settings reset broke the active IPC session"
|
|
}
|
|
|
|
$quitApp = Invoke-CliJson -Arguments @('app', 'quit')
|
|
Assert-JsonCommandSucceeded -Response $quitApp -Label 'app quit'
|
|
Add-Tested 'app quit'
|
|
|
|
$quitDeadline = (Get-Date).AddSeconds(30)
|
|
while (-not $process.HasExited -and (Get-Date) -lt $quitDeadline) {
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
|
|
if (-not $process.HasExited) {
|
|
throw "app quit did not stop the headless daemon"
|
|
}
|
|
|
|
$gracefulShutdown = $true
|
|
}
|
|
finally {
|
|
$coverage.status = if ($gracefulShutdown) { 'success' } else { 'failed' }
|
|
Set-Content -Path $coveragePath -Value ($coverage | ConvertTo-Json -Depth 12) -Encoding UTF8
|
|
|
|
if (-not $gracefulShutdown) {
|
|
Stop-Daemon
|
|
}
|
|
|
|
$daemonLog = Get-DaemonLog
|
|
if (-not [string]::IsNullOrWhiteSpace($daemonLog)) {
|
|
Write-Host '--- Headless daemon log ---'
|
|
Write-Host $daemonLog
|
|
}
|
|
|
|
if (-not $preserveArtifacts) {
|
|
Remove-Item -Recurse -Force $daemonRoot -ErrorAction SilentlyContinue
|
|
}
|
|
}
|