source: test/scripted_session.py @ 1760:5eb6ef13362a

Last change on this file since 1760:5eb6ef13362a was 1760:5eb6ef13362a, checked in by Stefan Schwarzer <sschwarzer@…>, 3 years ago
Implement the multi-script factory more clearly Use a class `MultisessionFactory` with a `__call__` method to return consecutive factories that will use the respective scripts from the constructor argument `scripts`. See also the docstring of `MultisessionFactory` for an example.
File size: 5.5 KB
Line 
1# Copyright (C) 2018-2019, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# and ftputil contributors (see `doc/contributors.txt`)
3# See the file LICENSE for licensing terms.
4
5
6__all__ = ["Call", "ScriptedSession", "factory"]
7
8
9class Call:
10
11    def __init__(self, method_name, result=None,
12                 expected_args=None, expected_kwargs=None):
13        self.method_name = method_name
14        self.result = result
15        self.expected_args = expected_args
16        self.expected_kwargs = expected_kwargs
17
18    def __repr__(self):
19        return ("{0.__class__.__name__}("
20                "method_name={0.method_name!r}, "
21                "result={0.result!r}, "
22                "expected_args={0.expected_args!r}, "
23                "expected_kwargs={0.expected_kwargs!r})".format(self))
24
25    def check_args(self, args, kwargs):
26        if self.expected_args is not None:
27            assert args == self.expected_args
28        if self.expected_kwargs is not None:
29            assert kwargs == self.expected_kwargs
30
31    @staticmethod
32    def _is_exception_class(obj):
33        """
34        Return `True` if `obj` is an exception class, else `False`.
35        """
36        try:
37            return issubclass(obj, Exception)
38        except TypeError:
39            # TypeError: issubclass() arg 1 must be a class
40            return False
41
42    def __call__(self):
43        """
44        Simulate call, returning the result or raising the exception.
45        """
46        if isinstance(self.result, Exception) or self._is_exception_class(self.result):
47            raise self.result
48        else:
49            return self.result
50
51
52class ScriptedSession:
53    """
54    "Scripted" `ftplib.FTP`-like class for testing.
55
56    To avoid actual input/output over sockets or files, specify the
57    values that should be returned by the class's methods.
58
59    The class is instantiated with a `script` argument. This is a list
60    of `Call` objects where each object specifies the name of the
61    `ftplib.FTP` method that is expected to be called and what the
62    method should return. If the value is an exception, it will be
63    raised, not returned.
64
65    In case the method returns a socket (like `transfercmd`), the
66    return value to be specified in the `Call` instance is the content
67    of the underlying socket file.
68
69    The advantage of the approach of this class over the use of
70    `unittest.mock.Mock` objects is that the sequence of calls is
71    clearly visible. With `Mock` objects, the developer must keep in
72    mind all the calls when specifying return values or side effects
73    for the mock methods.
74    """
75
76    def __init__(self, script):
77        self.script = script
78        # Index into `script`, the list of `Call` objects
79        self._index = 0
80        # Always expect an entry for the constructor.
81        init = self._next_call(expected_method_name="__init__")
82        # The constructor isn't supposed to return anything. The only
83        # reason to call it here is to raise an exception if that was
84        # specified in the `script`.
85        init()
86
87    def _print(self, text):
88        """
89        Print `text`, prefixed with a `repr` of the `ScriptedSession`
90        instance.
91        """
92        print("<ScriptedSession at {}> {}".format(hex(id(self)), text))
93
94    def _next_call(self, expected_method_name=None):
95        """
96        Return next `Call` object.
97
98        Print the `Call` object before returning it. This is useful for
99        testing and debugging.
100        """
101        self._print("Expected method name: {!r}".format(expected_method_name))
102        call = self.script[self._index]
103        self._index += 1
104        self._print("Next call: {!r}".format(call))
105        if expected_method_name is not None:
106            assert call.method_name == expected_method_name, (
107                     "called method {!r} instead of {!r}".format(expected_method_name,
108                                                                 call.method_name))
109        return call
110
111    def __getattr__(self, attribute_name):
112        call = self._next_call(expected_method_name=attribute_name)
113        def dummy_method(*args, **kwargs):
114            self._print("args: {!r}".format(args))
115            self._print("kwargs: {!r}".format(kwargs))
116            call.check_args(args, kwargs)
117            return call()
118        return dummy_method
119
120    def dir(self, path, callback):
121        """
122        Call the `callback` for each line in the multiline string
123        `call.result`.
124        """
125        call = self._next_call(expected_method_name="dir")
126        for line in call.result.splitlines():
127            callback(line)
128
129
130class MultisessionFactory:
131    """
132    Return a session factory using the scripted data from the given
133    "scripts" for each consecutive call ("creation") of a factory.
134
135    Example:
136
137      host = ftputil.FTPHost(host, user, password,
138                             session_factory=scripted_session.factory(script1, script2))
139
140    When the `session_factory` is "instantiated" for the first time by
141    `FTPHost._make_session`, the factory object will use the behavior
142    described by the script `script1`. When the `session_factory` is
143    "instantiated" a second time, the factory object will use the
144    behavior described by the script `script2`.
145    """
146
147    def __init__(self, *scripts):
148        self._scripts = iter(scripts)
149
150    def __call__(self, host, user, password):
151        """
152        Call the factory.
153
154        This is equivalent to the constructor of the session (e. g.
155        `ftplib.FTP` in a real application).
156        """
157        script = next(self._scripts)
158        return ScriptedSession(script)
159
160
161factory = MultisessionFactory
Note: See TracBrowser for help on using the repository browser.