diff --git a/README.md b/README.md
index baee3ae..6db864c 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,16 @@ ValgrindCI uses the `setuptools` to build its package which can then be installe
> pip install ValgrindCI --no-index -f dist
```
+#### Build and package an executable with `pyinstaller`
+
+You can use `pyinstaller` to create a single-file executable binary:
+
+```bash
+> pip install pyinstaller
+> pyinstaller --onefile --add-data ValgrindCI:ValgrindCI valgrind-ci
+> ./dist/valgrind-ci --help
+```
+
## How to use
ValgrindCI is a command tool designed to be executed within jobs of your favorite Continuous Integration platform. It parses the XML output of valgrind to provide its services.
diff --git a/ValgrindCI/__init__.py b/ValgrindCI/__init__.py
index 4786899..1da514f 100644
--- a/ValgrindCI/__init__.py
+++ b/ValgrindCI/__init__.py
@@ -16,9 +16,32 @@ def main():
"--source-dir",
help="specifies the source directory",
)
+ parser.add_argument(
+ "--substitute-path",
+ action="append",
+ help="specifies a substitution rule `from:to` for finding source files on disk. example: --substitute-path /foo:/bar",
+ nargs='?'
+ )
+ parser.add_argument(
+ "--relativize",
+ action="append",
+ help="specifies a prefix to remove from displayed source filenames. example: --relativize /foo/bar",
+ nargs='?'
+ )
+ parser.add_argument(
+ "--relativize-from-substitute-paths",
+ default=False,
+ action="store_true",
+ help="use the `from` values in the substitution rules as prefixes to remove from displayed source filenames",
+ )
parser.add_argument(
"--output-dir", help="directory where the HTML report will be generated"
)
+ parser.add_argument(
+ "--html-report-title",
+ default="ValgrindCI Report",
+ help="the title of the generated HTML report"
+ )
parser.add_argument(
"--summary",
default=False,
@@ -55,6 +78,27 @@ def main():
data.parse(args.xml_file)
data.set_source_dir(args.source_dir)
+ if args.substitute_path:
+ substitute_paths = []
+ for s in args.substitute_path:
+ substitute_paths.append({"from": s.split(":")[0], "to": s.split(":")[1] })
+ data.set_substitute_paths(substitute_paths)
+
+ if args.relativize:
+ prefixes = []
+ for p in args.relativize:
+ prefixes.append(p)
+ data.set_relative_prefixes(prefixes)
+
+ if args.relativize_from_substitute_paths:
+ if not args.substitute_path:
+ print("No substitution paths specified on the command line.")
+ else:
+ prefixes = data._relative_prefixes.copy()
+ for s in data._substitute_paths:
+ prefixes.append(s.get("from"))
+ data.set_relative_prefixes(prefixes)
+
errors_total = data.get_num_errors()
if args.abort_on_errors and errors_total != 0:
print("{} errors reported by Valgrind - Abort".format(errors_total))
@@ -63,7 +107,7 @@ def main():
if args.output_dir:
renderer = HTMLRenderer(data)
renderer.set_source_dir(args.source_dir)
- renderer.render(args.output_dir, args.lines_before, args.lines_after)
+ renderer.render(args.html_report_title, args.output_dir, args.lines_before, args.lines_after)
if args.number_of_errors:
print("{} errors.".format(errors_total))
diff --git a/ValgrindCI/data/index.html b/ValgrindCI/data/index.html
index de37adc..fb05d37 100644
--- a/ValgrindCI/data/index.html
+++ b/ValgrindCI/data/index.html
@@ -2,14 +2,14 @@
- Valgrind report
+ {{ title }}
-
ValgrindCI Report
+
{{ title }}
{{ num_errors }} errors
diff --git a/ValgrindCI/parse.py b/ValgrindCI/parse.py
index 49618a4..abb586a 100644
--- a/ValgrindCI/parse.py
+++ b/ValgrindCI/parse.py
@@ -118,6 +118,8 @@ class ValgrindData:
def __init__(self) -> None:
self.errors: List[Error] = []
self._source_dir: Optional[str] = None
+ self._substitute_paths: Optional[List[dict]] = []
+ self._relative_prefixes: Optional[List[str]] = []
def parse(self, xml_file: str) -> None:
root = et.parse(xml_file).getroot()
@@ -130,6 +132,27 @@ def set_source_dir(self, source_dir: Optional[str]) -> None:
else:
self._source_dir = None
+ def set_substitute_paths(self, substitute_paths: Optional[List[dict]]) -> None:
+ if substitute_paths is not None:
+ self._substitute_paths = substitute_paths
+
+ def set_relative_prefixes(self, relative_prefixes: Optional[str]) -> None:
+ if relative_prefixes is not None:
+ self._relative_prefixes = relative_prefixes
+
+ def substitute_path(self, path: str) -> str:
+ for s in self._substitute_paths:
+ path = path.replace(s.get("from"), s.get("to"))
+ return path
+
+ def relativize(self, path: str) -> str:
+ for p in self._relative_prefixes:
+ if path.startswith(p):
+ path = path.replace(p, "")
+ if path.startswith("/"):
+ path = path[1:]
+ return path
+
def get_num_errors(self) -> int:
if self._source_dir is None:
return len(self.errors)
diff --git a/ValgrindCI/render.py b/ValgrindCI/render.py
index 2c943e5..e4fd87d 100644
--- a/ValgrindCI/render.py
+++ b/ValgrindCI/render.py
@@ -44,7 +44,7 @@ def set_source_dir(self, source_dir: Optional[str]) -> None:
else:
self._source_dir = None
- def render(self, output_dir: str, lines_before: int, lines_after: int) -> None:
+ def render(self, report_title: str, output_dir: str, lines_before: int, lines_after: int) -> None:
if not os.path.exists(output_dir):
os.makedirs(output_dir)
shutil.copy(
@@ -71,14 +71,14 @@ def render(self, output_dir: str, lines_before: int, lines_after: int) -> None:
f.write(
self._source_tmpl.render(
num_errors=num_errors,
- source_file_name=source_file,
+ source_file_name=self._data.relativize(source_file),
codelines=lines_of_code,
)
)
summary.append(
{
- "filename": source_file,
+ "filename": self._data.relativize(source_file),
"errors": num_errors,
"link": html_filename,
}
@@ -88,7 +88,9 @@ def render(self, output_dir: str, lines_before: int, lines_after: int) -> None:
with open(os.path.join(output_dir, "index.html"), "w") as f:
f.write(
self._index_tmpl.render(
- source_list=summary, num_errors=total_num_errors
+ title=report_title,
+ source_list=summary,
+ num_errors=total_num_errors
)
)
@@ -121,13 +123,17 @@ def _extract_error_data(
assert error_line is not None
stack["line"] = error_line - lines_before - 1
stack["error_line"] = lines_before + 1
- stack["fileref"] = "{}:{}".format(
- frame.get_path(self._source_dir), error_line
- )
- with open(fullname, "r") as f:
- for l, code_line in enumerate(f.readlines()):
- if l >= stack["line"] and l <= error_line + lines_after - 1:
- stack["code"].append(code_line)
+ frame_source = frame.get_path(self._source_dir)
+ frame_source = self._data.relativize(frame_source)
+ stack["fileref"] = "{}:{}".format(frame_source, error_line)
+ fullname = self._data.substitute_path(fullname)
+ try:
+ with open(fullname, "r", errors="replace") as f:
+ for l, code_line in enumerate(f.readlines()):
+ if l >= stack["line"] and l <= error_line + lines_after - 1:
+ stack["code"].append(code_line)
+ except OSError as e:
+ print(f"Warning: cannot read stack data from missing source file: {e.filename}")
issue["stack"].append(stack)
return issue
@@ -143,16 +149,20 @@ def _extract_data_per_source_file(
else:
filename = source_file
- with open(filename, "r") as f:
- for l, line in enumerate(f.readlines()):
- klass = None
- issue = None
- if l + 1 in error_lines:
- klass = "error"
- issue = self._extract_error_data(
- src_data, l + 1, lines_before, lines_after
+ filename = self._data.substitute_path(filename)
+ try:
+ with open(filename, "r", errors="replace") as f:
+ for l, line in enumerate(f.readlines()):
+ klass = None
+ issue = None
+ if l + 1 in error_lines:
+ klass = "error"
+ issue = self._extract_error_data(
+ src_data, l + 1, lines_before, lines_after
+ )
+ lines_of_code.append(
+ {"line": line[:-1], "klass": klass, "issue": issue}
)
- lines_of_code.append(
- {"line": line[:-1], "klass": klass, "issue": issue}
- )
+ except OSError as e:
+ print(f"Warning: cannot extract data from missing source file: {e.filename}")
return lines_of_code, len(error_lines)