source: _test_ftp_stat.py @ 798:ff1b73253239

Last change on this file since 798:ff1b73253239 was 798:ff1b73253239, checked in by Stefan Schwarzer <sschwarzer@…>, 12 years ago
Fix handling of 12 AM and 12 PM times in the MS format parser.
File size: 16.7 KB
Line 
1# Copyright (C) 2003-2009, 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
34from __future__ import division
35
36import stat
37import time
38import unittest
39
40import _test_base
41import ftp_error
42import ftp_stat
43import ftputil
44
45
46def 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
53def 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
63class 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          "07-17-09  12:08AM             12266720 test.exe",
156          "07-17-09  12:08PM             12266720 test.exe"
157          ]
158        expected_stat_results = [
159          [16640, None, None, None, None, None, None, None,
160           (2001, 7, 27, 11, 16, 0), None],
161          [16640, None, None, None, None, None, None, None,
162           (1995, 10, 23, 15, 25, 0), None],
163          [33024, None, None, None, None, None, 12266720, None,
164           (2000, 7, 17, 14, 8, 0), None],
165          [33024, None, None, None, None, None, 12266720, None,
166           (2009, 7, 17, 0, 8, 0), None],
167          [33024, None, None, None, None, None, 12266720, None,
168           (2009, 7, 17, 12, 8, 0), None]
169          ]
170        self._test_valid_lines(ftp_stat.MSParser, lines, expected_stat_results)
171
172    def test_invalid_ms_lines(self):
173        lines = [
174          "07-27-01  11:16AM                      Test",
175          "07-17-00  02:08             12266720 test.exe",
176          "07-17-00  02:08AM           1226672x test.exe"
177          ]
178        self._test_invalid_lines(ftp_stat.MSParser, lines)
179
180    #
181    # the following code checks if the decision logic in the Unix
182    #  line parser for determining the year works
183    #
184    def datetime_string(self, time_float):
185        """
186        Return a datetime string generated from the value in
187        `time_float`. The parameter value is a floating point value
188        as returned by `time.time()`. The returned string is built as
189        if it were from a Unix FTP server (format: MMM dd hh:mm")
190        """
191        time_tuple = time.localtime(time_float)
192        return time.strftime("%b %d %H:%M", time_tuple)
193
194    def dir_line(self, time_float):
195        """
196        Return a directory line as from a Unix FTP server. Most of
197        the contents are fixed, but the timestamp is made from
198        `time_float` (seconds since the epoch, as from `time.time()`).
199        """
200        line_template = "-rw-r--r--   1   45854   200   4604   %s   index.html"
201        return line_template % self.datetime_string(time_float)
202
203    def assert_equal_times(self, time1, time2):
204        """
205        Check if both times (seconds since the epoch) are equal. For
206        the purpose of this test, two times are "equal" if they
207        differ no more than one minute from each other.
208
209        If the test fails, an exception is raised by the inherited
210        `failIf` method.
211        """
212        abs_difference = abs(time1 - time2)
213        try:
214            self.failIf(abs_difference > 60.0)
215        except AssertionError:
216            print "Difference is", abs_difference, "seconds"
217            raise
218
219    def _test_time_shift(self, supposed_time_shift, deviation=0.0):
220        """
221        Check if the stat parser considers the time shift value
222        correctly. `deviation` is the difference between the actual
223        time shift and the supposed time shift, which is rounded
224        to full hours.
225        """
226        host = _test_base.ftp_host_factory()
227        # explicitly use Unix format parser
228        host._stat._parser = ftp_stat.UnixParser()
229        host.set_time_shift(supposed_time_shift)
230        server_time = time.time() + supposed_time_shift + deviation
231        stat_result = host._stat._parser.parse_line(self.dir_line(server_time),
232                                                    host.time_shift())
233        self.assert_equal_times(stat_result.st_mtime, server_time)
234
235    def test_time_shifts(self):
236        """Test correct year depending on time shift value."""
237        # 1. test: client and server share the same local time
238        self._test_time_shift(0.0)
239        # 2. test: server is three hours ahead of client
240        self._test_time_shift(3 * 60 * 60)
241        # 3. test: client is three hours ahead of server
242        self._test_time_shift(- 3 * 60 * 60)
243        # 4. test: server 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        # 5. test: server 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        # 6. test: client is supposed to be three hours ahead, but
250        #  is ahead three hours and one minute
251        self._test_time_shift(-3 * 60 * 60, -60)
252        # 7. test: client is supposed to be three hours ahead, but
253        #  is ahead three hours minus one minute
254        self._test_time_shift(-3 * 60 * 60, 60)
255
256
257class TestLstatAndStat(unittest.TestCase):
258    """
259    Test `FTPHost.lstat` and `FTPHost.stat` (test currently only
260    implemented for Unix server format).
261    """
262    def setUp(self):
263        self.stat = test_stat()
264
265    def test_failing_lstat(self):
266        """Test whether lstat fails for a nonexistent path."""
267        self.assertRaises(ftp_error.PermanentError, self.stat.lstat,
268                          '/home/sschw/notthere')
269        self.assertRaises(ftp_error.PermanentError, self.stat.lstat,
270                          '/home/sschwarzer/notthere')
271
272    def test_lstat_for_root(self):
273        """Test `lstat` for `/` .
274        Note: `(l)stat` works by going one directory up and parsing
275        the output of an FTP `DIR` command. Unfortunately, it's not
276        possible to do this for the root directory `/`.
277        """
278        self.assertRaises(ftp_error.RootDirError, self.stat.lstat, '/')
279        try:
280            self.stat.lstat('/')
281        except ftp_error.RootDirError, exc_obj:
282            self.failIf(isinstance(exc_obj, ftp_error.FTPOSError))
283
284    def test_lstat_one_file(self):
285        """Test `lstat` for a file."""
286        stat_result = self.stat.lstat('/home/sschwarzer/index.html')
287        self.assertEqual(oct(stat_result.st_mode), '0100644')
288        self.assertEqual(stat_result.st_size, 4604)
289
290    def test_lstat_one_dir(self):
291        """Test `lstat` for a directory."""
292        stat_result = self.stat.lstat('/home/sschwarzer/scios2')
293        self.assertEqual(oct(stat_result.st_mode), '042755')
294        self.assertEqual(stat_result.st_ino, None)
295        self.assertEqual(stat_result.st_dev, None)
296        self.assertEqual(stat_result.st_nlink, 6)
297        self.assertEqual(stat_result.st_uid, '45854')
298        self.assertEqual(stat_result.st_gid, '200')
299        self.assertEqual(stat_result.st_size, 512)
300        self.assertEqual(stat_result.st_atime, None)
301        self.failUnless(stat_result.st_mtime ==
302                        stat_tuple_to_seconds((1999, 9, 20, 0, 0, 0)))
303        self.assertEqual(stat_result.st_ctime, None)
304        self.failUnless(stat_result ==
305          (17901, None, None, 6, '45854', '200', 512, None,
306           stat_tuple_to_seconds((1999, 9, 20, 0, 0, 0)), None))
307
308    def test_lstat_via_stat_module(self):
309        """Test `lstat` indirectly via `stat` module."""
310        stat_result = self.stat.lstat('/home/sschwarzer/')
311        self.failUnless(stat.S_ISDIR(stat_result.st_mode))
312
313    def test_stat_following_link(self):
314        """Test `stat` when invoked on a link."""
315        # simple link
316        stat_result = self.stat.stat('/home/link')
317        self.assertEqual(stat_result.st_size, 4604)
318        # link pointing to a link
319        stat_result = self.stat.stat('/home/python/link_link')
320        self.assertEqual(stat_result.st_size, 4604)
321        stat_result = self.stat.stat('../python/link_link')
322        self.assertEqual(stat_result.st_size, 4604)
323        # recursive link structures
324        self.assertRaises(ftp_error.PermanentError, self.stat.stat,
325                          '../python/bad_link')
326        self.assertRaises(ftp_error.PermanentError, self.stat.stat,
327                          '/home/bad_link')
328
329    #
330    # test automatic switching of Unix/MS parsers
331    #
332    def test_parser_switching_with_permanent_error(self):
333        """Test non-switching of parser format with `PermanentError`."""
334        self.assertEqual(self.stat._allow_parser_switching, True)
335        # with these directory contents, we get a `ParserError` for
336        #  the Unix parser, so `_allow_parser_switching` can be
337        #  switched off no matter whether we got a `PermanentError`
338        #  or not
339        self.assertRaises(ftp_error.PermanentError, self.stat.lstat,
340                          "/home/msformat/nonexistent")
341        self.assertEqual(self.stat._allow_parser_switching, False)
342
343    def test_parser_switching_default_to_unix(self):
344        """Test non-switching of parser format; stay with Unix."""
345        self.assertEqual(self.stat._allow_parser_switching, True)
346        self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
347        stat_result = self.stat.lstat("/home/sschwarzer/index.html")
348        self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
349        self.assertEqual(self.stat._allow_parser_switching, False)
350
351    def test_parser_switching_to_ms(self):
352        """Test switching of parser from Unix to MS format."""
353        self.assertEqual(self.stat._allow_parser_switching, True)
354        self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
355        stat_result = self.stat.lstat("/home/msformat/abcd.exe")
356        self.failUnless(isinstance(self.stat._parser, ftp_stat.MSParser))
357        self.assertEqual(self.stat._allow_parser_switching, False)
358        self.assertEqual(stat_result._st_name, "abcd.exe")
359        self.assertEqual(stat_result.st_size, 12266720)
360
361    def test_parser_switching_regarding_empty_dir(self):
362        """Test switching of parser if a directory is empty."""
363        self.assertEqual(self.stat._allow_parser_switching, True)
364        result = self.stat.listdir("/home/msformat/XPLaunch/empty")
365        self.assertEqual(result, [])
366        self.assertEqual(self.stat._allow_parser_switching, True)
367        self.failUnless(isinstance(self.stat._parser, ftp_stat.UnixParser))
368
369
370class TestListdir(unittest.TestCase):
371    """Test `FTPHost.listdir`."""
372    def setUp(self):
373        self.stat = test_stat()
374
375    def test_failing_listdir(self):
376        """Test failing `FTPHost.listdir`."""
377        self.assertRaises(ftp_error.PermanentError,
378                          self.stat.listdir, 'notthere')
379
380    def test_succeeding_listdir(self):
381        """Test succeeding `FTPHost.listdir`."""
382        # do we have all expected "files"?
383        self.assertEqual(len(self.stat.listdir('.')), 9)
384        # have they the expected names?
385        expected = ('chemeng download image index.html os2 '
386                    'osup publications python scios2').split()
387        remote_file_list = self.stat.listdir('.')
388        for file in expected:
389            self.failUnless(file in remote_file_list)
390
391
392if __name__ == '__main__':
393    unittest.main()
394
Note: See TracBrowser for help on using the repository browser.