Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions tests/HOWTO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,24 @@ Notation
Defaults, set once for everything below: host 127.0.0.1, port 6667 (the subject's
port; also the one irssi is told to use), password pass. Files for the bonus
transfer are names under files/ (files/test.txt -> test.txt).
Run every command from the repo root.

Where to run
------------
Every command below is written to be typed from the repo root: the ft_irc folder,
the one that contains the Makefile, srcs/ and tests/.
cd <path to>/ft_irc
"make -C tests X" means "run make X inside tests/". The same command from inside
the folder is just "make X":
from the repo root from inside the folder
make -C tests cd tests && make
make -C tests/checks bonus cd tests/checks && make bonus
make -C tests/dynamic_tester lab cd tests/dynamic_tester && make lab
Do not combine both ("cd tests" then "make -C tests"): there is no tests/tests.
Nothing needs to be built by hand first: the tests run make and make bonus
themselves. Run them in a normal terminal window (Terminal, iTerm), at least
100 columns by 30 rows; an editor's built-in terminal is fine for the checks
and the tester, but the lab, live, step, monitor and clients modes draw a
full-screen text interface and want a real terminal. Ctrl-C stops any of them.

Requirements (macOS and Linux)
------------------------------
Expand Down Expand Up @@ -56,6 +73,8 @@ the server sources; scripts get their own fresh server on a free port.
longline 10 KB without CRLF from three clients, a fourth stays responsive
churn 20 members, broadcast storm, JOIN/PART churn, RST disconnects
closing what the client gets before its link is closed: 464, ERROR
syscalls close/accept/send/recv return codes traced through every close
path, ircserv and ircserv_bonus (a fd closed twice, a failed accept); needs cc
client parser message grammar: prefix, command, params, trailing
reply numeric reply formatting
state client state: nick casefolding, invites, prefix, flags
Expand All @@ -74,6 +93,7 @@ the server sources; scripts get their own fresh server on a free port.
Against a server you started yourself (./ircserv <port> <password>):
bash tests/checks/networking/session.sh [host] [port] [password] [nick] raw IRC session, already registered
python3 tests/checks/networking/<probe|throw|overlong|longline|churn|closing>.py [port] [password]
bash tests/checks/networking/syscalls.sh [repo root] [password] starts its own traced servers
bash tests/checks/bonus/bot.sh [host] [port] [password] [file] ./ircserv_bonus
bash tests/checks/bonus/transfer.sh [host] [port] [password] [nick] [file] [botnick] ./ircserv_bonus

Expand All @@ -86,6 +106,7 @@ writes artifacts/report.html.
make -C tests/dynamic_tester menu; Enter runs everything
make -C tests/dynamic_tester <mandatory|robustness|load|bonus|compare>
make -C tests/dynamic_tester live everything, server and client windows shown while it runs
make -C tests/dynamic_tester step same, paused after every scenario: Enter next, c continue, p pause
make -C tests/dynamic_tester debug everything, every line sent and received printed
make -C tests/dynamic_tester open open the report (macOS)
make -C tests/dynamic_tester <suite> [PORT=<port>] [PASSWORD=<password>]
Expand All @@ -111,7 +132,7 @@ writes artifacts/report.html.
==============================================================================
Interactive
==============================================================================
make -C tests/dynamic_tester lab # client windows in one terminal, server log, walkthrough
make -C tests/dynamic_tester lab # client windows in one terminal, server log, menu bar, walkthrough
make -C tests/dynamic_tester clients # irssi or nc client windows on a launched server
make -C tests/dynamic_tester <irssi|nc> # same, program preselected
make -C tests/dynamic_tester monitor # live dashboard; starts the server itself
Expand All @@ -126,8 +147,12 @@ writes artifacts/report.html.
terminal 2: make -C tests/dynamic_tester
# or any suite

