Skip to content

Commit 8659f56

Browse files
Include skiplist file in the analysis info
This patch introduces a new table called AnalysisInfoFile, which is intended to store files related to analysis information. The actual file contents are stored separately in the FileContent table. During CodeChecker analyze, we create a new directory called conf/ in the report directory. The entire conf/ directory will be added to the massStoreRun ZIP file. With this PR, only the skipfile is copied to the conf directory, but this could be extended later.
1 parent a14dff8 commit 8659f56

10 files changed

Lines changed: 218 additions & 38 deletions

File tree

analyzer/codechecker_analyzer/cli/analyze.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,34 @@ def __update_review_status_config(args):
10941094
os.symlink(args.review_status_config, rs_config_to_send)
10951095

10961096

1097+
def __update_analysis_config_files(args):
1098+
"""
1099+
Copy analysis related configuration files (e.g. skipfile)
1100+
to report_dir/conf/.
1101+
This directory will be included in the ZIP file,
1102+
which will be stored on the server.
1103+
"""
1104+
conf_dir = os.path.join(args.output_path, "conf")
1105+
1106+
# Remove any config files used during previous analysis
1107+
if os.path.isdir(conf_dir):
1108+
shutil.rmtree(conf_dir)
1109+
1110+
# Create a new conf directory
1111+
os.makedirs(conf_dir)
1112+
1113+
def add_file_to_conf_dir(file_path: str):
1114+
if not os.path.isfile(file_path):
1115+
return
1116+
1117+
file_path = os.path.abspath(file_path)
1118+
filename = os.path.basename(file_path)
1119+
shutil.copyfile(file_path, os.path.join(conf_dir, filename))
1120+
1121+
if 'skipfile' in args:
1122+
add_file_to_conf_dir(args.skipfile)
1123+
1124+
10971125
def __cleanup_metadata(metadata_prev, metadata):
10981126
""" Cleanup metadata.
10991127
@@ -1455,6 +1483,7 @@ def main(args):
14551483

14561484
__update_skip_file(args)
14571485
__update_review_status_config(args)
1486+
__update_analysis_config_files(args)
14581487

14591488
LOG.debug("Cleanup metadata file started.")
14601489
__cleanup_metadata(metadata_prev, metadata)

web/client/codechecker_client/cli/store.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,13 @@ def assemble_zip(inputs,
505505
files_to_compress[os.path.dirname(review_status_file_path)]\
506506
.add(review_status_file_path)
507507

508+
# Add files from report_dir/conf/ directory
509+
conf_dir = os.path.join(dir_path, "conf")
510+
if os.path.isdir(conf_dir):
511+
for file in os.listdir(os.fsencode(conf_dir)):
512+
conf_file = os.path.join(conf_dir, os.fsdecode(file))
513+
files_to_compress[conf_dir].add(conf_file)
514+
508515
LOG.debug(f"Processing {len(analyzer_result_file_paths)} report files ...")
509516

510517
analyzer_result_file_reports = parse_analyzer_result_files(

web/server/codechecker_server/api/mass_store_run.py

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
from collections import defaultdict
1616
from datetime import datetime, timedelta
1717
import fnmatch
18+
import hashlib
1819
from hashlib import sha256
1920
import json
2021
import os
2122
from pathlib import Path
2223
import sqlalchemy
24+
from sqlalchemy.orm import Session as SA_Session
2325
import tempfile
2426
import time
2527
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, \
@@ -46,7 +48,7 @@
4648
from ..database.config_db_model import Product
4749
from ..database.database import DBSession
4850
from ..database.run_db_model import \
49-
AnalysisInfo, AnalysisInfoChecker, AnalyzerStatistic, \
51+
AnalysisInfo, AnalysisInfoChecker, AnalysisInfoFile, AnalyzerStatistic, \
5052
BugPathEvent, BugReportPoint, \
5153
Checker, \
5254
ExtendedReportData, \
@@ -814,8 +816,8 @@ def __add_file_content(
814816
self,
815817
session: DBSession,
816818
source_file_name: str,
817-
content_hash: Optional[str]
818-
):
819+
content_hash: Optional[str] = None
820+
) -> str:
819821
"""
820822
Add the necessary file contents. If content_hash in None then this
821823
function calculates the content hash. Or if it's available at the
@@ -871,6 +873,8 @@ def __add_file_content(
871873
# the meantime.
872874
session.rollback()
873875

876+
return content_hash
877+
874878
def __store_checker_identifiers(self, checkers: Set[Tuple[str, str]]):
875879
"""
876880
Stores the identifiers "(analyzer, checker_name)" in the database into
@@ -1000,6 +1004,32 @@ def __store_analysis_statistics(
10001004

10011005
session.add(analyzer_statistics)
10021006

1007+
def __store_analysis_info_files(
1008+
self,
1009+
session: SA_Session,
1010+
analysis_info_id: int,
1011+
report_dir_path: str
1012+
):
1013+
""" Store analyzer related config files (e.g. skipfile) """
1014+
conf_dir_path = os.path.join(report_dir_path, "conf")
1015+
zip_conf_dir = os.path.join(
1016+
self._zip_dir, "reports",
1017+
hashlib.md5(conf_dir_path.encode('utf-8')).hexdigest())
1018+
1019+
if not os.path.isdir(zip_conf_dir):
1020+
return
1021+
1022+
for file in os.listdir(os.fsencode(zip_conf_dir)):
1023+
conf_file = os.path.join(zip_conf_dir, os.fsdecode(file))
1024+
content_hash = self.__add_file_content(session, conf_file)
1025+
1026+
if (not session.get(AnalysisInfoFile,
1027+
(analysis_info_id, content_hash))):
1028+
session.add(AnalysisInfoFile(
1029+
analysis_info_id=analysis_info_id,
1030+
filename=os.path.basename(conf_file),
1031+
content_hash=content_hash))
1032+
10031033
def __store_analysis_info(
10041034
self,
10051035
session: DBSession,
@@ -1012,37 +1042,30 @@ def __store_analysis_info(
10121042
analyzer_command.encode("utf-8"),
10131043
zlib.Z_BEST_COMPRESSION)
10141044

1015-
analysis_info_rows = session \
1016-
.query(AnalysisInfo) \
1017-
.filter(AnalysisInfo.analyzer_command == cmd) \
1018-
.all()
1019-
1020-
if analysis_info_rows:
1021-
# It is possible when multiple runs are stored
1022-
# simultaneously to the server with the same analysis
1023-
# command that multiple entries are stored into the
1024-
# database. In this case we will select the first one.
1025-
analysis_info = analysis_info_rows[0]
1026-
else:
1027-
analysis_info = AnalysisInfo(analyzer_command=cmd)
1028-
1029-
# Obtain the ID eagerly to be able to use the M-to-N table.
1030-
session.add(analysis_info)
1031-
session.flush()
1032-
session.refresh(analysis_info, ["id"])
1033-
1034-
for analyzer in mip.analyzers:
1035-
q = session \
1036-
.query(Checker) \
1037-
.filter(Checker.analyzer_name == analyzer)
1038-
db_checkers = {r.checker_name: r for r in q.all()}
1039-
1040-
connection_rows = [AnalysisInfoChecker(
1041-
analysis_info, db_checkers[chk], is_enabled)
1042-
for chk, is_enabled
1043-
in mip.checkers.get(analyzer, {}).items()]
1044-
for r in connection_rows:
1045-
session.add(r)
1045+
analysis_info = AnalysisInfo(analyzer_command=cmd)
1046+
1047+
# Obtain the ID eagerly to be able to use the M-to-N table.
1048+
session.add(analysis_info)
1049+
session.flush()
1050+
session.refresh(analysis_info, ["id"])
1051+
1052+
for analyzer in mip.analyzers:
1053+
q = session \
1054+
.query(Checker) \
1055+
.filter(Checker.analyzer_name == analyzer)
1056+
db_checkers = {r.checker_name: r for r in q.all()}
1057+
1058+
connection_rows = [AnalysisInfoChecker(
1059+
analysis_info, db_checkers[chk], is_enabled)
1060+
for chk, is_enabled
1061+
in mip.checkers.get(analyzer, {}).items()]
1062+
for r in connection_rows:
1063+
session.add(r)
1064+
1065+
if mip.report_dir_path:
1066+
self.__store_analysis_info_files(session,
1067+
analysis_info.id,
1068+
mip.report_dir_path)
10461069

10471070
run_history.analysis_info.append(analysis_info)
10481071
self.__analysis_info[src_dir_path] = analysis_info

web/server/codechecker_server/api/report_server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@
6464
from ..database.config_db_model import Product
6565
from ..database.database import conv, DBSession, escape_like
6666
from ..database.run_db_model import \
67-
AnalysisInfo, AnalysisInfoChecker as DB_AnalysisInfoChecker, \
67+
AnalysisInfo, \
68+
AnalysisInfoChecker as DB_AnalysisInfoChecker, AnalysisInfoFile, \
6869
AnalyzerStatistic, \
6970
BugPathEvent, BugReportPoint, \
7071
CleanupPlan, CleanupPlanReportHash, Checker, Comment, \
@@ -1909,6 +1910,19 @@ def getAnalysisInfo(self, analysis_info_filter, limit, offset):
19091910
checkers[analyzer][checker] = API_AnalysisInfoChecker(
19101911
enabled=enabled)
19111912

1913+
analysis_config_files = session \
1914+
.query(AnalysisInfoFile.filename,
1915+
FileContent.content) \
1916+
.join(FileContent, AnalysisInfoFile.content_hash
1917+
== FileContent.content_hash) \
1918+
.filter(AnalysisInfoFile.analysis_info_id
1919+
== cmd.id).all()
1920+
1921+
# Append analysis files to the command string
1922+
for filename, content in analysis_config_files:
1923+
command += f"\n\n{filename}:\n"
1924+
command += zlib.decompress(content).decode("utf-8")
1925+
19121926
res.append(ttypes.AnalysisInfo(
19131927
analyzerCommand=html.escape(command),
19141928
checkers=checkers))

web/server/codechecker_server/database/db_cleanup.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import Dict
1414

1515
import sqlalchemy
16+
from sqlalchemy import union
1617

1718
from codechecker_api.codeCheckerDBAccess_v6.ttypes import Severity
1819

@@ -21,7 +22,7 @@
2122

2223
from .database import DBSession
2324
from .run_db_model import \
24-
AnalysisInfo, \
25+
AnalysisInfo, AnalysisInfoFile, \
2526
BugPathEvent, BugReportPoint, \
2627
Comment, Checker, \
2728
File, FileContent, \
@@ -108,8 +109,9 @@ def remove_unused_files(product):
108109
if total_count:
109110
LOG.debug("%d dangling files deleted.", total_count)
110111

111-
files = session.query(File.content_hash) \
112-
.group_by(File.content_hash)
112+
files = union(
113+
session.query(File.content_hash),
114+
session.query(AnalysisInfoFile.content_hash))
113115

114116
session.query(FileContent) \
115117
.filter(FileContent.content_hash.notin_(files)) \

web/server/codechecker_server/database/run_db_model.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,41 @@ def __init__(self,
7979
self.enabled = is_enabled
8080

8181

82+
class AnalysisInfoFile(Base):
83+
__tablename__ = "analysis_info_files"
84+
85+
analysis_info_id = Column(Integer,
86+
ForeignKey("analysis_info.id",
87+
deferrable=True,
88+
initially="DEFERRED",
89+
ondelete="CASCADE"),
90+
primary_key=True)
91+
92+
filename = Column(String, nullable=False)
93+
94+
content_hash = Column(String,
95+
ForeignKey("file_contents.content_hash",
96+
deferrable=True,
97+
initially="DEFERRED",
98+
ondelete="CASCADE"),
99+
primary_key=True)
100+
101+
def __init__(self,
102+
analysis_info_id: int,
103+
filename: str,
104+
content_hash: str):
105+
self.analysis_info_id = analysis_info_id
106+
self.filename = filename
107+
self.content_hash = content_hash
108+
109+
82110
class AnalysisInfo(Base):
83111
__tablename__ = "analysis_info"
84112

85113
id = Column(Integer, autoincrement=True, primary_key=True)
86114
analyzer_command = Column(LargeBinary)
87115
available_checkers = relationship(AnalysisInfoChecker, uselist=True)
116+
analyzer_files = relationship(AnalysisInfoFile, uselist=True)
88117

89118
def __init__(self, analyzer_command: bytes):
90119
self.analyzer_command = analyzer_command

web/server/codechecker_server/metadata.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def __init__(self, metadata_file_path):
6666
self.disabled_checkers: DisabledCheckers = set()
6767
self.checker_to_analyzer: CheckerToAnalyzer = {}
6868

69+
self.report_dir_path = None
70+
6971
self.__metadata_dict: Dict[str, Any] = {}
7072
if os.path.isfile(metadata_file_path):
7173
self.__metadata_dict = cast(Dict[str, Any],
@@ -184,6 +186,9 @@ def __process_metadata_info_v2(self):
184186
if tool['name'] == 'codechecker' and 'version' in tool:
185187
cc_versions.add(tool['version'])
186188

189+
if tool['name'] == 'codechecker':
190+
self.report_dir_path = tool.get('output_path')
191+
187192
if 'command' in tool:
188193
check_commands.add(' '.join(tool['command']))
189194

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Add analysis_info_files table
3+
4+
Revision ID: baa4ab435da6
5+
Revises: 24c9660f82b1
6+
Create Date: 2026-04-15 15:48:18.762625
7+
"""
8+
9+
from logging import getLogger
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# Revision identifiers, used by Alembic.
16+
revision = 'baa4ab435da6'
17+
down_revision = '24c9660f82b1'
18+
branch_labels = None
19+
depends_on = None
20+
21+
22+
def upgrade():
23+
LOG = getLogger("migration/report")
24+
# ### commands auto generated by Alembic - please adjust! ###
25+
op.create_table(
26+
'analysis_info_files',
27+
sa.Column('analysis_info_id', sa.Integer(), nullable=False),
28+
sa.Column('filename', sa.String(), nullable=False),
29+
sa.Column('content_hash', sa.String(), nullable=False),
30+
sa.ForeignKeyConstraint(
31+
['analysis_info_id'], ['analysis_info.id'],
32+
name=op.f(
33+
'fk_analysis_info_files_analysis_info_id_analysis_info'),
34+
ondelete='CASCADE', initially='DEFERRED', deferrable=True),
35+
sa.ForeignKeyConstraint(
36+
['content_hash'], ['file_contents.content_hash'],
37+
name=op.f(
38+
'fk_analysis_info_files_content_hash_file_contents'),
39+
ondelete='CASCADE', initially='DEFERRED', deferrable=True),
40+
sa.PrimaryKeyConstraint(
41+
'analysis_info_id', 'content_hash',
42+
name=op.f('pk_analysis_info_files'))
43+
)
44+
# ### end Alembic commands ###
45+
46+
47+
def downgrade():
48+
LOG = getLogger("migration/report")
49+
# ### commands auto generated by Alembic - please adjust! ###
50+
op.drop_table('analysis_info_files')
51+
# ### end Alembic commands ###
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-*.txt

0 commit comments

Comments
 (0)