diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.69.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.69.0.tgz deleted file mode 100644 index c55bae37e2..0000000000 Binary files a/web/api/js/codechecker-api-node/dist/codechecker-api-6.69.0.tgz and /dev/null differ diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.70.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.70.0.tgz new file mode 100644 index 0000000000..09d3b765e7 Binary files /dev/null and b/web/api/js/codechecker-api-node/dist/codechecker-api-6.70.0.tgz differ diff --git a/web/api/js/codechecker-api-node/package.json b/web/api/js/codechecker-api-node/package.json index 8921159775..b47d543df9 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.69.0", + "version": "6.70.0", "description": "Generated node.js compatible API stubs for CodeChecker server.", "main": "lib", "homepage": "https://github.com/Ericsson/codechecker", 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 e3707cf43d..695b1d232b 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 07209f6809..ecf0d97ee1 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.69.0' +api_version = '6.70.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 ff580aaf65..202acc1df2 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 605a39c2b8..f789554aa4 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.69.0' +api_version = '6.70.0' setup( name='codechecker_api_shared', diff --git a/web/api/report_server.thrift b/web/api/report_server.thrift index 3745c7e84b..60a133427f 100644 --- a/web/api/report_server.thrift +++ b/web/api/report_server.thrift @@ -946,6 +946,21 @@ service codeCheckerDBAccess { 5: i64 offset) throws (1: codechecker_api_shared.RequestFailed requestError), + // Returns detailed report statistics grouped by file. + // The inner map contains total report count ("reports") and + // counts per severity, review status and detection status. + // If the run id list is empty the metrics will be counted + // for all of the runs and in compare mode all of the runs + // will be used as a baseline excluding the runs in compare data. + // PERMISSION: PRODUCT_VIEW + map> getFileCountsSummary( + 1: list runIds, + 2: ReportFilter reportFilter, + 3: CompareData cmpData, + 4: i64 limit, + 5: i64 offset) + throws (1: codechecker_api_shared.RequestFailed requestError), + // If the run id list is empty the metrics will be counted // for all of the runs and in compare mode all of the runs // will be used as a baseline excluding the runs in compare data. diff --git a/web/client/codechecker_client/helpers/results.py b/web/client/codechecker_client/helpers/results.py index 4285b5db4b..bfc42ef6ae 100644 --- a/web/client/codechecker_client/helpers/results.py +++ b/web/client/codechecker_client/helpers/results.py @@ -140,6 +140,11 @@ def getDetectionStatusCounts(self, runIds, reportFilter, cmpData): def getFileCounts(self, runIds, reportFilter, cmpData, limit, offset): pass + @thrift_client_call + def getFileCountsSummary(self, runIds, reportFilter, cmpData, limit, + offset): + pass + @thrift_client_call def getCheckerCounts(self, base_run_ids, reportFilter, cmpData, limit, offset): diff --git a/web/codechecker_web/shared/version.py b/web/codechecker_web/shared/version.py index eb8d5245f7..75d538edef 100644 --- a/web/codechecker_web/shared/version.py +++ b/web/codechecker_web/shared/version.py @@ -20,7 +20,7 @@ # The newest supported minor version (value) for each supported major version # (key) in this particular build. SUPPORTED_VERSIONS = { - 6: 69 + 6: 70 } # Used by the client to automatically identify the latest major and minor diff --git a/web/server/codechecker_server/api/report_server.py b/web/server/codechecker_server/api/report_server.py index 8119ba085b..e45785c4f8 100644 --- a/web/server/codechecker_server/api/report_server.py +++ b/web/server/codechecker_server/api/report_server.py @@ -3718,6 +3718,100 @@ def getFileCounts(self, run_ids, report_filter, cmp_data, limit, offset): results[fp] = count return results + @exc_to_thrift_reqfail + @timeit + def getFileCountsSummary(self, run_ids, report_filter, cmp_data, + limit, offset): + # Returns detailed report statistics grouped by file. + # The inner map contains total report count ("reports") and + # counts per severity, review status and detection status. + # If the run id list is empty the metrics will be counted + # for all of the runs and in compare mode all of the runs + # will be used as a baseline excluding the runs in compare data. + # PERMISSION: PRODUCT_VIEW + self.__require_view() + + limit = verify_limit_range(limit) + + results = {} + with DBSession(self._Session) as session: + filter_expression, join_tables = process_report_filter( + session, run_ids, report_filter, cmp_data) + + # Get distinct file paths with pagination. + distinct_file_path = session.query(File.filepath.distinct()) \ + .join(Report, Report.file_id == File.id) + + if report_filter.annotations is not None: + distinct_file_path = distinct_file_path.outerjoin( + ReportAnnotations, + ReportAnnotations.report_id == Report.id) + distinct_file_path = distinct_file_path.group_by( + Report.id) + + distinct_file_path = apply_report_filter( + distinct_file_path, filter_expression, join_tables, [File]) + + if limit: + distinct_file_path = distinct_file_path.limit(limit) \ + .offset(offset) + + count_col = Report.bug_id.distinct() if \ + report_filter.isUnique else Report.bug_id + + # Query: file path, severity, review status, + # detection status, and count. + stmt = session.query( + File.filepath, + Checker.severity, + Report.review_status, + Report.detection_status, + func.count(count_col).label('cnt')) \ + .join(Report, Report.file_id == File.id) \ + .join(Checker, Report.checker_id == Checker.id) \ + .filter(File.filepath.in_(distinct_file_path)) + + stmt = apply_report_filter( + stmt, filter_expression, join_tables, [File, Checker]) + + stmt = stmt.group_by( + File.filepath, + Checker.severity, + Report.review_status, + Report.detection_status) + + severity_names = ttypes.Severity._VALUES_TO_NAMES + + for fp, sev, review_st, detect_st, cnt in stmt: + if fp not in results: + results[fp] = {} + + file_summary = results[fp] + + # Total report count. + file_summary["reports"] = \ + file_summary.get("reports", 0) + cnt + + # Severity count. + sev_name = severity_names.get(sev, str(sev)) + sev_key = f"severity:{sev_name}" + file_summary[sev_key] = \ + file_summary.get(sev_key, 0) + cnt + + # Review status count. + if review_st: + rs_key = f"review_status:{review_st}" + file_summary[rs_key] = \ + file_summary.get(rs_key, 0) + cnt + + # Detection status count. + if detect_st: + ds_key = f"detection_status:{detect_st}" + file_summary[ds_key] = \ + file_summary.get(ds_key, 0) + cnt + + return results + @exc_to_thrift_reqfail @timeit def getRunHistoryTagCounts(self, run_ids, report_filter, cmp_data, limit, diff --git a/web/server/vue-cli/e2e/pages/filePathTree.js b/web/server/vue-cli/e2e/pages/filePathTree.js new file mode 100644 index 0000000000..86d05a9706 --- /dev/null +++ b/web/server/vue-cli/e2e/pages/filePathTree.js @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------- +// Part of the CodeChecker project, under the Apache License v2.0 with +// LLVM Exceptions. See LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ------------------------------------------------------------------------- + +// Page object for the file-path filter's tree-based menu content +// (FilePathFilter.vue). Shares the same overall layout as the report page +// (login is performed via browser.page.login() in the spec), but only +// exposes the tree-specific elements/sections. + +const commands = { + waitForProgressBarNotPresent() { + this.pause(500, () => { + this.waitForElementNotPresent("@progressBar"); + }); + return this; + }, + + openFilePathFilterMenu() { + const filterSection = this.section.filePathFilter; + filterSection.click("@expansionBtn"); + filterSection.click("@settings"); + this.expect.section("@filePathTreeMenu").to.be.visible.before(5000); + return this; + } +}; + +module.exports = { + url: function() { + return this.api.launchUrl + "/e2e/reports?review-status=Unreviewed&" + + "review-status=Confirmed%20bug&detection-status=New&" + + "detection-status=Reopened&detection-status=Unresolved"; + }, + commands: [ commands ], + elements: { + page: ".v-data-table", + progressBar: ".v-data-table__progress" + }, + sections: { + filePathFilter: { + selector: "#filepath", + elements: { + expansionBtn: ".expansion-btn", + settings: ".settings-btn", + clearBtn: ".clear-btn", + selectedItems: ".selected-item" + } + }, + filePathTreeMenu: { + // The settings-menu popup that hosts the file-path tree. + selector: ".settings-menu.menuable__content__active", + elements: { + searchInput: "header input[type='text']", + anywhereSwitch: ".v-input--switch", + tree: ".file-path-tree", + treeNode: ".file-path-tree .v-treeview-node", + treeRootNode: ".file-path-tree > .v-treeview-node", + treeNodeRoot: ".file-path-tree .v-treeview-node__root", + treeItemLabel: ".file-path-tree .tree-item-label", + treeCheckbox: ".file-path-tree .v-treeview-node__checkbox", + applyBtn: ".apply-btn", + clearAllBtn: ".clear-all-btn" + } + } + } +}; diff --git a/web/server/vue-cli/e2e/pages/reportsTree.js b/web/server/vue-cli/e2e/pages/reportsTree.js new file mode 100644 index 0000000000..f3210f04f1 --- /dev/null +++ b/web/server/vue-cli/e2e/pages/reportsTree.js @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------- +// Part of the CodeChecker project, under the Apache License v2.0 with +// LLVM Exceptions. See LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ------------------------------------------------------------------------- + +// Page object for the new view-mode toggle (Report List / File Tree) +// and the file-tree container in web/server/vue-cli/src/views/Reports.vue. + +const commands = { + waitForProgressBarNotPresent() { + this.pause(500, () => { + this.waitForElementNotPresent("@progressBar"); + }); + return this; + }, + + switchToTreeView() { + this + .click("@treeViewBtn") + .pause(500) + .waitForElementNotPresent("@progressBar"); + this.expect.element("@treeViewContainer").to.be.visible.before(5000); + return this; + }, + + switchToTableView() { + this + .click("@tableViewBtn") + .pause(500) + .waitForElementNotPresent("@progressBar"); + this.expect.element("@dataTable").to.be.visible.before(5000); + return this; + } +}; + +module.exports = { + url: function() { + return this.api.launchUrl + "/e2e/reports?review-status=Unreviewed&" + + "review-status=Confirmed%20bug&detection-status=New&" + + "detection-status=Reopened&detection-status=Unresolved"; + }, + commands: [ commands ], + elements: { + page: ".v-data-table", + progressBar: ".v-data-table__progress", + + // View-mode toggle (Vuetify v-btn-toggle with two v-btn entries). + viewModeToggle: ".v-btn-toggle", + tableViewBtn: ".v-btn-toggle .v-btn:nth-child(1)", + treeViewBtn: ".v-btn-toggle .v-btn:nth-child(2)", + + dataTable: ".v-data-table", + + treeViewContainer: ".tree-view-container", + treeHeader: ".tree-view-container .tree-header", + treeHeaderName: ".tree-view-container .tree-header-name", + treeHeaderCell: ".tree-view-container .tree-header-cell", + treeRow: ".tree-view-container .tree-row", + treeItemLabel: + ".tree-view-container .tree-item-label.clickable", + treeStatCell: ".tree-view-container .tree-stat-cell" + }, + sections: { + // Re-declare the file-path filter section so this spec can assert that + // clicking a tree item populates it. + filePathFilter: { + selector: "#filepath", + elements: { + expansionBtn: ".expansion-btn", + clearBtn: ".clear-btn", + selectedItems: ".selected-item" + } + } + } +}; diff --git a/web/server/vue-cli/e2e/specs/filePathTree.js b/web/server/vue-cli/e2e/specs/filePathTree.js new file mode 100644 index 0000000000..90d906d1b2 --- /dev/null +++ b/web/server/vue-cli/e2e/specs/filePathTree.js @@ -0,0 +1,184 @@ +// ------------------------------------------------------------------------- +// Part of the CodeChecker project, under the Apache License v2.0 with +// LLVM Exceptions. See LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ------------------------------------------------------------------------- + +// E2E tests for the new tree view inside the File path filter +// (web/server/vue-cli/src/components/Report/ReportFilter/Filters/ +// FilePathFilter.vue). + +module.exports = { + before(browser) { + browser.resizeWindow(1600, 1000); + + const login = browser.page.login(); + const filePathTreePage = browser.page.filePathTree(); + + login + .navigate(filePathTreePage.url()) + .loginAsRoot(); + + browser.assert.urlContains("/e2e/reports"); + + filePathTreePage + .waitForElementVisible("@page", 10000) + .waitForProgressBarNotPresent(); + }, + + after(browser) { + browser.perform(() => { + browser.end(); + }); + }, + + "open the file path filter tree menu" (browser) { + const page = browser.page.filePathTree(); + page.openFilePathFilterMenu(); + + page.expect.section("@filePathTreeMenu").to.be.visible.before(5000); + + const menu = page.section.filePathTreeMenu; + // The tree should render at least one node when reports exist. + menu.expect.element("@tree").to.be.present.before(5000); + menu.api.elements("@treeRootNode", ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length > 0, + "file-path tree should contain at least one root node" + ); + }); + }, + + "filter the tree using the search input" (browser) { + const page = browser.page.filePathTree(); + const menu = page.section.filePathTreeMenu; + + // Filter for everything (matches all files). + menu.clearAndSetValue("@searchInput", "*", menu); + menu + .pause(500) + .api.elements("@treeRootNode", ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length > 0, + "wildcard filter should keep at least one root node visible" + ); + }); + + // Filter for a string that should match nothing. + menu.clearAndSetValue("@searchInput", + "definitely_not_a_real_path_zzz", menu); + menu + .pause(500) + .api.elements("@treeRootNode", ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length === 0, + "non-matching filter should hide every node" + ); + }); + + // Reset filter for subsequent tests. + menu.clearAndSetValue("@searchInput", "", menu); + menu.pause(300); + }, + + "toggle the anywhere-on-report-path switch" (browser) { + const page = browser.page.filePathTree(); + const menu = page.section.filePathTreeMenu; + + menu.expect.element("@anywhereSwitch").to.be.visible.before(5000); + menu + .click("@anywhereSwitch") + .pause(300) + .click("@anywhereSwitch") + .pause(300); + // No assertion on tree contents (data-dependent); we only verify the + // switch is interactive and does not throw. + }, + + "expand the first folder node" (browser) { + const page = browser.page.filePathTree(); + const menu = page.section.filePathTreeMenu; + + // Click the first node label area to toggle expansion (open-on-click). + menu + .click({ selector: "@treeNodeRoot", index: 0 }) + .pause(500); + + // After expansion, the total visible node count must not decrease. + menu.api.elements("@treeNode", ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length >= 1, + "expanding a folder should reveal at least the original nodes" + ); + }); + }, + + "select a tree item and apply" (browser) { + const page = browser.page.filePathTree(); + const menu = page.section.filePathTreeMenu; + + menu.expect.element("@treeCheckbox").to.be.present.before(5000); + + // Tick the first checkbox (independent selection mode means a single + // node is enough to enable the Apply button). + menu + .click({ selector: "@treeCheckbox", index: 0 }) + .pause(300); + + menu.click("@applyBtn"); + + // Menu must close after applying. + page.expect.section("@filePathTreeMenu") + .to.not.be.present.before(5000); + + page.waitForProgressBarNotPresent(); + + page.section.filePathFilter.api.elements("@selectedItems", + ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length >= 1, + "applying a tree selection should add at least one filter chip" + ); + }); + }, + + "clear the file path filter via the section clear button" (browser) { + const page = browser.page.filePathTree(); + const section = page.section.filePathFilter; + + section.click("@clearBtn"); + page.waitForProgressBarNotPresent(); + + section.api.elements("@selectedItems", ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length === 0, + "clear button should remove every file-path filter chip" + ); + }); + }, + + "clear-all button inside the tree menu closes and clears" (browser) { + const page = browser.page.filePathTree(); + page.openFilePathFilterMenu(); + + const menu = page.section.filePathTreeMenu; + + menu + .click({ selector: "@treeCheckbox", index: 0 }) + .pause(300) + .click("@clearAllBtn"); + + page.expect.section("@filePathTreeMenu") + .to.not.be.present.before(5000); + + page.waitForProgressBarNotPresent(); + + page.section.filePathFilter.api.elements("@selectedItems", + ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length === 0, + "clear-all should leave no file-path filter chips" + ); + }); + } +}; diff --git a/web/server/vue-cli/e2e/specs/reportsTreeView.js b/web/server/vue-cli/e2e/specs/reportsTreeView.js new file mode 100644 index 0000000000..04cc4d00ae --- /dev/null +++ b/web/server/vue-cli/e2e/specs/reportsTreeView.js @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------- +// Part of the CodeChecker project, under the Apache License v2.0 with +// LLVM Exceptions. See LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ------------------------------------------------------------------------- + +// E2E tests for the new file-tree view mode and the severity / review-status +// header columns added to web/server/vue-cli/src/views/Reports.vue. + +module.exports = { + before(browser) { + browser.resizeWindow(1600, 1000); + + const login = browser.page.login(); + const treePage = browser.page.reportsTree(); + + login + .navigate(treePage.url()) + .loginAsRoot(); + + browser.assert.urlContains("/e2e/reports"); + + treePage + .waitForElementVisible("@page", 10000) + .waitForProgressBarNotPresent(); + + // Make sure the file-path filter section is expanded so that we can + // inspect the chips later. + const reportPage = browser.page.report(); + reportPage.section.filePathFilter.click("@expansionBtn"); + }, + + after(browser) { + browser.perform(() => { + browser.end(); + }); + }, + + "default view is the data table" (browser) { + const page = browser.page.reportsTree(); + page.expect.element("@viewModeToggle").to.be.visible.before(5000); + page.expect.element("@dataTable").to.be.visible.before(5000); + page.expect.element("@treeViewContainer").to.not.be.present.before(5000); + }, + + "switching to tree view shows the tree container" (browser) { + const page = browser.page.reportsTree(); + page.switchToTreeView(); + + page.expect.element("@treeViewContainer").to.be.visible.before(5000); + page.expect.element("@dataTable").to.not.be.present.before(5000); + }, + + "tree header shows severity and review-status columns" (browser) { + const page = browser.page.reportsTree(); + + page.expect.element("@treeHeader").to.be.visible.before(5000); + page.expect.element("@treeHeaderName").text.to.contain("Name"); + + // The header has 1 "All" + 5 severities + 4 review statuses = 10 + // tree-header-cell elements. + browser.elements("css selector", ".tree-header-cell", function (result) { + browser.assert.ok( + Array.isArray(result.value) && result.value.length === 10, + "tree header should contain 10 stat columns " + + "(All + 5 severities + 4 review statuses), got " + + (result.value ? result.value.length : "none") + ); + }); + }, + + "tree rows render stat cells" (browser) { + const page = browser.page.reportsTree(); + + page.expect.element("@treeRow").to.be.present.before(5000); + browser.elements("css selector", + ".tree-view-container .tree-row", function (result) { + browser.assert.ok( + Array.isArray(result.value) && result.value.length > 0, + "the tree should render at least one row" + ); + }); + }, + + "clicking a tree item populates the file path filter" (browser) { + const page = browser.page.reportsTree(); + + page.expect.element("@treeItemLabel").to.be.present.before(5000); + + page + .click({ selector: "@treeItemLabel", index: 0 }) + .pause(500) + .waitForElementNotPresent("@progressBar"); + + page.section.filePathFilter.api.elements("@selectedItems", + ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length >= 1, + "clicking a tree node must add at least one file-path filter chip" + ); + }); + }, + + "switching back to table view clears the file path filter" (browser) { + const page = browser.page.reportsTree(); + + page.switchToTableView(); + + // Both view-toggle buttons in Reports.vue invoke + // setReportFilter({ filepath: null }) on click, so the chip set + // populated in the previous test must now be empty. + page.section.filePathFilter.api.elements("@selectedItems", + ({ result }) => { + browser.assert.ok( + Array.isArray(result.value) && result.value.length === 0, + "switching back to the table view should clear file-path filter" + ); + }); + + page.expect.element("@dataTable").to.be.visible.before(5000); + } +}; diff --git a/web/server/vue-cli/package-lock.json b/web/server/vue-cli/package-lock.json index 7f9266f546..160a54d7e1 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.69.0.tgz", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.70.0.tgz", "codemirror": "^5.65.0", "date-fns": "^2.28.0", "js-cookie": "^3.0.1", @@ -5059,9 +5059,9 @@ } }, "node_modules/codechecker-api": { - "version": "6.69.0", - "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.69.0.tgz", - "integrity": "sha512-+vw8RNIzxsjfAzx+YlqEB9UNuxiFav+/d5gUaALvNel6wk2jrWF2thh+xcrTTVqu/4jvZXSE/uZwOJBmw5S4og==", + "version": "6.70.0", + "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.70.0.tgz", + "integrity": "sha512-YMEkiEJFWbsufbqDGIlfhkcfK4MdJvXDXcEVyEKzfPMSPcBVM4c5yJ5RbDuxT2eIsMzGS6sE2pVE8YzEBJH27w==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "thrift": "0.13.0-hotfix.1" diff --git a/web/server/vue-cli/package.json b/web/server/vue-cli/package.json index 58a7fa3253..b2c1a426bb 100644 --- a/web/server/vue-cli/package.json +++ b/web/server/vue-cli/package.json @@ -29,7 +29,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.69.0.tgz", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.70.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/Report/ReportFilter/Filters/FilePathFilter.vue b/web/server/vue-cli/src/components/Report/ReportFilter/Filters/FilePathFilter.vue index ccee7da0b0..74059dd5f3 100644 --- a/web/server/vue-cli/src/components/Report/ReportFilter/Filters/FilePathFilter.vue +++ b/web/server/vue-cli/src/components/Report/ReportFilter/Filters/FilePathFilter.vue @@ -5,15 +5,90 @@ :bus="bus" :fetch-items="fetchItems" :selected-items="selectedItems" - :search="search" :loading="loading" :limit="defaultLimit" :panel="panel" @clear="clear(true)" @input="setSelectedItems" > - @@ -170,10 +278,17 @@ diff --git a/web/tests/functional/report_viewer_api/test_report_counting.py b/web/tests/functional/report_viewer_api/test_report_counting.py index 1170a052df..f182e04ce9 100644 --- a/web/tests/functional/report_viewer_api/test_report_counting.py +++ b/web/tests/functional/report_viewer_api/test_report_counting.py @@ -348,6 +348,101 @@ def test_run1_run2_file_filters(self): self.assertGreaterEqual(len(res), len(test_res)) self.assertLessEqual(test_res.items(), res.items()) + def test_run1_all_file_counts_summary(self): + """ + Get all the file count summaries for run1. + """ + runid = self._runids[0] + file_summary = self._cc_client.getFileCountsSummary( + [runid], ReportFilter(), None, None, 0) + res = {get_filename(k): v for k, v in file_summary.items()} + + self.assertEqual(len(res), len(self.run1_files)) + for filename, expected_count in self.run1_files.items(): + self.assertIn(filename, res) + self.assertEqual(res[filename]["reports"], expected_count) + + def test_run2_all_file_counts_summary(self): + """ + Get all the file count summaries for run2. + """ + runid = self._runids[1] + file_summary = self._cc_client.getFileCountsSummary( + [runid], ReportFilter(), None, None, 0) + res = {get_filename(k): v for k, v in file_summary.items()} + + self.assertEqual(len(res), len(self.run2_files)) + for filename, expected_count in self.run2_files.items(): + self.assertIn(filename, res) + self.assertEqual(res[filename]["reports"], expected_count) + + def test_run1_run2_all_file_counts_summary(self): + """ + Get all the file count summaries for run1 and run2. + """ + file_summary = self._cc_client.getFileCountsSummary( + self._runids, ReportFilter(), None, None, 0) + res = {get_filename(k): v for k, v in file_summary.items()} + + r1_count = Counter(self.run1_files) + r2_count = Counter(self.run2_files) + all_res = dict(r1_count + r2_count) + + self.assertEqual(len(res), len(all_res)) + for filename, expected_count in all_res.items(): + self.assertIn(filename, res) + self.assertEqual(res[filename]["reports"], expected_count) + + def test_run1_file_counts_summary_severity(self): + """ + Verify severity totals in file count summaries match + severity counts for run1. + """ + runid = self._runids[0] + file_summary = self._cc_client.getFileCountsSummary( + [runid], ReportFilter(), None, None, 0) + + severity_totals = defaultdict(int) + for summary in file_summary.values(): + for key, value in summary.items(): + if key.startswith("severity:"): + sev_name = key.split(":")[1] + sev_value = Severity._NAMES_TO_VALUES[sev_name] + severity_totals[sev_value] += value + + self.assertDictEqual(dict(severity_totals), self.run1_sev_counts) + + def test_run1_run2_file_counts_summary_filters(self): + """ + Get file count summaries with filepath filter for run1 and run2. + """ + null_stack_filter = ReportFilter( + filepath=["*null_dereference.cpp", "*stack_address_escape.cpp"]) + + file_summary = self._cc_client.getFileCountsSummary( + self._runids, null_stack_filter, None, None, 0) + res = {get_filename(k): v for k, v in file_summary.items()} + + null_r1 = {k: v for k, v in self.run1_files.items() + if "null_dereference" in k} + + stack_r1 = {k: v for k, v in self.run1_files.items() + if "stack_address_escape" in k} + + null_r2 = {k: v for k, v in self.run2_files.items() + if "null_dereference" in k} + + stack_r2 = {k: v for k, v in self.run2_files.items() + if "stack_address_escape" in k} + + test_res = dict(Counter(null_r1) + Counter(null_r2) + + Counter(stack_r1) + Counter(stack_r2)) + + self.assertGreaterEqual(len(res), len(test_res)) + for filename, expected_count in test_res.items(): + self.assertIn(filename, res) + self.assertEqual(res[filename]["reports"], expected_count) + def test_run1_run2_all_checker_msg(self): """ Get all the file checker messages for run1 and run2.