blob: 19bad897cdf36b44b7de76d7bf8a6e8844a65b17 [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
Paul Vinciguerra090096b2020-12-03 00:42:46 -05005import reprlib
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02006import unittest
Jakub Grajciar053204a2019-03-18 13:17:53 +01007from framework import VppTestCase
Jakub Grajciarb1be2a02018-09-19 13:36:16 +02008from multiprocessing import Process, Pipe
Paul Vinciguerra00671cf2018-11-25 12:47:04 -08009from pickle import dumps
Jakub Grajciar053204a2019-03-18 13:17:53 +010010import sys
snaramred6df3ac2019-08-29 18:00:26 +000011
Paul Vinciguerra27860dd2020-12-03 00:46:03 -050012from enum import IntEnum, IntFlag
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020013
14
Paul Vinciguerrae061dad2020-12-04 14:57:51 -050015class SerializableClassCopy:
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020016 """
17 Empty class used as a basis for a serializable copy of another class.
18 """
19 pass
20
Neale Ranns097fa662018-05-01 05:17:55 -070021 def __repr__(self):
22 return '<SerializableClassCopy dict=%s>' % self.__dict__
23
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020024
Paul Vinciguerrae061dad2020-12-04 14:57:51 -050025class RemoteClassAttr:
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020026 """
27 Wrapper around attribute of a remotely executed class.
28 """
29
30 def __init__(self, remote, attr):
31 self._path = [attr] if attr else []
32 self._remote = remote
33
34 def path_to_str(self):
35 return '.'.join(self._path)
36
37 def get_remote_value(self):
38 return self._remote._remote_exec(RemoteClass.GET, self.path_to_str())
39
40 def __repr__(self):
41 return self._remote._remote_exec(RemoteClass.REPR, self.path_to_str())
42
43 def __str__(self):
44 return self._remote._remote_exec(RemoteClass.STR, self.path_to_str())
45
46 def __getattr__(self, attr):
47 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070048 if not (attr.startswith('__') and attr.endswith('__')):
Neale Ranns097fa662018-05-01 05:17:55 -070049 raise AttributeError('tried to get private attribute: %s ',
50 attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020051 self._path.append(attr)
52 return self
53
54 def __setattr__(self, attr, val):
55 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070056 if not (attr.startswith('__') and attr.endswith('__')):
57 super(RemoteClassAttr, self).__setattr__(attr, val)
58 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020059 self._path.append(attr)
60 self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(),
Jakub Grajciar7db35de2019-06-25 10:22:11 +020061 value=val)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020062
63 def __call__(self, *args, **kwargs):
64 return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
Jakub Grajciar7db35de2019-06-25 10:22:11 +020065 *args, **kwargs)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020066
67
68class RemoteClass(Process):
69 """
70 This class can wrap around and adapt the interface of another class,
71 and then delegate its execution to a newly forked child process.
Dave Wallaced1706812021-08-12 18:36:02 -040072
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020073 Usage:
Dave Wallaced1706812021-08-12 18:36:02 -040074
75 #. Create a remotely executed instance of MyClass. ::
76
77 object = RemoteClass(MyClass, arg1='foo', arg2='bar')
78 object.start_remote()
79
80 #. Access the object normally as if it was an instance of your
81 class. ::
82
83 object.my_attribute = 20
84 print object.my_attribute
85 print object.my_method(object.my_attribute)
86 object.my_attribute.nested_attribute = 'test'
87
88 #. If you need the value of a remote attribute, use .get_remote_value
89 method. This method is automatically called when needed in the
90 context of a remotely executed class. E.g. ::
91
92 if (object.my_attribute.get_remote_value() > 20):
93 object.my_attribute2 = object.my_attribute
94
95 #. Destroy the instance. ::
96
97 object.quit_remote()
98 object.terminate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020099 """
100
101 GET = 0 # Get attribute remotely
102 CALL = 1 # Call method remotely
103 SETATTR = 2 # Set attribute remotely
104 REPR = 3 # Get representation of a remote object
105 STR = 4 # Get string representation of a remote object
106 QUIT = 5 # Quit remote execution
107
108 PIPE_PARENT = 0 # Parent end of the pipe
109 PIPE_CHILD = 1 # Child end of the pipe
110
111 DEFAULT_TIMEOUT = 2 # default timeout for an operation to execute
112
113 def __init__(self, cls, *args, **kwargs):
114 super(RemoteClass, self).__init__()
115 self._cls = cls
116 self._args = args
117 self._kwargs = kwargs
118 self._timeout = RemoteClass.DEFAULT_TIMEOUT
119 self._pipe = Pipe() # pipe for input/output arguments
120
121 def __repr__(self):
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500122 return reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200123
124 def __str__(self):
125 return str(RemoteClassAttr(self, None))
126
127 def __call__(self, *args, **kwargs):
128 return self.RemoteClassAttr(self, None)()
129
130 def __getattr__(self, attr):
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 if hasattr(super(RemoteClass, self), '__getattr__'):
134 return super(RemoteClass, self).__getattr__(attr)
Neale Ranns097fa662018-05-01 05:17:55 -0700135 raise AttributeError('missing: %s', attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200136 return RemoteClassAttr(self, attr)
137
138 def __setattr__(self, attr, val):
139 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700140 if not (attr.startswith('__') and attr.endswith('__')):
141 super(RemoteClass, self).__setattr__(attr, val)
142 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200143 setattr(RemoteClassAttr(self, None), attr, val)
144
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200145 def _remote_exec(self, op, path=None, *args, **kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200146 """
147 Execute given operation on a given, possibly nested, member remotely.
148 """
149 # automatically resolve remote objects in the arguments
150 mutable_args = list(args)
151 for i, val in enumerate(mutable_args):
152 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700153 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200154 mutable_args[i] = val.get_remote_value()
155 args = tuple(mutable_args)
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500156 for key, val in kwargs.items():
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200157 if isinstance(val, RemoteClass) or \
Neale Ranns097fa662018-05-01 05:17:55 -0700158 isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200159 kwargs[key] = val.get_remote_value()
160 # send request
161 args = self._make_serializable(args)
162 kwargs = self._make_serializable(kwargs)
163 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200164 timeout = self._timeout
165 # adjust timeout specifically for the .sleep method
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200166 if path is not None and path.split('.')[-1] == 'sleep':
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200167 if args and isinstance(args[0], (long, int)):
168 timeout += args[0]
169 elif 'timeout' in kwargs:
170 timeout += kwargs['timeout']
171 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
172 return None
173 try:
174 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
175 rv = self._deserialize(rv)
176 return rv
177 except EOFError:
178 return None
179
180 def _get_local_object(self, path):
181 """
182 Follow the path to obtain a reference on the addressed nested attribute
183 """
184 obj = self._instance
185 for attr in path:
186 obj = getattr(obj, attr)
187 return obj
188
189 def _get_local_value(self, path):
190 try:
191 return self._get_local_object(path)
192 except AttributeError:
193 return None
194
195 def _call_local_method(self, path, *args, **kwargs):
196 try:
197 method = self._get_local_object(path)
198 return method(*args, **kwargs)
199 except AttributeError:
200 return None
201
202 def _set_local_attr(self, path, value):
203 try:
204 obj = self._get_local_object(path[:-1])
205 setattr(obj, path[-1], value)
206 except AttributeError:
207 pass
208 return None
209
210 def _get_local_repr(self, path):
211 try:
212 obj = self._get_local_object(path)
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500213 return reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200214 except AttributeError:
215 return None
216
217 def _get_local_str(self, path):
218 try:
219 obj = self._get_local_object(path)
220 return str(obj)
221 except AttributeError:
222 return None
223
224 def _serializable(self, obj):
225 """ Test if the given object is serializable """
226 try:
227 dumps(obj)
228 return True
229 except:
230 return False
231
232 def _make_obj_serializable(self, obj):
233 """
234 Make a serializable copy of an object.
235 Members which are difficult/impossible to serialize are stripped.
236 """
237 if self._serializable(obj):
238 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100239
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200240 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100241
242 """
243 Dictionaries can hold complex values, so we split keys and values into
244 separate lists and serialize them individually.
245 """
246 if (type(obj) is dict):
247 copy.type = type(obj)
248 copy.k_list = list()
249 copy.v_list = list()
250 for k, v in obj.items():
251 copy.k_list.append(self._make_serializable(k))
252 copy.v_list.append(self._make_serializable(v))
253 return copy
254
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200255 # copy at least serializable attributes and properties
256 for name, member in inspect.getmembers(obj):
Neale Ranns097fa662018-05-01 05:17:55 -0700257 # skip private members and non-writable dunder methods.
258 if name[0] == '_':
259 if name in ['__weakref__']:
260 continue
Jakub Grajciar546f9552019-08-21 10:51:21 +0200261 if name in ['__dict__']:
262 continue
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700263 if not (name.startswith('__') and name.endswith('__')):
264 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200265 if callable(member) and not isinstance(member, property):
266 continue
267 if not self._serializable(member):
Jakub Grajciar546f9552019-08-21 10:51:21 +0200268 member = self._make_serializable(member)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200269 setattr(copy, name, member)
270 return copy
271
272 def _make_serializable(self, obj):
273 """
274 Make a serializable copy of an object or a list/tuple of objects.
275 Members which are difficult/impossible to serialize are stripped.
276 """
277 if (type(obj) is list) or (type(obj) is tuple):
278 rv = []
279 for item in obj:
280 rv.append(self._make_serializable(item))
281 if type(obj) is tuple:
282 rv = tuple(rv)
283 return rv
Jakub Grajciar053204a2019-03-18 13:17:53 +0100284 elif (isinstance(obj, IntEnum) or isinstance(obj, IntFlag)):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100285 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200286 else:
287 return self._make_obj_serializable(obj)
288
289 def _deserialize_obj(self, obj):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100290 if (hasattr(obj, 'type')):
291 if obj.type is dict:
292 _obj = dict()
293 for k, v in zip(obj.k_list, obj.v_list):
294 _obj[self._deserialize(k)] = self._deserialize(v)
295 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200296 return obj
297
298 def _deserialize(self, obj):
299 if (type(obj) is list) or (type(obj) is tuple):
300 rv = []
301 for item in obj:
302 rv.append(self._deserialize(item))
303 if type(obj) is tuple:
304 rv = tuple(rv)
305 return rv
306 else:
307 return self._deserialize_obj(obj)
308
309 def start_remote(self):
310 """ Start remote execution """
311 self.start()
312
313 def quit_remote(self):
314 """ Quit remote execution """
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200315 self._remote_exec(RemoteClass.QUIT, None)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200316
317 def get_remote_value(self):
318 """ Get value of a remotely held object """
319 return RemoteClassAttr(self, None).get_remote_value()
320
321 def set_request_timeout(self, timeout):
322 """ Change request timeout """
323 self._timeout = timeout
324
325 def run(self):
326 """
327 Create instance of the wrapped class and execute operations
328 on it as requested by the parent process.
329 """
330 self._instance = self._cls(*self._args, **self._kwargs)
331 while True:
332 try:
333 rv = None
334 # get request from the parent process
335 (op, path, args,
336 kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
337 args = self._deserialize(args)
338 kwargs = self._deserialize(kwargs)
339 path = path.split('.') if path else []
340 if op == RemoteClass.GET:
341 rv = self._get_local_value(path)
342 elif op == RemoteClass.CALL:
343 rv = self._call_local_method(path, *args, **kwargs)
344 elif op == RemoteClass.SETATTR and 'value' in kwargs:
345 self._set_local_attr(path, kwargs['value'])
346 elif op == RemoteClass.REPR:
347 rv = self._get_local_repr(path)
348 elif op == RemoteClass.STR:
349 rv = self._get_local_str(path)
350 elif op == RemoteClass.QUIT:
351 break
352 else:
353 continue
354 # send return value
355 if not self._serializable(rv):
356 rv = self._make_serializable(rv)
357 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
358 except EOFError:
359 break
360 self._instance = None # destroy the instance
361
362
363@unittest.skip("Remote Vpp Test Case Class")
364class RemoteVppTestCase(VppTestCase):
365 """ Re-use VppTestCase to create remote VPP segment
366
Dave Wallaced1706812021-08-12 18:36:02 -0400367 In your test case::
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200368
Dave Wallaced1706812021-08-12 18:36:02 -0400369 @classmethod
370 def setUpClass(cls):
371 # fork new process before client connects to VPP
372 cls.remote_test = RemoteClass(RemoteVppTestCase)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200373
Dave Wallaced1706812021-08-12 18:36:02 -0400374 # start remote process
375 cls.remote_test.start_remote()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200376
Dave Wallaced1706812021-08-12 18:36:02 -0400377 # set up your test case
378 super(MyTestCase, cls).setUpClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200379
Dave Wallaced1706812021-08-12 18:36:02 -0400380 # set up remote test
381 cls.remote_test.setUpClass(cls.tempdir)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200382
Dave Wallaced1706812021-08-12 18:36:02 -0400383 @classmethod
384 def tearDownClass(cls):
385 # tear down remote test
386 cls.remote_test.tearDownClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200387
Dave Wallaced1706812021-08-12 18:36:02 -0400388 # stop remote process
389 cls.remote_test.quit_remote()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200390
Dave Wallaced1706812021-08-12 18:36:02 -0400391 # tear down your test case
392 super(MyTestCase, cls).tearDownClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200393 """
394
395 def __init__(self):
396 super(RemoteVppTestCase, self).__init__("emptyTest")
397
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700398 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
399 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200400 def __del__(self):
401 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700402 self.vpp.poll()
403 if self.vpp.returncode is None:
404 self.vpp.terminate()
405 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200406
407 @classmethod
408 def setUpClass(cls, tempdir):
409 # disable features unsupported in remote VPP
410 orig_env = dict(os.environ)
411 if 'STEP' in os.environ:
412 del os.environ['STEP']
413 if 'DEBUG' in os.environ:
414 del os.environ['DEBUG']
415 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
416 super(RemoteVppTestCase, cls).setUpClass()
417 os.environ = orig_env
418
Paul Vinciguerra7f9b7f92019-03-12 19:23:27 -0700419 @classmethod
420 def tearDownClass(cls):
421 super(RemoteVppTestCase, cls).tearDownClass()
422
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200423 @unittest.skip("Empty test")
424 def emptyTest(self):
425 """ Do nothing """
426 pass
427
428 def setTestFunctionInfo(self, name, doc):
429 """
430 Store the name and documentation string of currently executed test
431 in the main VPP for logging purposes.
432 """
433 self._testMethodName = name
434 self._testMethodDoc = doc