diff --git a/.gitignore b/.gitignore index cd4c22c..0b273a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *__pycache__* +xfstestsdb/gtk/icons/xfstestsdb.gresource diff --git a/Makefile b/Makefile index 6ecdbcd..9f89f1d 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ export PREFIX = /usr/local export XFSTESTSDB_BIN = ${PREFIX}/bin export XFSTESTSDB_LIB = ${PREFIX}/lib/xfstestsdb +export GTK_DIR = xfstestsdb/gtk/icons + +all: xfstestsdb.gresource flake8 clean: find . -type d -name __pycache__ -exec rm -r {} \+ @@ -11,8 +14,12 @@ clean: flake8: flake8 +.PHONY: xfstestsdb.gresource +xfstestsdb.gresource: + glib-compile-resources --sourcedir=$(GTK_DIR) $(GTK_DIR)/xfstestsdb.gresource.xml + .PHONY: install -install: +install: xfstestsdb.gresource find ./xfstestsdb -type f -not -path "*/__pycache__/*" \ -exec install -v -C -D -m 755 "{}" "$(XFSTESTSDB_LIB)/{}" \; install -C -v -m 644 xfstestsdb.py $(XFSTESTSDB_LIB)/xfstestsdb.py @@ -24,7 +31,7 @@ pytest: pytest .PHONY: tests -tests: pytest flake8 +tests: xfstestsdb.gresource pytest flake8 .PHONY: uninstall uninstall: diff --git a/tests/gtk/test_gsetup.py b/tests/gtk/test_gsetup.py index 6f3ed08..f0fb0d1 100644 --- a/tests/gtk/test_gsetup.py +++ b/tests/gtk/test_gsetup.py @@ -40,3 +40,15 @@ class TestGSetup(unittest.TestCase): mock_add.assert_called_with(mock_get_default.return_value, xfstestsdb.gtk.gsetup.CSS_PROVIDER, xfstestsdb.gtk.gsetup.CSS_PRIORITY) + + @unittest.mock.patch("gi.repository.Gio.resources_register") + def test_resources(self, mock_register: unittest.mock.Mock): + """Test that icon resources have been added to the app.""" + gtk_init_py = pathlib.Path(xfstestsdb.gtk.__file__) + resources = gtk_init_py.parent / "icons" / "xfstestsdb.gresource" + + self.assertEqual(xfstestsdb.gtk.gsetup.RESOURCE_FILE, resources) + self.assertEqual(xfstestsdb.gtk.gsetup.RESOURCE_PATH, + "/com/nowheycreamery/xfstestsdb") + self.assertIsInstance(xfstestsdb.gtk.gsetup.RESOURCE, + gi.repository.Gio.Resource) diff --git a/tests/gtk/test_view.py b/tests/gtk/test_view.py index a3523a5..ce0cd7a 100644 --- a/tests/gtk/test_view.py +++ b/tests/gtk/test_view.py @@ -6,6 +6,63 @@ import xfstestsdb.gtk.view from gi.repository import Gtk +class TestFilterButtons(unittest.TestCase): + """Test case for our TestCaseView FilterButtons.""" + + def setUp(self): + """Set up common variables.""" + self.buttons = xfstestsdb.gtk.view.FilterButtons() + + def test_init(self): + """Check that the buttons were created correctly.""" + self.assertIsInstance(self.buttons, Gtk.Box) + self.assertTrue(self.buttons.has_css_class("linked")) + + def test_passed_button(self): + """Test the 'passed' button.""" + self.assertIsInstance(self.buttons._passed, + xfstestsdb.gtk.button.StatusToggle) + self.assertEqual(self.buttons.get_first_child(), self.buttons._passed) + + self.assertEqual(self.buttons._passed.props.icon_name, "test-pass") + self.assertTrue(self.buttons._passed.has_css_class("passed")) + self.assertFalse(self.buttons._passed.props.active) + + self.assertFalse(self.buttons.passed) + self.buttons._passed.props.active = True + self.assertTrue(self.buttons.passed) + + def test_skipped_button(self): + """Test the 'skipped' button.""" + self.assertIsInstance(self.buttons._skipped, + xfstestsdb.gtk.button.StatusToggle) + self.assertEqual(self.buttons._passed.get_next_sibling(), + self.buttons._skipped) + + self.assertEqual(self.buttons._skipped.props.icon_name, "test-skip") + self.assertTrue(self.buttons._skipped.has_css_class("skipped")) + self.assertFalse(self.buttons._skipped.props.active) + + self.assertFalse(self.buttons.skipped) + self.buttons._skipped.props.active = True + self.assertTrue(self.buttons.skipped) + + def test_failure_button(self): + """Test the 'failure' button.""" + self.assertIsInstance(self.buttons._failure, + xfstestsdb.gtk.button.StatusToggle) + self.assertEqual(self.buttons._skipped.get_next_sibling(), + self.buttons._failure) + + self.assertEqual(self.buttons._failure.props.icon_name, "test-fail") + self.assertTrue(self.buttons._failure.has_css_class("failure")) + self.assertTrue(self.buttons._failure.props.active) + + self.assertTrue(self.buttons.failure) + self.buttons._failure.props.active = False + self.assertFalse(self.buttons.failure) + + class TestTestCaseView(unittest.TestCase): """Tests the TestCaseView.""" @@ -39,6 +96,31 @@ class TestTestCaseView(unittest.TestCase): self.assertTrue(self.view.props.child.get_hexpand()) self.assertTrue(self.view.props.child.get_vexpand()) + def test_filter(self): + """Test that we set up the Gtk.FilterModel and Buttons correctly.""" + self.assertIsInstance(self.view._filtermodel, Gtk.FilterListModel) + self.assertIsInstance(self.view._testfilter, + xfstestsdb.gtk.model.TestCaseFilter) + self.assertIsInstance(self.view.filterbuttons, + xfstestsdb.gtk.view.FilterButtons) + + self.assertEqual(self.view.props.child.get_model().get_model(), + self.view._filtermodel) + self.assertEqual(self.view._filtermodel.props.filter, + self.view._testfilter) + + self.assertFalse(self.view._testfilter.passed) + self.view.filterbuttons.passed = True + self.assertTrue(self.view._testfilter.passed) + + self.assertFalse(self.view._testfilter.skipped) + self.view.filterbuttons.skipped = True + self.assertTrue(self.view._testfilter.skipped) + + self.assertTrue(self.view._testfilter.failure) + self.view.filterbuttons.failure = False + self.assertFalse(self.view._testfilter.failure) + def test_testcase_column(self): """Test that we set up the 'testcase' column correctly.""" self.assertIsInstance(self.view._testcase, Gtk.ColumnViewColumn) @@ -51,8 +133,7 @@ class TestTestCaseView(unittest.TestCase): def test_model(self): """Test setting the model property.""" self.view.model = self.model - self.assertEqual(self.view.props.child.get_model().get_model(), - self.model) + self.assertEqual(self.view._filtermodel.props.model, self.model) columns = self.view.props.child.get_columns() self.assertEqual(len(columns), 3) @@ -106,3 +187,8 @@ class TestXfstestsView(unittest.TestCase): self.assertIsNone(self.view.model) self.view.model = self.model self.assertEqual(self.view._testcaseview.model, self.model) + + def test_filterbuttons(self): + """Test the XfstestsView 'filterbuttons' property.""" + self.assertEqual(self.view.filterbuttons, + self.view._testcaseview.filterbuttons) diff --git a/tests/test_gtk.py b/tests/test_gtk.py index 7e7b35e..f6efec2 100644 --- a/tests/test_gtk.py +++ b/tests/test_gtk.py @@ -24,6 +24,8 @@ class TestApplication(unittest.TestCase): xfstestsdb.gtk.gsetup.APPLICATION_ID) self.assertEqual(self.application.get_flags(), Gio.ApplicationFlags.HANDLES_COMMAND_LINE) + self.assertEqual(self.application.get_resource_base_path(), + xfstestsdb.gtk.gsetup.RESOURCE_PATH) self.assertEqual(self.application.runid, 0) self.assertIsNone(self.application.model) diff --git a/xfstestsdb/gtk/__init__.py b/xfstestsdb/gtk/__init__.py index b7e8a09..859360b 100644 --- a/xfstestsdb/gtk/__init__.py +++ b/xfstestsdb/gtk/__init__.py @@ -26,6 +26,7 @@ class Application(Adw.Application): def __init__(self, sql: sqlite.Connection): """Initialize the application.""" super().__init__(application_id=gsetup.APPLICATION_ID, + resource_base_path=gsetup.RESOURCE_PATH, flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, sql=sql) @@ -50,6 +51,8 @@ class Application(Adw.Application): self.view = view.XfstestsView() self.win = window.Window(child=self.view) + self.win.headerbar.pack_end(self.view.filterbuttons) + self.bind_property("runid", self.win, "runid") self.bind_property("model", self.view, "model") self.add_window(self.win) diff --git a/xfstestsdb/gtk/gsetup.py b/xfstestsdb/gtk/gsetup.py index 46b207e..0c179e9 100644 --- a/xfstestsdb/gtk/gsetup.py +++ b/xfstestsdb/gtk/gsetup.py @@ -9,11 +9,17 @@ gi.importlib.import_module("gi.repository.Adw") DEBUG_STR = "-debug" if __debug__ else "" APPLICATION_ID = f"com.nowheycreamery.xfstestsdb.gtk{DEBUG_STR}" -CSS_FILE = pathlib.Path(__file__).parent / "xfstestsdb.css" +__here = pathlib.Path(__file__).parent +CSS_FILE = __here / "xfstestsdb.css" CSS_PRIORITY = gi.repository.Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION CSS_PROVIDER = gi.repository.Gtk.CssProvider() CSS_PROVIDER.load_from_path(str(CSS_FILE)) +RESOURCE_FILE = __here / "icons" / "xfstestsdb.gresource" +RESOURCE_PATH = "/com/nowheycreamery/xfstestsdb" +RESOURCE = gi.repository.Gio.Resource.load(str(RESOURCE_FILE)) +gi.repository.Gio.resources_register(RESOURCE) + def add_style(): """Add our custom stylesheet to the Gdk.Display.""" diff --git a/xfstestsdb/gtk/icons/test-fail-symbolic.svg b/xfstestsdb/gtk/icons/test-fail-symbolic.svg new file mode 100644 index 0000000..551beb4 --- /dev/null +++ b/xfstestsdb/gtk/icons/test-fail-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/xfstestsdb/gtk/icons/test-pass-symbolic.svg b/xfstestsdb/gtk/icons/test-pass-symbolic.svg new file mode 100644 index 0000000..9102ed6 --- /dev/null +++ b/xfstestsdb/gtk/icons/test-pass-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/xfstestsdb/gtk/icons/test-skip-symbolic.svg b/xfstestsdb/gtk/icons/test-skip-symbolic.svg new file mode 100644 index 0000000..74c836a --- /dev/null +++ b/xfstestsdb/gtk/icons/test-skip-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/xfstestsdb/gtk/icons/xfstestsdb.gresource.xml b/xfstestsdb/gtk/icons/xfstestsdb.gresource.xml new file mode 100644 index 0000000..140eaa7 --- /dev/null +++ b/xfstestsdb/gtk/icons/xfstestsdb.gresource.xml @@ -0,0 +1,8 @@ + + + + test-pass-symbolic.svg + test-skip-symbolic.svg + test-fail-symbolic.svg + + diff --git a/xfstestsdb/gtk/view.py b/xfstestsdb/gtk/view.py index 87ed652..2297336 100644 --- a/xfstestsdb/gtk/view.py +++ b/xfstestsdb/gtk/view.py @@ -3,37 +3,80 @@ from gi.repository import GObject from gi.repository import Gtk from .model import TestCaseList +from .model import TestCaseFilter +from . import button from . import row +class FilterButtons(Gtk.Box): + """Buttons for controlling the TestCaseFilter.""" + + passed = GObject.Property(type=bool, default=False) + skipped = GObject.Property(type=bool, default=False) + failure = GObject.Property(type=bool, default=True) + + def __init__(self): + """Initialize the FilterButtons.""" + super().__init__() + self.add_css_class("linked") + + self._passed = button.StatusToggle("test-pass", "passed") + self._skipped = button.StatusToggle("test-skip", "skipped") + self._failure = button.StatusToggle("test-fail", "failure", + active=True) + + self._passed.bind_property("active", self, "passed") + self._skipped.bind_property("active", self, "skipped") + self._failure.bind_property("active", self, "failure") + + self.append(self._passed) + self.append(self._skipped) + self.append(self._failure) + + class TestCaseView(Gtk.ScrolledWindow): """Displays our TestCaseList model to the user.""" + filterbuttons = GObject.Property(type=FilterButtons) + def __init__(self): """Initialize a TestCaseView.""" super().__init__(child=Gtk.ColumnView(model=Gtk.NoSelection(), show_row_separators=True, show_column_separators=True, - hexpand=True, vexpand=True)) + hexpand=True, vexpand=True), + filterbuttons=FilterButtons()) self._testcase = Gtk.ColumnViewColumn(title="testcase", factory=row.LabelFactory("name")) + + self._testfilter = TestCaseFilter() + self._filtermodel = Gtk.FilterListModel(filter=self._testfilter) + + for prop in ["passed", "skipped", "failure"]: + self.filterbuttons.bind_property(prop, self._testfilter, prop) + + self.props.child.get_model().set_model(self._filtermodel) self.add_css_class("card") def __xunit_column(self, xunit: str) -> None: return Gtk.ColumnViewColumn(title=xunit, expand=True, factory=row.ResultFactory(xunit)) + def make_buttons(self) -> FilterButtons: + """Make a new FilterButtons instance connected to this View.""" + return FilterButtons() + @GObject.Property(type=TestCaseList) def model(self) -> TestCaseList: """Get the TestCaseList shown by the View.""" - return self.props.child.get_model().get_model() + return self._filtermodel.props.model @model.setter def model(self, new: TestCaseList) -> None: for col in [col for col in self.props.child.get_columns()]: self.props.child.remove_column(col) - self.props.child.get_model().set_model(new) + self._filtermodel.props.model = new if new is not None: self.props.child.append_column(self._testcase) @@ -54,3 +97,8 @@ class XfstestsView(Gtk.Box): self.bind_property("model", self._testcaseview, "model") self.append(self._testcaseview) + + @GObject.Property(type=FilterButtons) + def filterbuttons(self) -> FilterButtons: + """Get the FilterButtons attached to the child TestCaseView.""" + return self._testcaseview.filterbuttons diff --git a/xfstestsdb/gtk/xfstestsdb.css b/xfstestsdb/gtk/xfstestsdb.css index 175a1cf..4cc5914 100644 --- a/xfstestsdb/gtk/xfstestsdb.css +++ b/xfstestsdb/gtk/xfstestsdb.css @@ -1,15 +1,27 @@ /* Copyright 2023 (c) Anna Schumaker. */ +button.passed > image { + color: @success_color; +} + cell.passed { color: @success_fg_color; background-color: @success_bg_color; } +button.skipped > image { + color: @warning_color; +} + cell.skipped { color: @warning_fg_color; background-color: @warning_bg_color; } +button.failure > image { + color: @error_color; +} + cell.failure { color: @error_fg_color; background-color: @error_bg_color;