blob: bc6d70764ec5ef05cf21a814a670c45618d1acff [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
13from enum 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
22
23class RemoteClassAttr(object):
24 """
25 Wrapper around attribute of a remotely executed class.
26 """
27
28 def __init__(self, remote, attr):
29 self._path = [attr] if attr else []
30 self._remote = remote
31
32 def path_to_str(self):
33 return '.'.join(self._path)
34
35 def get_remote_value(self):
36 return self._remote._remote_exec(RemoteClass.GET, self.path_to_str())
37
38 def __repr__(self):
39 return self._remote._remote_exec(RemoteClass.REPR, self.path_to_str())
40
41 def __str__(self):
42 return self._remote._remote_exec(RemoteClass.STR, self.path_to_str())
43
44 def __getattr__(self, attr):
45 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070046 if not (attr.startswith('__') and attr.endswith('__')):
47 raise AttributeError
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020048 self._path.append(attr)
49 return self
50
51 def __setattr__(self, attr, val):
52 if attr[0] == '_':
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -070053 if not (attr.startswith('__') and attr.endswith('__')):
54 super(RemoteClassAttr, self).__setattr__(attr, val)
55 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +020056 self._path.append(attr)
57 self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(),
58 True, value=val)
59
60 def __call__(self, *args, **kwargs):
61 return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
62 True, *args, **kwargs)
63
64
65class RemoteClass(Process):
66 """
67 This class can wrap around and adapt the interface of another class,
68 and then delegate its execution to a newly forked child process.
69 Usage:
70 # Create a remotely executed instance of MyClass
71 object = RemoteClass(MyClass, arg1='foo', arg2='bar')
72 object.start_remote()
73 # Access the object normally as if it was an instance of your class.
74 object.my_attribute = 20
75 print object.my_attribute
76 print object.my_method(object.my_attribute)
77 object.my_attribute.nested_attribute = 'test'
78 # If you need the value of a remote attribute, use .get_remote_value
79 method. This method is automatically called when needed in the context
80 of a remotely executed class. E.g.:
81 if (object.my_attribute.get_remote_value() > 20):
82 object.my_attribute2 = object.my_attribute
83 # Destroy the instance
84 object.quit_remote()
85 object.terminate()
86 """
87
88 GET = 0 # Get attribute remotely
89 CALL = 1 # Call method remotely
90 SETATTR = 2 # Set attribute remotely
91 REPR = 3 # Get representation of a remote object
92 STR = 4 # Get string representation of a remote object
93 QUIT = 5 # Quit remote execution
94
95 PIPE_PARENT = 0 # Parent end of the pipe
96 PIPE_CHILD = 1 # Child end of the pipe
97
98 DEFAULT_TIMEOUT = 2 # default timeout for an operation to execute
99
100 def __init__(self, cls, *args, **kwargs):
101 super(RemoteClass, self).__init__()
102 self._cls = cls
103 self._args = args
104 self._kwargs = kwargs
105 self._timeout = RemoteClass.DEFAULT_TIMEOUT
106 self._pipe = Pipe() # pipe for input/output arguments
107
108 def __repr__(self):
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800109 return moves.reprlib.repr(RemoteClassAttr(self, None))
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200110
111 def __str__(self):
112 return str(RemoteClassAttr(self, None))
113
114 def __call__(self, *args, **kwargs):
115 return self.RemoteClassAttr(self, None)()
116
117 def __getattr__(self, attr):
118 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700119 if not (attr.startswith('__') and attr.endswith('__')):
120 if hasattr(super(RemoteClass, self), '__getattr__'):
121 return super(RemoteClass, self).__getattr__(attr)
122 raise AttributeError
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200123 return RemoteClassAttr(self, attr)
124
125 def __setattr__(self, attr, val):
126 if attr[0] == '_' or not self.is_alive():
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700127 if not (attr.startswith('__') and attr.endswith('__')):
128 super(RemoteClass, self).__setattr__(attr, val)
129 return
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200130 setattr(RemoteClassAttr(self, None), attr, val)
131
132 def _remote_exec(self, op, path=None, ret=True, *args, **kwargs):
133 """
134 Execute given operation on a given, possibly nested, member remotely.
135 """
136 # automatically resolve remote objects in the arguments
137 mutable_args = list(args)
138 for i, val in enumerate(mutable_args):
139 if isinstance(val, RemoteClass) or \
140 isinstance(val, RemoteClassAttr):
141 mutable_args[i] = val.get_remote_value()
142 args = tuple(mutable_args)
Paul Vinciguerraf1f2aa62018-11-25 08:36:47 -0800143 for key, val in six.iteritems(kwargs):
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200144 if isinstance(val, RemoteClass) or \
145 isinstance(val, RemoteClassAttr):
146 kwargs[key] = val.get_remote_value()
147 # send request
148 args = self._make_serializable(args)
149 kwargs = self._make_serializable(kwargs)
150 self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
151 if not ret:
152 # no return value expected
153 return None
154 timeout = self._timeout
155 # adjust timeout specifically for the .sleep method
156 if path.split('.')[-1] == 'sleep':
157 if args and isinstance(args[0], (long, int)):
158 timeout += args[0]
159 elif 'timeout' in kwargs:
160 timeout += kwargs['timeout']
161 if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
162 return None
163 try:
164 rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
165 rv = self._deserialize(rv)
166 return rv
167 except EOFError:
168 return None
169
170 def _get_local_object(self, path):
171 """
172 Follow the path to obtain a reference on the addressed nested attribute
173 """
174 obj = self._instance
175 for attr in path:
176 obj = getattr(obj, attr)
177 return obj
178
179 def _get_local_value(self, path):
180 try:
181 return self._get_local_object(path)
182 except AttributeError:
183 return None
184
185 def _call_local_method(self, path, *args, **kwargs):
186 try:
187 method = self._get_local_object(path)
188 return method(*args, **kwargs)
189 except AttributeError:
190 return None
191
192 def _set_local_attr(self, path, value):
193 try:
194 obj = self._get_local_object(path[:-1])
195 setattr(obj, path[-1], value)
196 except AttributeError:
197 pass
198 return None
199
200 def _get_local_repr(self, path):
201 try:
202 obj = self._get_local_object(path)
Paul Vinciguerra6c746172018-11-26 09:57:21 -0800203 return moves.reprlib.repr(obj)
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200204 except AttributeError:
205 return None
206
207 def _get_local_str(self, path):
208 try:
209 obj = self._get_local_object(path)
210 return str(obj)
211 except AttributeError:
212 return None
213
214 def _serializable(self, obj):
215 """ Test if the given object is serializable """
216 try:
217 dumps(obj)
218 return True
219 except:
220 return False
221
222 def _make_obj_serializable(self, obj):
223 """
224 Make a serializable copy of an object.
225 Members which are difficult/impossible to serialize are stripped.
226 """
227 if self._serializable(obj):
228 return obj # already serializable
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100229
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200230 copy = SerializableClassCopy()
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100231
232 """
233 Dictionaries can hold complex values, so we split keys and values into
234 separate lists and serialize them individually.
235 """
236 if (type(obj) is dict):
237 copy.type = type(obj)
238 copy.k_list = list()
239 copy.v_list = list()
240 for k, v in obj.items():
241 copy.k_list.append(self._make_serializable(k))
242 copy.v_list.append(self._make_serializable(v))
243 return copy
244
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200245 # copy at least serializable attributes and properties
246 for name, member in inspect.getmembers(obj):
247 if name[0] == '_': # skip private members
Paul Vinciguerrad3a9be22019-03-10 07:03:22 -0700248 if not (name.startswith('__') and name.endswith('__')):
249 continue
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200250 if callable(member) and not isinstance(member, property):
251 continue
252 if not self._serializable(member):
253 continue
254 setattr(copy, name, member)
255 return copy
256
257 def _make_serializable(self, obj):
258 """
259 Make a serializable copy of an object or a list/tuple of objects.
260 Members which are difficult/impossible to serialize are stripped.
261 """
262 if (type(obj) is list) or (type(obj) is tuple):
263 rv = []
264 for item in obj:
265 rv.append(self._make_serializable(item))
266 if type(obj) is tuple:
267 rv = tuple(rv)
268 return rv
Ole Trøan3b0d7e42019-03-15 16:14:41 +0000269 elif (isinstance(obj, Enum)):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100270 return obj.value
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200271 else:
272 return self._make_obj_serializable(obj)
273
274 def _deserialize_obj(self, obj):
Jakub Grajciar0b1f8a72019-02-21 12:01:31 +0100275 if (hasattr(obj, 'type')):
276 if obj.type is dict:
277 _obj = dict()
278 for k, v in zip(obj.k_list, obj.v_list):
279 _obj[self._deserialize(k)] = self._deserialize(v)
280 return _obj
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200281 return obj
282
283 def _deserialize(self, obj):
284 if (type(obj) is list) or (type(obj) is tuple):
285 rv = []
286 for item in obj:
287 rv.append(self._deserialize(item))
288 if type(obj) is tuple:
289 rv = tuple(rv)
290 return rv
291 else:
292 return self._deserialize_obj(obj)
293
294 def start_remote(self):
295 """ Start remote execution """
296 self.start()
297
298 def quit_remote(self):
299 """ Quit remote execution """
300 self._remote_exec(RemoteClass.QUIT, None, False)
301
302 def get_remote_value(self):
303 """ Get value of a remotely held object """
304 return RemoteClassAttr(self, None).get_remote_value()
305
306 def set_request_timeout(self, timeout):
307 """ Change request timeout """
308 self._timeout = timeout
309
310 def run(self):
311 """
312 Create instance of the wrapped class and execute operations
313 on it as requested by the parent process.
314 """
315 self._instance = self._cls(*self._args, **self._kwargs)
316 while True:
317 try:
318 rv = None
319 # get request from the parent process
320 (op, path, args,
321 kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
322 args = self._deserialize(args)
323 kwargs = self._deserialize(kwargs)
324 path = path.split('.') if path else []
325 if op == RemoteClass.GET:
326 rv = self._get_local_value(path)
327 elif op == RemoteClass.CALL:
328 rv = self._call_local_method(path, *args, **kwargs)
329 elif op == RemoteClass.SETATTR and 'value' in kwargs:
330 self._set_local_attr(path, kwargs['value'])
331 elif op == RemoteClass.REPR:
332 rv = self._get_local_repr(path)
333 elif op == RemoteClass.STR:
334 rv = self._get_local_str(path)
335 elif op == RemoteClass.QUIT:
336 break
337 else:
338 continue
339 # send return value
340 if not self._serializable(rv):
341 rv = self._make_serializable(rv)
342 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
343 except EOFError:
344 break
345 self._instance = None # destroy the instance
346
347
348@unittest.skip("Remote Vpp Test Case Class")
349class RemoteVppTestCase(VppTestCase):
350 """ Re-use VppTestCase to create remote VPP segment
351
352 In your test case:
353
354 @classmethod
355 def setUpClass(cls):
356 # fork new process before clinet connects to VPP
357 cls.remote_test = RemoteClass(RemoteVppTestCase)
358
359 # start remote process
360 cls.remote_test.start_remote()
361
362 # set up your test case
363 super(MyTestCase, cls).setUpClass()
364
365 # set up remote test
366 cls.remote_test.setUpClass(cls.tempdir)
367
368 @classmethod
369 def tearDownClass(cls):
370 # tear down remote test
371 cls.remote_test.tearDownClass()
372
373 # stop remote process
374 cls.remote_test.quit_remote()
375
376 # tear down your test case
377 super(MyTestCase, cls).tearDownClass()
378 """
379
380 def __init__(self):
381 super(RemoteVppTestCase, self).__init__("emptyTest")
382
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700383 # Note: __del__ is a 'Finalizer" not a 'Destructor'.
384 # https://docs.python.org/3/reference/datamodel.html#object.__del__
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200385 def __del__(self):
386 if hasattr(self, "vpp"):
Paul Vinciguerraf70cead2019-03-10 07:32:59 -0700387 self.vpp.poll()
388 if self.vpp.returncode is None:
389 self.vpp.terminate()
390 self.vpp.communicate()
Jakub Grajciarb1be2a02018-09-19 13:36:16 +0200391
392 @classmethod
393 def setUpClass(cls, tempdir):
394 # disable features unsupported in remote VPP
395 orig_env = dict(os.environ)
396 if 'STEP' in os.environ:
397 del os.environ['STEP']
398 if 'DEBUG' in os.environ:
399 del os.environ['DEBUG']
400 cls.tempdir_prefix = os.path.basename(tempdir) + "/"
401 super(RemoteVppTestCase, cls).setUpClass()
402 os.environ = orig_env
403
404 @unittest.skip("Empty test")
405 def emptyTest(self):
406 """ Do nothing """
407 pass
408
409 def setTestFunctionInfo(self, name, doc):
410 """
411 Store the name and documentation string of currently executed test
412 in the main VPP for logging purposes.
413 """
414 self._testMethodName = name
415 self._testMethodDoc = doc