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