blob: dfdc6309a897e04ed71bac5c7998819a7447a46a [file] [log] [blame]
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02001#!/usr/bin/env python
2
Paul Vinciguerra00671cf2018-11-25 12:47:04 -08003import inspect
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02004import os
5import unittest
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02006from multiprocessing import Process, Pipe
Paul Vinciguerra00671cf2018-11-25 12:47:04 -08007from pickle import dumps
8
Paul Vinciguerra526ad042018-11-27 04:42:05 -08009import six
Paul Vinciguerra6c746172018-11-26 09:57:21 -080010from six import moves
Paul Vinciguerra00671cf2018-11-25 12:47:04 -080011
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020012from framework import VppTestCase
13
14
15class SerializableClassCopy(object):
16 """
17 Empty class used as a basis for a serializable copy of another class.
18 """
19 pass
20
21
22class 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
62class 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):
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800106 return moves.reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200107
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)
Paul Vinciguerraf1f2aa62018-11-25 08:36:47 -0800138 for key, val in six.iteritems(kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200139 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)
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800198 return moves.reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200199 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")
320class 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