mirror of
https://github.com/xroche/httrack.git
synced 2026-07-28 19:43:02 +03:00
* Match captured output with a here-string, not a pipe into grep -q grep -q exits on the first match, so whatever the producer still had to write takes SIGPIPE; under pipefail that becomes the pipeline's status and an assertion that held reports failure. bash issues one write() per line, so any match that is not on the last line is exposed. Converts every test assertion whose producer is a shell builtin or shell function, including two pipelines used as an if condition where the SIGPIPE silently flips the branch. Generalizes the AGENTS.md bullet, which only covered the "&& fail" spelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Xavier Roche <roche@httrack.com> * Convert the remaining pipe-into-grep -q sites The self-test drivers survive only because the matched line is the last thing httrack prints; one added line of output turns a pass into 141. Nothing in tests/ pipes into grep -q now. The two zlib drivers claimed the harness might run them under a POSIX /bin/sh: it does not. configure resolves $(BASH) to bash, test-timeout.sh execs it, and 01_zlib-warc-wacz.test already uses "set -o pipefail" (which dash lacks) on the macOS leg. Kept the half that is true, BSD tool flags. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Xavier Roche <roche@httrack.com> --------- Signed-off-by: Xavier Roche <roche@httrack.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
177 lines
5.9 KiB
Bash
177 lines
5.9 KiB
Bash
#!/bin/bash
|
|
#
|
|
# The "save settings" failure messages quote a project path the request body
|
|
# supplies, so composing them must clip instead of running off a fixed buffer.
|
|
|
|
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
|
|
}
|
|
|
|
# ERROR_MESSAGE_MAX - 1 in htsserver.c.
|
|
msgmax=1023
|
|
|
|
srv=
|
|
log=$(mktemp)
|
|
# Physical path: macOS resolves TMPDIR through /var -> private/var, and the
|
|
# paths built below sit within a few bytes of its 1024-byte PATH_MAX.
|
|
base=$(cd "$(mktemp -d)" && pwd -P)
|
|
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
|
|
|
|
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 XFSZ
|
|
# Caps regular-file writes, which is how the failing-write branch below
|
|
# is reached without /dev/full; the announce rides a pipe, exempt.
|
|
ulimit -f 8
|
|
exec htsserver "${distdir}/" --port "${port}" 2>&1
|
|
) | cat >"${log}" &
|
|
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}"
|
|
}
|
|
|
|
srvpid() { sed -n 's/^PID=//p' "${log}" | head -1; }
|
|
|
|
alive() { kill -0 "$1" 2>/dev/null; }
|
|
|
|
portof() { echo "${1##*:}" | tr -d /; }
|
|
|
|
# Raw request to 127.0.0.1:$1: GET the path $2, or POST the body $3 to / when
|
|
# $2 is empty. Prints the reply.
|
|
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(30)
|
|
s.sendall(req.encode())
|
|
out = b""
|
|
while True:
|
|
b = s.recv(65536)
|
|
if not b:
|
|
break
|
|
out += b
|
|
s.close()
|
|
sys.stdout.write(out.decode("latin-1"))' "$1" "$2" "$3"
|
|
}
|
|
|
|
get() { request "$1" "$2" ""; }
|
|
post() { request "$1" "" "$2"; }
|
|
|
|
# Echo a path under the root $1 exactly $2 bytes long, in NAME_MAX-sized parts.
|
|
padpath() {
|
|
local root=$1 want=$2 p=$1 last
|
|
while test $((${#p} + 203)) -lt "${want}"; do
|
|
p="${p}/$(printf '%0200d' 0)"
|
|
done
|
|
last=$((want - ${#p} - 1))
|
|
test "${last}" -ge 1 || fail "cannot build a ${want}-byte path under ${root}"
|
|
printf '%s/%0*d' "${p}" "${last}" 0
|
|
}
|
|
|
|
# Fail unless the error page renders the message opening with $1 about project
|
|
# path $2, clipped to msgmax. error.html puts ${error} alone on its own line.
|
|
check_clipped() {
|
|
local prefix=$1 full=$((${#1} + ${#2})) page line shown
|
|
test "${full}" -gt "${msgmax}" ||
|
|
fail "a ${full}-byte message fits in ${msgmax}: it cannot show a clip"
|
|
page=$(get "${port}" /server/error.html | tr -d '\r')
|
|
line=$(printf '%s\n' "${page}" | grep "^${prefix}") || {
|
|
shown=$(printf '%s\n' "${page}" | grep '^Unable to ' | cut -c1-80 || true)
|
|
fail "the error page reports '${shown:-nothing}', want: ${prefix}"
|
|
}
|
|
test "${#line}" -eq "${msgmax}" ||
|
|
fail "the message rendered ${#line} bytes, want ${full} clipped to ${msgmax}"
|
|
}
|
|
|
|
url=$(start)
|
|
port=$(portof "${url}")
|
|
srv=$(srvpid)
|
|
test -n "${srv}" || fail "htsserver did not report its pid"
|
|
|
|
# Every request body is gated by the session id (78_webhttrack-sid.test).
|
|
sid=$(get "${port}" /server/index.html |
|
|
sed -n 's/.*name="sid" value="\([0-9a-f]*\)".*/\1/p' | head -1)
|
|
test "${#sid}" -eq 32 || fail "did not scrape a 32-hex sid (got '${sid}')"
|
|
|
|
save() {
|
|
post "${port}" "sid=${sid}&command=httrack&command_do=save&winprofile=$2&path=$1&projname=$3"
|
|
}
|
|
|
|
# A project path too long to be created at all.
|
|
path=$(padpath "${base}/nodir" 1098)
|
|
save "${path}" x p >/dev/null
|
|
alive "${srv}" ||
|
|
fail "an over-long project path crashed the server: $(cat "${log}")"
|
|
check_clipped "Unable to create the directory structure in " "${path}/p"
|
|
|
|
# A creatable project path whose hts-cache is not a directory, so the init file
|
|
# under it cannot be opened.
|
|
path=$(padpath "${base}/isdir" 993)
|
|
mkdir -p "${path}/p"
|
|
ln -s /dev/null "${path}/p/hts-cache"
|
|
save "${path}" x p >/dev/null
|
|
alive "${srv}" || fail "an unwritable init file crashed the server: $(cat "${log}")"
|
|
check_clipped "Unable to create the init file " "${path}/p"
|
|
|
|
# The init file opens, then the write fails: the profile is past both stdio's
|
|
# buffer, so fwrite() reaches the descriptor, and the server's file-size limit.
|
|
path=$(padpath "${base}/full" 993)
|
|
mkdir -p "${path}/p/hts-cache"
|
|
profile=$(printf '%01024d' 0)
|
|
profile=${profile}${profile}${profile}${profile}
|
|
profile=${profile}${profile}${profile}${profile}
|
|
test "${#profile}" -eq 16384 || fail "built a ${#profile}-byte profile, want 16384"
|
|
save "${path}" "${profile}" p >/dev/null
|
|
alive "${srv}" || fail "a short init file write crashed the server: $(cat "${log}")"
|
|
written=$(wc -c <"${path}/p/hts-cache/winprofile.ini" | tr -d ' ')
|
|
test "${written}" -lt "${#profile}" ||
|
|
fail "the file-size limit did not stop the write (${written} bytes landed)"
|
|
check_clipped "Unable to write ${#profile} bytes in the the init file " "${path}/p"
|
|
|
|
reply=$(get "${port}" /server/index.html || true)
|
|
grep -q '200 OK' <<<"$reply" ||
|
|
fail "the server stopped answering after the refused saves"
|
|
|
|
echo "PASS"
|