blob: 89eca8c62dd7fb6d646ce597b9b37acad3ee8e32 [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
Dave Wallace8800f732023-08-31 00:47:44 -04007from 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
snaramred6df3ac2019-08-29 18:00:26 +000010
Paul Vinciguerra27860dd2020-12-03 00:46:03 -050011from enum import IntEnum, IntFlag
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020012
13
Paul Vinciguerrae061dad2020-12-04 14:57:51 -050014class SerializableClassCopy:
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020015 """
16 Empty class used as a basis for a serializable copy of another class.
17 """
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020018
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020019 pass
20
Neale Ranns097fa662018-05-01 05:17:55 -070021 def __repr__(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020022 return "<SerializableClassCopy dict=%s>" % self.__dict__
Neale Ranns097fa662018-05-01 05:17:55 -070023
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):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020035 return ".".join(self._path)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020036
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):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020047 if attr[0] == "_":
48 if not (attr.startswith("__") and attr.endswith("__")):
49 raise AttributeError("tried to get private attribute: %s ", attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020050 self._path.append(attr)
51 return self
52
53 def __setattr__(self, attr, val):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020054 if attr[0] == "_":
55 if not (attr.startswith("__") and attr.endswith("__")):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070056 super(RemoteClassAttr, self).__setattr__(attr, val)
57 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020058 self._path.append(attr)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020059 self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(), value=val)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020060
61 def __call__(self, *args, **kwargs):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020062 return self._remote._remote_exec(
63 RemoteClass.CALL, self.path_to_str(), *args, **kwargs
64 )
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020065
66
67class RemoteClass(Process):
68 """
69 This class can wrap around and adapt the interface of another class,
70 and then delegate its execution to a newly forked child process.
Dave Wallaced1706812021-08-12 18:36:02 -040071
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020072 Usage:
Dave Wallaced1706812021-08-12 18:36:02 -040073
74 #. Create a remotely executed instance of MyClass. ::
75
76 object = RemoteClass(MyClass, arg1='foo', arg2='bar')
77 object.start_remote()
78
79 #. Access the object normally as if it was an instance of your
80 class. ::
81
82 object.my_attribute = 20
83 print object.my_attribute
84 print object.my_method(object.my_attribute)
85 object.my_attribute.nested_attribute = 'test'
86
87 #. If you need the value of a remote attribute, use .get_remote_value
88 method. This method is automatically called when needed in the
89 context of a remotely executed class. E.g. ::
90
91 if (object.my_attribute.get_remote_value() > 20):
92 object.my_attribute2 = object.my_attribute
93
94 #. Destroy the instance. ::
95
96 object.quit_remote()
97 object.terminate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020098 """
99
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200100 GET = 0 # Get attribute remotely
101 CALL = 1 # Call method remotely
102 SETATTR = 2 # Set attribute remotely
103 REPR = 3 # Get representation of a remote object
104 STR = 4 # Get string representation of a remote object
105 QUIT = 5 # Quit remote execution
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200106
107 PIPE_PARENT = 0 # Parent end of the pipe
108 PIPE_CHILD = 1 # Child end of the pipe
109
110 DEFAULT_TIMEOUT = 2 # default timeout for an operation to execute
111
112 def __init__(self, cls, *args, **kwargs):
113 super(RemoteClass, self).__init__()
114 self._cls = cls
115 self._args = args
116 self._kwargs = kwargs
117 self._timeout = RemoteClass.DEFAULT_TIMEOUT
118 self._pipe = Pipe() # pipe for input/output arguments
119
120 def __repr__(self):
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500121 return reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200122
123 def __str__(self):
124 return str(RemoteClassAttr(self, None))
125
126 def __call__(self, *args, **kwargs):
127 return self.RemoteClassAttr(self, None)()
128
129 def __getattr__(self, attr):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200130 if attr[0] == "_" or not self.is_alive():
131 if not (attr.startswith("__") and attr.endswith("__")):
132 if hasattr(super(RemoteClass, self), "__getattr__"):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700133 return super(RemoteClass, self).__getattr__(attr)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200134 raise AttributeError("missing: %s", attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200135 return RemoteClassAttr(self, attr)
136
137 def __setattr__(self, attr, val):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200138 if attr[0] == "_" or not self.is_alive():
139 if not (attr.startswith("__") and attr.endswith("__")):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700140 super(RemoteClass, self).__setattr__(attr, val)
141 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200142 setattr(RemoteClassAttr(self, None), attr, val)
143
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200144 def _remote_exec(self, op, path=None, *args, **kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200145 """
146 Execute given operation on a given, possibly nested, member remotely.
147 """
148 # automatically resolve remote objects in the arguments
149 mutable_args = list(args)
150 for i, val in enumerate(mutable_args):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200151 if isinstance(val, RemoteClass) or isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200152 mutable_args[i] = val.get_remote_value()
153 args = tuple(mutable_args)
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500154 for key, val in kwargs.items():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200155 if isinstance(val, RemoteClass) or isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200156 kwargs[key] = val.get_remote_value()
157 # send request
158 args = self._make_serializable(args)
159 kwargs = self._make_serializable(kwargs)
160 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200161 timeout = self._timeout
162 # adjust timeout specifically for the .sleep method
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200163 if path is not None and path.split(".")[-1] == "sleep":
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200164 if args and isinstance(args[0], (long, int)):
165 timeout += args[0]
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200166 elif "timeout" in kwargs:
167 timeout += kwargs["timeout"]
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200168 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
169 return None
170 try:
171 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
172 rv = self._deserialize(rv)
173 return rv
174 except EOFError:
175 return None
176
177 def _get_local_object(self, path):
178 """
179 Follow the path to obtain a reference on the addressed nested attribute
180 """
181 obj = self._instance
182 for attr in path:
183 obj = getattr(obj, attr)
184 return obj
185
186 def _get_local_value(self, path):
187 try:
188 return self._get_local_object(path)
189 except AttributeError:
190 return None
191
192 def _call_local_method(self, path, *args, **kwargs):
193 try:
194 method = self._get_local_object(path)
195 return method(*args, **kwargs)
196 except AttributeError:
197 return None
198
199 def _set_local_attr(self, path, value):
200 try:
201 obj = self._get_local_object(path[:-1])
202 setattr(obj, path[-1], value)
203 except AttributeError:
204 pass
205 return None
206
207 def _get_local_repr(self, path):
208 try:
209 obj = self._get_local_object(path)
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500210 return reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200211 except AttributeError:
212 return None
213
214 def _get_local_str(self, path):
215 try:
216 obj = self._get_local_object(path)
217 return str(obj)
218 except AttributeError:
219 return None
220
221 def _serializable(self, obj):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200222 """Test if the given object is serializable"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200223 try:
224 dumps(obj)
225 return True
226 except:
227 return False
228
229 def _make_obj_serializable(self, obj):
230 """
231 Make a serializable copy of an object.
232 Members which are difficult/impossible to serialize are stripped.
233 """
234 if self._serializable(obj):
235 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100236
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200237 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100238
239 """
240 Dictionaries can hold complex values, so we split keys and values into
241 separate lists and serialize them individually.
242 """
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200243 if type(obj) is dict:
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100244 copy.type = type(obj)
245 copy.k_list = list()
246 copy.v_list = list()
247 for k, v in obj.items():
248 copy.k_list.append(self._make_serializable(k))
249 copy.v_list.append(self._make_serializable(v))
250 return copy
251
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200252 # copy at least serializable attributes and properties
253 for name, member in inspect.getmembers(obj):
Neale Ranns097fa662018-05-01 05:17:55 -0700254 # skip private members and non-writable dunder methods.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200255 if name[0] == "_":
256 if name in ["__weakref__"]:
Neale Ranns097fa662018-05-01 05:17:55 -0700257 continue
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200258 if name in ["__dict__"]:
Jakub Grajciar546f9552019-08-21 10:51:21 +0200259 continue
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200260 if not (name.startswith("__") and name.endswith("__")):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700261 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200262 if callable(member) and not isinstance(member, property):
263 continue
264 if not self._serializable(member):
Jakub Grajciar546f9552019-08-21 10:51:21 +0200265 member = self._make_serializable(member)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200266 setattr(copy, name, member)
267 return copy
268
269 def _make_serializable(self, obj):
270 """
271 Make a serializable copy of an object or a list/tuple of objects.
272 Members which are difficult/impossible to serialize are stripped.
273 """
274 if (type(obj) is list) or (type(obj) is tuple):
275 rv = []
276 for item in obj:
277 rv.append(self._make_serializable(item))
278 if type(obj) is tuple:
279 rv = tuple(rv)
280 return rv
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200281 elif isinstance(obj, IntEnum) or isinstance(obj, IntFlag):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100282 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200283 else:
284 return self._make_obj_serializable(obj)
285
286 def _deserialize_obj(self, obj):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200287 if hasattr(obj, "type"):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100288 if obj.type is dict:
289 _obj = dict()
290 for k, v in zip(obj.k_list, obj.v_list):
291 _obj[self._deserialize(k)] = self._deserialize(v)
292 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200293 return obj
294
295 def _deserialize(self, obj):
296 if (type(obj) is list) or (type(obj) is tuple):
297 rv = []
298 for item in obj:
299 rv.append(self._deserialize(item))
300 if type(obj) is tuple:
301 rv = tuple(rv)
302 return rv
303 else:
304 return self._deserialize_obj(obj)
305
306 def start_remote(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200307 """Start remote execution"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200308 self.start()
309
310 def quit_remote(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200311 """Quit remote execution"""
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200312 self._remote_exec(RemoteClass.QUIT, None)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200313
314 def get_remote_value(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200315 """Get value of a remotely held object"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200316 return RemoteClassAttr(self, None).get_remote_value()
317
318 def set_request_timeout(self, timeout):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200319 """Change request timeout"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200320 self._timeout = timeout
321
322 def run(self):
323 """
324 Create instance of the wrapped class and execute operations
325 on it as requested by the parent process.
326 """
327 self._instance = self._cls(*self._args, **self._kwargs)
328 while True:
329 try:
330 rv = None
331 # get request from the parent process
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200332 (op, path, args, kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200333 args = self._deserialize(args)
334 kwargs = self._deserialize(kwargs)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200335 path = path.split(".") if path else []
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200336 if op == RemoteClass.GET:
337 rv = self._get_local_value(path)
338 elif op == RemoteClass.CALL:
339 rv = self._call_local_method(path, *args, **kwargs)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200340 elif op == RemoteClass.SETATTR and "value" in kwargs:
341 self._set_local_attr(path, kwargs["value"])
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200342 elif op == RemoteClass.REPR:
343 rv = self._get_local_repr(path)
344 elif op == RemoteClass.STR:
345 rv = self._get_local_str(path)
346 elif op == RemoteClass.QUIT:
347 break
348 else:
349 continue
350 # send return value
351 if not self._serializable(rv):
352 rv = self._make_serializable(rv)
353 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
354 except EOFError:
355 break
356 self._instance = None # destroy the instance
357
358
359@unittest.skip("Remote Vpp Test Case Class")
360class RemoteVppTestCase(VppTestCase):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200361 """Re-use VppTestCase to create remote VPP segment
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200362
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200363 In your test case::
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200364
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200365 @classmethod
366 def setUpClass(cls):
367 # fork new process before client connects to VPP
368 cls.remote_test = RemoteClass(RemoteVppTestCase)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200369
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200370 # start remote process
371 cls.remote_test.start_remote()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200372
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200373 # set up your test case
374 super(MyTestCase, cls).setUpClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200375
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200376 # set up remote test
377 cls.remote_test.setUpClass(cls.tempdir)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200378
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200379 @classmethod
380 def tearDownClass(cls):
381 # tear down remote test
382 cls.remote_test.tearDownClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200383
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200384 # stop remote process
385 cls.remote_test.quit_remote()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200386
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200387 # tear down your test case
388 super(MyTestCase, cls).tearDownClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200389 """
390
391 def __init__(self):
392 super(RemoteVppTestCase, self).__init__("emptyTest")
393
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700394 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
395 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200396 def __del__(self):
397 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700398 self.vpp.poll()
399 if self.vpp.returncode is None:
400 self.vpp.terminate()
401 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200402
403 @classmethod
404 def setUpClass(cls, tempdir):
405 # disable features unsupported in remote VPP
406 orig_env = dict(os.environ)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200407 if "STEP" in os.environ:
408 del os.environ["STEP"]
409 if "DEBUG" in os.environ:
410 del os.environ["DEBUG"]
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200411 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
412 super(RemoteVppTestCase, cls).setUpClass()
413 os.environ = orig_env
414
Paul Vinciguerra7f9b7f92019-03-12 19:23:27 -0700415 @classmethod
416 def tearDownClass(cls):
417 super(RemoteVppTestCase, cls).tearDownClass()
418
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200419 @unittest.skip("Empty test")
420 def emptyTest(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200421 """Do nothing"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200422 pass
423
424 def setTestFunctionInfo(self, name, doc):
425 """
426 Store the name and documentation string of currently executed test
427 in the main VPP for logging purposes.
428 """
429 self._testMethodName = name
430 self._testMethodDoc = doc