root/trunk/_test_ftp_stat.py

Revision 672, 16.3 kB (checked in by schwa, 2 years ago)
Fixed typo.
  • Property svn:mime-type set to text/x-python
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 # Copyright (C) 2003-2006, Stefan Schwarzer
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # - Redistributions of source code must retain the above copyright
9 #   notice, this list of conditions and the following disclaimer.
10 #
11 # - Redistributions in binary form must reproduce the above copyright
12 #   notice, this list of conditions and the following disclaimer in the
13 #   documentation and/or other materials provided with the distribution.
14 #
15 # - Neither the name of the above author nor the names of the
16 #   contributors to the software may be used to endorse or promote
17 #   products derived from this software without specific prior written
18 #   permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
24 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 # $Id$
33
34 from __future__ import division
35
36 import stat
37 import time
38 import unittest
39
40 import _test_base
41 import ftp_error
42 import ftp_stat
43 import ftputil
44
45
46 def test_stat():
47     host = _test_base.ftp_host_factory()
48     stat = ftp_stat._Stat(host)
49     # use Unix format parser explicitly
50     stat._parser = ftp_stat.UnixParser()
51     return stat
52
53 def stat_tuple_to_seconds(t):
54     """
55     Return a float number representing the local time associated with
56     the six-element tuple `t`.
57     """
58     assert len(t) == 6, \
59            "need a six-element tuple (year, month, day, hour, min, sec)"
60     return time.mktime(t + (0, 0, -1))
61
62
63 class TestParsers(unittest.TestCase):
64     def _test_valid_lines(self, parser_class, lines, expected_stat_results):
65         parser = parser_class()
66         for line, expected_stat_result in zip(lines, expected_stat_results):
67             # convert to list to compare with the list `expected_stat_results`
68             stat_result = list(parser.parse_line(line))
69             # convert time tuple to seconds
70             expected_stat_result[8] = \
71               stat_tuple_to_seconds(expected_stat_result[8])
72             # compare both lists
73             self.assertEqual(stat_result, expected_stat_result)
74
75     def _test_invalid_lines(self, parser_class, lines):
76         parser = parser_class()
77         for line in lines:
78             self.assertRaises(ftp_error.ParserError, parser.parse_line, line)
79
80     def _expected_year(self):
81         """
82         Return the expected year for the second line in the
83         listing in `test_valid_unix_lines`.
84         """
85         # if in this year it's after Dec 19, 23:11, use the current
86         #  year, else use the previous year ...
87         now = time.localtime()
88         # we need only month, day, hour and minute
89         current_time_parts = now[1:5]
90         time_parts_in_listing = (12, 19, 23, 11)
91         if current_time_parts > time_parts_in_listing:
92             return now[0]
93         else:
94             return now[0] - 1
95
96     def test_valid_unix_lines(self):
97         lines = [
98           "drwxr-sr-x   2 45854    200           512 May  4  2000 chemeng",
99           # the year value for this line will change with the actual time
100           "-rw-r--r--   1 45854    200          4604 Dec 19 23:11 index.html",
101           "drwxr-sr-x   2 45854    200           512 May 29  2000 os2",
102           "lrwxrwxrwx   2 45854    200           512 May 29  2000 osup -> "
103                                                                   "../os2"
104           ]
105         expected_stat_results = [
106           [17901, None, None, 2, '45854', '200', 512, None,
107            (2000, 5, 4, 0, 0, 0), None],
108           [33188, None, None, 1, '45854', '200', 4604, None,
109            (self._expected_year(), 12, 19, 23, 11, 0), None],
110           [17901, None, None, 2, '45854', '200', 512, None,
111            (2000, 5, 29, 0, 0, 0), None],
112           [41471, None, None, 2, '45854', '200', 512, None,
113            (2000, 5, 29, 0, 0, 0), None]
114           ]
115         self._test_valid_lines(ftp_stat.UnixParser, lines,
116                                expected_stat_results)
117
118     def test_invalid_unix_lines(self):
119         lines = [
120           "total 14",
121           "drwxr-sr-    2 45854    200           512 May  4  2000 chemeng",
122           "xrwxr-sr-x   2 45854    200           512 May  4  2000 chemeng",
123           "xrwxr-sr-x   2 45854    200           51x May  4  2000 chemeng",
124           ]
125         self._test_invalid_lines(ftp_stat.UnixParser, lines)
126
127     def test_alternative_unix_format(self):
128         # see http://ftputil.sschwarzer.net/trac/ticket/12 for a
129         #  description for the need for an alternative format
130         lines = [
131           "drwxr-sr-x   2   200           512 May  4  2000 chemeng",
132           # the year value for this line will change with the actual time
133           "-rw-r--r--   1   200          4604 Dec 19 23:11 index.html",
134           "drwxr-sr-x   2   200           512 May 29  2000 os2",
135           "lrwxrwxrwx   2   200           512 May 29  2000 osup -> ../os2"
136           ]
137         expected_stat_results = [
138           [17901, None, None, 2, None, '200', 512, None,
139            (2000, 5, 4, 0, 0, 0), None],
140           [33188, None, None, 1, None, '200', 4604, None,
141            (self._expected_year(), 12, 19, 23, 11, 0), None],
142           [17901, None, None, 2, None, '200', 512, None,
143            (2000, 5, 29, 0, 0, 0), None],
144           [41471, None, None, 2, None, '200', 512, None,
145            (2000, 5, 29, 0, 0, 0), None]
146           ]
147         self._test_valid_lines(ftp_stat.UnixParser, lines,
148                                expected_stat_results)
149
150     def test_valid_ms_lines(self):
151         lines = [
152           "07-27-01  11:16AM       <DIR>          Test",
153           "10-23-95  03:25PM       <DIR>          WindowsXP",
154           "07-17-00  02:08PM             12266720 test.exe"
155           ]
156         expected_stat_results = [
157           [16640, None, None, None, None, None, None, None,
158            (2001, 7, 27, 11, 16, 0), None],
159           [16640, None, None, None, None, None, None, None,
160            (1995, 10, 23, 15, 25, 0), None],
161           [33024, None, None, None, None, None, 12266720, None,
162            (2000, 7, 17, 14, 8, 0), None]
163           ]
164         self._test_valid_lines(ftp_stat.MSParser, lines, expected_stat_results)
165
166     def test_invalid_ms_lines(self):
167         lines = [
168           "07-27-01  11:16AM                      Test",
169           "07-17-00  02:08             12266720 test.exe",
170           "07-17-00  02:08AM           1226672x test.exe"
171           ]
172         self._test_invalid_lines(ftp_stat.MSParser, lines)
173
174     #
175     # the following code checks if the decision logic in the Unix
176     #  line parser for determining the year works
177     #
178     def datetime_string(self, time_float):
179         """
180         Return a datetime string generated from the value in
181         `time_float`. The parameter value is a floating point value
182         as returned by `time.time()`. The returned string is built as
183         if it were from a Unix FTP server (format: MMM dd hh:mm")
184         """
185         time_tuple = time.localtime(time_float)
186         return time.strftime("%b %d %H:%M", time_tuple)
187
188     def dir_line(self, time_float):
189         """
190         Return a directory line as from a Unix FTP server. Most of
191         the contents are fixed, but the timestamp is made from
192         `time_float` (seconds since the epoch, as from `time.time()`).
193         """
194         line_template = "-rw-r--r--   1   45854   200   4604   %s   index.html"
195         return line_template % self.datetime_string(time_float)
196
197     def assert_equal_times(self, time1, time2):
198         """
199         Check if both times (seconds since the epoch) are equal. For
200         the purpose of this test, two times are "equal" if they
201         differ no more than one minute from each other.
202
203         If the test fails, an exception is raised by the inherited
204         `failIf` method.
205         """
206         abs_difference = abs(time1 - time2)
207         try:
208             self.failIf(abs_difference > 60.0)
209         except AssertionError:
210             print "Difference is", abs_difference, "seconds"
211             raise
212
213     def _test_time_shift(self, supposed_time_shift, deviation=0.0):
214         """
215         Check if the stat parser considers the time shift value
216         correctly. `deviation` is the difference between the actual
217         time shift and the supposed time shift, which is rounded
218         to full hours.
219         """
220         host = _test_base.ftp_host_factory()
221         # explicitly use Unix format parser
222         host._stat._parser = ftp_stat.UnixParser()
223         host.set_time_shift(supposed_time_shift)
224         server_time = time.time() + supposed_time_shift + deviation
225         stat_result = host._stat._parser.parse_line(self.dir_line(server_time),
226                                                     host.time_shift())
227         self.assert_equal_times(stat_result.st_mtime, server_time)
228
229     def test_time_shifts(self):
230         """Test correct year depending on time shift value."""
231         # 1. test: client and server share the same local time
232         self._test_time_shift(0.0)
233         # 2. test: server is three hours ahead of client
234         self._test_time_shift(3 * 60 * 60)
235         # 3. test: client is three hours ahead of server
236         self._test_time_shift(- 3 * 60 * 60)
237         # 4. test: server is supposed to be three hours ahead, but
238         #  is ahead three hours and one minute
239         self._test_time_shift(3 * 60 * 60, 60)
240         # 5. test: server is supposed to be three hours ahead, but
241         #  is ahead three hours minus one minute
242         self._test_time_shift(3 * 60 * 60, -60)
243         # 6. test: client is supposed to be three hours ahead, but
244         #  is ahead three hours and one minute
245         self._test_time_shift(-3 * 60 * 60, -60)
246         # 7. test: client is supposed to be three hours ahead, but
247         #  is ahead three hours minus one minute
248         self._test_time_shift(-3 * 60 * 60, 60)
249
250
251 class TestLstatAndStat(unittest.TestCase):
252     """
253     Test `FTPHost.lstat` and `FTPHost.stat` (test currently only
254     implemented for Unix server format).
255     """
256     def setUp(self):
257         self.stat = test_stat()
258
259     def test_failing_lstat(self):
260         """Test whether lstat fails for a nonexistent path."""
261         self.assertRaises(ftp_error.PermanentError, self.stat.lstat,
262                           '/home/sschw/notthere')
263         self.assertRaises(ftp_error.PermanentError, self.stat.lstat,
264                           '/home/sschwarzer/notthere')
265
266     def test_lstat_for_root(self):
267         """Test `lstat` for `/` .
268         Note: `(l)stat` works by going one directory up and parsing
269         the output of an FTP `DIR` command. Unfortunately, it's not
270         possible to do this for the root directory `/`.
271         """
272         self.assertRaises(ftp_error.RootDirError, self.stat.lstat, '/')
273         try:
274             self.stat.lstat('/')
275         except ftp_error.RootDirError, exc_obj:
276             self.failIf(isinstance(exc_obj, ftp_error.FTPOSError))
277
278     def test_lstat_one_file(self):
279         """Test `lstat` for a file."""
280         stat_result = self.stat.lstat('/home/sschwarzer/index.html')
281         self.assertEqual(oct(stat_result.st_mode), '0100644')
282         self.assertEqual(stat_result.st_size, 4604)
283
284     def test_lstat_one_dir(self):
285         """Test `lstat` for a directory."""
286         stat_result = self.stat.lstat('/home/sschwarzer/scios2')
287         self.assertEqual(oct(stat_result.st_mode), '042755')
288         self.assertEqual(stat_result.st_ino, None)
289         self.assertEqual(stat_result.st_dev, None)
290         self.assertEqual(stat_result.st_nlink, 6)
291         self.assertEqual(stat_result.st_uid, '45854')
292         self.assertEqual(stat_result.st_gid, '200')
293         self.assertEqual(stat_result.st_size, 512)
294         self.assertEqual(stat_result.st_atime, None)
295         self.failUnless(stat_result.st_mtime ==
296                         stat_tuple_to_seconds((1999, 9, 20, 0, 0, 0)))
297         self.assertEqual(stat_result.st_ctime, None)
298         self.failUnless(stat_result ==
299           (17901, None, None, 6, '45854', '200', 512, None,
300            stat_tuple_to_seconds((1999, 9, 20, 0, 0, 0)), None))
301
302     def test_lstat_via_stat_module(self):
303         """Test `lstat` indirectly via `stat` module."""
304         stat_result = self.stat.lstat('/home/sschwarzer/')
305         self.failUnless(stat.S_ISDIR(stat_result.st_mode))
306
307     def test_stat_following_link(self):
308         """Test `stat` when invoked on a link."""
309         # simple link
310         stat_result = self.stat.stat('/home/link')
311         self.assertEqual(stat_result.st_size, 4604)
312         # link pointing to a link
313         stat_result = self.stat.stat('/home/python/link_link')
314         self.assertEqual(stat_result.st_size, 4604)
315         stat_result = self.stat.stat('../python/link_link')
316         self.assertEqual(stat_result.st_size, 4604)
317         # recursive link structures
318         self.assertRaises(ftp_error.PermanentError, self.stat.stat,
319                           '../python/bad_link')
320         self.assertRaises(ftp_error.PermanentError, self.stat.stat,
321                           '/home/bad_link')
322
323     #
324     # test automatic switching of Unix/MS parsers
325     #
326     def test_parser_switching_with_permanent_error(self):
327         """Test non-switching of parser format with `PermanentError`."""
328         self.assertEqual(self.stat._allow_parser_switching, True)
329         # with these directory contents, we get a `ParserError` for
330         #  the Unix parser, so `_allow_parser_switching` can be
331         #  switched off no matter whether we got a `PermanentError`
332         #  or not
333         self.assertRaises(ftp_error.PermanentError, self.stat.lstat,
334                           "/home/msformat/nonexistent")
335         self.assertEqual(self.stat._allow_parser_switching, False)
336
337     def test_parser_switching_default_to_unix(self):
338         """Test non-switching of parser format; stay with Unix."""
339         self.assertEqual(self.stat._allow_parser_switching, True)
340         self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
341         stat_result = self.stat.lstat("/home/sschwarzer/index.html")
342         self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
343         self.assertEqual(self.stat._allow_parser_switching, False)
344
345     def test_parser_switching_to_ms(self):
346         """Test switching of parser from Unix to MS format."""
347         self.assertEqual(self.stat._allow_parser_switching, True)
348         self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
349         stat_result = self.stat.lstat("/home/msformat/abcd.exe")
350         self.failUnless(isinstance(self.stat._parser, ftp_stat.MSParser))
351         self.assertEqual(self.stat._allow_parser_switching, False)
352         self.assertEqual(stat_result._st_name, "abcd.exe")
353         self.assertEqual(stat_result.st_size, 12266720)
354
355     def test_parser_switching_regarding_empty_dir(self):
356         """Test switching of parser if a directory is empty."""
357         self.assertEqual(self.stat._allow_parser_switching, True)
358         result = self.stat.listdir("/home/msformat/XPLaunch/empty")
359         self.assertEqual(result, [])
360         self.assertEqual(self.stat._allow_parser_switching, True)
361         self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
362
363
364 class TestListdir(unittest.TestCase):
365     """Test `FTPHost.listdir`."""
366     def setUp(self):
367         self.stat = test_stat()
368
369     def test_failing_listdir(self):
370         """Test failing `FTPHost.listdir`."""
371         self.assertRaises(ftp_error.PermanentError,
372                           self.stat.listdir, 'notthere')
373
374     def test_succeeding_listdir(self):
375         """Test succeeding `FTPHost.listdir`."""
376         # do we have all expected "files"?
377         self.assertEqual(len(self.stat.listdir('.')), 9)
378         # have they the expected names?
379         expected = ('chemeng download image index.html os2 '
380                     'osup publications python scios2').split()
381         remote_file_list = self.stat.listdir('.')
382         for file in expected:
383             self.failUnless(file in remote_file_list)
384
385
386 if __name__ == '__main__':
387     unittest.main()
388
Note: See TracBrowser for help on using the browser.