blob: 092d3f8d2e7b33b058db2af602bce01a69d2c47e [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
Ole Trøan3b0d7e42019-03-15 16:14:41 +00008
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
Ole Trøan3b0d7e42019-03-15 16:14:41 +000012from framework import VppTestCase
Neale Ranns097fa662018-05-01 05:17:55 -070013from aenum import Enum
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(),
62 True, value=val)
63
64 def __call__(self, *args, **kwargs):
Neale Ranns097fa662018-05-01 05:17:55 -070065 ret = True if 'vapi' in self.path_to_str() else False
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020066 return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
Neale Ranns097fa662018-05-01 05:17:55 -070067 ret, *args, **kwargs)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020068
69
70class RemoteClass(Process):
71 """
72 This class can wrap around and adapt the interface of another class,
73 and then delegate its execution to a newly forked child process.
74 Usage:
75 # Create a remotely executed instance of MyClass
76 object = RemoteClass(MyClass, arg1='foo', arg2='bar')
77 object.start_remote()
78 # Access the object normally as if it was an instance of your class.
79 object.my_attribute = 20
80 print object.my_attribute
81 print object.my_method(object.my_attribute)
82 object.my_attribute.nested_attribute = 'test'
83 # If you need the value of a remote attribute, use .get_remote_value
84 method. This method is automatically called when needed in the context
85 of a remotely executed class. E.g.:
86 if (object.my_attribute.get_remote_value() > 20):
87 object.my_attribute2 = object.my_attribute
88 # Destroy the instance
89 object.quit_remote()
90 object.terminate()
91 """
92
93 GET = 0 # Get attribute remotely
94 CALL = 1 # Call method remotely
95 SETATTR = 2 # Set attribute remotely
96 REPR = 3 # Get representation of a remote object
97 STR = 4 # Get string representation of a remote object
98 QUIT = 5 # Quit remote execution
99
100 PIPE_PARENT = 0 # Parent end of the pipe
101 PIPE_CHILD = 1 # Child end of the pipe
102
103 DEFAULT_TIMEOUT = 2 # default timeout for an operation to execute
104
105 def __init__(self, cls, *args, **kwargs):
106 super(RemoteClass, self).__init__()
107 self._cls = cls
108 self._args = args
109 self._kwargs = kwargs
110 self._timeout = RemoteClass.DEFAULT_TIMEOUT
111 self._pipe = Pipe() # pipe for input/output arguments
112
113 def __repr__(self):
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800114 return moves.reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200115
116 def __str__(self):
117 return str(RemoteClassAttr(self, None))
118
119 def __call__(self, *args, **kwargs):
120 return self.RemoteClassAttr(self, None)()
121
122 def __getattr__(self, attr):
123 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700124 if not (attr.startswith('__') and attr.endswith('__')):
125 if hasattr(super(RemoteClass, self), '__getattr__'):
126 return super(RemoteClass, self).__getattr__(attr)
Neale Ranns097fa662018-05-01 05:17:55 -0700127 raise AttributeError('missing: %s', attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200128 return RemoteClassAttr(self, attr)
129
130 def __setattr__(self, attr, val):
131 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700132 if not (attr.startswith('__') and attr.endswith('__')):
133 super(RemoteClass, self).__setattr__(attr, val)
134 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200135 setattr(RemoteClassAttr(self, None), attr, val)
136
137 def _remote_exec(self, op, path=None, ret=True, *args, **kwargs):
138 """
139 Execute given operation on a given, possibly nested, member remotely.
140 """
141 # automatically resolve remote objects in the arguments
142 mutable_args = list(args)
143 for i, val in enumerate(mutable_args):
144 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700145 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200146 mutable_args[i] = val.get_remote_value()
147 args = tuple(mutable_args)
Paul Vinciguerraf1f2aa62018-11-25 08:36:47 -0800148 for key, val in six.iteritems(kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200149 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700150 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200151 kwargs[key] = val.get_remote_value()
152 # send request
153 args = self._make_serializable(args)
154 kwargs = self._make_serializable(kwargs)
155 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
156 if not ret:
157 # no return value expected
158 return None
159 timeout = self._timeout
160 # adjust timeout specifically for the .sleep method
161 if path.split('.')[-1] == 'sleep':
162 if args and isinstance(args[0], (long, int)):
163 timeout += args[0]
164 elif 'timeout' in kwargs:
165 timeout += kwargs['timeout']
166 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
167 return None
168 try:
169 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
170 rv = self._deserialize(rv)
171 return rv
172 except EOFError:
173 return None
174
175 def _get_local_object(self, path):
176 """
177 Follow the path to obtain a reference on the addressed nested attribute
178 """
179 obj = self._instance
180 for attr in path:
181 obj = getattr(obj, attr)
182 return obj
183
184 def _get_local_value(self, path):
185 try:
186 return self._get_local_object(path)
187 except AttributeError:
188 return None
189
190 def _call_local_method(self, path, *args, **kwargs):
191 try:
192 method = self._get_local_object(path)
193 return method(*args, **kwargs)
194 except AttributeError:
195 return None
196
197 def _set_local_attr(self, path, value):
198 try:
199 obj = self._get_local_object(path[:-1])
200 setattr(obj, path[-1], value)
201 except AttributeError:
202 pass
203 return None
204
205 def _get_local_repr(self, path):
206 try:
207 obj = self._get_local_object(path)
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800208 return moves.reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200209 except AttributeError:
210 return None
211
212 def _get_local_str(self, path):
213 try:
214 obj = self._get_local_object(path)
215 return str(obj)
216 except AttributeError:
217 return None
218
219 def _serializable(self, obj):
220 """ Test if the given object is serializable """
221 try:
222 dumps(obj)
223 return True
224 except:
225 return False
226
227 def _make_obj_serializable(self, obj):
228 """
229 Make a serializable copy of an object.
230 Members which are difficult/impossible to serialize are stripped.
231 """
232 if self._serializable(obj):
233 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100234
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200235 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100236
237 """
238 Dictionaries can hold complex values, so we split keys and values into
239 separate lists and serialize them individually.
240 """
241 if (type(obj) is dict):
242 copy.type = type(obj)
243 copy.k_list = list()
244 copy.v_list = list()
245 for k, v in obj.items():
246 copy.k_list.append(self._make_serializable(k))
247 copy.v_list.append(self._make_serializable(v))
248 return copy
249
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200250 # copy at least serializable attributes and properties
251 for name, member in inspect.getmembers(obj):
Neale Ranns097fa662018-05-01 05:17:55 -0700252 # skip private members and non-writable dunder methods.
253 if name[0] == '_':
254 if name in ['__weakref__']:
255 continue
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700256 if not (name.startswith('__') and name.endswith('__')):
257 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200258 if callable(member) and not isinstance(member, property):
259 continue
260 if not self._serializable(member):
261 continue
262 setattr(copy, name, member)
263 return copy
264
265 def _make_serializable(self, obj):
266 """
267 Make a serializable copy of an object or a list/tuple of objects.
268 Members which are difficult/impossible to serialize are stripped.
269 """
270 if (type(obj) is list) or (type(obj) is tuple):
271 rv = []
272 for item in obj:
273 rv.append(self._make_serializable(item))
274 if type(obj) is tuple:
275 rv = tuple(rv)
276 return rv
Ole Trøan3b0d7e42019-03-15 16:14:41 +0000277 elif (isinstance(obj, Enum)):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100278 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200279 else:
280 return self._make_obj_serializable(obj)
281
282 def _deserialize_obj(self, obj):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100283 if (hasattr(obj, 'type')):
284 if obj.type is dict:
285 _obj = dict()
286 for k, v in zip(obj.k_list, obj.v_list):
287 _obj[self._deserialize(k)] = self._deserialize(v)
288 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200289 return obj
290
291 def _deserialize(self, obj):
292 if (type(obj) is list) or (type(obj) is tuple):
293 rv = []
294 for item in obj:
295 rv.append(self._deserialize(item))
296 if type(obj) is tuple:
297 rv = tuple(rv)
298 return rv
299 else:
300 return self._deserialize_obj(obj)
301
302 def start_remote(self):
303 """ Start remote execution """
304 self.start()
305
306 def quit_remote(self):
307 """ Quit remote execution """
308 self._remote_exec(RemoteClass.QUIT, None, False)
309
310 def get_remote_value(self):
311 """ Get value of a remotely held object """
312 return RemoteClassAttr(self, None).get_remote_value()
313
314 def set_request_timeout(self, timeout):
315 """ Change request timeout """
316 self._timeout = timeout
317
318 def run(self):
319 """
320 Create instance of the wrapped class and execute operations
321 on it as requested by the parent process.
322 """
323 self._instance = self._cls(*self._args, **self._kwargs)
324 while True:
325 try:
326 rv = None
327 # get request from the parent process
328 (op, path, args,
329 kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
330 args = self._deserialize(args)
331 kwargs = self._deserialize(kwargs)
332 path = path.split('.') if path else []
333 if op == RemoteClass.GET:
334 rv = self._get_local_value(path)
335 elif op == RemoteClass.CALL:
336 rv = self._call_local_method(path, *args, **kwargs)
337 elif op == RemoteClass.SETATTR and 'value' in kwargs:
338 self._set_local_attr(path, kwargs['value'])
339 elif op == RemoteClass.REPR:
340 rv = self._get_local_repr(path)
341 elif op == RemoteClass.STR:
342 rv = self._get_local_str(path)
343 elif op == RemoteClass.QUIT:
344 break
345 else:
346 continue
347 # send return value
348 if not self._serializable(rv):
349 rv = self._make_serializable(rv)
350 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
351 except EOFError:
352 break
353 self._instance = None # destroy the instance
354
355
356@unittest.skip("Remote Vpp Test Case Class")
357class RemoteVppTestCase(VppTestCase):
358 """ Re-use VppTestCase to create remote VPP segment
359
360 In your test case:
361
362 @classmethod
363 def setUpClass(cls):
Paul Vinciguerra8feeaff2019-03-27 11:25:48 -0700364 # fork new process before client connects to VPP
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200365 cls.remote_test = RemoteClass(RemoteVppTestCase)
366
367 # start remote process
368 cls.remote_test.start_remote()
369
370 # set up your test case
371 super(MyTestCase, cls).setUpClass()
372
373 # set up remote test
374 cls.remote_test.setUpClass(cls.tempdir)
375
376 @classmethod
377 def tearDownClass(cls):
378 # tear down remote test
379 cls.remote_test.tearDownClass()
380
381 # stop remote process
382 cls.remote_test.quit_remote()
383
384 # tear down your test case
385 super(MyTestCase, cls).tearDownClass()
386 """
387
388 def __init__(self):
389 super(RemoteVppTestCase, self).__init__("emptyTest")
390
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700391 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
392 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200393 def __del__(self):
394 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700395 self.vpp.poll()
396 if self.vpp.returncode is None:
397 self.vpp.terminate()
398 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200399
400 @classmethod
401 def setUpClass(cls, tempdir):
402 # disable features unsupported in remote VPP
403 orig_env = dict(os.environ)
404 if 'STEP' in os.environ:
405 del os.environ['STEP']
406 if 'DEBUG' in os.environ:
407 del os.environ['DEBUG']
408 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
409 super(RemoteVppTestCase, cls).setUpClass()
410 os.environ = orig_env
411
Paul Vinciguerra7f9b7f92019-03-12 19:23:27 -0700412 @classmethod
413 def tearDownClass(cls):
414 super(RemoteVppTestCase, cls).tearDownClass()
415
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200416 @unittest.skip("Empty test")
417 def emptyTest(self):
418 """ Do nothing """
419 pass
420
421 def setTestFunctionInfo(self, name, doc):
422 """
423 Store the name and documentation string of currently executed test
424 in the main VPP for logging purposes.
425 """
426 self._testMethodName = name
427 self._testMethodDoc = doc