[PATCH 08 of 10] tests: use simple mock smtp server instead of deprecated asyncore smtpd
Mads Kiilerich
mads at kiilerich.com
Wed Jun 28 00:06:53 UTC 2023
# HG changeset patch
# User Mads Kiilerich <mads at kiilerich.com>
# Date 1679586312 -3600
# Thu Mar 23 16:45:12 2023 +0100
# Branch stable
# Node ID 7d0800b9c059349f6ad373215e718ddc7455ee91
# Parent 0fc04b7dda3c22b6eda5385e59be8307f883a88e
tests: use simple mock smtp server instead of deprecated asyncore smtpd
test-patchbomb-tls.t would fail with:
.../hg/tests/dummysmtpd.py:6: DeprecationWarning: The asyncore module is deprecated and will be removed in Python 3.12. The recommended replacement is asyncio
import asyncore
.../hg/tests/dummysmtpd.py:8: DeprecationWarning: The smtpd module is deprecated and unmaintained and will be removed in Python 3.12. Please see aiosmtpd (https://aiosmtpd.readthedocs.io/) for the recommended replacement.
import smtpd
The recommended migration path to the standalone asiosmtpd would be overkill.
The tests do not need a full smtp server - we can just use a very simple mock
hack to preserve the existing test coverage.
diff --git a/tests/dummysmtpd.py b/tests/dummysmtpd.py
--- a/tests/dummysmtpd.py
+++ b/tests/dummysmtpd.py
@@ -3,12 +3,11 @@
"""dummy SMTP server for use in tests"""
-import asyncore
import optparse
-import smtpd
+import os
+import socket
import ssl
import sys
-import traceback
from mercurial import (
pycompat,
@@ -18,57 +17,97 @@ from mercurial import (
)
+if os.environ.get('HGIPV6', '0') == '1':
+ family = socket.AF_INET6
+else:
+ family = socket.AF_INET
+
+
def log(msg):
sys.stdout.write(msg)
sys.stdout.flush()
-class dummysmtpserver(smtpd.SMTPServer):
- def __init__(self, localaddr):
- smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None)
+def mocksmtpserversession(conn, addr):
+ conn.send(b'220 smtp.example.com ESMTP\r\n')
+
+ line = conn.recv(1024)
+ if not line.lower().startswith(b'ehlo '):
+ log('no hello: %s\n' % line)
+ return
+
+ conn.send(b'250 Hello\r\n')
+
+ line = conn.recv(1024)
+ if not line.lower().startswith(b'mail from:'):
+ log('no mail from: %s\n' % line)
+ return
+ mailfrom = line[10:].decode().rstrip()
+ if mailfrom.startswith('<') and mailfrom.endswith('>'):
+ mailfrom = mailfrom[1:-1]
+
+ conn.send(b'250 Ok\r\n')
- def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
- log(
- '%s from=%s to=%s\n%s\n'
- % (peer[0], mailfrom, ', '.join(rcpttos), data.decode())
- )
+ rcpttos = []
+ while True:
+ line = conn.recv(1024)
+ if not line.lower().startswith(b'rcpt to:'):
+ break
+ rcptto = line[8:].decode().rstrip()
+ if rcptto.startswith('<') and rcptto.endswith('>'):
+ rcptto = rcptto[1:-1]
+ rcpttos.append(rcptto)
+
+ conn.send(b'250 Ok\r\n')
+
+ if not line.lower().strip() == b'data':
+ log('no rcpt to or data: %s' % line)
+
+ conn.send(b'354 Go ahead\r\n')
- def handle_error(self):
- # On Windows, a bad SSL connection sometimes generates a WSAECONNRESET.
- # The default handler will shutdown this server, and then both the
- # current connection and subsequent ones fail on the client side with
- # "No connection could be made because the target machine actively
- # refused it". If we eat the error, then the client properly aborts in
- # the expected way, and the server is available for subsequent requests.
- traceback.print_exc()
+ data = b''
+ while True:
+ line = conn.recv(1024)
+ if not line:
+ log('connection closed before end of data')
+ break
+ data += line
+ if data.endswith(b'\r\n.\r\n'):
+ data = data[:-5]
+ break
+
+ conn.send(b'250 Ok\r\n')
+
+ log(
+ '%s from=%s to=%s\n%s\n'
+ % (addr[0], mailfrom, ', '.join(rcpttos), data.decode())
+ )
-class dummysmtpsecureserver(dummysmtpserver):
- def __init__(self, localaddr, certfile):
- dummysmtpserver.__init__(self, localaddr)
- self._certfile = certfile
-
- def handle_accept(self):
- pair = self.accept()
- if not pair:
- return
- conn, addr = pair
- ui = uimod.ui.load()
+def run(host, port, certificate):
+ ui = uimod.ui.load()
+ with socket.socket(family, socket.SOCK_STREAM) as s:
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind((host, port))
+ # log('listening at %s:%d\n' % (host, port))
+ s.listen(1)
try:
- # wrap_socket() would block, but we don't care
- conn = sslutil.wrapserversocket(conn, ui, certfile=self._certfile)
- except ssl.SSLError as e:
- log('%s ssl error: %s\n' % (addr[0], e))
- conn.close()
- return
- smtpd.SMTPChannel(self, conn, addr)
-
-
-def run():
- try:
- asyncore.loop()
- except KeyboardInterrupt:
- pass
+ while True:
+ conn, addr = s.accept()
+ if certificate:
+ try:
+ conn = sslutil.wrapserversocket(
+ conn, ui, certfile=certificate
+ )
+ except ssl.SSLError as e:
+ log('%s ssl error: %s\n' % (addr[0], e))
+ conn.close()
+ continue
+ log("connection from %s:%s\n" % addr)
+ mocksmtpserversession(conn, addr)
+ conn.close()
+ except KeyboardInterrupt:
+ pass
def _encodestrsonly(v):
@@ -102,19 +141,9 @@ def main():
if (opts.tls == 'smtps') != bool(opts.certificate):
op.error('--certificate must be specified with --tls=smtps')
- addr = (opts.address, opts.port)
-
- def init():
- if opts.tls == 'none':
- dummysmtpserver(addr)
- else:
- dummysmtpsecureserver(addr, opts.certificate)
- log('listening at %s:%d\n' % addr)
-
server.runservice(
bytesvars(opts),
- initfn=init,
- runfn=run,
+ runfn=lambda: run(opts.address, opts.port, opts.certificate),
runargs=[pycompat.sysexecutable, pycompat.fsencode(__file__)]
+ pycompat.sysargv[1:],
logfile=opts.logfile,
diff --git a/tests/test-patchbomb-tls.t b/tests/test-patchbomb-tls.t
--- a/tests/test-patchbomb-tls.t
+++ b/tests/test-patchbomb-tls.t
@@ -7,7 +7,6 @@ Set up SMTP server:
$ "$PYTHON" "$TESTDIR/dummysmtpd.py" -p $HGPORT --pid-file a.pid --logfile log -d \
> --tls smtps --certificate `pwd`/server.pem
- listening at localhost:$HGPORT (?)
$ cat a.pid >> $DAEMON_PIDS
Set up repository:
@@ -47,6 +46,11 @@ we are able to load CA certs:
(an attempt was made to load CA certificates but none were loaded; see https://mercurial-scm.org/wiki/SecureConnections for how to configure Mercurial to avoid this error)
(?i)abort: .*?certificate.verify.failed.* (re)
[255]
+
+ $ cat ../log
+ * ssl error: * (glob)
+ $ : > ../log
+
#endif
#if defaultcacertsloaded
@@ -58,6 +62,10 @@ we are able to load CA certs:
(?i)abort: .*?certificate.verify.failed.* (re)
[255]
+ $ cat ../log
+ * ssl error: * (glob)
+ $ : > ../log
+
#endif
$ DISABLECACERTS="--config devel.disableloaddefaultcerts=true"
@@ -76,7 +84,8 @@ Without certificates:
[150]
$ cat ../log
- * ssl error: * (glob)
+ connection from * (glob)
+ no hello: b''
$ : > ../log
With global certificates:
@@ -91,6 +100,7 @@ With global certificates:
sending [PATCH] a ...
$ cat ../log
+ connection from * (glob)
* from=quux to=foo, bar (glob)
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
More information about the Mercurial-devel
mailing list