source: test/mock_ftplib.py @ 1713:f146a1ea66aa

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