Keys in the monitor: Tab moves clients -> log -> menu bar; in the menu bar Left/Right pick,
Enter or Up/Down change, b back; Up/Down Left/Right scroll the focused pane; q quits.
Keys in the monitor and the lab: Tab moves between the panes and ends on the menu bar;
in the menu bar Left/Right pick, Enter or Up/Down change, b back; Up/Down Left/Right
scroll the focused pane. Monitor: q quits.
Lab: typing goes to the focused client; Enter on an empty line runs the walkthrough step
shown above the input; type your own line to try something else first; /skip /back /stop;
the menu bar sets what the windows show, their order, log rows and filter, eol; Ctrl-D leaves.

------------------------------------------------------------------------------
Details: tests/dynamic_tester/README.md
6 changes: 3 additions & 3 deletions tests/checks/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
# commands and numerics, parser, replies (C++, no sockets) (client/)
# make -C tests/checks bonus bot dialogue, real DCC transfer (bash, fresh ircserv_bonus) (bonus/)
# make -C tests/checks socket one test by name: basic socket flood probe throw overlong longline
# churn closing parser reply state registration messaging operator bot transfer
# churn closing syscalls parser reply state registration messaging operator bot transfer
#
# By hand: bash tests/checks/run.sh <group|name...> (no argument = usage)

SHELL := /bin/bash
NAMES := basic socket flood probe throw overlong longline churn closing parser reply state registration messaging operator bot transfer
NAMES := basic socket flood probe throw overlong longline churn closing syscalls parser reply state registration messaging operator bot transfer

.PHONY: all basic networking client bonus $(NAMES) clean help

all networking bonus $(NAMES):
@bash "$(CURDIR)/run.sh" $@

clean:
@rm -f "$(CURDIR)"/.*.test "$(CURDIR)"/bonus/received.bin
@rm -f "$(CURDIR)"/.*.test "$(CURDIR)"/bonus/received.bin "$(CURDIR)"/networking/.syscalls.so

help:
@sed -n '2,15p' "$(CURDIR)/Makefile" | sed 's/^# \{0,1\}//'
53 changes: 53 additions & 0 deletions tests/checks/networking/syscalls.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* Preloaded into the server under test: logs every close() and every failed
* accept()/send()/recv() with its errno, so a second close of the same fd
* (EBADF) or an unexpected -1 is visible. Linux: LD_PRELOAD; macOS: dyld interpose. */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

static void note(const char *name, int fd, long r, int e) {
if (r < 0)
fprintf(stderr, "[sys] %s(%d) -> -1 %s\n", name, fd, strerror(e));
else if (name[0] == 'c')
fprintf(stderr, "[sys] %s(%d) -> %ld\n", name, fd, r);
}

#ifdef __APPLE__
#define INTERPOSE(mine, orig) \
__attribute__((used)) static struct { const void *m; const void *o; } \
_ip_##orig __attribute__((section("__DATA,__interpose"))) = \
{(const void *)(unsigned long)&mine, (const void *)(unsigned long)&orig};
int my_close(int fd) { int r = close(fd); int e = errno; note("close", fd, r, e); errno = e; return r; }
int my_accept(int fd, struct sockaddr *a, socklen_t *l) { int r = accept(fd, a, l); int e = errno; note("accept", fd, r, e); errno = e; return r; }
ssize_t my_send(int fd, const void *b, size_t n, int f) { ssize_t r = send(fd, b, n, f); int e = errno; note("send", fd, r, e); errno = e; return r; }
ssize_t my_recv(int fd, void *b, size_t n, int f) { ssize_t r = recv(fd, b, n, f); int e = errno; note("recv", fd, r, e); errno = e; return r; }
INTERPOSE(my_close, close)
INTERPOSE(my_accept, accept)
INTERPOSE(my_send, send)
INTERPOSE(my_recv, recv)
#else
int close(int fd) {
static int (*real)(int) = 0;
if (!real) real = dlsym(RTLD_NEXT, "close");
int r = real(fd); int e = errno; note("close", fd, r, e); errno = e; return r;
}
int accept(int fd, struct sockaddr *a, socklen_t *l) {
static int (*real)(int, struct sockaddr *, socklen_t *) = 0;
if (!real) real = dlsym(RTLD_NEXT, "accept");
int r = real(fd, a, l); int e = errno; note("accept", fd, r, e); errno = e; return r;
}
ssize_t send(int fd, const void *b, size_t n, int f) {
static ssize_t (*real)(int, const void *, size_t, int) = 0;
if (!real) real = dlsym(RTLD_NEXT, "send");
ssize_t r = real(fd, b, n, f); int e = errno; note("send", fd, r, e); errno = e; return r;
}
ssize_t recv(int fd, void *b, size_t n, int f) {
static ssize_t (*real)(int, void *, size_t, int) = 0;
if (!real) real = dlsym(RTLD_NEXT, "recv");
ssize_t r = real(fd, b, n, f); int e = errno; note("recv", fd, r, e); errno = e; return r;
}
#endif
114 changes: 114 additions & 0 deletions tests/checks/networking/syscalls.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/bin/bash
# Runs the server with close/accept/send/recv traced (syscalls.c preloaded) and
# reads the return codes afterwards:
# FAIL close() returned -1 (the same fd closed twice) or accept() failed
# ok every close() succeeded once
# Other failed calls (send/recv) are listed for information; EAGAIN is normal.
#
# ircserv: wrong password, QUIT, three over-long lines, an RST-closed
# client, a kicked client, SIGINT with clients connected
# ircserv_bonus: the same, plus a full transfer, an offer that is rejected and
# a new one after it, and SIGINT in the middle of a transfer
#
# bash tests/checks/networking/syscalls.sh [repo root] [password]
# Starts its own servers on free ports (the preload has to reach them).

set -u
ROOT=${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}
PASS=${2:-pass}
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB="$HERE/.syscalls.so"
BIG="$ROOT/files/.syscalls_big.bin"
failed=0
ok() { echo " ok $1"; }
bad() { echo " FAIL $1"; failed=1; }

echo "[trace library]"
if [ "$(uname)" = Darwin ]; then
cc -shared -o "$LIB" "$HERE/syscalls.c" 2>/dev/null && PRE="DYLD_INSERT_LIBRARIES=$LIB"
else
cc -shared -fPIC -o "$LIB" "$HERE/syscalls.c" -ldl 2>/dev/null && PRE="LD_PRELOAD=$LIB"
fi
[ -f "$LIB" ] && ok "built ($(basename "$LIB"))" || { bad "could not build syscalls.c (needs cc)"; exit 1; }
cleanup() { [ -n "${SRV:-}" ] && kill $SRV 2>/dev/null; rm -f "${TRACE:-}" "$LIB" "$BIG"; }
trap cleanup EXIT

trace_run() { # $1 = ircserv | ircserv_bonus
local bin=$1 bonus=0
[ "$bin" = ircserv_bonus ] && bonus=1
[ -x "$ROOT/$bin" ] || { echo " no $bin at $ROOT, skipped"; return; }
echo "[$bin: every close path, traced]"
TRACE=$(mktemp -t syscalls.XXXXXX)
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1])')
[ $bonus = 1 ] && head -c 20000000 /dev/urandom > "$BIG"
( cd "$ROOT" && exec env $PRE "./$bin" "$PORT" "$PASS" 2>"$TRACE" >/dev/null ) &
SRV=$!
for i in $(seq 1 40); do
python3 -c "import socket; socket.create_connection(('127.0.0.1', $PORT), timeout=0.5).close()" 2>/dev/null && break
sleep 0.1
done
python3 - "$PORT" "$PASS" "$SRV" "$bonus" <<'PYEOF'
import socket, time, re, os, signal, sys, struct
PORT, PW, SRV, BONUS = int(sys.argv[1]), sys.argv[2], int(sys.argv[3]), sys.argv[4] == "1"
def reg(n):
s = socket.create_connection(("127.0.0.1", PORT)); s.settimeout(1)
s.sendall(("PASS %s\r\nNICK %s\r\nUSER %s 0 * :x\r\n" % (PW, n, n)).encode()); time.sleep(0.3)
try: s.recv(4096)
except socket.timeout: pass
return s
def offer(a, b, fname):
a.sendall(("PRIVMSG DCCBot :!send bob %s\r\n" % fname).encode()); time.sleep(0.5)
d = b.recv(4096).decode(errors="replace")
m = re.search(r"DCC SEND \S+ (\S+) (\d+) (\d+)", d)
return (int(m.group(2)), int(m.group(3))) if m else (None, None)
def pull(port):
ds = socket.create_connection(("127.0.0.1", port)); ds.settimeout(3); got = 0
try:
while True:
c = ds.recv(65536)
if not c: break
got += len(c)
except socket.timeout: pass
ds.close(); return got
a = reg("alice"); b = reg("bob"); a.sendall(b"JOIN #x\r\n"); b.sendall(b"JOIN #x\r\n"); time.sleep(0.3)
w = socket.create_connection(("127.0.0.1", PORT)); w.sendall(b"PASS nope\r\nNICK w\r\nUSER w 0 * :w\r\n"); time.sleep(0.4); w.close()
q = reg("quitter"); q.sendall(b"QUIT :bye\r\n"); time.sleep(0.4); q.close()
o = reg("longy")
for _ in range(3): o.sendall(b"X" * 600 + b"\r\n"); time.sleep(0.2)
time.sleep(0.4); o.close()
k = reg("killed"); k.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)); k.close(); time.sleep(0.4)
a.sendall(b"KICK #x bob :out\r\n"); time.sleep(0.3)
print(" ok wrong password, QUIT, over-long close, RST close, KICK")
if BONUS:
b.sendall(b"JOIN #x\r\n"); time.sleep(0.2)
port, size = offer(a, b, "test.txt"); b.sendall(b"PRIVMSG DCCBot :!accept\r\n"); time.sleep(0.3)
got = pull(port); time.sleep(0.4)
print((" ok full transfer: %d of %d bytes" if got == size else " FAIL full transfer: %d of %d bytes") % (got, size))
offer(a, b, "test.txt"); b.sendall(b"PRIVMSG DCCBot :!reject\r\n"); time.sleep(0.5)
port, size = offer(a, b, "test.txt"); b.sendall(b"PRIVMSG DCCBot :!accept\r\n"); time.sleep(0.4)
pull(port); time.sleep(0.4)
print(" ok offer rejected, new offer accepted and transferred")
port, size = offer(a, b, ".syscalls_big.bin"); b.sendall(b"PRIVMSG DCCBot :!accept\r\n"); time.sleep(0.4)
ds = socket.create_connection(("127.0.0.1", port)); time.sleep(0.6) # connected, never reads
os.kill(SRV, signal.SIGINT); time.sleep(1.0)
print(" ok SIGINT with a transfer in progress")
else:
os.kill(SRV, signal.SIGINT); time.sleep(0.8)
print(" ok SIGINT with clients connected")
PYEOF
kill -INT $SRV 2>/dev/null; sleep 0.5; kill $SRV 2>/dev/null; wait $SRV 2>/dev/null; SRV=""
local closes dbl acc
closes=$(grep -c "^\[sys\] close(" "$TRACE")
dbl=$(grep "^\[sys\] close(" "$TRACE" | grep -c -- "-> -1")
acc=$(grep "^\[sys\] accept(" "$TRACE" | grep -- "-> -1" | grep -vc "again\|Resource temporarily")
if [ "$dbl" -eq 0 ]; then ok "every close() succeeded once ($closes calls)"; else
bad "close() returned -1 $dbl time(s): the same fd closed twice"
grep "^\[sys\] close(" "$TRACE" | grep -B1 -- "-> -1" | sed 's/^/ /'
fi
[ "$acc" -eq 0 ] && ok "no failed accept()" || bad "accept() failed $acc time(s)"
grep "^\[sys\] \(send\|recv\)(" "$TRACE" | grep -- "-> -1" | sed 's/^\[sys\] //' | sort | uniq -c | sed 's/^ *\([0-9]*\) \(.*\)$/ \2 x\1 (info)/'
rm -f "$TRACE" "$BIG"
}
trace_run ircserv
trace_run ircserv_bonus
exit $failed
10 changes: 9 additions & 1 deletion tests/checks/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ spec() {
longline) echo "networking/longline.py|ircserv|10 KB without CRLF from three clients while a fourth stays responsive" ;;
churn) echo "networking/churn.py|ircserv|20 members: broadcast storm, JOIN/PART churn, RST disconnects, a stalled member" ;;
closing) echo "networking/closing.py|ircserv|what the client gets before the server closes its link: 464, ERROR" ;;
syscalls) echo "networking/syscalls.sh|SELF|close/accept/send/recv return codes traced through every close path, both binaries (a fd closed twice, a failed accept)" ;;
parser) echo "client/parser.cpp||message grammar: prefix, command, params, trailing" ;;
reply) echo "client/reply.cpp||numeric reply formatting" ;;
state) echo "client/state.cpp|SRV|client state: nick casefolding, invites, identity, prefix, flags" ;;
Expand All @@ -46,7 +47,7 @@ spec() {
esac
}
G_BASIC="basic"
G_NET="socket flood probe throw overlong longline churn closing"
G_NET="socket flood probe throw overlong longline churn closing syscalls"
G_CLIENT="parser reply state registration messaging operator"
G_BONUS="bot transfer"
ALL="$G_BASIC $G_NET $G_CLIENT $G_BONUS"
Expand Down Expand Up @@ -105,6 +106,13 @@ for name in "${names[@]}"; do
IFS='|' read -r file kind desc <<< "$line"
file="$HERE/$file"; dir="$(dirname "$file")"
echo; echo "${B}${C}[ $name ]${N} ${D}${desc}${N}"
if [ "$kind" = SELF ]; then # the script starts (and traces) its own servers
build_server ircserv || { echo " ${R}[FAIL]${N} make failed"; failed+=("$name"); continue; }
build_server ircserv_bonus || true
bash "$file" "$ROOT" "$PASSWORD" 2>&1 | render; rc=${PIPESTATUS[0]}
[ "$rc" -eq 0 ] && passed+=("$name") || failed+=("$name")
continue
fi
case "$file" in
*.cpp)
objs=()
Expand Down
5 changes: 4 additions & 1 deletion tests/dynamic_tester/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# make everything -> artifacts/report.html
# make live everything, watching server + client windows live
# make step same, paused after every scenario (Enter = next, c = continue)
# make mandatory | robustness | load | bonus | compare
# make live-load the slow-reader load test with the live view
# make lab interactive: client windows + server log + help + /walk
Expand Down Expand Up @@ -31,7 +32,7 @@ SETS = $(foreach kv,$(SET),--set $(kv))
COMMON = --binary $(BINARY) --bonus-binary $(BONUS_BINARY) --host $(HOST) --port $(PORT) \
--password $(PASSWORD) --outdir $(OUTDIR) --report $(REPORT) --no-intro $(SETS)

.PHONY: all mandatory robustness load bonus compare terminal external debug live live-load \
.PHONY: all mandatory robustness load bonus compare terminal external debug live live-load step \
lab irssi nc clients monitor monitor-bonus attach config open clean help

all:
Expand All @@ -57,6 +58,8 @@ live:
@$(PYTHON) test_irc.py --mode all --live $(COMMON)
live-load:
@$(PYTHON) test_irc.py --mode load --live $(COMMON)
step:
@$(PYTHON) test_irc.py --mode all --live --step $(COMMON)

N ?= 3
lab:
Expand Down
Loading
Loading