Revert "fix(IPsecUtil): Delete keywords no longer used"
[csit.git] / resources / libraries / python / VppApiCrc.py
index d55058e..a8947a1 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2019 Cisco and/or its affiliates.
+# Copyright (c) 2023 Cisco and/or its affiliates.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
@@ -75,8 +75,9 @@ class VppApiCrcChecker:
 
         Starts the same as _expected, but each time an encountered api,crc pair
         fits the expectation, the pair is removed from all collections
-        within this mapping. Ideally, the active mappings will become empty.
-        If not, it is an error, VPP removed or renamed a message CSIT needs."""
+        within this mapping. It is fine if an api is missing
+        from some collections, as long as it is not missing from all collections
+        that remained in _expected."""
 
         self._found = dict()
         """Mapping from API name to CRC string.
@@ -84,6 +85,12 @@ class VppApiCrcChecker:
         This gets populated with CRCs found in .api.json,
         to serve as a hint when reporting errors."""
 
+        self._options = dict()
+        """Mapping from API name to options dictionary.
+
+        This gets populated with options found in .api.json,
+        to serve as a hint when reporting errors."""
+
         self._reported = dict()
         """Mapping from API name to CRC string.
 
@@ -132,7 +139,7 @@ class VppApiCrcChecker:
         file_path = os.path.normpath(os.path.join(
             os.path.dirname(os.path.abspath(__file__)), u"..", u"..",
             u"api", u"vpp", u"supported_crcs.yaml"))
-        with open(file_path, "r") as file_in:
+        with open(file_path, u"rt") as file_in:
             collections_dict = yaml.safe_load(file_in.read())
         for collection_name, name_to_crc_mapping in collections_dict.items():
             self._register_collection(collection_name, name_to_crc_mapping)
@@ -171,7 +178,34 @@ class VppApiCrcChecker:
                 return _str(crc)
         raise RuntimeError(f"No CRC found for message: {msg_obj!r}")
 
-    def _process_crc(self, api_name, crc):
+    @staticmethod
+    def _get_options(msg_obj, version):
+        """Utility function to extract API options from an intermediate json.
+
+        Empty dict is returned if options are not found,
+        so old VPP builds can be tested without spamming.
+        If version starts with "0.", add a fake option,
+        as the message is treated as "in-progress" by the API upgrade process.
+
+        :param msg_obj: Loaded json object, item of "messages" list.
+        :param version: Version string from the .api.json file.
+        :type msg_obj: list of various types
+        :type version: Optional[str]
+        :returns: Object found as value for "options" key.
+        :rtype: dict
+        """
+        options = dict()
+        for item in reversed(msg_obj):
+            if not isinstance(item, dict):
+                continue
+            options = item.get(u"options", dict())
+            if not options:
+                break
+        if version is None or version.startswith(u"0."):
+            options[u"version"] = version
+        return options
+
+    def _process_crc(self, api_name, crc, options):
         """Compare API to verified collections, update class state.
 
         Here, API stands for (message name, CRC) pair.
@@ -195,16 +229,21 @@ class VppApiCrcChecker:
         Attempts to overwrite value in _found or _reported should not happen,
         so the code does not check for that, simply overwriting.
 
+        Options are stored, to be examined later.
+
         The intended usage is to call this method multiple times,
         and then raise exception listing all _reported.
 
         :param api_name: API name to check.
         :param crc: Discovered CRC to check for the name.
+        :param options: Empty dict or options value for in .api.json
         :type api_name: str
         :type crc: str
+        :type options: dict
         """
         # Regardless of the result, remember as found.
         self._found[api_name] = crc
+        self._options[api_name] = options
         old_expected = self._expected
         new_expected = old_expected.copy()
         for collection_name, name_to_crc_mapping in old_expected.items():
@@ -242,13 +281,15 @@ class VppApiCrcChecker:
             for filename in files:
                 if not filename.endswith(u".api.json"):
                     continue
-                with open(f"{root}/{filename}", "r") as file_in:
+                with open(f"{root}/{filename}", u"rt") as file_in:
                     json_obj = json.load(file_in)
+                version = json_obj[u"options"].get(u"version", None)
                 msgs = json_obj[u"messages"]
                 for msg_obj in msgs:
                     msg_name = self._get_name(msg_obj)
                     msg_crc = self._get_crc(msg_obj)
-                    self._process_crc(msg_name, msg_crc)
+                    msg_options = self._get_options(msg_obj, version)
+                    self._process_crc(msg_name, msg_crc, msg_options)
         logger.debug(f"Surviving collections: {self._expected.keys()!r}")
 
     def report_initial_conflicts(self, report_missing=False):
@@ -285,16 +326,21 @@ class VppApiCrcChecker:
         if not report_missing:
             return
         missing = {name: mapp for name, mapp in self._missing.items() if mapp}
-        if missing:
-            missing_indented = json.dumps(
-                missing, indent=1, sort_keys=True, separators=[u",", u":"])
-            self.log_and_raise(
-                f"API CRCs missing from .api.json:\n{missing_indented}"
-            )
+        if set(missing.keys()) < set(self._expected.keys()):
+            # There is a collection where nothing is missing.
+            return
+        missing_indented = json.dumps(
+            missing, indent=1, sort_keys=True, separators=[u",", u":"]
+        )
+        self.log_and_raise(
+            f"API CRCs missing from .api.json:\n{missing_indented}"
+        )
 
     def check_api_name(self, api_name):
         """Fail if the api_name has no, or different from known CRC associated.
 
+        Print warning if options contain anything more than vat_help.
+
         Do not fail if this particular failure has been already reported.
 
         Intended use: Call during test (not in initialization),
@@ -328,9 +374,30 @@ class VppApiCrcChecker:
                 if name_to_crc_mapping[api_name] == crc:
                     matching = True
                     break
-        if matching:
+        if not matching:
+            self._reported[api_name] = crc
+            self.log_and_raise(
+                f"No active collection has API {api_name!r} with CRC {crc!r}"
+            )
+        options = self._options.get(api_name, None)
+        if not options:
+            # None means CSIT is attempting a new API on an old VPP build.
+            # If that is an issue, the API has been reported as missing already.
             return
-        self._reported[api_name] = crc
-        self.log_and_raise(
-            f"No active collection contains API {api_name!r} with CRC {crc!r}"
-        )
+        options.pop(u"vat_help", None)
+        if options:
+            self._reported[api_name] = crc
+            logger.console(f"{api_name} used but has options {options}")
+
+    def print_warnings(self):
+        """Call check_api_name for API names in surviving collections.
+
+        Useful for VPP CRC checking job.
+        The API name is only checked when it appears
+        in all surviving collections.
+        """
+        api_name_to_crc_maps = self._expected.values()
+        api_name_sets = (set(n2c.keys()) for n2c in api_name_to_crc_maps)
+        api_names = set.intersection(*api_name_sets)
+        for api_name in sorted(api_names):
+            self.check_api_name(api_name)