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