diff mbox series

[1/2] xen/misra: add diff-report.py tool

Message ID 20230504142523.2989306-2-luca.fancellu@arm.com (mailing list archive)
State Superseded
Headers show
Series diff-report.py tool | expand

Commit Message

Luca Fancellu May 4, 2023, 2:25 p.m. UTC
Add a new tool, diff-report.py that can be used to make diff between
reports generated by xen-analysis.py tool.
Currently this tool supports the Xen cppcheck text report format in
its operations.

The tool prints every finding that is in the report passed with -r
(check report) which is not in the report passed with -b (baseline).

Signed-off-by: Luca Fancellu <luca.fancellu@arm.com>
---
 xen/scripts/diff-report.py                    |  76 ++++++++++++
 .../xen_analysis/diff_tool/__init__.py        |   0
 .../xen_analysis/diff_tool/cppcheck_report.py |  41 +++++++
 xen/scripts/xen_analysis/diff_tool/debug.py   |  36 ++++++
 xen/scripts/xen_analysis/diff_tool/report.py  | 114 ++++++++++++++++++
 5 files changed, 267 insertions(+)
 create mode 100755 xen/scripts/diff-report.py
 create mode 100644 xen/scripts/xen_analysis/diff_tool/__init__.py
 create mode 100644 xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
 create mode 100644 xen/scripts/xen_analysis/diff_tool/debug.py
 create mode 100644 xen/scripts/xen_analysis/diff_tool/report.py

Comments

