From 78465deb3497a4814feb15f42a0c95390e241886 Mon Sep 17 00:00:00 2001
From: Tim Jammer <tim.jammer@sc.tu-darmstadt.de>
Date: Wed, 18 Jun 2025 17:03:08 +0200
Subject: [PATCH] added coverage for parcoach

---
 scripts/tools/parcoach.py | 193 ++++++++++++++++++++++++--------------
 1 file changed, 122 insertions(+), 71 deletions(-)

diff --git a/scripts/tools/parcoach.py b/scripts/tools/parcoach.py
index a40049f7..4663c9c3 100644
--- a/scripts/tools/parcoach.py
+++ b/scripts/tools/parcoach.py
@@ -2,85 +2,138 @@ import re
 import os
 from MBButils import *
 
+
 class Tool(AbstractTool):
     def identify(self):
         return "PARCOACH wrapper"
 
     def ensure_image(self):
-        AbstractTool.ensure_image(self, "-x parcoach")
+        #AbstractTool.ensure_image(self, "-x parcoach")
+        pass
 
     def build(self, rootdir, cached=True):
-        if cached and os.path.exists(f"/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/bin/parcoachcc"):
-            print("No need to rebuild PARCOACH.")
-            os.environ['PATH'] = os.environ['PATH'] + f":/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/bin/"
-            os.environ['OMPI_CC'] = "clang-15"
-            return
-
-        here = os.getcwd() # Save where we were
-        os.chdir(rootdir)
-        subprocess.run(f"wget https://gitlab.inria.fr/api/v4/projects/12320/packages/generic/parcoach/2.4.0/parcoach-2.4.0-shared-Linux.tar.gz", shell=True, check=True)
-        subprocess.run(f"tar xfz parcoach-*.tar.gz", shell=True, check=True)
-        if not os.path.exists("/usr/lib/llvm-15/bin/clang"):
-            subprocess.run("ln -s $(which clang) /usr/lib/llvm-15/bin/clang", shell=True, check=True)
-
-        # Go to where we want to install it, and build it out-of-tree (we're in the docker)
-        subprocess.run(f"rm -rf /tmp/build-parcoach/parcoach-2.4.0-shared-Linux/ && mkdir -p /tmp/build-parcoach/", shell=True, check=True)
-        subprocess.run(f"mv parcoach-*/ /tmp/build-parcoach/ ", shell=True, check=True)
-        os.environ['PATH'] = os.environ['PATH'] + f":/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/bin/"
+        # nothing to do
+        pass
+
+    def setup(self):
+        # os.environ['PATH'] = os.environ['PATH'] + f":/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/bin"
         os.environ['OMPI_CC'] = "clang-15"
+        result_parcoach = subprocess.run(
+            "which parcoach", shell=True, check=True, capture_output=True, text=True
+        )
+        self.parcoach_exe = result_parcoach.stdout.strip()
 
-        # Back to our previous directory
-        os.chdir(here)
+        clang_19_bin = os.environ["LLVM_19_BIN"]
+        self.llvm_cov = clang_19_bin + "/llvm-cov"
+        self.llvm_profdata = clang_19_bin + "/llvm-profdata"
+        assert os.path.isfile(self.llvm_cov)
+        assert os.path.isfile(self.llvm_profdata)
 
-    def setup(self): 
-        os.environ['PATH'] = os.environ['PATH'] + f":/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/bin"
-        os.environ['OMPI_CC'] = "clang-15"
+        self.llvm_profile_folder = os.environ["LLVM_PROFILE_FOLDER"]
+        assert os.path.isdir(self.llvm_profile_folder)
+        self.coverage_profile = "parcoach_profile.pro"
+        self.coverage_report_html = "parcoach_coverage_html"
 
+        self.coverage_source = os.path.dirname(self.parcoach_exe)+"/../src/"
 
     def run(self, execcmd, filename, binary, id, number, timeout, batchinfo, loglevel=logging.INFO):
+
+        self.setup()
+
         os.environ['PATH'] = os.environ['PATH'] + f":/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/bin"
         os.environ['OMPI_CC'] = "clang-15"
         os.environ['OMPI_ALLOW_RUN_AS_ROOT'] = "1"
         os.environ['OMPI_ALLOW_RUN_AS_ROOT_CONFIRM'] = "1"
         cachefile = f'{binary}_{id}'
+        current_env = os.environ.copy()
+        base_file = os.path.basename(filename)
+        current_env["LLVM_PROFILE_FILE"] = f"{self.llvm_profile_folder}/{base_file}-mbb{number}.profraw"
         execcmd = re.sub('\${EXE}', f'./{binary}', execcmd)
