source: test/mock_ftplib.py @ 1712:1b17d07f3a88

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