Add pygrader.test with asyncore SMTP client/server implemenations.
authorW. Trevor King <wking@tremily.us>
Tue, 24 Apr 2012 16:21:56 +0000 (12:21 -0400)
committerW. Trevor King <wking@tremily.us>
Tue, 24 Apr 2012 16:28:18 +0000 (12:28 -0400)
This should make it easier to build integration tests for `mailpipe()`.

pygrader/test/__init__.py [new file with mode: 0644]
pygrader/test/client.py [new file with mode: 0644]
pygrader/test/server.py [new file with mode: 0644]
setup.py

diff --git a/pygrader/test/__init__.py b/pygrader/test/__init__.py
new file mode 100644 (file)
index 0000000..b98f164
--- /dev/null
@@ -0,0 +1 @@
+# Copyright
diff --git a/pygrader/test/client.py b/pygrader/test/client.py
new file mode 100644 (file)
index 0000000..1432864
--- /dev/null
@@ -0,0 +1,108 @@
+# Copyright
+
+import asynchat as _asynchat
+import socket as _socket
+
+from pgp_mime import email as _email
+
+from .. import LOG as _LOG
+
+
+class MessageSender (_asynchat.async_chat):
+    """A SMTP message sender using ``asyncore``.
+
+    To test ``PygraderServer``, it's useful to have a message-sender
+    that also uses ``asyncore``.  This avoids the need to use
+    multithreaded tests.
+    """
+    def __init__(self, address, messages):
+        super(MessageSender, self).__init__()
+        self.address = address
+        self.messages = messages
+        self.create_socket(_socket.AF_INET, _socket.SOCK_STREAM)
+        self.connect(address)
+        self.intro = None
+        self.ilines = []
+        self.ibuffer = []
+        self.set_terminator(b'\r\n')
+        if self.messages:
+            self.callback = (self.send_message, [], {})
+        else:
+            self.callback = (self.quit_callback, [], {})
+        self.send_command('ehlo [127.0.0.1]')
+
+    def log_info(self, message, type='info'):
+        # TODO: type -> severity
+        _LOG.info(message)
+
+    def send_command(self, command, clear_command_list=True):
+        if clear_command_list:
+            self.commands = [command]
+            self.responses = []
+        _LOG.debug('push: {}'.format(command))
+        self.push(bytes(command + '\r\n', 'ascii'))
+
+    def send_commands(self, commands):
+        self.commands = commands
+        self.responses = []
+        for command in self.commands:
+            self.send_command(command=command, clear_command_list=False)
+
+    def collect_incoming_data(self, data):
+        self.ibuffer.append(data)
+
+    def found_terminator(self):
+        ibuffer = b''.join(self.ibuffer)
+        self.ibuffer = []
+        self.ilines.append(ibuffer)
+        if len(self.ilines[-1]) >= 4 and self.ilines[-1][3] == ord(b' '):
+            response = self.ilines
+            self.ilines = []
+            self.handle_response(response)
+
+    def handle_response(self, response):
+        _LOG.debug('handle response: {}'.format(response))
+        code = int(response[-1][:3])
+        if not self.intro:
+            self.intro = (code, response)
+        else:
+            self.responses.append((code, response))
+        if len(self.responses) == len(self.commands):
+            if self.callback:
+                callback,args,kwargs = self.callback
+                self.callback = None
+                commands = self.commands
+                self.commands = []
+                responses = self.responses
+                self.responses = []
+                _LOG.debug('callback: ({}, {})'.format(callback, list(zip(commands, responses))))
+                callback(commands, responses, *args, **kwargs)
+            else:
+                self.close()
+
+    def close_callback(self, commands, responses):
+        _LOG.debug(commands)
+        _LOG.debug(responses)
+        self.close()
+
+    def quit_callback(self, commands, responses):
+        self.send_command('quit')
+        self.callback = (self.close_callback, [], {})
+
+    def send_message(self, commands, responses):
+        message = self.messages.pop(0)
+        if self.messages:
+            self.callback = (self.send_message, [], {})
+        else:
+            self.callback = (self.quit_callback, [], {})
+        sources = list(_email.email_sources(message))
+        commands = [
+            'mail FROM:<{}>'.format(sources[0][1])
+            ]
+        for name,address in _email.email_targets(message):
+            commands.append('rcpt TO:<{}>'.format(address))
+        commands.extend([
+                'DATA',
+                message.as_string() + '\r\n.',
+                ])
+        self.send_commands(commands=commands)
diff --git a/pygrader/test/server.py b/pygrader/test/server.py
new file mode 100644 (file)
index 0000000..368aae5
--- /dev/null
@@ -0,0 +1,117 @@
+# Copyright
+
+import asyncore as _asyncore
+import email as _email
+import smtpd as _smptd
+import socket as _socket
+
+from .. import LOG as _LOG
+
+
+class SMTPChannel (_smptd.SMTPChannel):
+    def close(self):
+        super(SMTPChannel, self).close()
+        _LOG.debug('close {}'.format(self))
+        self.smtp_server.channel_closed()
+
+
+class SMTPServer (_smptd.SMTPServer):
+    """An SMTP server for testing pygrader.
+
+    >>> from asyncore import loop
+    >>> from smtplib import SMTP
+    >>> from pgp_mime.email import encodedMIMEText
+    >>> from pygrader.test.client import MessageSender
+
+    >>> def process(peer, mailfrom, rcpttos, data):
+    ...     print('peer:     {}'.format(peer))
+    ...     print('mailfrom: {}'.format(mailfrom))
+    ...     print('rcpttos:  {}'.format(rcpttos))
+    ...     print('message:')
+    ...     print(data)
+    >>> server = SMTPServer(
+    ...     ('localhost', 1025), None, process=process, count=3)
+
+    >>> message = encodedMIMEText('Ping')
+    >>> message['From'] = 'a@example.com'
+    >>> message['To'] = 'b@example.com, c@example.com'
+    >>> message['Cc'] = 'd@example.com'
+    >>> messages = [message, message, message]
+    >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
+    >>> loop()  # doctest: +REPORT_UDIFF, +ELLIPSIS
+    peer:     ('127.0.0.1', ...)
+    mailfrom: a@example.com
+    rcpttos:  ['b@example.com', 'c@example.com', 'd@example.com']
+    message:
+    Content-Type: text/plain; charset="us-ascii"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: 7bit
+    Content-Disposition: inline
+    From: a@example.com
+    To: b@example.com, c@example.com
+    Cc: d@example.com
+    <BLANKLINE>
+    Ping
+    peer:     ('127.0.0.1', ...)
+    mailfrom: a@example.com
+    rcpttos:  ['b@example.com', 'c@example.com', 'd@example.com']
+    message:
+    Content-Type: text/plain; charset="us-ascii"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: 7bit
+    Content-Disposition: inline
+    From: a@example.com
+    To: b@example.com, c@example.com
+    Cc: d@example.com
+    <BLANKLINE>
+    Ping
+    peer:     ('127.0.0.1', ...)
+    mailfrom: a@example.com
+    rcpttos:  ['b@example.com', 'c@example.com', 'd@example.com']
+    message:
+    Content-Type: text/plain; charset="us-ascii"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: 7bit
+    Content-Disposition: inline
+    From: a@example.com
+    To: b@example.com, c@example.com
+    Cc: d@example.com
+    <BLANKLINE>
+    Ping
+    """
+    channel_class = SMTPChannel
+
+    def __init__(self, *args, **kwargs):
+        self.count = kwargs.pop('count', None)
+        self.process = kwargs.pop('process', None)
+        self.channels_open = 0
+        super(SMTPServer, self).__init__(*args, **kwargs)
+
+    def log_info(self, message, type='info'):
+        # TODO: type -> severity
+        _LOG.info(message)
+
+    def handle_accepted(self, conn, addr):
+        if self.count <= 0:
+            conn.close()
+            return
+        super(SMTPServer, self).handle_accepted(conn, addr)
+        self.channels_open += 1
+
+    def channel_closed(self):
+        self.channels_open -= 1
+        if self.channels_open == 0 and self.count <= 0:
+            _LOG.debug('close {}'.format(self))
+            self.close()
+
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        if self.count is not None:
+            self.count -= 1
+            _LOG.debug('Count: {}'.format(self.count))
+        _LOG.debug('receiving message from: {}'.format(peer))
+        _LOG.debug('message addressed from: {}'.format(mailfrom))
+        _LOG.debug('message addressed to  : {}'.format(rcpttos))
+        _LOG.debug('message length        : {}'.format(len(data)))
+        if self.process:
+            self.process(peer, mailfrom, rcpttos, data)
+        return
index df1a0860b039b4a7e347b98f6df4a31bc8e387f1..22d0b23369b44e7af77d8698126f18a6f5366a67 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -46,6 +46,6 @@ _setup(
         'Topic :: Education',
         ],
     scripts = ['bin/pg.py'],
-    packages = ['pygrader', 'pygrader.model'],
-    provides = ['pygrader', 'pygrader.model'],
+    packages = ['pygrader', 'pygrader.model', 'pygrader.test'],
+    provides = ['pygrader', 'pygrader.model', 'pygrader.test'],
     )