source: test/mock_ftplib.py @ 1343:bcadc5b1504f

Last change on this file since 1343:bcadc5b1504f was 1343:bcadc5b1504f, checked in by Stefan Schwarzer <sschwarzer@…>, 6 years ago
Use double quotes (") for strings. I used to have a rule to put "identifier-like" strings in single quotes (') and other strings in double quotes. For example, dictionary keys would usually be in single quotes, but error messages in double quotes. Unfortunately, there are many border cases, so over time I've become tired of thinking about when I should use one kind of quotes or the other. So everything is in double quotes now. Also, I guess most people are rather used to double quotes for strings and thus will welcome the change. :-)
File size: 8.9 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_arguments(self, *args, **kwargs):
142        pass
143
144    delete = mkd = rename = rmd = _ignore_arguments
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.