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