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