Skip to content

Commit

Permalink
[client] Fix storage of multiple report directory
Browse files Browse the repository at this point in the history
It is possible that the same project is analyzed with different
configuration (e.g.: ctu, non-ctu) to different report directories and
the user want to store these results with one CodeChecker store command.

This command will create a storage zip file with all the plist files in it
under the reports directory. It is possible the the same file name can be
found in multiple report directories so for this reason we need to create a
unique plist file name when we build the zip file.
  • Loading branch information
csordasmarton committed Apr 13, 2021
1 parent a628f5e commit 14bda6c
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 30 deletions.
11 changes: 11 additions & 0 deletions web/client/codechecker_client/cmd/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,17 @@ def assemble_zip(inputs, zip_file, client):
# Add the files to the zip which will be sent to the server.
for ftc in files_to_compress:
_, filename = os.path.split(ftc)

# It is possible that multiple reports directories are given during
# the store command which contain the same plist file name but with
# different content. For this reason for plist files we need to
# generate a more unique file name when putting it to the storage
# zip file.
if filename.endswith(".plist"):
filename = \
(f"{os.path.splitext(filename)[0]}_"
f"{hashlib.md5(ftc.encode('utf-8')).hexdigest()}.plist")

zip_target = os.path.join('reports', filename)
zipf.write(ftc, zip_target)

Expand Down
13 changes: 11 additions & 2 deletions web/tests/functional/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from libtest import codechecker
from libtest import env
from libtest import plist_test

# Test workspace initialized at setup for report storage tests.
TEST_WORKSPACE = None
Expand All @@ -30,8 +31,6 @@ def setup_package():

# Configuration options.
codechecker_cfg = {
'suppress_file': None,
'skip_list_file': None,
'check_env': env.test_env(TEST_WORKSPACE),
'workspace': TEST_WORKSPACE,
'checkers': [],
Expand All @@ -52,6 +51,16 @@ def setup_package():
# Export the test configuration to the workspace.
env.export_test_cfg(TEST_WORKSPACE, {'codechecker_cfg': codechecker_cfg})

# Copy test files to a temporary directory not to modify the
# files in the repository.
# Report files will be overwritten during the tests.
test_dir = os.path.dirname(os.path.realpath(__file__))
dst_dir = os.path.join(TEST_WORKSPACE, "test_proj")
shutil.copytree(os.path.join(test_dir, "test_proj"), dst_dir)

report_file = os.path.join(dst_dir, "divide_zero.plist")
plist_test.prefix_file_path(report_file, dst_dir)


def teardown_package():
"""Clean up after the test."""
Expand Down
2 changes: 1 addition & 1 deletion web/tests/functional/store/test_proj/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# for the store update tests.

build:
$(CXX) divide_zero.cpp -o divide_zero
$(CXX) -c divide_zero.cpp -o /dev/null

clean:
rm -rf divide_zero
Expand Down
5 changes: 5 additions & 0 deletions web/tests/functional/store/test_proj/project_info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "store",
"clean_cmd": "make clean",
"build_cmd": "make build"
}
92 changes: 65 additions & 27 deletions web/tests/functional/store/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
import json
import os
import plistlib
import shutil
import subprocess
import unittest

from libtest import codechecker
from libtest import env
from libtest import plist_test

from codechecker_common import util

Expand All @@ -36,54 +35,39 @@ def setUp(self):
print("Running " + test_class + " tests in " + self._test_workspace)

self._test_cfg = env.import_test_cfg(self._test_workspace)
self.codechecker_cfg = self._test_cfg["codechecker_cfg"]
self.test_proj_dir = os.path.join(
self.codechecker_cfg["workspace"], "test_proj")

def test_tim_path_prefix_store(self):
def test_trim_path_prefix_store(self):
"""Trim the path prefix from the sored reports.
The source file paths are converted to absolute with the
temporary test directory, the test trims that temporary
test directory from the source file path during the storage.
"""

test_dir = os.path.dirname(os.path.realpath(__file__))

report_dir = os.path.join(test_dir, "test_proj")

codechecker_cfg = self._test_cfg["codechecker_cfg"]

# Copy report files to a temporary directory not to modify the
# files in the repository.
# Report files will be overwritten during the tests.
temp_workspace = os.path.join(
codechecker_cfg["workspace"], "test_proj"
)
shutil.copytree(report_dir, temp_workspace)

report_file = os.path.join(temp_workspace, "divide_zero.plist")

# Convert file paths to absolute in the report.
plist_test.prefix_file_path(report_file, temp_workspace)
report_file = os.path.join(self.test_proj_dir, "divide_zero.plist")

report_content = {}
with open(report_file, mode="rb") as rf:
report_content = plistlib.load(rf)

trimmed_paths = [
util.trim_path_prefixes(path, [temp_workspace])
util.trim_path_prefixes(path, [self.test_proj_dir])
for path in report_content["files"]
]

run_name = "store_test"
store_cmd = [
env.codechecker_cmd(),
"store",
temp_workspace,
self.test_proj_dir,
"--name",
run_name,
"--url",
env.parts_to_url(codechecker_cfg),
env.parts_to_url(self.codechecker_cfg),
"--trim-path-prefix",
temp_workspace,
self.test_proj_dir,
"--verbose",
"debug",
]
Expand All @@ -105,7 +89,7 @@ def test_tim_path_prefix_store(self):
run_name,
# Use the 'Default' product.
"--url",
env.parts_to_url(codechecker_cfg),
env.parts_to_url(self.codechecker_cfg),
"-o",
"json",
]
Expand All @@ -120,3 +104,57 @@ def test_tim_path_prefix_store(self):
self.assertEqual(len(reports), 4)
for report in reports:
self.assertIn(report["checkedFile"], trimmed_paths)

def test_store_multiple_report_dirs(self):
""" Test storing multiple report directories.
Analyze the same project to different report directories with different
checker configurations and store these report directories with one
store command to a run.
"""
cfg = dict(self.codechecker_cfg)
codechecker.log(cfg, self.test_proj_dir)

report_dir1 = os.path.join(self.test_proj_dir, 'report_dir1')
report_dir2 = os.path.join(self.test_proj_dir, 'report_dir2')

cfg['reportdir'] = report_dir1
cfg['checkers'] = [
'-d', 'core.DivideZero', '-e', 'deadcode.DeadStores']
codechecker.analyze(cfg, self.test_proj_dir)

cfg['reportdir'] = report_dir2
cfg['checkers'] = [
'-e', 'core.DivideZero', '-d', 'deadcode.DeadStores']
codechecker.analyze(cfg, self.test_proj_dir)

run_name = "multiple_report_dirs"
store_cmd = [
env.codechecker_cmd(), "store",
report_dir1, report_dir2,
"--name", run_name,
"--url", env.parts_to_url(self.codechecker_cfg)]

proc = subprocess.Popen(
store_cmd, encoding="utf-8", errors="ignore",
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
_, err = proc.communicate()

self.assertNotIn("UserWarning: Duplicate name", err)

# Check the reports.
query_cmd = [
env.codechecker_cmd(), "cmd", "results",
run_name,
"--url", env.parts_to_url(self.codechecker_cfg),
"-o", "json"]

out = subprocess.check_output(
query_cmd, encoding="utf-8", errors="ignore")
reports = json.loads(out)

self.assertTrue(
any(r['checkerId'] == 'core.DivideZero' for r in reports))

self.assertTrue(
any(r['checkerId'] == 'deadcode.DeadStores' for r in reports))

0 comments on commit 14bda6c

Please sign in to comment.