Compare commits

...

18 Commits

Author SHA1 Message Date
Anna Schumaker 4d3425bb57 xfstestsdb 1.3
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-15 10:57:27 -04:00
Anna Schumaker 455d933fc4 gtk: Create a SummaryView
The SummaryView displays a summary of the passed, failed, and skipped
tests for each xunit along with the time it took the xunit to run.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 16:06:32 -04:00
Anna Schumaker 8a5bc1527a gtk: Create a SummaryFactory
This is a Gtk.ListItemFactory that creates a Label widget and binds
xunit summaries to it. I use the summary field name to apply an
appropriate CSS class to each label.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 16:03:06 -04:00
Anna Schumaker d92b8148bf gtk: LabelFactory improvements
I add a global SizeGroup to the LabelFactory so the first column of each
table I make can have the same width. I also add style classes that are
applied with specific item values, which will mostly be used by the
SummaryView labels.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 16:02:21 -04:00
Anna Schumaker e14667691a gtk: Add a SummaryList Gio.ListModel
This is a list model designed to show a summary of xfstests results for
a given runid. I create a new one whenever the Application changes the
runid.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:41:05 -04:00
Anna Schumaker b143114108 gtk: Add SummaryValue and Summary objects
These are the underlying GObjects that will be returned by a SummaryList
ListModel.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:41:04 -04:00
Anna Schumaker 8a54cb5d98 gtk: Put the TestResultList behind a TestCaseFilter
And control the filter using the newly-created FilterButtons class. This
lets us hide completely skipped tests by default, since those are mostly
noise. I also add some custom icons used by the buttons to indicate
passed, failed, or skipped tests.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:41:02 -04:00
Anna Schumaker 37079ca7f5 gtk: Add a custom StatusToggle button
This is a Gtk.Button that has been customized to act similarily to a
Gtk.ToggleButton, but instead of giving the button a "pressed in" look
it changes the opacity of the displayed icon.

I'm planning to use this in the window headerbar in a set of buttons
designed to controll testcase filter state.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:35:01 -04:00
Anna Schumaker e865728357 gtk: Add a TestCaseFilter Gtk.Filter
This filter will be used to search the TestCaseList.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:35:00 -04:00
Anna Schumaker e0bb2d7be7 gtk: Display TestCase results in the TestCaseView
I use the ResultFactory to do this. I also create a custom css
stylesheet to use for each cell in the Gtk.ColumnView displaying the
results. This lets us add custom colors so we can easily see at a glance
what is failing, passing, or skipped.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:34:57 -04:00
Anna Schumaker 8ef06e9571 gtk: Create a ResultsFactory
This is a Gtk.ListItemFactory that creates a Label widget and binds
testcase results to it. I use the result status as the name of a CSS
class that will be created in the next patch so the ColumnView cells
have a nicer presentation.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:30:51 -04:00
Anna Schumaker 796cf6eec7 gtk: Create a TestCaseView and XfstestsView
The XfstestsView contains the TestCaseView, and will eventually contain
other views used to display test results and information.

For the moment, the TestCaseView displays the name of the testcases in a
single Xfstests run in a single column. I plan on adding more columns in
the near future.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 13:27:22 -04:00
Anna Schumaker 4bce147e45 gtk: Create a LabelFactory
This is a Gtk.ListItemFactory that creates a Label widget and binds a
generic property to it.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 10:17:10 -04:00
Anna Schumaker 7adbb5938d gtk: Add a TestCaseList Gio.ListModel
This is a list model designed to show xfstests results for a given
runid. I create a new one whenever the Application changes the runid.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 10:17:10 -04:00
Anna Schumaker 15cee521c2 gtk: Add TestCase and XunitResult objects
These are the underlying GObjects that will be returned by a
TestCaseList ListModel.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 10:17:10 -04:00
Anna Schumaker c1b73748f1 gtk: Add a window to the Application
This is an empty Adw.Window with a headerbar configured to display the
application name. I also bind the Application runid property to the
Window to print out the currently displayed run.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-14 10:17:08 -04:00
Anna Schumaker d5413700f0 gtk: Give the gtk command a `runid` argument
This argument is passed to the Application using the 'command-line'
signal, so a running Application can switch to a new runid when
requested.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-01 17:01:33 -04:00
Anna Schumaker 62bf603ca4 gtk: Add the `xfstestsdb gtk` command
This command doesn't do much at the moment, it simply sets up the Gtk
Application and then exits. The actual UI will be built up over the next
several patches.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-01 17:01:29 -04:00
25 changed files with 1932 additions and 6 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*__pycache__*
xfstestsdb/gtk/icons/xfstestsdb.gresource

View File

@ -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:

2
tests/gtk/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Copyright 2023 (c) Anna Schumaker.
"""Test package for the xfstestsdb gtk application."""

51
tests/gtk/test_button.py Normal file
View File

@ -0,0 +1,51 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our custom button classes."""
import unittest
import xfstestsdb.gtk.button
from gi.repository import Gtk
class TestStatusToggle(unittest.TestCase):
"""Test our StatusToggle button."""
def setUp(self):
"""Set up common variables."""
self.button = xfstestsdb.gtk.button.StatusToggle("icon-name",
"css-class")
def test_button(self):
"""Test the StatusToggle button."""
self.assertIsInstance(self.button, Gtk.Button)
self.assertEqual(self.button.icon_name, "icon-name")
self.assertTrue(self.button.has_css_class("css-class"))
self.assertFalse(self.button.props.has_frame)
self.assertFalse(self.button.active)
button2 = xfstestsdb.gtk.button.StatusToggle("icon-name", "css-class",
active=True)
self.assertTrue(button2.active)
self.assertAlmostEqual(button2.props.child.props.opacity, 1.0)
def test_image(self):
"""Test that the image is set up correctly, and opacity changes."""
self.assertIsInstance(self.button.props.child, Gtk.Image)
self.assertEqual(self.button.props.child.props.icon_name, "icon-name")
self.button.active = True
self.assertAlmostEqual(self.button.props.child.props.opacity, 1.0)
self.button.active = False
self.assertAlmostEqual(self.button.props.child.props.opacity, 0.4)
def test_clicked(self):
"""Test clicking the CSSToggle button."""
self.assertFalse(self.button.active)
self.assertAlmostEqual(self.button.props.child.props.opacity, 0.4)
self.button.emit("clicked")
self.assertTrue(self.button.active)
self.assertAlmostEqual(self.button.props.child.props.opacity, 1.0)
self.button.emit("clicked")
self.assertFalse(self.button.active)
self.assertAlmostEqual(self.button.props.child.props.opacity, 0.4)

54
tests/gtk/test_gsetup.py Normal file
View File

@ -0,0 +1,54 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our Gtk Setup."""
import gi
import pathlib
import unittest
import xfstestsdb.gtk.gsetup
class TestGSetup(unittest.TestCase):
"""Test our Gtk setup."""
def test_require_version(self):
"""Check that we have called gi.require_version()."""
self.assertEqual(gi.get_required_version("Adw"), "1")
def test_constants(self):
"""Check that constants are configured correctly."""
self.assertEqual(xfstestsdb.gtk.gsetup.DEBUG_STR, "-debug")
self.assertEqual(xfstestsdb.gtk.gsetup.APPLICATION_ID,
"com.nowheycreamery.xfstestsdb.gtk-debug")
@unittest.mock.patch.object(gi.repository.Gdk.Display, "get_default")
@unittest.mock.patch.object(gi.repository.Gtk.StyleContext,
"add_provider_for_display")
def test_add_style(self, mock_add: unittest.mock.Mock,
mock_get_default: unittest.mock.Mock):
"""Check that the CSS stylesheet is loaded correctly."""
gtk_init_py = pathlib.Path(xfstestsdb.gtk.__file__)
stylesheet = gtk_init_py.parent / "xfstestsdb.css"
self.assertEqual(xfstestsdb.gtk.gsetup.CSS_FILE, stylesheet)
self.assertEqual(xfstestsdb.gtk.gsetup.CSS_PRIORITY,
gi.repository.Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
self.assertIsInstance(xfstestsdb.gtk.gsetup.CSS_PROVIDER,
gi.repository.Gtk.CssProvider)
self.assertNotEqual(xfstestsdb.gtk.gsetup.CSS_PROVIDER.to_string(), "")
xfstestsdb.gtk.gsetup.add_style()
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)

324
tests/gtk/test_model.py Normal file
View File

@ -0,0 +1,324 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our Testcase Gio.ListModel."""
import random
import unittest
import tests.xunit
import xfstestsdb.gtk.model
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
class TestXunitResult(unittest.TestCase):
"""Tests a single TestCase Xunit instance."""
def test_xunit_result(self):
"""Test creating an xunit instance."""
xunit = xfstestsdb.gtk.model.XunitResult(name="my xunit name",
status="passed", time=123,
message="my message",
stdout="my stdout",
stderr="my stderr")
self.assertIsInstance(xunit, GObject.GObject)
self.assertEqual(xunit.name, "my xunit name")
self.assertEqual(xunit.status, "passed")
self.assertEqual(xunit.time, 123)
self.assertEqual(xunit.message, "my message")
self.assertEqual(xunit.stdout, "my stdout")
self.assertEqual(xunit.stderr, "my stderr")
class TestTestCase(unittest.TestCase):
"""Tests our TestCase GObject."""
def setUp(self):
"""Set up common variables."""
self.testcase = xfstestsdb.gtk.model.TestCase(name="test-case")
def test_init(self):
"""Check that the TestCase is set up properly."""
self.assertIsInstance(self.testcase, GObject.GObject)
self.assertEqual(self.testcase.name, "test-case")
def test_compare(self):
"""Test the less-than operator on TestCases."""
testcase2 = xfstestsdb.gtk.model.TestCase(name="test-case-2")
self.assertTrue(self.testcase < testcase2)
self.assertFalse(testcase2 < self.testcase)
self.assertFalse(self.testcase < self.testcase)
def test_xunits(self):
"""Test adding xunits to a TestCase."""
self.assertIsNone(self.testcase["xunit-1"])
self.testcase.add_xunit("xunit-1", "passed", 123, "message",
"stdout", "stderr")
xunit = self.testcase["xunit-1"]
self.assertIsInstance(xunit, xfstestsdb.gtk.model.XunitResult)
self.assertEqual(xunit.name, "xunit-1")
self.assertEqual(xunit.status, "passed")
self.assertEqual(xunit.time, 123)
self.assertEqual(xunit.message, "message")
self.assertEqual(xunit.stdout, "stdout")
self.assertEqual(xunit.stderr, "stderr")
self.testcase.add_xunit("xunit-2", "failed", 456, None, None, None)
xunit = self.testcase["xunit-2"]
self.assertIsInstance(xunit, xfstestsdb.gtk.model.XunitResult)
self.assertEqual(xunit.name, "xunit-2")
self.assertEqual(xunit.status, "failed")
self.assertEqual(xunit.time, 456)
self.assertEqual(xunit.message, "")
self.assertEqual(xunit.stdout, "")
self.assertEqual(xunit.stderr, "")
def test_get_results(self):
"""Test getting a set of results for this test case."""
self.testcase.add_xunit("xunit-1", "passed", 123, "", "", "")
self.assertSetEqual(self.testcase.get_results(), {"passed"})
self.testcase.add_xunit("xunit-2", "passed", 123, "", "", "")
self.assertSetEqual(self.testcase.get_results(), {"passed"})
self.testcase.add_xunit("xunit-3", "skipped", 123, "", "", "")
self.assertSetEqual(self.testcase.get_results(), {"passed", "skipped"})
self.testcase.add_xunit("xunit-4", "failure", 123, "", "", "")
self.assertSetEqual(self.testcase.get_results(),
{"passed", "skipped", "failure"})
class TestCaseList(unittest.TestCase):
"""Tests our TestCaseList Gio.ListModel."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-1",
"1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-2",
"1", str(tests.xunit.XUNIT_1)])
self.tclist = xfstestsdb.gtk.model.TestCaseList(self.xfstestsdb.sql, 1)
def test_init(self):
"""Test that the TestCaseList was set up properly."""
self.assertIsInstance(self.tclist, GObject.GObject)
self.assertIsInstance(self.tclist, Gio.ListModel)
self.assertEqual(self.tclist.runid, 1)
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.tclist.get_item_type(),
xfstestsdb.gtk.model.TestCase.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.tclist.get_n_items(), 10)
self.assertEqual(self.tclist.n_items, 10)
def test_get_item(self):
"""Test the get_item() function."""
testcase = self.tclist.get_item(0)
self.assertIsInstance(testcase, xfstestsdb.gtk.model.TestCase)
self.assertEqual(testcase.name, "test/01")
for xunit in ["xunit-1", "xunit-2"]:
with self.subTest(xunit=xunit):
self.assertEqual(testcase[xunit].name, xunit)
self.assertEqual(testcase[xunit].status, "passed")
self.assertEqual(testcase[xunit].time, 1)
self.assertEqual(testcase[xunit].message, "")
self.assertEqual(testcase[xunit].stdout, "")
self.assertEqual(testcase[xunit].stderr, "")
self.assertIsNone(self.tclist.get_item(10))
def test_get_xunits(self):
"""Test the get_xunits() function."""
self.assertListEqual(self.tclist.get_xunits(), ["xunit-1", "xunit-2"])
class TestCaseFilter(unittest.TestCase):
"""Tests our Gtk.Filter customized for filtering TestCases."""
def setUp(self):
"""Set up common variables."""
self.filter = xfstestsdb.gtk.model.TestCaseFilter()
def test_init(self):
"""Test that the TestCaseFilter is set up properly."""
self.assertIsInstance(self.filter, Gtk.Filter)
self.assertTrue(self.filter.failure)
self.assertFalse(self.filter.passed)
self.assertFalse(self.filter.skipped)
def test_strictness(self):
"""Test the TestCaseFilter strictness."""
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
self.filter.passed = True
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
self.filter.skipped = True
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.ALL)
self.filter.passed = False
self.filter.skipped = False
self.filter.failure = False
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.NONE)
def test_changed(self):
"""Test the TestCaseFilter 'changed' signal."""
changed = unittest.mock.Mock()
self.filter.connect("changed", changed)
self.filter.skipped = True
changed.assert_called_with(self.filter, Gtk.FilterChange.LESS_STRICT)
self.filter.passed = False
changed.assert_called_with(self.filter, Gtk.FilterChange.MORE_STRICT)
def test_match(self):
"""Test matching TestCases with the TestCaseFilter."""
testcase = xfstestsdb.gtk.model.TestCase(name="test-case")
self.assertFalse(self.filter.match(testcase))
self.filter.passed = False
self.filter.skipped = False
self.filter.failure = False
testcase.add_xunit("xunit-1", "skipped", 123, "", "", "")
self.assertFalse(self.filter.match(testcase))
self.filter.skipped = True
self.assertTrue(self.filter.match(testcase))
testcase.add_xunit("xunit-2", "passed", 123, "", "", "")
self.assertTrue(self.filter.match(testcase))
self.filter.skipped = False
self.assertFalse(self.filter.match(testcase))
self.filter.passed = True
self.assertTrue(self.filter.match(testcase))
testcase.add_xunit("xunit-3", "failure", 123, "", "", "")
self.assertTrue(self.filter.match(testcase))
self.filter.passed = False
self.assertFalse(self.filter.match(testcase))
self.filter.failure = True
self.assertTrue(self.filter.match(testcase))
class TestSummaryValue(unittest.TestCase):
"""Tests a single Summary Value object."""
def test_init(self):
"""Test creating a summary result instance."""
value = xfstestsdb.gtk.model.SummaryValue(name="my xunit name",
value=12345,
unit="testcase")
self.assertIsInstance(value, GObject.GObject)
self.assertEqual(value.name, "my xunit name")
self.assertEqual(value.unit, "testcase")
self.assertEqual(value.value, 12345)
def test_str(self):
"""Test converting a summary result to a string."""
value = xfstestsdb.gtk.model.SummaryValue(name="my xunit name",
value=1, unit="unit")
self.assertEqual(str(value), "1 unit")
value.value = 2
self.assertEqual(str(value), "2 units")
class TestSummary(unittest.TestCase):
"""Tests our Summary GObject."""
def setUp(self):
"""Set up common variables."""
self.summary = xfstestsdb.gtk.model.Summary(name="passed")
def test_init(self):
"""Check that the Summary is set up properly."""
self.assertIsInstance(self.summary, GObject.GObject)
self.assertEqual(self.summary.name, "passed")
def test_compare(self):
"""Test the less-than operator on Summaries."""
failed = xfstestsdb.gtk.model.Summary(name="failed")
skipped = xfstestsdb.gtk.model.Summary(name="skipped")
time = xfstestsdb.gtk.model.Summary(name="time")
expected = [self.summary, failed, skipped, time]
for i in range(10):
with self.subTest(i=i):
shuffled = [time, skipped, failed, self.summary]
random.shuffle(shuffled)
self.assertListEqual(sorted(shuffled), expected)
def test_xunits(self):
"""Test adding xunits to a Summary."""
self.assertIsNone(self.summary["xunit-1"])
self.summary.add_xunit("xunit-1", 123, "unit")
xunit = self.summary["xunit-1"]
self.assertIsInstance(xunit, xfstestsdb.gtk.model.SummaryValue)
self.assertEqual(xunit.name, "xunit-1")
self.assertEqual(xunit.value, 123)
self.assertEqual(xunit.unit, "unit")
def test_get_results(self):
"""Test getting a set of results for this summary."""
self.summary.add_xunit("xunit-1", 1, "unit")
self.assertSetEqual(self.summary.get_results(), {"1 unit"})
self.summary.add_xunit("xunit-2", 2, "unit")
self.assertSetEqual(self.summary.get_results(), {"1 unit", "2 units"})
class TestSummaryList(unittest.TestCase):
"""Test case for our summary list."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-1",
"1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-2",
"1", str(tests.xunit.XUNIT_1)])
self.summary = xfstestsdb.gtk.model.SummaryList(self.xfstestsdb.sql, 1)
def test_init(self):
"""Test that the SummaryList was set up properly."""
self.assertIsInstance(self.summary, GObject.GObject)
self.assertIsInstance(self.summary, Gio.ListModel)
self.assertEqual(self.summary.runid, 1)
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.summary.get_item_type(),
xfstestsdb.gtk.model.Summary.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.summary.get_n_items(), 4)
self.assertEqual(self.summary.n_items, 4)
def test_get_item(self):
"""Test the get_item() function."""
for i, field in enumerate(["passed", "failed", "skipped", "time"]):
with self.subTest(i=i, field=field):
summary = self.summary[i]
self.assertIsInstance(summary, xfstestsdb.gtk.model.Summary)
self.assertEqual(summary.name, field)
match field:
case "passed": expected = {"6 testcases"}
case "failed": expected = {"1 testcase"}
case "skipped": expected = {"3 testcases"}
case "time": expected = {"43 seconds"}
self.assertSetEqual(summary.get_results(), expected)
def test_get_xunits(self):
"""Test the get_xunits() function."""
self.assertListEqual(self.summary.get_xunits(), ["xunit-1", "xunit-2"])

247
tests/gtk/test_row.py Normal file
View File

@ -0,0 +1,247 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our row widgets and factories."""
import unittest
import xfstestsdb.gtk.row
from gi.repository import Gtk
from gi.repository import Adw
class TestLabelFactory(unittest.TestCase):
"""Tests our Gtk.Factory to make Gtk.Labels."""
def setUp(self):
"""Set up common variables."""
self.testcase = xfstestsdb.gtk.model.TestCase("test/case")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.testcase)
self.factory = xfstestsdb.gtk.row.LabelFactory("name")
self.group = xfstestsdb.gtk.row.LabelFactory.group
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, Gtk.SignalListItemFactory)
self.assertEqual(self.factory.property, "name")
def test_size_group(self):
"""Test the label factory global size group."""
self.assertIsInstance(xfstestsdb.gtk.row.LabelFactory.group,
Gtk.SizeGroup)
self.assertEqual(xfstestsdb.gtk.row.LabelFactory.group.props.mode,
Gtk.SizeGroupMode.HORIZONTAL)
def test_setup(self):
"""Test that the factory implements the 'setup' signal."""
self.factory.emit("setup", self.listitem)
self.assertIsInstance(self.listitem.get_child(), Gtk.Label)
self.assertTrue(self.listitem.get_child().has_css_class("numeric"))
self.assertIn(self.listitem.get_child(), self.group.get_widgets())
def test_bind(self):
"""Test that the factory implements the 'bind' signal."""
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "test/case")
for style in xfstestsdb.gtk.row.STYLES.keys():
self.assertFalse(self.listitem.get_child().has_css_class(style))
def test_unbind(self):
"""Test that the factory implements the 'unbind' signal."""
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
def test_teardown(self):
"""Test that the factory implements the 'teardown' signal."""
self.factory.emit("setup", self.listitem)
child = self.listitem.get_child()
self.factory.emit("teardown", self.listitem)
self.assertIsNone(self.listitem.get_child())
self.assertNotIn(child, self.group.get_widgets())
def test_styles(self):
"""Test the column text styles."""
self.assertDictEqual(xfstestsdb.gtk.row.STYLES,
{"passed": "success", "failed": "error",
"skipped": "warning", "time": "accent"})
for style in ["passed", "failed", "skipped", "time"]:
with self.subTest(style=style):
self.testcase.name = style
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
child = self.listitem.get_child()
expected = xfstestsdb.gtk.row.STYLES[style]
self.assertTrue(child.has_css_class(expected))
self.factory.emit("unbind", self.listitem)
self.assertFalse(child.has_css_class(expected))
class TestResultFactory(unittest.TestCase):
"""Tests our Gtk.Factory to show test results."""
def setup_parent(self, factory: xfstestsdb.gtk.row.ResultFactory,
listitem: Gtk.ListItem) -> None:
"""Set the child widget's parent for custom styling."""
self.parent.set_child(listitem.get_child())
def setUp(self):
"""Set up common variables."""
self.testcase = xfstestsdb.gtk.model.TestCase("test/case")
self.parent = Adw.Bin()
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.testcase)
self.factory = xfstestsdb.gtk.row.ResultFactory("xunit-1")
self.factory.connect("setup", self.setup_parent)
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, Gtk.SignalListItemFactory)
self.assertEqual(self.factory.xunit, "xunit-1")
def test_setup(self):
"""Test that the factory implements the 'setup' signal."""
self.factory.emit("setup", self.listitem)
self.assertIsInstance(self.listitem.get_child(), Gtk.Label)
self.assertTrue(self.listitem.get_child().has_css_class("numeric"))
self.assertEqual(self.parent.get_child(), self.listitem.get_child())
def test_bind_passed(self):
"""Test binding to a passing test."""
self.testcase.add_xunit("xunit-1", "passed", 3, "", None, None)
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "3 seconds")
self.assertIsNone(self.listitem.get_child().get_tooltip_text())
self.assertTrue(self.parent.has_css_class("passed"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.parent.has_css_class("passed"))
def test_bind_skipped(self):
"""Test binding to a skipped test."""
self.testcase.add_xunit("xunit-1", "skipped", 0,
"test skipped for ... reasons", None, None)
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "skipped")
self.assertEqual(self.listitem.get_child().get_tooltip_text(),
"test skipped for ... reasons")
self.assertTrue(self.parent.has_css_class("skipped"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.parent.has_css_class("skipped"))
def test_bind_failed(self):
"""Test binding to a failed test."""
self.testcase.add_xunit("xunit-1", "failure", 8,
"- failed. see output", None, None)
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "failure")
self.assertEqual(self.listitem.get_child().get_tooltip_text(),
"failed. see output")
self.assertTrue(self.parent.has_css_class("failure"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.parent.has_css_class("failure"))
def test_bind_missing(self):
"""Test binding to a missing test."""
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.factory.emit("unbind", self.listitem)
def test_teardown(self):
"""Test that the factory implements the 'teardown' signal."""
self.factory.emit("setup", self.listitem)
self.factory.emit("teardown", self.listitem)
self.assertIsNone(self.listitem.get_child())
class TestSummaryFactory(unittest.TestCase):
"""Tests our Gtk.Factory to show Xfstests results summaries."""
def setUp(self):
"""Set up common variables."""
self.summary = xfstestsdb.gtk.model.Summary("passed")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.summary)
self.factory = xfstestsdb.gtk.row.SummaryFactory("xunit-1")
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, Gtk.SignalListItemFactory)
self.assertEqual(self.factory.xunit, "xunit-1")
def test_setup(self):
"""Test that the factory implements the 'setup' signal."""
self.factory.emit("setup", self.listitem)
self.assertIsInstance(self.listitem.get_child(), Gtk.Label)
self.assertTrue(self.listitem.get_child().has_css_class("numeric"))
def test_bind_passed(self):
"""Test binding to the passed tests summary."""
self.summary.add_xunit("xunit-1", 1, "testcase")
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "1 testcase")
self.assertTrue(self.listitem.get_child().has_css_class("success"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.listitem.get_child().has_css_class("success"))
def test_bind_failed(self):
"""Test binding to the failed tests summary."""
self.summary.name = "failed"
self.summary.add_xunit("xunit-1", 1, "testcase")
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "1 testcase")
self.assertTrue(self.listitem.get_child().has_css_class("error"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.listitem.get_child().has_css_class("error"))
def test_bind_skipped(self):
"""Test binding to the skipped tests summary."""
self.summary.name = "skipped"
self.summary.add_xunit("xunit-1", 1, "testcase")
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "1 testcase")
self.assertTrue(self.listitem.get_child().has_css_class("warning"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.listitem.get_child().has_css_class("warning"))
def test_bind_time(self):
"""Test binding to the time summary."""
self.summary.name = "time"
self.summary.add_xunit("xunit-1", 1, "second")
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "1 second")
self.assertTrue(self.listitem.get_child().has_css_class("accent"))
self.factory.emit("unbind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "")
self.assertFalse(self.listitem.get_child().has_css_class("accent"))
def test_teardown(self):
"""Test that the factory implements the 'teardown' signal."""
self.factory.emit("setup", self.listitem)
self.factory.emit("teardown", self.listitem)
self.assertIsNone(self.listitem.get_child())

278
tests/gtk/test_view.py Normal file
View File

@ -0,0 +1,278 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our TestCaseView for displaying xfstests results."""
import unittest
import tests.xunit
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."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-1",
"1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-2",
"1", str(tests.xunit.XUNIT_1)])
self.model = xfstestsdb.gtk.model.TestCaseList(self.xfstestsdb.sql, 1)
self.view = xfstestsdb.gtk.view.TestCaseView()
def test_init(self):
"""Test that we create the TestCaseView correctly."""
self.assertIsInstance(self.view, Gtk.ScrolledWindow)
self.assertTrue(self.view.has_css_class("card"))
self.assertIsNone(self.view.model)
def test_columnview(self):
"""Test that we set up the Columnview correctly."""
self.assertIsInstance(self.view.props.child, Gtk.ColumnView)
self.assertIsInstance(self.view.props.child.get_model(),
Gtk.NoSelection)
self.assertEqual(len(self.view.props.child.get_columns()), 0)
self.assertTrue(self.view.props.child.get_show_column_separators())
self.assertTrue(self.view.props.child.get_show_row_separators())
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)
self.assertIsInstance(self.view._testcase.props.factory,
xfstestsdb.gtk.row.LabelFactory)
self.assertEqual(self.view._testcase.props.factory.property, "name")
self.assertEqual(self.view._testcase.props.title, "testcase")
self.assertFalse(self.view._testcase.props.expand)
def test_model(self):
"""Test setting the model property."""
self.view.model = self.model
self.assertEqual(self.view._filtermodel.props.model, self.model)
columns = self.view.props.child.get_columns()
self.assertEqual(len(columns), 3)
self.assertEqual(columns[0], self.view._testcase)
for i, title in enumerate(["xunit-1", "xunit-2"], start=1):
with self.subTest(i=i, title=title):
self.assertIsInstance(columns[i].props.factory,
xfstestsdb.gtk.row.ResultFactory)
self.assertEqual(columns[i].props.factory.xunit, title)
self.assertEqual(columns[i].props.title, title)
self.assertTrue(columns[i].props.expand)
self.view.model = None
self.assertEqual(len(self.view.props.child.get_columns()), 0)
self.view.model = self.model
self.assertEqual(self.view.props.child.get_columns()[0],
self.view._testcase)
class TestSummaryView(unittest.TestCase):
"""Tests the SummaryView."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-1",
"1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-2",
"1", str(tests.xunit.XUNIT_1)])
self.model = xfstestsdb.gtk.model.SummaryList(self.xfstestsdb.sql, 1)
self.view = xfstestsdb.gtk.view.SummaryView()
def test_init(self):
"""Test that we created the SummaryView correctly."""
self.assertIsInstance(self.view, Gtk.ScrolledWindow)
self.assertEqual(self.view.props.vscrollbar_policy,
Gtk.PolicyType.NEVER)
self.assertTrue(self.view.has_css_class("card"))
self.assertIsNone(self.view.model)
def test_columnview(self):
"""Test that we set up the Columnview correctly."""
self.assertIsInstance(self.view.props.child, Gtk.ColumnView)
self.assertIsInstance(self.view.props.child.get_model(),
Gtk.NoSelection)
self.assertEqual(len(self.view.props.child.get_columns()), 0)
self.assertTrue(self.view.props.child.get_show_column_separators())
self.assertTrue(self.view.props.child.get_show_row_separators())
self.assertTrue(self.view.props.child.get_hexpand())
def test_summary_column(self):
"""Test that we set up the 'summary' column correctly."""
self.assertIsInstance(self.view._summary, Gtk.ColumnViewColumn)
self.assertIsInstance(self.view._summary.props.factory,
xfstestsdb.gtk.row.LabelFactory)
self.assertEqual(self.view._summary.props.factory.property, "name")
self.assertEqual(self.view._summary.props.title, "summary")
self.assertFalse(self.view._summary.props.expand)
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)
columns = self.view.props.child.get_columns()
self.assertEqual(len(columns), 3)
self.assertEqual(columns[0], self.view._summary)
for i, title in enumerate(["xunit-1", "xunit-2"], start=1):
with self.subTest(i=i, title=title):
self.assertIsInstance(columns[i].props.factory,
xfstestsdb.gtk.row.SummaryFactory)
self.assertEqual(columns[i].props.factory.xunit, title)
self.assertEqual(columns[i].props.title, title)
self.assertTrue(columns[i].props.expand)
self.view.model = None
self.assertEqual(len(self.view.props.child.get_columns()), 0)
self.view.model = self.model
self.assertEqual(self.view.props.child.get_columns()[0],
self.view._summary)
class TestXfstestsView(unittest.TestCase):
"""Test the XfstestsView."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-1",
"1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "--name", "xunit-2",
"1", str(tests.xunit.XUNIT_1)])
self.model = xfstestsdb.gtk.model.TestCaseList(self.xfstestsdb.sql, 1)
self.summary = xfstestsdb.gtk.model.SummaryList(self.xfstestsdb.sql, 1)
self.view = xfstestsdb.gtk.view.XfstestsView()
def test_init(self):
"""Check that the XfstestsView was set up correctly."""
self.assertIsInstance(self.view, Gtk.Box)
self.assertEqual(self.view.props.orientation, Gtk.Orientation.VERTICAL)
def test_testcase_view(self):
"""Check that the XfstestsView sets up a TestCaseView correctly."""
self.assertIsInstance(self.view._testcaseview,
xfstestsdb.gtk.view.TestCaseView)
self.assertEqual(self.view.get_first_child(), self.view._testcaseview)
def test_summary_view(self):
"""Check that the XfstestsView sets up a SummaryView correctly."""
sep = self.view._testcaseview.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertIsInstance(self.view._summaryview,
xfstestsdb.gtk.view.SummaryView)
self.assertEqual(sep.get_next_sibling(), self.view._summaryview)
def test_model(self):
"""Test the XfstestsView 'model' property."""
self.assertIsNone(self.view.model)
self.view.model = self.model
self.assertEqual(self.view._testcaseview.model, self.model)
def test_summary(self):
"""Test the XfstestsView 'summary' property."""
self.assertIsNone(self.view.summary)
self.view.summary = self.summary
self.assertEqual(self.view._summaryview.model, self.summary)
def test_filterbuttons(self):
"""Test the XfstestsView 'filterbuttons' property."""
self.assertEqual(self.view.filterbuttons,
self.view._testcaseview.filterbuttons)

66
tests/gtk/test_window.py Normal file
View File

@ -0,0 +1,66 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our Window implementation."""
import unittest
import xfstestsdb.gtk.window
from gi.repository import Gtk
from gi.repository import Adw
class TestWindow(unittest.TestCase):
"""Test case for our Window subclass."""
def setUp(self):
"""Set up common variables."""
self.window = xfstestsdb.gtk.window.Window()
def test_init(self):
"""Check that the Window is set up correctly."""
self.assertIsInstance(self.window, Adw.Window)
self.assertEqual(self.window.props.default_height, 800)
self.assertEqual(self.window.props.default_width, 1200)
self.assertTrue(self.window.has_css_class("devel"))
self.assertIsInstance(self.window.props.content, Gtk.Box)
self.assertEqual(self.window.props.content.get_orientation(),
Gtk.Orientation.VERTICAL)
def test_headerbar(self):
"""Test the headerbar property."""
self.assertIsInstance(self.window.headerbar, Adw.HeaderBar)
self.assertEqual(self.window.props.content.get_first_child(),
self.window.headerbar)
self.assertTrue(self.window.headerbar.has_css_class("flat"))
def test_title_widget(self):
"""Test the title widget property."""
self.assertIsInstance(self.window.title, Adw.WindowTitle)
self.assertEqual(self.window.title.props.title, "xfstestsdb gtk")
self.assertEqual(self.window.headerbar.props.title_widget,
self.window.title)
def test_child(self):
"""Test the window child property."""
self.assertIsInstance(self.window.headerbar.get_next_sibling(),
Adw.Bin)
self.assertIsNone(self.window.child)
self.window.child = Gtk.Label()
self.assertEqual(self.window.headerbar.get_next_sibling().props.child,
self.window.child)
label = Gtk.Label()
window2 = xfstestsdb.gtk.window.Window(child=label)
self.assertEqual(window2.child, label)
self.assertEqual(window2.headerbar.get_next_sibling().props.child,
label)
def test_runid(self):
"""Test the window runid property."""
self.assertEqual(self.window.runid, 0)
self.assertEqual(self.window.title.props.subtitle, "")
self.window.runid = 3
self.assertEqual(self.window.title.props.subtitle, "runid #3")
self.window.runid = 0
self.assertEqual(self.window.title.props.subtitle, "")

165
tests/test_gtk.py Normal file
View File

@ -0,0 +1,165 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests the `xfstestsdb gtk` command."""
import errno
import io
import unittest
import unittest.mock
import xfstestsdb.gtk
from gi.repository import Gio
from gi.repository import Adw
class TestApplication(unittest.TestCase):
"""Tests the Gtk Application."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.application = xfstestsdb.gtk.Application(self.xfstestsdb.sql)
def test_init(self):
"""Check that the Gtk Application was set up properly."""
self.assertIsInstance(self.application, Adw.Application)
self.assertEqual(self.application.get_application_id(),
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)
self.assertIsNone(self.application.summary)
@unittest.mock.patch("gi.repository.Adw.Application.activate")
@unittest.mock.patch("gi.repository.Adw.Application.do_command_line")
def test_command_line(self, mock_command_line: unittest.mock.Mock,
mock_activate: unittest.mock.Mock):
"""Check that we handle the command-line signal."""
mock_cmd = unittest.mock.Mock()
mock_cmd.get_arguments.return_value = []
self.application.do_command_line(mock_cmd)
mock_command_line.assert_called_with(self.application, mock_cmd)
mock_cmd.get_arguments.assert_called()
mock_activate.assert_called()
self.assertEqual(self.application.runid, 0)
self.assertIsNone(self.application.model)
self.assertIsNone(self.application.summary)
mock_command_line.reset_mock()
mock_activate.reset_mock()
mock_cmd.reset_mock()
mock_cmd.get_arguments.return_value = ["runid=42"]
self.application.do_command_line(mock_cmd)
mock_command_line.assert_called_with(self.application, mock_cmd)
mock_cmd.get_arguments.assert_called()
mock_activate.assert_called()
self.assertEqual(self.application.runid, 42)
self.assertIsInstance(self.application.model,
xfstestsdb.gtk.model.TestCaseList)
self.assertIsInstance(self.application.summary,
xfstestsdb.gtk.model.SummaryList)
self.assertEqual(self.application.model.runid, 42)
@unittest.mock.patch("xfstestsdb.gtk.gsetup.add_style")
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
@unittest.mock.patch("gi.repository.Adw.Application.do_startup")
def test_startup(self, mock_startup: unittest.mock.Mock,
mock_add_window: unittest.mock.Mock,
mock_add_style: unittest.mock.Mock):
"""Check that startup sets up our application instance correctly."""
self.assertIsNone(self.application.win)
self.assertIsNone(self.application.view)
self.application.emit("startup")
self.assertIsInstance(self.application.view,
xfstestsdb.gtk.view.XfstestsView)
self.assertIsInstance(self.application.win,
xfstestsdb.gtk.window.Window)
self.assertEqual(self.application.win.child, self.application.view)
mock_startup.assert_called_with(self.application)
mock_add_window.assert_called_with(self.application.win)
mock_add_style.assert_called()
self.application.runid = 42
self.assertEqual(self.application.win.runid, 42)
model = xfstestsdb.gtk.model.TestCaseList(self.xfstestsdb.sql, 42)
self.application.model = model
self.assertEqual(self.application.view.model, model)
summary = xfstestsdb.gtk.model.SummaryList(self.xfstestsdb.sql, 42)
self.application.summary = summary
self.assertEqual(self.application.view.summary, summary)
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
@unittest.mock.patch("gi.repository.Adw.Application.do_startup")
@unittest.mock.patch("gi.repository.Adw.Application.do_activate")
def test_activate(self, mock_activate: unittest.mock.Mock,
mock_startup: unittest.mock.Mock,
mock_add_window: unittest.mock.Mock):
"""Check that activating our application works correctly."""
self.application.emit("startup")
with unittest.mock.patch.object(self.application.win,
"present") as mock_present:
self.application.emit("activate")
mock_activate.assert_called_with(self.application)
mock_present.assert_called()
@unittest.mock.patch("gi.repository.Adw.Application.do_shutdown")
def test_shutdown(self, mock_shutdown: unittest.mock.Mock):
"""Check that shutting down application cleans up global state."""
self.application.emit("shutdown")
mock_shutdown.assert_called_with(self.application)
class TestGtk(unittest.TestCase):
"""Tests the `xfstestsdb gtk` command."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.gtk = self.xfstestsdb.commands["gtk"]
def test_init(self):
"""Check that the gtk command was set up properly."""
self.assertIsInstance(self.gtk, xfstestsdb.commands.Command)
self.assertIsInstance(self.gtk, xfstestsdb.gtk.Command)
self.assertEqual(self.xfstestsdb.subparser.choices["gtk"],
self.gtk.parser)
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
@unittest.mock.patch("xfstestsdb.gtk.Application")
def test_gtk_empty(self, mock_app: unittest.mock.Mock,
mock_stderr: unittest.mock.Mock):
"""Check that running `xfstestsdb gtk` without a runid."""
with self.assertRaises(SystemExit):
self.xfstestsdb.run(["gtk"])
self.assertRegex(mock_stderr.getvalue(),
"error: the following arguments are required: runid")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
@unittest.mock.patch("xfstestsdb.gtk.Application")
def test_gtk_runid(self, mock_app: unittest.mock.Mock,
mock_stdout: io.StringIO):
"""Check running `xfstestsdb gtk` with the --runid option."""
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["gtk", "1"])
mock_app.assert_called_with(self.xfstestsdb.sql)
mock_app.return_value.run.assert_called_with(["runid=1"])
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
@unittest.mock.patch("xfstestsdb.gtk.Application")
def test_gtk_error(self, mock_app: unittest.mock.Mock,
mock_stderr: io.StringIO):
"""Check running the gtk command with an invalid runid."""
with self.assertRaises(SystemExit) as sys_exit:
self.xfstestsdb.run(["gtk", "2"])
mock_app.assert_not_called()
self.assertEqual(sys_exit.exception.code, errno.ENOENT)
self.assertEqual(mock_stderr.getvalue(),
"error: run #2 does not exist\n")

View File

@ -32,7 +32,7 @@ class TestXfstestsdb(unittest.TestCase):
self.xfstestsdb.run([])
self.assertRegex(mock_stdout.getvalue(),
r"^usage: pytest \[\-h\] \[\-\-version\]"
r"\s+\{.*?} ...$")
r"\s+\{.*?}\s+...$")
def test_run(self):
"""Test running the xfstestsdb."""
@ -52,7 +52,7 @@ class TestXfstestsdb(unittest.TestCase):
def test_version(self, mock_stdout: io.StringIO):
"""Test printing version information."""
self.assertEqual(xfstestsdb.MAJOR, 1)
self.assertEqual(xfstestsdb.MINOR, 2)
self.assertEqual(xfstestsdb.MINOR, 3)
self.xfstestsdb.run(["--version"])
self.assertEqual(mock_stdout.getvalue(), "xfstestsdb v1.2-debug\n")
self.assertEqual(mock_stdout.getvalue(), "xfstestsdb v1.3-debug\n")

View File

@ -4,6 +4,7 @@ import argparse
from . import sqlite
from . import delete
from . import gc
from . import gtk
from . import list
from . import new
from . import rename
@ -14,7 +15,7 @@ from . import untag
from . import xunit
MAJOR = 1
MINOR = 2
MINOR = 3
class Command:
@ -30,6 +31,7 @@ class Command:
self.sql = sqlite.Connection()
self.commands = {"delete": delete.Command(self.subparser, self.sql),
"gc": gc.Command(self.subparser, self.sql),
"gtk": gtk.Command(self.subparser, self.sql),
"list": list.Command(self.subparser, self.sql),
"new": new.Command(self.subparser, self.sql),
"rename": rename.Command(self.subparser, self.sql),

View File

@ -0,0 +1,95 @@
# Copyright 2023 (c) Anna Schumaker.
"""The `xfstestsdb gtk` command."""
import argparse
import errno
import sys
from . import gsetup
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Adw
from . import model
from . import view
from . import window
from .. import commands
from .. import sqlite
class Application(Adw.Application):
"""Our Adw.Application for displaying xfstests results."""
runid = GObject.Property(type=int)
summary = GObject.Property(type=model.SummaryList)
model = GObject.Property(type=model.TestCaseList)
win = GObject.Property(type=window.Window)
view = GObject.Property(type=view.XfstestsView)
sql = GObject.Property(type=GObject.TYPE_PYOBJECT)
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)
def do_command_line(self, cmd_line: Gio.ApplicationCommandLine) -> int:
"""Handle the Adw.Application::command-line signal."""
Adw.Application.do_command_line(self, cmd_line)
for arg in cmd_line.get_arguments():
split = arg.split("=")
match split[0]:
case "runid":
self.runid = int(split[1])
self.model = model.TestCaseList(self.sql, self.runid)
self.summary = model.SummaryList(self.sql, self.runid)
self.activate()
return 0
def do_startup(self) -> None:
"""Handle the Adw.Application::startup signal."""
Adw.Application.do_startup(self)
gsetup.add_style()
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.bind_property("summary", self.view, "summary")
self.add_window(self.win)
def do_activate(self) -> None:
"""Handle the Adw.Application::activate signal."""
Adw.Application.do_activate(self)
self.win.present()
def do_shutdown(self) -> None:
"""Handle the Adw.Application::shutdown signal."""
Adw.Application.do_shutdown(self)
class Command(commands.Command):
"""The `xfstestsdb gtk` command."""
def __init__(self, subparser: argparse.Action,
sql: sqlite.Connection) -> None:
"""Set up the gtk command."""
super().__init__(subparser, sql, "gtk", help="show a gtk-based ui")
self.parser.add_argument("runid", metavar="runid", nargs=1, type=int,
help="show a specific xfstests run")
def do_command(self, args: argparse.Namespace) -> None:
"""Run the Gtk UI."""
app_args = []
if self.sql("SELECT 1 FROM xfstests_runs WHERE runid=?",
args.runid[0]).fetchone():
app_args.append(f"runid={args.runid[0]}")
else:
print(f"error: run #{args.runid[0]} does not exist",
file=sys.stderr)
sys.exit(errno.ENOENT)
Application(self.sql).run(app_args)

29
xfstestsdb/gtk/button.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright 2023 (c) Anna Schumaker.
"""Custom, reusable Button classes."""
import typing
from gi.repository import GObject
from gi.repository import Gtk
class StatusToggle(Gtk.Button):
"""A toggle button that adds or removes a CSS class when pressed."""
active = GObject.Property(type=bool, default=False)
icon_name = GObject.Property(type=str)
def __init__(self, icon_name: str, css_class: str,
*, active: bool = False):
"""Initialize a StatusToggle button."""
super().__init__(icon_name=icon_name, has_frame=False,
child=Gtk.Image(icon_name=icon_name, opacity=0.4))
self.connect("notify::active", self.__notify_active)
self.add_css_class(css_class)
self.active = active
def __notify_active(self, toggle: typing.Self,
param: GObject.ParamSpec) -> None:
self.props.child.set_opacity(1.0 if self.active else 0.4)
def do_clicked(self) -> None:
"""Adjust image opacity when the button is toggled."""
self.active = not self.active

28
xfstestsdb/gtk/gsetup.py Normal file
View File

@ -0,0 +1,28 @@
# Copyright 2023 (c) Anna Schumaker.
"""Early setup of Gtk modules to avoid errors later."""
import gi
import pathlib
gi.require_version("Adw", "1")
gi.importlib.import_module("gi.repository.Adw")
DEBUG_STR = "-debug" if __debug__ else ""
APPLICATION_ID = f"com.nowheycreamery.xfstestsdb.gtk{DEBUG_STR}"
__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."""
style = gi.repository.Gtk.StyleContext
style.add_provider_for_display(gi.repository.Gdk.Display.get_default(),
CSS_PROVIDER, CSS_PRIORITY)

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 7.96875 1 c -3.851562 0 -6.96875 3.117188 -6.96875 6.96875 s 3.117188 6.96875 6.96875 6.96875 s 6.96875 -3.117188 6.96875 -6.96875 s -3.117188 -6.96875 -6.96875 -6.96875 z m -3 3.96875 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 1.28125 1.28125 l 1.3125 -1.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -1.28125 1.28125 l 1.25 1.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -1.28125 -1.28125 l -1.28125 1.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 1.28125 -1.25 l -1.28125 -1.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8.234375 1.003906 c -2.15625 -0.070312 -4.277344 0.859375 -5.683594 2.601563 c -1.871093 2.316406 -2.066406 5.582031 -0.484375 8.105469 c 1.582032 2.527343 4.601563 3.777343 7.503906 3.109374 c 2.90625 -0.667968 5.074219 -3.117187 5.390626 -6.082031 c 0.046874 -0.359375 -0.101563 -0.71875 -0.394532 -0.933593 c -0.292968 -0.21875 -0.679687 -0.257813 -1.011718 -0.109376 c -0.332032 0.152344 -0.554688 0.472657 -0.582032 0.835938 c -0.226562 2.121094 -1.769531 3.863281 -3.851562 4.34375 c -2.078125 0.476562 -4.226563 -0.414062 -5.359375 -2.222656 c -1.132813 -1.808594 -0.992188 -4.128906 0.347656 -5.792969 s 3.578125 -2.289063 5.585937 -1.5625 c 0.339844 0.128906 0.71875 0.066406 0.996094 -0.167969 c 0.28125 -0.230468 0.410156 -0.59375 0.34375 -0.949218 c -0.0625 -0.355469 -0.316406 -0.648438 -0.660156 -0.761719 c -0.699219 -0.253907 -1.421875 -0.390625 -2.140625 -0.414063 z m 0 0"/><path d="m 13.167969 2.542969 l -5.292969 5.292969 l -2.417969 -2.417969 l -1.414062 1.414062 l 3.832031 3.832031 l 6.707031 -6.707031 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8 1 c -3.855469 0 -7 3.144531 -7 7 s 3.144531 7 7 7 s 7 -3.144531 7 -7 s -3.144531 -7 -7 -7 z m 0 2 c 2.753906 0 5 2.246094 5 5 s -2.246094 5 -5 5 s -5 -2.246094 -5 -5 s 2.246094 -5 5 -5 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/nowheycreamery/xfstestsdb/icons/scalable/actions">
<file preprocess="xml-stripblanks">test-pass-symbolic.svg</file>
<file preprocess="xml-stripblanks">test-skip-symbolic.svg</file>
<file preprocess="xml-stripblanks">test-fail-symbolic.svg</file>
</gresource>
</gresources>

