tpotce/docker/_tests/lib/common.sh

257 lines
6.2 KiB
Bash
Raw Permalink Normal View History

2026-05-27 10:21:29 +00:00
#!/usr/bin/env bash
set -Eeuo pipefail
TEST_LIB_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$(cd -- "${TEST_LIB_DIR}/.." && pwd)"
REPO_ROOT="$(cd -- "${TEST_ROOT}/../.." && pwd)"
DOCKER_ROOT="${REPO_ROOT}/docker"
TEST_TIMEOUT="${TEST_TIMEOUT:-30}"
TEST_BIND_IP="${TEST_BIND_IP:-127.0.0.1}"
TEST_KEEP_ARTIFACTS="${TEST_KEEP_ARTIFACTS:-false}"
TEST_TMP_ROOT=""
TEST_HARNESS_COMPOSE=""
TEST_PROJECT_NAME=""
TEST_CONTAINER_NAME=""
TEST_ARTIFACT_LOG_DIR=""
test_info() {
printf '==> %s\n' "$*"
}
test_ok() {
printf '[OK] %s\n' "$*"
}
test_die() {
printf '[FAIL] %s\n' "$*" >&2
exit 1
}
test_require_command() {
command -v "$1" >/dev/null 2>&1 || test_die "Required command not found: $1"
}
test_check_dependencies() {
[[ -n "${BASH_VERSION:-}" ]] || test_die "This test suite requires bash"
test_require_command docker
test_require_command python3
docker compose version >/dev/null 2>&1 || test_die "Docker Compose plugin is required"
docker info >/dev/null 2>&1 || test_die "Docker daemon is not accessible"
}
test_validate_timeout() {
[[ "${TEST_TIMEOUT}" =~ ^[0-9]+$ ]] || test_die "--timeout must be a number"
(( TEST_TIMEOUT >= 1 )) || test_die "--timeout must be at least 1"
}
test_validate_port() {
local port="$1"
[[ "${port}" =~ ^[0-9]+$ ]] || test_die "Port must be a number: ${port}"
(( port >= 1 && port <= 65535 )) || test_die "Port must be between 1 and 65535: ${port}"
}
test_read_compose_image() {
local service="$1"
local fallback="$2"
local compose_file="${DOCKER_ROOT}/${service}/docker-compose.yml"
local detected=""
if [[ -f "${compose_file}" ]]; then
detected="$(
sed -n 's/^[[:space:]]*image:[[:space:]]*//p' "${compose_file}" \
| head -n 1 \
| tr -d "\"'"
)"
fi
if [[ -n "${detected}" ]]; then
printf '%s\n' "${detected}"
else
printf '%s\n' "${fallback}"
fi
}
test_require_image() {
local image="$1"
local build_hint="$2"
docker image inspect "${image}" >/dev/null 2>&1 || test_die "Image not found: ${image}. Build it first, for example: ${build_hint}"
}
test_ensure_port_free() {
local bind_ip="$1"
local host_port="$2"
python3 - "${bind_ip}" "${host_port}" <<'PY'
import socket
import sys
host = sys.argv[1]
port = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((host, port))
except OSError as exc:
print(f"{host}:{port} is not available: {exc}", file=sys.stderr)
sys.exit(1)
finally:
sock.close()
PY
}
test_ensure_udp_port_free() {
local bind_ip="$1"
local host_port="$2"
python3 - "${bind_ip}" "${host_port}" <<'PY'
import socket
import sys
host = sys.argv[1]
port = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.bind((host, port))
except OSError as exc:
print(f"{host}:{port}/udp is not available: {exc}", file=sys.stderr)
sys.exit(1)
finally:
sock.close()
PY
}
test_prepare_harness() {
local test_name="$1"
TEST_TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/tpot-${test_name}.XXXXXX")"
TEST_HARNESS_COMPOSE="${TEST_TMP_ROOT}/docker-compose.yml"
TEST_PROJECT_NAME="tpot-${test_name}-$(date +%s)-$$"
TEST_CONTAINER_NAME="${TEST_PROJECT_NAME}-service"
chmod 0777 "${TEST_TMP_ROOT}"
}
test_compose() {
docker compose -f "${TEST_HARNESS_COMPOSE}" -p "${TEST_PROJECT_NAME}" "$@"
}
test_wait_for_container() {
local deadline=$((SECONDS + TEST_TIMEOUT))
local state=""
while (( SECONDS < deadline )); do
state="$(docker inspect -f '{{.State.Status}}' "${TEST_CONTAINER_NAME}" 2>/dev/null || true)"
case "${state}" in
running)
return 0
;;
exited|dead)
return 1
;;
esac
sleep 1
done
return 1
}
test_get_mapped_port() {
local service="$1"
local container_port="$2"
local mapping=""
local deadline=$((SECONDS + TEST_TIMEOUT))
while (( SECONDS < deadline )); do
mapping="$(test_compose port "${service}" "${container_port}" 2>/dev/null | tail -n 1 || true)"
if [[ -z "${mapping}" && -n "${TEST_CONTAINER_NAME}" ]]; then
mapping="$(docker port "${TEST_CONTAINER_NAME}" "${container_port}" 2>/dev/null | tail -n 1 || true)"
fi
if [[ -n "${mapping}" ]]; then
printf '%s\n' "${mapping##*:}"
return 0
fi
sleep 1
done
return 1
}
test_wait_for_file_text() {
local text="$1"
local directory="$2"
local deadline=$((SECONDS + TEST_TIMEOUT))
while (( SECONDS < deadline )); do
if grep -R -F -- "${text}" "${directory}" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
test_show_diagnostics() {
printf '\n[diagnostics] Container state\n' >&2
if [[ -n "${TEST_CONTAINER_NAME}" ]]; then
docker inspect -f 'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}' "${TEST_CONTAINER_NAME}" >&2 || true
else
printf 'No container name available.\n' >&2
fi
printf '\n[diagnostics] Docker logs\n' >&2
if [[ -n "${TEST_HARNESS_COMPOSE}" && -f "${TEST_HARNESS_COMPOSE}" ]]; then
test_compose logs --no-color --tail=120 >&2 || true
else
printf 'No temporary compose file available.\n' >&2
fi
if [[ -n "${TEST_ARTIFACT_LOG_DIR}" ]]; then
printf '\n[diagnostics] Test log artifacts\n' >&2
if [[ -d "${TEST_ARTIFACT_LOG_DIR}" ]]; then
find "${TEST_ARTIFACT_LOG_DIR}" -maxdepth 1 -type f -print | sort >&2 || true
while IFS= read -r file; do
printf '\n--- %s ---\n' "${file}" >&2
tail -n 80 "${file}" >&2 || true
done < <(find "${TEST_ARTIFACT_LOG_DIR}" -maxdepth 1 -type f -print | sort)
else
printf 'No temporary log directory available.\n' >&2
fi
fi
}
test_cleanup() {
local status=$?
trap - EXIT
if (( status != 0 )); then
test_show_diagnostics
fi
if [[ -n "${TEST_HARNESS_COMPOSE}" && -f "${TEST_HARNESS_COMPOSE}" ]]; then
test_compose down --volumes --remove-orphans >/dev/null 2>&1 || true
fi
if [[ "${TEST_KEEP_ARTIFACTS}" == "true" ]]; then
if [[ -n "${TEST_TMP_ROOT}" ]]; then
printf 'Artifacts kept at: %s\n' "${TEST_TMP_ROOT}" >&2
fi
elif [[ -n "${TEST_TMP_ROOT}" && -d "${TEST_TMP_ROOT}" ]]; then
rm -rf -- "${TEST_TMP_ROOT}"
fi
exit "${status}"
}
test_enable_cleanup() {
trap test_cleanup EXIT
trap 'exit 130' INT
}