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