From 139d2e70240c78d0602705def28404c060a59e0d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 24 Apr 2012 12:21:56 -0400 Subject: [PATCH] Add pygrader.test with asyncore SMTP client/server implemenations. This should make it easier to build integration tests for `mailpipe()`. --- pygrader/test/__init__.py | 1 + pygrader/test/client.py | 108 +++++++++++++++++++++++++++++++++++ pygrader/test/server.py | 117 ++++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 4 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 pygrader/test/__init__.py create mode 100644 pygrader/test/client.py create mode 100644 pygrader/test/server.py diff --git a/pygrader/test/__init__.py b/pygrader/test/__init__.py new file mode 100644 index 0000000..b98f164 --- /dev/null +++ b/pygrader/test/__init__.py @@ -0,0 +1 @@ +# Copyright diff --git a/pygrader/test/client.py b/pygrader/test/client.py new file mode 100644 index 0000000..1432864 --- /dev/null +++ b/pygrader/test/client.py @@ -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 index 0000000..368aae5 --- /dev/null +++ b/pygrader/test/server.py @@ -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 + + 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 + + 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 + + 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 diff --git a/setup.py b/setup.py index df1a086..22d0b23 100644 --- 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'], ) -- 2.26.2