Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,81 @@
# Discovery, installation and uninstallation verification

## Executable test wrapper repair (2026-11-06)

The first hosted run of the performance changes passed PowerShell, Git Bash
and MSYS2 but failed both Bash versions on Linux/macOS and current Cygwin
when creating the first installation's default link. Removing `ln` from the
ordinary utility wrapper loop also removed its initial `chmod +x`; rewriting
the failure-injection wrapper later did not set that permission. The harness
now explicitly makes the completed `ln` wrapper executable before isolating
PATH. Runtime code and test coverage are unchanged.

The [hosted run](https://github.com/codejive/jvm/actions/runs/42131463064)
completed its passing Windows jobs in 3:27 (PowerShell), 3:22 (Git Bash) and
4:48 (MSYS2), including setup/checkout. Those results precede this permission
repair; Linux, macOS and Cygwin still require a hosted rerun.

Local Git Bash passed `bash tests/install.sh --defaults` (47 seconds), covering
default creation, replacement, interrupted updates and removal rollback with
simulated host layouts. Bash syntax checks and `git diff --check` passed.
ShellCheck is unavailable. No local Unix/Cygwin runs, commit, push or hosted
rerun were performed for the repair.

## CI harness performance (2026-11-06)

The PowerShell smoke harness now launches its ordinary script and CMD checks
with `Diagnostics.Process`, drains stdout and stderr concurrently, and waits
on the process directly. It preserves the PowerShell working directory,
argument quoting, fresh-process isolation, stream assertions and exit-code
checks. The three CMD paste checks retain file-backed `Start-Process` input.
The suite reports elapsed time and the number of processes launched by the
new helper. On the same local Windows machine, all 30 smoke groups passed in
297.37 seconds before the change and 145.52 seconds afterwards (about 51%
less elapsed time). These are local measurements, not hosted CI results.

Discovery and installation link ordinary utilities into their isolated PATH
instead of launching an extra Bash wrapper for every utility call. On MSYS,
shortcut links avoid elevation and preserve the executable's DLL location.
Installation retains scripts for failure injection and for utilities moved
or replaced during capability tests; renaming MSYS shortcut links can lose
their link identity. All 56 discovery groups passed locally in 52.97 seconds
with the original wrappers and about 35 seconds with links. Runs overlapped
other verification work, so these timings are indicative rather than a
controlled benchmark.

`tests/ci.sh` reports per-suite and total timings. Windows runs three installation
partitions concurrently, each with independent temporary HOME/XDG roots,
fixtures, logs and cleanup. A fourth worker runs discovery, home/env and the
Bash installer suite in sequence while those partitions run. The harness waits
for every partition and propagates any failure. Unix retains the full serial
installation suite. The partition commands are also usable individually with
the interpreter being tested:

```sh
bash tests/install.sh --contracts
bash tests/install.sh --selection
bash tests/install.sh --archives
```

Contracts cover metadata, ranges, manifests, inventory and uninstall selectors;
selection covers defaults, flags, execution and configuration; archives cover
the remaining installation, extraction and concurrent-publication checks.
`bash tests/install.sh` and `bash tests/run.sh` retain their full serial behavior.
Runtime scripts and product requirements are unchanged.

The complete local Git Bash CI harness passed in 192.56 seconds (about 3:13),
including all 56 discovery groups, the original 35 installation groups split
12/9/14 across the partitions, home/env checks, all 19 Bash installer groups,
native junction smoke and all 14 PowerShell installer groups. Comparing the
installation group descriptions with the preceding hosted run found no missing
or additional groups. Overlapping the other suites reduced the local harness
from the first optimized schedule's 331.10 seconds (5:31). The PowerShell
process helper also captured simultaneous 128 KiB stdout/stderr output and exit
37 without blocking. Bash syntax, PowerShell parsing, LF endings and
`git diff --check` passed. ShellCheck is unavailable. Linux, macOS, Bash 3.2,
MSYS2 and Cygwin validation and same-runner hosted performance measurements
remain pending; no commit, push or workflow dispatch was performed.

## Patch release preparation: 0.2.1 (2026-11-05)

The runtime version constants, PowerShell HTTP User-Agent, version-output
Expand Down
54 changes: 49 additions & 5 deletions tests/ci.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Run with the interpreter being tested, never a compatibility-mode substitute.
set -eu
CI_STARTED=$SECONDS
ROOT=$(cd "$(dirname "$0")/.." && pwd)
case "${1:-}" in
3.2)
Expand All @@ -14,7 +15,19 @@ esac
printf 'Testing %s on %s\n' "$BASH_VERSION" "$(uname -a)"
BASH_BIN=$BASH
TEMP=$(mktemp -d "${TMPDIR:-/tmp}/jvm-ci.XXXXXXXX")
trap 'rm -rf -- "$TEMP"' EXIT
suite_pids=()
cleanup() {
local pid
for pid in "${suite_pids[@]:-}"; do
[ -z "$pid" ] || { kill -TERM "$pid" 2>/dev/null || :; }
done
for pid in "${suite_pids[@]:-}"; do
[ -z "$pid" ] || { wait "$pid" 2>/dev/null || :; }
done
rm -rf -- "$TEMP"
printf 'TIMING: total: %ss\n' "$((SECONDS-CI_STARTED))"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
mkdir "$TEMP/bin"
# Also cover env-bash shebangs and bare `bash` commands with this interpreter.
Expand All @@ -25,14 +38,45 @@ cd "$ROOT"
"$BASH_BIN" -n ./jvm
"$BASH_BIN" -n web/install.sh
for script in tests/*.sh; do "$BASH_BIN" -n "$script"; done
"$BASH_BIN" tests/run.sh
"$BASH_BIN" tests/tool-installer.sh
run_suite() {
local name=$1 started=$SECONDS status=0
shift
"$@" || status=$?
printf 'TIMING: %s: %ss (exit %s)\n' "$name" "$((SECONDS-started))" "$status"
return "$status"
}
case "$(uname -s)" in
CYGWIN*|MSYS*|MINGW*)
"$BASH_BIN" tests/default-native-windows.sh
# Bound concurrency to three independent installation suites. Capture
# separate logs so output and failures cannot interleave or get lost.
suites=(contracts selection archives)
for suite in "${suites[@]}"; do
"$BASH_BIN" tests/install.sh "--$suite" > "$TEMP/$suite.log" 2>&1 &
suite_pids[${#suite_pids[@]}]=$!
done
# A fourth worker runs the remaining independent suites in sequence.
run_suite discovery "$BASH_BIN" tests/run.sh --discovery
run_suite home-env "$BASH_BIN" tests/home-env.sh
run_suite tool-installer "$BASH_BIN" tests/tool-installer.sh
status=0
for index in 0 1 2; do
wait "${suite_pids[$index]}" || status=1
suite_pids[$index]=
cat "$TEMP/${suites[$index]}.log"
done
[ "$status" -eq 0 ] || exit "$status" ;;
*)
run_suite discovery "$BASH_BIN" tests/run.sh --discovery
run_suite installation "$BASH_BIN" tests/install.sh
run_suite home-env "$BASH_BIN" tests/home-env.sh
run_suite tool-installer "$BASH_BIN" tests/tool-installer.sh ;;
esac
case "$(uname -s)" in
CYGWIN*|MSYS*|MINGW*)
run_suite native-junction "$BASH_BIN" tests/default-native-windows.sh
# A restricted Cygwin PATH may contain System32 without its PowerShell
# subdirectory. Include it for the suite's nested PowerShell/CMD calls.
system_directory=$(cygpath -u -S)
export PATH="$PATH:$system_directory/WindowsPowerShell/v1.0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -aw "$ROOT/tests/tool-installer.ps1")" ;;
run_suite powershell-installer powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -aw "$ROOT/tests/tool-installer.ps1")" ;;
esac
35 changes: 31 additions & 4 deletions tests/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ TEMP=$(mktemp -d "${TMPDIR:-/tmp}/jvm-install-tests.XXXXXXXX") || exit 1
# Fixtures and exact home/exec expectations share the manager's physical paths.
# TMPDIR can name an alias such as /var on macOS or a Windows runner mount.
TEMP=$(cd "$TEMP" && pwd -P) || exit 1
SUITE_NAME=${1:-all}
SUITE_STARTED=$SECONDS
cleanup() {
if [ -n "${TEST_PUBLISH_GATE:-}" ]; then
: > "$TEST_PUBLISH_GATE/first.release"
Expand All @@ -22,7 +24,7 @@ cleanup() {
done
rm -rf -- "$TEMP"
}
trap cleanup EXIT
trap 'status=$?; cleanup; printf "TIMING: installation %s: %ss (exit %s)\n" "$SUITE_NAME" "$((SECONDS-SUITE_STARTED))" "$status"; exit "$status"' EXIT
trap 'exit 1' HUP INT TERM
mkdir -p "$TEMP/project with spaces"
cd "$TEMP/project with spaces" || exit 1
Expand All @@ -31,10 +33,18 @@ export XDG_CACHE_HOME="$TEMP/cache with spaces" XDG_CONFIG_HOME="$TEMP/config" X
mkdir -p "$HOME" "$TEMP/bin"
TOOLS=$TEMP/bin
export MSYS=winsymlinks:nativestrict
for tool in ln readlink mkdir mktemp rm rmdir sed awk cat cmp mv date cp sleep wc find tar gzip unzip sha256sum openssl; do
for tool in readlink mkdir mktemp rmdir sed awk cat cmp date cp sleep wc find tar gzip unzip sha256sum openssl; do
path=$(command -v "$tool") || continue
printf '#!%s\nexec "%s" "$@"\n' "$BASH_BIN" "$path" > "$TOOLS/$tool"
chmod +x "$TOOLS/$tool" || exit 1
case "$tool" in
readlink|awk|tar|unzip|sha256sum|openssl)
# These entries are moved/replaced in capability/failure tests.
# MSYS shortcut links lose their link identity when renamed.
printf '#!%s\nexec "%s" "$@"\n' "$BASH_BIN" "$path" > "$TOOLS/$tool"
chmod +x "$TOOLS/$tool" || exit 1 ;;
*)
# Avoid another Bash process for ordinary utility calls.
MSYS=winsymlinks ln -s "$path" "$TOOLS/$tool" || exit 1 ;;
esac
done
printf '#!%s\nprintf "%%s\\n" "$2"\n' "$BASH_BIN" > "$TOOLS/cygpath"
printf '#!%s\ncat > /dev/null\nln -s "$JVM_TARGET" "$JVM_LINK"\n' "$BASH_BIN" > "$TOOLS/cmd.exe"
Expand Down Expand Up @@ -171,6 +181,7 @@ if [ -n "$TEST_DEFAULT_GATE" ] && [ "${3:-}" = "$XDG_DATA_HOME/jvm/jdks/$TEST_DE
fi
exec "$TEST_REAL_LN" "$@"
EOF
chmod +x "$TOOLS/ln" || exit 1
export PATH=$TOOLS
passed=0
fail() { printf 'FAIL: %s\n' "$*" >&2; cat "$TEMP/err" >&2; exit 1; }
Expand Down Expand Up @@ -1396,6 +1407,18 @@ if [ "${1:-}" = --flags ]; then flag_alias_checks; exit 0; fi
if [ "${1:-}" = --execution ]; then execution_checks; exit 0; fi
if [ "${1:-}" = --defaults ]; then default_checks; exit 0; fi

# Independent partitions of the full suite. Each process creates its own
# fixtures/storage; the ordinary no-argument invocation still runs everything.
case "${1:-}" in
--contracts)
ea_checks; type_checks; range_checks; java_major_checks
manifest_checks; inventory_checks; uninstall_checks
exit 0 ;;
--selection)
default_checks; flag_alias_checks; execution_checks; configuration_checks
exit 0 ;;
esac

if [ "${1:-}" != --publication ]; then
run 0 list
[ "$(< "$TEMP/out")" = "No JDKs are installed. Use 'install' to install one" ] || fail 'wrong empty inventory message'
Expand All @@ -1408,6 +1431,7 @@ done
[ ! -e "$XDG_DATA_HOME" ] || fail 'empty listing created data storage'
run 2 list ''; run 2 list ../21; run 2 list extra extra
check 'empty offline inventory succeeds without creating storage; command usage'
if [ "${1:-}" != --archives ]; then
ea_checks
type_checks
range_checks
Expand All @@ -1416,6 +1440,7 @@ manifest_checks
inventory_checks
uninstall_checks
default_checks
fi

index; row 21.0.2+9 vendor; run 0 install vendor-21; diagnostic '21.0.2+9-vendor'
diagnostic 'Successfully installed 21.0.2+9-vendor'; diagnostic 'Installing 21.0.2+9-vendor...'
Expand Down Expand Up @@ -1715,9 +1740,11 @@ run 0 list
contains '23+1-vendor'; contains '(invalid JDK)'
check 'independent staging, race loser success, publication failure and preservation of prior installation/default'

if [ "${1:-}" != --archives ]; then
flag_alias_checks
execution_checks
configuration_checks
fi

saved_data=$XDG_DATA_HOME; XDG_DATA_HOME=relative
index; row 24+1 vendor; run 0 install vendor-24
Expand Down
51 changes: 37 additions & 14 deletions tests/powershell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ $scriptPath = Join-Path $repository 'jvm.ps1'
$testRoot = Join-Path ([IO.Path]::GetTempPath()) ('jvm powershell ' + [guid]::NewGuid().ToString('N'))
$oldEnvironment = @{}
$script:passed = 0
$script:processCount = 0
$suiteTimer = [Diagnostics.Stopwatch]::StartNew()
foreach ($name in @('HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_CONFIG_HOME', 'JVM_TEST_ARGS')) {
$oldEnvironment[$name] = [Environment]::GetEnvironmentVariable($name)
}
Expand All @@ -27,6 +29,34 @@ function Assert-Equal {
}
}

function Invoke-TestProcess {
param([string]$FilePath, [string]$ArgumentText)
# Start-Process -Wait polls the job at one-second intervals in Windows
# PowerShell. Drain both pipes concurrently and wait on the process itself.
$process = New-Object Diagnostics.Process
$process.StartInfo.FileName = $FilePath
$process.StartInfo.Arguments = $ArgumentText
$process.StartInfo.WorkingDirectory = (Get-Location).ProviderPath
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.CreateNoWindow = $true
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
try {
if (-not $process.Start()) { throw "Cannot start test process: $FilePath" }
$script:processCount++
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$process.WaitForExit()
return [pscustomobject]@{
ExitCode = $process.ExitCode
Out = $stdoutTask.GetAwaiter().GetResult()
Err = $stderrTask.GetAwaiter().GetResult()
}
} finally {
$process.Dispose()
}
}

function Invoke-JvmTest {
param([string[]]$Arguments, [int]$ExpectedExitCode = 0, [switch]$Interactive,
[switch]$Redirect, [switch]$Capture, [switch]$Streams)
Expand Down Expand Up @@ -62,12 +92,10 @@ function Invoke-JvmTest {
$quotedArguments = @($allArguments | ForEach-Object {
'"' + ([string]$_).Replace('"', '\"') + '"'
})
$process = Start-Process -FilePath $powershellExe `
-ArgumentList ($quotedArguments -join ' ') -Wait -PassThru -NoNewWindow `
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
$process = Invoke-TestProcess $powershellExe ($quotedArguments -join ' ')
$code = $process.ExitCode
$outText = [IO.File]::ReadAllText($stdout)
$errText = [IO.File]::ReadAllText($stderr)
$outText = $process.Out
$errText = $process.Err
if ($Redirect -or $Capture) {
Assert-Equal $outText '' 'Output bypassed PowerShell stdout redirection.'
Assert-Equal $errText '' 'Output bypassed PowerShell stderr redirection.'
Expand All @@ -76,7 +104,6 @@ function Invoke-JvmTest {
Remove-Item -LiteralPath $redirectOut, $redirectErr -Force
}
$output = $outText + $errText
Remove-Item -LiteralPath $stdout, $stderr -Force
if ($code -ne $ExpectedExitCode) {
throw "jvm.ps1 $($Arguments -join ' ') exited $code; expected $ExpectedExitCode.`n$output"
}
Expand Down Expand Up @@ -106,15 +133,10 @@ function Assert-DefaultUsage {

function Invoke-JvmLauncherTest {
param([string]$ArgumentText, [int]$ExpectedExitCode = 0)
$stdout = Join-Path $testRoot ([guid]::NewGuid().ToString('N') + '.out')
$stderr = Join-Path $testRoot ([guid]::NewGuid().ToString('N') + '.err')
$command = '/d /s /c ""' + $launcherPath + '" ' + $ArgumentText + '"'
$process = Start-Process -FilePath (Join-Path ([Environment]::SystemDirectory) 'cmd.exe') `
-ArgumentList $command -Wait -PassThru -NoNewWindow `
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
$outText = [IO.File]::ReadAllText($stdout).TrimEnd()
$errText = [IO.File]::ReadAllText($stderr).TrimEnd()
Remove-Item -LiteralPath $stdout, $stderr -Force
$process = Invoke-TestProcess (Join-Path ([Environment]::SystemDirectory) 'cmd.exe') $command
$outText = $process.Out.TrimEnd()
$errText = $process.Err.TrimEnd()
Assert-Equal ([string]$process.ExitCode) ([string]$ExpectedExitCode) `
"jvm.cmd $ArgumentText returned the wrong exit code. $outText $errText"
return [pscustomobject]@{ Out = $outText; Err = $errText }
Expand Down Expand Up @@ -624,6 +646,7 @@ try {

[Console]::Out.WriteLine("PASS: $script:passed PowerShell smoke checks (offline; synthetic JDKs) using $PowerShellExecutable.")
} finally {
[Console]::Out.WriteLine(('PowerShell suite: {0:N2}s, {1} test processes' -f $suiteTimer.Elapsed.TotalSeconds, $script:processCount))
if ((Get-Location).Path -eq $testRoot) { Pop-Location }
foreach ($name in $oldEnvironment.Keys) {
[Environment]::SetEnvironmentVariable($name, $oldEnvironment[$name])
Expand Down
5 changes: 3 additions & 2 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ mkdir -p "$HOME" "$TEMP/bin"
TOOLS="$TEMP/bin"
for tool in mkdir mktemp rm sed awk cat mv date cp; do
path=$(command -v "$tool") || exit 1
printf '#!%s\nexec "%s" "$@"\n' "$BASH_BIN" "$path" > "$TOOLS/$tool"
chmod +x "$TOOLS/$tool" || exit 1
# MSYS shortcut links work without elevation and retain the executable's
# original DLL location. Do not add an interpreter to every utility call.
MSYS=winsymlinks ln -s "$path" "$TOOLS/$tool" || exit 1
done
export PATH=$TOOLS
export TEST_SYSTEM=Linux TEST_MACHINE=x86_64 TEST_LIBC=glibc
Expand Down
Loading