Luca Fancellu May 6, 2023, 6:51 a.m. UTC | #1
> On 4 May 2023, at 15:25, Luca Fancellu <Luca.Fancellu@arm.com> wrote:
> 
> Add a new tool, diff-report.py that can be used to make diff between
> reports generated by xen-analysis.py tool.
> Currently this tool supports the Xen cppcheck text report format in
> its operations.
> 
> The tool prints every finding that is in the report passed with -r
> (check report) which is not in the report passed with -b (baseline).
> 
> Signed-off-by: Luca Fancellu <luca.fancellu@arm.com>
> ---
> xen/scripts/diff-report.py                    |  76 ++++++++++++
> .../xen_analysis/diff_tool/__init__.py        |   0
> .../xen_analysis/diff_tool/cppcheck_report.py |  41 +++++++
> xen/scripts/xen_analysis/diff_tool/debug.py   |  36 ++++++
> xen/scripts/xen_analysis/diff_tool/report.py  | 114 ++++++++++++++++++
> 5 files changed, 267 insertions(+)
> create mode 100755 xen/scripts/diff-report.py
> create mode 100644 xen/scripts/xen_analysis/diff_tool/__init__.py
> create mode 100644 xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
> create mode 100644 xen/scripts/xen_analysis/diff_tool/debug.py
> create mode 100644 xen/scripts/xen_analysis/diff_tool/report.py
> 
> diff --git a/xen/scripts/diff-report.py b/xen/scripts/diff-report.py
> new file mode 100755
> index 000000000000..4913fb43a8f9
> --- /dev/null
> +++ b/xen/scripts/diff-report.py
> @@ -0,0 +1,76 @@
> +#!/usr/bin/env python3
> +
> +import os, sys
> +from argparse import ArgumentParser
> +from xen_analysis.diff_tool.debug import Debug
> +from xen_analysis.diff_tool.report import ReportError
> +from xen_analysis.diff_tool.cppcheck_report import CppcheckReport
> +
> +
> +def log_info(text, end='\n'):
> +    global args
> +    global file_out
> +
> +    if (args.verbose):
> +        print(text, end=end, file=file_out)
> +
> +
> +def main(argv):
> +    global args
> +    global file_out
> +
> +    parser = ArgumentParser(prog="diff-report.py")
> +    parser.add_argument("-b", "--baseline", required=True, type=str,
> +                        help="Path to the baseline report.")
> +    parser.add_argument("--debug", action='store_true',
> +                        help="Produce intermediate reports during operations.")
> +    parser.add_argument("-o", "--out", default="stdout", type=str,
> +                        help="Where to print the tool output. Default is "
> +                             "stdout")
> +    parser.add_argument("-r", "--report", required=True, type=str,
> +                        help="Path to the 'check report', the one checked "
> +                             "against the baseline.")
> +    parser.add_argument("-v", "--verbose", action='store_true',
> +                        help="Print more informations during the run.")
> +
> +    args = parser.parse_args()
> +
> +    if args.out == "stdout":
> +        file_out = sys.stdout
> +    else:
> +        try:
> +            file_out = open(args.out, "wt")
> +        except OSError as e:
> +            print("ERROR: Issue opening file {}: {}".format(args.out, e))
> +            sys.exit(1)
> +
> +    debug = Debug(args)
> +
> +    try:
> +        baseline_path = os.path.realpath(args.baseline)
> +        log_info("Loading baseline report {}".format(baseline_path), "")
> +        baseline = CppcheckReport(baseline_path)
> +        baseline.parse()
> +        debug.debug_print_parsed_report(baseline)
> +        log_info(" [OK]")
> +        new_rep_path = os.path.realpath(args.report)
> +        log_info("Loading check report {}".format(new_rep_path), "")
> +        new_rep = CppcheckReport(new_rep_path)
> +        new_rep.parse()
> +        debug.debug_print_parsed_report(new_rep)
> +        log_info(" [OK]")
> +    except ReportError as e:
> +        print("ERROR: {}".format(e))
> +        sys.exit(1)
> +
> +    output = new_rep - baseline
> +    print(output, end="", file=file_out)
> +
> +    if len(output) > 0:
> +        sys.exit(1)
> +
> +    sys.exit(0)
> +
> +
> +if __name__ == "__main__":
> +    main(sys.argv[1:])
> diff --git a/xen/scripts/xen_analysis/diff_tool/__init__.py b/xen/scripts/xen_analysis/diff_tool/__init__.py
> new file mode 100644
> index 000000000000..e69de29bb2d1
> diff --git a/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py b/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
> new file mode 100644
> index 000000000000..787a51aca583
> --- /dev/null
> +++ b/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
> @@ -0,0 +1,41 @@
> +#!/usr/bin/env python3
> +
> +import re
> +from .report import Report, ReportError
> +
> +
> +class CppcheckReport(Report):
> +    def __init__(self, report_path: str) -> None:
> +        super().__init__(report_path)
> +        # This matches a string like:
> +        # path/to/file.c(<line number>,<digits>):<whatever>
> +        # and captures file name path and line number
> +        # the last capture group is used for text substitution in __str__
> +        self.__report_entry_regex = re.compile(r'^(.*)\((\d+)(,\d+\):.*)$')
> +
> +    def parse(self) -> None:
> +        report_path = self.get_report_path()
> +        try:
> +            with open(report_path, "rt") as infile:
> +                report_lines = infile.readlines()
> +        except OSError as e:
> +            raise ReportError("Issue with reading file {}: {}"
> +                              .format(report_path, e))
> +        for line in report_lines:
> +            entry = self.__report_entry_regex.match(line)
> +            if entry and entry.group(1) and entry.group(2):
> +                file_path = entry.group(1)
> +                line_number = int(entry.group(2))
> +                self.add_entry(file_path, line_number, line)
> +            else:
> +                raise ReportError("Malformed report entry in file {}:\n{}"
> +                                  .format(report_path, line))
> +
> +    def __str__(self) -> str:
> +        ret = ""
> +        for entry in self.to_list():
> +            ret += re.sub(self.__report_entry_regex,
> +                          r'{}({}\3'.format(entry.file_path,
> +                                            entry.line_number),
> +                          entry.text)
> +        return ret
> diff --git a/xen/scripts/xen_analysis/diff_tool/debug.py b/xen/scripts/xen_analysis/diff_tool/debug.py
> new file mode 100644
> index 000000000000..d46df3300d21
> --- /dev/null
> +++ b/xen/scripts/xen_analysis/diff_tool/debug.py
> @@ -0,0 +1,36 @@
> +#!/usr/bin/env python3
> +
> +import os
> +from .report import Report
> +
> +
> +class Debug:
> +    def __init__(self, args):
> +        self.args = args
> +
> +    def __get_debug_out_filename(self, path: str, type: str) -> str:
> +        # Take basename
> +        file_name = os.path.basename(path)
> +        # Split in name and extension
> +        file_name = os.path.splitext(file_name)
> +        if self.args.out != "stdout":
> +            out_folder = os.path.dirname(self.args.out)
> +        else:
> +            out_folder = "./"
> +        dbg_report_path = out_folder + file_name[0] + type + file_name[1]
> +
> +        return dbg_report_path
> +
> +    def __debug_print_report(self, report: Report, type: str) -> None:
> +        report_name = self.__get_debug_out_filename(report.get_report_path(),
> +                                                    type)
> +        try:
> +            with open(report_name, "wt") as outfile:
> +                print(report, end="", file=outfile)
> +        except OSError as e:
> +            print("ERROR: Issue opening file {}: {}".format(report_name, e))
> +
> +    def debug_print_parsed_report(self, report: Report) -> None:
> +        if not self.args.debug:
> +            return
> +        self.__debug_print_report(report, ".parsed")
> diff --git a/xen/scripts/xen_analysis/diff_tool/report.py b/xen/scripts/xen_analysis/diff_tool/report.py
> new file mode 100644
> index 000000000000..d958d1816eb4
> --- /dev/null
> +++ b/xen/scripts/xen_analysis/diff_tool/report.py
> @@ -0,0 +1,114 @@
> +#!/usr/bin/env python3
> +
> +import os
> +
> +
> +class ReportError(Exception):
> +    pass
> +
> +
> +class Report:
> +    class ReportEntry:
> +        def __init__(self, file_path: str, line_number: int,
> +                     entry_text: list, line_id: int) -> None:
> +            if not isinstance(line_number, int) or \
> +               not isinstance(line_id, int):
> +                raise ReportError("ReportEntry constructor wrong type args")
> +            self.file_path = file_path
> +            self.line_number = line_number
> +            self.text = entry_text
> +            self.line_id = line_id
> +