216
xfstestsdb/gtk/model.py Normal file
View File

@ -0,0 +1,216 @@
# Copyright 2023 (c) Anna Schumaker.
"""Our Testcase Gio.ListModel."""
import typing
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .. import sqlite
class XunitResult(GObject.GObject):
"""The results for a single TestCase with a specific Xunit."""
name = GObject.Property(type=str)
status = GObject.Property(type=str)
time = GObject.Property(type=int)
message = GObject.Property(type=str)
stdout = GObject.Property(type=str)
stderr = GObject.Property(type=str)
class TestCase(GObject.GObject):
"""Collects results for a single TestCase with multiple Xunits."""
name = GObject.Property(type=str)
def __init__(self, name: str) -> None:
"""Initialize a TestCase object."""
super().__init__(name=name)
self.__xunits = {}
def __getitem__(self, xunit: str) -> XunitResult | None:
"""Get the results for a specific Xunit."""
return self.__xunits.get(xunit)
def __lt__(self, rhs: typing.Self) -> bool:
"""Compare the names of two TestCases."""
return self.name < rhs.name
def add_xunit(self, name: str, status: str, time: int,
message: str | None, stdout: str | None,
stderr: str | None) -> None:
"""Add an xunit result to the TestCase."""
message = "" if message is None else message
stdout = "" if stdout is None else stdout
stderr = "" if stderr is None else stderr
self.__xunits[name] = XunitResult(name=name, status=status, time=time,
message=message, stdout=stdout,
stderr=stderr)
def get_results(self) -> set[str]:
"""Get a set of results for each added xunit."""
return {xunit.status for xunit in self.__xunits.values()}
class TestCaseList(GObject.GObject, Gio.ListModel):
"""A list of TestCases for a specific Xfstests Run."""
runid = GObject.Property(type=int)
n_items = GObject.Property(type=int)
def __init__(self, sql: sqlite.Connection, runid: int) -> None:
"""Initialize a TestCaseList."""
super().__init__(runid=runid)
self.__xunits = set()
cases = {}
cur = sql("""SELECT testcase, xunit, status,
time, message, stdout, stderr
FROM testcases_view WHERE runid=?""", runid)
for row in cur.fetchall():
testcase = cases.setdefault(row["testcase"],
TestCase(row["testcase"]))
testcase.add_xunit(row["xunit"], row["status"], row["time"],
row["message"], row["stdout"], row["stderr"])
self.__xunits.add(row["xunit"])
self.__items = sorted(cases.values())
self.n_items = len(self.__items)
def do_get_item_type(self) -> GObject.GType:
"""Get the type of the objects in the list."""
return TestCase.__gtype__
def do_get_n_items(self) -> int:
"""Get the number of items in the list."""
return self.n_items
def do_get_item(self, n: int) -> TestCase | None:
"""Get a specific item on the list."""
return self.__items[n] if n < self.n_items else None
def get_xunits(self) -> list[str]:
"""Get a list of xunits attached to this xfstests run."""
return sorted(self.__xunits)
class TestCaseFilter(Gtk.Filter):
"""A filter for TestCases."""
passed = GObject.Property(type=bool, default=False)
skipped = GObject.Property(type=bool, default=False)
failure = GObject.Property(type=bool, default=True)
def __init__(self) -> None:
"""Initialize a TestCaseFilter."""
super().__init__()
self.connect("notify", self.__notify)
def __notify(self, filter: typing.Self, param: GObject.ParamSpec) -> None:
match param.name:
case "passed" | "skipped" | "failure":
if self.get_property(param.name):
change = Gtk.FilterChange.LESS_STRICT
else:
change = Gtk.FilterChange.MORE_STRICT
self.changed(change)
def do_get_strictness(self) -> Gtk.FilterMatch:
"""Get the current strictness of the filter."""
match (self.passed, self.skipped, self.failure):
case (True, True, True): return Gtk.FilterMatch.ALL
case (False, False, False): return Gtk.FilterMatch.NONE
case (_, _, _): return Gtk.FilterMatch.SOME
def do_match(self, testcase: TestCase) -> bool:
"""Check if a testcase matches the current filter."""
results = testcase.get_results()
if self.passed and "passed" in results:
return True
if self.skipped and "skipped" in results:
return True
if self.failure and "failure" in results:
return True
return False
class SummaryValue(GObject.GObject):
"""The summary of a single Xfstests xunit field."""
name = GObject.Property(type=str)
unit = GObject.Property(type=str)
value = GObject.Property(type=int)
def __str__(self) -> str:
"""Convert a Summary Value to a string."""
s = '' if self.value == 1 else 's'
return f"{self.value} {self.unit}{s}"
class Summary(GObject.GObject):
"""Collects values for each summary field with multiple Xunits."""
name = GObject.Property(type=str)
def __init__(self, name: str) -> None:
"""Initialize a Summary object."""
super().__init__(name=name)
self.__xunits = {}
def __getitem__(self, xunit: str) -> SummaryValue | None:
"""Get the summary for a specific Xunit."""
return self.__xunits.get(xunit)
def __lt__(self, rhs: typing.Self) -> bool:
"""Compare the fields of two Summaries."""
order = ["passed", "failed", "skipped", "time"]
return order.index(self.name) < order.index(rhs.name)
def add_xunit(self, name: str, value: int, unit: str) -> None:
"""Add an xunit summary to the Summary."""
self.__xunits[name] = SummaryValue(name=name, value=value, unit=unit)
def get_results(self) -> set[str]:
"""Get a set of results for each added xunit."""
return {str(value) for value in self.__xunits.values()}
class SummaryList(GObject.GObject, Gio.ListModel):
"""A list summarizing the results of a specific Xfstests Run."""
runid = GObject.Property(type=int)
n_items = GObject.Property(type=int)
def __init__(self, sql: sqlite.Connection, runid: int) -> None:
"""Initialize a SummaryList."""
super().__init__(runid=runid)
self.__xunits = set()
results = {}
cur = sql("""SELECT name AS xunit, passed, failed, skipped, time
FROM xunits_view WHERE runid=?""", runid)
for row in cur.fetchall():
for field in ["passed", "failed", "skipped", "time"]:
summary = results.setdefault(field, Summary(field))
summary.add_xunit(row["xunit"], row[field],
"second" if field == "time" else "testcase")
self.__xunits.add(row["xunit"])
self.__items = sorted(results.values())
self.n_items = len(self.__items)
def do_get_item_type(self) -> GObject.GType:
"""Get the type of the objects in the list."""
return Summary.__gtype__
def do_get_n_items(self) -> int:
"""Get the number of items in the list."""
return self.n_items
def do_get_item(self, n: int) -> Summary | None:
"""Get a specific item on the list."""
return self.__items[n] if n < self.n_items else None
def get_xunits(self) -> list[str]:
"""Get a list of xunits attached to this xfstests run."""
return sorted(self.__xunits)

