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}
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}
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}
${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
*** 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
${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
*** 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
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}
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
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}
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.
--- /dev/null
+#!/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_<x>.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())
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: