source: test/mock_ftplib.py @ 1274:ec941369fe8d

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