I realise now that I had a rebase mistake here, the class ReportEntry should not
have these two functions, it was part of another branch, I will fix this in the next
version, in the mean time I’ll wait for other possible findings for this patch.

<delete from here>

> +        def __str__(self) -> str:
> +            ret = ''
> +            header = 'File path:Count\n'
> +
> +            for path in self.stats:
> +                ret += f'{path}: {len(self.stats[path])}\n'
> +
> +            if ret == '':
> +                ret += 'No new issues introduced\n'
> +
> +            ret = header + ret
> +
> +            return ret
> +
> +        def __len__(self) -> int:
> +            ret = 0
> +
> +            for ln_list in self.stats.values():
> +                ret += len(ln_list)
> +
> +            return ret

<to here>

> +
> +    def __init__(self, report_path: str) -> None:
> +        self.__entries = {}
> +        self.__path = report_path
> +        self.__last_line_order = 0
> +
> +    def parse(self) -> None:
> +        raise ReportError("Please create a specialised class from 'Report'.")
> +
> +    def get_report_path(self) -> str:
> +        return self.__path
> +
> +    def get_report_entries(self) -> dict:
> +        return self.__entries
> +
> +    def add_entry(self, entry_path: str, entry_line_number: int,
> +                  entry_text: list) -> None:
> +        entry = Report.ReportEntry(entry_path, entry_line_number, entry_text,
> +                                   self.__last_line_order)
> +        if entry_path in self.__entries.keys():
> +            self.__entries[entry_path].append(entry)
> +        else:
> +            self.__entries[entry_path] = [entry]
> +        self.__last_line_order += 1
> +
> +    def to_list(self) -> list:
> +        report_list = []
> +        for _, entries in self.__entries.items():
> +            for entry in entries:
> +                report_list.append(entry)
> +
> +        report_list.sort(key=lambda x: x.line_id)
> +        return report_list
> +
> +    def __str__(self) -> str:
> +        ret = ""
> +        for entry in self.to_list():
> +            ret += entry.file_path + ":" + entry.line_number + ":" + entry.text
> +
> +        return ret
> +
> +    def __len__(self) -> int:
> +        return len(self.to_list())
> +
> +    def __sub__(self, report_b: 'Report') -> 'Report':
> +        if self.__class__ != report_b.__class__:
> +            raise ReportError("Diff of different type of report!")
> +
> +        filename, file_extension = os.path.splitext(self.__path)
> +        diff_report = self.__class__(filename + ".diff" + file_extension)
> +        # Put in the diff report only records of this report that are not
> +        # present in the report_b.
> +        for file_path, entries in self.__entries.items():
> +            rep_b_entries = report_b.get_report_entries()
> +            if file_path in rep_b_entries.keys():
> +                # File path exists in report_b, so check what entries of that
> +                # file path doesn't exist in report_b and add them to the diff
> +                rep_b_entries_num = [
> +                    x.line_number for x in rep_b_entries[file_path]
> +                ]
> +                for entry in entries:
> +                    if entry.line_number not in rep_b_entries_num:
> +                        diff_report.add_entry(file_path, entry.line_number,
> +                                              entry.text)
> +            else:
> +                # File path doesn't exist in report_b, so add every entry
> +                # of that file path to the diff
> +                for entry in entries:
> +                    diff_report.add_entry(file_path, entry.line_number,
> +                                          entry.text)
> +
> +        return diff_report
> -- 
> 2.34.1
> 
>
Stefano Stabellini May 17, 2023, 1:26 a.m. UTC | #2
On Thu, 4 May 2023, Luca Fancellu wrote:
> Add a new tool, diff-report.py that can be used to make diff between
> reports generated by xen-analysis.py tool.
> Currently this tool supports the Xen cppcheck text report format in
> its operations.
> 
> The tool prints every finding that is in the report passed with -r
> (check report) which is not in the report passed with -b (baseline).
> 
> Signed-off-by: Luca Fancellu <luca.fancellu@arm.com>

