tests: support skipping to test method with STEP
[vpp.git] / test / hook.py
1 import os
2 import sys
3 import traceback
4 import ipaddress
5 from subprocess import check_output, CalledProcessError
6
7 import scapy.compat
8 import framework
9 from log import RED, single_line_delim, double_line_delim
10 from util import check_core_path, get_core_path
11
12
13 class Hook:
14     """
15     Generic hooks before/after API/CLI calls
16     """
17
18     def __init__(self, test):
19         self.test = test
20         self.logger = test.logger
21
22     def before_api(self, api_name, api_args):
23         """
24         Function called before API call
25         Emit a debug message describing the API name and arguments
26
27         @param api_name: name of the API
28         @param api_args: tuple containing the API arguments
29         """
30
31         def _friendly_format(val):
32             if not isinstance(val, str):
33                 return val
34             if len(val) == 6:
35                 return '{!s} ({!s})'.format(val, ':'.join(['{:02x}'.format(
36                     scapy.compat.orb(x)) for x in val]))
37             try:
38                 # we don't call test_type(val) because it is a packed value.
39                 return '{!s} ({!s})'.format(val, str(
40                     ipaddress.ip_address(val)))
41             except ValueError:
42                 return val
43
44         _args = ', '.join("{!s}={!r}".format(key, _friendly_format(val)) for
45                           (key, val) in api_args.items())
46         self.logger.debug("API: %s (%s)" %
47                           (api_name, _args), extra={'color': RED})
48
49     def after_api(self, api_name, api_args):
50         """
51         Function called after API call
52
53         @param api_name: name of the API
54         @param api_args: tuple containing the API arguments
55         """
56         pass
57
58     def before_cli(self, cli):
59         """
60         Function called before CLI call
61         Emit a debug message describing the CLI
62
63         @param cli: CLI string
64         """
65         self.logger.debug("CLI: %s" % (cli), extra={'color': RED})
66
67     def after_cli(self, cli):
68         """
69         Function called after CLI call
70         """
71         pass
72
73
74 class PollHook(Hook):
75     """ Hook which checks if the vpp subprocess is alive """
76
77     def __init__(self, test):
78         super(PollHook, self).__init__(test)
79
80     def on_crash(self, core_path):
81         self.logger.error("Core file present, debug with: gdb %s %s",
82                           self.test.vpp_bin, core_path)
83         check_core_path(self.logger, core_path)
84         self.logger.error("Running `file %s':", core_path)
85         try:
86             info = check_output(["file", core_path])
87             self.logger.error(info)
88         except CalledProcessError as e:
89             self.logger.error(
90                 "Subprocess returned with error running `file' utility on "
91                 "core-file, "
92                 "rc=%s",  e.returncode)
93         except OSError as e:
94             self.logger.error(
95                 "Subprocess returned OS error running `file' utility on "
96                 "core-file, "
97                 "oserror=(%s) %s", e.errno, e.strerror)
98         except Exception as e:
99             self.logger.error(
100                 "Subprocess returned unanticipated error running `file' "
101                 "utility on core-file, "
102                 "%s", e)
103
104     def poll_vpp(self):
105         """
106         Poll the vpp status and throw an exception if it's not running
107         :raises VppDiedError: exception if VPP is not running anymore
108         """
109         if self.test.vpp_dead:
110             # already dead, nothing to do
111             return
112
113         self.test.vpp.poll()
114         if self.test.vpp.returncode is not None:
115             self.test.vpp_dead = True
116             raise framework.VppDiedError(rv=self.test.vpp.returncode)
117             core_path = get_core_path(self.test.tempdir)
118             if os.path.isfile(core_path):
119                 self.on_crash(core_path)
120
121     def before_api(self, api_name, api_args):
122         """
123         Check if VPP died before executing an API
124
125         :param api_name: name of the API
126         :param api_args: tuple containing the API arguments
127         :raises VppDiedError: exception if VPP is not running anymore
128
129         """
130         super(PollHook, self).before_api(api_name, api_args)
131         self.poll_vpp()
132
133     def before_cli(self, cli):
134         """
135         Check if VPP died before executing a CLI
136
137         :param cli: CLI string
138         :raises Exception: exception if VPP is not running anymore
139
140         """
141         super(PollHook, self).before_cli(cli)
142         self.poll_vpp()
143
144
145 class StepHook(PollHook):
146     """ Hook which requires user to press ENTER before doing any API/CLI """
147
148     def __init__(self, test):
149         self.skip_stack = None
150         self.skip_num = None
151         self.skip_count = 0
152         self.break_func = None
153         super(StepHook, self).__init__(test)
154
155     def skip(self):
156         if self.break_func is not None:
157             return self.should_skip_func_based()
158         if self.skip_stack is not None:
159             return self.should_skip_stack_based()
160
161     def should_skip_func_based(self):
162         stack = traceback.extract_stack()
163         for e in stack:
164             if e[2] == self.break_func:
165                 self.break_func = None
166                 return False
167         return True
168
169     def should_skip_stack_based(self):
170         stack = traceback.extract_stack()
171         counter = 0
172         skip = True
173         for e in stack:
174             if counter > self.skip_num:
175                 break
176             if e[0] != self.skip_stack[counter][0]:
177                 skip = False
178             if e[1] != self.skip_stack[counter][1]:
179                 skip = False
180             counter += 1
181         if skip:
182             self.skip_count += 1
183             return True
184         else:
185             print("%d API/CLI calls skipped in specified stack "
186                   "frame" % self.skip_count)
187             self.skip_count = 0
188             self.skip_stack = None
189             self.skip_num = None
190             return False
191
192     def user_input(self):
193         print('number\tfunction\tfile\tcode')
194         counter = 0
195         stack = traceback.extract_stack()
196         for e in stack:
197             print('%02d.\t%s\t%s:%d\t[%s]' % (counter, e[2], e[0], e[1], e[3]))
198             counter += 1
199         print(single_line_delim)
200         print("You may enter a number of stack frame chosen from above")
201         print("Calls in/below that stack frame will be not be stepped anymore")
202         print("Alternatively, enter a test function name to stop at")
203         print(single_line_delim)
204         while True:
205             print("Enter your choice, if any, and press ENTER to continue "
206                   "running the testcase...")
207             choice = sys.stdin.readline().rstrip('\r\n')
208             if choice == "":
209                 choice = None
210             try:
211                 if choice is not None:
212                     num = int(choice)
213             except ValueError:
214                 if choice.startswith("test_"):
215                     break
216                 print("Invalid input")
217                 continue
218             if choice is not None and (num < 0 or num >= len(stack)):
219                 print("Invalid choice")
220                 continue
221             break
222         if choice is not None:
223             if choice.startswith("test_"):
224                 self.break_func = choice
225             else:
226                 self.break_func = None
227                 self.skip_stack = stack
228                 self.skip_num = num
229
230     def before_cli(self, cli):
231         """ Wait for ENTER before executing CLI """
232         if self.skip():
233             print("Skip pause before executing CLI: %s" % cli)
234         else:
235             print(double_line_delim)
236             print("Test paused before executing CLI: %s" % cli)
237             print(single_line_delim)
238             self.user_input()
239         super(StepHook, self).before_cli(cli)
240
241     def before_api(self, api_name, api_args):
242         """ Wait for ENTER before executing API """
243         if self.skip():
244             print("Skip pause before executing API: %s (%s)"
245                   % (api_name, api_args))
246         else:
247             print(double_line_delim)
248             print("Test paused before executing API: %s (%s)"
249                   % (api_name, api_args))
250             print(single_line_delim)
251             self.user_input()
252         super(StepHook, self).before_api(api_name, api_args)