root/_test_real_ftp.py @ 869:808476873684

Revision 869:808476873684, 23.4 kB (checked in by Stefan Schwarzer <sschwarzer@…>, 4 months ago)
Replaced licenses in each file with reference to common `LICENSE` file (suggested by Steve Steiner). I removed the copyright notice for Roger Demetrescu from `_test_ftputil.py` because I remembered only after the last commit that the tests for the `with` statement had gone into their own file, `_test_with_statement.py`. I added Roger's name there, and also in `ftp_file.py` which had also gotten `with` support.
Line 
1# Copyright (C) 2003-2010, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# See the file LICENSE for licensing terms.
3
4# Execute a test on a real FTP server (other tests use a mock server)
5
6import getpass
7import operator
8import os
9import time
10import unittest
11import stat
12import sys
13
14import ftputil
15from ftputil import ftp_error
16from ftputil import ftp_stat
17
18
19def get_login_data():
20    """
21    Return a three-element tuple consisting of server name, user id
22    and password. The data - used to be - requested interactively.
23    """
24    #server = raw_input("Server: ")
25    #user = raw_input("User: ")
26    #password = getpass.getpass()
27    #return server, user, password
28    return "localhost", 'ftptest', 'd605581757de5eb56d568a4419f4126e'
29
30def utc_local_time_shift():
31    """
32    Return the expected time shift in seconds assuming the server
33    uses UTC in its listings and the client uses local time.
34
35    This is needed because Pure-FTPd meanwhile seems to insist that
36    the displayed time for files is in UTC.
37    """
38    utc_tuple = time.gmtime()
39    localtime_tuple = time.localtime()
40    # to calculate the correct times shift, we need to ignore the
41    #  DST component in the localtime tuple, i. e. set it to 0
42    localtime_tuple = localtime_tuple[:-1] + (0,)
43    time_shift_in_seconds = time.mktime(utc_tuple) - \
44                            time.mktime(localtime_tuple)
45    # to be safe, round the above value to units of 3600 s (1 hour)
46    return round(time_shift_in_seconds / 3600.0) * 3600
47
48# difference between local times of server and client; if 0.0, server
49#  and client use the same timezone
50#EXPECTED_TIME_SHIFT = utc_local_time_shift()
51# Pure-FTPd seems to have changed its mind (see docstring of
52#  `utc_local_time_shift`
53EXPECTED_TIME_SHIFT = 0.0
54
55
56class Cleaner(object):
57    """This class helps to remove directories and files which
58    might be left behind if a test fails in unexpected ways.
59    """
60
61    def __init__(self, host):
62        # the test class (probably `RealFTPTest`) and the helper
63        #  class share the same `FTPHost` object
64        self._host = host
65        self._ftp_items = []
66
67    def add_dir(self, path):
68        """Schedule a directory with path `path` for removal."""
69        self._ftp_items.append(('d', self._host.path.abspath(path)))
70
71    def add_file(self, path):
72        """Schedule a file with path `path` for removal."""
73        self._ftp_items.append(('f', self._host.path.abspath(path)))
74
75    def clean(self):
76        """Remove the directories and files previously remembered.
77        The removal works in reverse order of the scheduling with
78        `add_dir` and `add_file`.
79
80        Errors due to a removal are ignored.
81        """
82        self._host.chdir("/")
83        # code should work with Python 2.3
84        self._ftp_items.reverse()
85        for type_, path in self._ftp_items:
86            try:
87                if type_ == 'd':
88                    # if something goes wrong in `rmtree` we might
89                    #  leave a mess behind
90                    self._host.rmtree(path)
91                elif type_ == 'f':
92                    # minor mess if `remove` fails
93                    self._host.remove(path)
94            except ftp_error.FTPError:
95                pass
96
97
98class RealFTPTest(unittest.TestCase):
99    def setUp(self):
100        self.host = ftputil.FTPHost(server, user, password)
101        self.cleaner = Cleaner(self.host)
102
103    def tearDown(self):
104        self.cleaner.clean()
105        self.host.close()
106
107    #
108    # helper methods
109    #
110    def make_file(self, path):
111        self.cleaner.add_file(path)
112        file_ = self.host.file(path, 'wb')
113        # write something; otherwise the FTP server might not update
114        #  the time of last modification if the file existed before
115        file_.write("\n")
116        file_.close()
117
118    def make_local_file(self):
119        fobj = file('_local_file_', 'wb')
120        fobj.write("abc\x12\x34def\t")
121        fobj.close()
122
123    #
124    # `mkdir`, `makedirs`, `rmdir` and `rmtree`
125    #
126    def test_mkdir_rmdir(self):
127        host = self.host
128        dir_name = "_testdir_"
129        file_name = host.path.join(dir_name, "_nonempty_")
130        self.cleaner.add_dir(dir_name)
131        # make dir and check if it's there
132        host.mkdir(dir_name)
133        files = host.listdir(host.curdir)
134        self.failIf(dir_name not in files)
135        # try to remove non-empty directory
136        self.cleaner.add_file(file_name)
137        non_empty = host.file(file_name, "w")
138        non_empty.close()
139        self.assertRaises(ftp_error.PermanentError, host.rmdir, dir_name)
140        # remove file
141        host.unlink(file_name)
142        # `remove` on a directory should fail
143        try:
144            try:
145                host.remove(dir_name)
146            except ftp_error.PermanentError, exc:
147                self.failUnless(str(exc).startswith(
148                                "remove/unlink can only delete files"))
149            else:
150                self.failIf(True, "we shouldn't have come here")
151        finally:
152            # delete empty directory
153            host.rmdir(dir_name)
154        files = host.listdir(host.curdir)
155        self.failIf(dir_name in files)
156
157    def test_makedirs_without_existing_dirs(self):
158        host = self.host
159        # no `_dir1_` yet
160        self.failIf('_dir1_' in host.listdir(host.curdir))
161        # vanilla case, all should go well
162        host.makedirs('_dir1_/dir2/dir3/dir4')
163        self.cleaner.add_dir('_dir1_')
164        # check host
165        self.failUnless(host.path.isdir('_dir1_'))
166        self.failUnless(host.path.isdir('_dir1_/dir2'))
167        self.failUnless(host.path.isdir('_dir1_/dir2/dir3'))
168        self.failUnless(host.path.isdir('_dir1_/dir2/dir3/dir4'))
169
170    def test_makedirs_from_non_root_directory(self):
171        # this is a testcase for issue #22, see
172        #  http://ftputil.sschwarzer.net/trac/ticket/22
173        host = self.host
174        # no `_dir1_` and `_dir2_` yet
175        self.failIf('_dir1_' in host.listdir(host.curdir))
176        self.failIf('_dir2_' in host.listdir(host.curdir))
177        # part 1: try to make directories starting from `_dir1_`
178        # make and change to non-root directory
179        self.cleaner.add_dir("_dir1_")
180        host.mkdir('_dir1_')
181        host.chdir('_dir1_')
182        host.makedirs('_dir2_/_dir3_')
183        # test for expected directory hierarchy
184        self.failUnless(host.path.isdir('/_dir1_'))
185        self.failUnless(host.path.isdir('/_dir1_/_dir2_'))
186        self.failUnless(host.path.isdir('/_dir1_/_dir2_/_dir3_'))
187        self.failIf(host.path.isdir('/_dir1_/_dir1_'))
188        # remove all but the directory we're in
189        host.rmdir('/_dir1_/_dir2_/_dir3_')
190        host.rmdir('/_dir1_/_dir2_')
191        # part 2: try to make directories starting from root
192        self.cleaner.add_dir("/_dir2_")
193        host.makedirs('/_dir2_/_dir3_')
194        # test for expected directory hierarchy
195        self.failUnless(host.path.isdir('/_dir2_'))
196        self.failUnless(host.path.isdir('/_dir2_/_dir3_'))
197        self.failIf(host.path.isdir('/_dir1_/_dir2_'))
198
199    def test_makedirs_from_non_root_directory_fake_windows_os(self):
200        saved_sep = os.sep
201        os.sep = '\\'
202        try:
203            self.test_makedirs_from_non_root_directory()
204        finally:
205            os.sep = saved_sep
206
207    def test_makedirs_of_existing_directory(self):
208        host = self.host
209        # the (chrooted) login directory
210        host.makedirs('/')
211
212    def test_makedirs_with_file_in_the_way(self):
213        host = self.host
214        self.cleaner.add_dir('_dir1_')
215        host.mkdir('_dir1_')
216        self.make_file('_dir1_/file1')
217        # try it
218        self.assertRaises(ftp_error.PermanentError, host.makedirs,
219                          '_dir1_/file1')
220        self.assertRaises(ftp_error.PermanentError, host.makedirs,
221                          '_dir1_/file1/dir2')
222
223    def test_makedirs_with_existing_directory(self):
224        host = self.host
225        self.cleaner.add_dir("_dir1_")
226        host.mkdir('_dir1_')
227        host.makedirs('_dir1_/dir2')
228        # check
229        self.failUnless(host.path.isdir('_dir1_'))
230        self.failUnless(host.path.isdir('_dir1_/dir2'))
231
232    def test_makedirs_in_non_writable_directory(self):
233        host = self.host
234        # preparation: `rootdir1` exists but is only writable by root
235        self.assertRaises(ftp_error.PermanentError, host.makedirs,
236                          'rootdir1/dir2')
237
238    def test_makedirs_with_writable_directory_at_end(self):
239        host = self.host
240        self.cleaner.add_dir('rootdir2/dir2')
241        # preparation: `rootdir2` exists but is only writable by root;
242        #  `dir2` is writable by regular ftp user
243        # these both should work
244        host.makedirs('rootdir2/dir2')
245        host.makedirs('rootdir2/dir2/dir3')
246
247    def test_rmtree_without_error_handler(self):
248        host = self.host
249        # build a tree
250        self.cleaner.add_dir('_dir1_')
251        host.makedirs('_dir1_/dir2')
252        self.make_file('_dir1_/file1')
253        self.make_file('_dir1_/file2')
254        self.make_file('_dir1_/dir2/file3')
255        self.make_file('_dir1_/dir2/file4')
256        # try to remove a _file_ with `rmtree`
257        self.assertRaises(ftp_error.PermanentError, host.rmtree, '_dir1_/file2')
258        # remove dir2
259        host.rmtree('_dir1_/dir2')
260        self.failIf(host.path.exists('_dir1_/dir2'))
261        self.failUnless(host.path.exists('_dir1_/file2'))
262        # remake dir2 and remove _dir1_
263        host.mkdir('_dir1_/dir2')
264        self.make_file('_dir1_/dir2/file3')
265        self.make_file('_dir1_/dir2/file4')
266        host.rmtree('_dir1_')
267        self.failIf(host.path.exists('_dir1_'))
268
269    def test_rmtree_with_error_handler(self):
270        host = self.host
271        self.cleaner.add_dir('_dir1_')
272        host.mkdir('_dir1_')
273        self.make_file('_dir1_/file1')
274        # prepare error "handler"
275        log = []
276        def error_handler(*args):
277            log.append(args)
278        # try to remove a file as root "directory"
279        host.rmtree('_dir1_/file1', ignore_errors=True, onerror=error_handler)
280        self.assertEqual(log, [])
281        host.rmtree('_dir1_/file1', ignore_errors=False, onerror=error_handler)
282        self.assertEqual(log[0][0], host.listdir)
283        self.assertEqual(log[0][1], '_dir1_/file1')
284        self.assertEqual(log[1][0], host.rmdir)
285        self.assertEqual(log[1][1], '_dir1_/file1')
286        host.rmtree('_dir1_')
287        # try to remove a non-existent directory
288        del log[:]
289        host.rmtree('_dir1_', ignore_errors=False, onerror=error_handler)
290        self.assertEqual(log[0][0], host.listdir)
291        self.assertEqual(log[0][1], '_dir1_')
292        self.assertEqual(log[1][0], host.rmdir)
293        self.assertEqual(log[1][1], '_dir1_')
294
295    #
296    # directory tree walking
297    #
298    def test_walk_topdown(self):
299        # preparation: build tree in directory `walk_test`
300        host = self.host
301        expected = [
302          ('walk_test', ['dir1', 'dir2', 'dir3'], ['file4']),
303          ('walk_test/dir1', ['dir11', 'dir12'], []),
304          ('walk_test/dir1/dir11', [], []),
305          ('walk_test/dir1/dir12', ['dir123'], ['file121', 'file122']),
306          ('walk_test/dir1/dir12/dir123', [], ['file1234']),
307          ('walk_test/dir2', [], []),
308          ('walk_test/dir3', ['dir33'], ['file31', 'file32']),
309          ('walk_test/dir3/dir33', [], []),
310          ]
311        # collect data, using `walk`
312        actual = []
313        for items in host.walk('walk_test'):
314            actual.append(items)
315        # compare with expected results
316        self.assertEqual(len(actual), len(expected))
317        for index in range(len(actual)):
318            self.assertEqual(actual[index], expected[index])
319
320    def test_walk_depth_first(self):
321        # preparation: build tree in directory `walk_test`
322        host = self.host
323        expected = [
324          ('walk_test/dir1/dir11', [], []),
325          ('walk_test/dir1/dir12/dir123', [], ['file1234']),
326          ('walk_test/dir1/dir12', ['dir123'], ['file121', 'file122']),
327          ('walk_test/dir1', ['dir11', 'dir12'], []),
328          ('walk_test/dir2', [], []),
329          ('walk_test/dir3/dir33', [], []),
330          ('walk_test/dir3', ['dir33'], ['file31', 'file32']),
331          ('walk_test', ['dir1', 'dir2', 'dir3'], ['file4'])
332          ]
333        # collect data, using `walk`
334        actual = []
335        for items in host.walk('walk_test', topdown=False):
336            actual.append(items)
337        # compare with expected results
338        self.assertEqual(len(actual), len(expected))
339        for index in range(len(actual)):
340            self.assertEqual(actual[index], expected[index])
341
342    #
343    # renaming
344    #
345    def test_rename(self):
346        host = self.host
347        # make sure the target of the renaming operation is removed
348        self.cleaner.add_file('_testfile2_')
349        self.make_file("_testfile1_")
350        host.rename('_testfile1_', '_testfile2_')
351        self.failIf(host.path.exists('_testfile1_'))
352        self.failUnless(host.path.exists('_testfile2_'))
353        host.remove('_testfile2_')
354
355    def test_rename_with_spaces_in_directory(self):
356        host = self.host
357        dir_name = "_dir with spaces_"
358        self.cleaner.add_dir(dir_name)
359        host.mkdir(dir_name)
360        self.make_file(dir_name + "/testfile1")
361        host.rename(dir_name + "/testfile1", dir_name + "/testfile2")
362        self.failIf(host.path.exists(dir_name + "/testfile1"))
363        self.failUnless(host.path.exists(dir_name + "/testfile2"))
364
365    #
366    # stat'ing
367    #
368    def test_stat(self):
369        host = self.host
370        dir_name = "_testdir_"
371        file_name = host.path.join(dir_name, "_nonempty_")
372        # make a directory and a file in it
373        self.cleaner.add_dir(dir_name)
374        host.mkdir(dir_name)
375        fobj = host.file(file_name, "wb")
376        fobj.write("abc\x12\x34def\t")
377        fobj.close()
378        # do some stats
379        # - dir
380        self.assertEqual(host.listdir(dir_name), ["_nonempty_"])
381        self.assertEqual(bool(host.path.isdir(dir_name)), True)
382        self.assertEqual(bool(host.path.isfile(dir_name)), False)
383        self.assertEqual(bool(host.path.islink(dir_name)), False)
384        # - file
385        self.assertEqual(bool(host.path.isdir(file_name)), False)
386        self.assertEqual(bool(host.path.isfile(file_name)), True)
387        self.assertEqual(bool(host.path.islink(file_name)), False)
388        self.assertEqual(host.path.getsize(file_name), 9)
389        # - file's modification time; allow up to two minutes difference
390        host.synchronize_times()
391        server_mtime = host.path.getmtime(file_name)
392        client_mtime = time.mktime(time.localtime())
393        calculated_time_shift = server_mtime - client_mtime
394        self.failIf(abs(calculated_time_shift-host.time_shift()) > 120)
395
396#    def test_special_broken_link(self):
397#        # test for ticket #39
398#        # this test currently fails; I guess I'll postpone it until
399#        #  at least ftputil 2.5
400#        host = self.host
401#        broken_link_name = os.path.join("dir_with_broken_link", "nonexistent")
402#        self.assertEqual(host.lstat(broken_link_name)._st_target,
403#                         "../nonexistent/nonexistent")
404#        self.assertEqual(bool(host.path.isdir(broken_link_name)), False)
405#        self.assertEqual(bool(host.path.isfile(broken_link_name)), False)
406#        self.assertEqual(bool(host.path.islink(broken_link_name)), True)
407
408    def test_concurrent_access(self):
409        self.make_file("_testfile_")
410        host1 = ftputil.FTPHost(server, user, password)
411        host2 = ftputil.FTPHost(server, user, password)
412        stat_result1 = host1.stat("_testfile_")
413        stat_result2 = host2.stat("_testfile_")
414        self.assertEqual(stat_result1, stat_result2)
415        host2.remove("_testfile_")
416        # can still get the result via `host1`
417        stat_result1 = host1.stat("_testfile_")
418        self.assertEqual(stat_result1, stat_result2)
419        # stat'ing on `host2` gives an exception
420        self.assertRaises(ftp_error.PermanentError, host2.stat, "_testfile_")
421        # stat'ing on `host1` after invalidation
422        absolute_path = host1.path.join(host1.getcwd(), "_testfile_")
423        host1.stat_cache.invalidate(absolute_path)
424        self.assertRaises(ftp_error.PermanentError, host1.stat, "_testfile_")
425
426    #
427    # `upload` (including time shift test)
428    #
429    def test_time_shift(self):
430        self.host.synchronize_times()
431        self.assertEqual(self.host.time_shift(), EXPECTED_TIME_SHIFT)
432
433    def test_upload(self):
434        host = self.host
435        host.synchronize_times()
436        local_file = '_local_file_'
437        remote_file = '_remote_file_'
438        # make local file to upload
439        self.make_local_file()
440        # wait; else small time differences between client and server
441        #  actually could trigger the update
442        time.sleep(65)
443        try:
444            self.cleaner.add_file(remote_file)
445            host.upload(local_file, remote_file, 'b')
446            # retry; shouldn't be uploaded
447            uploaded = host.upload_if_newer(local_file, remote_file, 'b')
448            self.assertEqual(uploaded, False)
449            # rewrite the local file
450            self.make_local_file()
451            # retry; should be uploaded now
452            uploaded = host.upload_if_newer(local_file, remote_file, 'b')
453            self.assertEqual(uploaded, True)
454        finally:
455            # clean up
456            os.unlink(local_file)
457
458    def test_download(self):
459        host = self.host
460        host.synchronize_times()
461        local_file = '_local_file_'
462        remote_file = '_remote_file_'
463        # make a remote file
464        self.make_file(remote_file)
465        # file should be downloaded as it's not present yet
466        downloaded = host.download_if_newer(remote_file, local_file, 'b')
467        self.assertEqual(downloaded, True)
468        try:
469            # local file is present and newer, so shouldn't download
470            downloaded = host.download_if_newer(remote_file, local_file, 'b')
471            self.assertEqual(downloaded, False)
472            # wait; else small time differences between client and server
473            #  actually could trigger the update
474            time.sleep(65)
475            # re-make the remote file
476            self.make_file(remote_file)
477            # local file is present but older, so should download
478            downloaded = host.download_if_newer(remote_file, local_file, 'b')
479            self.assertEqual(downloaded, True)
480        finally:
481            # clean up
482            os.unlink(local_file)
483
484    #
485    # remove/unlink
486    #
487    def test_remove_non_existent_item(self):
488        host = self.host
489        self.assertRaises(ftp_error.PermanentError, host.remove, "nonexistent")
490
491    def test_remove_existent_file(self):
492        self.cleaner.add_file('_testfile_')
493        self.make_file('_testfile_')
494        host = self.host
495        self.failUnless(host.path.isfile('_testfile_'))
496        host.remove('_testfile_')
497        self.failIf(host.path.exists('_testfile_'))
498
499    #
500    # `chmod`
501    #
502    def assert_mode(self, path, expected_mode):
503        """Return an integer containing the allowed bits in the
504        mode change command.
505
506        The `FTPHost` object to test against is `self.host`.
507        """
508        full_mode = self.host.stat(path).st_mode
509        # remove flags we can't set via `chmod`
510        # allowed flags according to Python documentation
511        #  http://docs.python.org/lib/os-file-dir.html
512        allowed_flags = [stat.S_ISUID, stat.S_ISGID, stat.S_ENFMT,
513          stat.S_ISVTX, stat.S_IREAD, stat.S_IWRITE, stat.S_IEXEC,
514          stat.S_IRWXU, stat.S_IRUSR, stat.S_IWUSR, stat.S_IXUSR,
515          stat.S_IRWXG, stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP,
516          stat.S_IRWXO, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH]
517        allowed_mask = reduce(operator.or_, allowed_flags)
518        mode = full_mode & allowed_mask
519        self.assertEqual(mode, expected_mode,
520                         "mode %s != %s" % (oct(mode), oct(expected_mode)))
521
522    def test_chmod_existing_directory(self):
523        host = self.host
524        host.mkdir("_test dir_")
525        self.cleaner.add_dir("_test dir_")
526        # set/get mode of a directory
527        host.chmod("_test dir_", 0757)
528        self.assert_mode("_test dir_", 0757)
529        # set/get mode in nested directory
530        host.mkdir("_test dir_/nested_dir")
531        self.cleaner.add_dir("_test dir_/nested_dir")
532        # set/get mode of a directory
533        host.chmod("_test dir_/nested_dir", 0757)
534        self.assert_mode("_test dir_/nested_dir", 0757)
535
536    def test_chmod_existing_file(self):
537        host = self.host
538        host.mkdir("_test dir_")
539        self.cleaner.add_dir("_test dir_")
540        # set/get mode on a file
541        file_name = host.path.join("_test dir_", "_testfile_")
542        self.make_file(file_name)
543        host.chmod(file_name, 0646)
544        self.assert_mode(file_name, 0646)
545
546    def test_chmod_nonexistent_path(self):
547        # set/get mode of a directory
548        self.assertRaises(ftp_error.PermanentError, self.host.chmod,
549                          "nonexistent", 0757)
550
551    def test_cache_invalidation(self):
552        host = self.host
553        host.mkdir("_test dir_")
554        self.cleaner.add_dir("_test dir_")
555        # make sure the mode is in the cache
556        unused_stat_result = host.stat("_test dir_")
557        # set/get mode of a directory
558        host.chmod("_test dir_", 0757)
559        self.assert_mode("_test dir_", 0757)
560        # set/get mode on a file
561        file_name = host.path.join("_test dir_", "_testfile_")
562        self.make_file(file_name)
563        # make sure the mode is in the cache
564        unused_stat_result = host.stat(file_name)
565        host.chmod(file_name, 0646)
566        self.assert_mode(file_name, 0646)
567
568    #
569    # other tests
570    #
571    def test_open_for_reading(self):
572        # test for issue #17, http://ftputil.sschwarzer.net/trac/ticket/17
573        file1 = self.host.file("debian-keyring.tar.gz", 'rb')
574        file1.close()
575        # make sure that there are no problems if the connection is reused
576        file2 = self.host.file("debian-keyring.tar.gz", 'rb')
577        file2.close()
578        self.failUnless(file1._session is file2._session)
579
580    def test_names_with_spaces(self):
581        # test if directories and files with spaces in their names
582        #  can be used
583        host = self.host
584        self.failUnless(host.path.isdir("dir with spaces"))
585        self.assertEqual(host.listdir("dir with spaces"),
586                         ['second dir', 'some file', 'some_file'])
587        self.failUnless(host.path.isdir("dir with spaces/second dir"))
588        self.failUnless(host.path.isfile("dir with spaces/some_file"))
589        self.failUnless(host.path.isfile("dir with spaces/some file"))
590
591    def test_synchronize_times_without_write_access(self):
592        """Test failing synchronization because of non-writable directory."""
593        host = self.host
594        # this isn't writable by the ftp account the tests are run under
595        host.chdir("rootdir1")
596        self.assertRaises(ftp_error.TimeShiftError, host.synchronize_times)
597
598
599if __name__ == '__main__':
600    print """\
601Test for real FTP access.
602
603This test writes some files and directories on the local client and the
604remote server. Thus, you may want to skip this test by pressing [Ctrl-C].
605If the test should run, enter the login data for the remote server. You
606need write access in the login directory. This test can last a few minutes
607because it has to wait to test the timezone calculation.
608"""
609    try:
610        raw_input("[Return] to continue, or [Ctrl-C] to skip test. ")
611    except KeyboardInterrupt:
612        print "\nTest aborted."
613        sys.exit()
614    # get login data only once, not for each test
615    server, user, password = get_login_data()
616    unittest.main()
Note: See TracBrowser for help on using the browser.