diff --git a/setoolsgui/apol.py b/setoolsgui/apol.py
index e677103..7827c2d 100644
--- a/setoolsgui/apol.py
+++ b/setoolsgui/apol.py
@@ -19,7 +19,8 @@ from . import config, widgets
# Supported analyses. These are not directly used here, but
# will init the tab registry in widgets.tab for apol's analyses.
# pylint: disable=unused-import
-from .widgets import (constraintquery,
+from .widgets import (boolquery,
+ constraintquery,
fsusequery,
genfsconquery,
ibendportconquery,
diff --git a/setoolsgui/apol/boolquery.py b/setoolsgui/apol/boolquery.py
deleted file mode 100644
index 6116fe4..0000000
--- a/setoolsgui/apol/boolquery.py
+++ /dev/null
@@ -1,192 +0,0 @@
-# Copyright 2016, Tresys Technology, LLC
-#
-# SPDX-License-Identifier: LGPL-2.1-only
-#
-#
-
-import logging
-from contextlib import suppress
-
-from PyQt5.QtCore import Qt, QSortFilterProxyModel, QStringListModel, QThread
-from PyQt5.QtGui import QPalette, QTextCursor
-from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog
-from setools import BoolQuery
-
-from ..logtosignal import LogHandlerToSignal
-from ..models import SEToolsListModel, invert_list_selection
-from ..boolmodel import BooleanTableModel, boolean_detail
-from .analysistab import AnalysisSection, AnalysisTab
-from .exception import TabFieldError
-from .queryupdater import QueryResultsUpdater
-from .workspace import load_checkboxes, load_lineedits, load_textedits, \
- save_checkboxes, save_lineedits, save_textedits
-
-
-class BoolQueryTab(AnalysisTab):
-
- """Bool browser and query tab."""
-
- section = AnalysisSection.Components
- tab_title = "Booleans"
- mlsonly = False
-
- def __init__(self, parent, policy, perm_map):
- super(BoolQueryTab, self).__init__(parent)
- self.log = logging.getLogger(__name__)
- self.policy = policy
- self.query = BoolQuery(policy)
- self.setupUi()
-
- def __del__(self):
- with suppress(RuntimeError):
- self.thread.quit()
- self.thread.wait(5000)
-
- logging.getLogger("setools.boolquery").removeHandler(self.handler)
-
- def setupUi(self):
- self.load_ui("apol/boolquery.ui")
-
- # populate bool list
- self.bool_model = SEToolsListModel(self)
- self.bool_model.item_list = sorted(r for r in self.policy.bools())
- self.bools.setModel(self.bool_model)
-
- # set up results
- self.table_results_model = BooleanTableModel(self)
- self.sort_proxy = QSortFilterProxyModel(self)
- self.sort_proxy.setSourceModel(self.table_results_model)
- self.table_results.setModel(self.sort_proxy)
- self.table_results.sortByColumn(0, Qt.SortOrder.AscendingOrder)
-
- # setup indications of errors on level/range
- self.errors = set()
- self.orig_palette = self.name.palette()
- self.error_palette = self.name.palette()
- self.error_palette.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.red)
- self.clear_name_error()
-
- # set up processing thread
- self.thread = QThread()
- self.worker = QueryResultsUpdater(self.query, self.table_results_model)
- self.worker.moveToThread(self.thread)
- self.worker.raw_line.connect(self.raw_results.appendPlainText)
- self.worker.finished.connect(self.update_complete)
- self.worker.finished.connect(self.thread.quit)
- self.thread.started.connect(self.worker.update)
-
- # create a "busy, please wait" dialog
- self.busy = QProgressDialog(self)
- self.busy.setModal(True)
- self.busy.setRange(0, 0)
- self.busy.setMinimumDuration(0)
- self.busy.canceled.connect(self.thread.requestInterruption)
- self.busy.reset()
-
- # update busy dialog from query INFO logs
- self.handler = LogHandlerToSignal()
- self.handler.message.connect(self.busy.setLabelText)
- logging.getLogger("setools.boolquery").addHandler(self.handler)
-
- # Ensure settings are consistent with the initial .ui state
- self.notes.setHidden(not self.notes_expander.isChecked())
-
- # connect signals
- self.bools.doubleClicked.connect(self.get_detail)
- self.bools.get_detail.triggered.connect(self.get_detail)
- self.name.textEdited.connect(self.clear_name_error)
- self.name.editingFinished.connect(self.set_name)
- self.name_regex.toggled.connect(self.set_name_regex)
- self.buttonBox.clicked.connect(self.run)
-
- #
- # Booleans browser
- #
- def get_detail(self):
- # .ui is set for single item selection.
- index = self.bools.selectedIndexes()[0]
- item = self.bool_model.data(index, Qt.ItemDataRole.UserRole)
-
- self.log.debug("Generating detail window for {0}".format(item))
- boolean_detail(self, item)
-
- #
- # Name criteria
- #
- def clear_name_error(self):
- self.clear_criteria_error(self.name, "Match the Boolean name.")
-
- def set_name(self):
- try:
- self.query.name = self.name.text()
- except Exception as ex:
- self.log.error("Boolean name error: {0}".format(ex))
- self.set_criteria_error(self.name, ex)
-
- def set_name_regex(self, state):
- self.log.debug("Setting name_regex {0}".format(state))
- self.query.name_regex = state
- self.clear_name_error()
- self.set_name()
-
- #
- # Save/Load tab
- #
- def save(self):
- """Return a dictionary of settings."""
- if self.errors:
- raise TabFieldError("Field(s) are in error: {0}".
- format(" ".join(o.objectName() for o in self.errors)))
-
- settings = {}
- save_checkboxes(self, settings, ["criteria_expander", "notes_expander",
- "default_any", "default_true", "default_false",
- "name_regex"])
- save_lineedits(self, settings, ["name"])
- save_textedits(self, settings, ["notes"])
- return settings
-
- def load(self, settings):
- load_checkboxes(self, settings, ["criteria_expander", "notes_expander",
- "default_any", "default_true", "default_false",
- "name_regex"])
- load_lineedits(self, settings, ["name"])
- load_textedits(self, settings, ["notes"])
-
- #
- # Results runner
- #
-
- def run(self, button):
- # right now there is only one button.
- if self.default_any.isChecked():
- self.query.default = None
- else:
- self.query.default = self.default_true.isChecked()
-
- # start processing
- self.busy.setLabelText("Processing query...")
- self.busy.show()
- self.raw_results.clear()
- self.thread.start()
-
- def update_complete(self, count):
- self.log.info("{0} Boolean(s) found.".format(count))
-
- # update sizes/location of result displays
- if not self.busy.wasCanceled():
- self.busy.setLabelText("Resizing the result table's columns; GUI may be unresponsive")
- self.busy.repaint()
- self.table_results.resizeColumnsToContents()
-
- if not self.busy.wasCanceled():
- self.busy.setLabelText("Resizing the result table's rows; GUI may be unresponsive")
- self.busy.repaint()
- self.table_results.resizeRowsToContents()
-
- if not self.busy.wasCanceled():
- self.busy.setLabelText("Moving the raw result to top; GUI may be unresponsive")
- self.busy.repaint()
- self.raw_results.moveCursor(QTextCursor.MoveOperation.Start)
-
- self.busy.reset()
diff --git a/setoolsgui/apol/boolquery.ui b/setoolsgui/apol/boolquery.ui
deleted file mode 100644
index 0acb7b3..0000000
--- a/setoolsgui/apol/boolquery.ui
+++ /dev/null
@@ -1,459 +0,0 @@
-
-
Search for Booleans by name.
") + + state = criteria.BooleanState("Default State", self.query, "default", + enable_any=True, + parent=self.criteria_frame) + state.setToolTip("Search for Booleans by default state.") + state.setWhatsThis("Search for Booleans by default state.
") + + # Add widgets to layout + self.criteria_frame_layout.addWidget(name, 0, 0, 1, 1) + self.criteria_frame_layout.addWidget(state, 0, 1, 1, 1) + self.criteria_frame_layout.addWidget(self.buttonBox, 1, 0, 1, 2) + + # Save widget references + self.criteria = (name, state) + + # Set result table's model + self.table_results_model = models.BooleanTable(self.table_results) + + # + # Set up browser + # + self.browser.setModel(models.BooleanTable(self.browser, + data=sorted(self.query.policy.bools()))) + + +if __name__ == '__main__': + import sys + import warnings + import pprint + import logging + + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s|%(levelname)s|%(name)s|%(message)s') + warnings.simplefilter("default") + + app = QtWidgets.QApplication(sys.argv) + mw = QtWidgets.QMainWindow() + widget = BoolQueryTab(setools.SELinuxPolicy(), mw) + mw.setCentralWidget(widget) + mw.resize(1280, 1024) + whatsthis = QtWidgets.QWhatsThis.createAction(mw) + mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] + mw.setStatusBar(QtWidgets.QStatusBar(mw)) + mw.show() + rc = app.exec() + pprint.pprint(widget.save()) + sys.exit(rc) diff --git a/setoolsgui/widgets/criteria/boolean.py b/setoolsgui/widgets/criteria/boolean.py index 2e1ee48..95176df 100644 --- a/setoolsgui/widgets/criteria/boolean.py +++ b/setoolsgui/widgets/criteria/boolean.py @@ -1,15 +1,17 @@ # SPDX-License-Identifier: LGPL-2.1-only from PyQt6 import QtWidgets +import setools from .. import models +from .combobox import ComboBoxWidget from .list import ListCriteriaWidget from .name import NameCriteriaWidget # Regex for exact matches to types/attrs VALIDATE_EXACT = r"[A-Za-z0-9._-]*" -__all__ = ("BooleanListCriteriaWidget", "BooleanNameCriteriaWidget",) +__all__ = ("BooleanListCriteriaWidget", "BooleanNameCriteriaWidget", "BooleanState") class BooleanListCriteriaWidget(ListCriteriaWidget): @@ -48,12 +50,24 @@ class BooleanNameCriteriaWidget(NameCriteriaWidget): enable_regex=enable_regex, required=required, parent=parent) +class BooleanState(ComboBoxWidget): + + """Criteria selection widget presenting possible Boolean states.""" + + def __init__(self, title: str, query: setools.PolicyQuery, attrname: str, /, *, + enable_any: bool = True, parent: QtWidgets.QWidget | None = None) -> None: + + super().__init__(title, query, attrname, enable_any=enable_any, parent=parent) + + self.criteria.addItem("False", False) + self.criteria.addItem("True", True) + + if __name__ == '__main__': import sys import logging import warnings import setools - import pprint logging.basicConfig(level=logging.DEBUG, format='%(asctime)s|%(levelname)s|%(name)s|%(message)s') @@ -69,8 +83,10 @@ if __name__ == '__main__': layout = QtWidgets.QHBoxLayout(window) widget1 = BooleanListCriteriaWidget("Test Booleans list", q1, "boolean", parent=window) widget2 = BooleanNameCriteriaWidget("Test Booleans linedit", q2, "name", parent=window) + widget3 = BooleanState("Test Booleans State", q2, "default", enable_any=True, parent=window) layout.addWidget(widget1) layout.addWidget(widget2) + layout.addWidget(widget3) window.setToolTip("test tooltip") window.setWhatsThis("test whats this") mw.setCentralWidget(window) @@ -79,6 +95,4 @@ if __name__ == '__main__': mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] mw.show() rc = app.exec() - print("Query settings:") - pprint.pprint(q1.boolean) sys.exit(rc) diff --git a/tests-gui/widgets/test_boolquery.py b/tests-gui/widgets/test_boolquery.py new file mode 100644 index 0000000..f4a6591 --- /dev/null +++ b/tests-gui/widgets/test_boolquery.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: GPL-2.0-only +import typing + +from PyQt6 import QtWidgets +from pytestqt.qtbot import QtBot + +import setools +from setoolsgui.widgets.boolquery import BoolQueryTab + +from .criteria.util import build_mock_policy + + +def test_docs(qtbot: QtBot) -> None: + """Check that docs are provided for the widget.""" + mock_policy = build_mock_policy() + widget = BoolQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + assert widget.whatsThis() + assert widget.table_results.whatsThis() + assert widget.raw_results.whatsThis() + + for w in widget.criteria: + assert w.toolTip() + assert w.whatsThis() + + results = typing.cast(QtWidgets.QTabWidget, widget.results) + for index in range(results.count()): + assert results.tabWhatsThis(index) + + +def test_layout(qtbot: QtBot) -> None: + """Test the layout of the criteria frame.""" + mock_policy = build_mock_policy() + widget = BoolQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + name, state = widget.criteria + + assert widget.criteria_frame_layout.columnCount() == 2 + assert widget.criteria_frame_layout.rowCount() == 2 + assert widget.criteria_frame_layout.itemAtPosition(0, 0).widget() == name + assert widget.criteria_frame_layout.itemAtPosition(0, 1).widget() == state + assert widget.criteria_frame_layout.itemAtPosition(1, 0).widget() == widget.buttonBox + assert widget.criteria_frame_layout.itemAtPosition(1, 1).widget() == widget.buttonBox + + +def test_criteria_mapping(qtbot: QtBot) -> None: + """Test that widgets save to the correct query fields.""" + mock_policy = build_mock_policy() + widget = BoolQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + name, state = widget.criteria + + assert isinstance(widget.query, setools.BoolQuery) + assert name.attrname == "name" + assert state.attrname == "default"