blob: 8b3def2b8c634493bf487ed656d10dc62b90f2c9 [file] [log] [blame]
Renato Botelho do Coutoead1e532019-10-31 13:31:07 -05001#!/usr/bin/env python3
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02002
Paul Vinciguerra00671cf2018-11-25 12:47:04 -08003import inspect
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02004import os
5import unittest
Jakub Grajciar053204a2019-03-18 13:17:53 +01006from framework import VppTestCase
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02007from multiprocessing import Process, Pipe
Paul Vinciguerra00671cf2018-11-25 12:47:04 -08008from pickle import dumps
Paul Vinciguerra526ad042018-11-27 04:42:05 -08009import six
Paul Vinciguerra6c746172018-11-26 09:57:21 -080010from six import moves
Jakub Grajciar053204a2019-03-18 13:17:53 +010011import sys
snaramred6df3ac2019-08-29 18:00:26 +000012
13if sys.version_info < (3,):
14 from aenum import IntEnum, IntFlag
15else:
16 from enum import IntEnum, IntFlag
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020017
18
19class SerializableClassCopy(object):
20 """
21 Empty class used as a basis for a serializable copy of another class.
22 """
23 pass
24
Neale Ranns097fa662018-05-01 05:17:55 -070025 def __repr__(self):
26 return '<SerializableClassCopy dict=%s>' % self.__dict__
27
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020028
29class RemoteClassAttr(object):
30 """
31 Wrapper around attribute of a remotely executed class.
32 """
33
34 def __init__(self, remote, attr):
35 self._path = [attr] if attr else []
36 self._remote = remote
37
38 def path_to_str(self):
39 return '.'.join(self._path)
40
41 def get_remote_value(self):
42 return self._remote._remote_exec(RemoteClass.GET, self.path_to_str())
43
44 def __repr__(self):
45 return self._remote._remote_exec(RemoteClass.REPR, self.path_to_str())
46
47 def __str__(self):
48 return self._remote._remote_exec(RemoteClass.STR, self.path_to_str())
49
50 def __getattr__(self, attr):
51 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070052 if not (attr.startswith('__') and attr.endswith('__')):
Neale Ranns097fa662018-05-01 05:17:55 -070053 raise AttributeError('tried to get private attribute: %s ',
54 attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020055 self._path.append(attr)
56 return self
57
58 def __setattr__(self, attr, val):
59 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070060 if not (attr.startswith('__') and attr.endswith('__')):
61 super(RemoteClassAttr, self).__setattr__(attr, val)
62 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020063 self._path.append(attr)
64 self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(),
Jakub Grajciar7db35de2019-06-25 10:22:11 +020065 value=val)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020066
67 def __call__(self, *args, **kwargs):
68 return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
Jakub Grajciar7db35de2019-06-25 10:22:11 +020069 *args, **kwargs)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020070
71
72class RemoteClass(Process):
73 """
74 This class can wrap around and adapt the interface of another class,
75 and then delegate its execution to a newly forked child process.
76 Usage:
77 # Create a remotely executed instance of MyClass
78 object = RemoteClass(MyClass, arg1='foo', arg2='bar')
79 object.start_remote()
80 # Access the object normally as if it was an instance of your class.
81 object.my_attribute = 20
82 print object.my_attribute
83 print object.my_method(object.my_attribute)
84 object.my_attribute.nested_attribute = 'test'
85 # If you need the value of a remote attribute, use .get_remote_value
86 method. This method is automatically called when needed in the context
87 of a remotely executed class. E.g.:
88 if (object.my_attribute.get_remote_value() > 20):
89 object.my_attribute2 = object.my_attribute
90 # Destroy the instance
91 object.quit_remote()
92 object.terminate()
93 """
94
95 GET = 0 # Get attribute remotely
96 CALL = 1 # Call method remotely
97 SETATTR = 2 # Set attribute remotely
98 REPR = 3 # Get representation of a remote object
99 STR = 4 # Get string representation of a remote object
100 QUIT = 5 # Quit remote execution
101
102 PIPE_PARENT = 0 # Parent end of the pipe
103 PIPE_CHILD = 1 # Child end of the pipe
104
105 DEFAULT_TIMEOUT = 2 # default timeout for an operation to execute
106
107 def __init__(self, cls, *args, **kwargs):
108 super(RemoteClass, self).__init__()
109 self._cls = cls
110 self._args = args
111 self._kwargs = kwargs
112 self._timeout = RemoteClass.DEFAULT_TIMEOUT
113 self._pipe = Pipe() # pipe for input/output arguments
114
115 def __repr__(self):
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800116 return moves.reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200117
118 def __str__(self):
119 return str(RemoteClassAttr(self, None))
120
121 def __call__(self, *args, **kwargs):
122 return self.RemoteClassAttr(self, None)()
123
124 def __getattr__(self, attr):
125 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700126 if not (attr.startswith('__') and attr.endswith('__')):
127 if hasattr(super(RemoteClass, self), '__getattr__'):
128 return super(RemoteClass, self).__getattr__(attr)
Neale Ranns097fa662018-05-01 05:17:55 -0700129 raise AttributeError('missing: %s', attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200130 return RemoteClassAttr(self, attr)
131
132 def __setattr__(self, attr, val):
133 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700134 if not (attr.startswith('__') and attr.endswith('__')):
135 super(RemoteClass, self).__setattr__(attr, val)
136 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200137 setattr(RemoteClassAttr(self, None), attr, val)
138
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200139 def _remote_exec(self, op, path=None, *args, **kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200140 """
141 Execute given operation on a given, possibly nested, member remotely.
142 """
143 # automatically resolve remote objects in the arguments
144 mutable_args = list(args)
145 for i, val in enumerate(mutable_args):
146 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700147 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200148 mutable_args[i] = val.get_remote_value()
149 args = tuple(mutable_args)
Paul Vinciguerraf1f2aa62018-11-25 08:36:47 -0800150 for key, val in six.iteritems(kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200151 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700152 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200153 kwargs[key] = val.get_remote_value()
154 # send request
155 args = self._make_serializable(args)
156 kwargs = self._make_serializable(kwargs)
157 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200158 timeout = self._timeout
159 # adjust timeout specifically for the .sleep method
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200160 if path is not None and path.split('.')[-1] == 'sleep':
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200161 if args and isinstance(args[0], (long, int)):
162 timeout += args[0]
163 elif 'timeout' in kwargs:
164 timeout += kwargs['timeout']
165 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
166 return None
167 try:
168 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
169 rv = self._deserialize(rv)
170 return rv
171 except EOFError:
172 return None
173
174 def _get_local_object(self, path):
175 """
176 Follow the path to obtain a reference on the addressed nested attribute
177 """
178 obj = self._instance
179 for attr in path:
180 obj = getattr(obj, attr)
181 return obj
182
183 def _get_local_value(self, path):
184 try:
185 return self._get_local_object(path)
186 except AttributeError:
187 return None
188
189 def _call_local_method(self, path, *args, **kwargs):
190 try:
191 method = self._get_local_object(path)
192 return method(*args, **kwargs)
193 except AttributeError:
194 return None
195
196 def _set_local_attr(self, path, value):
197 try:
198 obj = self._get_local_object(path[:-1])
199 setattr(obj, path[-1], value)
200 except AttributeError:
201 pass
202 return None
203
204 def _get_local_repr(self, path):
205 try:
206 obj = self._get_local_object(path)
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800207 return moves.reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200208 except AttributeError:
209 return None
210
211 def _get_local_str(self, path):
212 try:
213 obj = self._get_local_object(path)
214 return str(obj)
215 except AttributeError:
216 return None
217
218 def _serializable(self, obj):
219 """ Test if the given object is serializable """
220 try:
221 dumps(obj)
222 return True
223 except:
224 return False
225
226 def _make_obj_serializable(self, obj):
227 """
228 Make a serializable copy of an object.
229 Members which are difficult/impossible to serialize are stripped.
230 """
231 if self._serializable(obj):
232 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100233
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200234 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100235
236 """
237 Dictionaries can hold complex values, so we split keys and values into
238 separate lists and serialize them individually.
239 """
240 if (type(obj) is dict):
241 copy.type = type(obj)
242 copy.k_list = list()
243 copy.v_list = list()
244 for k, v in obj.items():
245 copy.k_list.append(self._make_serializable(k))
246 copy.v_list.append(self._make_serializable(v))
247 return copy
248
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200249 # copy at least serializable attributes and properties
250 for name, member in inspect.getmembers(obj):
Neale Ranns097fa662018-05-01 05:17:55 -0700251 # skip private members and non-writable dunder methods.
252 if name[0] == '_':
253 if name in ['__weakref__']:
254 continue
Jakub Grajciar546f9552019-08-21 10:51:21 +0200255 if name in ['__dict__']:
256 continue
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700257 if not (name.startswith('__') and name.endswith('__')):
258 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200259 if callable(member) and not isinstance(member, property):
260 continue
261 if not self._serializable(member):
Jakub Grajciar546f9552019-08-21 10:51:21 +0200262 member = self._make_serializable(member)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200263 setattr(copy, name, member)
264 return copy
265
266 def _make_serializable(self, obj):
267 """
268 Make a serializable copy of an object or a list/tuple of objects.
269 Members which are difficult/impossible to serialize are stripped.
270 """
271 if (type(obj) is list) or (type(obj) is tuple):
272 rv = []
273 for item in obj:
274 rv.append(self._make_serializable(item))
275 if type(obj) is tuple:
276 rv = tuple(rv)
277 return rv
Jakub Grajciar053204a2019-03-18 13:17:53 +0100278 elif (isinstance(obj, IntEnum) or isinstance(obj, IntFlag)):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100279 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200280 else:
281 return self._make_obj_serializable(obj)
282
283 def _deserialize_obj(self, obj):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100284 if (hasattr(obj, 'type')):
285 if obj.type is dict:
286 _obj = dict()
287 for k, v in zip(obj.k_list, obj.v_list):
288 _obj[self._deserialize(k)] = self._deserialize(v)
289 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200290 return obj
291
292 def _deserialize(self, obj):
293 if (type(obj) is list) or (type(obj) is tuple):
294 rv = []
295 for item in obj:
296 rv.append(self._deserialize(item))
297 if type(obj) is tuple:
298 rv = tuple(rv)
299 return rv
300 else:
301 return self._deserialize_obj(obj)
302
303 def start_remote(self):
304 """ Start remote execution """
305 self.start()
306
307 def quit_remote(self):
308 """ Quit remote execution """
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200309 self._remote_exec(RemoteClass.QUIT, None)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200310
311 def get_remote_value(self):
312 """ Get value of a remotely held object """
313 return RemoteClassAttr(self, None).get_remote_value()
314
315 def set_request_timeout(self, timeout):
316 """ Change request timeout """
317 self._timeout = timeout
318
319 def run(self):
320 """
321 Create instance of the wrapped class and execute operations
322 on it as requested by the parent process.
323 """
324 self._instance = self._cls(*self._args, **self._kwargs)
325 while True:
326 try:
327 rv = None
328 # get request from the parent process
329 (op, path, args,
330 kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
331 args = self._deserialize(args)
332 kwargs = self._deserialize(kwargs)
333 path = path.split('.') if path else []
334 if op == RemoteClass.GET:
335 rv = self._get_local_value(path)
336 elif op == RemoteClass.CALL:
337 rv = self._call_local_method(path, *args, **kwargs)
338 elif op == RemoteClass.SETATTR and 'value' in kwargs:
339 self._set_local_attr(path, kwargs['value'])
340 elif op == RemoteClass.REPR:
341 rv = self._get_local_repr(path)
342 elif op == RemoteClass.STR:
343 rv = self._get_local_str(path)
344 elif op == RemoteClass.QUIT:
345 break
346 else:
347 continue
348 # send return value
349 if not self._serializable(rv):
350 rv = self._make_serializable(rv)
351 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
352 except EOFError:
353 break
354 self._instance = None # destroy the instance
355
356
357@unittest.skip("Remote Vpp Test Case Class")
358class RemoteVppTestCase(VppTestCase):
359 """ Re-use VppTestCase to create remote VPP segment
360
361 In your test case:
362
363 @classmethod
364 def setUpClass(cls):
Paul Vinciguerra8feeaff2019-03-27 11:25:48 -0700365 # fork new process before client connects to VPP
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200366 cls.remote_test = RemoteClass(RemoteVppTestCase)
367
368 # start remote process
369 cls.remote_test.start_remote()
370
371 # set up your test case
372 super(MyTestCase, cls).setUpClass()
373
374 # set up remote test
375 cls.remote_test.setUpClass(cls.tempdir)
376
377 @classmethod
378 def tearDownClass(cls):
379 # tear down remote test
380 cls.remote_test.tearDownClass()
381
382 # stop remote process
383 cls.remote_test.quit_remote()
384
385 # tear down your test case
386 super(MyTestCase, cls).tearDownClass()
387 """
388
389 def __init__(self):
390 super(RemoteVppTestCase, self).__init__("emptyTest")
391
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700392 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
393 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200394 def __del__(self):
395 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700396 self.vpp.poll()
397 if self.vpp.returncode is None:
398 self.vpp.terminate()
399 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200400
401 @classmethod
402 def setUpClass(cls, tempdir):
403 # disable features unsupported in remote VPP
404 orig_env = dict(os.environ)
405 if 'STEP' in os.environ:
406 del os.environ['STEP']
407 if 'DEBUG' in os.environ:
408 del os.environ['DEBUG']
409 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
410 super(RemoteVppTestCase, cls).setUpClass()
411 os.environ = orig_env
412
Paul Vinciguerra7f9b7f92019-03-12 19:23:27 -0700413 @classmethod
414 def tearDownClass(cls):
415 super(RemoteVppTestCase, cls).tearDownClass()
416
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200417 @unittest.skip("Empty test")
418 def emptyTest(self):
419 """ Do nothing """
420 pass
421
422 def setTestFunctionInfo(self, name, doc):
423 """
424 Store the name and documentation string of currently executed test
425 in the main VPP for logging purposes.
426 """
427 self._testMethodName = name
428 self._testMethodDoc = doc