mirror of
https://github.com/xroche/httrack.git
synced 2026-07-28 11:32:48 +03:00
Under `set -e` a failing command in an EXIT trap becomes the script's exit status, so a hiccup while tearing down fixtures fails a test whose assertions all passed. That is what turned `57_local-proxy-connect.test` red on the Windows x64 leg of #765: five OK lines, no FAIL, exit 1. Every EXIT trap in the suite now runs teardown with errexit off, and the signal traps keep their own `trap` line, since sharing `set +e` with HUP/INT/QUIT/PIPE/TERM would leave errexit off for the rest of a signalled run and let a torn-down test still report success. A `|| true` on the `rm` would have been smaller, but it throws away the only diagnostic, and the evidence does not say which teardown command failed: a blocked `rm -rf` on the Windows runner exits 1 and prints "Device or resource busy", while the log shows exit 1 and nothing at all. The sharing violation in the issue is the plausible mechanism rather than a confirmed one, so whatever it really is now prints its own error. `99_teardown-status.test` pins the semantics both ways and scans the suite so a new test cannot reintroduce the shape, the leaky combined trap included. The `return 0` that three `cleanup()` bodies ended with never protected anything, since errexit fires at the failing command before it is reached. Closes #773 Signed-off-by: Xavier Roche <roche@httrack.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
183 lines
5.7 KiB
Bash
183 lines
5.7 KiB
Bash
#!/bin/bash
|
|
#
|
|
# fopen() succeeds on a directory on POSIX and reading one never reaches EOF; a
|
|
# FIFO blocks in fopen() instead. Either used to wedge the single-threaded server.
|
|
|
|
set -euo pipefail
|
|
|
|
testdir=$(cd "$(dirname "$0")" && pwd)
|
|
distdir=${top_srcdir:-$(cd "${testdir}/.." && pwd)}
|
|
distdir=$(cd "${distdir}" && pwd)
|
|
|
|
fail() {
|
|
echo "FAIL: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
command -v htsserver >/dev/null || fail "no htsserver in PATH"
|
|
command -v python3 >/dev/null || {
|
|
echo "python3 not found; skipping" >&2
|
|
exit 77
|
|
}
|
|
|
|
srv=
|
|
log=$(mktemp)
|
|
base=$(mktemp -d)
|
|
cleanup() {
|
|
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
|
|
rm -f "${log}"
|
|
rm -rf "${base}"
|
|
}
|
|
trap 'set +e; cleanup' EXIT
|
|
trap cleanup HUP INT QUIT PIPE TERM
|
|
|
|
# First line only. A "| head -1" would close the pipe early and, under pipefail,
|
|
# SIGPIPE the producer into a spurious failure.
|
|
firstline() { echo "${1%%$'\n'*}"; }
|
|
|
|
freeport() {
|
|
python3 -c 'import socket
|
|
s = socket.socket()
|
|
s.bind(("127.0.0.1", 0))
|
|
print(s.getsockname()[1])
|
|
s.close()'
|
|
}
|
|
|
|
# Echo the announced URL. Runs in a command substitution, so it is a
|
|
# subshell and cannot export the pid: the caller reads it back with srvpid.
|
|
start() {
|
|
local port url
|
|
port=$(freeport)
|
|
: >"${log}"
|
|
(
|
|
trap '' TERM TTOU
|
|
exec htsserver "${distdir}/" --port "${port}" >"${log}" 2>&1
|
|
) &
|
|
for _ in $(seq 1 40); do
|
|
url=$(sed -n 's/^URL=//p' "${log}" 2>/dev/null) && test -n "${url}" && break
|
|
sleep 0.25
|
|
done
|
|
test -n "${url:-}" || fail "htsserver did not come up: $(cat "${log}")"
|
|
echo "${url}"
|
|
}
|
|
|
|
# The server reports its own pid; the aliveness assertion below hangs off it.
|
|
srvpid() { firstline "$(sed -n 's/^PID=//p' "${log}")"; }
|
|
|
|
alive() { kill -0 "$1" 2>/dev/null; }
|
|
|
|
portof() { echo "${1##*:}" | tr -d /; }
|
|
|
|
# GET the path $2 from 127.0.0.1:$1, or POST the body $3 to / when $2 is empty.
|
|
# The socket timeout is what makes an unfixed tree fail rather than wedge CI.
|
|
request() {
|
|
python3 -c 'import socket, sys
|
|
port, path, body = int(sys.argv[1]), sys.argv[2], sys.argv[3]
|
|
if path:
|
|
req = "GET %s HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" % path
|
|
else:
|
|
req = ("POST / HTTP/1.0\r\nHost: 127.0.0.1\r\n"
|
|
"Content-type: application/x-www-form-urlencoded\r\n"
|
|
"Content-length: %d\r\n\r\n%s" % (len(body), body))
|
|
s = socket.create_connection(("127.0.0.1", port), 10)
|
|
s.settimeout(15)
|
|
s.sendall(req.encode())
|
|
out = b""
|
|
try:
|
|
while True:
|
|
b = s.recv(65536)
|
|
if not b:
|
|
break
|
|
out += b
|
|
except socket.timeout:
|
|
sys.stderr.write("timed out after %d bytes\n" % len(out))
|
|
sys.exit(9)
|
|
s.close()
|
|
sys.stdout.write(out.decode("latin-1"))' "$1" "$2" "$3"
|
|
}
|
|
|
|
# GET $2, or fail with $3 when the reply never comes.
|
|
get() {
|
|
local out
|
|
out=$(request "$1" "$2" "") || fail "$2 did not answer: $3"
|
|
echo "${out}"
|
|
}
|
|
|
|
post() { request "$1" "" "$2"; }
|
|
|
|
url=$(start)
|
|
port=$(portof "${url}")
|
|
srv=$(srvpid)
|
|
test -n "${srv}" || fail "htsserver did not report its pid"
|
|
|
|
# Needs no session id and no project: html/server is a directory of the install.
|
|
reply=$(get "${port}" /server/ "an unauthenticated request wedged the server")
|
|
case "${reply}" in
|
|
"HTTP/1.0 404 "*) ;;
|
|
*) fail "a GUI directory did not answer 404: $(firstline "${reply}")" ;;
|
|
esac
|
|
|
|
# Every request body is gated by the session id.
|
|
reply=$(get "${port}" /server/index.html "the GUI is not served")
|
|
sid=$(firstline "$(echo "${reply}" |
|
|
sed -n 's/.*name="sid" value="\([0-9a-f]*\)".*/\1/p')")
|
|
test "${#sid}" -eq 32 || fail "did not scrape a 32-hex sid (got '${sid}')"
|
|
|
|
mkdir -p "${base}/proj/sub"
|
|
echo "PROBEMARKER" >"${base}/proj/probe.txt"
|
|
|
|
# mkfifo and ln -s may not work on a Windows checkout, so both stay optional.
|
|
refuse=(/website/ /website/sub/)
|
|
if mkfifo "${base}/proj/fifo.txt" 2>/dev/null; then
|
|
refuse+=(/website/fifo.txt)
|
|
fi
|
|
ln -s probe.txt "${base}/proj/link.txt" 2>/dev/null || true
|
|
|
|
# step4's "save settings" flow registers the project without crawling, and that
|
|
# is what arms the /website/ root.
|
|
body="sid=${sid}&command=httrack&command_do=save&winprofile=x"
|
|
body="${body}&path=${base}&projname=proj"
|
|
post "${port}" "${body}" >/dev/null || fail "the save request did not answer"
|
|
test -f "${base}/proj/hts-cache/winprofile.ini" ||
|
|
fail "the project was not registered: $(cat "${log}")"
|
|
|
|
# Positive control: every assertion below would pass on a server serving nothing.
|
|
served() {
|
|
case "$(get "$1" "$2" "$3")" in
|
|
*PROBEMARKER*) ;;
|
|
*) fail "$3" ;;
|
|
esac
|
|
}
|
|
served "${port}" /website/probe.txt "the registered project's mirror is not served"
|
|
if test -L "${base}/proj/link.txt"; then
|
|
# The guard stats rather than lstats, so a symlinked mirror file still serves.
|
|
served "${port}" /website/link.txt "a symlink to a mirror file is not served"
|
|
fi
|
|
|
|
# /website/ is where the GUI's own "browse mirrored site" link points.
|
|
for path in "${refuse[@]}"; do
|
|
reply=$(get "${port}" "${path}" "the server wedged on ${path}")
|
|
case "${reply}" in
|
|
"HTTP/1.0 404 "*) ;;
|
|
*) fail "${path} did not answer 404: $(firstline "${reply}")" ;;
|
|
esac
|
|
done
|
|
|
|
# The accept loop is single-threaded: one spin denies every later request.
|
|
alive "${srv}" || fail "the server died: $(cat "${log}")"
|
|
served "${port}" /website/probe.txt "the server stopped answering after a directory request"
|
|
|
|
# This fopen() is reached before any guard, so its read loop must stop on ferror().
|
|
mkdir -p "${base}/proj2/hts-cache/winprofile.ini"
|
|
reply=$(post "${port}" "sid=${sid}&path=${base}&loadprojname=proj2") ||
|
|
fail "a directory winprofile.ini wedged the server"
|
|
case "${reply}" in
|
|
"HTTP/1.0 3"*) ;;
|
|
*) fail "loading a project did not redirect: $(firstline "${reply}")" ;;
|
|
esac
|
|
|
|
alive "${srv}" || fail "the server died: $(cat "${log}")"
|
|
served "${port}" /website/probe.txt "the server stopped answering after a project load"
|
|
|
|
echo "PASS"
|