blob: ea271d58a89790f9c38c8ae734a2438c1e24fa47 [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
Paul Vinciguerra27860dd2020-12-03 00:46:03 -050013from enum import IntEnum, IntFlag
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020014
15
16class SerializableClassCopy(object):
17 """
18 Empty class used as a basis for a serializable copy of another class.
19 """
20 pass
21
Neale Ranns097fa662018-05-01 05:17:55 -070022 def __repr__(self):
23 return '<SerializableClassCopy dict=%s>' % self.__dict__
24
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020025
26class RemoteClassAttr(object):
27 """
28 Wrapper around attribute of a remotely executed class.
29 """
30
31 def __init__(self, remote, attr):
32 self._path = [attr] if attr else []
33 self._remote = remote
34
35 def path_to_str(self):
36 return '.'.join(self._path)
37
38 def get_remote_value(self):
39 return self._remote._remote_exec(RemoteClass.GET, self.path_to_str())
40
41 def __repr__(self):
42 return self._remote._remote_exec(RemoteClass.REPR, self.path_to_str())
43
44 def __str__(self):
45 return self._remote._remote_exec(RemoteClass.STR, self.path_to_str())
46
47 def __getattr__(self, attr):
48 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070049 if not (attr.startswith('__') and attr.endswith('__')):
Neale Ranns097fa662018-05-01 05:17:55 -070050 raise AttributeError('tried to get private attribute: %s ',
51 attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020052 self._path.append(attr)
53 return self
54
55 def __setattr__(self, attr, val):
56 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070057 if not (attr.startswith('__') and attr.endswith('__')):
58 super(RemoteClassAttr, self).__setattr__(attr, val)
59 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020060 self._path.append(attr)
61 self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(),
Jakub Grajciar7db35de2019-06-25 10:22:11 +020062 value=val)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020063
64 def __call__(self, *args, **kwargs):
65 return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
Jakub Grajciar7db35de2019-06-25 10:22:11 +020066 *args, **kwargs)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020067
68
69class RemoteClass(Process):
70 """
71 This class can wrap around and adapt the interface of another class,
72 and then delegate its execution to a newly forked child process.
73 Usage:
74 # Create a remotely executed instance of MyClass
75 object = RemoteClass(MyClass, arg1='foo', arg2='bar')
76 object.start_remote()
77 # Access the object normally as if it was an instance of your class.
78 object.my_attribute = 20
79 print object.my_attribute
80 print object.my_method(object.my_attribute)
81 object.my_attribute.nested_attribute = 'test'
82 # If you need the value of a remote attribute, use .get_remote_value
83 method. This method is automatically called when needed in the context
84 of a remotely executed class. E.g.:
85 if (object.my_attribute.get_remote_value() > 20):
86 object.my_attribute2 = object.my_attribute
87 # Destroy the instance
88 object.quit_remote()
89 object.terminate()
90 """
91
92 GET = 0 # Get attribute remotely
93 CALL = 1 # Call method remotely
94 SETATTR = 2 # Set attribute remotely
95 REPR = 3 # Get representation of a remote object
96 STR = 4 # Get string representation of a remote object
97 QUIT = 5 # Quit remote execution
98
99 PIPE_PARENT = 0 # Parent end of the pipe
100 PIPE_CHILD = 1 # Child end of the pipe
101
102 DEFAULT_TIMEOUT = 2 # default timeout for an operation to execute
103
104 def __init__(self, cls, *args, **kwargs):
105 super(RemoteClass, self).__init__()
106 self._cls = cls
107 self._args = args
108 self._kwargs = kwargs
109 self._timeout = RemoteClass.DEFAULT_TIMEOUT
110 self._pipe = Pipe() # pipe for input/output arguments
111
112 def __repr__(self):
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800113 return moves.reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200114
115 def __str__(self):
116 return str(RemoteClassAttr(self, None))
117
118 def __call__(self, *args, **kwargs):
119 return self.RemoteClassAttr(self, None)()
120
121 def __getattr__(self, attr):
122 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700123 if not (attr.startswith('__') and attr.endswith('__')):
124 if hasattr(super(RemoteClass, self), '__getattr__'):
125 return super(RemoteClass, self).__getattr__(attr)
Neale Ranns097fa662018-05-01 05:17:55 -0700126 raise AttributeError('missing: %s', attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200127 return RemoteClassAttr(self, attr)
128
129 def __setattr__(self, attr, val):
130 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700131 if not (attr.startswith('__') and attr.endswith('__')):
132 super(RemoteClass, self).__setattr__(attr, val)
133 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200134 setattr(RemoteClassAttr(self, None), attr, val)
135
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200136 def _remote_exec(self, op, path=None, *args, **kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200137 """
138 Execute given operation on a given, possibly nested, member remotely.
139 """
140 # automatically resolve remote objects in the arguments
141 mutable_args = list(args)
142 for i, val in enumerate(mutable_args):
143 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700144 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200145 mutable_args[i] = val.get_remote_value()
146 args = tuple(mutable_args)
Paul Vinciguerraf1f2aa62018-11-25 08:36:47 -0800147 for key, val in six.iteritems(kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200148 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700149 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200150 kwargs[key] = val.get_remote_value()
151 # send request
152 args = self._make_serializable(args)
153 kwargs = self._make_serializable(kwargs)
154 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200155 timeout = self._timeout
156 # adjust timeout specifically for the .sleep method
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200157 if path is not None and path.split('.')[-1] == 'sleep':
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200158 if args and isinstance(args[0], (long, int)):
159 timeout += args[0]
160 elif 'timeout' in kwargs:
161 timeout += kwargs['timeout']
162 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
163 return None
164 try:
165 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
166 rv = self._deserialize(rv)
167 return rv
168 except EOFError:
169 return None
170
171 def _get_local_object(self, path):
172 """
173 Follow the path to obtain a reference on the addressed nested attribute
174 """
175 obj = self._instance
176 for attr in path:
177 obj = getattr(obj, attr)
178 return obj
179
180 def _get_local_value(self, path):
181 try:
182 return self._get_local_object(path)
183 except AttributeError:
184 return None
185
186 def _call_local_method(self, path, *args, **kwargs):
187 try:
188 method = self._get_local_object(path)
189 return method(*args, **kwargs)
190 except AttributeError:
191 return None
192
193 def _set_local_attr(self, path, value):
194 try:
195 obj = self._get_local_object(path[:-1])
196 setattr(obj, path[-1], value)
197 except AttributeError:
198 pass
199 return None
200
201 def _get_local_repr(self, path):
202 try:
203 obj = self._get_local_object(path)
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800204 return moves.reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200205 except AttributeError:
206 return None
207
208 def _get_local_str(self, path):
209 try:
210 obj = self._get_local_object(path)
211 return str(obj)
212 except AttributeError:
213 return None
214
215 def _serializable(self, obj):
216 """ Test if the given object is serializable """
217 try:
218 dumps(obj)
219 return True
220 except:
221 return False
222
223 def _make_obj_serializable(self, obj):
224 """
225 Make a serializable copy of an object.
226 Members which are difficult/impossible to serialize are stripped.
227 """
228 if self._serializable(obj):
229 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100230
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200231 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100232
233 """
234 Dictionaries can hold complex values, so we split keys and values into
235 separate lists and serialize them individually.
236 """
237 if (type(obj) is dict):
238 copy.type = type(obj)
239 copy.k_list = list()
240 copy.v_list = list()
241 for k, v in obj.items():
242 copy.k_list.append(self._make_serializable(k))
243 copy.v_list.append(self._make_serializable(v))
244 return copy
245
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200246 # copy at least serializable attributes and properties
247 for name, member in inspect.getmembers(obj):
Neale Ranns097fa662018-05-01 05:17:55 -0700248 # skip private members and non-writable dunder methods.
249 if name[0] == '_':
250 if name in ['__weakref__']:
251 continue
Jakub Grajciar546f9552019-08-21 10:51:21 +0200252 if name in ['__dict__']:
253 continue
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700254 if not (name.startswith('__') and name.endswith('__')):
255 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200256 if callable(member) and not isinstance(member, property):
257 continue
258 if not self._serializable(member):
Jakub Grajciar546f9552019-08-21 10:51:21 +0200259 member = self._make_serializable(member)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200260 setattr(copy, name, member)
261 return copy
262
263 def _make_serializable(self, obj):
264 """
265 Make a serializable copy of an object or a list/tuple of objects.
266 Members which are difficult/impossible to serialize are stripped.
267 """
268 if (type(obj) is list) or (type(obj) is tuple):
269 rv = []
270 for item in obj:
271 rv.append(self._make_serializable(item))
272 if type(obj) is tuple:
273 rv = tuple(rv)
274 return rv
Jakub Grajciar053204a2019-03-18 13:17:53 +0100275 elif (isinstance(obj, IntEnum) or isinstance(obj, IntFlag)):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100276 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200277 else:
278 return self._make_obj_serializable(obj)
279
280 def _deserialize_obj(self, obj):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100281 if (hasattr(obj, 'type')):
282 if obj.type is dict:
283 _obj = dict()
284 for k, v in zip(obj.k_list, obj.v_list):
285 _obj[self._deserialize(k)] = self._deserialize(v)
286 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200287 return obj
288
289 def _deserialize(self, obj):
290 if (type(obj) is list) or (type(obj) is tuple):
291 rv = []
292 for item in obj:
293 rv.append(self._deserialize(item))
294 if type(obj) is tuple:
295 rv = tuple(rv)
296 return rv
297 else:
298 return self._deserialize_obj(obj)
299
300 def start_remote(self):
301 """ Start remote execution """
302 self.start()
303
304 def quit_remote(self):
305 """ Quit remote execution """
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200306 self._remote_exec(RemoteClass.QUIT, None)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200307
308 def get_remote_value(self):
309 """ Get value of a remotely held object """
310 return RemoteClassAttr(self, None).get_remote_value()
311
312 def set_request_timeout(self, timeout):
313 """ Change request timeout """
314 self._timeout = timeout
315
316 def run(self):
317 """
318 Create instance of the wrapped class and execute operations
319 on it as requested by the parent process.
320 """
321 self._instance = self._cls(*self._args, **self._kwargs)
322 while True:
323 try:
324 rv = None
325 # get request from the parent process
326 (op, path, args,
327 kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
328 args = self._deserialize(args)
329 kwargs = self._deserialize(kwargs)
330 path = path.split('.') if path else []
331 if op == RemoteClass.GET:
332 rv = self._get_local_value(path)
333 elif op == RemoteClass.CALL:
334 rv = self._call_local_method(path, *args, **kwargs)
335 elif op == RemoteClass.SETATTR and 'value' in kwargs:
336 self._set_local_attr(path, kwargs['value'])
337 elif op == RemoteClass.REPR:
338 rv = self._get_local_repr(path)
339 elif op == RemoteClass.STR:
340 rv = self._get_local_str(path)
341 elif op == RemoteClass.QUIT:
342 break
343 else:
344 continue
345 # send return value
346 if not self._serializable(rv):
347 rv = self._make_serializable(rv)
348 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
349 except EOFError:
350 break
351 self._instance = None # destroy the instance
352
353
354@unittest.skip("Remote Vpp Test Case Class")
355class RemoteVppTestCase(VppTestCase):
356 """ Re-use VppTestCase to create remote VPP segment
357
358 In your test case:
359
360 @classmethod
361 def setUpClass(cls):
Paul Vinciguerra8feeaff2019-03-27 11:25:48 -0700362 # fork new process before client connects to VPP
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200363 cls.remote_test = RemoteClass(RemoteVppTestCase)
364
365 # start remote process
366 cls.remote_test.start_remote()
367
368 # set up your test case
369 super(MyTestCase, cls).setUpClass()
370
371 # set up remote test
372 cls.remote_test.setUpClass(cls.tempdir)
373
374 @classmethod
375 def tearDownClass(cls):
376 # tear down remote test
377 cls.remote_test.tearDownClass()
378
379 # stop remote process
380 cls.remote_test.quit_remote()
381
382 # tear down your test case
383 super(MyTestCase, cls).tearDownClass()
384 """
385
386 def __init__(self):
387 super(RemoteVppTestCase, self).__init__("emptyTest")
388
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700389 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
390 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200391 def __del__(self):
392 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700393 self.vpp.poll()
394 if self.vpp.returncode is None:
395 self.vpp.terminate()
396 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200397
398 @classmethod
399 def setUpClass(cls, tempdir):
400 # disable features unsupported in remote VPP
401 orig_env = dict(os.environ)
402 if 'STEP' in os.environ:
403 del os.environ['STEP']
404 if 'DEBUG' in os.environ:
405 del os.environ['DEBUG']
406 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
407 super(RemoteVppTestCase, cls).setUpClass()
408 os.environ = orig_env
409
Paul Vinciguerra7f9b7f92019-03-12 19:23:27 -0700410 @classmethod
411 def tearDownClass(cls):
412 super(RemoteVppTestCase, cls).tearDownClass()
413
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200414 @unittest.skip("Empty test")
415 def emptyTest(self):
416 """ Do nothing """
417 pass
418
419 def setTestFunctionInfo(self, name, doc):
420 """
421 Store the name and documentation string of currently executed test
422 in the main VPP for logging purposes.
423 """
424 self._testMethodName = name
425 self._testMethodDoc = doc