Skip to content

Commit

Permalink
Cleanup plans
Browse files Browse the repository at this point in the history
- Manage cleanup plans (create, edit, remove, close, reopen).
- Assign reports to cleanup plans.
- Filter reports by cleanup plans.
- Add functional and e2e test cases.
  • Loading branch information
csordasmarton committed Sep 2, 2021
1 parent 5aec531 commit 2220d66
Show file tree
Hide file tree
Showing 43 changed files with 2,045 additions and 23 deletions.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion web/api/js/codechecker-api-node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codechecker-api",
"version": "6.42.0",
"version": "6.43.0",
"description": "Generated node.js compatible API stubs for CodeChecker server.",
"main": "lib",
"homepage": "https://github.com/Ericsson/codechecker",
Expand Down
Binary file modified web/api/py/codechecker_api/dist/codechecker_api.tar.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion web/api/py/codechecker_api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
with open('README.md', encoding='utf-8', errors="ignore") as f:
long_description = f.read()

api_version = '6.42.0'
api_version = '6.43.0'

setup(
name='codechecker_api',
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion web/api/py/codechecker_api_shared/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
with open('README.md', encoding='utf-8', errors="ignore") as f:
long_description = f.read()

api_version = '6.42.0'
api_version = '6.43.0'

setup(
name='codechecker_api_shared',
Expand Down
56 changes: 55 additions & 1 deletion web/api/report_server.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ struct ReportFilter {
16: optional ReportDate date, // Dates of the report.
17: optional list<string> analyzerNames, // Names of the code analyzers.
18: optional i64 openReportsDate, // Open reports date in unix time format.
19: optional list<string> cleanupPlans, // Cleanup plan names.
}

struct RunReportCount {
Expand Down Expand Up @@ -405,6 +406,20 @@ struct BlameInfo {
2: list<BlameData> blame,
}

struct CleanupPlan {
1: string name,
2: i64 dueDate, // Unix (epoch) time.
3: string description,
4: i64 closedAt, // Unix (epoch) time.
5: list<string> reportHashes,
}
typedef list<CleanupPlan> CleanupPlans

struct CleanupPlanFilter {
1: list<string> names,
2: bool isOpen,
}

service codeCheckerDBAccess {

// Gives back all analyzed runs.
Expand Down Expand Up @@ -790,5 +805,44 @@ service codeCheckerDBAccess {
// Import data from the server.
// PERMISSION: PRODUCT_ADMIN
bool importData(1: ExportData exportData)
throws (1: codechecker_api_shared.RequestFailed requestError),
throws (1: codechecker_api_shared.RequestFailed requestError),

// Add a new cleanup plan or manage an existing one.
// PERMISSION: PRODUCT_ADMIN
bool addCleanupPlan(1: string name,
2: string description,
3: i64 dueDate)
throws (1: codechecker_api_shared.RequestFailed requestError),

// Get cleanup plans.
// PERMISSION: PRODUCT_VIEW
CleanupPlans getCleanupPlans(1: CleanupPlanFilter filter)
throws (1: codechecker_api_shared.RequestFailed requestError),

// Remove a cleanup plan.
// PERMISSION: PRODUCT_ADMIN
bool removeCleanupPlan(1: string name)
throws (1: codechecker_api_shared.RequestFailed requestError),

// Close a cleanup plan.
// PERMISSION: PRODUCT_ADMIN
bool closeCleanupPlan(1: string name)
throws (1: codechecker_api_shared.RequestFailed requestError),

// Reopen a cleanup plan.
// PERMISSION: PRODUCT_ADMIN
bool reopenCleanupPlan(1: string name)
throws (1: codechecker_api_shared.RequestFailed requestError),

// Add report hashes to the given cleanup plan.
// PERMISSION: PRODUCT_ADMIN
bool setCleanupPlan(1: string cleanupPlanName,
2: list<string> reportHashes)
throws (1: codechecker_api_shared.RequestFailed requestError),

// Remove report hashes from the given cleanup plan.
// PERMISSION: PRODUCT_ADMIN
bool unsetCleanupPlan(1: string cleanupPlanName,
2: list<string> reportHashes)
throws (1: codechecker_api_shared.RequestFailed requestError),
}
2 changes: 1 addition & 1 deletion web/codechecker_web/shared/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# The newest supported minor version (value) for each supported major version
# (key) in this particular build.
SUPPORTED_VERSIONS = {
6: 42
6: 43
}

# Used by the client to automatically identify the latest major and minor
Expand Down
194 changes: 189 additions & 5 deletions web/server/codechecker_server/api/report_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
import os
import re
import shlex
import time
import zlib

from collections import defaultdict
from datetime import datetime, timedelta
from typing import List, Optional
from typing import Dict, List, Optional

import sqlalchemy
from sqlalchemy.sql.expression import or_, and_, not_, func, \
Expand Down Expand Up @@ -46,10 +47,10 @@
from ..database.config_db_model import Product
from ..database.database import conv, DBSession, escape_like
from ..database.run_db_model import \
AnalysisInfo, AnalyzerStatistic, BugPathEvent, BugReportPoint, Comment, \
ExtendedReportData, File, FileContent, Report, ReportAnalysisInfo, \
ReviewStatus, Run, RunHistory, RunHistoryAnalysisInfo, RunLock, \
SourceComponent
AnalysisInfo, AnalyzerStatistic, BugPathEvent, BugReportPoint, \
CleanupPlan, CleanupPlanReportHash, Comment, ExtendedReportData, File, \
FileContent, Report, ReportAnalysisInfo, ReviewStatus, Run, RunHistory, \
RunHistoryAnalysisInfo, RunLock, SourceComponent

from .thrift_enum_helper import detection_status_enum, \
detection_status_str, review_status_enum, review_status_str, \
Expand Down Expand Up @@ -241,6 +242,18 @@ def process_report_filter(session, run_ids, report_filter, cmp_data=None):

AND.append(or_(*OR))

if report_filter.cleanupPlans:
OR = []
for cleanup_plan in report_filter.cleanupPlans:
q = select([CleanupPlanReportHash.bug_hash]) \
.where(
CleanupPlanReportHash.cleanup_plan_name == cleanup_plan) \
.distinct()

OR.append(Report.bug_id.in_(q))

AND.append(or_(*OR))

if report_filter.severity:
AND.append(Report.severity.in_(report_filter.severity))

Expand Down Expand Up @@ -966,6 +979,41 @@ def get_commit_url(
return url


def get_cleanup_plan(session, name: str) -> CleanupPlan:
"""
Check if the given cleanup name exists in the database and returns
the cleanup. Otherwise it will raise an exception.
"""
cleanup_plan = session.query(CleanupPlan).get(name)

if not cleanup_plan:
raise codechecker_api_shared.ttypes.RequestFailed(
codechecker_api_shared.ttypes.ErrorCode.DATABASE,
f"Cleanup plan '{name}' was not found in the database.")

return cleanup_plan


def get_cleanup_plan_report_hashes(
session,
cleanup_plan_names: List[str]
) -> Dict[str, List[str]]:
""" Get report hashes for the given cleanup plan names. """
cleanup_plan_hashes = defaultdict(list)

q = session \
.query(
CleanupPlanReportHash.cleanup_plan_name,
CleanupPlanReportHash.bug_hash) \
.filter(CleanupPlanReportHash.cleanup_plan_name.in_(
cleanup_plan_names))

for name, report_hash in q:
cleanup_plan_hashes[name].append(report_hash)

return cleanup_plan_hashes


class ThriftRequestHandler:
"""
Connect to database and handle thrift client requests.
Expand Down Expand Up @@ -2977,3 +3025,139 @@ def importData(self, exportData):

session.commit()
return True

@exc_to_thrift_reqfail
@timeit
def addCleanupPlan(self, name, description, dueDate):
self.__require_admin()
with DBSession(self._Session) as session:
cleanup_plan = session.query(CleanupPlan).get(name)

if not cleanup_plan:
cleanup_plan = CleanupPlan(name)

cleanup_plan.description = description
cleanup_plan.due_date = \
datetime.fromtimestamp(dueDate) if dueDate else None

session.add(cleanup_plan)
session.commit()

return True

@exc_to_thrift_reqfail
@timeit
def getCleanupPlans(self, cleanup_plan_filter):
self.__require_view()
with DBSession(self._Session) as session:
q = session \
.query(CleanupPlan) \
.order_by(CleanupPlan.name)

if cleanup_plan_filter:
if cleanup_plan_filter.names:
q = q.filter(CleanupPlan.name.in_(
cleanup_plan_filter.names))

if cleanup_plan_filter.isOpen is not None:
if cleanup_plan_filter.isOpen:
q = q.filter(CleanupPlan.closed_at.is_(None))
else:
q = q.filter(CleanupPlan.closed_at.isnot(None))

cleanup_plans = q.all()

cleanup_plan_hashes = get_cleanup_plan_report_hashes(
session, [c.name for c in cleanup_plans])

return [ttypes.CleanupPlan(
name=cp.name,
description=cp.description,
dueDate=int(time.mktime(
cp.due_date.timetuple())) if cp.due_date else None,
closedAt=int(time.mktime(
cp.closed_at.timetuple())) if cp.closed_at else None,
reportHashes=cleanup_plan_hashes[cp.name]) for cp in q]

@exc_to_thrift_reqfail
@timeit
def removeCleanupPlan(self, name):
self.__require_admin()
with DBSession(self._Session) as session:
cleanup_plan = get_cleanup_plan(session, name)
session.delete(cleanup_plan)
session.commit()
LOG.info("Cleanup plan '%s' has been removed by '%s'",
name, self._get_username())
return True

@exc_to_thrift_reqfail
@timeit
def closeCleanupPlan(self, name):
self.__require_admin()

with DBSession(self._Session) as session:
cleanup_plan = get_cleanup_plan(session, name)

cleanup_plan.closed_at = datetime.now()
session.add(cleanup_plan)
session.commit()
LOG.info("Cleanup plan '%s' has been closed by '%s'",
name, self._get_username())
return True

@exc_to_thrift_reqfail
@timeit
def reopenCleanupPlan(self, name):
self.__require_admin()

with DBSession(self._Session) as session:
cleanup_plan = get_cleanup_plan(session, name)

cleanup_plan.closed_at = None
session.add(cleanup_plan)
session.commit()
LOG.info("Cleanup plan '%s' has been reopened by '%s'",
name, self._get_username())
return True

@exc_to_thrift_reqfail
@timeit
def setCleanupPlan(self, name, reportHashes):
self.__require_admin()

with DBSession(self._Session) as session:
get_cleanup_plan(session, name)

q = session \
.query(CleanupPlanReportHash.bug_hash) \
.filter(CleanupPlanReportHash.cleanup_plan_name == name) \
.filter(CleanupPlanReportHash.bug_hash.in_(reportHashes))
new_report_hashes = set(reportHashes) - set(b[0] for b in q)

for report_hash in new_report_hashes:
session.add(CleanupPlanReportHash(
cleanup_plan_name=name, bug_hash=report_hash))

session.commit()

return True

@exc_to_thrift_reqfail
@timeit
def unsetCleanupPlan(self, name, reportHashes):
self.__require_admin()

with DBSession(self._Session) as session:
get_cleanup_plan(session, name)

session \
.query(CleanupPlanReportHash) \
.filter(CleanupPlanReportHash.cleanup_plan_name == name) \
.filter(CleanupPlanReportHash.bug_hash.in_(reportHashes)) \
.delete(synchronize_session=False)

session.commit()
session.close()

return True
29 changes: 29 additions & 0 deletions web/server/codechecker_server/database/run_db_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,35 @@ def __init__(self, name, value, description=None, user_name=None):
self.username = user_name


class CleanupPlan(Base):
__tablename__ = 'cleanup_plans'

name = Column(String, nullable=False, primary_key=True)
due_date = Column(DateTime, nullable=True)
description = Column(String, nullable=True)
closed_at = Column(DateTime, nullable=True)

def __init__(self, name, due_date=None, description=None, closed_at=None):
self.name = name
self.due_date = due_date
self.description = description
self.closed_at = closed_at


class CleanupPlanReportHash(Base):
__tablename__ = 'cleanup_plan_report_hashes'

cleanup_plan_name = Column(
Integer,
ForeignKey('cleanup_plans.name',
deferrable=True,
initially="DEFERRED",
ondelete="CASCADE"),
primary_key=True)

bug_hash = Column(Integer, primary_key=True)


IDENTIFIER = {
'identifier': "RunDatabase",
'orm_meta': CC_META
Expand Down
Loading

0 comments on commit 2220d66

Please sign in to comment.