Skip to content

Commit

Permalink
[report-converter] Enable multiple input for report-converter
Browse files Browse the repository at this point in the history
Sometimes dynamic analysis is executed for the code base several times
with different parameters. The output of these analyses may go to
different log files which are given to report-converter one-by-one.
If there are reports for the same source file in different log output
files then the resulting .plist files will be overwritten at a
subsequent execution of report-converter.

In this patch we make it possible to give multiple log files or
directories containing log files to the report-converter. The reports
in these files and directories will be collected all and will be
converted to .plist without overriding the ones coming from a different
log file.
  • Loading branch information
bruntib committed Jun 20, 2023
1 parent f8c6ef0 commit 0c62f8a
Show file tree
Hide file tree
Showing 29 changed files with 173 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from abc import ABCMeta, abstractmethod
from collections import defaultdict
import hashlib
from typing import Dict, List, Optional
from typing import Dict, Iterable, List, Optional

from codechecker_report_converter.report import Report, report_file
from codechecker_report_converter.report.hash import get_report_hash, HashType
Expand All @@ -37,7 +37,7 @@ class AnalyzerResultBase(metaclass=ABCMeta):

def transform(
self,
analyzer_result_file_path: str,
analyzer_result_file_paths: Iterable[str],
output_dir_path: str,
export_type: str,
file_name: str = "{source_file}_{analyzer}_{file_hash}",
Expand All @@ -53,23 +53,27 @@ def transform(
export_type)
return False

analyzer_result_file_path = os.path.abspath(analyzer_result_file_path)
reports = self.get_reports(analyzer_result_file_path)
if not reports:
LOG.info("No '%s' results can be found in the given code analyzer "
"output.", self.TOOL_NAME)
return False
all_reports = []

self._post_process_result(reports)
for file_path in analyzer_result_file_paths:
file_path = os.path.abspath(file_path)
reports = self.get_reports(file_path)
if not reports:
LOG.info("No '%s' results can be found in '%s'.",
self.TOOL_NAME, file_path)

for report in reports:
report.analyzer_result_file_path = analyzer_result_file_path
self._post_process_result(reports)

for report in reports:
report.analyzer_result_file_path = file_path

if not report.checker_name:
report.checker_name = self.TOOL_NAME

if not report.checker_name:
report.checker_name = self.TOOL_NAME
all_reports.extend(reports)

self._write(
reports, output_dir_path, parser, export_type, file_name)
all_reports, output_dir_path, parser, export_type, file_name)

if metadata:
self._save_metadata(metadata, output_dir_path)
Expand All @@ -79,7 +83,7 @@ def transform(
"and analysis command when storing the results to it. "
"For more information see the --help.")

return True
return bool(all_reports)

@abstractmethod
def get_reports(self, analyzer_result_file_path: str) -> List[Report]:
Expand Down
57 changes: 50 additions & 7 deletions tools/report-converter/codechecker_report_converter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@


import argparse
from collections.abc import Iterable, Sequence
import glob
import importlib
import logging
import os
import shutil
import sys

from typing import Dict, Optional, Tuple
from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union


# If we run this script in an environment where 'codechecker_report_converter'
Expand Down Expand Up @@ -79,7 +80,7 @@ class RawDescriptionDefaultHelpFormatter(


def transform_output(
analyzer_result: str,
analyzer_result: Iterable[str],
parser_type: str,
output_dir: str,
file_name: str,
Expand Down Expand Up @@ -118,15 +119,57 @@ def process_metadata(metadata) -> Tuple[Dict[str, str], Dict[str, str]]:
return valid_values, invalid_values


class CollectFiles(argparse.Action):
"""
This report-converter tool can be given a set of files and directories as
positional command line arguments. This action collects all files at the
given locations: if an argument is a file then that file is collected, if
it's a directory then its content is inspected recursively.
"""
def __init__(
self,
option_strings: Sequence[str],
dest: str,
nargs: Union[int, str, None],
**kwargs
) -> None:
if nargs != "+":
raise ValueError("'nargs' option for 'input' should be '+'.")
super().__init__(option_strings, dest, nargs, **kwargs)

def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: Union[str, Sequence[Any], None],
option_string: Union[str, None] = None
):
# Type of "values" can have any of the indicated types above. But in
# argument parser it is given by "nargs='+'", so it will be a list in
# this specific case.
assert isinstance(values, Sequence)

all_files = []

for path in values:
if os.path.isfile(path):
all_files.append(path)
else:
for root, _, files in os.walk(path):
all_files.extend(os.path.join(root, f) for f in files)

setattr(namespace, 'input', all_files)


def __add_arguments_to_parser(parser):
""" Add arguments to the the given parser. """
parser.add_argument('input',
type=str,
metavar='file',
nargs='+',
action=CollectFiles,
default=argparse.SUPPRESS,
help="Code analyzer output result file which will be "
"parsed and used to generate a CodeChecker "
"report directory.")
help="Code analyzer output result files or "
"directories which will be parsed and used to "
"generate a CodeChecker report directory.")

