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