mirror of
https://github.com/HendrikRauh/dmx-interface.git
synced 2026-04-07 10:22:21 +00:00
feat(doxygen): add Doxygen code coverage script and pre-commit hook
This commit is contained in:
parent
5517a49e14
commit
8f5b6327bc
3 changed files with 233 additions and 0 deletions
|
|
@ -98,3 +98,14 @@ repos:
|
|||
entry: nixfmt
|
||||
language: system
|
||||
types: [nix]
|
||||
|
||||
# Doxygen code coverage
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: doxygen-coverage
|
||||
name: Doxygen code coverage
|
||||
language: system
|
||||
entry: tools/doxy-coverage.py
|
||||
args: [docs/doxygen/xml, --no-error, --summary-only]
|
||||
types_or: [c, c++, header]
|
||||
verbose: true
|
||||
|
|
|
|||
1
Doxyfile
1
Doxyfile
|
|
@ -17,6 +17,7 @@ USE_MDFILE_AS_MAINPAGE = README.md
|
|||
# Documentation settings
|
||||
GENERATE_LATEX = NO
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_XML=YES
|
||||
|
||||
# doxygen-awesome-css settings
|
||||
HTML_EXTRA_STYLESHEET = docs/external/doxygen-awesome-css/doxygen-awesome.css \
|
||||
|
|
|
|||
221
tools/doxy-coverage.py
Executable file
221
tools/doxy-coverage.py
Executable file
|
|
@ -0,0 +1,221 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
|
||||
# Based on https://github.com/davatorium/doxy-coverage
|
||||
# which was forked from https://github.com/alobbs/doxy-coverage
|
||||
# and modified for use in this project.
|
||||
|
||||
# All files in doxy-coverage are Copyright 2014 Alvaro Lopez Ortega.
|
||||
#
|
||||
# Authors:
|
||||
# * Alvaro Lopez Ortega <alvaro@gnu.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
__author__ = "Alvaro Lopez Ortega"
|
||||
__email__ = "alvaro@alobbs.com"
|
||||
__copyright__ = "Copyright (C) 2014 Alvaro Lopez Ortega"
|
||||
|
||||
from filecmp import cmp
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Defaults
|
||||
ACCEPTABLE_COVERAGE = 80
|
||||
|
||||
# Global
|
||||
ns = None
|
||||
|
||||
|
||||
def ERROR(*objs):
|
||||
print("ERROR: ", *objs, end="\n", file=sys.stderr)
|
||||
|
||||
|
||||
def FATAL(*objs):
|
||||
ERROR(*objs)
|
||||
sys.exit(0 if ns.no_error else 1)
|
||||
|
||||
|
||||
def parse_file(fullpath):
|
||||
tree = ET.parse(fullpath)
|
||||
|
||||
sourcefile = None
|
||||
definitions = {}
|
||||
|
||||
for definition in tree.findall("./compounddef//memberdef"):
|
||||
# Should it be documented
|
||||
if definition.get("kind") == "function" and definition.get("static") == "yes":
|
||||
continue
|
||||
|
||||
# Is the definition documented?
|
||||
documented = False
|
||||
for k in ("briefdescription", "detaileddescription", "inbodydescription"):
|
||||
if definition.findall(f"./{k}/"):
|
||||
documented = True
|
||||
break
|
||||
|
||||
# Name
|
||||
d_def = definition.find("./definition")
|
||||
d_nam = definition.find("./name")
|
||||
|
||||
if not sourcefile:
|
||||
l = definition.find("./location")
|
||||
if l is not None:
|
||||
sourcefile = l.get("file")
|
||||
|
||||
if d_def is not None:
|
||||
name = d_def.text
|
||||
elif d_nam is not None:
|
||||
name = d_nam.text
|
||||
else:
|
||||
name = definition.get("id")
|
||||
|
||||
# Aggregate
|
||||
definitions[name] = documented
|
||||
|
||||
if not sourcefile:
|
||||
sourcefile = fullpath
|
||||
|
||||
return (sourcefile, definitions)
|
||||
|
||||
|
||||
def parse(path):
|
||||
index_fp = os.path.join(path, "index.xml")
|
||||
if not os.path.exists(index_fp):
|
||||
FATAL("Documentation not present. Exiting.", index_fp)
|
||||
|
||||
tree = ET.parse(index_fp)
|
||||
|
||||
files = {}
|
||||
for entry in tree.findall("compound"):
|
||||
if entry.get("kind") == "dir":
|
||||
continue
|
||||
|
||||
file_fp = os.path.join(path, f"{entry.get('refid')}.xml")
|
||||
sourcefile, definitions = parse_file(file_fp)
|
||||
|
||||
if definitions != {}:
|
||||
files[sourcefile] = definitions
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def report(files, include_files, summary_only):
|
||||
files_sorted = sorted(files.keys())
|
||||
|
||||
if len(include_files) != 0:
|
||||
files_sorted = list(filter(lambda f: f in include_files, files_sorted))
|
||||
|
||||
if len(files_sorted) == 0:
|
||||
FATAL("No files to report on. Exiting.")
|
||||
|
||||
files_sorted.reverse()
|
||||
|
||||
total_yes = 0
|
||||
total_no = 0
|
||||
|
||||
for f in files_sorted:
|
||||
defs = files[f]
|
||||
|
||||
doc_yes = len([d for d in defs.values() if d])
|
||||
doc_no = len([d for d in defs.values() if not d])
|
||||
doc_per = doc_yes * 100.0 / (doc_yes + doc_no)
|
||||
|
||||
total_yes += doc_yes
|
||||
total_no += doc_no
|
||||
|
||||
if not summary_only:
|
||||
print(f"{int(doc_per):3d}% - {f} - ({doc_yes} of {doc_yes + doc_no})")
|
||||
|
||||
if None in defs:
|
||||
del defs[None]
|
||||
if not summary_only:
|
||||
defs_sorted = sorted(defs.keys())
|
||||
for d in defs_sorted:
|
||||
if not defs[d]:
|
||||
print("\t", d)
|
||||
|
||||
total_all = total_yes + total_no
|
||||
total_percentage = total_yes * 100 / total_all
|
||||
print(
|
||||
f"{"" if summary_only else "\n"}{int(total_percentage)}% API documentation coverage"
|
||||
)
|
||||
return (ns.threshold - total_percentage, 0)[total_percentage > ns.threshold]
|
||||
|
||||
|
||||
def main():
|
||||
# Arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"dir", action="store", help="Path to Doxygen's XML doc directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"include_files",
|
||||
action="extend",
|
||||
nargs="*",
|
||||
help="List of files to check coverage for (Default: all files)",
|
||||
type=str,
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-error",
|
||||
action="store_true",
|
||||
help="Do not return error code after execution",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--summary-only",
|
||||
action="store_true",
|
||||
help="Only print the summary of the coverage report, without listing the coverage of each file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--threshold",
|
||||
action="store",
|
||||
help=f"Min acceptable coverage percentage (Default: {ACCEPTABLE_COVERAGE})",
|
||||
default=ACCEPTABLE_COVERAGE,
|
||||
type=int,
|
||||
)
|
||||
|
||||
global ns
|
||||
ns = parser.parse_args()
|
||||
if not ns:
|
||||
FATAL("ERROR: Couldn't parse parameters")
|
||||
|
||||
# Parse
|
||||
files = parse(ns.dir)
|
||||
|
||||
# Print report
|
||||
err = report(files, ns.include_files, ns.summary_only)
|
||||
if ns.no_error:
|
||||
return
|
||||
|
||||
sys.exit(round(err))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue