From: Vsevolod Stakhov Date: Sun, 31 May 2026 19:48:45 +0000 (+0100) Subject: [Test] functional: centralize dummy helper readiness barrier X-Git-Tag: 4.1.0~11^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fb175294f64ac87d5f985342ae821178e675bda9;p=thirdparty%2Frspamd.git [Test] functional: centralize dummy helper readiness barrier Fix a start/scan race in the functional suite: dummy_* mock services were started and then connected to (by rspamd or the test) before they were listening. Under parallel pabot the short 2s PID waits timed out under CPU contention, one-shot helpers (clam/fprot/avast/p0f) left stale PID files so a same-port restart satisfied Wait Until Created instantly and raced the new bind, and p0f derived its PID path inconsistently between helper and suite. Every dummy_* helper already writes its PID only after server_bind/ server_activate, so PID-existence is a valid "listening" signal. This routes all helper startup through one barrier: * Start Dummy Service (lib/rspamd.robot): drop stale PID, start the helper, block until the PID file appears (5s). Single source of truth for startup ordering. * Wait Until Dummy Listening: active TCP-connect probe layered on top for loop servers (http/https/ssl) only; not used for one-shot or single-threaded smtp helpers, where a probe would consume the one session the test needs. Rewrite Run Dummy Http/Https/Llm/Http Early/Ssl/Udp/Clam/Fprot/Avast/p0f and the 168/169 SMTP suites to go through it; move SMTP temp files from /tmp to the per-worker RSPAMD_TMP_PREFIX; teach dummy_p0f.py to accept an explicit PID path. Add util/check_no_bare_dummy_start.py, run as a run-parallel.sh preflight, which fails if a suite reintroduces a bare Start Process ... dummy_*.py instead of using the barrier. --- diff --git a/test/functional/cases/001_merged/160_antivirus.robot b/test/functional/cases/001_merged/160_antivirus.robot index b0d83a7399..f63ed8ebc9 100644 --- a/test/functional/cases/001_merged/160_antivirus.robot +++ b/test/functional/cases/001_merged/160_antivirus.robot @@ -106,32 +106,23 @@ Double FProt Teardown Terminate Process ${process1} Terminate Process ${process2} -Run Dummy - [Arguments] @{varargs} - ${process} = Start Process @{varargs} - ${pid} = Get From List ${varargs} -1 - ${pass} = Run Keyword And Return Status Wait Until Created ${pid} - IF ${pass} - Return From Keyword - END - Wait For Process ${process} - ${res} = Get Process Result ${process} - Log To Console ${res.stdout} - Log To Console ${res.stderr} - Fail Dummy server failed to start - RETURN ${process} - Run Dummy Clam [Arguments] ${port} ${found}= ${pid}=${RSPAMD_TMP_PREFIX}/dummy_clamav-${port}.pid - ${process} = Run Dummy ${RSPAMD_TESTDIR}/util/dummy_clam.py ${port} ${found} ${pid} + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_clamav-${port}.log + ${process} = Start Dummy Service dummy_clam.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_clam.py ${port} ${found} ${pid} RETURN ${process} Run Dummy Fprot [Arguments] ${port} ${found}= ${pid}=${RSPAMD_TMP_PREFIX}/dummy_fprot-${port}.pid - ${process} = Run Dummy ${RSPAMD_TESTDIR}/util/dummy_fprot.py ${port} ${found} ${pid} + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_fprot-${port}.log + ${process} = Start Dummy Service dummy_fprot.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_fprot.py ${port} ${found} ${pid} RETURN ${process} Run Dummy Avast [Arguments] ${port} ${found}= ${pid}=${RSPAMD_TMP_PREFIX}/dummy_avast-${port}.pid - ${process} = Run Dummy ${RSPAMD_TESTDIR}/util/dummy_avast.py ${port} ${found} ${pid} + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_avast-${port}.log + ${process} = Start Dummy Service dummy_avast.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_avast.py ${port} ${found} ${pid} RETURN ${process} diff --git a/test/functional/cases/001_merged/310_udp.robot b/test/functional/cases/001_merged/310_udp.robot index 4df7f994de..b0350f5061 100644 --- a/test/functional/cases/001_merged/310_udp.robot +++ b/test/functional/cases/001_merged/310_udp.robot @@ -37,6 +37,7 @@ UDP Teardown Run Dummy UDP [Arguments] ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_udp-${RSPAMD_PORT_DUMMY_UDP}.pid - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_udp.py ${RSPAMD_PORT_DUMMY_UDP} ${pid} - Wait Until Created ${pid} + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_udp-${RSPAMD_PORT_DUMMY_UDP}.log + ${result} = Start Dummy Service dummy_udp.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_udp.py ${RSPAMD_PORT_DUMMY_UDP} ${pid} Set Suite Variable ${DUMMY_UDP_PROC} ${result} diff --git a/test/functional/cases/161_p0f.robot b/test/functional/cases/161_p0f.robot index 5d43fd5e83..177e58a364 100644 --- a/test/functional/cases/161_p0f.robot +++ b/test/functional/cases/161_p0f.robot @@ -88,15 +88,15 @@ p0f Teardown Terminate All Processes kill=True Shutdown p0f - # dummy_p0f derives its pidfile from the socket basename; mirror that here. - ${sockname} = Evaluate os.path.basename($RSPAMD_P0F_SOCKET) modules=os - ${pidfile} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_p0f-${sockname}.pid + ${pidfile} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_p0f.pid ${p0f_pid} = Get File if exists ${pidfile} Run Keyword if ${p0f_pid} Shutdown Process With Children ${p0f_pid} Run Dummy p0f [Arguments] ${socket}=${RSPAMD_P0F_SOCKET} ${os}=linux ${status}=ok - ${sockname} = Evaluate os.path.basename($socket) modules=os - ${pidfile} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_p0f-${sockname}.pid - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_p0f.py ${socket} ${os} ${status} - Wait Until Created ${pidfile} + # Pass an explicit pid path so the helper writes exactly where we wait, + # and route through Start Dummy Service for the stale-pid-safe barrier. + ${pidfile} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_p0f.pid + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_p0f.log + Start Dummy Service dummy_p0f.py ${pidfile} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_p0f.py ${socket} ${os} ${status} ${pidfile} diff --git a/test/functional/cases/168_mx_check_greeting.robot b/test/functional/cases/168_mx_check_greeting.robot index 6a5810598b..7a308d07ff 100644 --- a/test/functional/cases/168_mx_check_greeting.robot +++ b/test/functional/cases/168_mx_check_greeting.robot @@ -14,7 +14,7 @@ ${REDIS_SCOPE} Suite ${RSPAMD_SCOPE} Suite ${RSPAMD_URL_TLD} ${RSPAMD_TESTDIR}/../lua/unit/test_tld.dat ${SETTINGS} {symbols_enabled = [MX_INVALID]} -${SINGLE_STATUS} /tmp/dummy_smtp_greeting_single.status +${SINGLE_STATUS} ${RSPAMD_TMP_PREFIX}/dummy_smtp_greeting_single.status *** Test Cases *** Silent SMTP listener triggers MX_TIMEOUT_READ @@ -44,20 +44,13 @@ Non-SMTP line triggers MX_INVALID *** Keywords *** Start Plain Dummy [Arguments] ${mode} ${host} - Start Process ${RSPAMD_TESTDIR}/util/dummy_smtp.py - ... --port 11125 --mode ${mode} --host ${host} - ... stderr=/tmp/dummy_smtp_${mode}.log - ... stdout=/tmp/dummy_smtp_${mode}.log - Wait Until Created /tmp/dummy_smtp_${mode}.pid timeout=2 second + Start Dummy Smtp 11125 ${mode} ${host} + ... ${RSPAMD_TMP_PREFIX}/dummy_smtp_${mode}.pid Mx Greeting Setup Start Plain Dummy silent 127.0.0.1 - Start Process ${RSPAMD_TESTDIR}/util/dummy_smtp.py - ... --port 11125 --mode greeting_single --host 127.0.0.2 - ... --status-file ${SINGLE_STATUS} - ... stderr=/tmp/dummy_smtp_greeting_single.log - ... stdout=/tmp/dummy_smtp_greeting_single.log - Wait Until Created /tmp/dummy_smtp_greeting_single.pid timeout=2 second + Start Dummy Smtp 11125 greeting_single 127.0.0.2 + ... ${RSPAMD_TMP_PREFIX}/dummy_smtp_greeting_single.pid --status-file ${SINGLE_STATUS} Start Plain Dummy error 127.0.0.3 Start Plain Dummy messy 127.0.0.4 Rspamd Redis Setup diff --git a/test/functional/cases/169_mx_check_greeting_quit.robot b/test/functional/cases/169_mx_check_greeting_quit.robot index 0d2e96a6f2..6435d5b89a 100644 --- a/test/functional/cases/169_mx_check_greeting_quit.robot +++ b/test/functional/cases/169_mx_check_greeting_quit.robot @@ -14,8 +14,8 @@ ${REDIS_SCOPE} Suite ${RSPAMD_SCOPE} Suite ${RSPAMD_URL_TLD} ${RSPAMD_TESTDIR}/../lua/unit/test_tld.dat ${SETTINGS} {symbols_enabled = [MX_INVALID]} -${PROPER_STATUS} /tmp/dummy_smtp_greeting_proper.status -${SLOW_STATUS} /tmp/dummy_smtp_greeting_slow.status +${PROPER_STATUS} ${RSPAMD_TMP_PREFIX}/dummy_smtp_greeting_proper.status +${SLOW_STATUS} ${RSPAMD_TMP_PREFIX}/dummy_smtp_greeting_slow.status *** Test Cases *** Multi-line greeting with send_quit=true emits MX_GOOD with QUIT after final line @@ -32,16 +32,9 @@ Slow second banner line triggers MX_TIMEOUT_READ *** Keywords *** Start Greeting Dummy [Arguments] ${host} ${between_wait} ${status_file} ${pid_suffix} - Start Process ${RSPAMD_TESTDIR}/util/dummy_smtp.py - ... --port 11125 - ... --mode greeting_multi - ... --host ${host} - ... --between-wait ${between_wait} - ... --status-file ${status_file} - ... --pid-file /tmp/dummy_smtp_${pid_suffix}.pid - ... stderr=/tmp/dummy_smtp_${pid_suffix}.log - ... stdout=/tmp/dummy_smtp_${pid_suffix}.log - Wait Until Created /tmp/dummy_smtp_${pid_suffix}.pid timeout=2 second + Start Dummy Smtp 11125 greeting_multi ${host} + ... ${RSPAMD_TMP_PREFIX}/dummy_smtp_${pid_suffix}.pid + ... --between-wait ${between_wait} --status-file ${status_file} Mx Quit Setup Start Greeting Dummy 127.0.0.6 0.2 ${PROPER_STATUS} greeting_proper diff --git a/test/functional/cases/230_tcp.robot b/test/functional/cases/230_tcp.robot index 2481b95cd5..729fe9b616 100644 --- a/test/functional/cases/230_tcp.robot +++ b/test/functional/cases/230_tcp.robot @@ -82,9 +82,11 @@ Servers Teardown Run Dummy Ssl [Arguments] ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_ssl-${RSPAMD_PORT_DUMMY_SSL}.pid + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_ssl-${RSPAMD_PORT_DUMMY_SSL}.log Set Suite Variable ${DUMMY_SSL_PID_FILE} ${pid} - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_ssl.py ${RSPAMD_TESTDIR}/util/server.pem ${RSPAMD_PORT_DUMMY_SSL} ${pid} - Wait Until Created ${pid} timeout=2 second + Start Dummy Service dummy_ssl.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_ssl.py ${RSPAMD_TESTDIR}/util/server.pem ${RSPAMD_PORT_DUMMY_SSL} ${pid} + Wait Until Dummy Listening ${RSPAMD_LOCAL_ADDR} ${RSPAMD_PORT_DUMMY_SSL} Teardown Dummy Ssl ${ssl_pid} = Get File ${DUMMY_SSL_PID_FILE} diff --git a/test/functional/lib/rspamd.robot b/test/functional/lib/rspamd.robot index 25c63e8c81..1f0514502f 100644 --- a/test/functional/lib/rspamd.robot +++ b/test/functional/lib/rspamd.robot @@ -576,57 +576,74 @@ Run Control Command JSON Log ${result.stderr} RETURN ${result} -Run Dummy Http - ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_http-${RSPAMD_PORT_DUMMY_HTTP}.pid - ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_http-${RSPAMD_PORT_DUMMY_HTTP}.log - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_http.py -pf ${pid} -p ${RSPAMD_PORT_DUMMY_HTTP} - ... stderr=${log} stdout=${log} - ${status} ${error} = Run Keyword And Ignore Error Wait Until Created ${pid} timeout=2 second +Start Dummy Service + [Documentation] Start a dummy_* helper and block until it is ready, + ... then return its process handle. Readiness is the PID file: every + ... dummy_* helper calls dummy_killer.write_pid() only AFTER it has + ... bound and activated its listening socket (see server_bind / + ... server_activate / server.start in util/dummy_*.py), so the moment + ... the PID file appears the kernel is already accepting connections. + ... This keyword is the ONE place that barrier lives -- start every + ... dummy through here (or a Run Dummy * / Start Dummy * wrapper), + ... never via a bare Start Process followed straight by a scan, or you + ... reintroduce the start/scan race that flakes under parallel pabot. + [Arguments] ${name} ${pidfile} ${logfile} @{command} + # Drop any stale PID file from a previous instance on this same path. + # One-shot helpers (clam/fprot/avast/p0f) exit after a single request + # and leave their PID file behind; without this a same-port restart + # would satisfy Wait Until Created instantly and race the new bind. + Remove File ${pidfile} + ${result} = Start Process @{command} stdout=${logfile} stderr=${logfile} + ${status} ${error} = Run Keyword And Ignore Error + ... Wait Until Created ${pidfile} timeout=5 second IF '${status}' == 'FAIL' - ${logstatus} ${out} = Run Keyword And Ignore Error Get File ${log} + ${logstatus} ${out} = Run Keyword And Ignore Error Get File ${logfile} IF '${logstatus}' == 'PASS' - Log dummy_http.py failed to start. Log output:\n${out} level=ERROR + Log ${name} failed to start. Log output:\n${out} level=ERROR ELSE - Log dummy_http.py failed to start. No log file found at ${log} level=ERROR + Log ${name} failed to start. No log file found at ${logfile} level=ERROR END - Fail dummy_http.py did not create PID file in 2 seconds - END + Fail ${name} did not create PID file in 5 seconds + END + RETURN ${result} + +Wait Until Dummy Listening + [Documentation] Belt-and-suspenders readiness probe on top of the PID + ... barrier: block until a TCP connect to host:port actually succeeds. + ... Self-contained (BuiltIn Evaluate, no library dependency). Use ONLY + ... for helpers that loop and accept many connections (http/https/ssl). + ... Do NOT probe one-shot helpers (clam/fprot/avast/p0f) or the + ... single-threaded smtp helper -- a probe connection would consume or + ... block the very session the scan needs; for those the PID barrier in + ... Start Dummy Service is the correct and sufficient readiness signal. + [Arguments] ${host} ${port} + Wait Until Keyword Succeeds 15x 0.2s + ... Evaluate __import__('socket').create_connection(("${host}", ${port}), 1).close() + +Run Dummy Http + ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_http-${RSPAMD_PORT_DUMMY_HTTP}.pid + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_http-${RSPAMD_PORT_DUMMY_HTTP}.log + ${result} = Start Dummy Service dummy_http.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_http.py -pf ${pid} -p ${RSPAMD_PORT_DUMMY_HTTP} + Wait Until Dummy Listening ${RSPAMD_LOCAL_ADDR} ${RSPAMD_PORT_DUMMY_HTTP} Export Scoped Variables ${RSPAMD_SCOPE} DUMMY_HTTP_PROC=${result} DUMMY_HTTP_LOG=${log} Run Dummy Https ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_https-${RSPAMD_PORT_DUMMY_HTTPS}.pid ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_https-${RSPAMD_PORT_DUMMY_HTTPS}.log - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_http.py + ${result} = Start Dummy Service dummy_https.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_http.py ... -c ${RSPAMD_TESTDIR}/util/server.pem -k ${RSPAMD_TESTDIR}/util/server.pem ... -pf ${pid} -p ${RSPAMD_PORT_DUMMY_HTTPS} - ... stderr=${log} stdout=${log} - ${status} ${error} = Run Keyword And Ignore Error Wait Until Created ${pid} timeout=2 second - IF '${status}' == 'FAIL' - ${logstatus} ${out} = Run Keyword And Ignore Error Get File ${log} - IF '${logstatus}' == 'PASS' - Log dummy_https.py failed to start. Log output:\n${out} level=ERROR - ELSE - Log dummy_https.py failed to start. No log file found at ${log} level=ERROR - END - Fail dummy_https.py did not create PID file in 2 seconds - END + Wait Until Dummy Listening ${RSPAMD_LOCAL_ADDR} ${RSPAMD_PORT_DUMMY_HTTPS} Export Scoped Variables ${RSPAMD_SCOPE} DUMMY_HTTPS_PROC=${result} Run Dummy Llm ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_llm-${RSPAMD_PORT_DUMMY_HTTP}.pid ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_llm-${RSPAMD_PORT_DUMMY_HTTP}.log - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_llm.py ${RSPAMD_PORT_DUMMY_HTTP} ${pid} - ... stderr=${log} stdout=${log} - ${status} ${error} = Run Keyword And Ignore Error Wait Until Created ${pid} timeout=2 second - IF '${status}' == 'FAIL' - ${logstatus} ${out} = Run Keyword And Ignore Error Get File ${log} - IF '${logstatus}' == 'PASS' - Log dummy_llm.py failed to start. Log output:\n${out} level=ERROR - ELSE - Log dummy_llm.py failed to start. No log file found at ${log} level=ERROR - END - Fail dummy_llm.py did not create PID file in 2 seconds - END + ${result} = Start Dummy Service dummy_llm.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_llm.py ${RSPAMD_PORT_DUMMY_HTTP} ${pid} + Wait Until Dummy Listening ${RSPAMD_LOCAL_ADDR} ${RSPAMD_PORT_DUMMY_HTTP} Export Scoped Variables ${RSPAMD_SCOPE} DUMMY_LLM_PROC=${result} Dummy Llm Teardown @@ -644,20 +661,26 @@ Dummy Https Teardown Run Dummy Http Early Response ${pid} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_http_early-${RSPAMD_PORT_DUMMY_HTTP_EARLY}.pid ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_http_early-${RSPAMD_PORT_DUMMY_HTTP_EARLY}.log - ${result} = Start Process ${RSPAMD_TESTDIR}/util/dummy_http_early_response.py -pf ${pid} -p ${RSPAMD_PORT_DUMMY_HTTP_EARLY} - ... stderr=${log} stdout=${log} - ${status} ${error} = Run Keyword And Ignore Error Wait Until Created ${pid} timeout=2 second - IF '${status}' == 'FAIL' - ${logstatus} ${out} = Run Keyword And Ignore Error Get File ${log} - IF '${logstatus}' == 'PASS' - Log dummy_http_early_response.py failed to start. Log output:\n${out} level=ERROR - ELSE - Log dummy_http_early_response.py failed to start. No log file found at ${log} level=ERROR - END - Fail dummy_http_early_response.py did not create PID file in 2 seconds - END + ${result} = Start Dummy Service dummy_http_early_response.py ${pid} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_http_early_response.py -pf ${pid} -p ${RSPAMD_PORT_DUMMY_HTTP_EARLY} + Wait Until Dummy Listening ${RSPAMD_LOCAL_ADDR} ${RSPAMD_PORT_DUMMY_HTTP_EARLY} Export Scoped Variables ${RSPAMD_SCOPE} DUMMY_HTTP_EARLY_PROC=${result} Dummy Http Early Teardown Terminate Process ${DUMMY_HTTP_EARLY_PROC} Wait For Process ${DUMMY_HTTP_EARLY_PROC} + +Start Dummy Smtp + [Documentation] Start dummy_smtp.py and block until it is listening, + ... then return the process handle for teardown. No connect probe here: + ... the smtp helper runs single-threaded and its modes hold the handler + ... (silent sleeps 30s; greeting modes drive a state machine and write a + ... status file), so a probe connection would borrow the very session + ... the scan needs -- the PID barrier in Start Dummy Service is correct. + ... @{extra} carries optional flags such as --status-file / --between-wait. + [Arguments] ${port} ${mode} ${host} ${pidfile} @{extra} + ${log} = Set Variable ${RSPAMD_TMP_PREFIX}/dummy_smtp-${mode}-${host}.log + ${result} = Start Dummy Service dummy_smtp.py ${pidfile} ${log} + ... ${RSPAMD_TESTDIR}/util/dummy_smtp.py --port ${port} --mode ${mode} + ... --host ${host} --pid-file ${pidfile} @{extra} + RETURN ${result} diff --git a/test/functional/run-parallel.sh b/test/functional/run-parallel.sh index cdb8d06594..d423999d4c 100755 --- a/test/functional/run-parallel.sh +++ b/test/functional/run-parallel.sh @@ -41,6 +41,11 @@ if ! command -v pabot >/dev/null 2>&1; then exit 1 fi +# Preflight: every dummy_* helper must be started through the centralized +# Start Dummy Service barrier (lib/rspamd.robot), never a bare Start Process, +# or the start/scan race that flakes under parallel execution comes back. +python3 "$SCRIPT_DIR/util/check_no_bare_dummy_start.py" || exit 1 + # Suites that still bake dummy_http/llm/udp/http_early port numbers into # Lua test scripts or configs. Track in task #5 follow-up; tag and exclude # until those are templated. diff --git a/test/functional/util/check_no_bare_dummy_start.py b/test/functional/util/check_no_bare_dummy_start.py new file mode 100755 index 0000000000..2b06865f00 --- /dev/null +++ b/test/functional/util/check_no_bare_dummy_start.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Guard against reintroducing the dummy-helper start/scan race. + +Every dummy_* helper must be started through the centralized +``Start Dummy Service`` keyword (or one of the ``Run Dummy *`` / +``Start Dummy *`` wrappers) defined in ``test/functional/lib/rspamd.robot``. +Those wrappers block until the helper's PID file appears -- which each +helper writes only after it has bound and is listening -- so the rspamd +worker (or the test) never races a not-yet-listening helper. + +A bare ``Start Process .../dummy_.py`` inside a suite bypasses that +barrier and reintroduces the flakiness this guard exists to prevent. +This script exits non-zero if it finds one. + +Usage: + python3 test/functional/util/check_no_bare_dummy_start.py +""" + +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +CASES = os.path.normpath(os.path.join(HERE, '..', 'cases')) +REPO = os.path.normpath(os.path.join(HERE, '..', '..', '..')) + +# A line that starts a dummy helper directly via Robot's Process library. +BARE = re.compile(r'Start Process\b.*dummy_[A-Za-z0-9_]*\.py') + + +def main(): + offenders = [] + for root, _dirs, files in os.walk(CASES): + for name in sorted(files): + if not name.endswith('.robot'): + continue + path = os.path.join(root, name) + with open(path, encoding='utf-8') as fh: + for lineno, line in enumerate(fh, 1): + if line.lstrip().startswith('#'): + continue + if BARE.search(line): + offenders.append((path, lineno, line.strip())) + + if offenders: + sys.stderr.write( + "ERROR: bare 'Start Process ... dummy_*.py' found in functional " + "suites.\n" + "Start dummy helpers through the 'Start Dummy Service' keyword (or " + "a\n" + "Run Dummy * / Start Dummy * wrapper) in lib/rspamd.robot so they " + "block\n" + "until the helper is listening. Offending lines:\n\n") + for path, lineno, text in offenders: + sys.stderr.write( + " {}:{}: {}\n".format(os.path.relpath(path, REPO), lineno, text)) + return 1 + + print("OK: no bare dummy_*.py 'Start Process' in {}".format( + os.path.relpath(CASES, REPO))) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/test/functional/util/dummy_p0f.py b/test/functional/util/dummy_p0f.py index 130474ddd4..72cd2c669d 100755 --- a/test/functional/util/dummy_p0f.py +++ b/test/functional/util/dummy_p0f.py @@ -86,9 +86,14 @@ if __name__ == "__main__": server.server_activate() dummy_killer.setup_killer(server) - # Derive PID path from socket basename so multiple workers/instances - # never collide on the historical /tmp/dummy_p0f.pid. - pid_path = dummy_pidfile.pid_path('p0f', os.path.basename(SOCK)) + # PID path: explicit 4th arg if the harness passed one (so it can wait + # on exactly this path), else derive from the socket basename so + # multiple workers/instances never collide on the historical + # /tmp/dummy_p0f.pid. + if alen > 4: + pid_path = sys.argv[4] + else: + pid_path = dummy_pidfile.pid_path('p0f', os.path.basename(SOCK)) dummy_killer.write_pid(pid_path) try: