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