diff --git a/tests/README.md b/tests/README.md index 50f7d41..0b937df 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 diff --git a/tests/ci.sh b/tests/ci.sh index 284146f..1507f50 100644 --- a/tests/ci.sh +++ b/tests/ci.sh @@ -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) @@ -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. @@ -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 diff --git a/tests/install.sh b/tests/install.sh index ba71018..7fbfd5d 100644 --- a/tests/install.sh +++ b/tests/install.sh @@ -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" @@ -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 @@ -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" @@ -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; } @@ -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' @@ -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 @@ -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...' @@ -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 diff --git a/tests/powershell.ps1 b/tests/powershell.ps1 index b280580..40c8632 100644 --- a/tests/powershell.ps1 +++ b/tests/powershell.ps1 @@ -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) } @@ -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) @@ -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.' @@ -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" } @@ -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 } @@ -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]) diff --git a/tests/run.sh b/tests/run.sh index 332d5ea..88b2ca7 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -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