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