
Implement Unit Testing Script
- 39 installs
- 54 repo stars
- Updated July 22, 2026
- codeplain-ai/plain-forge
Generate a Bash or PowerShell unit-test runner script for a new language in a ***plain project, following the reference pattern.
About
Generates a language-agnostic unit-test runner script (Bash or PowerShell) for a ***plain build folder. A developer uses it to add a unit-testing script for a new language to a ***plain project.
- Generates a Bash or PowerShell unit-test runner per language
- Follows the same seven-step pattern as the bundled reference scripts
Implement Unit Testing Script by the numbers
- 39 all-time installs (skills.sh)
- Ranked #1,290 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/codeplain-ai/plain-forge --skill implement-unit-testing-scriptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 54 |
| Last updated | July 22, 2026 |
| Repository | codeplain-ai/plain-forge ↗ |
What it does
Generate a Bash or PowerShell unit-test runner script for a new language in a ***plain project, following the reference pattern.
Files
Implement Unit Testing Script
This skill produces a single executable script that runs the unit tests for a generated build folder, following a consistent, language-agnostic pattern.
The reference implementation is assets/run_unittests_java.sh. Read it first — every script you produce must be a faithful translation of that pattern into the target language's tooling and the user's shell environment. There are also Windows PowerShell equivalents of these scripts in assets/run_unittests_*.ps1.
Pick the Shell First
Before writing anything, decide which shell flavor the script must target — it depends on the user's environment, not on the language:
- Bash (`.sh`) — macOS, Linux, WSL, CI runners on Linux. Default unless the user is on native Windows.
- PowerShell (`.ps1`) — native Windows / PowerShell-only environments.
If you can't tell from the project (no obvious OS hints, no existing scripts), ask the user.
The same seven-step pattern applies to both. Only the syntax changes.
The Pattern
Every testing script must implement these steps in this order:
1. Toolchain check. Verify that the required language runtime / build tool (and the required version, if any) is installed. If not, print an error and exit with code 69. 2. Argument validation. Require exactly one positional argument: the source build folder name. If missing, print usage and exit with code 1. 3. Working directory setup. Define a working folder at .tmp/<lang>_<arg>. If it exists, wipe its contents; otherwise create it. This folder — and only this folder — is where every subsequent write must land. 4. Copy the build. Recursively copy everything from the source folder into the working folder. After this step the source folder ($1) is treated as read-only for the rest of the script. 5. Enter the working directory. cd / Set-Location into .tmp/<lang>_<arg>. If that fails, exit with code 2. All remaining steps run from inside the working folder; they must never write back to the source build folder. 6. Install dependencies into an isolated environment inside `.tmp/<lang>_<arg>`. Set up a per-working-folder dependency location (a Python venv at ./.venv, a local ./node_modules, a project-scoped Maven repo at ./.m2, etc.) and install/resolve all dependencies into it. Never install into the source build folder, the user's global cache (~/.m2, system-wide pip, ~/.cargo, ~/.npm, ...), or anywhere outside .tmp/<lang>_<arg>. If the install command fails, propagate its exit code immediately and do not proceed to step 7. See Dependency isolation for per-language specifics. 7. Run the tests. Invoke the language's standard test command (e.g. mvn test, pytest, npm test, go test ./..., cargo test), pointed at the same isolated environment from step 6. The script's final exit code is whatever the test command returns.
The build folder is read-only — hard rule
The source build folder passed in as $1 is input only. The script must never:
- install dependencies into it (no
pip installinside$1, nonpm installinside$1, nomvn installwriting into$1, no Cargo build artifacts ending up under$1), - write a virtualenv /
node_modules/.m2/.gocache/.cargodirectory inside it, - run the test command from inside it (every test command runs from inside
.tmp/<lang>_<arg>after thecdin step 5), - create logs, caches, build outputs, or temp files inside it.
The build folder is shared with the renderer (plain_modules/... by default) and downstream tooling. Writing into it corrupts the renderer's view of "what was generated" and breaks subsequent renders. Every write must go into .tmp/<lang>_<arg> — the whole point of staging via .tmp is so the source build folder stays a clean, reproducible artifact of the render.
If you find yourself about to issue any command whose cwd is the source folder, or whose target path starts with $1/, stop. Either move the operation into .tmp/<lang>_<arg>, or you're doing something the script must not do.
Conventions
Shared across both shell flavors:
- Exit codes:
1— bad usage (missing argument).2— filesystem problem (couldn't enter the working folder).69— required toolchain / runtime is not installed.- Any other non-zero code — propagated from the underlying test command.
- Working folder naming:
.tmp/<lang>_<arg>where<lang>is a short identifier for the language (java,python,node,go,rust, ...). All dependency installs, build outputs, caches, and the test run itself live inside this folder. Nothing the script does should touch the source build folder after step 4. - Logging: print short progress lines (
"Copied from ... to ...","Installing dependencies into ...","Running <lang> unittests in ...") so failures are easy to triage.
Dependency isolation
The dependency environment must live inside $WORKING_FOLDER so the test run can't be polluted by — or pollute — the user's global caches. Pick the most idiomatic isolation mechanism for the language:
| Language | Isolation mechanism | Install command (run inside $WORKING_FOLDER) | Test command |
|---|---|---|---|
| Python | venv at ./.venv | python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt (or pyproject.toml / uv sync / poetry install) | ./.venv/bin/pytest (or ./.venv/bin/python -m pytest) |
| Node.js | local ./node_modules (default) | npm ci (preferred) or npm install | npm test |
| Java | project-scoped Maven repo at ./.m2 | mvn -Dmaven.repo.local=./.m2 dependency:resolve (optional pre-warm) | mvn -Dmaven.repo.local=./.m2 test |
| Go | module cache at ./.gocache | GOMODCACHE="$PWD/.gocache" go mod download (optional pre-warm) | GOMODCACHE="$PWD/.gocache" go test ./... |
| Rust | cargo home at ./.cargo | CARGO_HOME="$PWD/.cargo" cargo fetch (optional pre-warm) | CARGO_HOME="$PWD/.cargo" cargo test |
Notes:
- Every path in the install command and test command is relative to `.tmp/<lang>_<arg>`. That's why the script
cds into the working folder in step 5 — from that point on,./.venv,./node_modules,./.m2, etc. all resolve under.tmp/<lang>_<arg>, never under the source build folder. - Always pass the isolation flag/env var to both the install command and the test command — they must agree on where deps live, otherwise the test command will silently fall back to the global cache or (worse) the source build folder.
- Python is the only ecosystem where the venv is mandatory to satisfy "into a virtual environment" literally. The others use language-native equivalents that achieve the same isolation.
- Pre-warming is optional for Java/Go/Rust — their test commands will fetch deps on demand. Doing it as a separate step makes failures easier to diagnose and gives a clean "install failed vs test failed" signal.
- Don't activate the venv in Bash via
source .venv/bin/activate— call./.venv/bin/<tool>directly. It's more portable and avoids subshell weirdness. In PowerShell, use& .\.venv\Scripts\<tool>.exesimilarly. - Propagate the install exit code immediately. In Bash:
<install cmd> || exit $?. In PowerShell: check$LASTEXITCODEandexit $LASTEXITCODEif non-zero.
Bash specifics
- Shebang:
#!/bin/bash. - File naming:
run_unittests_<lang>.sh, placed inassets/. - Argument:
$1. - Make it executable:
chmod +x assets/run_unittests_<lang>.sh.
PowerShell specifics
- No shebang. Use a
param([Parameter(Mandatory=$true)][string]$Subfolder)block at the top instead. - File naming:
run_unittests_<lang>.ps1, placed inassets/. - Exit codes: use
exit 69etc. (PowerShell honors them just like Bash). - Toolchain check: prefer
Get-Command <tool> -ErrorAction SilentlyContinueand, where a specific version is needed, parse the tool's--versionoutput. - Filesystem: use
Test-Path,Remove-Item -Recurse -Force,New-Item -ItemType Directory,Copy-Item -Recurse,Set-Location. Quote paths to handle spaces. - No `chmod` step needed. If execution policy is likely to block the script, mention
Set-ExecutionPolicy -Scope CurrentUser RemoteSignedto the user — don't bake it into the script.
Workflow
1. Confirm the target language, shell flavor (Bash or PowerShell), and dependency manifest (pom.xml, requirements.txt / pyproject.toml, package.json, go.mod, Cargo.toml, ...). Ask if any is unclear. 2. Read assets/run_unittests_java.sh to refresh the exact structure. 3. Translate each of the seven steps above into the equivalent commands for the target language and shell. The toolchain check, dependency install, and test invocation are the language-specific parts; the rest is mechanical translation between Bash and PowerShell syntax. 4. Pick the dependency-isolation mechanism from the Dependency isolation table and use it consistently in both step 6 and step 7. 5. Save the new script to assets/run_unittests_<lang>.sh or assets/run_unittests_<lang>.ps1. For Bash, chmod +x it.
Anti-Patterns
- (Hard mistake) Don't install into, build into, or otherwise write to the source build folder. The build folder passed as
$1is read-only input. Every install, cache, build artifact, log, and temp file must land in.tmp/<lang>_<arg>. This includes never runningpip install,npm install,mvn install, orcargo buildwith the source folder as theircwdor target, never letting a venv /node_modules/.m2/.gocache/.cargodirectory appear inside the source folder, and never running the test command from inside it. The whole point of staging the build into.tmp/is so the source folder remains a clean, reproducible artifact of the render — writing to it corrupts the renderer's view and breaks subsequent renders. - Don't skip the toolchain check, even when "everyone has it installed" — exit code
69is what the calling system relies on to detect a missing runtime. - Don't reuse the source folder in place. Always copy into
.tmp/<lang>_<arg>first; the renderer relies on this isolation. - Don't change the exit-code contract. Other parts of the system branch on
1,2, and69specifically — and these codes must be identical between the Bash and PowerShell variants. - Don't write a cross-shell hybrid (e.g. a
.shthat detects PowerShell, or vice versa). Ship one script per shell, named with the appropriate extension. - Don't install dependencies into the user's global location (
~/.m2, system-widepip,~/.cargo, etc.). Always isolate inside$WORKING_FOLDERso concurrent runs and other projects can't interfere. - Don't run the test command without first verifying the install step succeeded. A failed install followed by a "test" run produces misleading errors that look like test failures.
#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'
$UNRECOVERABLE_ERROR_EXIT_CODE = 69
if (-not $args[0]) {
Write-Host "Error: No source folder name provided."
Write-Host "Usage: $($MyInvocation.MyCommand.Name) <source_folder_name>"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
if (-not (Get-Command flutter -ErrorAction SilentlyContinue)) {
Write-Host "Error: flutter is not available in PATH."
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
$SOURCE_FOLDER = $args[0]
$BUILD_SUBFOLDER = ".tmp/flutter_build_unittests"
Write-Host "Current directory: $(Get-Location)"
Write-Host "Source folder: $SOURCE_FOLDER"
Write-Host "--------------------------------"
if (Test-Path $BUILD_SUBFOLDER) {
Remove-Item -Path $BUILD_SUBFOLDER -Recurse -Force
}
New-Item -ItemType Directory -Path $BUILD_SUBFOLDER -Force | Out-Null
Copy-Item -Path "$SOURCE_FOLDER/*" -Destination "$BUILD_SUBFOLDER/" -Recurse -Force
if (-not (Test-Path $BUILD_SUBFOLDER)) {
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
Push-Location $BUILD_SUBFOLDER
try {
Write-Host "Resolving Flutter dependencies..."
if (Test-Path "pubspec.yaml") {
flutter pub get
} else {
Write-Host "Warning: pubspec.yaml not found. Dependencies might be missing."
}
Write-Host "Running Flutter unittests in $BUILD_SUBFOLDER..."
# Run flutter test with a timeout
$process = Start-Process -FilePath "flutter" -ArgumentList "test", "--reporter", "expanded" `
-NoNewWindow -PassThru -RedirectStandardOutput "flutter_test_stdout.txt" -RedirectStandardError "flutter_test_stderr.txt"
$timedOut = $false
if (-not $process.WaitForExit(120000)) {
$timedOut = $true
$process | Stop-Process -Force
}
$output = ""
if (Test-Path "flutter_test_stdout.txt") {
$output += Get-Content "flutter_test_stdout.txt" -Raw -ErrorAction SilentlyContinue
}
if (Test-Path "flutter_test_stderr.txt") {
$output += Get-Content "flutter_test_stderr.txt" -Raw -ErrorAction SilentlyContinue
}
$exit_code = $process.ExitCode
if ($timedOut) {
Write-Host "`nError: Unittests timed out after 120 seconds."
exit 124
}
Write-Host $output
exit $exit_code
} finally {
# Clean up temp files
Remove-Item -Path "flutter_test_stdout.txt" -ErrorAction SilentlyContinue
Remove-Item -Path "flutter_test_stderr.txt" -ErrorAction SilentlyContinue
Pop-Location
}
#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'
$UNRECOVERABLE_ERROR_EXIT_CODE = 69
# Check if subfolder name is provided
if (-not $args[0]) {
Write-Host "Error: No subfolder name provided."
Write-Host "Usage: $($MyInvocation.MyCommand.Name) <subfolder_name>"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
$BuildFolder = $args[0]
$GO_BUILD_SUBFOLDER = "go_$BuildFolder"
if ($env:VERBOSE -eq "1") {
Write-Host "Preparing Go build subfolder: $GO_BUILD_SUBFOLDER"
}
# Check if the go build subfolder exists
if (Test-Path $GO_BUILD_SUBFOLDER) {
# Delete all files and folders inside
Get-ChildItem -Path $GO_BUILD_SUBFOLDER -Force | Remove-Item -Recurse -Force
if ($env:VERBOSE -eq "1") {
Write-Host "Cleanup completed."
}
} else {
if ($env:VERBOSE -eq "1") {
Write-Host "Subfolder does not exist. Creating it..."
}
New-Item -ItemType Directory -Path $GO_BUILD_SUBFOLDER -Force | Out-Null
}
Copy-Item -Path "$BuildFolder/*" -Destination $GO_BUILD_SUBFOLDER -Recurse -Force
# Move to the subfolder
if (-not (Test-Path $GO_BUILD_SUBFOLDER)) {
Write-Host "Error: Go build folder '$GO_BUILD_SUBFOLDER' does not exist."
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
Push-Location $GO_BUILD_SUBFOLDER
try {
Write-Host "Running go get..."
# Temporarily allow stderr output without throwing (Go tools write to stderr)
# ForEach-Object converts ErrorRecord objects (from stderr) to plain strings to avoid verbose error formatting
$ErrorActionPreference = 'Continue'
$output = go get 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
$ErrorActionPreference = 'Stop'
if ($output.Trim()) { Write-Host $output }
# Execute all Golang unittests in the subfolder
Write-Host "Running Golang unittests in $BuildFolder..."
$ErrorActionPreference = 'Continue'
$output = go test 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
$exit_code = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
Write-Host $output
exit $exit_code
} finally {
Pop-Location
}
#!/bin/bash
# Check that Java 21 is installed
if ! /usr/libexec/java_home -v 21 >/dev/null 2>&1; then
echo "Error: Java 21 is not installed."
exit 69
fi
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
java --version
# Check if subfolder name is provided
if [ -z "$1" ]; then
echo "Error: No subfolder name provided."
echo "Usage: $0 <subfolder_name>"
exit 1
fi
# Define the path to the java build subfolder
WORKING_FOLDER=.tmp/$1
# Check if the java subfolder exists
if [ -d "$WORKING_FOLDER" ]; then
# delete everything in the subfolder
rm -rf "$WORKING_FOLDER"/*
else
echo "Subfolder '$WORKING_FOLDER' does not exist. Creating it now..."
mkdir -p "$WORKING_FOLDER"
fi
# copy all folders and files from the build folder to the subfolder
cp -R $1/* $WORKING_FOLDER
printf "Copied from $1 to $WORKING_FOLDER...\n"
# Move to the subfolder
cd "$WORKING_FOLDER" 2>/dev/null
printf "Moved to $WORKING_FOLDER...\n"
if [ $? -ne 0 ]; then
echo "Error: Subfolder '$1' does not exist."
exit 2
fi
# Execute all Java unittests in the subfolder
echo "Running Java unittests in $(pwd)..."
mvn test#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'
$UNRECOVERABLE_ERROR_EXIT_CODE = 69
# Check if subfolder name is provided
if (-not $args[0]) {
Write-Host "Error: No subfolder name provided."
Write-Host "Usage: $($MyInvocation.MyCommand.Name) <subfolder_name>"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
$BuildFolder = $args[0]
# Try to find Python interpreter (python3 first, then python)
if (Get-Command python3 -ErrorAction SilentlyContinue) {
$PYTHON_CMD = "python3"
} elseif (Get-Command python -ErrorAction SilentlyContinue) {
$PYTHON_CMD = "python"
} else {
Write-Host "Error: Python interpreter not found. Please install Python."
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
$PYTHON_BUILD_SUBFOLDER = "python_$BuildFolder"
if ($env:VERBOSE -eq "1") {
Write-Host "Preparing Python build subfolder: $PYTHON_BUILD_SUBFOLDER"
}
# Check if the Python build subfolder exists
if (Test-Path $PYTHON_BUILD_SUBFOLDER) {
# Delete all files and folders inside
Get-ChildItem -Path $PYTHON_BUILD_SUBFOLDER -Force | Remove-Item -Recurse -Force
if ($env:VERBOSE -eq "1") {
Write-Host "Cleanup completed."
}
} else {
if ($env:VERBOSE -eq "1") {
Write-Host "Subfolder does not exist. Creating it..."
}
New-Item -ItemType Directory -Path $PYTHON_BUILD_SUBFOLDER -Force | Out-Null
}
Copy-Item -Path "$BuildFolder/*" -Destination $PYTHON_BUILD_SUBFOLDER -Recurse -Force
# Move to the subfolder
if (-not (Test-Path $PYTHON_BUILD_SUBFOLDER)) {
Write-Host "Error: Python build folder '$PYTHON_BUILD_SUBFOLDER' does not exist."
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
Push-Location $PYTHON_BUILD_SUBFOLDER
try {
# Execute all Python unittests in the subfolder
Write-Host "Running Python unittests in $PYTHON_BUILD_SUBFOLDER..."
# Temporarily allow stderr output without throwing (Python unittest writes progress to stderr)
# ForEach-Object converts ErrorRecord objects (from stderr) to plain strings to avoid verbose error formatting
$ErrorActionPreference = 'Continue'
$output = & $PYTHON_CMD -m unittest discover -b 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
$exit_code = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
# Echo the original output
Write-Host $output
# Return the exit code of the unittest command
exit $exit_code
} finally {
Pop-Location
}
#!/bin/bash
UNRECOVERABLE_ERROR_EXIT_CODE=69
# Check if subfolder name is provided
if [ -z "$1" ]; then
echo "Error: No subfolder name provided."
echo "Usage: $0 <subfolder_name>"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
fi
current_dir=$(pwd)
echo "Current directory: $current_dir"
echo "Build folder name: $1"
echo "--------------------------------"
PYTHON_BUILD_SUBFOLDER=.tmp/$1
if [ "${VERBOSE:-}" -eq 1 ] 2>/dev/null; then
printf "Preparing Python build subfolder: $PYTHON_BUILD_SUBFOLDER\n"
fi
rm -rf $PYTHON_BUILD_SUBFOLDER
mkdir -p $PYTHON_BUILD_SUBFOLDER
cp -R $1/* $PYTHON_BUILD_SUBFOLDER
# Move to the subfolder
cd "$PYTHON_BUILD_SUBFOLDER" 2>/dev/null
if [ $? -ne 0 ]; then
printf "Error: Python build folder '$PYTHON_BUILD_SUBFOLDER' does not exist.\n"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
fi
printf "Creating and activating virtual environment...\n"
# Time the virtual environment creation and activation
start_time=$(date +%s.%N)
VENV_DIR=".venv"
if ! $PYTHON_CMD -m venv "$VENV_DIR"; then
printf "Error: Failed to create virtual environment in '$VENV_DIR'.\n"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
fi
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
if [ $? -ne 0 ]; then
printf "Error: Failed to activate virtual environment at '$VENV_DIR/bin/activate'.\n"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
fi
# Install requirements if requirements.txt exists
if [ -f "requirements.txt" ]; then
pip install --upgrade pip
pip install -r requirements.txt
else
echo "Error: requirements.txt not found. Cannot proceed with setting up requirements."
fi
end_time=$(date +%s.%N)
# Calculate and display the time taken
duration=$(echo "$end_time - $start_time" | bc)
printf "Requirements setup completed in %.2f seconds\n\n" "$duration"
# Execute all Python unittests in the subfolder
echo "Running Python unittests in $PYTHON_BUILD_SUBFOLDER..."
output=$(timeout 120s python -m unittest discover -b -v 2>&1)
exit_code=$?
# Check if the command timed out
if [ $exit_code -eq 124 ]; then
printf "\nError: Unittests timed out after 120 seconds.\n"
exit $exit_code
fi
# Echo the original output
echo "$output"
# Return the exit code of the unittest command
exit $exit_code
# Note: The 'discover' option automatically identifies and runs all unittests in the current directory and subdirectories
# Ensure that your Python files are named according to the unittest discovery pattern (test*.py by default)#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'
$UNRECOVERABLE_ERROR_EXIT_CODE = 69
# ANSI escape code pattern to remove color codes and formatting from output
$ANSI_ESCAPE_PATTERN = '\x1b\[[0-9;]*[mK]'
# Check if subfolder name is provided
if (-not $args[0]) {
Write-Host "Error: No subfolder name provided."
Write-Host "Usage: $($MyInvocation.MyCommand.Name) <subfolder_name>"
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
$BuildFolder = $args[0]
# Define the path to the subfolder
$NODE_SUBFOLDER = "node_$BuildFolder"
if ($env:VERBOSE -eq "1") {
Write-Host "Preparing Node subfolder: $NODE_SUBFOLDER"
}
# Check if the node subfolder exists
if (Test-Path $NODE_SUBFOLDER) {
# Delete all files and folders except "node_modules", "build", and "package-lock.json"
Get-ChildItem -Path $NODE_SUBFOLDER -Force |
Where-Object {
$_.Name -ne "node_modules" -and
$_.Name -ne "build" -and
$_.Name -ne "package-lock.json"
} | Remove-Item -Recurse -Force
if ($env:VERBOSE -eq "1") {
Write-Host "Cleanup completed, keeping 'node_modules' and 'package-lock.json'."
}
} else {
if ($env:VERBOSE -eq "1") {
Write-Host "Subfolder does not exist. Creating it..."
}
New-Item -ItemType Directory -Path $NODE_SUBFOLDER -Force | Out-Null
}
Copy-Item -Path "$BuildFolder/*" -Destination $NODE_SUBFOLDER -Recurse -Force
# Move to the subfolder
if (-not (Test-Path $NODE_SUBFOLDER)) {
Write-Host "Error: Subfolder '$BuildFolder' does not exist."
exit $UNRECOVERABLE_ERROR_EXIT_CODE
}
Push-Location $NODE_SUBFOLDER
try {
# Install libraries
npm install
# Execute all React unittests in the subfolder
Write-Host "Running React unittests in $BuildFolder..."
# Temporarily allow stderr output without throwing (npm/jest may write to stderr)
# ForEach-Object converts ErrorRecord objects (from stderr) to plain strings to avoid verbose error formatting
$ErrorActionPreference = 'Continue'
$output = npm test -- --runInBand --silent --detectOpenHandles 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
$TEST_EXIT_CODE = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
# Strip ANSI escape codes
$output = $output -replace $ANSI_ESCAPE_PATTERN, ''
Write-Host $output
# Check if tests failed
if ($TEST_EXIT_CODE -ne 0) {
Write-Host "Error: Tests failed with exit code $TEST_EXIT_CODE"
exit $TEST_EXIT_CODE
}
exit $TEST_EXIT_CODE
} finally {
Pop-Location
}