Acked-by: Stefano Stabellini <sstabellini@kernel.org>
Tested-by: Stefano Stabellini <sstabellini@kernel.org>


> ---
>  xen/scripts/diff-report.py                    |  76 ++++++++++++
>  .../xen_analysis/diff_tool/__init__.py        |   0
>  .../xen_analysis/diff_tool/cppcheck_report.py |  41 +++++++
>  xen/scripts/xen_analysis/diff_tool/debug.py   |  36 ++++++
>  xen/scripts/xen_analysis/diff_tool/report.py  | 114 ++++++++++++++++++
>  5 files changed, 267 insertions(+)
>  create mode 100755 xen/scripts/diff-report.py
>  create mode 100644 xen/scripts/xen_analysis/diff_tool/__init__.py
>  create mode 100644 xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
>  create mode 100644 xen/scripts/xen_analysis/diff_tool/debug.py
>  create mode 100644 xen/scripts/xen_analysis/diff_tool/report.py
> 
> diff --git a/xen/scripts/diff-report.py b/xen/scripts/diff-report.py
> new file mode 100755
> index 000000000000..4913fb43a8f9
> --- /dev/null
> +++ b/xen/scripts/diff-report.py
> @@ -0,0 +1,76 @@
> +#!/usr/bin/env python3
> +
> +import os, sys
> +from argparse import ArgumentParser
> +from xen_analysis.diff_tool.debug import Debug
> +from xen_analysis.diff_tool.report import ReportError
> +from xen_analysis.diff_tool.cppcheck_report import CppcheckReport
> +
> +
> +def log_info(text, end='\n'):
> +    global args
> +    global file_out
> +
> +    if (args.verbose):
> +        print(text, end=end, file=file_out)
> +
> +
> +def main(argv):
> +    global args
> +    global file_out
> +
> +    parser = ArgumentParser(prog="diff-report.py")
> +    parser.add_argument("-b", "--baseline", required=True, type=str,
> +                        help="Path to the baseline report.")
> +    parser.add_argument("--debug", action='store_true',
> +                        help="Produce intermediate reports during operations.")
> +    parser.add_argument("-o", "--out", default="stdout", type=str,
> +                        help="Where to print the tool output. Default is "
> +                             "stdout")
> +    parser.add_argument("-r", "--report", required=True, type=str,
> +                        help="Path to the 'check report', the one checked "
> +                             "against the baseline.")
> +    parser.add_argument("-v", "--verbose", action='store_true',
> +                        help="Print more informations during the run.")
> +
> +    args = parser.parse_args()
> +
> +    if args.out == "stdout":
> +        file_out = sys.stdout
> +    else:
> +        try:
> +            file_out = open(args.out, "wt")
> +        except OSError as e:
> +            print("ERROR: Issue opening file {}: {}".format(args.out, e))
> +            sys.exit(1)
> +
> +    debug = Debug(args)
> +
> +    try:
> +        baseline_path = os.path.realpath(args.baseline)
> +        log_info("Loading baseline report {}".format(baseline_path), "")
> +        baseline = CppcheckReport(baseline_path)
> +        baseline.parse()
> +        debug.debug_print_parsed_report(baseline)
> +        log_info(" [OK]")
> +        new_rep_path = os.path.realpath(args.report)
> +        log_info("Loading check report {}".format(new_rep_path), "")
> +        new_rep = CppcheckReport(new_rep_path)
> +        new_rep.parse()
> +        debug.debug_print_parsed_report(new_rep)
> +        log_info(" [OK]")
> +    except ReportError as e:
> +        print("ERROR: {}".format(e))
> +        sys.exit(1)
> +
> +    output = new_rep - baseline
> +    print(output, end="", file=file_out)
> +
> +    if len(output) > 0:
> +        sys.exit(1)
> +
> +    sys.exit(0)
> +
> +
> +if __name__ == "__main__":
> +    main(sys.argv[1:])
> diff --git a/xen/scripts/xen_analysis/diff_tool/__init__.py b/xen/scripts/xen_analysis/diff_tool/__init__.py
> new file mode 100644
> index 000000000000..e69de29bb2d1
> diff --git a/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py b/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
> new file mode 100644
> index 000000000000..787a51aca583
> --- /dev/null
> +++ b/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
> @@ -0,0 +1,41 @@
> +#!/usr/bin/env python3
> +
> +import re
> +from .report import Report, ReportError
> +
> +
> +class CppcheckReport(Report):
> +    def __init__(self, report_path: str) -> None:
> +        super().__init__(report_path)
> +        # This matches a string like:
> +        # path/to/file.c(<line number>,<digits>):<whatever>
> +        # and captures file name path and line number
> +        # the last capture group is used for text substitution in __str__
> +        self.__report_entry_regex = re.compile(r'^(.*)\((\d+)(,\d+\):.*)$')
> +
> +    def parse(self) -> None:
> +        report_path = self.get_report_path()
> +        try:
> +            with open(report_path, "rt") as infile:
> +                report_lines = infile.readlines()
> +        except OSError as e:
> +            raise ReportError("Issue with reading file {}: {}"
> +                              .format(report_path, e))
> +        for line in report_lines:
> +            entry = self.__report_entry_regex.match(line)
> +            if entry and entry.group(1) and entry.group(2):
> +                file_path = entry.group(1)
> +                line_number = int(entry.group(2))
> +                self.add_entry(file_path, line_number, line)
> +            else:
> +                raise ReportError("Malformed report entry in file {}:\n{}"
> +                                  .format(report_path, line))
> +
> +    def __str__(self) -> str:
> +        ret = ""
> +        for entry in self.to_list():
> +            ret += re.sub(self.__report_entry_regex,
> +                          r'{}({}\3'.format(entry.file_path,
> +                                            entry.line_number),
> +                          entry.text)
> +        return ret
> diff --git a/xen/scripts/xen_analysis/diff_tool/debug.py b/xen/scripts/xen_analysis/diff_tool/debug.py
> new file mode 100644
> index 000000000000..d46df3300d21
> --- /dev/null
> +++ b/xen/scripts/xen_analysis/diff_tool/debug.py
> @@ -0,0 +1,36 @@
> +#!/usr/bin/env python3
> +
> +import os
> +from .report import Report
> +
> +
> +class Debug:
> +    def __init__(self, args):
> +        self.args = args
> +
> +    def __get_debug_out_filename(self, path: str, type: str) -> str:
> +        # Take basename
> +        file_name = os.path.basename(path)
> +        # Split in name and extension
> +        file_name = os.path.splitext(file_name)
> +        if self.args.out != "stdout":
> +            out_folder = os.path.dirname(self.args.out)
> +        else:
> +            out_folder = "./"
> +        dbg_report_path = out_folder + file_name[0] + type + file_name[1]
> +
> +        return dbg_report_path
> +
> +    def __debug_print_report(self, report: Report, type: str) -> None:
> +        report_name = self.__get_debug_out_filename(report.get_report_path(),
> +                                                    type)
> +        try:
> +            with open(report_name, "wt") as outfile:
> +                print(report, end="", file=outfile)
> +        except OSError as e:
> +            print("ERROR: Issue opening file {}: {}".format(report_name, e))
> +
> +    def debug_print_parsed_report(self, report: Report) -> None:
> +        if not self.args.debug:
> +            return
> +        self.__debug_print_report(report, ".parsed")
> diff --git a/xen/scripts/xen_analysis/diff_tool/report.py b/xen/scripts/xen_analysis/diff_tool/report.py
> new file mode 100644
> index 000000000000..d958d1816eb4
> --- /dev/null
> +++ b/xen/scripts/xen_analysis/diff_tool/report.py
> @@ -0,0 +1,114 @@
> +#!/usr/bin/env python3
> +
> +import os
> +
> +
> +class ReportError(Exception):
> +    pass
> +
> +
> +class Report:
> +    class ReportEntry:
> +        def __init__(self, file_path: str, line_number: int,
> +                     entry_text: list, line_id: int) -> None:
> +            if not isinstance(line_number, int) or \
> +               not isinstance(line_id, int):
> +                raise ReportError("ReportEntry constructor wrong type args")
> +            self.file_path = file_path
> +            self.line_number = line_number
> +            self.text = entry_text
> +            self.line_id = line_id
> +
> +        def __str__(self) -> str:
> +            ret = ''
> +            header = 'File path:Count\n'
> +
> +            for path in self.stats:
> +                ret += f'{path}: {len(self.stats[path])}\n'
> +
> +            if ret == '':
> +                ret += 'No new issues introduced\n'
> +
> +            ret = header + ret
> +
> +            return ret
> +
> +        def __len__(self) -> int:
> +            ret = 0
> +
> +            for ln_list in self.stats.values():
> +                ret += len(ln_list)
> +
> +            return ret
> +
> +    def __init__(self, report_path: str) -> None:
> +        self.__entries = {}
> +        self.__path = report_path
> +        self.__last_line_order = 0
> +
> +    def parse(self) -> None:
> +        raise ReportError("Please create a specialised class from 'Report'.")
> +
> +    def get_report_path(self) -> str:
> +        return self.__path
> +
> +    def get_report_entries(self) -> dict:
> +        return self.__entries
> +
> +    def add_entry(self, entry_path: str, entry_line_number: int,
> +                  entry_text: list) -> None:
> +        entry = Report.ReportEntry(entry_path, entry_line_number, entry_text,
> +                                   self.__last_line_order)
> +        if entry_path in self.__entries.keys():
> +            self.__entries[entry_path].append(entry)
> +        else:
> +            self.__entries[entry_path] = [entry]
> +        self.__last_line_order += 1
> +
> +    def to_list(self) -> list:
> +        report_list = []
> +        for _, entries in self.__entries.items():
> +            for entry in entries:
> +                report_list.append(entry)
> +
> +        report_list.sort(key=lambda x: x.line_id)
> +        return report_list
> +
> +    def __str__(self) -> str:
> +        ret = ""
> +        for entry in self.to_list():
> +            ret += entry.file_path + ":" + entry.line_number + ":" + entry.text
> +
> +        return ret
> +
> +    def __len__(self) -> int:
> +        return len(self.to_list())
> +
> +    def __sub__(self, report_b: 'Report') -> 'Report':
> +        if self.__class__ != report_b.__class__:
> +            raise ReportError("Diff of different type of report!")
> +
> +        filename, file_extension = os.path.splitext(self.__path)
> +        diff_report = self.__class__(filename + ".diff" + file_extension)
> +        # Put in the diff report only records of this report that are not
> +        # present in the report_b.
> +        for file_path, entries in self.__entries.items():
> +            rep_b_entries = report_b.get_report_entries()
> +            if file_path in rep_b_entries.keys():
> +                # File path exists in report_b, so check what entries of that
> +                # file path doesn't exist in report_b and add them to the diff
> +                rep_b_entries_num = [
> +                    x.line_number for x in rep_b_entries[file_path]
> +                ]
> +                for entry in entries:
> +                    if entry.line_number not in rep_b_entries_num:
> +                        diff_report.add_entry(file_path, entry.line_number,
> +                                              entry.text)
> +            else:
> +                # File path doesn't exist in report_b, so add every entry
> +                # of that file path to the diff
> +                for entry in entries:
> +                    diff_report.add_entry(file_path, entry.line_number,
> +                                          entry.text)
> +
> +        return diff_report
> -- 
> 2.34.1
>
Luca Fancellu May 17, 2023, 10:50 a.m. UTC | #3
> On 17 May 2023, at 02:26, Stefano Stabellini <sstabellini@kernel.org> wrote:
> 
> On Thu, 4 May 2023, Luca Fancellu wrote:
>> Add a new tool, diff-report.py that can be used to make diff between
>> reports generated by xen-analysis.py tool.
>> Currently this tool supports the Xen cppcheck text report format in
>> its operations.
>> 
>> The tool prints every finding that is in the report passed with -r
>> (check report) which is not in the report passed with -b (baseline).
>> 
>> Signed-off-by: Luca Fancellu <luca.fancellu@arm.com>
> 
> Acked-by: Stefano Stabellini <sstabellini@kernel.org>
> Tested-by: Stefano Stabellini <sstabellini@kernel.org>