parser.add_argument('-o', '--output',
dest="output_dir",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ def __init__(self, dict_type=dict):
# complain that too many arguments are used to invoke __init__, but
# with newer interpreters, this is deadcode.
if len(params) == 3 and "use_builtin_types" in params:
# Before 3.9 interpreter.
# pylint: disable=E1121
_PlistParser.__init__(self, True, dict_type)
# After 3.9 interpreter.
else:
# After 3.9 interpreter.
_PlistParser.__init__(self, dict_type) # pylint: disable=E1120

self.event_handler = _LXMLPlistEventHandler()
Expand Down
38 changes: 38 additions & 0 deletions tools/report-converter/tests/functional/cmdline/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,41 @@ def test_metadata(self):
self.assertEqual(tool['name'], "golint")
self.assertEqual(tool['version'], analyzer_version)
self.assertEqual(tool['command'], analyzer_command)

def test_input_dir(self):
""" Execution with file and directory inputs. """
def count_reports(report_dir):
counter = 0

for plist_file in os.listdir(report_dir):
plist_file = os.path.join(report_dir, plist_file)
with open(plist_file, encoding="utf-8", errors="ignore") as f:
counter += f.read().count(
"issue_hash_content_of_line_in_context")

return counter

test_dir = os.path.dirname(os.path.realpath(__file__))
outputs_dir = os.path.join(test_dir, "test_files", "test_outputs")

with tempfile.TemporaryDirectory() as tmp_dir:
ret = subprocess.call(['report-converter', '-t', 'golint',
'-o', tmp_dir, outputs_dir])
self.assertEqual(0, ret)

self.assertEqual(
len(glob.glob(os.path.join(tmp_dir, '*.plist'))), 3)

self.assertEqual(count_reports(tmp_dir), 4)

test_input1 = os.path.join(outputs_dir, "subdir", "simple2.out")
test_input2 = os.path.join(outputs_dir, "subdir", "simple3.out")
with tempfile.TemporaryDirectory() as tmp_dir:
ret = subprocess.call(['report-converter', '-t', 'golint',
'-o', tmp_dir, test_input1, test_input2])
self.assertEqual(0, ret)

self.assertEqual(
len(glob.glob(os.path.join(tmp_dir, '*.plist'))), 2)

self.assertEqual(count_reports(tmp_dir), 3)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
./files/simple.go:3:6: exported type T should have comment or be unexported
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
./files/simple.go:5:5: exported var Z should have its own declaration
./files/b/simple.go:3:6: exported type T2 should have comment or be unexported
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
./files/b/simple.go:5:5: exported var Z2 should have its own declaration
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def tearDown(self):
def test_asan(self):
""" Test for the asan.plist file. """
self.analyzer_result.transform(
'asan.out', self.cc_result_dir, plist.EXTENSION,
['asan.out'], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

with open('asan.plist', mode='rb') as pfile:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __check_analyzer_result(self, analyzer_result, analyzer_result_plist,
source_files, expected_plist):
""" Check the result of the analyzer transformation. """
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir, analyzer_result_plist)
Expand All @@ -75,13 +75,13 @@ def __check_analyzer_result(self, analyzer_result, analyzer_result_plist,
def test_empty1(self):
""" Test for empty Messages. """
ret = self.analyzer_result.transform(
'empty1.out', self.cc_result_dir, plist.EXTENSION)
['empty1.out'], self.cc_result_dir, plist.EXTENSION)
self.assertFalse(ret)

def test_empty2(self):
""" Test for empty Messages with multiple line. """
ret = self.analyzer_result.transform(
'empty2.out', self.cc_result_dir, plist.EXTENSION)
['empty2.out'], self.cc_result_dir, plist.EXTENSION)
self.assertFalse(ret)

def test_tidy1(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_no_cocci_output_file(self):
'sample.c')

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

Expand All @@ -52,15 +52,15 @@ def test_transform_dir(self):
analyzer_result = os.path.join(self.test_files)

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

def test_transform_single_file(self):
""" Test transforming single output file. """
analyzer_result = os.path.join(self.test_files, 'sample.out')
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def test_no_plist_file(self):
'divide_zero.cpp')

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

Expand All @@ -68,7 +68,7 @@ def test_no_plist_dir(self):
analyzer_result = os.path.join(self.test_files, 'non_existing')

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

Expand All @@ -77,7 +77,7 @@ def test_transform_single_file(self):
analyzer_result = os.path.join(
self.test_files, 'out', 'divide_zero.plist')
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir,
Expand All @@ -102,7 +102,7 @@ def test_transform_directory(self):
""" Test transforming a directory of plist files. """
analyzer_result = os.path.join(self.test_files, 'out')
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_no_cpplint_output_file(self):
'sample.cpp')

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

Expand All @@ -51,15 +51,15 @@ def test_transform_dir(self):
analyzer_result = os.path.join(self.test_files)

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

def test_transform_single_file(self):
""" Test transforming single output file. """
analyzer_result = os.path.join(self.test_files, 'sample.out')
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_no_json_file(self):
'index.js')

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

Expand All @@ -51,15 +51,15 @@ def test_transform_dir(self):
analyzer_result = os.path.join(self.test_files)

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

def test_transform_single_file(self):
""" Test transforming single plist file. """
analyzer_result = os.path.join(self.test_files, 'reports.json')
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_no_go_output_file(self):
'simple.go')

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

Expand All @@ -51,15 +51,15 @@ def test_transform_dir(self):
analyzer_result = os.path.join(self.test_files)

ret = self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")
self.assertFalse(ret)

def test_transform_single_file(self):
""" Test transforming single plist file. """
analyzer_result = os.path.join(self.test_files, 'simple.out')
self.analyzer_result.transform(
analyzer_result, self.cc_result_dir, plist.EXTENSION,
[analyzer_result], self.cc_result_dir, plist.EXTENSION,
file_name="{source_file}_{analyzer}")

plist_file = os.path.join(self.cc_result_dir,
Expand Down
Loading

0 comments on commit 0c62f8a

Please sign in to comment.