source: test/mock_ftplib.py @ 1564:c5b353a1c23d

Last change on this file since 1564:c5b353a1c23d was 1564:c5b353a1c23d, checked in by Stefan Schwarzer <sschwarzer@…>, 6 years ago
List contributors in `doc/contributors.txt`. So far, individual files had copyright notices for contributors. However, this makes it difficult to properly adapt files in case of refactoring: If a piece of code is moved to another file, I would need to find out if this code was contributed by someone else and change the copyright notice in the target file accordingly. With the new approach, every file refers to the file `doc/contributors.txt`, which contains the names of contributors.
File size: 9.0 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
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.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):
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 file_name_test""",
223
224      "/home/python": """\
225lrwxrwxrwx   1 45854    200             7 Jan 19  2002 link_link -> ../link
226lrwxrwxrwx   1 45854    200            14 Jan 19  2002 bad_link -> /home/bad_link""",
227
228      "/home/sschwarzer": """\
229total 14
230drwxr-sr-x   2 45854    200           512 May  4  2000 chemeng
231drwxr-sr-x   2 45854    200           512 Jan  3 17:17 download
232drwxr-sr-x   2 45854    200           512 Jul 30 17:14 image
233-rw-r--r--   1 45854    200          4604 Jan 19 23:11 index.html
234drwxr-sr-x   2 45854    200           512 May 29  2000 os2
235lrwxrwxrwx   2 45854    200             6 May 29  2000 osup -> ../os2
236drwxr-sr-x   2 45854    200           512 May 25  2000 publications
237drwxr-sr-x   2 45854    200           512 Jan 20 16:12 python
238drwxr-sr-x   6 45854    200           512 Sep 20  1999 scios2""",
239
240      "/home/dir with spaces": """\
241total 1
242-rw-r--r--   1 45854    200          4604 Jan 19 23:11 file with spaces""",
243
244      "/home/file_name_test": """\
245drwxr-sr-x   2 45854    200           512 May 29  2000 ä
246drwxr-sr-x   2 45854    200           512 May 29  2000 empty_ä
247-rw-r--r--   1 45854    200          4604 Jan 19 23:11 ö
248lrwxrwxrwx   2 45854    200             6 May 29  2000 ü -> ä""",
249
250      "/home/file_name_test/ä": """\
251-rw-r--r--   1 45854    200          4604 Jan 19 23:11 ö
252-rw-r--r--   1 45854    200          4604 Jan 19 23:11 o""",
253
254      "/home/file_name_test/empty_ä": """\
255""",
256      # Fail when trying to write to this directory (the content isn't
257      # relevant).
258      "sschwarzer": "",
259    }
260
261
262class MockMSFormatSession(MockSession):
263
264    dir_contents = {
265      "/": """\
26610-23-01  03:25PM       <DIR>          home""",
267
268      "/home": """\
26910-23-01  03:25PM       <DIR>          msformat""",
270
271      "/home/msformat": """\
27210-23-01  03:25PM       <DIR>          WindowsXP
27312-07-01  02:05PM       <DIR>          XPLaunch
27407-17-00  02:08PM             12266720 abcd.exe
27507-17-00  02:08PM                89264 O2KKeys.exe""",
276
277      "/home/msformat/XPLaunch": """\
27810-23-01  03:25PM       <DIR>          WindowsXP
27912-07-01  02:05PM       <DIR>          XPLaunch
28012-07-01  02:05PM       <DIR>          empty
28107-17-00  02:08PM             12266720 abcd.exe
28207-17-00  02:08PM                89264 O2KKeys.exe""",
283
284      "/home/msformat/XPLaunch/empty": "total 0",
285    }
Note: See TracBrowser for help on using the repository browser.