Thank you Stefano for taking the time to review and test it, I will push
the new version of the serie with the stale functions removed and I will
add your A-by and T-by.

Cheers,
Luca
diff mbox series

Patch

diff --git a/xen/scripts/diff-report.py b/xen/scripts/diff-report.py
new file mode 100755
index 000000000000..4913fb43a8f9
--- /dev/null
+++ b/xen/scripts/diff-report.py
@@ -0,0 +1,76 @@ 
+#!/usr/bin/env python3
+
+import os, sys
+from argparse import ArgumentParser
+from xen_analysis.diff_tool.debug import Debug
+from xen_analysis.diff_tool.report import ReportError
+from xen_analysis.diff_tool.cppcheck_report import CppcheckReport
+
+
+def log_info(text, end='\n'):
+    global args
+    global file_out
+
+    if (args.verbose):
+        print(text, end=end, file=file_out)
+
+
+def main(argv):
+    global args
+    global file_out
+
+    parser = ArgumentParser(prog="diff-report.py")
+    parser.add_argument("-b", "--baseline", required=True, type=str,
+                        help="Path to the baseline report.")
+    parser.add_argument("--debug", action='store_true',
+                        help="Produce intermediate reports during operations.")
+    parser.add_argument("-o", "--out", default="stdout", type=str,
+                        help="Where to print the tool output. Default is "
+                             "stdout")
+    parser.add_argument("-r", "--report", required=True, type=str,
+                        help="Path to the 'check report', the one checked "
+                             "against the baseline.")
+    parser.add_argument("-v", "--verbose", action='store_true',
+                        help="Print more informations during the run.")
+
+    args = parser.parse_args()
+
+    if args.out == "stdout":
+        file_out = sys.stdout
+    else:
+        try:
+            file_out = open(args.out, "wt")
+        except OSError as e:
+            print("ERROR: Issue opening file {}: {}".format(args.out, e))
+            sys.exit(1)
+
+    debug = Debug(args)
+
+    try:
+        baseline_path = os.path.realpath(args.baseline)
+        log_info("Loading baseline report {}".format(baseline_path), "")
+        baseline = CppcheckReport(baseline_path)
+        baseline.parse()
+        debug.debug_print_parsed_report(baseline)
+        log_info(" [OK]")
+        new_rep_path = os.path.realpath(args.report)
+        log_info("Loading check report {}".format(new_rep_path), "")
+        new_rep = CppcheckReport(new_rep_path)
+        new_rep.parse()
+        debug.debug_print_parsed_report(new_rep)
+        log_info(" [OK]")
+    except ReportError as e:
+        print("ERROR: {}".format(e))
+        sys.exit(1)
+
+    output = new_rep - baseline
+    print(output, end="", file=file_out)
+
+    if len(output) > 0:
+        sys.exit(1)
+
+    sys.exit(0)
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/xen/scripts/xen_analysis/diff_tool/__init__.py b/xen/scripts/xen_analysis/diff_tool/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py b/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
new file mode 100644
index 000000000000..787a51aca583
--- /dev/null
+++ b/xen/scripts/xen_analysis/diff_tool/cppcheck_report.py
@@ -0,0 +1,41 @@ 
+#!/usr/bin/env python3
+
+import re
+from .report import Report, ReportError
+
+
+class CppcheckReport(Report):
+    def __init__(self, report_path: str) -> None:
+        super().__init__(report_path)
+        # This matches a string like:
+        # path/to/file.c(<line number>,<digits>):<whatever>
+        # and captures file name path and line number
+        # the last capture group is used for text substitution in __str__
+        self.__report_entry_regex = re.compile(r'^(.*)\((\d+)(,\d+\):.*)$')
+
+    def parse(self) -> None:
+        report_path = self.get_report_path()
+        try:
+            with open(report_path, "rt") as infile:
+                report_lines = infile.readlines()
+        except OSError as e:
+            raise ReportError("Issue with reading file {}: {}"
+                              .format(report_path, e))
+        for line in report_lines:
+            entry = self.__report_entry_regex.match(line)
+            if entry and entry.group(1) and entry.group(2):
+                file_path = entry.group(1)
+                line_number = int(entry.group(2))
+                self.add_entry(file_path, line_number, line)
+            else:
+                raise ReportError("Malformed report entry in file {}:\n{}"
+                                  .format(report_path, line))
+
+    def __str__(self) -> str:
+        ret = ""
+        for entry in self.to_list():
+            ret += re.sub(self.__report_entry_regex,
+                          r'{}({}\3'.format(entry.file_path,
+                                            entry.line_number),
+                          entry.text)
+        return ret
diff --git a/xen/scripts/xen_analysis/diff_tool/debug.py b/xen/scripts/xen_analysis/diff_tool/debug.py
new file mode 100644
index 000000000000..d46df3300d21
--- /dev/null
+++ b/xen/scripts/xen_analysis/diff_tool/debug.py
@@ -0,0 +1,36 @@ 
+#!/usr/bin/env python3
+
+import os
+from .report import Report
+
+
+class Debug:
+    def __init__(self, args):
+        self.args = args
+
+    def __get_debug_out_filename(self, path: str, type: str) -> str:
+        # Take basename
+        file_name = os.path.basename(path)
+        # Split in name and extension
+        file_name = os.path.splitext(file_name)
+        if self.args.out != "stdout":
+            out_folder = os.path.dirname(self.args.out)
+        else:
+            out_folder = "./"
+        dbg_report_path = out_folder + file_name[0] + type + file_name[1]
+
+        return dbg_report_path
+
+    def __debug_print_report(self, report: Report, type: str) -> None:
+        report_name = self.__get_debug_out_filename(report.get_report_path(),
+                                                    type)
+        try:
+            with open(report_name, "wt") as outfile:
+                print(report, end="", file=outfile)
+        except OSError as e:
+            print("ERROR: Issue opening file {}: {}".format(report_name, e))
+
+    def debug_print_parsed_report(self, report: Report) -> None:
+        if not self.args.debug:
+            return
+        self.__debug_print_report(report, ".parsed")
diff --git a/xen/scripts/xen_analysis/diff_tool/report.py b/xen/scripts/xen_analysis/diff_tool/report.py
new file mode 100644
index 000000000000..d958d1816eb4
--- /dev/null
+++ b/xen/scripts/xen_analysis/diff_tool/report.py
@@ -0,0 +1,114 @@ 
+#!/usr/bin/env python3
+
+import os
+
+
+class ReportError(Exception):
+    pass
+
+
+class Report:
+    class ReportEntry:
+        def __init__(self, file_path: str, line_number: int,
+                     entry_text: list, line_id: int) -> None:
+            if not isinstance(line_number, int) or \
+               not isinstance(line_id, int):
+                raise ReportError("ReportEntry constructor wrong type args")
+            self.file_path = file_path
+            self.line_number = line_number
+            self.text = entry_text
+            self.line_id = line_id
+
+        def __str__(self) -> str:
+            ret = ''
+            header = 'File path:Count\n'
+
+            for path in self.stats:
+                ret += f'{path}: {len(self.stats[path])}\n'
+
+            if ret == '':
+                ret += 'No new issues introduced\n'
+
+            ret = header + ret
+
+            return ret
+
+        def __len__(self) -> int:
+            ret = 0
+
+            for ln_list in self.stats.values():
+                ret += len(ln_list)
+
+            return ret
+
+    def __init__(self, report_path: str) -> None:
+        self.__entries = {}
+        self.__path = report_path
+        self.__last_line_order = 0
+
+    def parse(self) -> None:
+        raise ReportError("Please create a specialised class from 'Report'.")
+
+    def get_report_path(self) -> str:
+        return self.__path
+
+    def get_report_entries(self) -> dict:
+        return self.__entries
+
+    def add_entry(self, entry_path: str, entry_line_number: int,
+                  entry_text: list) -> None:
+        entry = Report.ReportEntry(entry_path, entry_line_number, entry_text,
+                                   self.__last_line_order)
+        if entry_path in self.__entries.keys():
+            self.__entries[entry_path].append(entry)
+        else:
+            self.__entries[entry_path] = [entry]
+        self.__last_line_order += 1
+
+    def to_list(self) -> list:
+        report_list = []
+        for _, entries in self.__entries.items():
+            for entry in entries:
+                report_list.append(entry)
+
+        report_list.sort(key=lambda x: x.line_id)
+        return report_list
+
+    def __str__(self) -> str:
+        ret = ""
+        for entry in self.to_list():
+            ret += entry.file_path + ":" + entry.line_number + ":" + entry.text
+
+        return ret
+
+    def __len__(self) -> int:
+        return len(self.to_list())
+
+    def __sub__(self, report_b: 'Report') -> 'Report':
+        if self.__class__ != report_b.__class__:
+            raise ReportError("Diff of different type of report!")
+
+        filename, file_extension = os.path.splitext(self.__path)
+        diff_report = self.__class__(filename + ".diff" + file_extension)
+        # Put in the diff report only records of this report that are not
+        # present in the report_b.
+        for file_path, entries in self.__entries.items():
+            rep_b_entries = report_b.get_report_entries()
+            if file_path in rep_b_entries.keys():
+                # File path exists in report_b, so check what entries of that
+                # file path doesn't exist in report_b and add them to the diff
+                rep_b_entries_num = [
+                    x.line_number for x in rep_b_entries[file_path]
+                ]
+                for entry in entries:
+                    if entry.line_number not in rep_b_entries_num:
+                        diff_report.add_entry(file_path, entry.line_number,
+                                              entry.text)
+            else:
+                # File path doesn't exist in report_b, so add every entry
+                # of that file path to the diff
+                for entry in entries:
+                    diff_report.add_entry(file_path, entry.line_number,
+                                          entry.text)
+
+        return diff_report