FIX: Naming convention compliance
[csit.git] / resources / libraries / python / autogen / Regenerator.py
1 # Copyright (c) 2021 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     for kwargs in tc_kwargs_list:
134         # TODO: Is there a better way to disable some combinations?
135         emit = True
136         if kwargs[u"frame_size"] == 9000:
137             if u"vic1227" in iface:
138                 # Not supported in HW.
139                 emit = False
140             if u"vic1385" in iface:
141                 # Not supported in HW.
142                 emit = False
143         if u"-16vm2t-" in suite_id or u"-16dcr2t-" in suite_id:
144             if kwargs[u"phy_cores"] > 3:
145                 # CSIT lab only has 28 (physical) core processors,
146                 # so these test would fail when attempting to assign cores.
147                 emit = False
148         if u"-24vm1t-" in suite_id or u"-24dcr1t-" in suite_id:
149             if kwargs[u"phy_cores"] > 3:
150                 # CSIT lab only has 28 (physical) core processors,
151                 # so these test would fail when attempting to assign cores.
152                 emit = False
153         if u"soak" in suite_id:
154             # Soak test take too long, do not risk other than tc01.
155             if kwargs[u"phy_cores"] != 1:
156                 emit = False
157             if kwargs[u"frame_size"] not in MIN_FRAME_SIZE_VALUES:
158                 emit = False
159         if (
160             u"-cps-" in suite_id
161             or u"-pps-" in suite_id
162             or u"-tput-" in suite_id
163         ):
164             if kwargs[u"frame_size"] not in MIN_FRAME_SIZE_VALUES:
165                 emit = False
166         if emit:
167             file_out.write(testcase.generate(**kwargs))
168
169
170 def add_tcp_testcases(testcase, file_out, tc_kwargs_list):
171     """Add TCP testcases to file.
172
173     :param testcase: Testcase class.
174     :param file_out: File to write testcases to.
175     :param tc_kwargs_list: Key-value pairs used to construct testcases.
176     :type testcase: Testcase
177     :type file_out: file
178     :type tc_kwargs_list: dict
179     """
180     for kwargs in tc_kwargs_list:
181         file_out.write(testcase.generate(**kwargs))
182
183
184 def add_iperf3_testcases(testcase, file_out, tc_kwargs_list):
185     """Add iperf3 testcases to file.
186
187     :param testcase: Testcase class.
188     :param file_out: File to write testcases to.
189     :param tc_kwargs_list: Key-value pairs used to construct testcases.
190     :type testcase: Testcase
191     :type file_out: file
192     :type tc_kwargs_list: dict
193     """
194     for kwargs in tc_kwargs_list:
195         file_out.write(testcase.generate(**kwargs))
196
197
198 def write_default_files(in_filename, in_prolog, kwargs_list):
199     """Using given filename and prolog, write all generated suites.
200
201     :param in_filename: Template filename to derive real filenames from.
202     :param in_prolog: Template content to derive real content from.
203     :param kwargs_list: List of kwargs for add_default_testcase.
204     :type in_filename: str
205     :type in_prolog: str
206     :type kwargs_list: list of dict
207     """
208     for suite_type in Constants.PERF_TYPE_TO_KEYWORD:
209         tmp_filename = replace_defensively(
210             in_filename, u"ndrpdr", suite_type, 1,
211             u"File name should contain suite type once.", in_filename
212         )
213         tmp_prolog = replace_defensively(
214             in_prolog, u"ndrpdr".upper(), suite_type.upper(), 1,
215             u"Suite type should appear once in uppercase (as tag).",
216             in_filename
217         )
218         tmp_prolog = replace_defensively(
219             tmp_prolog,
220             u"Find NDR and PDR intervals using optimized search",
221             Constants.PERF_TYPE_TO_KEYWORD[suite_type], 1,
222             u"Main search keyword should appear once in suite.",
223             in_filename
224         )
225         tmp_prolog = replace_defensively(
226             tmp_prolog,
227             Constants.PERF_TYPE_TO_SUITE_DOC_VER[u"ndrpdr"],
228             Constants.PERF_TYPE_TO_SUITE_DOC_VER[suite_type],
229             1, u"Exact suite type doc not found.", in_filename
230         )
231         tmp_prolog = replace_defensively(
232             tmp_prolog,
233             Constants.PERF_TYPE_TO_TEMPLATE_DOC_VER[u"ndrpdr"],
234             Constants.PERF_TYPE_TO_TEMPLATE_DOC_VER[suite_type],
235             1, u"Exact template type doc not found.", in_filename
236         )
237         _, suite_id, _ = get_iface_and_suite_ids(tmp_filename)
238         testcase = Testcase.default(suite_id)
239         for nic_name in Constants.NIC_NAME_TO_CODE:
240             tmp2_filename = replace_defensively(
241                 tmp_filename, u"10ge2p1x710",
242                 Constants.NIC_NAME_TO_CODE[nic_name], 1,
243                 u"File name should contain NIC code once.", in_filename
244             )
245             tmp2_prolog = replace_defensively(
246                 tmp_prolog, u"Intel-X710", nic_name, 2,
247                 u"NIC name should appear twice (tag and variable).",
248                 in_filename
249             )
250             if tmp2_prolog.count(u"HW_") == 2:
251                 # TODO CSIT-1481: Crypto HW should be read
252                 #      from topology file instead.
253                 if nic_name in Constants.NIC_NAME_TO_CRYPTO_HW:
254                     tmp2_prolog = replace_defensively(
255                         tmp2_prolog, u"HW_DH895xcc",
256                         Constants.NIC_NAME_TO_CRYPTO_HW[nic_name], 1,
257                         u"HW crypto name should appear.", in_filename
258                     )
259             iface, old_suite_id, old_suite_tag = get_iface_and_suite_ids(
260                 tmp2_filename
261             )
262             if u"DPDK" in in_prolog:
263                 for driver in Constants.DPDK_NIC_NAME_TO_DRIVER[nic_name]:
264                     out_filename = replace_defensively(
265                         tmp2_filename, old_suite_id,
266                         Constants.DPDK_NIC_DRIVER_TO_SUITE_PREFIX[driver] \
267                             + old_suite_id,
268                         1, u"Error adding driver prefix.", in_filename
269                     )
270                     out_prolog = replace_defensively(
271                         tmp2_prolog, u"vfio-pci", driver, 1,
272                         u"Driver name should appear once.", in_filename
273                     )
274                     out_prolog = replace_defensively(
275                         out_prolog,
276                         Constants.DPDK_NIC_DRIVER_TO_TAG[u"vfio-pci"],
277                         Constants.DPDK_NIC_DRIVER_TO_TAG[driver], 1,
278                         u"Driver tag should appear once.", in_filename
279                     )
280                     iface, suite_id, suite_tag = get_iface_and_suite_ids(
281                         out_filename
282                     )
283                     # The next replace is probably a noop, but it is safer to
284                     # maintain the same structure as for other edits.
285                     out_prolog = replace_defensively(
286                         out_prolog, old_suite_tag, suite_tag, 1,
287                         f"Perf suite tag {old_suite_tag} should appear once.",
288                         in_filename
289                     )
290                     check_suite_tag(suite_tag, out_prolog)
291                     # TODO: Reorder loops so suite_id is finalized sooner.
292                     testcase = Testcase.default(suite_id)
293                     with open(out_filename, u"wt") as file_out:
294                         file_out.write(out_prolog)
295                         add_default_testcases(
296                             testcase, iface, suite_id, file_out, kwargs_list
297                         )
298                 continue
299             for driver in Constants.NIC_NAME_TO_DRIVER[nic_name]:
300                 out_filename = replace_defensively(
301                     tmp2_filename, old_suite_id,
302                     Constants.NIC_DRIVER_TO_SUITE_PREFIX[driver] + old_suite_id,
303                     1, u"Error adding driver prefix.", in_filename
304                 )
305                 out_prolog = replace_defensively(
306                     tmp2_prolog, u"vfio-pci", driver, 1,
307                     u"Driver name should appear once.", in_filename
308                 )
309                 out_prolog = replace_defensively(
310                     out_prolog, Constants.NIC_DRIVER_TO_TAG[u"vfio-pci"],
311                     Constants.NIC_DRIVER_TO_TAG[driver], 1,
312                     u"Driver tag should appear once.", in_filename
313                 )
314                 out_prolog = replace_defensively(
315                     out_prolog, Constants.NIC_DRIVER_TO_PLUGINS[u"vfio-pci"],
316                     Constants.NIC_DRIVER_TO_PLUGINS[driver], 1,
317                     u"Driver plugin should appear once.", in_filename
318                 )
319                 out_prolog = replace_defensively(
320                     out_prolog, Constants.NIC_DRIVER_TO_VFS[u"vfio-pci"],
321                     Constants.NIC_DRIVER_TO_VFS[driver], 1,
322                     u"NIC VFs argument should appear once.", in_filename
323                 )
324                 iface, suite_id, suite_tag = get_iface_and_suite_ids(
325                     out_filename
326                 )
327                 # The next replace is probably a noop, but it is safer to
328                 # maintain the same structure as for other edits.
329                 out_prolog = replace_defensively(
330                     out_prolog, old_suite_tag, suite_tag, 1,
331                     f"Perf suite tag {old_suite_tag} should appear once.",
332                     in_filename
333                 )
334                 check_suite_tag(suite_tag, out_prolog)
335                 # TODO: Reorder loops so suite_id is finalized sooner.
336                 testcase = Testcase.default(suite_id)
337                 with open(out_filename, u"wt") as file_out:
338                     file_out.write(out_prolog)
339                     add_default_testcases(
340                         testcase, iface, suite_id, file_out, kwargs_list
341                     )
342
343
344 def write_reconf_files(in_filename, in_prolog, kwargs_list):
345     """Using given filename and prolog, write all generated reconf suites.
346
347     Use this for suite type reconf, as its local template
348     is incompatible with mrr/ndrpdr/soak local template,
349     while test cases are compatible.
350
351     :param in_filename: Template filename to derive real filenames from.
352     :param in_prolog: Template content to derive real content from.
353     :param kwargs_list: List of kwargs for add_testcase.
354     :type in_filename: str
355     :type in_prolog: str
356     :type kwargs_list: list of dict
357     """
358     _, suite_id, _ = get_iface_and_suite_ids(in_filename)
359     testcase = Testcase.default(suite_id)
360     for nic_name in Constants.NIC_NAME_TO_CODE:
361         tmp_filename = replace_defensively(
362             in_filename, u"10ge2p1x710",
363             Constants.NIC_NAME_TO_CODE[nic_name], 1,
364             u"File name should contain NIC code once.", in_filename
365         )
366         tmp_prolog = replace_defensively(
367             in_prolog, u"Intel-X710", nic_name, 2,
368             u"NIC name should appear twice (tag and variable).",
369             in_filename
370         )
371         if tmp_prolog.count(u"HW_") == 2:
372             # TODO CSIT-1481: Crypto HW should be read
373             #      from topology file instead.
374             if nic_name in Constants.NIC_NAME_TO_CRYPTO_HW.keys():
375                 tmp_prolog = replace_defensively(
376                     tmp_prolog, u"HW_DH895xcc",
377                     Constants.NIC_NAME_TO_CRYPTO_HW[nic_name], 1,
378                     u"HW crypto name should appear.", in_filename
379                 )
380         iface, old_suite_id, old_suite_tag = get_iface_and_suite_ids(
381             tmp_filename
382         )
383         for driver in Constants.NIC_NAME_TO_DRIVER[nic_name]:
384             out_filename = replace_defensively(
385                 tmp_filename, old_suite_id,
386                 Constants.NIC_DRIVER_TO_SUITE_PREFIX[driver] + old_suite_id,
387                 1, u"Error adding driver prefix.", in_filename
388             )
389             out_prolog = replace_defensively(
390                 tmp_prolog, u"vfio-pci", driver, 1,
391                 u"Driver name should appear once.", in_filename
392             )
393             out_prolog = replace_defensively(
394                 out_prolog, Constants.NIC_DRIVER_TO_TAG[u"vfio-pci"],
395                 Constants.NIC_DRIVER_TO_TAG[driver], 1,
396                 u"Driver tag should appear once.", in_filename
397             )
398             out_prolog = replace_defensively(
399                 out_prolog, Constants.NIC_DRIVER_TO_PLUGINS[u"vfio-pci"],
400                 Constants.NIC_DRIVER_TO_PLUGINS[driver], 1,
401                 u"Driver plugin should appear once.", in_filename
402             )
403             out_prolog = replace_defensively(
404                 out_prolog, Constants.NIC_DRIVER_TO_VFS[u"vfio-pci"],
405                 Constants.NIC_DRIVER_TO_VFS[driver], 1,
406                 u"NIC VFs argument should appear once.", in_filename
407             )
408             iface, suite_id, suite_tag = get_iface_and_suite_ids(out_filename)
409             out_prolog = replace_defensively(
410                 out_prolog, old_suite_tag, suite_tag, 1,
411                 u"Perf suite tag should appear once.", in_filename
412             )
413             check_suite_tag(suite_tag, out_prolog)
414             # TODO: Reorder loops so suite_id is finalized sooner.
415             testcase = Testcase.default(suite_id)
416             with open(out_filename, u"wt") as file_out:
417                 file_out.write(out_prolog)
418                 add_default_testcases(
419                     testcase, iface, suite_id, file_out, kwargs_list
420                 )
421
422
423 def write_tcp_files(in_filename, in_prolog, kwargs_list):
424     """Using given filename and prolog, write all generated tcp suites.
425
426     TODO: Suport drivers.
427
428     :param in_filename: Template filename to derive real filenames from.
429     :param in_prolog: Template content to derive real content from.
430     :param kwargs_list: List of kwargs for add_default_testcase.
431     :type in_filename: str
432     :type in_prolog: str
433     :type kwargs_list: list of dict
434     """
435     # TODO: Generate rps from cps? There are subtle differences.
436     _, suite_id, suite_tag = get_iface_and_suite_ids(in_filename)
437     testcase = Testcase.tcp(suite_id)
438     for nic_name in Constants.NIC_NAME_TO_CODE:
439         out_filename = replace_defensively(
440             in_filename, u"10ge2p1x710",
441             Constants.NIC_NAME_TO_CODE[nic_name], 1,
442             u"File name should contain NIC code once.", in_filename
443         )
444         out_prolog = replace_defensively(
445             in_prolog, u"Intel-X710", nic_name, 2,
446             u"NIC name should appear twice (tag and variable).",
447             in_filename
448         )
449         check_suite_tag(suite_tag, out_prolog)
450         with open(out_filename, u"wt") as file_out:
451             file_out.write(out_prolog)
452             add_tcp_testcases(testcase, file_out, kwargs_list)
453
454
455 def write_iperf3_files(in_filename, in_prolog, kwargs_list):
456     """Using given filename and prolog, write all generated iperf3 suites.
457
458     :param in_filename: Template filename to derive real filenames from.
459     :param in_prolog: Template content to derive real content from.
460     :param kwargs_list: List of kwargs for add_default_testcase.
461     :type in_filename: str
462     :type in_prolog: str
463     :type kwargs_list: list of dict
464     """
465     _, suite_id, suite_tag = get_iface_and_suite_ids(in_filename)
466     testcase = Testcase.iperf3(suite_id)
467     out_filename = replace_defensively(
468         in_filename, u"10ge2p1x710",
469         Constants.NIC_NAME_TO_CODE[u"Intel-X710"], 1,
470         u"File name should contain NIC code once.", in_filename
471     )
472     out_prolog = replace_defensively(
473         in_prolog, u"Intel-X710", u"Intel-X710", 2,
474         u"NIC name should appear twice (tag and variable).",
475         in_filename
476     )
477     check_suite_tag(suite_tag, out_prolog)
478     with open(out_filename, u"wt") as file_out:
479         file_out.write(out_prolog)
480         add_iperf3_testcases(testcase, file_out, kwargs_list)
481
482
483 class Regenerator:
484     """Class containing file generating methods."""
485
486     def __init__(self, quiet=True):
487         """Initialize the instance.
488
489         :param quiet: Reduce log prints (to stderr) when True (default).
490         :type quiet: boolean
491         """
492         self.quiet = quiet
493
494     def regenerate_glob(self, pattern, protocol=u"ip4"):
495         """Regenerate files matching glob pattern based on arguments.
496
497         In the current working directory, find all files matching
498         the glob pattern. Use testcase template to regenerate test cases
499         according to suffix, governed by protocol, autonumbering them.
500         Also generate suites for other NICs and drivers.
501
502         Log-like prints are emitted to sys.stderr.
503
504         :param pattern: Glob pattern to select files. Example: *-ndrpdr.robot
505         :param protocol: String determining minimal frame size. Default: "ip4"
506         :type pattern: str
507         :type protocol: str
508         :raises RuntimeError: If invalid source suite is encountered.
509         """
510         if not self.quiet:
511             print(f"Regenerator starts at {getcwd()}", file=sys.stderr)
512
513         min_frame_size = PROTOCOL_TO_MIN_FRAME_SIZE[protocol]
514         default_kwargs_list = [
515             {u"frame_size": min_frame_size, u"phy_cores": 1},
516             {u"frame_size": min_frame_size, u"phy_cores": 2},
517             {u"frame_size": min_frame_size, u"phy_cores": 4},
518             {u"frame_size": 1518, u"phy_cores": 1},
519             {u"frame_size": 1518, u"phy_cores": 2},
520             {u"frame_size": 1518, u"phy_cores": 4},
521             {u"frame_size": 9000, u"phy_cores": 1},
522             {u"frame_size": 9000, u"phy_cores": 2},
523             {u"frame_size": 9000, u"phy_cores": 4},
524             {u"frame_size": u"IMIX_v4_1", u"phy_cores": 1},
525             {u"frame_size": u"IMIX_v4_1", u"phy_cores": 2},
526             {u"frame_size": u"IMIX_v4_1", u"phy_cores": 4}
527         ]
528         hs_bps_kwargs_list = [
529             {u"frame_size": 1460, u"phy_cores": 1},
530         ]
531         hs_quic_kwargs_list = [
532             {u"frame_size": 1280, u"phy_cores": 1},
533         ]
534         iperf3_kwargs_list = [
535             {u"frame_size": 128000, u"phy_cores": 1},
536             {u"frame_size": 128000, u"phy_cores": 2},
537             {u"frame_size": 128000, u"phy_cores": 4}
538         ]
539
540         for in_filename in glob(pattern):
541             if not self.quiet:
542                 print(
543                     u"Regenerating in_filename:", in_filename, file=sys.stderr
544                 )
545             iface, _, _ = get_iface_and_suite_ids(in_filename)
546             if not iface.endswith(u"10ge2p1x710"):
547                 raise RuntimeError(
548                     f"Error in {in_filename}: non-primary NIC found."
549                 )
550             for prefix in Constants.FORBIDDEN_SUITE_PREFIX_LIST:
551                 if prefix in in_filename:
552                     raise RuntimeError(
553                         f"Error in {in_filename}: non-primary driver found."
554                     )
555             with open(in_filename, u"rt") as file_in:
556                 in_prolog = u"".join(
557                     file_in.read().partition(u"*** Test Cases ***")[:-1]
558                 )
559             if in_filename.endswith(u"-ndrpdr.robot"):
560                 write_default_files(in_filename, in_prolog, default_kwargs_list)
561             elif in_filename.endswith(u"-reconf.robot"):
562                 write_reconf_files(in_filename, in_prolog, default_kwargs_list)
563             elif in_filename.endswith(u"-bps.robot"):
564                 hoststack_kwargs_list = \
565                     hs_quic_kwargs_list if u"quic" in in_filename \
566                     else hs_bps_kwargs_list
567                 write_tcp_files(in_filename, in_prolog, hoststack_kwargs_list)
568             elif in_filename.endswith(u"-iperf3-mrr.robot"):
569                 write_iperf3_files(in_filename, in_prolog, iperf3_kwargs_list)
570             else:
571                 raise RuntimeError(
572                     f"Error in {in_filename}: non-primary suite type found."
573                 )
574         if not self.quiet:
575             print(u"Regenerator ends.", file=sys.stderr)
576         print(file=sys.stderr)  # To make autogen check output more readable.