-        if filename.find('lock')>=0 or filename.find('fence')>=0:
+        if filename.find('lock') >= 0 or filename.find('fence') >= 0:
             self.run_cmd(
-                    #buildcmd=f"parcoachcc -check=rma --args mpicc {filename} -c -o {binary}.o",
-                    buildcmd=f"parcoachcc -check=rma --args mpicc {filename} -c -o {binary}.o \n mpicc {binary}.o -o {binary} -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/ -lParcoachRMADynamic_MPI_C",
-                    execcmd=f"mpicc {binary}.o -lParcoachRMADynamic_MPI_C -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/",
-                    #execcmd=execcmd,
-                    cachefile=cachefile,
-                    filename=filename,
-                    number=number,
-                    binary=binary,
-                    timeout=timeout,
-                    batchinfo=batchinfo,
-                    loglevel=loglevel)
+                # buildcmd=f"parcoachcc -check=rma --args mpicc {filename} -c -o {binary}.o",
+                # buildcmd=f"parcoachcc -check=rma --args mpicc {filename} -c -o {binary}.o \n mpicc {binary}.o -o {binary} -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/ -lParcoachRMADynamic_MPI_C",
+                # execcmd=f"mpicc {binary}.o -lParcoachRMADynamic_MPI_C -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/",
+                buildcmd=f"mpicc {filename} -S -emit-llvm -o {binary}.ir",
+                execcmd=f"{self.parcoach_exe} --check=rma {binary}.ir ",
+                # execcmd=execcmd,
+                cachefile=cachefile,
+                filename=filename,
+                number=number,
+                binary=binary,
+                timeout=timeout,
+                batchinfo=batchinfo,
+                loglevel=loglevel,
+                current_env=current_env, )
         else:
             self.run_cmd(
-                    #buildcmd=f"parcoachcc -instrum-inter --args mpicc {filename} -c -o {binary}.o",
-                    buildcmd=f"parcoachcc -instrum-inter --args mpicc {filename} -c -o {binary}.o \n mpicc {binary}.o -o {binary} -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/ -lParcoachCollDynamic_MPI_C",
-                    execcmd=f"mpicc {binary}.o -lParcoachCollDynamic_MPI_C -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/",
-                    #execcmd=execcmd,
-                    cachefile=cachefile,
-                    filename=filename,
-                    number=number,
-                    binary=binary,
-                    timeout=timeout,
-                    batchinfo=batchinfo,
-                    loglevel=loglevel)
-
-
+                # buildcmd=f"parcoachcc -instrum-inter --args mpicc {filename} -c -o {binary}.o",
+                # buildcmd=f"parcoachcc -instrum-inter --args mpicc {filename} -c -o {binary}.o \n mpicc {binary}.o -o {binary} -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/ -lParcoachCollDynamic_MPI_C",
+                buildcmd=f"mpicc {filename} -S -emit-llvm -o {binary}.ir",
+                execcmd=f"{self.parcoach_exe} --check=mpi {binary}.ir ",
+                # execcmd=f"mpicc {binary}.o -lParcoachCollDynamic_MPI_C -L/tmp/build-parcoach/parcoach-2.4.0-shared-Linux/lib/",
+                # execcmd=execcmd,
+                cachefile=cachefile,
+                filename=filename,
+                number=number,
+                binary=binary,
+                timeout=timeout,
+                batchinfo=batchinfo,
+                loglevel=loglevel,
+                current_env=current_env, )
+
+        self.merge_coverage_single(filename=filename,
+                                   profile=f"{self.llvm_profile_folder}/{base_file}-mbb{number}.profraw", number=number)
         subprocess.run("rm -f *.bc core", shell=True, check=True)
 
+    def merge_coverage_single(self, filename, profile, number):
+        here = os.getcwd()
+        os.chdir(self.llvm_profile_folder)
+        # print(f"We are here: {profile}")
+        try:
+            if os.path.isfile(profile):
+                command = f"-o {os.path.basename(filename)}-mbb{number}.pro"
+                subprocess.run(
+                    f"{self.llvm_profdata} merge -sparse {profile} {command}",
+                    shell=True,
+                    check=True,
+                )
+                subprocess.run(
+                    f"rm {profile}",
+                    shell=True,
+                    check=False,
+                )
+        except Exception:
+            pass
+        finally:
+            os.chdir(here)
+
+    def teardown(self):
+        self.setup()
+        here = os.getcwd()
+        os.chdir(self.llvm_profile_folder)
+        subprocess.run(
+            f"{self.llvm_profdata} merge -sparse *.pro -o {self.coverage_profile}",
+            shell=True,
+            check=True,
+        )
+        if os.path.exists(self.coverage_profile):
+            subprocess.run(
+                f"{self.llvm_cov} show -format=html {self.parcoach_exe} "
+                f"--instr-profile={self.coverage_profile} "
+                f"--show-directory-coverage --output-dir {self.coverage_report_html} "
+                f"--sources {self.coverage_source}",
+                shell=True,
+                check=True,
+            )
+        os.chdir(here)
+
     def get_mbb_error_label(self, error_message):
         mbb_error_dict = {
             'LocalConcurrency': "LocalConcurrency",
             'Call Ordering': "CallOrdering"
         }
 
-
         for k, v in mbb_error_dict.items():
             if error_message.startswith(k):
                 return v
@@ -88,25 +141,23 @@ class Tool(AbstractTool):
         assert False and "ERROR MESSAGE NOT KNOWN PARSING DOES NOT WORK CORRECLTY"
         return "UNKNOWN"
 
