The parts every wpallstars plugin needs: settings screen with search and an empty General tab, Read Me tab, features as classes, replaced plugins, the shared GitHub updater, release and check scripts, CI and the shared rules. scripts/rename-plugin.sh starts a new plugin; scripts/sync-core.sh keeps plugins' core files in step with this starter. Built with aidevops (https://aidevops.sh).
334 lines
11 KiB
Bash
Executable File
334 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Smoke test: install the release zip on a disposable WordPress in Docker and
|
|
# load the site and admin screens, first with the default settings and then
|
|
# with every feature switched on. Then uninstall it and check it left nothing
|
|
# behind. Nothing is kept and no other site is touched.
|
|
#
|
|
# Usage: scripts/smoke-test.sh [--wp VERSION] [--php VERSION] [--ref REF]
|
|
# [--zip FILE] [--wporg] [--keep-log FILE]
|
|
# --wp VERSION WordPress version (default: latest). The minimum is 6.2.
|
|
# --php VERSION PHP version of the Docker images (default: 8.3). The
|
|
# minimum is 7.4.
|
|
# --ref REF Build the zip from REF (default: HEAD).
|
|
# --zip FILE Test this zip instead of building one.
|
|
# --wporg Test the WordPress.org build instead of the GitHub one.
|
|
# --keep-log FILE Save the site's debug.log to FILE.
|
|
#
|
|
# Fails on any PHP error, warning, notice or deprecation in debug.log (from
|
|
# the pages loaded, WP-CLI and cron), a page that fails to load (HTTP 500 or
|
|
# WordPress's critical error page), or settings left after uninstalling.
|
|
# Needs Docker, curl and internet access (WordPress is downloaded).
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly SCRIPT_DIR
|
|
# shellcheck source=scripts/lib/plugin.sh disable=SC1091 # followed only with -x
|
|
. "$SCRIPT_DIR/lib/plugin.sh"
|
|
|
|
# Set in main() from the plugin's main file.
|
|
SLUG=""
|
|
DB_IMAGE=""
|
|
NAME=""
|
|
TMP_DIR=""
|
|
STARTED=0
|
|
WP_VERSION="latest"
|
|
PHP_VERSION="8.3"
|
|
BASE_URL=""
|
|
DB_PASSWORD=""
|
|
ADMIN_PASSWORD=""
|
|
FAILED=0
|
|
|
|
die() {
|
|
local message="$1"
|
|
printf 'smoke-test: %s\n' "$message" >&2
|
|
exit 2
|
|
}
|
|
|
|
usage() {
|
|
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
|
return 0
|
|
}
|
|
|
|
fail() {
|
|
local message="$1"
|
|
printf ' FAIL %s\n' "$message"
|
|
FAILED=1
|
|
return 0
|
|
}
|
|
|
|
cleanup() {
|
|
if [ "$STARTED" -eq 1 ]; then
|
|
docker rm -f "$NAME-web" "$NAME-db" >/dev/null 2>&1 || true
|
|
docker volume rm "$NAME-wp" >/dev/null 2>&1 || true
|
|
docker network rm "$NAME" >/dev/null 2>&1 || true
|
|
fi
|
|
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
|
|
rm -rf "$TMP_DIR"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
random_password() {
|
|
LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 24
|
|
return 0
|
|
}
|
|
|
|
# wp-cli in the disposable site; the zip is mounted at /zips.
|
|
wp_cli() {
|
|
docker run --rm --network "$NAME" -v "$NAME-wp:/var/www/html" -v "$TMP_DIR/zips:/zips:ro" \
|
|
--user 33:33 "wordpress:cli-php$PHP_VERSION" php -d memory_limit=1G /usr/local/bin/wp "$@"
|
|
return $?
|
|
}
|
|
|
|
start_site() {
|
|
printf 'Starting a disposable WordPress %s on PHP %s (%s)...\n' "$WP_VERSION" "$PHP_VERSION" "$NAME"
|
|
STARTED=1
|
|
DB_PASSWORD="$(random_password)"
|
|
ADMIN_PASSWORD="$(random_password)"
|
|
docker network create "$NAME" >/dev/null
|
|
docker volume create "$NAME-wp" >/dev/null
|
|
docker run -d --name "$NAME-db" --network "$NAME" \
|
|
-e MARIADB_ROOT_PASSWORD="$DB_PASSWORD" -e MARIADB_DATABASE=wordpress "$DB_IMAGE" >/dev/null
|
|
docker run --rm -v "$NAME-wp:/var/www/html" --user 0:0 "wordpress:cli-php$PHP_VERSION" chown 33:33 /var/www/html
|
|
|
|
local waited=0
|
|
until docker exec "$NAME-db" mariadb-admin ping -h127.0.0.1 -uroot -p"$DB_PASSWORD" --silent >/dev/null 2>&1; do
|
|
[ "$waited" -lt 90 ] || die "the database did not start"
|
|
sleep 2
|
|
waited=$((waited + 2))
|
|
done
|
|
|
|
# Core first, so the web image finds WordPress and does not copy its own.
|
|
wp_cli core download --version="$WP_VERSION" --quiet
|
|
docker run -d --name "$NAME-web" --network "$NAME" -v "$NAME-wp:/var/www/html" \
|
|
-p 127.0.0.1::80 "wordpress:php$PHP_VERSION-apache" >/dev/null
|
|
BASE_URL="http://$(docker port "$NAME-web" 80 | head -n 1)"
|
|
|
|
wp_cli config create --dbname=wordpress --dbuser=root --dbpass="$DB_PASSWORD" --dbhost="$NAME-db" --skip-check --quiet
|
|
wp_cli config set WP_DEBUG true --raw --quiet
|
|
wp_cli config set WP_DEBUG_LOG true --raw --quiet
|
|
wp_cli config set WP_DEBUG_DISPLAY false --raw --quiet
|
|
wp_cli config set DISABLE_WP_CRON true --raw --quiet
|
|
wp_cli core install --url="$BASE_URL" --title=SmokeTest --admin_user=admin --admin_password="$ADMIN_PASSWORD" \
|
|
--admin_email=admin@example.com --skip-email --quiet
|
|
# WP-CLI cannot see Apache's mod_rewrite, so write core's rules itself.
|
|
wp_cli rewrite structure '/%postname%/' --quiet
|
|
docker exec -u www-data "$NAME-web" sh -c 'printf "%s\n" "# BEGIN WordPress" "RewriteEngine On" "RewriteBase /" \
|
|
"RewriteRule ^index\.php$ - [L]" "RewriteCond %{REQUEST_FILENAME} !-f" "RewriteCond %{REQUEST_FILENAME} !-d" \
|
|
"RewriteRule . /index.php [L]" "# END WordPress" >/var/www/html/.htaccess'
|
|
printf 'WordPress %s at %s\n' "$(wp_cli core version)" "$BASE_URL"
|
|
return 0
|
|
}
|
|
|
|
log_in() {
|
|
curl -sS -o /dev/null -c "$TMP_DIR/cookies" -b 'wordpress_test_cookie=WP%20Cookie%20check' \
|
|
--data-urlencode 'log=admin' --data-urlencode "pwd=$ADMIN_PASSWORD" \
|
|
--data-urlencode 'testcookie=1' --data-urlencode "redirect_to=$BASE_URL/wp-admin/" \
|
|
"$BASE_URL/wp-login.php"
|
|
local status
|
|
status="$(curl -sS -o /dev/null -w '%{http_code}' -b "$TMP_DIR/cookies" "$BASE_URL/wp-admin/")"
|
|
[ "$status" = "200" ] || die "could not log in (wp-admin answered $status)"
|
|
return 0
|
|
}
|
|
|
|
# Load one page; $2 is "admin" to send the login cookie. Fails on 5xx or
|
|
# WordPress's critical error page; other statuses are listed.
|
|
fetch() {
|
|
local path="$1"
|
|
local who="$2"
|
|
local status
|
|
local args=(-sS -o "$TMP_DIR/page" -w '%{http_code}' --max-time 60)
|
|
[ "$who" = "admin" ] && args+=(-b "$TMP_DIR/cookies")
|
|
status="$(curl "${args[@]}" "$BASE_URL$path" || printf '000')"
|
|
if grep -q 'There has been a critical error' "$TMP_DIR/page"; then
|
|
fail "$status $who $path: critical error page"
|
|
elif [ "$status" -ge 500 ] || [ "$status" = "000" ]; then
|
|
fail "$status $who $path"
|
|
else
|
|
printf ' %s %s %s\n' "$status" "$who" "$path"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
load_pages() {
|
|
local path
|
|
local visitor_pages=(
|
|
'/' '/hello-world/' '/sample-page/' '/?s=hello' '/category/uncategorized/' '/feed/'
|
|
'/wp-json/' '/wp-json/wp/v2/posts' '/wp-sitemap.xml' '/no-such-page/' '/wp-login.php'
|
|
)
|
|
local admin_pages=(
|
|
'/' '/hello-world/' '/wp-admin/' '/wp-admin/plugins.php' "/wp-admin/options-general.php?page=$SLUG"
|
|
'/wp-admin/edit.php' '/wp-admin/edit.php?post_type=page' '/wp-admin/post-new.php'
|
|
'/wp-admin/post.php?post=1&action=edit' '/wp-admin/upload.php' '/wp-admin/edit-comments.php'
|
|
'/wp-admin/themes.php' '/wp-admin/users.php' '/wp-admin/profile.php' '/wp-admin/tools.php'
|
|
'/wp-admin/site-health.php' '/wp-admin/options-general.php' '/wp-admin/options-permalink.php'
|
|
)
|
|
for path in "${visitor_pages[@]}"; do
|
|
fetch "$path" visitor
|
|
done
|
|
for path in "${admin_pages[@]}"; do
|
|
fetch "$path" admin
|
|
done
|
|
wp_cli cron event run --due-now --quiet || fail "wp cron event run"
|
|
return 0
|
|
}
|
|
|
|
# Switch on every feature that has an on/off setting, except maintenance
|
|
# mode, which would answer every visitor page with its 503 notice.
|
|
switch_all_on() {
|
|
# The class prefix is the plugin's @package (scripts/lib/plugin.sh).
|
|
# shellcheck disable=SC2016 # PHP code: its $variables are PHP's, not the shell's.
|
|
wp_cli eval '
|
|
$plugin = "'"$PLUGIN_PACKAGE"'";
|
|
$settings = $plugin . "_Settings";
|
|
$schema = $settings::schema();
|
|
$on = 0;
|
|
foreach ($plugin::features() as $class) {
|
|
$key = defined($class . "::KEY") ? $class::KEY : "";
|
|
if ("" !== $key && "maintenance" !== $key && isset($schema[$key]["type"]) && in_array($schema[$key]["type"], array("bool", "boolean"), true)) {
|
|
$settings::set($key, true);
|
|
$on++;
|
|
}
|
|
}
|
|
echo "$on features switched on\n";'
|
|
return $?
|
|
}
|
|
|
|
check_uninstall() {
|
|
wp_cli plugin deactivate "$SLUG" --quiet || fail "deactivate"
|
|
wp_cli plugin uninstall "$SLUG" --quiet || fail "uninstall"
|
|
local left
|
|
left="$(wp_cli option list --search="*$PLUGIN_PREFIX*" --field=option_name 2>/dev/null || true)"
|
|
[ -z "$left" ] || fail "options left after uninstalling: $(printf '%s' "$left" | tr '\n' ' ')"
|
|
left="$(wp_cli cron event list --field=hook 2>/dev/null | grep -i "$PLUGIN_PREFIX" || true)"
|
|
[ -z "$left" ] || fail "cron events left after uninstalling: $(printf '%s' "$left" | tr '\n' ' ')"
|
|
return 0
|
|
}
|
|
|
|
# A must-use plugin that logs a notice for /?smoke-canary, so the test can
|
|
# prove that page loads reach debug.log (an empty log proves nothing otherwise).
|
|
add_canary() {
|
|
docker exec -u www-data "$NAME-web" sh -c 'mkdir -p /var/www/html/wp-content/mu-plugins && printf "%s\n" "<?php" \
|
|
"if (isset(\$_GET[\"smoke-canary\"])) { trigger_error(\"smoke-test canary\", E_USER_NOTICE); }" \
|
|
>/var/www/html/wp-content/mu-plugins/smoke-canary.php'
|
|
return $?
|
|
}
|
|
|
|
check_debug_log() {
|
|
local keep="$1"
|
|
local log
|
|
fetch '/?smoke-canary' visitor
|
|
log="$(docker exec "$NAME-web" sh -c 'cat /var/www/html/wp-content/debug.log 2>/dev/null' || true)"
|
|
if [ -n "$keep" ]; then
|
|
printf '%s\n' "$log" >"$keep"
|
|
fi
|
|
if ! printf '%s\n' "$log" | grep -q 'smoke-test canary'; then
|
|
fail "debug.log does not work: the canary notice is missing"
|
|
return 0
|
|
fi
|
|
log="$(printf '%s\n' "$log" | grep -v 'smoke-test canary' || true)"
|
|
if [ -n "$log" ]; then
|
|
printf '\ndebug.log:\n%s\n' "$log"
|
|
fail "PHP messages in debug.log"
|
|
else
|
|
printf '\ndebug.log: no PHP messages (the canary notice arrived)\n'
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
main() {
|
|
local ref="HEAD"
|
|
local zip=""
|
|
local wporg=0
|
|
local keep=""
|
|
local arg
|
|
while [ $# -gt 0 ]; do
|
|
arg="$1"
|
|
case "$arg" in
|
|
--wp | --php | --ref | --zip | --keep-log)
|
|
[ $# -ge 2 ] || die "$arg needs a value"
|
|
case "$arg" in
|
|
--wp) WP_VERSION="$2" ;;
|
|
--php) PHP_VERSION="$2" ;;
|
|
--ref) ref="$2" ;;
|
|
--zip)
|
|
[ -f "$2" ] || die "no such zip: $2"
|
|
zip="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")"
|
|
;;
|
|
--keep-log)
|
|
case "$2" in
|
|
/*) keep="$2" ;;
|
|
*) keep="$PWD/$2" ;;
|
|
esac
|
|
;;
|
|
esac
|
|
shift
|
|
;;
|
|
--wporg) wporg=1 ;;
|
|
-h | --help)
|
|
usage
|
|
return 0
|
|
;;
|
|
*) die "unknown argument: $arg" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
command -v docker >/dev/null 2>&1 || die "needs Docker"
|
|
command -v curl >/dev/null 2>&1 || die "needs curl"
|
|
docker info >/dev/null 2>&1 || die "Docker is not running"
|
|
local root
|
|
root="$(git rev-parse --show-toplevel)" || die "run this inside a checkout of the plugin"
|
|
cd "$root"
|
|
plugin_identity "$ref" || die "cannot tell which plugin this is at $ref"
|
|
SLUG="$PLUGIN_SLUG"
|
|
DB_IMAGE="$(plugin_env DB_IMAGE mariadb:10.6)"
|
|
NAME="$SLUG-smoke-$$"
|
|
|
|
trap cleanup EXIT
|
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/$SLUG-smoke.XXXXXX")"
|
|
mkdir -p "$TMP_DIR/zips"
|
|
chmod 755 "$TMP_DIR" "$TMP_DIR/zips"
|
|
if [ -z "$zip" ]; then
|
|
"$root/scripts/build-release.sh" --ref "$ref" --out "$TMP_DIR/build" --quiet >/dev/null || die "build failed"
|
|
local built=()
|
|
if [ "$wporg" -eq 1 ]; then
|
|
built=("$TMP_DIR"/build/wordpress-org-"$SLUG"-*.zip)
|
|
else
|
|
built=("$TMP_DIR"/build/"$SLUG"-*.zip)
|
|
fi
|
|
[ -f "${built[0]}" ] || die "the build made no zip"
|
|
zip="${built[0]}"
|
|
fi
|
|
cp "$zip" "$TMP_DIR/zips/$SLUG.zip"
|
|
chmod 644 "$TMP_DIR/zips/$SLUG.zip"
|
|
|
|
start_site
|
|
add_canary || die "could not add the debug.log canary"
|
|
printf '\nInstalling %s\n' "$(basename "$zip")"
|
|
wp_cli plugin install "/zips/$SLUG.zip" --activate --quiet || die "could not install and activate the plugin"
|
|
log_in
|
|
|
|
printf '\nDefault settings:\n'
|
|
load_pages
|
|
|
|
printf '\nEvery feature on:\n'
|
|
switch_all_on || fail "switching features on"
|
|
load_pages
|
|
|
|
printf '\nUninstall:\n'
|
|
check_uninstall
|
|
check_debug_log "$keep"
|
|
|
|
printf '\n'
|
|
if [ "$FAILED" -eq 1 ]; then
|
|
printf 'Smoke test failed (WordPress %s, PHP %s).\n' "$WP_VERSION" "$PHP_VERSION"
|
|
return 1
|
|
fi
|
|
printf 'Smoke test passed (WordPress %s, PHP %s).\n' "$WP_VERSION" "$PHP_VERSION"
|
|
return 0
|
|
}
|
|
|
|
main "$@"
|