feat(ci): unify CI and local integration test infrastructure

- Replace tests/infrastructure/Dockerfile with tests/Dockerfile.test
  (single multi-stage Dockerfile for both CI and local dev)
- CI container-image job now builds from Dockerfile.test target dev-infra
- Add ARM support: PowerShell installed via dotnet tool on arm64,
  APT package on amd64
- Replace tests/dev.sh (bash) with tests/dev.ps1 (PowerShell) for
  cross-platform support (Windows, macOS, Linux)
- Add -DockerHost parameter for running x86 containers on a remote
  Docker host from ARM Macs (rsyncs repo, uses SSH Docker transport)
- Add -NoCleanup switch to keep nested PVE VMs after integration tests
- integration command now provisions nested PVE VMs instead of testing
  against a pre-existing PVE directly
- Share /opt/pve-isos host path between CI and local dev (was separate
  Docker named volume)
- Delete tools/Invoke-Tests.ps1 (unused, overlapped with run-integration.sh)
- Add .gitignore entries for Terraform state/artifacts
- Update all documentation references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-24 10:41:28 -05:00
parent c1e1442155
commit f21c7f84b7
13 changed files with 304 additions and 617 deletions
+3 -2
View File
@@ -96,8 +96,9 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v7
with:
context: tests/infrastructure
file: tests/infrastructure/Dockerfile
context: .
file: tests/Dockerfile.test
target: dev-infra
push: true
tags: ${{ env.TEST_IMAGE }}:${{ github.sha }},${{ env.TEST_IMAGE }}:latest
+4
View File
@@ -15,3 +15,7 @@ BenchmarkDotNet.Artifacts/
.env.*
!.env.*.example
tests/.env.test
tests/infrastructure/.terraform/
tests/infrastructure/.terraform.lock.hcl
tests/infrastructure/terraform.tfstate*
tests/infrastructure/*.tfvars.json
+9 -9
View File
@@ -32,15 +32,15 @@ gh pr create
A Docker-based dev environment replicates the full CI setup locally. Works on ARM Macs
(build + test) and x86 (full provisioning flow).
```bash
./tests/dev.sh # Open pwsh shell in dev container
./tests/dev.sh build # Build the module
./tests/dev.sh test # Run unit tests
./tests/dev.sh integration # Run integration tests against existing PVE
./tests/dev.sh provision # Full CI flow: provision → test → cleanup (x86 only)
```powershell
./tests/dev.ps1 # Open pwsh shell in dev container
./tests/dev.ps1 build # Build the module
./tests/dev.ps1 test # Run unit tests (ARM + x86)
./tests/dev.ps1 integration # Provision nested PVE, run tests, cleanup (x86 only)
./tests/dev.ps1 provision # Provision nested PVE only, no tests (x86 only)
```
Configure integration test targets by copying `tests/.env.test.example` to `tests/.env.test`.
Configure parent PVE credentials by copying `tests/.env.test.example` to `tests/.env.test`.
### Build & test without container
@@ -54,8 +54,8 @@ dotnet test tests/PSProxmoxVE.Core.Tests/
# Pester tests (requires pwsh)
pwsh -Command "Invoke-Pester tests/PSProxmoxVE.Tests/ -Output Detailed"
# Run all tests via helper
pwsh -File tools/Invoke-Tests.ps1
# Run all tests via dev container
./tests/dev.ps1 test
```
## Key Conventions
+2 -2
View File
@@ -483,8 +483,8 @@ SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4
1. Clone the repository
2. Open `PSProxmoxVE.sln` in your IDE
3. Build: `dotnet build`
4. Run unit tests: `./tools/Invoke-Tests.ps1`
5. Run integration tests (requires live PVE node): `./tools/Invoke-Tests.ps1 -Tier Integration`
4. Run unit tests: `./tests/dev.ps1 test`
5. Run integration tests (provisions nested PVE, x86 only): `./tests/dev.ps1 integration`
### Commit Convention
+10 -14
View File
@@ -1,21 +1,17 @@
# PSProxmoxVE integration test configuration.
# Copy to .env.test and fill in values. This file is gitignored.
#
# These credentials point to the PARENT PVE host where nested test VMs
# will be provisioned. Integration tests run against the nested VMs,
# not against this host directly.
# ── For testing against a pre-existing PVE (./tests/dev.sh integration) ──
PVETEST_HOST=pve.example.com
PVETEST_PORT=8006
PVETEST_APITOKEN=user@realm!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PVETEST_NODE=pve1
PVETEST_STORAGE=local
PVETEST_PASSWORD=<your-test-password>
# ── Required: parent PVE for provisioning ─────────────────────────────
PVE_ENDPOINT=https://pve.example.com:8006
PVE_API_TOKEN=user@realm!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PVE_TARGET_NODE=pve1
PVE_PASSWORD=<root-password-for-nested-pve>
# ── For full provisioning (./tests/dev.sh provision, x86 only) ───────────
# PVE_ENDPOINT=https://pve.example.com:8006
# PVE_API_TOKEN=user@realm!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# PVE_TARGET_NODE=pve1
# PVE_PASSWORD=<root-password-for-nested-pve>
# ── Optional overrides ──────────────────────────────────────────────────
# ── Optional overrides ────────────────────────────────────────────────
# CACHE_DIR=/opt/pve-isos
# WORK_DIR=/tmp/pve-integration
# PVE_VERSIONS=9 8
+20 -8
View File
@@ -16,16 +16,28 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS dev
ENV DEBIAN_FRONTEND=noninteractive
# Install base packages
RUN apt-get update && apt-get install -y --no-install-recommends \
curl jq openssh-client ca-certificates apt-transport-https gnupg \
&& curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] \
https://packages.microsoft.com/ubuntu/24.04/prod noble main" \
> /etc/apt/sources.list.d/microsoft-prod.list \
&& apt-get update && apt-get install -y --no-install-recommends powershell \
&& rm -rf /var/lib/apt/lists/* \
&& pwsh -NoProfile -Command \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell: APT package on amd64, dotnet global tool on arm64.
# The Microsoft APT repo does not publish arm64 packages for PowerShell.
RUN if [ "$(dpkg --print-architecture)" = "amd64" ]; then \
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] \
https://packages.microsoft.com/ubuntu/24.04/prod noble main" \
> /etc/apt/sources.list.d/microsoft-prod.list \
&& apt-get update && apt-get install -y --no-install-recommends powershell \
&& rm -rf /var/lib/apt/lists/*; \
else \
dotnet tool install --global PowerShell \
&& ln -s /root/.dotnet/tools/pwsh /usr/local/bin/pwsh; \
fi
# Install Pester and prepare module directory
RUN pwsh -NoProfile -Command \
'Set-PSRepository -Name PSGallery -InstallationPolicy Trusted; \
Install-Module -Name Pester -MinimumVersion 5.0 -Scope AllUsers -Force' \
&& mkdir -p /usr/local/share/powershell/Modules/PSProxmoxVE
@@ -24,8 +24,8 @@
To run:
Invoke-Pester -Path ./tests/PSProxmoxVE.Tests -Tag Integration
# or use the project's helper script:
./Invoke-Tests.ps1 -Tier Integration
# or use the dev container:
./tests/dev.ps1 integration
#>
BeforeAll {
@@ -119,10 +119,10 @@ env:
The integration suite is tagged `Integration`. Use the `-Tag` filter so that the unit
tests and integration tests can be run independently.
### Via the project helper script (recommended)
### Via the dev container (recommended)
```powershell
./Invoke-Tests.ps1 -Tier Integration
./tests/dev.ps1 integration
```
### Directly with Invoke-Pester
+251
View File
@@ -0,0 +1,251 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Helper script for the dev/test containers.
.DESCRIPTION
Manages Docker-based dev containers for building, testing, and running
integration tests for PSProxmoxVE. Works on Windows, macOS, and Linux.
For x86-only commands (integration, provision, cleanup), use -DockerHost
to run containers on a remote Docker host via SSH. The script syncs the
repo to the remote host automatically.
.PARAMETER Command
The action to perform:
shell - Open pwsh in the dev container (default)
build - Build the module inside the container
test - Run unit tests (ARM + x86)
integration - Provision nested PVE VMs, run integration tests, cleanup (x86 only)
provision - Provision nested PVE VMs only, no tests (x86 only)
cleanup - Destroy provisioned VMs (x86 only)
stop - Stop all containers
rebuild - Rebuild container image(s)
.PARAMETER PveVersion
PVE version to test against (8, 9, or all). Default: all.
Only used with 'integration' and 'provision' commands.
.PARAMETER NoCleanup
When used with 'integration', skips cleanup after tests complete.
Nested PVE VMs are left running for inspection or re-testing.
Run './tests/dev.ps1 cleanup' to destroy them later.
.PARAMETER DockerHost
SSH destination for a remote Docker host (e.g. user@runner-vm).
When set, the repo is rsynced to the remote host and all Docker
commands run against the remote daemon. Useful for running x86
containers from an ARM Mac.
Prerequisites on the remote host:
- Docker installed
- SSH user in the 'docker' group (sudo usermod -aG docker $USER)
- SSH key-based auth from the local machine
.EXAMPLE
./tests/dev.ps1
# Opens a pwsh shell in the dev container
.EXAMPLE
./tests/dev.ps1 test
# Builds the module and runs unit tests
.EXAMPLE
./tests/dev.ps1 integration -DockerHost user@runner-vm
# Syncs repo to runner-vm, provisions nested PVE, runs tests, cleans up
.EXAMPLE
./tests/dev.ps1 integration 9 -DockerHost user@runner-vm
# Same but only for PVE 9
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet('shell', 'build', 'test', 'integration', 'provision', 'cleanup', 'stop', 'rebuild')]
[string] $Command = 'shell',
[Parameter(Position = 1)]
[string] $PveVersion = 'all',
[Parameter()]
[string] $DockerHost,
[Parameter()]
[Alias('k')]
[switch] $NoCleanup
)
$ErrorActionPreference = 'Stop'
# Resolve repo root (parent of tests/)
$RepoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $RepoRoot
try {
# ── Remote Docker host support ────────────────────────────────────────
# When -DockerHost is specified, we rsync the repo (including .env.test)
# to the remote host and set DOCKER_HOST so all docker/compose commands
# execute on the remote daemon. The compose file's volume mount (..:/repo)
# and build context reference the remote copy.
$RemoteRepoPath = $null
if ($DockerHost) {
$RemoteRepoPath = "/tmp/psproxmoxve-dev"
$env:DOCKER_HOST = "ssh://$DockerHost"
Write-Host "Syncing repo to ${DockerHost}:${RemoteRepoPath}..."
# Create remote directory
ssh $DockerHost "mkdir -p $RemoteRepoPath"
if ($LASTEXITCODE -ne 0) { throw "Failed to create remote directory" }
# Rsync repo to remote host, excluding build artifacts
rsync -az --delete `
--exclude 'bin/' `
--exclude 'obj/' `
--exclude 'publish/' `
--exclude 'TestResults/' `
--exclude '.terraform/' `
--exclude 'terraform.tfstate*' `
--include '.env.test' `
./ "${DockerHost}:${RemoteRepoPath}/"
if ($LASTEXITCODE -ne 0) { throw "Failed to sync repo to remote host" }
Write-Host "Using remote Docker host: $DockerHost"
}
# When running remotely, docker compose reads the compose file locally but
# volume mounts resolve on the remote host. We generate a temporary override
# file that remaps volume mounts to the rsynced remote path.
$ComposeFile = 'tests/docker-compose.test.yml'
$ComposeArgs = @('-f', $ComposeFile)
$OverrideFile = $null
if ($RemoteRepoPath) {
$OverrideFile = Join-Path ([System.IO.Path]::GetTempPath()) 'docker-compose.remote-override.yml'
# Override volume mounts to point at the rsynced remote repo path.
# env_file is NOT overridden — compose reads it locally and injects
# the values as container env vars, which is what we want.
@"
services:
dev:
volumes:
- ${RemoteRepoPath}:/repo
dev-infra:
volumes:
- ${RemoteRepoPath}:/repo
- /opt/pve-isos:/opt/pve-isos
"@ | Set-Content -Path $OverrideFile -Encoding utf8
$ComposeArgs = @('-f', $ComposeFile, '-f', $OverrideFile)
}
$DevContainer = 'psproxmoxve-dev'
$InfraContainer = 'psproxmoxve-dev-infra'
$RunIntegration = 'tests/infrastructure/scripts/run-integration.sh'
function Start-DevContainer {
# 'up -d' is idempotent: starts if stopped, recreates if config/env changed, no-ops if current
docker compose @ComposeArgs up -d dev
if ($LASTEXITCODE -ne 0) { throw 'Failed to start dev container' }
}
function Start-InfraContainer {
docker compose @ComposeArgs --profile infra up -d dev-infra
if ($LASTEXITCODE -ne 0) { throw 'Failed to start infra container (x86 only)' }
}
function Invoke-BuildModule {
param([string] $Container)
docker exec $Container bash -c @"
dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj -c Release -f netstandard2.0 -o /tmp/publish 2>&1 | tail -1 && \
cp -r /tmp/publish/* /usr/local/share/powershell/Modules/PSProxmoxVE/ && \
echo 'Module installed to /usr/local/share/powershell/Modules/PSProxmoxVE'
"@
if ($LASTEXITCODE -ne 0) { throw "Module build failed (exit code $LASTEXITCODE)" }
}
switch ($Command) {
'shell' {
if ($DockerHost) {
Write-Warning "Interactive shell over remote Docker is not supported. Use: ssh $DockerHost 'docker exec -it $DevContainer pwsh -NoProfile'"
return
}
Start-DevContainer
docker exec -it $DevContainer pwsh -NoProfile
}
'build' {
Start-DevContainer
Invoke-BuildModule $DevContainer
}
'test' {
Start-DevContainer
Invoke-BuildModule $DevContainer
docker exec $DevContainer pwsh -NoProfile -Command @'
$config = New-PesterConfiguration
$config.Run.Path = 'tests/PSProxmoxVE.Tests'
$config.Run.Exit = $true
$config.Filter.ExcludeTag = @('Integration')
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $config
'@
if ($LASTEXITCODE -ne 0) { throw "Unit tests failed (exit code $LASTEXITCODE)" }
}
'integration' {
# Full lifecycle: provision nested PVE VMs -> test -> cleanup (x86 only)
# Requires PVE_ENDPOINT, PVE_API_TOKEN, PVE_TARGET_NODE, PVE_PASSWORD in .env.test
# run-integration.sh handles module build if no pre-built artifact exists
Start-InfraContainer
if ($NoCleanup) {
# Provision and test separately — leave VMs running for inspection
docker exec $InfraContainer bash $RunIntegration provision
if ($LASTEXITCODE -ne 0) { throw "Provisioning failed (exit code $LASTEXITCODE)" }
docker exec $InfraContainer bash $RunIntegration test $PveVersion
if ($LASTEXITCODE -ne 0) { throw "Integration tests failed (exit code $LASTEXITCODE)" }
Write-Host 'VMs left running (-NoCleanup). Run ./tests/dev.ps1 cleanup to destroy them.'
} else {
docker exec $InfraContainer bash $RunIntegration all $PveVersion
if ($LASTEXITCODE -ne 0) { throw "Integration tests failed (exit code $LASTEXITCODE)" }
}
}
'provision' {
# Provision nested PVE VMs only, without running tests (x86 only)
# Useful for iterating: provision once, then shell in and run tests manually
Start-InfraContainer
docker exec $InfraContainer bash $RunIntegration provision
if ($LASTEXITCODE -ne 0) { throw "Provisioning failed (exit code $LASTEXITCODE)" }
}
'cleanup' {
# Destroy provisioned VMs
Start-InfraContainer
docker exec $InfraContainer bash $RunIntegration cleanup
if ($LASTEXITCODE -ne 0) { throw "Cleanup failed (exit code $LASTEXITCODE)" }
}
'stop' {
docker compose @ComposeArgs --profile infra down
}
'rebuild' {
docker compose @ComposeArgs --profile infra down
docker compose @ComposeArgs build --no-cache dev
docker compose @ComposeArgs --profile infra build --no-cache dev-infra
docker compose @ComposeArgs up -d dev
}
}
} finally {
if ($OverrideFile -and (Test-Path $OverrideFile)) {
Remove-Item $OverrideFile -Force -ErrorAction SilentlyContinue
}
if ($DockerHost) {
Remove-Item Env:\DOCKER_HOST -ErrorAction SilentlyContinue
}
Pop-Location
}
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env bash
# Helper script for the dev/test containers.
#
# Usage:
# ./tests/dev.sh # Start dev container and open pwsh shell
# ./tests/dev.sh build # Build the module inside the container
# ./tests/dev.sh test # Run unit tests
# ./tests/dev.sh integration # Run integration tests against existing PVE (needs .env.test)
# ./tests/dev.sh provision # Full CI flow: provision → test → cleanup (x86 only)
# ./tests/dev.sh cleanup # Destroy provisioned VMs (x86 only)
# ./tests/dev.sh stop # Stop all containers
# ./tests/dev.sh rebuild # Rebuild container image(s)
set -euo pipefail
cd "$(dirname "$0")/.."
COMPOSE="docker compose -f tests/docker-compose.test.yml"
DEV_CONTAINER="psproxmoxve-dev"
INFRA_CONTAINER="psproxmoxve-dev-infra"
MODULE_PATH="/usr/local/share/powershell/Modules/PSProxmoxVE"
RUN_INTEGRATION="tests/infrastructure/scripts/run-integration.sh"
ensure_dev() {
if ! docker inspect "$DEV_CONTAINER" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then
echo "Starting dev container..."
$COMPOSE up -d dev
fi
}
ensure_infra() {
if ! docker inspect "$INFRA_CONTAINER" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then
echo "Starting infra container (x86 only)..."
$COMPOSE --profile infra up -d dev-infra
fi
}
build_module() {
local container="$1"
docker exec "$container" bash -c "
dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj \
-c Release -f netstandard2.0 -o /tmp/publish 2>&1 | tail -1 && \
cp -r /tmp/publish/* $MODULE_PATH/ && \
echo "Module installed to $MODULE_PATH"
"
}
case "${1:-shell}" in
shell)
ensure_dev
docker exec -it "$DEV_CONTAINER" pwsh -NoProfile
;;
build)
ensure_dev
build_module "$DEV_CONTAINER"
;;
test)
ensure_dev
build_module "$DEV_CONTAINER"
docker exec "$DEV_CONTAINER" pwsh -NoProfile -Command "
\$config = New-PesterConfiguration
\$config.Run.Path = 'tests/PSProxmoxVE.Tests'
\$config.Run.Exit = \$true
\$config.Filter.ExcludeTag = @('Integration')
\$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration \$config
"
;;
integration)
# Run integration tests against a pre-existing PVE (set via .env.test)
ensure_dev
build_module "$DEV_CONTAINER"
docker exec "$DEV_CONTAINER" bash -c "
SKIP_PROVISION=true bash $RUN_INTEGRATION test ${2:-all}
"
;;
provision)
# Full CI lifecycle: provision → test → cleanup (x86 infra container)
ensure_infra
build_module "$INFRA_CONTAINER"
docker exec "$INFRA_CONTAINER" bash "$RUN_INTEGRATION" all "${2:-all}"
;;
cleanup)
# Destroy provisioned VMs
ensure_infra
docker exec "$INFRA_CONTAINER" bash "$RUN_INTEGRATION" cleanup
;;
stop)
$COMPOSE --profile infra down
;;
rebuild)
$COMPOSE --profile infra down
$COMPOSE build --no-cache dev
$COMPOSE --profile infra build --no-cache dev-infra
$COMPOSE up -d dev
;;
*)
echo "Usage: $0 {shell|build|test|integration|provision|cleanup|stop|rebuild}"
echo ""
echo " shell Open pwsh in the dev container"
echo " build Build the module"
echo " test Run unit tests"
echo " integration Run integration tests against existing PVE (.env.test)"
echo " provision Full CI flow: provision → test → cleanup (x86 only)"
echo " cleanup Destroy provisioned VMs (x86 only)"
echo " stop Stop all containers"
echo " rebuild Rebuild container images"
exit 1
;;
esac
+1 -4
View File
@@ -37,7 +37,7 @@ services:
profiles: [infra]
volumes:
- ..:/repo
- pve-isos:/opt/pve-isos
- /opt/pve-isos:/opt/pve-isos
working_dir: /repo
stdin_open: true
tty: true
@@ -45,6 +45,3 @@ services:
env_file:
- path: .env.test
required: false
volumes:
pve-isos:
-67
View File
@@ -1,67 +0,0 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# ── Base packages ──────────────────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
jq \
dosfstools \
mtools \
openssh-client \
sshpass \
apt-transport-https \
gnupg \
lsb-release \
ca-certificates \
python3 \
xorriso \
&& rm -rf /var/lib/apt/lists/*
# ── Microsoft repo (PowerShell) ───────────────────────────────────────
RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] \
https://packages.microsoft.com/ubuntu/24.04/prod noble main" \
> /etc/apt/sources.list.d/microsoft-prod.list
# ── HashiCorp repo (Terraform) ────────────────────────────────────────
RUN curl -fsSL https://apt.releases.hashicorp.com/gpg \
| gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com noble main" \
> /etc/apt/sources.list.d/hashicorp.list
# ── Proxmox repo (proxmox-auto-install-assistant) ─────────────────────
RUN curl -fsSL https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg \
-o /usr/share/keyrings/proxmox-release-bookworm.gpg && \
echo "deb [signed-by=/usr/share/keyrings/proxmox-release-bookworm.gpg] \
http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
> /etc/apt/sources.list.d/proxmox-pve.list
# ── Install tooling ───────────────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
powershell \
terraform \
proxmox-auto-install-assistant \
qemu-utils \
&& rm -rf /var/lib/apt/lists/*
# ── Install Pester ────────────────────────────────────────────────────
RUN pwsh -NoProfile -Command \
'Set-PSRepository -Name PSGallery -InstallationPolicy Trusted; \
Install-Module -Name Pester -MinimumVersion 5.0 -Scope AllUsers -Force'
# ── Verify all tools are present ──────────────────────────────────────
RUN echo "=== Tool verification ===" && \
pwsh --version && \
terraform --version && \
proxmox-auto-install-assistant --version && \
sshpass -V | head -1 && \
xorriso --version 2>&1 | head -1 && \
qemu-img --version | head -1 && \
curl --version | head -1 && \
jq --version && \
python3 --version && \
ssh -V 2>&1 && \
echo "=== All tools verified ==="
-390
View File
@@ -1,390 +0,0 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Test runner for the PSProxmoxVE project.
.DESCRIPTION
Runs unit and/or integration tests for PSProxmoxVE. Unit tests include both
xUnit (.NET) and Pester (PowerShell) suites. Integration tests require a live
or mocked PVE endpoint and are executed via Pester with the Integration tag.
.PARAMETER Tier
Which tier of tests to run. Valid values: Unit, Integration, All. Default: Unit.
.PARAMETER Framework
Dotnet target framework moniker to pass to 'dotnet test' (e.g. net8.0, net9.0).
When omitted the framework is auto-detected from the first *.csproj found under
tests/PSProxmoxVE.Core.Tests.
.PARAMETER FromTerraform
Read PVE connection details from 'terraform output -json' in
tests/infrastructure/. Requires Terraform to be installed and
'terraform apply' to have been run. Implies -Tier Integration.
.PARAMETER PveHost
Hostname or IP of the PVE test node. Sets PVETEST_HOST.
.PARAMETER PvePort
API port. Default 8006. Sets PVETEST_PORT.
.PARAMETER PveApiToken
API token in USER@REALM!TOKENID=UUID format. Sets PVETEST_APITOKEN.
.PARAMETER PveNode
PVE node name (e.g. pve). Sets PVETEST_NODE.
.PARAMETER PveStorage
Storage pool for disk/ISO operations (e.g. local). Sets PVETEST_STORAGE.
.PARAMETER PveIsoPath
Local filesystem path to a small .iso for upload tests. Sets PVETEST_ISO_PATH.
.EXAMPLE
./tools/Invoke-Tests.ps1
.EXAMPLE
./tools/Invoke-Tests.ps1 -Tier All
.EXAMPLE
./tools/Invoke-Tests.ps1 -FromTerraform
.EXAMPLE
./tools/Invoke-Tests.ps1 -Tier Integration `
-PveHost 192.168.1.200 -PveApiToken "root@pam!integration=abc123..." `
-PveNode pve -PveStorage local -PveIsoPath /tmp/test.iso
.EXAMPLE
./tools/Invoke-Tests.ps1 -Tier Unit -Framework net9.0
#>
[CmdletBinding(DefaultParameterSetName = 'Explicit')]
param(
[Parameter()]
[ValidateSet('Unit', 'Integration', 'All')]
[string] $Tier = 'Unit',
[Parameter()]
[string] $Framework,
# --- Terraform-sourced connection ---
[Parameter(ParameterSetName = 'Terraform')]
[switch] $FromTerraform,
# --- Explicit connection params ---
[Parameter(ParameterSetName = 'Explicit')]
[string] $PveHost,
[Parameter(ParameterSetName = 'Explicit')]
[int] $PvePort = 8006,
[Parameter(ParameterSetName = 'Explicit')]
[string] $PveApiToken,
[Parameter(ParameterSetName = 'Explicit')]
[string] $PveNode,
[Parameter(ParameterSetName = 'Explicit')]
[string] $PveStorage,
[Parameter(ParameterSetName = 'Explicit')]
[string] $PveIsoPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ---------------------------------------------------------------------------
# Integration connection setup
# ---------------------------------------------------------------------------
if ($FromTerraform) {
$Tier = 'Integration'
$infraDir = Join-Path $PSScriptRoot '../tests/infrastructure'
if (-not (Get-Command terraform -ErrorAction SilentlyContinue)) {
throw 'terraform not found on PATH. Install Terraform >= 1.5 first.'
}
if (-not (Test-Path $infraDir)) {
throw "Infrastructure directory not found: $infraDir"
}
Write-Host 'Reading connection details from terraform output...' -ForegroundColor Cyan
$tfOutputJson = terraform -chdir:$infraDir output -json 2>&1
if ($LASTEXITCODE -ne 0) {
throw "terraform output failed. Have you run 'terraform apply' in $infraDir?`n$tfOutputJson"
}
$tfOutput = $tfOutputJson | ConvertFrom-Json
$env:PVETEST_HOST = $tfOutput.pve_test_host.value
$env:PVETEST_PORT = $tfOutput.pve_test_port.value
$env:PVETEST_NODE = $tfOutput.pve_test_node_name.value
$env:PVETEST_APITOKEN = terraform -chdir:$infraDir output -raw pve_test_api_token 2>&1
# Storage and ISO path are not provisioned by Terraform; require env vars or defaults
if (-not $env:PVETEST_STORAGE) { $env:PVETEST_STORAGE = 'local' }
if (-not $env:PVETEST_ISO_PATH) {
Write-Warning 'PVETEST_ISO_PATH not set — ISO upload tests will be skipped.'
}
Write-Host " PVE host : $($env:PVETEST_HOST):$($env:PVETEST_PORT)" -ForegroundColor DarkGray
Write-Host " PVE node : $($env:PVETEST_NODE)" -ForegroundColor DarkGray
Write-Host " Storage : $($env:PVETEST_STORAGE)" -ForegroundColor DarkGray
}
elseif ($PSBoundParameters.ContainsKey('PveHost') -or $PSBoundParameters.ContainsKey('PveApiToken')) {
# Explicit params override env vars
if ($PveHost) { $env:PVETEST_HOST = $PveHost }
if ($PvePort) { $env:PVETEST_PORT = $PvePort }
if ($PveApiToken) { $env:PVETEST_APITOKEN = $PveApiToken }
if ($PveNode) { $env:PVETEST_NODE = $PveNode }
if ($PveStorage) { $env:PVETEST_STORAGE = $PveStorage }
if ($PveIsoPath) { $env:PVETEST_ISO_PATH = $PveIsoPath }
if ($Tier -eq 'Unit') { $Tier = 'Integration' }
}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Write-Header {
param([string] $Text)
$line = '-' * 70
Write-Host ''
Write-Host $line -ForegroundColor Cyan
Write-Host " $Text" -ForegroundColor Cyan
Write-Host $line -ForegroundColor Cyan
}
function Assert-Command {
param([string] $Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "Required command '$Name' was not found on PATH."
}
}
# ---------------------------------------------------------------------------
# Path resolution
# ---------------------------------------------------------------------------
$repoRoot = Split-Path -Parent $PSScriptRoot
$coreTestProject = Join-Path $repoRoot 'tests' 'PSProxmoxVE.Core.Tests'
$pesterTestDir = Join-Path $repoRoot 'tests' 'PSProxmoxVE.Tests'
$integrationDir = Join-Path $pesterTestDir 'Integration'
# ---------------------------------------------------------------------------
# Auto-detect framework when not supplied
# ---------------------------------------------------------------------------
if (-not $Framework) {
$csproj = Get-ChildItem -Path $coreTestProject -Filter '*.csproj' -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($csproj) {
# Try singular TargetFramework first, then first entry from TargetFrameworks
$tfmNode = Select-Xml -Path $csproj.FullName -XPath '//*[local-name()="TargetFramework" or local-name()="TargetFrameworks"]' |
Select-Object -First 1
if ($tfmNode) {
# TargetFrameworks may be semicolon-separated; pick the first .NET Core/5+ TFM
# On non-Windows, skip net48 since .NET Framework is not available
$allTfms = $tfmNode.Node.InnerText.Trim() -split ';' | ForEach-Object { $_.Trim() }
$onWindows = $PSVersionTable.Platform -eq 'Win32NT' -or $PSVersionTable.PSEdition -eq 'Desktop'
$Framework = $allTfms | Where-Object { $_ -notmatch 'net4' -or $onWindows } | Select-Object -First 1
Write-Verbose "Auto-detected target framework: $Framework"
}
}
}
# ---------------------------------------------------------------------------
# Result tracking
# ---------------------------------------------------------------------------
$results = [ordered] @{}
# ---------------------------------------------------------------------------
# xUnit tests
# ---------------------------------------------------------------------------
function Invoke-XUnitTests {
Write-Header 'xUnit Tests (PSProxmoxVE.Core.Tests)'
Assert-Command 'dotnet'
if (-not (Test-Path $coreTestProject)) {
Write-Warning "xUnit test project not found at: $coreTestProject -- skipping."
$script:results['xUnit'] = 'Skipped'
return
}
$dotnetArgs = @(
'test'
$coreTestProject
'--logger'
'console;verbosity=normal'
'--nologo'
)
if ($Framework) {
$dotnetArgs += '--framework'
$dotnetArgs += $Framework
}
Write-Host "Running: dotnet $($dotnetArgs -join ' ')" -ForegroundColor DarkGray
& dotnet @dotnetArgs
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host 'xUnit tests PASSED.' -ForegroundColor Green
$script:results['xUnit'] = 'Passed'
}
else {
Write-Host "xUnit tests FAILED (exit code $exitCode)." -ForegroundColor Red
$script:results['xUnit'] = "Failed (exit $exitCode)"
}
}
# ---------------------------------------------------------------------------
# Pester unit tests
# ---------------------------------------------------------------------------
function Invoke-PesterUnitTests {
Write-Header 'Pester Unit Tests (PSProxmoxVE.Tests)'
if (-not (Test-Path $pesterTestDir)) {
Write-Warning "Pester test directory not found at: $pesterTestDir -- skipping."
$script:results['Pester-Unit'] = 'Skipped'
return
}
try {
$pesterModule = Get-Module -Name Pester -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $pesterModule) {
throw 'Pester module is not installed. Run: Install-Module Pester -Force'
}
Import-Module -Name Pester -RequiredVersion $pesterModule.Version -Force
$config = New-PesterConfiguration
$config.Run.Path = $pesterTestDir
$config.Filter.ExcludeTag = @('Integration')
$config.Output.Verbosity = 'Detailed'
$config.Run.PassThru = $true
$pesterResult = Invoke-Pester -Configuration $config
if ($pesterResult.FailedCount -gt 0) {
Write-Host "Pester unit tests FAILED ($($pesterResult.FailedCount) failed, $($pesterResult.PassedCount) passed)." -ForegroundColor Red
$script:results['Pester-Unit'] = "Failed ($($pesterResult.FailedCount) failed)"
}
else {
Write-Host "Pester unit tests PASSED ($($pesterResult.PassedCount) passed)." -ForegroundColor Green
$script:results['Pester-Unit'] = "Passed ($($pesterResult.PassedCount) passed)"
}
}
catch {
Write-Host "Pester unit tests ERROR: $_" -ForegroundColor Red
$script:results['Pester-Unit'] = "Error: $_"
}
}
# ---------------------------------------------------------------------------
# Pester integration tests
# ---------------------------------------------------------------------------
function Invoke-PesterIntegrationTests {
Write-Header 'Pester Integration Tests (PSProxmoxVE.Tests/Integration)'
if (-not (Test-Path $integrationDir)) {
Write-Warning "Integration test directory not found at: $integrationDir -- skipping."
$script:results['Pester-Integration'] = 'Skipped'
return
}
try {
$pesterModule = Get-Module -Name Pester -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $pesterModule) {
throw 'Pester module is not installed. Run: Install-Module Pester -Force'
}
Import-Module -Name Pester -RequiredVersion $pesterModule.Version -Force
$config = New-PesterConfiguration
$config.Run.Path = $integrationDir
$config.Filter.Tag = @('Integration')
$config.Output.Verbosity = 'Detailed'
$config.Run.PassThru = $true
$pesterResult = Invoke-Pester -Configuration $config
if ($pesterResult.FailedCount -gt 0) {
Write-Host "Integration tests FAILED ($($pesterResult.FailedCount) failed, $($pesterResult.PassedCount) passed)." -ForegroundColor Red
$script:results['Pester-Integration'] = "Failed ($($pesterResult.FailedCount) failed)"
}
else {
Write-Host "Integration tests PASSED ($($pesterResult.PassedCount) passed)." -ForegroundColor Green
$script:results['Pester-Integration'] = "Passed ($($pesterResult.PassedCount) passed)"
}
}
catch {
Write-Host "Integration tests ERROR: $_" -ForegroundColor Red
$script:results['Pester-Integration'] = "Error: $_"
}
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
switch ($Tier) {
'Unit' {
Invoke-XUnitTests
Invoke-PesterUnitTests
}
'Integration' {
Invoke-PesterIntegrationTests
}
'All' {
Invoke-XUnitTests
Invoke-PesterUnitTests
Invoke-PesterIntegrationTests
}
}
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
Write-Header 'Test Run Summary'
$anyFailure = $false
foreach ($suite in $results.Keys) {
$status = $results[$suite]
$color = switch -Wildcard ($status) {
'Passed*' { 'Green' }
'Skipped' { 'Yellow' }
default { 'Red' }
}
if ($color -eq 'Red') { $anyFailure = $true }
Write-Host (' {0,-30} {1}' -f "${suite}:", $status) -ForegroundColor $color
}
Write-Host ''
if ($anyFailure) {
Write-Host 'One or more test suites FAILED.' -ForegroundColor Red
exit 1
}
else {
Write-Host 'All test suites completed successfully.' -ForegroundColor Green
exit 0
}