source: _test_ftp_stat.py @ 642:69be188f3e6e

Last change on this file since 642:69be188f3e6e was 642:69be188f3e6e, checked in by Stefan Schwarzer <sschwarzer@…>, 15 years ago
Fixed typo.
File size: 16.3 KB
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
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          ]
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
251class 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
364class 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
386if __name__ == '__main__':
387    unittest.main()
388
Note: See TracBrowser for help on using the repository browser.