source: test/mock_ftplib.py @ 1246:dc0733466e9a

Last change on this file since 1246:dc0733466e9a was 1246:dc0733466e9a, checked in by Stefan Schwarzer <sschwarzer@…>, 6 years ago
Cleaned up `mock_ftplib.py`.
File size: 8.1 KB
Line 
1# Copyright (C) 2003-2013, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# See the file LICENSE for licensing terms.
3
4"""
5This module implements a mock version of the standard library's
6`ftplib.py` module. Some code is taken from there.
7
8Not all functionality is implemented, only what is needed to run the
9unit tests.
10"""
11
12from __future__ import unicode_literals
13
14import io
15import collections
16import ftplib
17import posixpath
18
19DEBUG = 0
20
21# Use a global dictionary of the form `{path: `MockFile` object, ...}`
22# to make "remote" mock files that were generated during a test
23# accessible. For example, this is used for testing the contents of a
24# file after an `FTPHost.upload` call.
25mock_files = {}
26
27def content_of(path):
28    """
29    Return the data stored in a mock remote file identified by `path`.
30    """
31    return mock_files[path].getvalue()
32
33
34class MockFile(io.BytesIO, object):
35    """
36    Mock class for the file objects _contained in_ `_FTPFile` objects
37    (not `_FTPFile` objects themselves!).
38
39    Contrary to `StringIO.StringIO` instances, `MockFile` objects can
40    be queried for their contents after they have been closed.
41    """
42    def __init__(self, path, content=b""):
43        global mock_files
44        mock_files[path] = self
45        self._super = super(MockFile, self)
46        self._super.__init__(content)
47
48    def getvalue(self):
49        if not self.closed:
50            return self._super.getvalue()
51        else:
52            return self._value_after_close
53
54    def close(self):
55        if not self.closed:
56            self._value_after_close = self._super.getvalue()
57        self._super.close()
58
59
60class MockSocket(object):
61    """
62    Mock class which is used to return something from
63    `MockSession.transfercmd`.
64    """
65    def __init__(self, path, mock_file_content=b""):
66        if DEBUG:
67            print("File content: *{0}*".format(mock_file_content))
68        self.file_path = path
69        self.mock_file_content = mock_file_content
70        self._timeout = 60
71
72    def makefile(self, mode):
73        return MockFile(self.file_path, self.mock_file_content)
74
75    def close(self):
76        pass
77
78    # Timeout-related methods are used in `FTPFile.close`.
79    def gettimeout(self):
80        return self._timeout
81
82    def settimeout(self, timeout):
83        self._timeout = timeout
84
85
86class MockSession(object):
87    """
88    Mock class which works like `ftplib.FTP` for the purpose of the
89    unit tests.
90    """
91    # Used by `MockSession.cwd` and `MockSession.pwd`
92    current_dir = '/home/sschwarzer'
93
94    # Used by `MockSession.dir`. This is a mapping from absolute path
95    # to the multi-line string that would show up in an FTP
96    # command-line client for this directory.
97    dir_contents = {}
98
99    # File content to be used (indirectly) with `transfercmd`.
100    mock_file_content = b''
101
102    def __init__(self, host='', user='', password=''):
103        self.closed = 0
104        # Count successful `transfercmd` invocations to ensure that
105        # each has a corresponding `voidresp`.
106        self._transfercmds = 0
107        # Dummy, only for getting/setting timeout in `_FTPFile.close`
108        self.sock = MockSocket("", "")
109
110    def voidcmd(self, cmd):
111        if DEBUG:
112            print(cmd)
113        if cmd == 'STAT':
114            return 'MockSession server awaiting your commands ;-)'
115        elif cmd.startswith('TYPE '):
116            return
117        elif cmd.startswith('SITE CHMOD'):
118            raise ftplib.error_perm("502 command not implemented")
119        else:
120            raise ftplib.error_perm
121
122    def pwd(self):
123        return self.current_dir
124
125    def _remove_trailing_slash(self, path):
126        if path != '/' and path.endswith('/'):
127            path = path[:-1]
128        return path
129
130    def _transform_path(self, path):
131        return posixpath.normpath(posixpath.join(self.pwd(), path))
132
133    def cwd(self, path):
134        self.current_dir = self._transform_path(path)
135
136    def dir(self, *args):
137        """
138        Provide a callback function for processing each line of a
139        directory listing. Return nothing.
140        """
141        # The callback comes last in `ftplib.FTP.dir`.
142        if isinstance(args[-1], collections.Callable):
143            # Get `args[-1]` _before_ removing it in the line after.
144            callback = args[-1]
145            args = args[:-1]
146        else:
147            callback = None
148        # Everything before the path argument are options.
149        path = args[-1]
150        if DEBUG:
151            print("dir: {0}".format(path))
152        path = self._transform_path(path)
153        if path not in self.dir_contents:
154            raise ftplib.error_perm
155        dir_lines = self.dir_contents[path].split('\n')
156        for line in dir_lines:
157            if callback is None:
158                print(line)
159            else:
160                callback(line)
161
162    def voidresp(self):
163        assert self._transfercmds == 1
164        self._transfercmds = self._transfercmds - 1
165        return '2xx'
166
167    def transfercmd(self, cmd):
168        """
169        Return a `MockSocket` object whose `makefile` method will
170        return a mock file object.
171        """
172        if DEBUG:
173            print(cmd)
174        # Fail if attempting to read from/write to a directory.
175        cmd, path = cmd.split()
176        #  Normalize path for lookup.
177        path = self._remove_trailing_slash(path)
178        if path in self.dir_contents:
179            raise ftplib.error_perm
180        # Fail if path isn't available (this name is hard-coded here
181        # and has to be used for the corresponding tests).
182        if (cmd, path) == ('RETR', 'notthere'):
183            raise ftplib.error_perm
184        assert self._transfercmds == 0
185        self._transfercmds = self._transfercmds + 1
186        return MockSocket(path, self.mock_file_content)
187
188    def close(self):
189        if not self.closed:
190            self.closed = 1
191            assert self._transfercmds == 0
192
193
194class MockUnixFormatSession(MockSession):
195
196    dir_contents = {
197      '/': """\
198drwxr-xr-x   2 45854    200           512 May  4  2000 home""",
199
200      '/home': """\
201drwxr-sr-x   2 45854    200           512 May  4  2000 sschwarzer
202-rw-r--r--   1 45854    200          4605 Jan 19  1970 older
203-rw-r--r--   1 45854    200          4605 Jan 19  2020 newer
204lrwxrwxrwx   1 45854    200            21 Jan 19  2002 link -> sschwarzer/index.html
205lrwxrwxrwx   1 45854    200            15 Jan 19  2002 bad_link -> python/bad_link
206drwxr-sr-x   2 45854    200           512 May  4  2000 dir with spaces""",
207
208      '/home/python': """\
209lrwxrwxrwx   1 45854    200             7 Jan 19  2002 link_link -> ../link
210lrwxrwxrwx   1 45854    200            14 Jan 19  2002 bad_link -> /home/bad_link""",
211
212      '/home/sschwarzer': """\
213total 14
214drwxr-sr-x   2 45854    200           512 May  4  2000 chemeng
215drwxr-sr-x   2 45854    200           512 Jan  3 17:17 download
216drwxr-sr-x   2 45854    200           512 Jul 30 17:14 image
217-rw-r--r--   1 45854    200          4604 Jan 19 23:11 index.html
218drwxr-sr-x   2 45854    200           512 May 29  2000 os2
219lrwxrwxrwx   2 45854    200             6 May 29  2000 osup -> ../os2
220drwxr-sr-x   2 45854    200           512 May 25  2000 publications
221drwxr-sr-x   2 45854    200           512 Jan 20 16:12 python
222drwxr-sr-x   6 45854    200           512 Sep 20  1999 scios2""",
223
224      '/home/dir with spaces': """\
225total 1
226-rw-r--r--   1 45854    200          4604 Jan 19 23:11 file with spaces""",
227
228      # Fail when trying to write to this directory (the content isn't
229      # relevant).
230      'sschwarzer': "",
231    }
232
233
234class MockMSFormatSession(MockSession):
235
236    dir_contents = {
237      '/': """\
23810-23-01  03:25PM       <DIR>          home""",
239
240      '/home': """\
24110-23-01  03:25PM       <DIR>          msformat""",
242
243      '/home/msformat': """\
24410-23-01  03:25PM       <DIR>          WindowsXP
24512-07-01  02:05PM       <DIR>          XPLaunch
24607-17-00  02:08PM             12266720 abcd.exe
24707-17-00  02:08PM                89264 O2KKeys.exe""",
248
249      '/home/msformat/XPLaunch': """\
25010-23-01  03:25PM       <DIR>          WindowsXP
25112-07-01  02:05PM       <DIR>          XPLaunch
25212-07-01  02:05PM       <DIR>          empty
25307-17-00  02:08PM             12266720 abcd.exe
25407-17-00  02:08PM                89264 O2KKeys.exe""",
255
256      '/home/msformat/XPLaunch/empty': "total 0",
257    }
Note: See TracBrowser for help on using the repository browser.