From df4837edd6fbf2ce1faed0f80609be789d8cbaf5 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 15 Jun 2026 10:39:58 +0200 Subject: [PATCH 1/2] feat: shared site testing script and move to linkinator Relates-to: keymanapp/keyman.com#788 Test-bot: skip --- .bootstrap-registry | 1 + _common/docker.inc.sh | 54 ++++++++++++++++++++++++- _common/tests.inc.sh | 93 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 _common/tests.inc.sh diff --git a/.bootstrap-registry b/.bootstrap-registry index 53924c3..7f40dcb 100644 --- a/.bootstrap-registry +++ b/.bootstrap-registry @@ -20,3 +20,4 @@ KeymanHosts.php KeymanSentry.php KeymanVersion.php MarkdownHost.php +tests.inc.sh diff --git a/_common/docker.inc.sh b/_common/docker.inc.sh index ae9bac8..9547d33 100644 --- a/_common/docker.inc.sh +++ b/_common/docker.inc.sh @@ -1,5 +1,7 @@ # Common docker functions. +source _common/tests.inc.sh + function get_docker_image_id() { local IMAGE_NAME=$1 echo "$(docker images -q $IMAGE_NAME)" @@ -153,4 +155,54 @@ function start_docker_container() { fi builder_echo green "Listening on http://$HOST:$PORT" -} \ No newline at end of file +} + +# +# Test PHP site in a docker container +# +# Parameters: +# 1: CONTAINER_DESC desc of container to test +# 2: CONTAINER_PORT localhost http port for container +# 3: TEST_PATH path to start link check at, under http://localhost:CONTAINER_PORT +# 4..: SKIP_PATHS paths to skip crawling in link check, optional +# +# Builder options --no-unit-test, --no-lint, --no-link-check are honoured +# +function test_docker_container() { + local CONTAINER_DESC="$1" + local CONTAINER_PORT="$2" + local TEST_PATH="$3" + shift 3 + local SKIP_PATHS=("$*") + + local LINK_RESULT + echo "TIER_TEST" > tier.txt + + # from commands.inc.sh + + do_test_record_start_time + + if ! builder_has_option --no-unit-test; then + builder_echo blue "---- PHP unit tests" + do_test_unit_tests "${CONTAINER_DESC}" + fi + + if ! builder_has_option --no-lint; then + builder_echo blue "---- Lint PHP files" + do_test_lint "${CONTAINER_DESC}" + fi + + if ! builder_has_option --no-link-check; then + builder_echo blue "---- Testing links" + + LINK_RESULT=0 + do_test_links "http://localhost:${CONTAINER_PORT}" "$TEST_PATH" "${SKIP_PATHS[@]}" || LINK_RESULT=$? + builder_echo blue "Done checking links; linkinator exit code: ${LINK_RESULT}" + do_test_print_link_report + + do_test_print_container_error_logs "${CONTAINER_DESC}" + fi + + rm tier.txt + return "$LINK_RESULT" +} diff --git a/_common/tests.inc.sh b/_common/tests.inc.sh new file mode 100644 index 0000000..41d7e79 --- /dev/null +++ b/_common/tests.inc.sh @@ -0,0 +1,93 @@ +# shellcheck shell=bash + +# Record the start time for unit tests for later log review +function do_test_record_start_time() { + TEST_START_TIME=$(date -Is -u) +} + +# Run unit tests through phpunit +# +# Parameters +# 1: CONTAINER container_desc to run on +# +function do_test_unit_tests() { + local CONTAINER="$1" + docker exec "${CONTAINER}" sh -c "vendor/bin/phpunit --testdox ${builder_extra_params[*]}" +} + +# Lint .php files for obvious errors +# +# Parameters +# 1: CONTAINER container_desc to run on +# +function do_test_lint() { + local CONTAINER="$1" + docker exec "${CONTAINER}" sh -c "find . -name '*.php' | grep -v '/vendor/' | xargs -n 1 -d '\\n' php -l" +} + +## Check links on live local server using linkinator +# +# Parameters +# 1: baseURL the top level URL for the site +# 2: testPath path under baseURL to start testing, e.g. / +# 3[,4..]: skipPaths list of paths (under baseURL) to skip crawling, optional +# +function do_test_links() { + local baseURL="$1" + local testPath="$2" + shift 2 + local skipPaths skip skipParams=() + if [[ $# -gt 1 ]]; then + skipPaths=("$*") + else + skipPaths=() + fi + + for skip in "${skipPaths[@]}"; do + skipParams+=(--skip "^${baseURL}${skip}") + done + + npx https://github.com/keymanapp/linkinator \ + "${baseURL}${testPath}" \ + --clean-urls \ + --concurrency 50 \ + --format json \ + --output-filename linkinator-results.json \ + --skip "^(?!${baseURL})" \ + "${skipParams[@]}" \ + --recurse \ + --redirects verify \ + --retry-errors \ + --root-path "${baseURL}" +} + +# Print summary of results from linkinator +function do_test_print_link_report() { + echo ---------------------------------------------------------------------- + echo Link check summary + echo ---------------------------------------------------------------------- + # Emit full JSON detail for broken links (may not be necessary) + jq '.links[] | select(.state != "OK")' < linkinator-results.json + echo + echo + # Emit a summary report + jq -r '.links[] | select(.state != "OK") | "\(.state)[\(.status)]: \(.parent) --> \(.url)"' < linkinator-results.json +} + +# Scan logs recorded on container since start of tests to find any reported PHP +# errors (note, depends on 'php7' string) +# +# Parameters +# 1: CONTAINER container_desc to run on +# +function do_test_print_container_error_logs() { + local CONTAINER="$1" + if docker container logs "${CONTAINER}" --since "${TEST_START_TIME}" 2>&1 | grep -q 'php7'; then + echo 'PHP reported errors or warnings:' + docker container logs "${CONTAINER}" --since "${TEST_START_TIME}" 2>&1 | grep 'php7' + return 1 + else + echo 'No PHP errors found' + return 0 + fi +} From 25765bf960514555b0754138ecc77c0131fc8450 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 16 Jun 2026 07:56:46 +0200 Subject: [PATCH 2/2] chore: address review comments Co-authored-by: Eberhard Beilharz --- _common/docker.inc.sh | 14 +++++++++----- _common/tests.inc.sh | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/_common/docker.inc.sh b/_common/docker.inc.sh index 9547d33..c098650 100644 --- a/_common/docker.inc.sh +++ b/_common/docker.inc.sh @@ -1,4 +1,9 @@ -# Common docker functions. +# shellcheck shell=bash +# +# Keyman is copyright (C) SIL Global. MIT License. +# +# Common docker functions for (generally PHP-based) containers +# source _common/tests.inc.sh @@ -173,12 +178,12 @@ function test_docker_container() { local CONTAINER_PORT="$2" local TEST_PATH="$3" shift 3 - local SKIP_PATHS=("$*") + local SKIP_PATHS=("$@") - local LINK_RESULT + local LINK_RESULT=0 echo "TIER_TEST" > tier.txt - # from commands.inc.sh + # Similar pattern in ci.yml on sites do_test_record_start_time @@ -195,7 +200,6 @@ function test_docker_container() { if ! builder_has_option --no-link-check; then builder_echo blue "---- Testing links" - LINK_RESULT=0 do_test_links "http://localhost:${CONTAINER_PORT}" "$TEST_PATH" "${SKIP_PATHS[@]}" || LINK_RESULT=$? builder_echo blue "Done checking links; linkinator exit code: ${LINK_RESULT}" do_test_print_link_report diff --git a/_common/tests.inc.sh b/_common/tests.inc.sh index 41d7e79..8dfe344 100644 --- a/_common/tests.inc.sh +++ b/_common/tests.inc.sh @@ -1,4 +1,9 @@ # shellcheck shell=bash +# +# Keyman is copyright (C) SIL Global. MIT License. +# +# Shared test functions for PHP lint, unit test, and general broken link checks +# # Record the start time for unit tests for later log review function do_test_record_start_time() { @@ -36,12 +41,8 @@ function do_test_links() { local baseURL="$1" local testPath="$2" shift 2 - local skipPaths skip skipParams=() - if [[ $# -gt 1 ]]; then - skipPaths=("$*") - else - skipPaths=() - fi + local skipPaths=("$@") + local skip skipParams=() for skip in "${skipPaths[@]}"; do skipParams+=(--skip "^${baseURL}${skip}") @@ -75,16 +76,16 @@ function do_test_print_link_report() { } # Scan logs recorded on container since start of tests to find any reported PHP -# errors (note, depends on 'php7' string) +# errors (note, depends on '[php#:xxxx]' marker string, where # = 7 for PHP7, omitted for PHP8) # # Parameters # 1: CONTAINER container_desc to run on # function do_test_print_container_error_logs() { local CONTAINER="$1" - if docker container logs "${CONTAINER}" --since "${TEST_START_TIME}" 2>&1 | grep -q 'php7'; then + if docker container logs "${CONTAINER}" --since "${TEST_START_TIME}" 2>&1 | grep -qP '\[php7?:(error|warn|notice)\]'; then echo 'PHP reported errors or warnings:' - docker container logs "${CONTAINER}" --since "${TEST_START_TIME}" 2>&1 | grep 'php7' + docker container logs "${CONTAINER}" --since "${TEST_START_TIME}" 2>&1 | grep -P '\[php7?:(error|warn|notice)\]' return 1 else echo 'No PHP errors found'