~sschwarzer/ftputil

ftputil/test/test_public_servers.py -rw-r--r-- 7.0 KiB
77f2ca24Stefan Schwarzer Move item "Push to repository" a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# Copyright (C) 2009-2023, Stefan Schwarzer <sschwarzer@sschwarzer.net>
# and ftputil contributors (see `doc/contributors.txt`)
# See the file LICENSE for licensing terms.

import os
import socket
import subprocess

import pytest

import ftputil

import test


def email_address():
    """
    Return the email address used to identify the client to an FTP server.

    If the hostname is "warpy", use my (Stefan's) email address, else try to
    use the content of the `$EMAIL` environment variable. If that doesn't
    exist, use a dummy address.
    """
    hostname = socket.gethostname()
    if hostname == "warpy":
        email = "sschwarzer@sschwarzer.net"
    else:
        dummy_address = "anonymous@example.com"
        email = os.environ.get("EMAIL", dummy_address)
        if not email:
            # Environment variable exists but content is an empty string
            email = dummy_address
    return email


EMAIL = email_address()


def ftp_client_listing(server, directory):
    """
    Log into the FTP server `server` using the command line client, then change
    to the `directory` and retrieve a listing with "dir".

    Return the list of items found as an `os.listdir` would return it.
    """
    # The `-n` option prevents an auto-login.
    ftp_popen = subprocess.Popen(
        ["ftp", "-n", server],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        universal_newlines=True,
    )
    commands = ["user anonymous {}".format(EMAIL), "dir", "bye"]
    if directory:
        # Change to this directory before calling "dir".
        commands.insert(1, "cd {}".format(directory))
    input_ = "\n".join(commands)
    stdout, unused_stderr = ftp_popen.communicate(input_)
    # Collect the directory/file names from the listing's text
    names = []
    for line in stdout.strip().split("\n"):
        if line.startswith("total ") or line.startswith("Trying "):
            continue
        parts = line.split()
        if parts[-2] == "->":
            # Most likely a link
            name = parts[-3]
        else:
            name = parts[-1]
        names.append(name)
    # Remove entries for current and parent directory since they aren't
    # included in the result of `FTPHost.listdir` either.
    names = [name for name in names if name not in (".", "..")]
    return names


class TestPublicServers:
    """
    Get directory listings from various public FTP servers with a command line
    client and ftputil and compare both.

    An important aspect is to test different "spellings" of the same directory.
    For example, to list the root directory which is usually set after login,
    use "" (nothing), ".", "/", "/.", "./.", "././", "..", "../.", "../.." etc.

    The command line client `ftp` has to be in the path.
    """

    # Implementation note:
    #
    # I (Stefan) implement the code so it works with Ubuntu's client. Other
    # clients may work or not. If you have problems testing some other client,
    # please send me a (small) patch. Keep in mind that I don't plan supporting
    # as many FTP obscure commandline clients as servers. ;-)

    # List of pairs with server name and a directory "guaranteed to exist"
    # under the login directory which is assumed to be the root directory.
    servers = [
        # Posix format
        ("ftp.de.debian.org", "debian"),
        ("ftp.heise.de", "pub"),
        ("ftp.tu-chemnitz.de", "pub"),
        ("ftp.uni-erlangen.de", "pub"),
        # DOS/Microsoft format
        # Do you know any FTP servers that use Microsoft format?
        # `ftp.microsoft.com` doesn't seem to be reachable anymore.
    ]

    # This data structure contains the initial directories "." and "DIR" (which
    # will be replaced by a valid directory name for each server). The list
    # after the initial directory contains paths that will be queried after
    # changing into the initial directory. All items in these lists are
    # actually supposed to yield the same directory contents.
    paths_table = [
        (
            ".",
            [
                ".",
                "/",
                "/.",
                "./.",
                "././",
                "..",
                "../.",
                "../..",
                "DIR/..",
                "/DIR/../.",
                "/DIR/../..",
            ],
        ),
        ("DIR", [".", "/DIR", "/DIR/", "../DIR", "../../DIR"]),
    ]

    def inner_test_server(self, server, initial_directory, paths):
        """
        Test one server for one initial directory.

        Connect to the server `server`; if the string argument
        `initial_directory` has a true value, change to this directory. Then
        iterate over all strings in the sequence `paths`, comparing the results
        of a listdir call with the listing from the command line client.
        """
        canonical_names = ftp_client_listing(server, initial_directory)
        host = ftputil.FTPHost(server, "anonymous", EMAIL)
        try:
            host.chdir(initial_directory)
            for path in paths:
                path = path.replace("DIR", initial_directory)
                # Make sure that we don't recycle directory entries, i. e.
                # really repeatedly retrieve the directory contents (shouldn't
                # happen anyway with the current implementation).
                host.stat_cache.clear()
                names = host.listdir(path)
                # Filter out "hidden" names since the FTP command line client
                # won't include them in its listing either.
                names = [
                    name
                    for name in names
                    if not (
                        name.startswith(".")
                        or
                        # The login directory of `ftp.microsoft.com` contains
                        # this "hidden" entry that ftputil finds but not the
                        # FTP command line client.
                        name == "mscomtest"
                    )
                ]
                failure_message = "For server {}, directory {}: {} != {}".format(
                    server, initial_directory, names, canonical_names
                )
                assert names == canonical_names, failure_message
        finally:
            host.close()

    @pytest.mark.slow_test
    def test_servers(self):
        """
        Test all servers in `self.servers`.

        For each server, get the listings for the login directory and one other
        directory which is known to exist. Use different "spellings" to
        retrieve each list via ftputil and compare with the results gotten with
        the command line client.
        """
        for server, actual_initial_directory in self.servers:
            print("=== server:", server)
            for initial_directory, paths in self.paths_table:
                initial_directory = initial_directory.replace(
                    "DIR", actual_initial_directory
                )
                print(server, initial_directory)
                self.inner_test_server(server, initial_directory, paths)