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;