sr: fix added for configuring vlan sub interface as iif interface in End.AD.Flow...
[vpp.git] / test / remote_test.py
1 #!/usr/bin/env python3
2
3 import inspect
4 import os
5 import reprlib
6 import unittest
7 from framework import VppTestCase
8 from multiprocessing import Process, Pipe
9 from pickle import dumps
10 import sys
11
12 from enum import IntEnum, IntFlag
13
14
15 class SerializableClassCopy:
16     """
17     Empty class used as a basis for a serializable copy of another class.
18     """
19
20     pass
21
22     def __repr__(self):
23         return "<SerializableClassCopy dict=%s>" % self.__dict__
24
25
26 class RemoteClassAttr:
27     """
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):
36         return ".".join(self._path)
37
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):
48         if attr[0] == "_":
49             if not (attr.startswith("__") and attr.endswith("__")):
50                 raise AttributeError("tried to get private attribute: %s ", attr)
51         self._path.append(attr)
52         return self
53
54     def __setattr__(self, attr, val):
55         if attr[0] == "_":
56             if not (attr.startswith("__") and attr.endswith("__")):
57                 super(RemoteClassAttr, self).__setattr__(attr, val)
58                 return
59         self._path.append(attr)
60         self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(), value=val)
61
62     def __call__(self, *args, **kwargs):
63         return self._remote._remote_exec(
64             RemoteClass.CALL, self.path_to_str(), *args, **kwargs
65         )
66
67
68 class 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.
72
73     Usage:
74
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()
99     """
100
101     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
107
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):
122         return reprlib.repr(RemoteClassAttr(self, None))
123
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):
131         if attr[0] == "_" or not self.is_alive():
132             if not (attr.startswith("__") and attr.endswith("__")):
133                 if hasattr(super(RemoteClass, self), "__getattr__"):
134                     return super(RemoteClass, self).__getattr__(attr)
135                 raise AttributeError("missing: %s", attr)
136         return RemoteClassAttr(self, attr)
137
138     def __setattr__(self, attr, val):
139         if attr[0] == "_" or not self.is_alive():
140             if not (attr.startswith("__") and attr.endswith("__")):
141                 super(RemoteClass, self).__setattr__(attr, val)
142                 return
143         setattr(RemoteClassAttr(self, None), attr, val)
144
145     def _remote_exec(self, op, path=None, *args, **kwargs):
146         """
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):
152             if isinstance(val, RemoteClass) or isinstance(val, RemoteClassAttr):
153                 mutable_args[i] = val.get_remote_value()
154         args = tuple(mutable_args)
155         for key, val in kwargs.items():
156             if isinstance(val, RemoteClass) or isinstance(val, RemoteClassAttr):
157                 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))
162         timeout = self._timeout
163         # adjust timeout specifically for the .sleep method
164         if path is not None and path.split(".")[-1] == "sleep":
165             if args and isinstance(args[0], (long, int)):
166                 timeout += args[0]
167             elif "timeout" in kwargs:
168                 timeout += kwargs["timeout"]
169         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)
211             return reprlib.repr(obj)
212         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):
223         """Test if the given object is serializable"""
224         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
237
238         copy = SerializableClassCopy()
239
240         """
241         Dictionaries can hold complex values, so we split keys and values into
242         separate lists and serialize them individually.
243         """
244         if type(obj) is dict:
245             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
253         # copy at least serializable attributes and properties
254         for name, member in inspect.getmembers(obj):
255             # skip private members and non-writable dunder methods.
256             if name[0] == "_":
257                 if name in ["__weakref__"]:
258                     continue
259                 if name in ["__dict__"]:
260                     continue
261                 if not (name.startswith("__") and name.endswith("__")):
262                     continue
263             if callable(member) and not isinstance(member, property):
264                 continue
265             if not self._serializable(member):
266                 member = self._make_serializable(member)
267             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
282         elif isinstance(obj, IntEnum) or isinstance(obj, IntFlag):
283             return obj.value
284         else:
285             return self._make_obj_serializable(obj)
286
287     def _deserialize_obj(self, obj):
288         if hasattr(obj, "type"):
289             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
294         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):
308         """Start remote execution"""
309         self.start()
310
311     def quit_remote(self):
312         """Quit remote execution"""
313         self._remote_exec(RemoteClass.QUIT, None)
314
315     def get_remote_value(self):
316         """Get value of a remotely held object"""
317         return RemoteClassAttr(self, None).get_remote_value()
318
319     def set_request_timeout(self, timeout):
320         """Change request timeout"""
321         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
333                 (op, path, args, kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
334                 args = self._deserialize(args)
335                 kwargs = self._deserialize(kwargs)
336                 path = path.split(".") if path else []
337                 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)
341                 elif op == RemoteClass.SETATTR and "value" in kwargs:
342                     self._set_local_attr(path, kwargs["value"])
343                 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")
361 class RemoteVppTestCase(VppTestCase):
362     """Re-use VppTestCase to create remote VPP segment
363
364     In your test case::
365
366         @classmethod
367         def setUpClass(cls):
368             # fork new process before client connects to VPP
369             cls.remote_test = RemoteClass(RemoteVppTestCase)
370
371             # start remote process
372             cls.remote_test.start_remote()
373
374             # set up your test case
375             super(MyTestCase, cls).setUpClass()
376
377             # set up remote test
378             cls.remote_test.setUpClass(cls.tempdir)
379
380         @classmethod
381         def tearDownClass(cls):
382             # tear down remote test
383             cls.remote_test.tearDownClass()
384
385             # stop remote process
386             cls.remote_test.quit_remote()
387
388             # tear down your test case
389             super(MyTestCase, cls).tearDownClass()
390     """
391
392     def __init__(self):
393         super(RemoteVppTestCase, self).__init__("emptyTest")
394
395     # Note: __del__ is a 'Finalizer" not a 'Destructor'.
396     # https://docs.python.org/3/reference/datamodel.html#object.__del__
397     def __del__(self):
398         if hasattr(self, "vpp"):
399             self.vpp.poll()
400             if self.vpp.returncode is None:
401                 self.vpp.terminate()
402                 self.vpp.communicate()
403
404     @classmethod
405     def setUpClass(cls, tempdir):
406         # disable features unsupported in remote VPP
407         orig_env = dict(os.environ)
408         if "STEP" in os.environ:
409             del os.environ["STEP"]
410         if "DEBUG" in os.environ:
411             del os.environ["DEBUG"]
412         cls.tempdir_prefix = os.path.basename(tempdir) + "/"
413         super(RemoteVppTestCase, cls).setUpClass()
414         os.environ = orig_env
415
416     @classmethod
417     def tearDownClass(cls):
418         super(RemoteVppTestCase, cls).tearDownClass()
419
420     @unittest.skip("Empty test")
421     def emptyTest(self):
422         """Do nothing"""
423         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