blob: 16595002583d885dbbbd5b74827a5fa4c25cc491 [file] [log] [blame]
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02001#!/usr/bin/env python
2
3import os
4import unittest
5import inspect
6from multiprocessing import Process, Pipe
7from pickle import dumps, PicklingError
8from framework import VppTestCase
9
10
11class SerializableClassCopy(object):
12 """
13 Empty class used as a basis for a serializable copy of another class.
14 """
15 pass
16
17
18class 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
58class 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")
316class 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