diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c1ea63..77d6679 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: persist-credentials: false - name: ShellCheck - run: shellcheck -x git-extensions.sh git-release-api.sh git-release-style.sh git-release-langpack.sh lib/github-release.sh tests/test-release-scripts.sh tests/test-release-api.sh phpbb-test-board/setup-board.sh phpbb-test-board/run-unit-tests.sh tests/test-phpbb-test-board.sh + run: shellcheck -x git-extensions.sh git-release-api.sh git-release-style.sh git-release-langpack.sh lib/github-release.sh tests/test-release-scripts.sh tests/test-release-api.sh phpbb-test-board/setup-board.sh phpbb-test-board/run-unit-tests.sh phpbb-test-board/php-lint.sh tests/test-phpbb-test-board.sh - name: Tests run: tests/test-release-scripts.sh diff --git a/phpbb-test-board/README.md b/phpbb-test-board/README.md index 4aea136..689bb9f 100644 --- a/phpbb-test-board/README.md +++ b/phpbb-test-board/README.md @@ -11,6 +11,7 @@ They build a board on SQLite and run an extension on it with PHP error logging o - `git`, `php` (with the `sqlite3` extension), `composer`, `curl`, `jq` and `openssl` - Python 3 with `venv` - ImageMagick's `convert`, for the reassignthumbs feature check +- Docker, for `php-lint.sh` The Python scripts create their own virtualenv (`.venv` in this folder) on first run and install `requirements.txt` into it; nothing needs installing or activating by hand. `screenshots.py` also downloads Playwright's Chromium on its first run. @@ -57,6 +58,16 @@ Runs the extension's `phpunit.xml.dist` the way [phpbb-extensions/test-framework Exit status: PHPUnit's, 1 for a setup failure, 2 for bad arguments. Needs `git`, `php`, `composer`, `rsync` and `jq`. +### `php-lint.sh`: syntax check on other PHP versions + +```bash +phpbb-test-board/php-lint.sh path/to/extension [-p 7.4,8.4] [--strict] +``` + +Runs `php -l` on every PHP file under each PHP version (default 7.4 and 8.4), in a throwaway container from the official `php:VERSION-cli` image, so an extension can be checked against its minimum PHP without that PHP installed. The folder is mounted read-only, the container has no network and is removed afterwards; `vendor/`, `node_modules/` and `.git/` are skipped. Compile-time deprecation notices, such as implicitly nullable parameters on PHP 8.4, are shown, and fail the run only with `--strict`. It needs no board. + +Exit status: 0 when every file passes on every version, 1 when a file fails or Docker fails, 2 for bad arguments. The first run of a version pulls its image. + ### `smoke_test.py`: does the extension break anything? ```bash diff --git a/phpbb-test-board/php-lint.sh b/phpbb-test-board/php-lint.sh new file mode 100755 index 0000000..52fefc0 --- /dev/null +++ b/phpbb-test-board/php-lint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/bash +# php-lint.sh +# +# Lints every PHP file of an extension (or any folder) with `php -l` under +# one or more PHP versions, each in a throwaway Docker container from the +# official php:VERSION-cli image. This checks an extension against its +# minimum PHP version (7.4 for phpbbmodders extensions) without that PHP +# being installed on this machine. +# +# The folder is mounted read-only, the container has no network and is +# removed when it exits. vendor/, node_modules/ and .git/ are skipped. +# Compile-time deprecation notices (for example implicitly nullable +# parameters on PHP 8.4) are shown, and fail the run only with --strict. +# +# Usage: php-lint.sh [-p VERSIONS] [-s] DIR +# Outputs: one line per PHP version with the file count and result, plus the +# message for every file that fails, on stdout +# Exit status: 0 when every file passes on every version, 1 when a file +# fails or Docker fails, 2 for bad arguments. + +set -Eeuo pipefail + +usage() { + cat <<'EOF' +Usage: php-lint.sh [options] DIR + +Lint every PHP file in DIR with `php -l` under each chosen PHP version, in a +throwaway Docker container (official php:VERSION-cli image, DIR mounted +read-only, no network). vendor/, node_modules/ and .git/ are skipped. + +Arguments: + DIR the extension's checkout, or any folder of PHP files + +Options: + -p, --php VERSIONS comma-separated PHP versions (default: 7.4,8.4) + -s, --strict also fail on compile-time deprecation notices + -h, --help show this help and exit + +Examples: + php-lint.sh ~/repos/contactadmin + php-lint.sh -p 7.4 ~/repos/contactadmin + php-lint.sh -p 7.4,8.2,8.4 --strict ~/repos/phpbb-ext-wiki +EOF +} + +die() { + echo "Error: $*" >&2 + exit 1 +} + +usage_error() { + echo "Error: $*" >&2 + echo "Run with --help for usage." >&2 + exit 2 +} + +versions="7.4,8.4" +strict=0 +dir="" + +if [ "$#" -eq 0 ]; then + usage + exit 0 +fi + +while [ "$#" -gt 0 ]; do + case "$1" in + -p|--php) + [ "$#" -ge 2 ] || usage_error "$1 needs a value" + versions="$2" + shift 2 + ;; + -s|--strict) + strict=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + usage_error "unknown option: $1" + ;; + *) + [ -z "$dir" ] || usage_error "only one DIR may be given" + dir="$1" + shift + ;; + esac +done + +[ -n "$dir" ] || usage_error "DIR is required" +[ -d "$dir" ] || usage_error "not a directory: $dir" +dir="$(cd "$dir" && pwd)" + +# Validate every version before starting a container: MAJOR.MINOR only, so +# the image tag can't be anything but an official php:X.Y-cli image. +IFS=',' read -r -a version_list <<<"$versions" +[ "${#version_list[@]}" -gt 0 ] || usage_error "--php needs at least one version" +for version in "${version_list[@]}"; do + [[ "$version" =~ ^[0-9]+\.[0-9]+$ ]] || usage_error "bad PHP version: '$version' (expected MAJOR.MINOR, e.g. 7.4)" +done + +command -v docker >/dev/null 2>&1 || die "docker is required but not installed" +docker info >/dev/null 2>&1 || die "cannot reach the Docker daemon (is it running, and may this user use it?)" + +# Runs inside the container: lints each file, prints "FAIL " with the +# message for a parse error, "DEPRECATED " with the notice for a +# compile-time deprecation, then a "TOTAL " line. +# shellcheck disable=SC2016 # expanded by the container's shell, not this one +lint_script=' +total=0; failed=0; deprecated=0 +while IFS= read -r -d "" file; do + total=$((total + 1)) + if ! out="$(php -d display_errors=stderr -d error_reporting=-1 -l "$file" 2>&1)"; then + failed=$((failed + 1)) + printf "FAIL %s\n%s\n" "${file#/src/}" "$out" + elif printf "%s" "$out" | grep -q "Deprecated:"; then + deprecated=$((deprecated + 1)) + printf "DEPRECATED %s\n%s\n" "${file#/src/}" "$(printf "%s" "$out" | grep "Deprecated:")" + fi +done < <(find /src \( -name vendor -o -name node_modules -o -name .git \) -prune -o -type f -name "*.php" -print0 | sort -z) +printf "TOTAL %s %s %s\n" "$total" "$failed" "$deprecated" +' + +status=0 +for version in "${version_list[@]}"; do + image="php:${version}-cli" + if ! output="$(docker run --rm --network none --read-only \ + -v "$dir:/src:ro" "$image" bash -c "$lint_script" 2>&1)"; then + echo "$output" >&2 + die "the PHP $version container failed (could the image $image not be pulled?)" + fi + + summary="$(grep '^TOTAL ' <<<"$output" | tail -n 1)" || die "no result from the PHP $version container" + read -r _ total failed deprecated <<<"$summary" + grep -v '^TOTAL ' <<<"$output" || true + + result="ok" + if [ "$failed" -gt 0 ]; then + result="FAIL ($failed with errors)" + status=1 + elif [ "$deprecated" -gt 0 ]; then + result="ok, $deprecated with deprecation notices" + if [ "$strict" -eq 1 ]; then + result="FAIL ($deprecated with deprecation notices, --strict)" + status=1 + fi + fi + echo "PHP $version: $total files, $result" +done + +exit "$status" diff --git a/tests/test-phpbb-test-board.sh b/tests/test-phpbb-test-board.sh index 7c988b4..f077707 100755 --- a/tests/test-phpbb-test-board.sh +++ b/tests/test-phpbb-test-board.sh @@ -1,7 +1,7 @@ #!/usr/bin/bash # Option-handling tests for the phpbb-test-board tools: help output, exit # status for bad arguments, and the error for a missing board. Does not build -# a board or contact phpBB, and run-unit-tests.sh is only given bad arguments; +# a board or contact phpBB, and run-unit-tests.sh and php-lint.sh are only given bad arguments; # the Python scripts' first run creates their virtualenv # (phpbb-test-board/.venv), which needs network access once. # Usage: tests/test-phpbb-test-board.sh @@ -142,6 +142,17 @@ check "run-unit-tests.sh with two directories exits 2" \ check "run-unit-tests.sh rejects a composer name that is not vendor/name" \ output_has "composer.json has no valid vendor/name" "$tools/run-unit-tests.sh" -e "$tmp/no-env" "$tmp/ext-bad-name" +# php-lint.sh: option handling only; every case exits before Docker is used +check "php-lint.sh without arguments shows help and exits 0" exits_with 0 "$tools/php-lint.sh" +check "php-lint.sh --help shows usage" output_has "Usage: php-lint.sh" "$tools/php-lint.sh" --help +check "php-lint.sh with an unknown option exits 2" exits_with 2 "$tools/php-lint.sh" --nope "$tmp" +check "php-lint.sh with a missing directory exits 2" exits_with 2 "$tools/php-lint.sh" "$tmp/no-such-dir" +check "php-lint.sh with two directories exits 2" exits_with 2 "$tools/php-lint.sh" "$tmp" "$tmp" +check "php-lint.sh with --php and no value exits 2" exits_with 2 "$tools/php-lint.sh" "$tmp" --php +check "php-lint.sh rejects a version that is not MAJOR.MINOR" \ + output_has "bad PHP version: '7'" "$tools/php-lint.sh" -p 7 "$tmp" +check "php-lint.sh rejects an image-like version" exits_with 2 "$tools/php-lint.sh" -p "7.4-cli;id" "$tmp" + if [ "$failures" -eq 0 ]; then echo "All tests passed." else