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