135
xfstestsdb/gtk/row.py Normal file
View File

@ -0,0 +1,135 @@
# Copyright 2023 (c) Anna Schumaker.
"""Our TestCase row widgets and factory."""
import typing
from gi.repository import GObject
from gi.repository import Gtk
STYLES = {"passed": "success", "failed": "error",
"skipped": "warning", "time": "accent"}
class LabelFactory(Gtk.SignalListItemFactory):
"""Create Gtk.Labels for each testcase."""
property = GObject.Property(type=str)
group = Gtk.SizeGroup()
def __init__(self, property: str):
"""Initialize our InscriptionFactory."""
super().__init__(property=property)
self.connect("setup", self.do_setup)
self.connect("bind", self.do_bind)
self.connect("unbind", self.do_unbind)
self.connect("teardown", self.do_teardown)
def do_setup(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Set up a ListItem child widget."""
child = Gtk.Label()
child.add_css_class("numeric")
listitem.set_child(child)
LabelFactory.group.add_widget(child)
def do_bind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Bind a ListItem to the child widget."""
text = listitem.get_item().get_property(self.property)
child = listitem.get_child()
if style := STYLES.get(text):
child.add_css_class(style)
child.set_text(text)
def do_unbind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Unbind a ListItem from the child widget."""
child = listitem.get_child()
for style in STYLES.values():
child.remove_css_class(style)
child.set_text("")
def do_teardown(self, factory: typing.Self,
listitem: Gtk.ListItem) -> None:
"""Clean up a ListItem child widget."""
if (child := listitem.get_child()) is not None:
LabelFactory.group.remove_widget(child)
listitem.set_child(None)
class ResultFactory(Gtk.SignalListItemFactory):
"""Factory for making test result widgets."""
xunit = GObject.Property(type=str)
def __init__(self, xunit: str):
"""Initialize our ResultFactory."""
super().__init__(xunit=xunit)
self.connect("setup", self.do_setup)
self.connect("bind", self.do_bind)
self.connect("unbind", self.do_unbind)
self.connect("teardown", self.do_teardown)
def do_setup(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Set up a ListItem child widget."""
listitem.set_child(Gtk.Label())
listitem.get_child().add_css_class("numeric")
def do_bind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Bind a ListItem to the child widget."""
if (result := listitem.get_item()[self.xunit]) is None:
return
if (text := result.status) == "passed":
text = f"{result.time} seconds"
child = listitem.get_child()
child.set_text(text)
child.set_tooltip_text(result.message.lstrip(" -"))
child.get_parent().add_css_class(result.status)
def do_unbind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Unbind a ListItem from the child widget."""
if (result := listitem.get_item()[self.xunit]) is not None:
child = listitem.get_child()
child.set_text("")
child.get_parent().remove_css_class(result.status)
def do_teardown(self, factory: typing.Self,
listitem: Gtk.ListItem) -> None:
"""Clean up a ListItem child widget."""
listitem.set_child(None)
class SummaryFactory(Gtk.SignalListItemFactory):
"""Factory for making test summary widgets."""
xunit = GObject.Property(type=str)
def __init__(self, xunit: str):
"""Initialize our ResultFactory."""
super().__init__(xunit=xunit)
self.connect("setup", self.do_setup)
self.connect("bind", self.do_bind)
self.connect("unbind", self.do_unbind)
self.connect("teardown", self.do_teardown)
def do_setup(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Set up a ListItem child widget."""
listitem.set_child(Gtk.Label())
listitem.get_child().add_css_class("numeric")
def do_bind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Bind a ListItem to the child widget."""
summary = listitem.get_item()
result = summary[self.xunit]
child = listitem.get_child()
child.set_text(str(result))
child.add_css_class(STYLES[summary.name])
def do_unbind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Unbind a ListItem from the child widget."""
child = listitem.get_child()
child.set_text("")
child.remove_css_class(STYLES[listitem.get_item().name])
def do_teardown(self, factory: typing.Self,
listitem: Gtk.ListItem) -> None:
"""Clean up a ListItem child widget."""
listitem.set_child(None)

146
xfstestsdb/gtk/view.py Normal file
View File

@ -0,0 +1,146 @@
# Copyright 2023 (c) Anna Schumaker.
"""A view widget used to display our TestCaseModel."""
from gi.repository import GObject
from gi.repository import Gtk
from .model import TestCaseList
from .model import TestCaseFilter
from .model import SummaryList
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),
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._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._filtermodel.props.model = new
if new is not None:
self.props.child.append_column(self._testcase)
for xunit in new.get_xunits():
self.props.child.append_column(self.__xunit_column(xunit))
class SummaryView(Gtk.ScrolledWindow):
"""Displays our SummaryList model to the user."""
def __init__(self):
"""Initialize a SummaryView."""
super().__init__(child=Gtk.ColumnView(model=Gtk.NoSelection(),
show_row_separators=True,
show_column_separators=True,
hexpand=True),
vscrollbar_policy=Gtk.PolicyType.NEVER)
self._summary = Gtk.ColumnViewColumn(title="summary",
factory=row.LabelFactory("name"))
self.add_css_class("card")
def __summary_column(self, xunit: str) -> None:
return Gtk.ColumnViewColumn(title=xunit, expand=True,
factory=row.SummaryFactory(xunit))
@GObject.Property(type=SummaryList)
def model(self) -> SummaryList:
"""Get the SummaryList shown by the View."""
return self.props.child.get_model().get_model()
@model.setter
def model(self, new: SummaryList) -> 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)
if new is not None:
self.props.child.append_column(self._summary)
for xunit in new.get_xunits():
self.props.child.append_column(self.__summary_column(xunit))
class XfstestsView(Gtk.Box):
"""A widget to display the results of an Xfstests runs."""
model = GObject.Property(type=TestCaseList)
summary = GObject.Property(type=SummaryList)
def __init__(self):
"""Initialize an XfstestsView."""
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self._testcaseview = TestCaseView()
self._summaryview = SummaryView()
self.bind_property("model", self._testcaseview, "model")
self.bind_property("summary", self._summaryview, "model")
self.append(self._testcaseview)
self.append(Gtk.Separator())
self.append(self._summaryview)
@GObject.Property(type=FilterButtons)
def filterbuttons(self) -> FilterButtons:
"""Get the FilterButtons attached to the child TestCaseView."""
return self._testcaseview.filterbuttons

38
xfstestsdb/gtk/window.py Normal file
View File

@ -0,0 +1,38 @@
# Copyright 2023 (c) Anna Schumaker.
"""The main Adw.Window implementation for our application."""
import typing
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Adw
class Window(Adw.Window):
"""Our customized Window displayed to the user."""
child = GObject.Property(type=Gtk.Widget)
headerbar = GObject.Property(type=Adw.HeaderBar)
title = GObject.Property(type=Adw.WindowTitle)
runid = GObject.Property(type=int)
def __init__(self, **kwargs):
"""Set up our Window."""
super().__init__(default_height=800, default_width=1200,
content=Gtk.Box(orientation=Gtk.Orientation.VERTICAL),
title=Adw.WindowTitle(title="xfstestsdb gtk"),
headerbar=Adw.HeaderBar(), **kwargs)
self.headerbar.props.title_widget = self.title
self.headerbar.add_css_class("flat")
self.props.content.append(self.headerbar)
self.props.content.append(Adw.Bin(child=self.child))
self.bind_property("child", self.headerbar.get_next_sibling(), "child")
self.connect("notify::runid", self.__notify_runid)
if __debug__:
self.add_css_class("devel")
def __notify_runid(self, window: typing.Self,
param: GObject.ParamSpec) -> None:
text = f"runid #{self.runid}" if self.runid > 0 else ""
self.title.props.subtitle = text

View File

@ -0,0 +1,28 @@
/* 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;
}