source: test/mock_ftplib.py @ 1723:7d731aea5360

Last change on this file since 1723:7d731aea5360 was 1723:7d731aea5360, checked in by Stefan Schwarzer <sschwarzer@…>, 6 months ago
Remove positional argument specifiers for `format` In strings for `format` calls, remove the digits for positional arguments. For example, "{0}, {1}" becomes "{}, {}".
File size: 9.0 KB
Line 
1# Copyright (C) 2003-2018, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# and ftputil contributors (see `doc/contributors.txt`)
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
13import io
14import collections.abc
15import ftplib
16import posixpath
17
18import ftputil.tool
19
20
21DEBUG = 0
22
23# Use a global dictionary of the form `{path: `MockFile` object, ...}`
24# to make "remote" mock files that were generated during a test
25# accessible. For example, this is used for testing the contents of a
26# file after an `FTPHost.upload` call.
27mock_files = {}
28
29def content_of(path):
30    """
31    Return the data stored in a mock remote file identified by `path`.
32    """
33    return mock_files[path].getvalue()
34
35
36class MockFile(io.BytesIO):
37    """
38    Mock class for the file objects _contained in_ `FTPFile` objects
39    (not `FTPFile` objects themselves!).
40
41    Contrary to `StringIO.StringIO` instances, `MockFile` objects can
42    be queried for their contents after they have been closed.
43    """
44
45    def __init__(self, path, content=b""):
46        global mock_files
47        mock_files[path] = self
48        self._value_after_close = ""
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:
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: *{}*".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:
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        # Copy default from class.
109        self.current_dir = self.__class__.current_dir
110        # Count successful `transfercmd` invocations to ensure that
111        # each has a corresponding `voidresp`.
112        self._transfercmds = 0
113        # Dummy, only for getting/setting timeout in `FTPFile.close`
114        self.sock = MockSocket("", b"")
115
116    def voidcmd(self, cmd):
117        if DEBUG:
118            print(cmd)
119        if cmd == "STAT":
120            return "MockSession server awaiting your commands ;-)"
121        elif cmd.startswith("TYPE "):
122            return
123        elif cmd.startswith("SITE CHMOD"):
124            raise ftplib.error_perm("502 command not implemented")
125        else:
126            raise ftplib.error_perm
127
128    def pwd(self):
129        return self.current_dir
130
131    def _remove_trailing_slash(self, path):
132        if path != "/" and path.endswith("/"):
133            path = path[:-1]
134        return path
135
136    def _transform_path(self, path):
137        return posixpath.normpath(posixpath.join(self.pwd(), path))
138
139    def cwd(self, path):
140        path = ftputil.tool.as_unicode(path)
141        self.current_dir = self._transform_path(path)
142
143    def _ignore_arguments(self, *args, **kwargs):
144        pass
145
146    delete = mkd = rename = rmd = _ignore_arguments
147
148    def dir(self, *args):
149        """
150        Provide a callback function for processing each line of a
151        directory listing. Return nothing.
152        """
153        # The callback comes last in `ftplib.FTP.dir`.
154        if isinstance(args[-1], collections.abc.Callable):
155            # Get `args[-1]` _before_ removing it in the line after.
156            callback = args[-1]
157            args = args[:-1]
158        else:
159            callback = None
160        # Everything before the path argument are options.
161        path = args[-1]
162        if DEBUG:
163            print("dir: {}".format(path))
164        path = self._transform_path(path)
165        if path not in self.dir_contents:
166            raise ftplib.error_perm
167        dir_lines = self.dir_contents[path].split("\n")
168        for line in dir_lines:
169            if callback is None:
170                print(line)
171            else:
172                callback(line)
173
174    def voidresp(self):
175        assert self._transfercmds == 1
176        self._transfercmds -= 1
177        return "2xx"
178
179    def transfercmd(self, cmd, rest=None):
180        """
181        Return a `MockSocket` object whose `makefile` method will
182        return a mock file object.
183        """
184        if DEBUG:
185            print(cmd)
186        # Fail if attempting to read from/write to a directory.
187        cmd, path = cmd.split()
188        #  Normalize path for lookup.
189        path = self._remove_trailing_slash(path)
190        if path in self.dir_contents:
191            raise ftplib.error_perm
192        # Fail if path isn't available (this name is hard-coded here
193        # and has to be used for the corresponding tests).
194        if (cmd, path) == ("RETR", "notthere"):
195            raise ftplib.error_perm
196        assert self._transfercmds == 0
197        self._transfercmds += 1
198        return MockSocket(path, self.mock_file_content)
199
200    def close(self):
201        if not self.closed:
202            self.closed = 1
203            assert self._transfercmds == 0
204
205
206class MockUnixFormatSession(MockSession):
207
208    dir_contents = {
209      "/": """\
210drwxr-xr-x   2 45854    200           512 May  4  2000 home""",
211
212      "/home": """\
213drwxr-sr-x   2 45854    200           512 May  4  2000 sschwarzer
214-rw-r--r--   1 45854    200          4605 Jan 19  1970 older
215-rw-r--r--   1 45854    200          4605 Jan 19  2020 newer
216lrwxrwxrwx   1 45854    200            21 Jan 19  2002 link -> sschwarzer/index.html
217lrwxrwxrwx   1 45854    200            15 Jan 19  2002 bad_link -> python/bad_link
218drwxr-sr-x   2 45854    200           512 May  4  2000 dir with spaces
219drwxr-sr-x   2 45854    200           512 May  4  2000 python
220drwxr-sr-x   2 45854    200           512 May  4  2000 file_name_test""",
221
222      "/home/python": """\
223lrwxrwxrwx   1 45854    200             7 Jan 19  2002 link_link -> ../link
224lrwxrwxrwx   1 45854    200            14 Jan 19  2002 bad_link -> /home/bad_link""",
225
226      "/home/sschwarzer": """\
227total 14
228drwxr-sr-x   2 45854    200           512 May  4  2000 chemeng
229drwxr-sr-x   2 45854    200           512 Jan  3 17:17 download
230drwxr-sr-x   2 45854    200           512 Jul 30 17:14 image
231-rw-r--r--   1 45854    200          4604 Jan 19 23:11 index.html
232drwxr-sr-x   2 45854    200           512 May 29  2000 os2
233lrwxrwxrwx   2 45854    200             6 May 29  2000 osup -> ../os2
234drwxr-sr-x   2 45854    200           512 May 25  2000 publications
235drwxr-sr-x   2 45854    200           512 Jan 20 16:12 python
236drwxr-sr-x   6 45854    200           512 Sep 20  1999 scios2""",
237
238      "/home/dir with spaces": """\
239total 1
240-rw-r--r--   1 45854    200          4604 Jan 19 23:11 file with spaces""",
241
242      "/home/file_name_test": """\
243drwxr-sr-x   2 45854    200           512 May 29  2000 ä
244drwxr-sr-x   2 45854    200           512 May 29  2000 empty_ä
245-rw-r--r--   1 45854    200          4604 Jan 19 23:11 ö
246lrwxrwxrwx   2 45854    200             6 May 29  2000 ü -> ä""",
247
248      "/home/file_name_test/ä": """\
249-rw-r--r--   1 45854    200          4604 Jan 19 23:11 ö
250-rw-r--r--   1 45854    200          4604 Jan 19 23:11 o""",
251
252      "/home/file_name_test/empty_ä": """\
253""",
254      # Fail when trying to write to this directory (the content isn't
255      # relevant).
256      "sschwarzer": "",
257    }
258
259
260class MockMSFormatSession(MockSession):
261
262    dir_contents = {
263      "/": """\
26410-23-01  03:25PM       <DIR>          home""",
265
266      "/home": """\
26710-23-01  03:25PM       <DIR>          msformat""",
268
269      "/home/msformat": """\
27010-23-01  03:25PM       <DIR>          WindowsXP
27112-07-01  02:05PM       <DIR>          XPLaunch
27207-17-00  02:08PM             12266720 abcd.exe
27307-17-00  02:08PM                89264 O2KKeys.exe""",
274
275      "/home/msformat/XPLaunch": """\
27610-23-01  03:25PM       <DIR>          WindowsXP
27712-07-01  02:05PM       <DIR>          XPLaunch
27812-07-01  02:05PM       <DIR>          empty
27907-17-00  02:08PM             12266720 abcd.exe
28007-17-00  02:08PM                89264 O2KKeys.exe""",
281
282      "/home/msformat/XPLaunch/empty": "total 0",
283    }
Note: See TracBrowser for help on using the repository browser.