Typos. A bunch of typos I've been collecting.
[vpp.git] / test / remote_test.py
1 #!/usr/bin/env python
2
3 import inspect
4 import os
5 import unittest
6 from multiprocessing import Process, Pipe
7 from pickle import dumps
8
9 import six
10 from six import moves
11
12 from framework import VppTestCase
13 from enum import Enum
14
15
16 class SerializableClassCopy(object):
17     """
18     Empty class used as a basis for a serializable copy of another class.
19     """
20     pass
21
22
23 class RemoteClassAttr(object):
24     """
25     Wrapper around attribute of a remotely executed class.
26     """
27
28     def __init__(self, remote, attr):
29         self._path = [attr] if attr else []
30         self._remote = remote
31
32     def path_to_str(self):
33         return '.'.join(self._path)
34
35     def get_remote_value(self):
36         return self._remote._remote_exec(RemoteClass.GET, self.path_to_str())
37
38     def __repr__(self):
39         return self._remote._remote_exec(RemoteClass.REPR, self.path_to_str())
40
41     def __str__(self):
42         return self._remote._remote_exec(RemoteClass.STR, self.path_to_str())
43
44     def __getattr__(self, attr):
45         if attr[0] == '_':
46             if not (attr.startswith('__') and attr.endswith('__')):
47                 raise AttributeError
48         self._path.append(attr)
49         return self
50
51     def __setattr__(self, attr, val):
52         if attr[0] == '_':
53             if not (attr.startswith('__') and attr.endswith('__')):
54                 super(RemoteClassAttr, self).__setattr__(attr, val)
55                 return
56         self._path.append(attr)
57         self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(),
58                                   True, value=val)
59
60     def __call__(self, *args, **kwargs):
61         return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
62                                          True, *args, **kwargs)
63
64
65 class RemoteClass(Process):
66     """
67     This class can wrap around and adapt the interface of another class,
68     and then delegate its execution to a newly forked child process.
69     Usage:
70         # Create a remotely executed instance of MyClass
71         object = RemoteClass(MyClass, arg1='foo', arg2='bar')
72         object.start_remote()
73         # Access the object normally as if it was an instance of your class.
74         object.my_attribute = 20
75         print object.my_attribute
76         print object.my_method(object.my_attribute)
77         object.my_attribute.nested_attribute = 'test'
78         # If you need the value of a remote attribute, use .get_remote_value
79         method. This method is automatically called when needed in the context
80         of a remotely executed class. E.g.:
81         if (object.my_attribute.get_remote_value() > 20):
82             object.my_attribute2 = object.my_attribute
83         # Destroy the instance
84         object.quit_remote()
85         object.terminate()
86     """
87
88     GET = 0       # Get attribute remotely
89     CALL = 1      # Call method remotely
90     SETATTR = 2   # Set attribute remotely
91     REPR = 3      # Get representation of a remote object
92     STR = 4       # Get string representation of a remote object
93     QUIT = 5      # Quit remote execution
94
95     PIPE_PARENT = 0  # Parent end of the pipe
96     PIPE_CHILD = 1  # Child end of the pipe
97
98     DEFAULT_TIMEOUT = 2  # default timeout for an operation to execute
99
100     def __init__(self, cls, *args, **kwargs):
101         super(RemoteClass, self).__init__()
102         self._cls = cls
103         self._args = args
104         self._kwargs = kwargs
105         self._timeout = RemoteClass.DEFAULT_TIMEOUT
106         self._pipe = Pipe()  # pipe for input/output arguments
107
108     def __repr__(self):
109         return moves.reprlib.repr(RemoteClassAttr(self, None))
110
111     def __str__(self):
112         return str(RemoteClassAttr(self, None))
113
114     def __call__(self, *args, **kwargs):
115         return self.RemoteClassAttr(self, None)()
116
117     def __getattr__(self, attr):
118         if attr[0] == '_' or not self.is_alive():
119             if not (attr.startswith('__') and attr.endswith('__')):
120                 if hasattr(super(RemoteClass, self), '__getattr__'):
121                     return super(RemoteClass, self).__getattr__(attr)
122                 raise AttributeError
123         return RemoteClassAttr(self, attr)
124
125     def __setattr__(self, attr, val):
126         if attr[0] == '_' or not self.is_alive():
127             if not (attr.startswith('__') and attr.endswith('__')):
128                 super(RemoteClass, self).__setattr__(attr, val)
129                 return
130         setattr(RemoteClassAttr(self, None), attr, val)
131
132     def _remote_exec(self, op, path=None, ret=True, *args, **kwargs):
133         """
134         Execute given operation on a given, possibly nested, member remotely.
135         """
136         # automatically resolve remote objects in the arguments
137         mutable_args = list(args)
138         for i, val in enumerate(mutable_args):
139             if isinstance(val, RemoteClass) or \
140                isinstance(val, RemoteClassAttr):
141                 mutable_args[i] = val.get_remote_value()
142         args = tuple(mutable_args)
143         for key, val in six.iteritems(kwargs):
144             if isinstance(val, RemoteClass) or \
145                isinstance(val, RemoteClassAttr):
146                 kwargs[key] = val.get_remote_value()
147         # send request
148         args = self._make_serializable(args)
149         kwargs = self._make_serializable(kwargs)
150         self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
151         if not ret:
152             # no return value expected
153             return None
154         timeout = self._timeout
155         # adjust timeout specifically for the .sleep method
156         if path.split('.')[-1] == 'sleep':
157             if args and isinstance(args[0], (long, int)):
158                 timeout += args[0]
159             elif 'timeout' in kwargs:
160                 timeout += kwargs['timeout']
161         if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
162             return None
163         try:
164             rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
165             rv = self._deserialize(rv)
166             return rv
167         except EOFError:
168             return None
169
170     def _get_local_object(self, path):
171         """
172         Follow the path to obtain a reference on the addressed nested attribute
173         """
174         obj = self._instance
175         for attr in path:
176             obj = getattr(obj, attr)
177         return obj
178
179     def _get_local_value(self, path):
180         try:
181             return self._get_local_object(path)
182         except AttributeError:
183             return None
184
185     def _call_local_method(self, path, *args, **kwargs):
186         try:
187             method = self._get_local_object(path)
188             return method(*args, **kwargs)
189         except AttributeError:
190             return None
191
192     def _set_local_attr(self, path, value):
193         try:
194             obj = self._get_local_object(path[:-1])
195             setattr(obj, path[-1], value)
196         except AttributeError:
197             pass
198         return None
199
200     def _get_local_repr(self, path):
201         try:
202             obj = self._get_local_object(path)
203             return moves.reprlib.repr(obj)
204         except AttributeError:
205             return None
206
207     def _get_local_str(self, path):
208         try:
209             obj = self._get_local_object(path)
210             return str(obj)
211         except AttributeError:
212             return None
213
214     def _serializable(self, obj):
215         """ Test if the given object is serializable """
216         try:
217             dumps(obj)
218             return True
219         except:
220             return False
221
222     def _make_obj_serializable(self, obj):
223         """
224         Make a serializable copy of an object.
225         Members which are difficult/impossible to serialize are stripped.
226         """
227         if self._serializable(obj):
228             return obj  # already serializable
229
230         copy = SerializableClassCopy()
231
232         """
233         Dictionaries can hold complex values, so we split keys and values into
234         separate lists and serialize them individually.
235         """
236         if (type(obj) is dict):
237             copy.type = type(obj)
238             copy.k_list = list()
239             copy.v_list = list()
240             for k, v in obj.items():
241                 copy.k_list.append(self._make_serializable(k))
242                 copy.v_list.append(self._make_serializable(v))
243             return copy
244
245         # copy at least serializable attributes and properties
246         for name, member in inspect.getmembers(obj):
247             if name[0] == '_':  # skip private members
248                 if not (name.startswith('__') and name.endswith('__')):
249                     continue
250             if callable(member) and not isinstance(member, property):
251                 continue
252             if not self._serializable(member):
253                 continue
254             setattr(copy, name, member)
255         return copy
256
257     def _make_serializable(self, obj):
258         """
259         Make a serializable copy of an object or a list/tuple of objects.
260         Members which are difficult/impossible to serialize are stripped.
261         """
262         if (type(obj) is list) or (type(obj) is tuple):
263             rv = []
264             for item in obj:
265                 rv.append(self._make_serializable(item))
266             if type(obj) is tuple:
267                 rv = tuple(rv)
268             return rv
269         elif (isinstance(obj, Enum)):
270             return obj.value
271         else:
272             return self._make_obj_serializable(obj)
273
274     def _deserialize_obj(self, obj):
275         if (hasattr(obj, 'type')):
276             if obj.type is dict:
277                 _obj = dict()
278                 for k, v in zip(obj.k_list, obj.v_list):
279                     _obj[self._deserialize(k)] = self._deserialize(v)
280             return _obj
281         return obj
282
283     def _deserialize(self, obj):
284         if (type(obj) is list) or (type(obj) is tuple):
285             rv = []
286             for item in obj:
287                 rv.append(self._deserialize(item))
288             if type(obj) is tuple:
289                 rv = tuple(rv)
290             return rv
291         else:
292             return self._deserialize_obj(obj)
293
294     def start_remote(self):
295         """ Start remote execution """
296         self.start()
297
298     def quit_remote(self):
299         """ Quit remote execution """
300         self._remote_exec(RemoteClass.QUIT, None, False)
301
302     def get_remote_value(self):
303         """ Get value of a remotely held object """
304         return RemoteClassAttr(self, None).get_remote_value()
305
306     def set_request_timeout(self, timeout):
307         """ Change request timeout """
308         self._timeout = timeout
309
310     def run(self):
311         """
312         Create instance of the wrapped class and execute operations
313         on it as requested by the parent process.
314         """
315         self._instance = self._cls(*self._args, **self._kwargs)
316         while True:
317             try:
318                 rv = None
319                 # get request from the parent process
320                 (op, path, args,
321                  kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
322                 args = self._deserialize(args)
323                 kwargs = self._deserialize(kwargs)
324                 path = path.split('.') if path else []
325                 if op == RemoteClass.GET:
326                     rv = self._get_local_value(path)
327                 elif op == RemoteClass.CALL:
328                     rv = self._call_local_method(path, *args, **kwargs)
329                 elif op == RemoteClass.SETATTR and 'value' in kwargs:
330                     self._set_local_attr(path, kwargs['value'])
331                 elif op == RemoteClass.REPR:
332                     rv = self._get_local_repr(path)
333                 elif op == RemoteClass.STR:
334                     rv = self._get_local_str(path)
335                 elif op == RemoteClass.QUIT:
336                     break
337                 else:
338                     continue
339                 # send return value
340                 if not self._serializable(rv):
341                     rv = self._make_serializable(rv)
342                 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
343             except EOFError:
344                 break
345         self._instance = None  # destroy the instance
346
347
348 @unittest.skip("Remote Vpp Test Case Class")
349 class RemoteVppTestCase(VppTestCase):
350     """ Re-use VppTestCase to create remote VPP segment
351
352         In your test case:
353
354         @classmethod
355         def setUpClass(cls):
356             # fork new process before client connects to VPP
357             cls.remote_test = RemoteClass(RemoteVppTestCase)
358
359             # start remote process
360             cls.remote_test.start_remote()
361
362             # set up your test case
363             super(MyTestCase, cls).setUpClass()
364
365             # set up remote test
366             cls.remote_test.setUpClass(cls.tempdir)
367
368         @classmethod
369         def tearDownClass(cls):
370             # tear down remote test
371             cls.remote_test.tearDownClass()
372
373             # stop remote process
374             cls.remote_test.quit_remote()
375
376             # tear down your test case
377             super(MyTestCase, cls).tearDownClass()
378     """
379
380     def __init__(self):
381         super(RemoteVppTestCase, self).__init__("emptyTest")
382
383     # Note: __del__ is a 'Finalizer" not a 'Destructor'.
384     # https://docs.python.org/3/reference/datamodel.html#object.__del__
385     def __del__(self):
386         if hasattr(self, "vpp"):
387             self.vpp.poll()
388             if self.vpp.returncode is None:
389                 self.vpp.terminate()
390                 self.vpp.communicate()
391
392     @classmethod
393     def setUpClass(cls, tempdir):
394         # disable features unsupported in remote VPP
395         orig_env = dict(os.environ)
396         if 'STEP' in os.environ:
397             del os.environ['STEP']
398         if 'DEBUG' in os.environ:
399             del os.environ['DEBUG']
400         cls.tempdir_prefix = os.path.basename(tempdir) + "/"
401         super(RemoteVppTestCase, cls).setUpClass()
402         os.environ = orig_env
403
404     @unittest.skip("Empty test")
405     def emptyTest(self):
406         """ Do nothing """
407         pass
408
409     def setTestFunctionInfo(self, name, doc):
410         """
411         Store the name and documentation string of currently executed test
412         in the main VPP for logging purposes.
413         """
414         self._testMethodName = name
415         self._testMethodDoc = doc