Performance: DPDK refactor
[csit.git] / resources / libraries / python / autogen / Regenerator.py
1 # Copyright (c) 2020 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Module defining utilities for test directory regeneration.
15
16 TODO: How can we check each suite id is unique,
17       when currently the suite generation is run on each directory separately?
18 """
19
20 import sys
21
22 from glob import glob
23 from io import open
24 from os import getcwd
25
26
27 from resources.libraries.python.Constants import Constants
28 from resources.libraries.python.autogen.Testcase import Testcase
29
30
31 PROTOCOL_TO_MIN_FRAME_SIZE = {
32     u"ip4": 64,
33     u"ip6": 78,
34     u"ethip4vxlan": 114,  # What is the real minimum for latency stream?
35     u"dot1qip4vxlan": 118
36 }
37 MIN_FRAME_SIZE_VALUES = list(PROTOCOL_TO_MIN_FRAME_SIZE.values())
38
39
40 def replace_defensively(
41         whole, to_replace, replace_with, how_many, msg, in_filename):
42     """Replace substrings while checking the number of occurrences.
43
44     Return edited copy of the text. Assuming "whole" is really a string,
45     or something else with .replace not affecting it.
46
47     :param whole: The text to perform replacements on.
48     :param to_replace: Substring occurrences of which to replace.
49     :param replace_with: Substring to replace occurrences with.
50     :param how_many: Number of occurrences to expect.
51     :param msg: Error message to raise.
52     :param in_filename: File name in which the error occurred.
53     :type whole: str
54     :type to_replace: str
55     :type replace_with: str
56     :type how_many: int
57     :type msg: str
58     :type in_filename: str
59     :returns: The whole text after replacements are done.
60     :rtype: str
61     :raises ValueError: If number of occurrences does not match.
62     """
63     found = whole.count(to_replace)
64     if found != how_many:
65         raise ValueError(f"{in_filename}: {msg}")
66     return whole.replace(to_replace, replace_with)
67
68
69 def get_iface_and_suite_ids(filename):
70     """Get NIC code, suite ID and suite tag.
71
72     NIC code is the part of suite name
73     which should be replaced for other NIC.
74     Suite ID is the part os suite name
75     which is appended to test case names.
76     Suite tag is suite ID without both test type and NIC driver parts.
77
78     :param filename: Suite file.
79     :type filename: str
80     :returns: NIC code, suite ID, suite tag.
81     :rtype: 3-tuple of str
82     """
83     dash_split = filename.split(u"-", 1)
84     if len(dash_split[0]) <= 4:
85         # It was something like "2n1l", we need one more split.
86         dash_split = dash_split[1].split(u"-", 1)
87     nic_code = dash_split[0]
88     suite_id = dash_split[1].split(u".", 1)[0]
89     suite_tag = suite_id.rsplit(u"-", 1)[0]
90     for prefix in Constants.FORBIDDEN_SUITE_PREFIX_LIST:
91         if suite_tag.startswith(prefix):
92             suite_tag = suite_tag[len(prefix):]
93     return nic_code, suite_id, suite_tag
94
95
96 def check_suite_tag(suite_tag, prolog):
97     """Verify suite tag occurres once in prolog.
98
99     Call this after all edits are done,
100     to confirm the (edited) suite tag still matches the (edited) suite name.
101
102     Currently, the edited suite tag is expect to be identical
103     to the primary suite tag, but having a function is more flexible.
104
105     The occurences are counted including "| " prefix,
106     to lower the chance to match a comment.
107
108     :param suite_tag: Part of suite name, between NIC driver and suite type.
109     :param prolog: The part of .robot file content without test cases.
110     :type suite_tag: str
111     :type prolog: str
112     :raises ValueError: If suite_tag not found exactly once.
113     """
114     found = prolog.count(u"| " + suite_tag)
115     if found != 1:
116         raise ValueError(f"Suite tag found {found} times for {suite_tag}")
117
118
119 def add_default_testcases(testcase, iface, suite_id, file_out, tc_kwargs_list):
120     """Add default testcases to file.
121
122     :param testcase: Testcase class.
123     :param iface: Interface.
124     :param suite_id: Suite ID.
125     :param file_out: File to write testcases to.
126     :param tc_kwargs_list: Key-value pairs used to construct testcases.
127     :type testcase: Testcase
128     :type iface: str
129     :type suite_id: str
130     :type file_out: file
131     :type tc_kwargs_list: dict
132     """
133     # We bump tc number in any case, so that future enables/disables
134     # do not affect the numbering of other test cases.
135     for num, kwargs in enumerate(tc_kwargs_list, start=1):
136         # TODO: Is there a better way to disable some combinations?
137         emit = True
138         if kwargs[u"frame_size"] == 9000:
139             if u"vic1227" in iface:
140                 # Not supported in HW.
141                 emit = False
142             if u"vic1385" in iface:
143                 # Not supported in HW.
144                 emit = False
145             if u"ipsec" in suite_id:
146                 # IPsec code does not support chained buffers.
147                 # Tracked by Jira ticket VPP-1207.
148                 emit = False
149         if u"-16vm2t-" in suite_id or u"-16dcr2t-" in suite_id:
150             if kwargs[u"phy_cores"] > 3:
151                 # CSIT lab only has 28 (physical) core processors,
152                 # so these test would fail when attempting to assign cores.
153                 emit = False
154         if u"-24vm1t-" in suite_id or u"-24dcr1t-" in suite_id:
155             if kwargs[u"phy_cores"] > 3:
156                 # CSIT lab only has 28 (physical) core processors,
157                 # so these test would fail when attempting to assign cores.
158                 emit = False
159         if u"soak" in suite_id:
160             # Soak test take too long, do not risk other than tc01.
161             if kwargs[u"phy_cores"] != 1:
162                 emit = False
163             if kwargs[u"frame_size"] not in MIN_FRAME_SIZE_VALUES:
164                 emit = False
165         if emit:
166             file_out.write(testcase.generate(num=num, **kwargs))
167
168
169 def add_tcp_testcases(testcase, file_out, tc_kwargs_list):
170     """Add TCP testcases to file.
171
172     :param testcase: Testcase class.
173     :param file_out: File to write testcases to.
174     :param tc_kwargs_list: Key-value pairs used to construct testcases.
175     :type testcase: Testcase
176     :type file_out: file
177     :type tc_kwargs_list: dict
178     """
179     for num, kwargs in enumerate(tc_kwargs_list, start=1):
180         file_out.write(testcase.generate(num=num, **kwargs))
181
182
183 def write_default_files(in_filename, in_prolog, kwargs_list):
184     """Using given filename and prolog, write all generated suites.
185
186     :param in_filename: Template filename to derive real filenames from.
187     :param in_prolog: Template content to derive real content from.
188     :param kwargs_list: List of kwargs for add_default_testcase.
189     :type in_filename: str
190     :type in_prolog: str
191     :type kwargs_list: list of dict
192     """
193     for suite_type in Constants.PERF_TYPE_TO_KEYWORD:
194         tmp_filename = replace_defensively(
195             in_filename, u"ndrpdr", suite_type, 1,
196             u"File name should contain suite type once.", in_filename
197         )
198         tmp_prolog = replace_defensively(
199             in_prolog, u"ndrpdr".upper(), suite_type.upper(), 1,
200             u"Suite type should appear once in uppercase (as tag).",
201             in_filename
202         )
203         tmp_prolog = replace_defensively(
204             tmp_prolog,
205             u"Find NDR and PDR intervals using optimized search",
206             Constants.PERF_TYPE_TO_KEYWORD[suite_type], 1,
207             u"Main search keyword should appear once in suite.",
208             in_filename
209         )
210         tmp_prolog = replace_defensively(
211             tmp_prolog,
212             Constants.PERF_TYPE_TO_SUITE_DOC_VER[u"ndrpdr"],
213             Constants.PERF_TYPE_TO_SUITE_DOC_VER[suite_type],
214             1, u"Exact suite type doc not found.", in_filename
215         )
216         tmp_prolog = replace_defensively(
217             tmp_prolog,
218             Constants.PERF_TYPE_TO_TEMPLATE_DOC_VER[u"ndrpdr"],
219             Constants.PERF_TYPE_TO_TEMPLATE_DOC_VER[suite_type],
220             1, u"Exact template type doc not found.", in_filename
221         )
222         _, suite_id, _ = get_iface_and_suite_ids(tmp_filename)
223         testcase = Testcase.default(suite_id)
224         for nic_name in Constants.NIC_NAME_TO_CODE:
225             tmp2_filename = replace_defensively(
226                 tmp_filename, u"10ge2p1x710",
227                 Constants.NIC_NAME_TO_CODE[nic_name], 1,
228                 u"File name should contain NIC code once.", in_filename
229             )
230             tmp2_prolog = replace_defensively(
231                 tmp_prolog, u"Intel-X710", nic_name, 2,
232                 u"NIC name should appear twice (tag and variable).",
233                 in_filename
234             )
235             if tmp2_prolog.count(u"HW_") == 2:
236                 # TODO CSIT-1481: Crypto HW should be read
237                 #      from topology file instead.
238                 if nic_name in Constants.NIC_NAME_TO_CRYPTO_HW:
239                     tmp2_prolog = replace_defensively(
240                         tmp2_prolog, u"HW_DH895xcc",
241                         Constants.NIC_NAME_TO_CRYPTO_HW[nic_name], 1,
242                         u"HW crypto name should appear.", in_filename
243                     )
244             iface, old_suite_id, old_suite_tag = get_iface_and_suite_ids(
245                 tmp2_filename
246             )
247             if u"DPDK" in in_prolog:
248                 for driver in Constants.DPDK_NIC_NAME_TO_DRIVER[nic_name]:
249                     out_filename = replace_defensively(
250                         tmp2_filename, old_suite_id,
251                         Constants.DPDK_NIC_DRIVER_TO_SUITE_PREFIX[driver] \
252                             + old_suite_id,
253                         1, u"Error adding driver prefix.", in_filename
254                     )
255                     out_prolog = replace_defensively(
256                         tmp2_prolog, u"vfio-pci", driver, 1,
257                         u"Driver name should appear once.", in_filename
258                     )
259                     out_prolog = replace_defensively(
260                         out_prolog,
261                         Constants.DPDK_NIC_DRIVER_TO_TAG[u"vfio-pci"],
262                         Constants.DPDK_NIC_DRIVER_TO_TAG[driver], 1,
263                         u"Driver tag should appear once.", in_filename
264                     )
265                     iface, suite_id, suite_tag = get_iface_and_suite_ids(
266                         out_filename
267                     )
268                     # The next replace is probably a noop, but it is safer to
269                     # maintain the same structure as for other edits.
270                     out_prolog = replace_defensively(
271                         out_prolog, old_suite_tag, suite_tag, 1,
272                         f"Perf suite tag {old_suite_tag} should appear once.",
273                         in_filename
274                     )
275                     check_suite_tag(suite_tag, out_prolog)
276                     # TODO: Reorder loops so suite_id is finalized sooner.
277                     testcase = Testcase.default(suite_id)
278                     with open(out_filename, u"wt") as file_out:
279                         file_out.write(out_prolog)
280                         add_default_testcases(
281                             testcase, iface, suite_id, file_out, kwargs_list
282                         )
283                 continue
284             for driver in Constants.NIC_NAME_TO_DRIVER[nic_name]:
285                 out_filename = replace_defensively(
286                     tmp2_filename, old_suite_id,
287                     Constants.NIC_DRIVER_TO_SUITE_PREFIX[driver] + old_suite_id,
288                     1, u"Error adding driver prefix.", in_filename
289                 )
290                 out_prolog = replace_defensively(
291                     tmp2_prolog, u"vfio-pci", driver, 1,
292                     u"Driver name should appear once.", in_filename
293                 )
294                 out_prolog = replace_defensively(
295                     out_prolog, Constants.NIC_DRIVER_TO_TAG[u"vfio-pci"],
296                     Constants.NIC_DRIVER_TO_TAG[driver], 1,
297                     u"Driver tag should appear once.", in_filename
298                 )
299                 out_prolog = replace_defensively(
300                     out_prolog, Constants.NIC_DRIVER_TO_PLUGINS[u"vfio-pci"],
301                     Constants.NIC_DRIVER_TO_PLUGINS[driver], 1,
302                     u"Driver plugin should appear once.", in_filename
303                 )
304                 out_prolog = replace_defensively(
305                     out_prolog, Constants.NIC_DRIVER_TO_VFS[u"vfio-pci"],
306                     Constants.NIC_DRIVER_TO_VFS[driver], 1,
307                     u"NIC VFs argument should appear once.", in_filename
308                 )
309                 iface, suite_id, suite_tag = get_iface_and_suite_ids(
310                     out_filename
311                 )
312                 # The next replace is probably a noop, but it is safer to
313                 # maintain the same structure as for other edits.
314                 out_prolog = replace_defensively(
315                     out_prolog, old_suite_tag, suite_tag, 1,
316                     f"Perf suite tag {old_suite_tag} should appear once.",
317                     in_filename
318                 )
319                 check_suite_tag(suite_tag, out_prolog)
320                 # TODO: Reorder loops so suite_id is finalized sooner.
321                 testcase = Testcase.default(suite_id)
322                 with open(out_filename, u"wt") as file_out:
323                     file_out.write(out_prolog)
324                     add_default_testcases(
325                         testcase, iface, suite_id, file_out, kwargs_list
326                     )
327
328
329 def write_reconf_files(in_filename, in_prolog, kwargs_list):
330     """Using given filename and prolog, write all generated reconf suites.
331
332     Use this for suite type reconf, as its local template
333     is incompatible with mrr/ndrpdr/soak local template,
334     while test cases are compatible.
335
336     :param in_filename: Template filename to derive real filenames from.
337     :param in_prolog: Template content to derive real content from.
338     :param kwargs_list: List of kwargs for add_testcase.
339     :type in_filename: str
340     :type in_prolog: str
341     :type kwargs_list: list of dict
342     """
343     _, suite_id, _ = get_iface_and_suite_ids(in_filename)
344     testcase = Testcase.default(suite_id)
345     for nic_name in Constants.NIC_NAME_TO_CODE:
346         tmp_filename = replace_defensively(
347             in_filename, u"10ge2p1x710",
348             Constants.NIC_NAME_TO_CODE[nic_name], 1,
349             u"File name should contain NIC code once.", in_filename
350         )
351         tmp_prolog = replace_defensively(
352             in_prolog, u"Intel-X710", nic_name, 2,
353             u"NIC name should appear twice (tag and variable).",
354             in_filename
355         )
356         if tmp_prolog.count(u"HW_") == 2:
357             # TODO CSIT-1481: Crypto HW should be read
358             #      from topology file instead.
359             if nic_name in Constants.NIC_NAME_TO_CRYPTO_HW.keys():
360                 tmp_prolog = replace_defensively(
361                     tmp_prolog, u"HW_DH895xcc",
362                     Constants.NIC_NAME_TO_CRYPTO_HW[nic_name], 1,
363                     u"HW crypto name should appear.", in_filename
364                 )
365         iface, old_suite_id, old_suite_tag = get_iface_and_suite_ids(
366             tmp_filename
367         )
368         for driver in Constants.NIC_NAME_TO_DRIVER[nic_name]:
369             out_filename = replace_defensively(
370                 tmp_filename, old_suite_id,
371                 Constants.NIC_DRIVER_TO_SUITE_PREFIX[driver] + old_suite_id,
372                 1, u"Error adding driver prefix.", in_filename
373             )
374             out_prolog = replace_defensively(
375                 tmp_prolog, u"vfio-pci", driver, 1,
376                 u"Driver name should appear once.", in_filename
377             )
378             out_prolog = replace_defensively(
379                 out_prolog, Constants.NIC_DRIVER_TO_TAG[u"vfio-pci"],
380                 Constants.NIC_DRIVER_TO_TAG[driver], 1,
381                 u"Driver tag should appear once.", in_filename
382             )
383             out_prolog = replace_defensively(
384                 out_prolog, Constants.NIC_DRIVER_TO_PLUGINS[u"vfio-pci"],
385                 Constants.NIC_DRIVER_TO_PLUGINS[driver], 1,
386                 u"Driver plugin should appear once.", in_filename
387             )
388             out_prolog = replace_defensively(
389                 out_prolog, Constants.NIC_DRIVER_TO_VFS[u"vfio-pci"],
390                 Constants.NIC_DRIVER_TO_VFS[driver], 1,
391                 u"NIC VFs argument should appear once.", in_filename
392             )
393             iface, suite_id, suite_tag = get_iface_and_suite_ids(out_filename)
394             out_prolog = replace_defensively(
395                 out_prolog, old_suite_tag, suite_tag, 1,
396                 u"Perf suite tag should appear once.", in_filename
397             )
398             check_suite_tag(suite_tag, out_prolog)
399             # TODO: Reorder loops so suite_id is finalized sooner.
400             testcase = Testcase.default(suite_id)
401             with open(out_filename, u"wt") as file_out:
402                 file_out.write(out_prolog)
403                 add_default_testcases(
404                     testcase, iface, suite_id, file_out, kwargs_list
405                 )
406
407
408 def write_tcp_files(in_filename, in_prolog, kwargs_list):
409     """Using given filename and prolog, write all generated tcp suites.
410
411     TODO: Suport drivers.
412
413     :param in_filename: Template filename to derive real filenames from.
414     :param in_prolog: Template content to derive real content from.
415     :param kwargs_list: List of kwargs for add_default_testcase.
416     :type in_filename: str
417     :type in_prolog: str
418     :type kwargs_list: list of dict
419     """
420     # TODO: Generate rps from cps? There are subtle differences.
421     _, suite_id, suite_tag = get_iface_and_suite_ids(in_filename)
422     testcase = Testcase.tcp(suite_id)
423     for nic_name in Constants.NIC_NAME_TO_CODE:
424         out_filename = replace_defensively(
425             in_filename, u"10ge2p1x710",
426             Constants.NIC_NAME_TO_CODE[nic_name], 1,
427             u"File name should contain NIC code once.", in_filename
428         )
429         out_prolog = replace_defensively(
430             in_prolog, u"Intel-X710", nic_name, 2,
431             u"NIC name should appear twice (tag and variable).",
432             in_filename
433         )
434         check_suite_tag(suite_tag, out_prolog)
435         with open(out_filename, u"wt") as file_out:
436             file_out.write(out_prolog)
437             add_tcp_testcases(testcase, file_out, kwargs_list)
438
439
440 class Regenerator:
441     """Class containing file generating methods."""
442
443     def __init__(self, quiet=True):
444         """Initialize the instance.
445
446         :param quiet: Reduce log prints (to stderr) when True (default).
447         :type quiet: boolean
448         """
449         self.quiet = quiet
450
451     def regenerate_glob(self, pattern, protocol=u"ip4"):
452         """Regenerate files matching glob pattern based on arguments.
453
454         In the current working directory, find all files matching
455         the glob pattern. Use testcase template to regenerate test cases
456         according to suffix, governed by protocol, autonumbering them.
457         Also generate suites for other NICs and drivers.
458
459         Log-like prints are emitted to sys.stderr.
460
461         :param pattern: Glob pattern to select files. Example: *-ndrpdr.robot
462         :param protocol: String determining minimal frame size. Default: "ip4"
463         :type pattern: str
464         :type protocol: str
465         :raises RuntimeError: If invalid source suite is encountered.
466         """
467         if not self.quiet:
468             print(f"Regenerator starts at {getcwd()}", file=sys.stderr)
469
470         min_frame_size = PROTOCOL_TO_MIN_FRAME_SIZE[protocol]
471         default_kwargs_list = [
472             {u"frame_size": min_frame_size, u"phy_cores": 1},
473             {u"frame_size": min_frame_size, u"phy_cores": 2},
474             {u"frame_size": min_frame_size, u"phy_cores": 4},
475             {u"frame_size": 1518, u"phy_cores": 1},
476             {u"frame_size": 1518, u"phy_cores": 2},
477             {u"frame_size": 1518, u"phy_cores": 4},
478             {u"frame_size": 9000, u"phy_cores": 1},
479             {u"frame_size": 9000, u"phy_cores": 2},
480             {u"frame_size": 9000, u"phy_cores": 4},
481             {u"frame_size": u"IMIX_v4_1", u"phy_cores": 1},
482             {u"frame_size": u"IMIX_v4_1", u"phy_cores": 2},
483             {u"frame_size": u"IMIX_v4_1", u"phy_cores": 4}
484         ]
485         hs_wrk_kwargs_list = [
486             {u"frame_size": 0, u"phy_cores": i} for i in (1, 2, 4)
487         ]
488         hs_bps_kwargs_list = [
489             {u"frame_size": 1460, u"phy_cores": 1},
490         ]
491         hs_quic_kwargs_list = [
492             {u"frame_size": 1280, u"phy_cores": 1},
493         ]
494
495         for in_filename in glob(pattern):
496             if not self.quiet:
497                 print(
498                     u"Regenerating in_filename:", in_filename, file=sys.stderr
499                 )
500             iface, _, _ = get_iface_and_suite_ids(in_filename)
501             if not iface.endswith(u"10ge2p1x710"):
502                 raise RuntimeError(
503                     f"Error in {in_filename}: non-primary NIC found."
504                 )
505             for prefix in Constants.FORBIDDEN_SUITE_PREFIX_LIST:
506                 if prefix in in_filename:
507                     raise RuntimeError(
508                         f"Error in {in_filename}: non-primary driver found."
509                     )
510             with open(in_filename, u"rt") as file_in:
511                 in_prolog = u"".join(
512                     file_in.read().partition(u"*** Test Cases ***")[:-1]
513                 )
514             if in_filename.endswith(u"-ndrpdr.robot"):
515                 write_default_files(in_filename, in_prolog, default_kwargs_list)
516             elif in_filename.endswith(u"-reconf.robot"):
517                 write_reconf_files(in_filename, in_prolog, default_kwargs_list)
518             elif in_filename[-10:] in (u"-cps.robot", u"-rps.robot"):
519                 write_tcp_files(in_filename, in_prolog, hs_wrk_kwargs_list)
520             elif in_filename.endswith(u"-bps.robot"):
521                 hoststack_kwargs_list = \
522                     hs_quic_kwargs_list if u"quic" in in_filename \
523                     else hs_bps_kwargs_list
524                 write_tcp_files(in_filename, in_prolog, hoststack_kwargs_list)
525             else:
526                 raise RuntimeError(
527                     f"Error in {in_filename}: non-primary suite type found."
528                 )
529         if not self.quiet:
530             print(u"Regenerator ends.", file=sys.stderr)
531         print(file=sys.stderr)  # To make autogen check output more readable.