source: test/mock_ftplib.py @ 1306:f56ba409991b

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