diff --git a/docs/web/products.md b/docs/web/products.md index 6a529977cb..2dc4540f1a 100644 --- a/docs/web/products.md +++ b/docs/web/products.md @@ -118,6 +118,7 @@ making a new product available on the server. ``` usage: CodeChecker cmd products add [-h] [-n DISPLAY_NAME] [--description DESCRIPTION] + [--report-limit REPORT_LIMIT] [--sqlite SQLITE_FILE | --postgresql] [--dbaddress DBADDRESS] [--dbport DBPORT] [--dbusername DBUSERNAME] @@ -142,6 +143,9 @@ optional arguments: --description DESCRIPTION A custom textual description to be shown alongside the product. + --report-limit REPORT_LIMIT + The maximum number of reports allowed to store in one + run, if exceeded, the storeaction will be rejected. database arguments: NOTE: These database arguments are relative to the server machine, as it diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz similarity index 51% rename from web/api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz rename to web/api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz index 416c878d13..ad14888696 100644 Binary files a/web/api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz and b/web/api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz differ diff --git a/web/api/js/codechecker-api-node/package.json b/web/api/js/codechecker-api-node/package.json index e876b60239..6bdf935418 100644 --- a/web/api/js/codechecker-api-node/package.json +++ b/web/api/js/codechecker-api-node/package.json @@ -1,6 +1,6 @@ { "name": "codechecker-api", - "version": "6.53.0", + "version": "6.54.0", "description": "Generated node.js compatible API stubs for CodeChecker server.", "main": "lib", "homepage": "https://github.com/Ericsson/codechecker", diff --git a/web/api/products.thrift b/web/api/products.thrift index 835c9c00cb..b6b3cf1bda 100644 --- a/web/api/products.thrift +++ b/web/api/products.thrift @@ -33,7 +33,8 @@ struct ProductConfiguration { 5: optional DatabaseConnection connection, 6: i64 runLimit, 7: optional bool isReviewStatusChangeDisabled, - 8: optional Confidentiality confidentiality + 8: optional Confidentiality confidentiality, + 9: optional i64 reportLimit } typedef list ProductConfigurations @@ -53,7 +54,8 @@ struct Product { 11: i64 runLimit, // Number of allowed runs for this product. 12: list admins, // Administrators of this product. 13: list runStoreInProgress, // List of run names which are in progress. - 14: optional Confidentiality confidentiality // Confidentiality classification of the product + 14: optional Confidentiality confidentiality, // Confidentiality classification of the product. + 15: optional i64 reportLimit // Maximum number of reports allowed in a run. } typedef list Products diff --git a/web/api/py/codechecker_api/dist/codechecker_api.tar.gz b/web/api/py/codechecker_api/dist/codechecker_api.tar.gz index fe714e89f6..e2fb1622df 100644 Binary files a/web/api/py/codechecker_api/dist/codechecker_api.tar.gz and b/web/api/py/codechecker_api/dist/codechecker_api.tar.gz differ diff --git a/web/api/py/codechecker_api/setup.py b/web/api/py/codechecker_api/setup.py index f5627f0eed..806b546b36 100644 --- a/web/api/py/codechecker_api/setup.py +++ b/web/api/py/codechecker_api/setup.py @@ -8,7 +8,7 @@ with open('README.md', encoding='utf-8', errors="ignore") as f: long_description = f.read() -api_version = '6.53.0' +api_version = '6.54.0' setup( name='codechecker_api', diff --git a/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz b/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz index 098e124d0a..ee89d70e05 100644 Binary files a/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz and b/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz differ diff --git a/web/api/py/codechecker_api_shared/setup.py b/web/api/py/codechecker_api_shared/setup.py index f16016b4e3..747e74be03 100644 --- a/web/api/py/codechecker_api_shared/setup.py +++ b/web/api/py/codechecker_api_shared/setup.py @@ -8,7 +8,7 @@ with open('README.md', encoding='utf-8', errors="ignore") as f: long_description = f.read() -api_version = '6.53.0' +api_version = '6.54.0' setup( name='codechecker_api_shared', diff --git a/web/client/codechecker_client/cmd/cmd.py b/web/client/codechecker_client/cmd/cmd.py index e8c74600b6..6cb34a94f3 100644 --- a/web/client/codechecker_client/cmd/cmd.py +++ b/web/client/codechecker_client/cmd/cmd.py @@ -743,6 +743,15 @@ def __register_add(parser): help="A custom textual description to be shown " "alongside the product.") + parser.add_argument('--report-limit', + type=int, + dest="report_limit", + default=argparse.SUPPRESS, + required=False, + help="The maximum number of reports allowed to " + "store in one run, if exceeded, the store " + "action will be rejected.") + dbmodes = parser.add_argument_group( "database arguments", "NOTE: These database arguments are relative to the server " diff --git a/web/client/codechecker_client/cmd/store.py b/web/client/codechecker_client/cmd/store.py index b0663bbb7d..2c6271a935 100644 --- a/web/client/codechecker_client/cmd/store.py +++ b/web/client/codechecker_client/cmd/store.py @@ -32,11 +32,13 @@ from codechecker_report_converter import twodim from codechecker_report_converter.report import Report, report_file, \ reports as reports_helper, statistics as report_statistics -from codechecker_report_converter.report.hash import HashType +from codechecker_report_converter.report.hash import HashType, \ + get_report_path_hash from codechecker_report_converter.source_code_comment_handler import \ SourceCodeCommentHandler from codechecker_client import client as libclient +from codechecker_client import product from codechecker_common import arg, logger, cmd_config from codechecker_common.checker_labels import CheckerLabels from codechecker_common.util import load_json @@ -415,7 +417,11 @@ def parse_analyzer_result_files( return analyzer_result_file_reports -def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels): +def assemble_zip(inputs, + zip_file, + client, + prod_client, + checker_labels: CheckerLabels): """Collect and compress report and source files, together with files contanining analysis related information into a zip file which will be sent to the server. @@ -459,6 +465,7 @@ def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels): changed_files = set() file_paths = set() file_report_positions: FileReportPositions = defaultdict(set) + unique_reports = set() for file_path, reports in analyzer_result_file_reports.items(): files_to_compress.add(file_path) stats.num_of_analyzer_result_files += 1 @@ -467,6 +474,9 @@ def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels): if report.changed_files: changed_files.update(report.changed_files) continue + # Need to calculate unique reoirt count to determine report limit + unique_reports.add(get_report_path_hash(report)) + stats.add_report(report) file_paths.update(report.original_files) @@ -576,6 +586,25 @@ def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels): # Print statistics what will be stored to the server. stats.write() + # Fail store early if too many reports. + p = prod_client.getCurrentProduct() + if len(unique_reports) > p.reportLimit: + LOG.error(f"""Report Limit Exceeded + +This report folder cannot be stored because the number of reports in the +result folder is too high. Usually noisy checkers, generating a lot of +reports are not useful and it is better to disable them. + +Run `CodeChecker parse ` to gain a better understanding on +report counts. + +Disable checkers that have generated an excessive number of reports and then +rerun the analysis to be able to store the results on the server. + +Configured report limit for this product: {p.reportLimit} + """) + sys.exit(1) + zip_size = os.stat(zip_file).st_size LOG.info("Compressing report zip file...") @@ -761,6 +790,12 @@ def main(args): # Setup connection to the remote server. client = libclient.setup_client(args.product_url) + protocol, host, port, product_name = \ + product.split_product_url(args.product_url) + prod_client = libclient.setup_product_client(protocol, + host, + port, + product_name=product_name) zip_file_handle, zip_file = tempfile.mkstemp('.zip') LOG.debug("Will write mass store ZIP to '%s'...", zip_file) @@ -770,7 +805,11 @@ def main(args): LOG.debug("Assembling zip file.") try: - assemble_zip(args.input, zip_file, client, context.checker_labels) + assemble_zip(args.input, + zip_file, + client, + prod_client, + context.checker_labels) except Exception as ex: print(ex) import traceback diff --git a/web/client/codechecker_client/product_client.py b/web/client/codechecker_client/product_client.py index 725cd6c372..618bb4b7dc 100644 --- a/web/client/codechecker_client/product_client.py +++ b/web/client/codechecker_client/product_client.py @@ -113,10 +113,15 @@ def handle_add_product(args): desc = convert.to_b64(args.description) \ if 'description' in args else None + report_limit = None + if hasattr(args, "report_limit") and args.report_limit: + report_limit = int(args.report_limit) + prod = ProductConfiguration( endpoint=args.endpoint, displayedName_b64=name, description_b64=desc, + reportLimit=report_limit, connection=dbc) LOG.debug("Sending request to add product...") diff --git a/web/client/codechecker_client/thrift_call.py b/web/client/codechecker_client/thrift_call.py index 0b55f646b9..e3544ef945 100644 --- a/web/client/codechecker_client/thrift_call.py +++ b/web/client/codechecker_client/thrift_call.py @@ -56,26 +56,36 @@ def wrapper(self, *args, **kwargs): except codechecker_api_shared.ttypes.RequestFailed as reqfailure: LOG.error('Calling API endpoint: %s', funcName) if reqfailure.errorCode ==\ - codechecker_api_shared.ttypes.ErrorCode.DATABASE: - LOG.error('Database error on server\n%s', - str(reqfailure.message)) - elif reqfailure.errorCode ==\ - codechecker_api_shared.ttypes.ErrorCode.AUTH_DENIED: - LOG.error('Authentication denied\n %s', - str(reqfailure.message)) - elif reqfailure.errorCode ==\ - codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED: - LOG.error('Unauthorized to access\n %s', - str(reqfailure.message)) - LOG.error('Ask the product admin for additional access ' - 'rights.') - elif reqfailure.errorCode ==\ - codechecker_api_shared.ttypes.ErrorCode.API_MISMATCH: - LOG.error('Client/server API mismatch\n %s', - str(reqfailure.message)) + codechecker_api_shared.ttypes.ErrorCode.GENERAL and \ + reqfailure.extraInfo and \ + reqfailure.extraInfo[0] == "report_limit": + # We handle this error in near the business logic. + raise reqfailure else: - LOG.error('API call error: %s\n%s', funcName, str(reqfailure)) - sys.exit(1) + if reqfailure.errorCode ==\ + codechecker_api_shared.ttypes.ErrorCode.DATABASE: + LOG.error('Database error on server\n%s', + str(reqfailure.message)) + elif reqfailure.errorCode ==\ + codechecker_api_shared.ttypes.ErrorCode.AUTH_DENIED: + LOG.error('Authentication denied\n %s', + str(reqfailure.message)) + elif reqfailure.errorCode ==\ + codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED: + LOG.error('Unauthorized to access\n %s', + str(reqfailure.message)) + LOG.error('Ask the product admin for additional access ' + 'rights.') + elif reqfailure.errorCode ==\ + codechecker_api_shared.ttypes.ErrorCode.API_MISMATCH: + LOG.error('Client/server API mismatch\n %s', + str(reqfailure.message)) + else: + LOG.error('API call error: %s\n%s', + funcName, + str(reqfailure) + ) + sys.exit(1) except TApplicationException as ex: LOG.error("Internal server error: %s", str(ex.message)) sys.exit(1) diff --git a/web/codechecker_web/shared/version.py b/web/codechecker_web/shared/version.py index 52c8d760d9..c045815196 100644 --- a/web/codechecker_web/shared/version.py +++ b/web/codechecker_web/shared/version.py @@ -18,7 +18,7 @@ # The newest supported minor version (value) for each supported major version # (key) in this particular build. SUPPORTED_VERSIONS = { - 6: 53 + 6: 54 } # Used by the client to automatically identify the latest major and minor diff --git a/web/server/codechecker_server/api/mass_store_run.py b/web/server/codechecker_server/api/mass_store_run.py index e15362eeca..75a6a98569 100644 --- a/web/server/codechecker_server/api/mass_store_run.py +++ b/web/server/codechecker_server/api/mass_store_run.py @@ -266,6 +266,7 @@ def __init__( self.__analysis_info: Dict[str, AnalysisInfo] = {} self.__duration: int = 0 self.__report_count: int = 0 + self.__report_limit: int = 0 self.__wrong_src_code_comments: List[str] = [] self.__already_added_report_hashes: Set[str] = set() self.__severity_map: Dict[str, int] = {} @@ -273,6 +274,8 @@ def __init__( self.__all_report_checkers: Set[str] = set() self.__added_reports: List[Tuple[DBReport, Report]] = list() + self.__get_report_limit_for_product() + @property def __manager(self): return self.__report_server._manager @@ -1011,6 +1014,7 @@ def get_missing_file_ids(report: Report) -> List[str]: else: fixed_at = run_history_time + self.__check_report_count() report_id = self.__add_report( session, run_id, report, file_path_to_id, rs_from_source, detection_status, detected_at, @@ -1069,6 +1073,41 @@ def __validate_and_add_report_annotations( f"'{ALLOWED_ANNOTATIONS[key]['display']}'." ) + def __get_report_limit_for_product(self): + with DBSession(self.__config_database) as session: + product = session.query(Product).get(self.__product.id) + if product.report_limit: + self.__report_limit = product.report_limit + + def __check_report_count(self): + """ + This method comparest the already added report count to the report + limit, Raises exception if the number of reports is more than the + that is configured for the product. + """ + if len(self.__added_reports) >= self.__report_limit: + LOG.error("The number of reports in the given report folder is " + + "larger than the allowed." + + f"The limit: {self.__report_limit}!") + extra_info = [ + "report_limit", + f"limit:{self.__report_limit}" + ] + raise codechecker_api_shared.ttypes.RequestFailed( + codechecker_api_shared.ttypes. + ErrorCode.GENERAL, + "**Report Limit Exceeded** " + + "This report folder cannot be stored because the number of " + + "reports in the result folder is too high. Usually noisy " + + "checkers, generating a lot of reports are not useful and " + + "it is better to disable them. Run `CodeChecker parse " + + "` to gain a better understanding on report " + + "counts. Disable checkers that have generated an excessive " + + "number of reports and then rerun the analysis to be able " + + "to store the results on the server. " + + f"Limit: {self.__report_limit}", + extra_info) + def __store_reports( self, session: DBSession, diff --git a/web/server/codechecker_server/api/product_server.py b/web/server/codechecker_server/api/product_server.py index ad11dad4f2..1d14307fdb 100644 --- a/web/server/codechecker_server/api/product_server.py +++ b/web/server/codechecker_server/api/product_server.py @@ -129,6 +129,8 @@ def __get_product(self, session, product): confidentiality = \ confidentiality_enum(product.confidentiality) + report_limit = product.report_limit + return server_product, ttypes.Product( id=product.id, endpoint=product.endpoint, @@ -141,7 +143,8 @@ def __get_product(self, session, product): administrating=self.__administrating(args), databaseStatus=server_product.db_status, admins=[admin.name for admin in admins], - confidentiality=confidentiality) + confidentiality=confidentiality, + reportLimit=report_limit) @timeit def getPackageVersion(self): @@ -295,6 +298,7 @@ def getProductConfiguration(self, product_id): description_b64=descr, connection=dbc, runLimit=product.run_limit, + reportLimit=product.report_limit, isReviewStatusChangeDisabled=is_review_status_change_disabled, confidentiality=confidentiality) @@ -387,6 +391,7 @@ def addProduct(self, product): name=displayed_name, description=description, run_limit=product.runLimit, + report_limit=product.reportLimit, is_review_status_change_disabled=is_rws_change_disabled, confidentiality=confidentiality) @@ -583,6 +588,7 @@ def editProduct(self, product_id, new_config): # Update the settings in the database. product.endpoint = new_config.endpoint product.run_limit = new_config.runLimit + product.report_limit = new_config.reportLimit product.is_review_status_change_disabled = \ new_config.isReviewStatusChangeDisabled product.connection = conn_str diff --git a/web/server/codechecker_server/database/config_db_model.py b/web/server/codechecker_server/database/config_db_model.py index 48c94f9115..376162d90c 100644 --- a/web/server/codechecker_server/database/config_db_model.py +++ b/web/server/codechecker_server/database/config_db_model.py @@ -39,6 +39,7 @@ class Product(Base): display_name = Column(String, nullable=False) description = Column(Text) run_limit = Column(Integer) + report_limit = Column(Integer, nullable=False, server_default="500000") num_of_runs = Column(Integer, server_default="0") latest_storage_date = Column(DateTime, nullable=True) @@ -49,13 +50,15 @@ class Product(Base): confidentiality = Column(String, nullable=True) def __init__(self, endpoint, conn_str, name=None, description=None, - run_limit=None, is_review_status_change_disabled=False, + run_limit=None, report_limit=500000, + is_review_status_change_disabled=False, confidentiality=None): self.endpoint = endpoint self.connection = conn_str self.display_name = name if name else endpoint self.description = description self.run_limit = run_limit + self.report_limit = report_limit self.is_review_status_change_disabled = \ True if is_review_status_change_disabled else False self.confidentiality = confidentiality diff --git a/web/server/codechecker_server/migrations/config/versions/00099e8bc212_store_limit.py b/web/server/codechecker_server/migrations/config/versions/00099e8bc212_store_limit.py new file mode 100644 index 0000000000..22b459797f --- /dev/null +++ b/web/server/codechecker_server/migrations/config/versions/00099e8bc212_store_limit.py @@ -0,0 +1,28 @@ +"""Store limit + +Revision ID: 00099e8bc212 +Revises: 7829789fc19c +Create Date: 2023-03-10 16:45:19.301602 + +""" + +# revision identifiers, used by Alembic. +revision = '00099e8bc212' +down_revision = '7829789fc19c' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('products', sa.Column('report_limit', sa.Integer(), server_default='500000', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('products', 'report_limit') + # ### end Alembic commands ### diff --git a/web/server/vue-cli/package-lock.json b/web/server/vue-cli/package-lock.json index e6ee7a3154..6860da6d5a 100644 --- a/web/server/vue-cli/package-lock.json +++ b/web/server/vue-cli/package-lock.json @@ -11,7 +11,7 @@ "@mdi/font": "^6.5.95", "chart.js": "^2.9.4", "chartjs-plugin-datalabels": "^0.7.0", - "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz", "codemirror": "^5.65.0", "date-fns": "^2.28.0", "js-cookie": "^3.0.1", @@ -4834,9 +4834,9 @@ } }, "node_modules/codechecker-api": { - "version": "6.53.0", - "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz", - "integrity": "sha512-156RuFb5HCRhvxVbvJDbNKUsjxuga4DdjaiC0FbIORhQlIt/Wm1RtBxx9mPVp+YTJCpWh3MxFTrvxH8JlIb6zA==", + "version": "6.54.0", + "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz", + "integrity": "sha512-1+5Q0B4ehDO7s9bHe0iIqprajdQqVl9iW4TPjyMq9ITlsN5/XQLhzyCwmvhlADpNLKVQVY7oAuvQ67JVMw62GQ==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "thrift": "0.13.0-hotfix.1" @@ -19756,8 +19756,8 @@ "dev": true }, "codechecker-api": { - "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz", - "integrity": "sha512-156RuFb5HCRhvxVbvJDbNKUsjxuga4DdjaiC0FbIORhQlIt/Wm1RtBxx9mPVp+YTJCpWh3MxFTrvxH8JlIb6zA==", + "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz", + "integrity": "sha512-1+5Q0B4ehDO7s9bHe0iIqprajdQqVl9iW4TPjyMq9ITlsN5/XQLhzyCwmvhlADpNLKVQVY7oAuvQ67JVMw62GQ==", "requires": { "thrift": "0.13.0-hotfix.1" } diff --git a/web/server/vue-cli/package.json b/web/server/vue-cli/package.json index 5f4b729ef6..3923339d3b 100644 --- a/web/server/vue-cli/package.json +++ b/web/server/vue-cli/package.json @@ -27,9 +27,9 @@ }, "dependencies": { "@mdi/font": "^6.5.95", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.54.0.tgz", "chart.js": "^2.9.4", "chartjs-plugin-datalabels": "^0.7.0", - "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.53.0.tgz", "codemirror": "^5.65.0", "date-fns": "^2.28.0", "js-cookie": "^3.0.1", diff --git a/web/server/vue-cli/src/components/Product/ProductConfigForm.vue b/web/server/vue-cli/src/components/Product/ProductConfigForm.vue index 09142a126b..f76a170f1c 100644 --- a/web/server/vue-cli/src/components/Product/ProductConfigForm.vue +++ b/web/server/vue-cli/src/components/Product/ProductConfigForm.vue @@ -39,6 +39,27 @@ @input="productConfig.runLimit = $event || null" /> + + + + + The maximum number of reports allowed to + store in one run, if exceeded, the store + action will be rejected. + + + @@ -196,6 +217,9 @@ export default { ], runLimit: [ v => (!v || !!v && !isNaN(parseInt(v))) || "Number is required" + ], + reportLimit: [ + v => (!v || !!v && !isNaN(parseInt(v))) || "Number is required" ] }, }; diff --git a/web/tests/functional/store/test_store.py b/web/tests/functional/store/test_store.py index 3dc6097b4a..9bcf3a8151 100644 --- a/web/tests/functional/store/test_store.py +++ b/web/tests/functional/store/test_store.py @@ -62,13 +62,19 @@ def setup_class(self): 'workspace': TEST_WORKSPACE, 'checkers': [], 'reportdir': os.path.join(TEST_WORKSPACE, 'reports'), - 'test_project': 'store_test' + 'test_project': 'store_test', + 'test_project_2': 'store_limited_product' } # Start or connect to the running CodeChecker server and get connection # details. print("This test uses a CodeChecker server... connecting...") server_access = codechecker.start_or_get_server() + + server_access['viewer_product'] = 'store_limited_product' + codechecker.add_test_package_product(server_access, TEST_WORKSPACE, + report_limit=2) + server_access['viewer_product'] = 'store_test' codechecker.add_test_package_product(server_access, TEST_WORKSPACE) @@ -104,6 +110,11 @@ def teardown_class(self): 'codechecker_cfg']['check_env'] codechecker.remove_test_package_product(TEST_WORKSPACE, check_env) + codechecker.remove_test_package_product( + TEST_WORKSPACE, + check_env, + product="store_limited_product") + print("Removing: " + TEST_WORKSPACE) shutil.rmtree(TEST_WORKSPACE, ignore_errors=True) @@ -115,7 +126,9 @@ def setup_method(self, method): 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_directory = os.path.dirname(os.path.abspath(inspect.getfile( inspect.currentframe()))) self._temp_workspace = os.path.join(self._codechecker_cfg["workspace"], @@ -130,13 +143,20 @@ def setup_method(self, method): self.product_name = self._codechecker_cfg['viewer_product'] + self.limited_product_name = self._codechecker_cfg['test_project_2'] + # Setup a viewer client to test viewer API calls. self._cc_client = env.setup_viewer_client(self._test_workspace) self.assertIsNotNone(self._cc_client) self._pr_client = env.setup_product_client( self._test_workspace, product=self.product_name) + + self._limited_pr_client = env.setup_product_client( + self._test_workspace, product=self.limited_product_name) + self.assertIsNotNone(self._pr_client) + self.assertIsNotNone(self._limited_pr_client) def test_product_details(self): """ @@ -454,3 +474,21 @@ def analyze_tidy(cfg): reports = json.loads(out) self.assertEqual(len(reports), 2) + + def test_store_limit(self): + """ + Test store limit of a product. + """ + + run_name = "limit_test" + store_cmd = [ + env.codechecker_cmd(), "store", + self._divide_zero_workspace, + "--name", run_name, + "--url", env.parts_to_url(self._codechecker_cfg, 'test_project_2'), + "--trim-path-prefix", self._divide_zero_workspace, + "--verbose", "debug", + ] + + _, out, _ = _call_cmd(store_cmd) + self.assertIn("Report Limit Exceeded", out) diff --git a/web/tests/libtest/codechecker.py b/web/tests/libtest/codechecker.py index 4ec0859b6e..2dd773727b 100644 --- a/web/tests/libtest/codechecker.py +++ b/web/tests/libtest/codechecker.py @@ -717,7 +717,7 @@ def start_server_proc(event, server_cmd, checking_env): def add_test_package_product(server_data, test_folder, check_env=None, - protocol='http', + protocol='http', report_limit=None, user_permissions=DEFAULT_USER_PERMISSIONS): """ Add a product for a test suite to the server provided by server_data. @@ -745,6 +745,8 @@ def add_test_package_product(server_data, test_folder, check_env=None, '--name', os.path.basename(test_folder), '--description', "Automatically created product for test.", '--verbose', 'debug'] + if report_limit: + add_command.extend(['--report-limit', str(report_limit)]) # If tests are running on postgres, we need to create a database. pg_config = env.get_postgresql_cfg() @@ -806,7 +808,8 @@ def add_test_package_product(server_data, test_folder, check_env=None, raise Exception("Failed to add the product to the test server!") -def remove_test_package_product(test_folder, check_env=None, protocol='http'): +def remove_test_package_product(test_folder, check_env=None, protocol='http', + product=None): """ Remove the product associated with the given test folder. The folder must exist, as the server configuration is read from the folder. @@ -817,6 +820,7 @@ def remove_test_package_product(test_folder, check_env=None, protocol='http'): server_data = env.import_test_cfg(test_folder)['codechecker_cfg'] print(server_data) + product_to_remove = product if product else server_data['viewer_product'] if 'check_env' not in server_data: server_data['check_env'] = check_env @@ -827,12 +831,11 @@ def remove_test_package_product(test_folder, check_env=None, protocol='http'): str(server_data['viewer_port']), '') del_command = ['CodeChecker', 'cmd', 'products', 'del', - server_data['viewer_product'], - '--url', url] + product_to_remove, '--url', url] print(' '.join(del_command)) - # Authenticate as SUPERUSER to be able to create the product. + # Authenticate as SUPERUSER to be able to delete the product. login(server_data, test_folder, "root", "root", protocol) returncode = subprocess.call( del_command, @@ -845,7 +848,7 @@ def remove_test_package_product(test_folder, check_env=None, protocol='http'): # SQLite databases are deleted automatically as part of the # workspace removal. if env.get_postgresql_cfg(): - env.del_database(server_data['viewer_product'], check_env) + env.del_database(product_to_remove, check_env) if returncode: raise Exception("Failed to remove the product from the test server!") diff --git a/web/tests/libtest/env.py b/web/tests/libtest/env.py index ec5e932028..b6642771fc 100644 --- a/web/tests/libtest/env.py +++ b/web/tests/libtest/env.py @@ -256,13 +256,13 @@ def get_run_names(workspace): return import_test_cfg(workspace)['codechecker_cfg']['run_names'] -def parts_to_url(codechecker_cfg): +def parts_to_url(codechecker_cfg, product='viewer_product'): """ Creates a product URL string from the test configuration dict. """ return codechecker_cfg['viewer_host'] + ':' + \ str(codechecker_cfg['viewer_port']) + '/' + \ - codechecker_cfg['viewer_product'] + codechecker_cfg[product] def get_workspace(test_id='test'):