-
-
     def parse(self, cachefile, logs_dir):
         if os.path.exists(f'{cachefile}.timeout') or os.path.exists(f'{logs_dir}/parcoach/{cachefile}.timeout'):
             outcome = 'timeout'
         if not (os.path.exists(f'{cachefile}.txt') or os.path.exists(f'{logs_dir}/parcoach/{cachefile}.txt')):
             return 'failure'
 
-        with open(f'{cachefile}.txt' if os.path.exists(f'{cachefile}.txt') else f'{logs_dir}/parcoach/{cachefile}.txt', 'r') as infile:
+        with open(f'{cachefile}.txt' if os.path.exists(f'{cachefile}.txt') else f'{logs_dir}/parcoach/{cachefile}.txt',
+                  'r') as infile:
             output = infile.read()
 
-
         if re.search('Compilation of .*? raised an error \(retcode: ', output):
             output = {}
             output["status"] = "UNIMPLEMENTED"
             return output
 
-        #if re.search('0 warning\(s\) issued', output):
-        #if re.search('No issues found', output):
+        # if re.search('0 warning\(s\) issued', output):
+        # if re.search('No issues found', output):
         #    return 'OK'
 
         if re.search('missing info for external function', output):
@@ -125,10 +176,11 @@ class Tool(AbstractTool):
 
         for line in lines:
             # get the error/warning blocks:
-            if re.match(COerror_line_prefix, line) or re.match(LC1error_line_prefix,line) or re.match(LC2error_line_prefix, line) or re.match(LCinfo_line_prefix,line):
+            if re.match(COerror_line_prefix, line) or re.match(LC1error_line_prefix, line) or re.match(
+                    LC2error_line_prefix, line) or re.match(LCinfo_line_prefix, line):
                 current_report.append(line)
                 reports.append(current_report)
-        #print("--- current_report = ", current_report, "\n")
+        # print("--- current_report = ", current_report, "\n")
         test_file_name = cachefile.rsplit('_', 1)[0].strip() + ".c"
         output = {}
         output["status"] = "successful"
@@ -145,13 +197,13 @@ class Tool(AbstractTool):
             for i, line in enumerate(report):
                 error_found = {}
                 error_class = {}
-                func = {} 
-                mpi_func = {} 
+                func = {}
+                mpi_func = {}
                 number = {}
                 line_number = {}
-                #print( test_file_name, ":\n")
-                #print("======> ", line, "\n")
-                #if test_file_name in line:
+                # print( test_file_name, ":\n")
+                # print("======> ", line, "\n")
+                # if test_file_name in line:
                 if "PARCOACH" in line:
                     # get the error class for Call ordering errors
                     error_found = re.search(r"(\w+) (\w+) Error", line)
@@ -163,19 +215,19 @@ class Tool(AbstractTool):
                     number = re.search('line (\d+)', line)
                     line_number = number.group(1)
                     parsed_report['error_class'] = self.get_mbb_error_label(error_class)
-                    #parsed_report['error_class'].append(self.get_mbb_error_label(error_class))
+                    # parsed_report['error_class'].append(self.get_mbb_error_label(error_class))
                     parsed_report['calls'].append(mpi_func)
                     parsed_report['lines'].append(int(line_number))
-                    #print("CO ----> ", test_file_name, " - func = ", mpi_func, " - line_number = ", line_number, " - error class = ", error_class)
+                    # print("CO ----> ", test_file_name, " - func = ", mpi_func, " - line_number = ", line_number, " - error class = ", error_class)
                 if "i32" in line:
-                    #error_found = re.search(r"(\w+) detected:", before_line)
-                    #error_class = error_found.group(1)
+                    # error_found = re.search(r"(\w+) detected:", before_line)
+                    # error_class = error_found.group(1)
                     error_class = "LocalConcurrency"
                     func = re.search("call i32 \@(\S+)\(", line)
                     mpi_func = func.group(1)
                     number = re.search('LINE (\d+)', line)
                     line_number = number.group(1)
-                    #print("LC ----> ", test_file_name, " - func = ", mpi_func, " - line_number = ", line_number, " - error class = ", error_class)
+                    # print("LC ----> ", test_file_name, " - func = ", mpi_func, " - line_number = ", line_number, " - error class = ", error_class)
                     parsed_report['error_class'] = self.get_mbb_error_label(error_class)
                     parsed_report['calls'].append(mpi_func)
                     parsed_report['lines'].append(int(line_number))
@@ -186,9 +238,8 @@ class Tool(AbstractTool):
             parsed_report['ranks'] = list(set(parsed_report['ranks']))
             parsed_reports.append(parsed_report)
 
-
-        #parsed_reports = list(set(parsed_reports))
+        # parsed_reports = list(set(parsed_reports))
         output["messages"] = parsed_reports
-        #output["messages"] = list(set(output["messages"])) 
-        #print("---> Messages = ", output["messages"], " ---> parsed_reports = ", parsed_reports, "\n")
+        # output["messages"] = list(set(output["messages"]))
+        # print("---> Messages = ", output["messages"], " ---> parsed_reports = ", parsed_reports, "\n")
         return output
-- 
GitLab