blob: 7743c7782e4e26d5e8c4d536e8c374bf38f0d845 [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
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04007from asfframework 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 """
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020019
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020020 pass
21
Neale Ranns097fa662018-05-01 05:17:55 -070022 def __repr__(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020023 return "<SerializableClassCopy dict=%s>" % self.__dict__
Neale Ranns097fa662018-05-01 05:17:55 -070024
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020025
Paul Vinciguerrae061dad2020-12-04 14:57:51 -050026class RemoteClassAttr:
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020027 """
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):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020036 return ".".join(self._path)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020037
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):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020048 if attr[0] == "_":
49 if not (attr.startswith("__") and attr.endswith("__")):
50 raise AttributeError("tried to get private attribute: %s ", attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020051 self._path.append(attr)
52 return self
53
54 def __setattr__(self, attr, val):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020055 if attr[0] == "_":
56 if not (attr.startswith("__") and attr.endswith("__")):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070057 super(RemoteClassAttr, self).__setattr__(attr, val)
58 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020059 self._path.append(attr)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020060 self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(), value=val)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020061
62 def __call__(self, *args, **kwargs):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020063 return self._remote._remote_exec(
64 RemoteClass.CALL, self.path_to_str(), *args, **kwargs
65 )
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
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200101 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
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200107
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):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200131 if attr[0] == "_" or not self.is_alive():
132 if not (attr.startswith("__") and attr.endswith("__")):
133 if hasattr(super(RemoteClass, self), "__getattr__"):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700134 return super(RemoteClass, self).__getattr__(attr)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200135 raise AttributeError("missing: %s", attr)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200136 return RemoteClassAttr(self, attr)
137
138 def __setattr__(self, attr, val):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200139 if attr[0] == "_" or not self.is_alive():
140 if not (attr.startswith("__") and attr.endswith("__")):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700141 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):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200152 if isinstance(val, RemoteClass) or isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200153 mutable_args[i] = val.get_remote_value()
154 args = tuple(mutable_args)
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500155 for key, val in kwargs.items():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200156 if isinstance(val, RemoteClass) or isinstance(val, RemoteClassAttr):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200157 kwargs[key] = val.get_remote_value()
158 # send request
159 args = self._make_serializable(args)
160 kwargs = self._make_serializable(kwargs)
161 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200162 timeout = self._timeout
163 # adjust timeout specifically for the .sleep method
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200164 if path is not None and path.split(".")[-1] == "sleep":
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200165 if args and isinstance(args[0], (long, int)):
166 timeout += args[0]
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200167 elif "timeout" in kwargs:
168 timeout += kwargs["timeout"]
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200169 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
170 return None
171 try:
172 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
173 rv = self._deserialize(rv)
174 return rv
175 except EOFError:
176 return None
177
178 def _get_local_object(self, path):
179 """
180 Follow the path to obtain a reference on the addressed nested attribute
181 """
182 obj = self._instance
183 for attr in path:
184 obj = getattr(obj, attr)
185 return obj
186
187 def _get_local_value(self, path):
188 try:
189 return self._get_local_object(path)
190 except AttributeError:
191 return None
192
193 def _call_local_method(self, path, *args, **kwargs):
194 try:
195 method = self._get_local_object(path)
196 return method(*args, **kwargs)
197 except AttributeError:
198 return None
199
200 def _set_local_attr(self, path, value):
201 try:
202 obj = self._get_local_object(path[:-1])
203 setattr(obj, path[-1], value)
204 except AttributeError:
205 pass
206 return None
207
208 def _get_local_repr(self, path):
209 try:
210 obj = self._get_local_object(path)
Paul Vinciguerra090096b2020-12-03 00:42:46 -0500211 return reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200212 except AttributeError:
213 return None
214
215 def _get_local_str(self, path):
216 try:
217 obj = self._get_local_object(path)
218 return str(obj)
219 except AttributeError:
220 return None
221
222 def _serializable(self, obj):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200223 """Test if the given object is serializable"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200224 try:
225 dumps(obj)
226 return True
227 except:
228 return False
229
230 def _make_obj_serializable(self, obj):
231 """
232 Make a serializable copy of an object.
233 Members which are difficult/impossible to serialize are stripped.
234 """
235 if self._serializable(obj):
236 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100237
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200238 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100239
240 """
241 Dictionaries can hold complex values, so we split keys and values into
242 separate lists and serialize them individually.
243 """
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200244 if type(obj) is dict:
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100245 copy.type = type(obj)
246 copy.k_list = list()
247 copy.v_list = list()
248 for k, v in obj.items():
249 copy.k_list.append(self._make_serializable(k))
250 copy.v_list.append(self._make_serializable(v))
251 return copy
252
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200253 # copy at least serializable attributes and properties
254 for name, member in inspect.getmembers(obj):
Neale Ranns097fa662018-05-01 05:17:55 -0700255 # skip private members and non-writable dunder methods.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200256 if name[0] == "_":
257 if name in ["__weakref__"]:
Neale Ranns097fa662018-05-01 05:17:55 -0700258 continue
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200259 if name in ["__dict__"]:
Jakub Grajciar546f9552019-08-21 10:51:21 +0200260 continue
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200261 if not (name.startswith("__") and name.endswith("__")):
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700262 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200263 if callable(member) and not isinstance(member, property):
264 continue
265 if not self._serializable(member):
Jakub Grajciar546f9552019-08-21 10:51:21 +0200266 member = self._make_serializable(member)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200267 setattr(copy, name, member)
268 return copy
269
270 def _make_serializable(self, obj):
271 """
272 Make a serializable copy of an object or a list/tuple of objects.
273 Members which are difficult/impossible to serialize are stripped.
274 """
275 if (type(obj) is list) or (type(obj) is tuple):
276 rv = []
277 for item in obj:
278 rv.append(self._make_serializable(item))
279 if type(obj) is tuple:
280 rv = tuple(rv)
281 return rv
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200282 elif isinstance(obj, IntEnum) or isinstance(obj, IntFlag):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100283 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200284 else:
285 return self._make_obj_serializable(obj)
286
287 def _deserialize_obj(self, obj):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200288 if hasattr(obj, "type"):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100289 if obj.type is dict:
290 _obj = dict()
291 for k, v in zip(obj.k_list, obj.v_list):
292 _obj[self._deserialize(k)] = self._deserialize(v)
293 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200294 return obj
295
296 def _deserialize(self, obj):
297 if (type(obj) is list) or (type(obj) is tuple):
298 rv = []
299 for item in obj:
300 rv.append(self._deserialize(item))
301 if type(obj) is tuple:
302 rv = tuple(rv)
303 return rv
304 else:
305 return self._deserialize_obj(obj)
306
307 def start_remote(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200308 """Start remote execution"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200309 self.start()
310
311 def quit_remote(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200312 """Quit remote execution"""
Jakub Grajciar7db35de2019-06-25 10:22:11 +0200313 self._remote_exec(RemoteClass.QUIT, None)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200314
315 def get_remote_value(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200316 """Get value of a remotely held object"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200317 return RemoteClassAttr(self, None).get_remote_value()
318
319 def set_request_timeout(self, timeout):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200320 """Change request timeout"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200321 self._timeout = timeout
322
323 def run(self):
324 """
325 Create instance of the wrapped class and execute operations
326 on it as requested by the parent process.
327 """
328 self._instance = self._cls(*self._args, **self._kwargs)
329 while True:
330 try:
331 rv = None
332 # get request from the parent process
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200333 (op, path, args, kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200334 args = self._deserialize(args)
335 kwargs = self._deserialize(kwargs)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200336 path = path.split(".") if path else []
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200337 if op == RemoteClass.GET:
338 rv = self._get_local_value(path)
339 elif op == RemoteClass.CALL:
340 rv = self._call_local_method(path, *args, **kwargs)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200341 elif op == RemoteClass.SETATTR and "value" in kwargs:
342 self._set_local_attr(path, kwargs["value"])
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200343 elif op == RemoteClass.REPR:
344 rv = self._get_local_repr(path)
345 elif op == RemoteClass.STR:
346 rv = self._get_local_str(path)
347 elif op == RemoteClass.QUIT:
348 break
349 else:
350 continue
351 # send return value
352 if not self._serializable(rv):
353 rv = self._make_serializable(rv)
354 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
355 except EOFError:
356 break
357 self._instance = None # destroy the instance
358
359
360@unittest.skip("Remote Vpp Test Case Class")
361class RemoteVppTestCase(VppTestCase):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200362 """Re-use VppTestCase to create remote VPP segment
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200363
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200364 In your test case::
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200365
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200366 @classmethod
367 def setUpClass(cls):
368 # fork new process before client connects to VPP
369 cls.remote_test = RemoteClass(RemoteVppTestCase)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200370
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200371 # start remote process
372 cls.remote_test.start_remote()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200373
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200374 # set up your test case
375 super(MyTestCase, cls).setUpClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200376
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200377 # set up remote test
378 cls.remote_test.setUpClass(cls.tempdir)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200379
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200380 @classmethod
381 def tearDownClass(cls):
382 # tear down remote test
383 cls.remote_test.tearDownClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200384
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200385 # stop remote process
386 cls.remote_test.quit_remote()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200387
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200388 # tear down your test case
389 super(MyTestCase, cls).tearDownClass()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200390 """
391
392 def __init__(self):
393 super(RemoteVppTestCase, self).__init__("emptyTest")
394
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700395 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
396 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200397 def __del__(self):
398 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700399 self.vpp.poll()
400 if self.vpp.returncode is None:
401 self.vpp.terminate()
402 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200403
404 @classmethod
405 def setUpClass(cls, tempdir):
406 # disable features unsupported in remote VPP
407 orig_env = dict(os.environ)
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200408 if "STEP" in os.environ:
409 del os.environ["STEP"]
410 if "DEBUG" in os.environ:
411 del os.environ["DEBUG"]
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200412 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
413 super(RemoteVppTestCase, cls).setUpClass()
414 os.environ = orig_env
415
Paul Vinciguerra7f9b7f92019-03-12 19:23:27 -0700416 @classmethod
417 def tearDownClass(cls):
418 super(RemoteVppTestCase, cls).tearDownClass()
419
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200420 @unittest.skip("Empty test")
421 def emptyTest(self):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200422 """Do nothing"""
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200423 pass
424
425 def setTestFunctionInfo(self, name, doc):
426 """
427 Store the name and documentation string of currently executed test
428 in the main VPP for logging purposes.
429 """
430 self._testMethodName = name
431 self._testMethodDoc = doc