From 3be56ea13958b9c24cb256a5760a967def563406 Mon Sep 17 00:00:00 2001
From: Gabriel Ferreira <gabrielcarvfer@gmail.com>
Date: Fri, 15 Dec 2023 13:27:16 -0300
Subject: [PATCH] tests: update test-ns3.py dependencies test to find minimal
 set of modules to link (excluding implicitly linked)

---
 utils/tests/test-ns3.py | 92 +++++++++++++++++++++++++++++++++--------
 1 file changed, 74 insertions(+), 18 deletions(-)

diff --git a/utils/tests/test-ns3.py b/utils/tests/test-ns3.py
index 84bd0249d38..0f0e3259107 100755
--- a/utils/tests/test-ns3.py
+++ b/utils/tests/test-ns3.py
@@ -417,11 +417,15 @@ def test_01_CheckIfIncludedHeadersMatchLinkedModules(self):
 
             # Separate list of source files and header files
             for line in cmake_contents:
-                base_name = os.path.basename(line[:-1])
-                if not os.path.exists(os.path.join(path, line.strip())):
+                source_file_path = re.findall(r"\b(?:[^\s]+\.[ch]{1,2})\b", line.strip())
+                if not source_file_path:
+                    continue
+                source_file_path = source_file_path[0]
+                base_name = os.path.basename(source_file_path)
+                if not os.path.exists(os.path.join(path, source_file_path)):
                     continue
 
-                if ".h" in line:
+                if ".h" in source_file_path:
                     # Register all module headers as module headers and sources
                     modules[module_name_nodir]["headers"].add(base_name)
                     modules[module_name_nodir]["sources"].add(base_name)
@@ -429,13 +433,13 @@ def test_01_CheckIfIncludedHeadersMatchLinkedModules(self):
                     # Register the header as part of the current module
                     headers_to_modules[base_name] = module_name_nodir
 
-                if ".cc" in line:
+                if ".cc" in source_file_path:
                     # Register the source file as part of the current module
                     modules[module_name_nodir]["sources"].add(base_name)
 
-                if ".cc" in line or ".h" in line:
+                if ".cc" in source_file_path or ".h" in source_file_path:
                     # Extract includes from headers and source files and then add to a list of included headers
-                    source_file = os.path.join(ns3_path, module_name, line.strip())
+                    source_file = os.path.join(ns3_path, module_name, source_file_path)
                     with open(source_file, "r", encoding="utf-8") as f:
                         source_contents = f.read()
                     modules[module_name_nodir]["included_headers"].update(
@@ -448,7 +452,14 @@ def test_01_CheckIfIncludedHeadersMatchLinkedModules(self):
 
             # Extract libraries linked to the module
             modules[module_name_nodir]["libraries"].update(
-                re.findall("\\${lib(.*)}", "".join(cmake_contents))
+                re.findall("\${lib(.*?)}", "".join(cmake_contents))
+            )
+            modules[module_name_nodir]["libraries"] = list(
+                filter(
+                    lambda x: x
+                    not in ["raries_to_link", module_name_nodir, module_name_nodir + "-obj"],
+                    modules[module_name_nodir]["libraries"],
+                )
             )
 
         # Now that we have all the information we need, check if we have all the included libraries linked
@@ -466,17 +477,62 @@ def test_01_CheckIfIncludedHeadersMatchLinkedModules(self):
             ).difference({module})
 
             diff = modules[module]["included_libraries"].difference(modules[module]["libraries"])
-            if len(diff) > 0:
-                print(
-                    "Module %s includes modules that are not linked: %s"
-                    % (module, ", ".join(list(diff))),
-                    file=sys.stderr,
-                )
-                sys.stderr.flush()
-            # Uncomment this to turn into a real test
-            # self.assertEqual(len(diff), 0,
-            #                 msg="Module %s includes modules that are not linked: %s" % (module, ", ".join(list(diff)))
-            #                 )
+
+        # Find graph with least amount of edges based on included_libraries
+        def recursive_check_dependencies(checked_module):
+            # Remove direct explicit dependencies
+            for module_to_link in modules[checked_module]["included_libraries"]:
+                modules[checked_module]["included_libraries"] = set(
+                    modules[checked_module]["included_libraries"]
+                ) - set(modules[module_to_link]["included_libraries"])
+
+            for module_to_link in modules[checked_module]["included_libraries"]:
+                recursive_check_dependencies(module_to_link)
+
+            # Remove unnecessary implicit dependencies
+            def is_implicitly_linked(searched_module, current_module):
+                if len(modules[current_module]["included_libraries"]) == 0:
+                    return False
+                if searched_module in modules[current_module]["included_libraries"]:
+                    return True
+                for module in modules[current_module]["included_libraries"]:
+                    if is_implicitly_linked(searched_module, module):
+                        return True
+                return False
+
+            from itertools import combinations
+
+            implicitly_linked = set()
+            for dep1, dep2 in combinations(modules[checked_module]["included_libraries"], 2):
+                if is_implicitly_linked(dep1, dep2):
+                    implicitly_linked.add(dep1)
+                if is_implicitly_linked(dep2, dep1):
+                    implicitly_linked.add(dep2)
+
+            modules[checked_module]["included_libraries"] = (
+                set(modules[checked_module]["included_libraries"]) - implicitly_linked
+            )
+
+        for module in modules:
+            recursive_check_dependencies(module)
+
+        # Print findings
+        for module in sorted(modules):
+            if module == "test":
+                continue
+            minimal_linking_set = ", ".join(modules[module]["included_libraries"])
+            unnecessarily_linked = ", ".join(
+                set(modules[module]["libraries"]) - set(modules[module]["included_libraries"])
+            )
+            missing_linked = ", ".join(
+                set(modules[module]["included_libraries"]) - set(modules[module]["libraries"])
+            )
+            if unnecessarily_linked:
+                print(f"Module '{module}' unnecessarily linked: {unnecessarily_linked}.")
+            if missing_linked:
+                print(f"Module '{module}' missing linked: {missing_linked}.")
+            if unnecessarily_linked or missing_linked:
+                print(f"Module '{module}' minimal linking set: {minimal_linking_set}.")
         self.assertTrue(True)