mirror of
https://github.com/telekom-security/tpotce.git
synced 2026-05-29 17:24:15 +00:00
257 lines
6.2 KiB
Bash
257 lines
6.2 KiB
Bash
|
|
#!/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
|
||
|
|
}
|