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