From 2c8166434709adb0b8bc4e8f2a558e009b19e36a Mon Sep 17 00:00:00 2001
From: Thibault Martin <thi.martin.pro@pm.me>
Date: Mon, 2 Oct 2023 09:02:32 +0200
Subject: [PATCH] New script to compute dir coverage using --coverage output
 See `python dev/compute_dir_coverage.py -h` for more info

---
 dev/compute_dir_coverage.py | 173 ++++++++++++++++++++++++++++++++++++
 1 file changed, 173 insertions(+)
 create mode 100755 dev/compute_dir_coverage.py

diff --git a/dev/compute_dir_coverage.py b/dev/compute_dir_coverage.py
new file mode 100755
index 00000000000..5a619ec6871
--- /dev/null
+++ b/dev/compute_dir_coverage.py
@@ -0,0 +1,173 @@
+# usage: compute_dir_coverage.py [-h] [-f] [-r] filename
+#
+# positional arguments:
+#   filename     path to coverage index.html file
+#
+# options:
+#   -h, --help   show this help message and exit
+#   -f, --files  display files coverage
+#   -r, --ratio  display ratio covered lines / total lines
+
+import re, sys, os.path
+from bs4 import BeautifulSoup
+import argparse
+
+FILE_MARKER = '<files>'
+DISPLAY_FILES = False
+DISPLAY_RATIO = False
+DISPLAY_BAR = False
+root = {FILE_MARKER: []}
+coverage = {}
+
+# Generate command line arguments and format
+def options():
+  argparser = argparse.ArgumentParser()
+  argparser.add_argument("filepath", help="path to frama-c/_coverage/index.html file")
+  argparser.add_argument("-f", "--files", action="store_true", help="display files coverage")
+  argparser.add_argument("-r", "--ratio", action="store_true", help="display ratio (covered lines / total lines)")
+  argparser.add_argument("-b", "--bar", action="store_true", help="display %% bars")
+  args = argparser.parse_args()
+  return args
+
+# Open the html file and create the structure to navigate inside
+def parse(filename):
+  if not os.path.exists(filename):
+    print("No such file or directory: " + filename)
+    exit()
+  with open(filename) as fp:
+    return BeautifulSoup(fp, 'html.parser')
+
+# From our html structure, extract files informations
+# Return a list of nuples (file_path, covered_lines, total_lines)
+def extract(html_parser):
+  # This div contains all files data
+  all_files = html_parser.find(id="files")
+
+  file_list = list()
+  for file in all_files.find_all("div"):
+    # Access to covered/total lines data
+    data = file.contents[3].contents[1].text.split('/', 1)
+    covered_lines = int(re.sub(r'[^\d]+', '', data[0]))
+    total_lines = int(re.sub(r'[^\d]+', '', data[1]))
+    # Access to file path
+    path_to_file = file.contents[5].text.strip()
+    file_list.append((path_to_file,covered_lines,total_lines))
+  return file_list
+
+# Build file directory tree
+# Each folder is a dictionnary containing a list of files marked with FILE_MARKER
+# Other entries are folder
+# We store each files with it's coverage data
+def build_tree(path, covered_lines, total_lines, current_dir):
+  parts = path.split('/', 1) # Perform only 1 split
+  if len(parts) == 1:  # path contains only a filename
+    current_dir[FILE_MARKER].append((parts[0], covered_lines, total_lines))
+  else:
+    directory, remaining_path = parts
+    if directory not in current_dir:
+      current_dir[directory] = {FILE_MARKER:[]}
+    build_tree(remaining_path, covered_lines, total_lines, current_dir[directory])
+
+def concat_path(path, file):
+  if path == "":
+    return file
+  else:
+    return path+"/"+file
+
+def dir_coverage(d, path=""):
+  acc_coverage, acc_total = 0, 0
+  for key, value in d.items():
+    if key == FILE_MARKER:  # Files
+      # Add files stats to current dir
+      for file, covered_lines, total_lines in value:
+        acc_coverage = acc_coverage + covered_lines
+        acc_total = acc_total + total_lines
+    else: # Directory
+      currpath = concat_path(path, key)
+      # Compute subdir stats
+      covered_lines, total_lines = dir_coverage(value, currpath)
+      coverage[currpath] = covered_lines, total_lines
+      # Add subdir stats to current dir
+      acc_coverage = acc_coverage + covered_lines
+      acc_total = acc_total + total_lines
+  return acc_coverage, acc_total
+
+def percentage(covered_lines, total_lines):
+  if total_lines != 0:
+    return covered_lines / total_lines * 100
+  else:
+    return 0.0
+
+###################
+# Print functions #
+###################
+
+def str_per(per):
+  return "{:>6.2f}".format(per)
+
+def str_bar(per):
+  if DISPLAY_BAR:
+    nb = round(per / 5)
+    return "|" + nb * "#" + (20-nb) * "-" + "| "
+  else:
+    return ""
+
+def str_ratio(covered_lines, total_lines):
+  if DISPLAY_RATIO:
+    return " : (" + str(covered_lines) + " / " + str(total_lines) + ")"
+  else:
+    return ""
+
+def print_line(name, covered_lines, total_lines, indent):
+  per = percentage(covered_lines, total_lines)
+
+  bar = str_bar(per)
+  p = str_per(per)
+  ratio =  str_ratio(covered_lines, total_lines)
+
+  if indent != "":
+    indent = indent + " "
+
+  print(bar + p + "% " + indent + name + ratio)
+
+def print_files(files, indent):
+  for file, covered_lines, total_lines in files :
+    print_line(file, covered_lines, total_lines, '---' + indent)
+
+def print_dir(directory, children, path, indent):
+  currpath = concat_path(path, directory)
+  covered_lines, total_lines = coverage[currpath]
+  print_line(directory, covered_lines, total_lines, indent)
+  print_tree(children, '---' + indent, currpath)
+
+# !!! dir_coverage must be called before !!!
+def print_tree(d, indent="", path=""):
+  for key, value in d.items():
+    if key != FILE_MARKER: # Direcory
+      print_dir(key, value, path, indent)
+    elif DISPLAY_FILES: # Files
+      print_files(value, indent)
+
+def main():
+
+  args = options()
+
+  # Set options booleans
+  global DISPLAY_FILES
+  DISPLAY_FILES = args.files
+  global DISPLAY_RATIO
+  DISPLAY_RATIO = args.ratio
+  global DISPLAY_BAR
+  DISPLAY_BAR = args.bar
+
+  html_parser = parse(args.filepath)
+  files = extract(html_parser)
+
+  for path_to_file, cov, tot in files:
+    build_tree(path_to_file, cov, tot, root)
+
+  dir_coverage(root)
+  print_tree(root)
+
+if __name__ == "__main__":
+    main()
-- 
GitLab