The MHonArc archiver must set stdin=PIPE when calling the subprocess. Given
[mailman.git] / src / mailman / archiving / tests / test_prototype.py
blob216a6187d403b3e7c7bbe0f79ee908ef678db42b
1 # Copyright (C) 2012-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Test the prototype archiver."""
20 __all__ = [
21 'TestPrototypeArchiver',
25 import os
26 import shutil
27 import tempfile
28 import unittest
29 import threading
31 from email import message_from_file
32 from flufl.lock import Lock
33 from mailman.app.lifecycle import create_list
34 from mailman.archiving.prototype import Prototype
35 from mailman.config import config
36 from mailman.database.transaction import transaction
37 from mailman.testing.helpers import LogFileMark
38 from mailman.testing.helpers import (
39 specialized_message_from_string as mfs)
40 from mailman.testing.layers import ConfigLayer
41 from mailman.utilities.email import add_message_hash
44 class TestPrototypeArchiver(unittest.TestCase):
45 """Test the prototype archiver."""
47 layer = ConfigLayer
49 def setUp(self):
50 # Create a fake mailing list and message object.
51 self._msg = mfs("""\
52 To: test@example.com
53 From: anne@example.com
54 Subject: Testing the test list
55 Message-ID: <ant>
56 X-Message-ID-Hash: MS6QLWERIJLGCRF44J7USBFDELMNT2BW
58 Tests are better than no tests
59 but the water deserves to be swum.
60 """)
61 with transaction():
62 self._mlist = create_list('test@example.com')
63 # Set up a temporary directory for the prototype archiver so that it's
64 # easier to clean up.
65 self._tempdir = tempfile.mkdtemp()
66 config.push('prototype', """
67 [paths.testing]
68 archive_dir: {0}
69 """.format(self._tempdir))
70 # Capture the structure of a maildir.
71 self._expected_dir_structure = set(
72 (os.path.join(config.ARCHIVE_DIR, path) for path in (
73 'prototype',
74 os.path.join('prototype', self._mlist.fqdn_listname),
75 os.path.join('prototype', self._mlist.fqdn_listname, 'cur'),
76 os.path.join('prototype', self._mlist.fqdn_listname, 'new'),
77 os.path.join('prototype', self._mlist.fqdn_listname, 'tmp'),
78 )))
79 self._expected_dir_structure.add(config.ARCHIVE_DIR)
81 def tearDown(self):
82 shutil.rmtree(self._tempdir)
83 config.pop('prototype')
85 def _find(self, path):
86 all_filenames = set()
87 for dirpath, dirnames, filenames in os.walk(path):
88 if isinstance(dirpath, bytes):
89 dirpath = dirpath.decode('utf-8')
90 all_filenames.add(dirpath)
91 for filename in filenames:
92 new_filename = filename
93 if isinstance(filename, bytes):
94 new_filename = filename.decode('utf-8')
95 all_filenames.add(os.path.join(dirpath, new_filename))
96 return all_filenames
98 def test_archive_maildir_created(self):
99 # Archiving a message to the prototype archiver should create the
100 # expected directory structure.
101 Prototype.archive_message(self._mlist, self._msg)
102 all_filenames = self._find(config.ARCHIVE_DIR)
103 # Check that the directory structure has been created and we have one
104 # more file (the archived message) than expected directories.
105 archived_messages = all_filenames - self._expected_dir_structure
106 self.assertEqual(len(archived_messages), 1)
107 self.assertTrue(
108 archived_messages.pop().startswith(
109 os.path.join(config.ARCHIVE_DIR, 'prototype',
110 self._mlist.fqdn_listname, 'new')))
112 def test_archive_maildir_existence_does_not_raise(self):
113 # Archiving a second message does not cause an EEXIST to be raised
114 # when a second message is archived.
115 new_dir = None
116 Prototype.archive_message(self._mlist, self._msg)
117 for directory in ('cur', 'new', 'tmp'):
118 path = os.path.join(config.ARCHIVE_DIR, 'prototype',
119 self._mlist.fqdn_listname, directory)
120 if directory == 'new':
121 new_dir = path
122 self.assertTrue(os.path.isdir(path))
123 # There should be one message in the 'new' directory.
124 self.assertEqual(len(os.listdir(new_dir)), 1)
125 # Archive a second message. If an exception occurs, let it fail the
126 # test. Afterward, two messages should be in the 'new' directory.
127 del self._msg['message-id']
128 del self._msg['x-message-id-hash']
129 self._msg['Message-ID'] = '<bee>'
130 add_message_hash(self._msg)
131 Prototype.archive_message(self._mlist, self._msg)
132 self.assertEqual(len(os.listdir(new_dir)), 2)
134 def test_archive_lock_used(self):
135 # Test that locking the maildir when adding works as a failure here
136 # could mean we lose mail.
137 lock_file = os.path.join(
138 config.LOCK_DIR, '{0}-maildir.lock'.format(
139 self._mlist.fqdn_listname))
140 with Lock(lock_file):
141 # Acquire the archiver lock, then make sure the archiver logs the
142 # fact that it could not acquire the lock.
143 archive_thread = threading.Thread(
144 target=Prototype.archive_message,
145 args=(self._mlist, self._msg))
146 mark = LogFileMark('mailman.error')
147 archive_thread.run()
148 # Test that the archiver output the correct error.
149 line = mark.readline()
150 # XXX 2012-03-15 BAW: we really should remove timestamp prefixes
151 # from the loggers when under test.
152 self.assertTrue(line.endswith(
153 'Unable to acquire prototype archiver lock for {0}, '
154 'discarding: {1}\n'.format(
155 self._mlist.fqdn_listname,
156 self._msg.get('message-id'))))
157 # Check that the message didn't get archived.
158 created_files = self._find(config.ARCHIVE_DIR)
159 self.assertEqual(self._expected_dir_structure, created_files)
161 def test_prototype_archiver_good_path(self):
162 # Verify the good path; the message gets archived.
163 Prototype.archive_message(self._mlist, self._msg)
164 new_path = os.path.join(
165 config.ARCHIVE_DIR, 'prototype', self._mlist.fqdn_listname, 'new')
166 archived_messages = list(os.listdir(new_path))
167 self.assertEqual(len(archived_messages), 1)
168 # Check that the email has been added.
169 with open(os.path.join(new_path, archived_messages[0])) as fp:
170 archived_message = message_from_file(fp)
171 self.assertEqual(self._msg.as_string(), archived_message.as_string())