Compare commits

...

17 Commits

Author SHA1 Message Date
Anna Schumaker 06d10cf883 xfstestsdb 1.6
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 14:11:51 -05:00
Anna Schumaker 3cc3412c6d gtk: Add a TagList to the Sidebar
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:02 -05:00
Anna Schumaker 11941c3bd3 gtk: Add a TagList listmodel
This includes a treemodel property that is intended to be set on a
listview to show our tag tree.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:02 -05:00
Anna Schumaker ce2c36a0dd gtk: Add a TagDeviceList
This will be used to make a tree of tag / device / run objects for
viewing. I sort the tags so that newer ones appear towards the top of
the list.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:02 -05:00
Anna Schumaker 6c8e155a44 gtk: Add the Sidebar to the Window
And connect things so changing the runid in the sidebar changes the
displayed xfstests run.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:02 -05:00
Anna Schumaker 521c96b432 gtk: Create a Sidebar for selecting xfstests runs
I plan to have a few different ways to view and select xfstests runs, so
I use an Gtk.Stack() and Gtk.StackSwitcher() to animate switching
between the different sidebar pages.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:02 -05:00
Anna Schumaker 704eb08091 gtk: Add a CalendarView class for selecting xfstests runs by date
This combines a Calendar and RunidView to select runs on a given day.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:01 -05:00
Anna Schumaker bac99c9c54 gtk: Add a RunidView widget
This will be placed in the sidebar to display our tree of potential runs
to view. I do this as its own widget so we can reuse it for different
sidebar views.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:45:19 -05:00
Anna Schumaker 2352bc3512 gtk: Add a SidebarFactory
This will be used to make Gtk.TreeExpander rows containing a
Gtk.Label to display the XfstestsRuns on a given day.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-06 16:12:35 -05:00
Anna Schumaker 6fcb4eb5e7 gtk: Add a DateDeviceList listmodel
This contains a list of DeviceRunsList objects for every run on a given
day.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-06 14:56:01 -05:00
Anna Schumaker 70274c448c gtk: Add a ListModel for holding the XfstestsRuns attached to a device
I will add to this to create a TreeModel to display individual runs
associated with each device in the sidebar.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-06 14:55:01 -05:00
Anna Schumaker bf668fc936 gtk: Make the runid argument optional
And set things up so we show the sidebar when the user doesn't specify a
runid, but have it hidden when they do.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-10-06 17:10:23 -04:00
Anna Schumaker 64abc86fee gtk: Give Windows a sidebar property
This will be used to display a list of xfstests runs that the user can
select.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-10-06 16:44:17 -04:00
Anna Schumaker b05a9ecc82 gtk: Give Windows a button for toggling the sidebar
Users can press this to show and hide the sidebar overlay.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-10-06 16:43:42 -04:00
Anna Schumaker 5621847451 gtk: Put the Window child into an Adw.OverlaySplitView
The OverlaySplitView contains a sidebar area that can be toggled by the
user that I will be using to create an xfstests history browser.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-10-06 16:27:00 -04:00
Anna Schumaker f951d4c998 gtk: Add undershoots to the XunitView
Undershoot CSS classes were added in libadwaita 1.4, and look nice when
applied.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-10-06 15:59:43 -04:00
Anna Schumaker daae654b8e gtk: Improvements to the failed test output viewer
I change the icon to a "down" arrow, and update the animation direction
to go up-and-down rather than left-and-right. I also change the button
text to say "done", which is a little clearer than "back".

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-10-06 15:57:24 -04:00
19 changed files with 1206 additions and 36 deletions

View File

@ -1,5 +1,6 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our row widgets and factories."""
import datetime
import unittest
import xfstestsdb.gtk.row
import tests.xunit
@ -375,3 +376,67 @@ class TestSummaryFactory(unittest.TestCase):
self.factory.emit("unbind", self.listitem)
self.assertFalse(self.listitem.get_child().has_css_class("accent"))
class TestSidebarFactory(unittest.TestCase):
"""Tests our Gtk.Factory to show Xfstsets runs in the sidebar."""
def setUp(self):
"""Set up common variables."""
self.devlist = xfstestsdb.gtk.tree.DeviceRunsList("/dev/vda1")
self.treeitem = Gtk.TreeListRow()
self.treeitem.get_item = unittest.mock.Mock(return_value=self.devlist)
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.treeitem)
self.factory = xfstestsdb.gtk.row.SidebarFactory()
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, Gtk.SignalListItemFactory)
def test_setup(self):
"""Test that thefactory implements the 'setup' signal."""
self.factory.emit("setup", self.listitem)
expander = self.listitem.get_child()
self.assertIsInstance(expander, Gtk.TreeExpander)
self.assertIsInstance(expander.get_child(), Gtk.Label)
self.assertEqual(expander.get_child().props.yalign, 0.75)
self.assertTrue(expander.get_child().has_css_class("numeric"))
def test_bind_device_list(self):
"""Test binding to a DeviceRunsList object."""
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_child().get_text(),
"/dev/vda1")
self.assertEqual(self.listitem.get_child().get_list_row(),
self.treeitem)
self.assertFalse(self.listitem.props.selectable)
def test_bind_xfstests_run(self):
"""Test binding to an XfstestsRun object."""
now = datetime.datetime.now()
self.devlist.add_run(1, now)
self.treeitem.get_item.return_value = self.devlist[0]
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_child().get_text(),
f"#1: {now.strftime('%T')}")
self.assertEqual(self.listitem.get_child().get_list_row(),
self.treeitem)
self.assertTrue(self.listitem.props.selectable)
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_child().get_text(), "")
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())

246
tests/gtk/test_sidebar.py Normal file
View File

@ -0,0 +1,246 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our sidebar test selector."""
import datetime
import unittest
import xfstestsdb.gtk.sidebar
from gi.repository import GLib
from gi.repository import Gtk
class TestRunidView(unittest.TestCase):
"""Test the RunidView class."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda2"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda3"])
self.today = datetime.date.today()
self.devlist = xfstestsdb.gtk.tree.DateDeviceList(self.xfstestsdb.sql,
self.today)
self.view = xfstestsdb.gtk.sidebar.RunidView(self.devlist)
def test_init(self):
"""Test that the RunidView was set up properly."""
self.assertIsInstance(self.view, Gtk.ScrolledWindow)
self.assertEqual(self.view.props.vexpand, True)
def test_listview(self):
"""Test that the listview child is set up properly."""
self.assertIsInstance(self.view._view, Gtk.ListView)
self.assertIsInstance(self.view._selection, Gtk.SingleSelection)
self.assertIsInstance(self.view._view.props.factory,
xfstestsdb.gtk.row.SidebarFactory)
self.assertTrue(self.view._view.props.single_click_activate)
self.assertTrue(self.view._view.has_css_class("navigation-sidebar"))
self.assertTrue(self.view._view.has_css_class("background"))
self.assertEqual(self.view._view.props.model,
self.view._selection)
self.assertEqual(self.view.props.child, self.view._view)
def test_model_property(self):
"""Test the model property."""
self.assertEqual(self.view.model, self.devlist)
self.assertEqual(self.view._selection.props.model,
self.devlist.treemodel)
self.view.model = None
self.assertIsNone(self.view._selection.props.model)
self.view.model = self.devlist
self.assertEqual(self.view._selection.props.model,
self.devlist.treemodel)
def test_runid_property(self):
"""Test the runid property."""
self.view.model = self.devlist
self.assertEqual(self.view.runid, 0)
self.view._view.emit("activate", 1)
self.assertEqual(self.view.runid, 2)
self.view._view.emit("activate", 2)
self.assertEqual(self.view.runid, 3)
self.view._view.emit("activate", 4)
self.assertEqual(self.view.runid, 1)
self.view._view.emit("activate", 6)
self.assertEqual(self.view.runid, 4)
def test_expand_collapse(self):
"""Test expanding and collapsing child rows."""
self.view.model = self.devlist
self.assertTrue(self.view._selection[0].get_expanded())
self.assertEqual(self.view.runid, 0)
self.view._view.emit("activate", 0)
self.assertFalse(self.view._selection[0].get_expanded())
self.assertEqual(self.view.runid, 0)
self.view._view.emit("activate", 0)
self.assertTrue(self.view._selection[0].get_expanded())
self.assertEqual(self.view.runid, 2)
class TestCalendarView(unittest.TestCase):
"""Test the CalendarView class."""
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(["new", "/dev/vda2"])
self.xfstestsdb.run(["new", "/dev/vda3"])
self.xfstestsdb.run(["new", "/dev/vda4"])
query = "UPDATE xfstests_runs SET timestamp=? WHERE runid=?"
dtime = datetime.datetime.now().replace(year=2023, month=1)
self.xfstestsdb.sql(query, dtime.replace(day=1), 1)
self.xfstestsdb.sql(query, dtime.replace(day=8), 2)
self.xfstestsdb.sql(query, dtime.replace(day=15), 3)
self.sidebar = xfstestsdb.gtk.sidebar.CalendarView(self.xfstestsdb.sql)
def test_init(self):
"""Test that the calendar view was set up properly."""
self.assertIsInstance(self.sidebar, Gtk.Box)
self.assertEqual(self.sidebar.props.spacing, 6)
self.assertEqual(self.sidebar.props.orientation,
Gtk.Orientation.VERTICAL)
self.assertEqual(self.sidebar.sql, self.xfstestsdb.sql)
def test_calendar(self):
"""Test the calendar widget."""
self.assertIsInstance(self.sidebar._calendar, Gtk.Calendar)
self.assertEqual(self.sidebar.get_first_child(),
self.sidebar._calendar)
today = datetime.date.today()
self.assertTrue(self.sidebar._calendar.get_day_is_marked(today.day))
def test_view(self):
"""Test the runid view widget."""
self.assertIsInstance(self.sidebar._view,
xfstestsdb.gtk.sidebar.RunidView)
self.assertIsInstance(self.sidebar._view.model,
xfstestsdb.gtk.tree.DateDeviceList)
self.assertEqual(self.sidebar._calendar.get_next_sibling(),
self.sidebar._view)
self.assertEqual(self.sidebar._view.model.date, datetime.date.today())
def test_marked_days(self):
"""Test marking days in the calendar."""
gl_date = GLib.DateTime.new_local(2023, 1, 10, 0, 0, 0)
self.sidebar._calendar.select_day(gl_date)
for signal in ["next-month", "next-year", "prev-month", "prev-year"]:
with self.subTest(signal=signal):
with unittest.mock.patch.object(self.sidebar._calendar,
"clear_marks") as mock_clear:
with unittest.mock.patch.object(self.sidebar._calendar,
"mark_day") as mock_mark:
self.sidebar._calendar.emit(signal)
mock_clear.assert_called()
mock_mark.assert_has_calls([unittest.mock.call(1),
unittest.mock.call(8),
unittest.mock.call(15)])
def test_select_day(self):
"""Test selecting a day in the calendar."""
gl_now = GLib.DateTime.new_now_local()
self.sidebar._calendar.select_day(gl_now.add_days(-1))
today = datetime.date.today()
self.assertEqual(self.sidebar._view.model.date,
today - datetime.timedelta(days=1))
def test_runid_property(self):
"""Test the runid property."""
self.assertEqual(self.sidebar.runid, 0)
self.sidebar._view.runid = 42
self.assertEqual(self.sidebar.runid, 42)
class TestSidebar(unittest.TestCase):
"""Test the Sidebar class."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.sidebar = xfstestsdb.gtk.sidebar.Sidebar(self.xfstestsdb.sql)
def test_init(self):
"""Test taht the sidebar was set up properly."""
self.assertIsInstance(self.sidebar, Gtk.Box)
self.assertEqual(self.sidebar.props.orientation,
Gtk.Orientation.VERTICAL)
self.assertEqual(self.sidebar.sql, self.xfstestsdb.sql)
def test_stack_switcher(self):
"""Test the Gtk.StackSwitcher."""
self.assertIsInstance(self.sidebar._switcher, Gtk.StackSwitcher)
self.assertEqual(self.sidebar._switcher.props.stack,
self.sidebar._stack)
self.assertTrue(self.sidebar._switcher.has_css_class("large-icons"))
self.assertEqual(self.sidebar._switcher.props.margin_top, 6)
self.assertEqual(self.sidebar._switcher.props.margin_bottom, 6)
self.assertEqual(self.sidebar._switcher.props.margin_start, 80)
self.assertEqual(self.sidebar._switcher.props.margin_end, 80)
self.assertEqual(self.sidebar.get_first_child(),
self.sidebar._switcher)
def test_separator(self):
"""Test the Gtk.Separator between sidebar widgets."""
sep = self.sidebar._switcher.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertEqual(sep.props.orientation, Gtk.Orientation.HORIZONTAL)
self.assertEqual(sep.get_next_sibling(), self.sidebar._stack)
def test_stack(self):
"""Test the Gtk.Stack."""
self.assertIsInstance(self.sidebar._stack, Gtk.Stack)
self.assertEqual(self.sidebar._stack.props.transition_type,
Gtk.StackTransitionType.OVER_LEFT_RIGHT)
self.assertEqual(self.sidebar.get_last_child(), self.sidebar._stack)
def test_calendar_page(self):
"""Test the Sidebar calendar view page."""
self.assertIsInstance(self.sidebar._calendar,
xfstestsdb.gtk.sidebar.CalendarView)
self.assertEqual(self.sidebar._calendar.sql, self.xfstestsdb.sql)
self.assertEqual(self.sidebar._stack.get_child_by_name("calendar"),
self.sidebar._calendar)
page = self.sidebar._stack.get_page(self.sidebar._calendar)
self.assertEqual(page.props.title, "Calendar")
self.assertEqual(page.props.icon_name, "month-symbolic")
def test_tag_page(self):
"""Test the Sidebar tag view page."""
self.assertIsInstance(self.sidebar._tags,
xfstestsdb.gtk.sidebar.RunidView)
self.assertEqual(self.sidebar._stack.get_child_by_name("tags"),
self.sidebar._tags)
page = self.sidebar._stack.get_page(self.sidebar._tags)
self.assertEqual(page.props.title, "Tags")
self.assertEqual(page.props.icon_name, "tag-symbolic")
def test_runid(self):
"""Test the runid property."""
self.assertEqual(self.sidebar.runid, 0)
self.sidebar._calendar.runid = 1
self.assertEqual(self.sidebar.runid, 1)
self.sidebar._tags.runid = 2
self.assertEqual(self.sidebar.runid, 2)

330
tests/gtk/test_tree.py Normal file
View File

@ -0,0 +1,330 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our xfstests run selector tree."""
import datetime
import unittest
import xfstestsdb.gtk.tree
import tests.xunit
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
class TestXfstestsRun(unittest.TestCase):
"""Test case for our XfstestsRun GObject."""
def setUp(self):
"""Set up common variables."""
self.now = datetime.datetime.now()
self.run = xfstestsdb.gtk.tree.XfstestsRun(1, self.now)
def test_run(self):
"""Test the XfsetstsRun object."""
self.assertIsInstance(self.run, GObject.GObject)
self.assertEqual(self.run.runid, 1)
self.assertEqual(self.run.timestamp, self.now)
def test_gt(self):
"""Test the XfstestsRun Greater-Than operator."""
soon = self.now + datetime.timedelta(seconds=5)
run2 = xfstestsdb.gtk.tree.XfstestsRun(2, soon)
run3 = xfstestsdb.gtk.tree.XfstestsRun(3, soon)
self.assertTrue(run2 > self.run)
self.assertFalse(self.run > run2)
self.assertFalse(self.run > self.run)
self.assertTrue(run3 > run2)
self.assertFalse(run2 > run3)
def test_lt(self):
"""Test the XfstestsRun Less-Than operator."""
soon = self.now + datetime.timedelta(seconds=5)
run2 = xfstestsdb.gtk.tree.XfstestsRun(2, soon)
run3 = xfstestsdb.gtk.tree.XfstestsRun(3, soon)
self.assertTrue(self.run < run2)
self.assertFalse(run2 < self.run)
self.assertFalse(self.run < self.run)
self.assertTrue(run2 < run3)
self.assertFalse(run3 < run2)
def test_str(self):
"""Test converting an XfstestsRun to a string."""
self.assertEqual(self.run.ftime, "%T")
self.assertEqual(str(self.run), f"#1: {self.now.strftime('%T')}")
self.run.ftime = "%c"
self.assertEqual(str(self.run), f"#1: {self.now.strftime('%c')}")
class TestDeviceRunModel(unittest.TestCase):
"""Test case for our DeviceRow GObject."""
def setUp(self):
"""Set up common variables."""
self.device = xfstestsdb.gtk.tree.DeviceRunsList(name="/dev/vda1")
def test_init(self):
"""Test creating a DeviceRow instance."""
self.assertIsInstance(self.device, GObject.GObject)
self.assertIsInstance(self.device, Gio.ListModel)
self.assertEqual(self.device.name, "/dev/vda1")
self.assertEqual(str(self.device), "/dev/vda1")
def test_lt(self):
"""Test comparing DeviceRow instances."""
dev_a = xfstestsdb.gtk.tree.DeviceRunsList(name="a")
dev_b = xfstestsdb.gtk.tree.DeviceRunsList(name="b")
self.assertTrue(dev_a < dev_b)
self.assertFalse(dev_b < dev_a)
self.assertFalse(dev_a < dev_a)
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.device.get_item_type(),
xfstestsdb.gtk.tree.XfstestsRun.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.device.get_n_items(), 0)
self.assertEqual(self.device.n_items, 0)
self.device.add_run(1, datetime.datetime.now())
self.assertEqual(self.device.get_n_items(), 1)
self.assertEqual(self.device.n_items, 1)
def test_get_item(self):
"""Test the get_item() function."""
now = datetime.datetime.now()
then = now - datetime.timedelta(seconds=42)
self.device.add_run(1, now)
self.assertIsInstance(self.device[0], xfstestsdb.gtk.tree.XfstestsRun)
self.assertEqual(self.device[0].runid, 1)
self.assertEqual(self.device[0].timestamp, now)
self.device.add_run(2, then, "%c")
self.assertEqual(self.device[0].runid, 2)
self.assertEqual(self.device[0].ftime, "%c")
self.assertEqual(self.device[1].runid, 1)
def test_get_earliest_run(self):
"""Test the get_earliest_run() function."""
self.assertIsNone(self.device.get_earliest_run())
now = datetime.datetime.now()
self.device.add_run(1, now)
self.assertEqual(self.device.get_earliest_run().runid, 1)
then = now - datetime.timedelta(seconds=42)
self.device.add_run(2, then)
self.assertEqual(self.device.get_earliest_run().runid, 2)
class TestDateDeviceList(unittest.TestCase):
"""Test case for our listmodel of test devices on a specific day."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda2"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda3"])
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)])
yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
self.xfstestsdb.sql("""UPDATE xfstests_runs SET timestamp=?
WHERE device=?""", yesterday, "/dev/vda3")
self.today = datetime.date.today()
self.devlist = xfstestsdb.gtk.tree.DateDeviceList(self.xfstestsdb.sql,
self.today)
def test_init(self):
"""Test that the DateDeviceList was set up properly."""
self.assertIsInstance(self.devlist, GObject.GObject)
self.assertIsInstance(self.devlist, Gio.ListModel)
self.assertEqual(self.devlist.date, datetime.date.today())
def test_get_range(self):
"""Test finding the date range for the displayed entries."""
min = datetime.datetime.combine(self.today, datetime.time())
max = min + datetime.timedelta(days=1)
self.assertTupleEqual(self.devlist.get_range(self.today), (min, max))
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.devlist.get_item_type(),
xfstestsdb.gtk.tree.DeviceRunsList.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.devlist.get_n_items(), 2)
self.assertEqual(self.devlist.n_items, 2)
def test_get_item(self):
"""Test the get_item() function."""
self.assertIsInstance(self.devlist.get_item(0),
xfstestsdb.gtk.tree.DeviceRunsList)
self.assertIsInstance(self.devlist.get_item(0).get_item(0).timestamp,
datetime.datetime)
self.assertEqual(self.devlist.get_item(0).name, "/dev/vda1")
self.assertEqual(self.devlist.get_item(1).name, "/dev/vda2")
def test_treemodel(self):
"""Test the treemodel property."""
self.assertIsInstance(self.devlist.treemodel, Gtk.TreeListModel)
self.assertFalse(self.devlist.treemodel.props.passthrough)
self.assertTrue(self.devlist.treemodel.props.autoexpand)
tree = self.devlist.treemodel
self.assertEqual(tree[0].get_item().name, "/dev/vda1")
self.assertEqual(tree[1].get_item().runid, 2)
self.assertEqual(tree[2].get_item().runid, 3)
self.assertEqual(tree[3].get_item().name, "/dev/vda2")
self.assertEqual(tree[4].get_item().runid, 1)
with self.assertRaises(IndexError):
self.assertIsNone(tree[5])
class TestTagDeviceList(unittest.TestCase):
"""Test case for our TagDeviceList GObject."""
def setUp(self):
"""Set up common variables."""
self.tag = xfstestsdb.gtk.tree.TagDeviceList(name="mytag")
def test_init(self):
"""Test creating a TagDeviceList instance."""
self.assertIsInstance(self.tag, GObject.GObject)
self.assertIsInstance(self.tag, Gio.ListModel)
self.assertEqual(self.tag.name, "mytag")
self.assertEqual(str(self.tag), "mytag")
def test_lt(self):
"""Test comparing TagDeviceList instances."""
tag_a = xfstestsdb.gtk.tree.TagDeviceList(name="a")
tag_b = xfstestsdb.gtk.tree.TagDeviceList(name="b")
now = datetime.datetime.now()
then = now - datetime.timedelta(seconds=42)
tag_a.add_run(1, "/dev/vda1", now)
tag_b.add_run(2, "/dev/vda2", then)
self.assertTrue(tag_a < tag_b)
self.assertFalse(tag_b < tag_a)
self.assertFalse(tag_a < tag_a)
tag_a.add_run(2, "/dev/vda2", then)
self.assertTrue(tag_b < tag_a)
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.tag.get_item_type(),
xfstestsdb.gtk.tree.DeviceRunsList.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.tag.get_n_items(), 0)
self.assertEqual(self.tag.n_items, 0)
self.tag.add_run(1, "/dev/vda1", datetime.datetime.now())
self.assertEqual(self.tag.get_n_items(), 1)
self.assertEqual(self.tag.n_items, 1)
def test_get_item(self):
"""Test the get_item() function."""
now = datetime.datetime.now()
then = now - datetime.timedelta(seconds=42)
self.tag.add_run(1, "/dev/vda2", now)
self.assertIsInstance(self.tag[0], xfstestsdb.gtk.tree.DeviceRunsList)
self.assertEqual(self.tag[0].name, "/dev/vda2")
self.assertEqual(self.tag[0][0].runid, 1)
self.assertEqual(self.tag[0][0].timestamp, now)
self.assertEqual(self.tag[0][0].ftime, "%c")
self.tag.add_run(2, "/dev/vda1", then)
self.assertEqual(self.tag[0].name, "/dev/vda1")
self.assertEqual(self.tag[1].name, "/dev/vda2")
def test_get_earliest_run(self):
"""Test the get_earliest_run() function."""
self.assertIsNone(self.tag.get_earliest_run())
now = datetime.datetime.now()
self.tag.add_run(1, "/dev/vda1", now)
self.assertEqual(self.tag.get_earliest_run().runid, 1)
then = now - datetime.timedelta(seconds=42)
self.tag.add_run(2, "/dev/vda2", then)
self.assertEqual(self.tag.get_earliest_run().runid, 2)
class TestTagList(unittest.TestCase):
"""Test case for our TagList ListModel."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
with unittest.mock.patch("sys.stdout"):
self.xfstestsdb.run(["new", "/dev/vda2"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda3"])
self.xfstestsdb.run(["tag", "1", "mytag2"])
self.xfstestsdb.run(["tag", "2", "mytag2"])
self.xfstestsdb.run(["tag", "4", "mytag1"])
self.taglist = xfstestsdb.gtk.tree.TagList(self.xfstestsdb.sql)
def test_init(self):
"""Test that the TagList was set up properly."""
self.assertIsInstance(self.taglist, GObject.GObject)
self.assertIsInstance(self.taglist, Gio.ListModel)
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.taglist.get_item_type(),
xfstestsdb.gtk.tree.TagDeviceList.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.taglist.get_n_items(), 2)
self.assertEqual(self.taglist.n_items, 2)
def test_get_item(self):
"""Test the get_item() function."""
self.assertIsInstance(self.taglist.get_item(0),
xfstestsdb.gtk.tree.TagDeviceList)
self.assertIsInstance(self.taglist[0][0][0].timestamp,
datetime.datetime)
self.assertEqual(self.taglist.get_item(0).name, "mytag1")
self.assertEqual(self.taglist.get_item(1).name, "mytag2")
def test_treemodel(self):
"""Test the treemodel property."""
self.assertIsInstance(self.taglist.treemodel, Gtk.TreeListModel)
self.assertFalse(self.taglist.treemodel.props.passthrough)
self.assertFalse(self.taglist.treemodel.props.autoexpand)
tree = self.taglist.treemodel
self.assertEqual(tree[0].get_item().name, "mytag1")
tree[0].set_expanded(True)
self.assertEqual(tree[1].get_item().name, "/dev/vda3")
tree[1].set_expanded(True)
self.assertEqual(tree[2].get_item().runid, 4)
self.assertEqual(tree[3].get_item().name, "mytag2")
tree[3].set_expanded(True)
self.assertEqual(tree[4].get_item().name, "/dev/vda1")
self.assertEqual(tree[5].get_item().name, "/dev/vda2")

View File

@ -26,6 +26,8 @@ class TestXunitView(unittest.TestCase):
def test_init(self):
"""Test that we created the XunitView correctly."""
self.assertIsInstance(self.view, Gtk.ScrolledWindow)
self.assertTrue(self.view.has_css_class("undershoot-top"))
self.assertTrue(self.view.has_css_class("undershoot-bottom"))
self.assertTrue(self.view.has_css_class("card"))
def test_columnview(self):
@ -383,8 +385,8 @@ class MessagesView(unittest.TestCase):
self.view._back)
self.assertEqual(self.view._back.props.child.props.icon_name,
"go-previous-symbolic")
self.assertEqual(self.view._back.props.child.props.label, "back")
"down-large-symbolic")
self.assertEqual(self.view._back.props.child.props.label, "done")
self.assertTrue(self.view._back.has_css_class("suggested-action"))
self.assertTrue(self.view._back.has_css_class("pill"))
@ -500,7 +502,7 @@ class TestXfstestsView(unittest.TestCase):
self.assertIsInstance(sep, Gtk.Separator)
self.assertIsInstance(self.view._stack, Gtk.Stack)
self.assertEqual(self.view._stack.props.transition_type,
Gtk.StackTransitionType.OVER_LEFT_RIGHT)
Gtk.StackTransitionType.OVER_UP_DOWN)
self.assertEqual(sep.get_next_sibling(), self.view._stack)
def test_testcase_view(self):

View File

@ -38,21 +38,61 @@ class TestWindow(unittest.TestCase):
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)
def test_splitview(self):
"""Check that the Window's splitview is set up correctly."""
self.assertIsInstance(self.window._splitview, Adw.OverlaySplitView)
self.assertEqual(self.window.headerbar.get_next_sibling(),
self.window._splitview)
self.assertTrue(self.window._splitview.props.collapsed)
def test_show_sidebar(self):
"""Test the window show sidebar button."""
self.assertIsInstance(self.window._show_sidebar, Gtk.ToggleButton)
self.assertEqual(self.window._show_sidebar.get_ancestor(Adw.HeaderBar),
self.window.headerbar)
self.assertEqual(self.window._show_sidebar.props.icon_name,
"sidebar-show")
self.assertFalse(self.window.show_sidebar)
self.assertFalse(self.window._show_sidebar.props.active)
self.assertFalse(self.window._splitview.props.show_sidebar)
self.window._show_sidebar.props.active = True
self.assertTrue(self.window._splitview.props.show_sidebar)
self.assertTrue(self.window.show_sidebar)
self.window.show_sidebar = False
self.assertFalse(self.window._show_sidebar.props.active)
self.assertFalse(self.window._splitview.props.show_sidebar)
window2 = xfstestsdb.gtk.window.Window(show_sidebar=True)
self.assertTrue(window2.show_sidebar)
self.assertTrue(window2._show_sidebar.props.active)
self.assertTrue(window2._splitview.props.show_sidebar)
def test_content(self):
"""Test the window content property."""
self.assertIsNone(self.window.child)
self.window.child = Gtk.Label()
self.assertEqual(self.window.headerbar.get_next_sibling().props.child,
self.assertEqual(self.window._splitview.props.content,
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)
self.assertEqual(window2._splitview.props.content, label)
def test_sidebar(self):
"""Test the window sidebar property."""
self.assertIsNone(self.window.sidebar)
self.window.sidebar = Gtk.Label()
self.assertEqual(self.window._splitview.props.sidebar,
self.window.sidebar)
label = Gtk.Label()
window2 = xfstestsdb.gtk.window.Window(sidebar=label)
self.assertEqual(window2.sidebar, label)
self.assertEqual(window2._splitview.props.sidebar, label)
def test_runid(self):
"""Test the window runid property."""

View File

@ -44,11 +44,21 @@ class TestApplication(unittest.TestCase):
mock_cmd.get_arguments.assert_called()
mock_activate.assert_called()
self.assertEqual(self.application.runid, 0)
self.assertFalse(self.application.show_sidebar)
self.assertIsNone(self.application.environment)
self.assertIsNone(self.application.properties)
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 = ["show-sidebar"]
self.application.do_command_line(mock_cmd)
self.assertTrue(self.application.show_sidebar)
self.application.show_sidebar = False
mock_command_line.reset_mock()
mock_activate.reset_mock()
mock_cmd.reset_mock()
@ -68,6 +78,7 @@ class TestApplication(unittest.TestCase):
self.assertEqual(self.application.environment,
self.application.properties.environment)
self.assertEqual(self.application.model.runid, 42)
self.assertFalse(self.application.show_sidebar)
@unittest.mock.patch("xfstestsdb.gtk.gsetup.add_style")
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
@ -77,22 +88,33 @@ class TestApplication(unittest.TestCase):
mock_add_style: unittest.mock.Mock):
"""Check that startup sets up our application instance correctly."""
self.assertIsNone(self.application.win)
self.assertIsNone(self.application.sidebar)
self.assertIsNone(self.application.view)
self.application.emit("startup")
self.assertIsInstance(self.application.sidebar,
xfstestsdb.gtk.sidebar.Sidebar)
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)
self.assertEqual(self.application.win.sidebar,
self.application.sidebar)
self.assertEqual(self.application.sidebar.sql,
self.application.sql)
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.application.sidebar.runid = 42
self.assertEqual(self.application.runid, 42)
self.assertEqual(self.application.win.runid, 42)
self.application.show_sidebar = True
self.assertTrue(self.application.win.show_sidebar)
properties = xfstestsdb.gtk.model.PropertyList(self.xfstestsdb.sql, 42)
self.application.properties = properties
self.assertEqual(self.application.view.properties, properties)
@ -150,11 +172,9 @@ class TestGtk(unittest.TestCase):
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")
self.xfstestsdb.run(["gtk"])
mock_app.assert_called_with(self.xfstestsdb.sql)
mock_app.return_value.run.assert_called_with(["show-sidebar"])
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
@unittest.mock.patch("xfstestsdb.gtk.Application")

View File

@ -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, 5)
self.assertEqual(xfstestsdb.MINOR, 6)
self.xfstestsdb.run(["--version"])
self.assertEqual(mock_stdout.getvalue(), "xfstestsdb v1.5-debug\n")
self.assertEqual(mock_stdout.getvalue(), "xfstestsdb v1.6-debug\n")

View File

@ -15,7 +15,7 @@ from . import untag
from . import xunit
MAJOR = 1
MINOR = 5
MINOR = 6
class Command:

View File

@ -8,6 +8,7 @@ from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Adw
from . import model
from . import sidebar
from . import view
from . import window
from .. import commands
@ -18,11 +19,13 @@ class Application(Adw.Application):
"""Our Adw.Application for displaying xfstests results."""
runid = GObject.Property(type=int)
show_sidebar = GObject.Property(type=bool, default=False)
environment = GObject.Property(type=Gio.ListStore)
properties = GObject.Property(type=model.PropertyList)
summary = GObject.Property(type=model.SummaryList)
model = GObject.Property(type=model.TestCaseList)
win = GObject.Property(type=window.Window)
sidebar = GObject.Property(type=sidebar.Sidebar)
view = GObject.Property(type=view.XfstestsView)
sql = GObject.Property(type=GObject.TYPE_PYOBJECT)
@ -32,6 +35,14 @@ class Application(Adw.Application):
resource_base_path=gsetup.RESOURCE_PATH,
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
sql=sql)
self.connect("notify::runid", self.__notify_runid)
def __notify_runid(self, app: Adw.Application,
param: GObject.ParamSpec) -> None:
self.properties = model.PropertyList(self.sql, self.runid)
self.environment = self.properties.environment
self.model = model.TestCaseList(self.sql, self.runid)
self.summary = model.SummaryList(self.sql, self.runid)
def do_command_line(self, cmd_line: Gio.ApplicationCommandLine) -> int:
"""Handle the Adw.Application::command-line signal."""
@ -42,10 +53,8 @@ class Application(Adw.Application):
match split[0]:
case "runid":
self.runid = int(split[1])
self.properties = model.PropertyList(self.sql, self.runid)
self.environment = self.properties.environment
self.model = model.TestCaseList(self.sql, self.runid)
self.summary = model.SummaryList(self.sql, self.runid)
case "show-sidebar":
self.show_sidebar = True
self.activate()
return 0
@ -55,11 +64,14 @@ class Application(Adw.Application):
Adw.Application.do_startup(self)
gsetup.add_style()
self.sidebar = sidebar.Sidebar(self.sql)
self.view = view.XfstestsView()
self.win = window.Window(child=self.view)
self.win = window.Window(child=self.view, sidebar=self.sidebar)
self.win.headerbar.pack_end(self.view.filterbuttons)
self.sidebar.bind_property("runid", self, "runid")
self.bind_property("runid", self.win, "runid")
self.bind_property("show-sidebar", self.win, "show-sidebar")
self.bind_property("environment", self.view, "environment")
self.bind_property("properties", self.view, "properties")
self.bind_property("model", self.view, "model")
@ -83,19 +95,22 @@ class Command(commands.Command):
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,
self.parser.add_argument("runid", metavar="runid", nargs='?', 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]}")
if args.runid is not None:
if self.sql("SELECT 1 FROM xfstests_runs WHERE runid=?",
args.runid).fetchone():
app_args.append(f"runid={args.runid}")
else:
print(f"error: run #{args.runid} does not exist",
file=sys.stderr)
sys.exit(errno.ENOENT)
else:
print(f"error: run #{args.runid[0]} does not exist",
file=sys.stderr)
sys.exit(errno.ENOENT)
app_args.append("show-sidebar")
Application(self.sql).run(app_args)

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 -0.00390625 3 v 1 c 0 0.292969 0.12890625 0.558594 0.32812525 0.738281 l 7.671875 7.675781 l 7.707031 -7.707031 c 0.183594 -0.179687 0.292969 -0.429687 0.292969 -0.707031 v -1 h -1 c -0.273438 0 -0.523438 0.113281 -0.707032 0.292969 c -0.007812 0.011719 -0.019531 0.019531 -0.03125 0.03125 l -6.261718 6.261719 l -6.257813 -6.261719 c -0.183593 -0.199219 -0.445312 -0.324219 -0.742187 -0.324219 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 549 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 3.042969 1 c -1.128907 0 -2.042969 0.914062 -2.042969 2.042969 v 9.875 c 0 1.132812 0.914062 2.042969 2.042969 2.042969 h 9.914062 c 1.128907 0 2.042969 -0.910157 2.042969 -2.042969 v -9.875 c 0 -1.128907 -0.914062 -2.042969 -2.042969 -2.042969 z m -0.042969 4.960938 h 10 v 7 h -10 z m 1 1.039062 v 2 h 2 v -2 z m 3 0 v 2 h 2 v -2 z m 3 0 v 2 h 2 v -2 z m -6 3 v 2 h 2 v -2 z m 3 0 v 2 h 2 v -2 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 550 B

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7384"
height="16"
width="16"
version="1.1">
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs9" />
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path855"
d="m 1,1 v 6.0008052 l 7.9783226,7.9783228 c 0.5857862,0.585786 1.5355344,0.585786 2.1213204,0 l 0.0097,-0.0097 3.87942,-3.87942 0.0097,-0.0097 c 0.585786,-0.585786 0.585786,-1.535534 0,-2.1213204 L 7.0394752,1 Z m 2.4775289,2.4403054 c 0.271447,-0.271447 0.6464468,-0.4391799 1.0606602,-0.4391797 0.8284268,3e-7 1.4998395,0.671413 1.4998398,1.4998398 4e-7,0.8284269 -0.6714131,1.4998403 -1.4998399,1.49984 -0.8284275,4e-7 -1.4998402,-0.6714123 -1.4998398,-1.4998399 -2e-7,-0.4142134 0.1677334,-0.7892139 0.4391797,-1.0606602 z"
style="display:inline;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1">
<title
id="title864">tag-symbolic</title>
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -4,5 +4,8 @@
<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>
<file preprocess="xml-stripblanks">down-large-symbolic.svg</file>
<file preprocess="xml-stripblanks">month-symbolic.svg</file>
<file preprocess="xml-stripblanks">tag-symbolic.svg</file>
</gresource>
</gresources>

View File

@ -4,6 +4,7 @@ import typing
from gi.repository import GObject
from gi.repository import Gtk
from . import model
from . import tree
STYLES = {"passed": "success", "failed": "error",
@ -181,3 +182,40 @@ class SummaryFactory(XunitFactory):
def do_unbind(self, row: model.TestCase, child: Gtk.Inscription) -> None:
"""Unbind a ListItem from the child widget."""
child.remove_css_class(STYLES[row.name])
class SidebarFactory(Gtk.SignalListItemFactory):
"""Factory for making sidebar widgets."""
def __init__(self, *args, **kwargs):
"""Initialize our InscriptionFactory."""
super().__init__(*args, **kwargs)
self.connect("setup", self.__setup)
self.connect("bind", self.__bind)
self.connect("unbind", self.__unbind)
self.connect("teardown", self.__teardown)
def __setup(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Set up a ListItem child widget."""
child = Gtk.Label(yalign=0.75)
child.add_css_class("numeric")
expander = Gtk.TreeExpander(child=child)
listitem.set_child(expander)
def __bind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Bind a ListItem to the child widget."""
treeitem = listitem.get_item()
expander = listitem.get_child()
expander.set_list_row(treeitem)
row = treeitem.get_item()
child = expander.get_child()
child.set_text(str(row))
listitem.props.selectable = isinstance(row, tree.XfstestsRun)
def __unbind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Unbind a ListItem from the child widget."""
listitem.get_child().get_child().set_text("")
def __teardown(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
listitem.set_child(None)

131
xfstestsdb/gtk/sidebar.py Normal file
View File

@ -0,0 +1,131 @@
# Copyright 2023 (c) Anna Schumaker.
"""Our sidebar for selecting a specific xfstests run to view."""
import datetime
import typing
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .. import sqlite
from . import row
from . import tree
class RunidView(Gtk.ScrolledWindow):
"""Our RunidView class."""
runid = GObject.Property(type=int)
model = GObject.Property(type=Gio.ListModel)
def __init__(self, model: tree.DateDeviceList) -> None:
"""Initialize a RunidView instance."""
super().__init__(model=model, vexpand=True)
self._selection = Gtk.SingleSelection(model=model.treemodel)
self._view = Gtk.ListView(model=self._selection,
factory=row.SidebarFactory(),
single_click_activate=True)
self._view.connect("activate", self.__activate)
self.connect("notify::model", self.__notify_model)
self._view.add_css_class("navigation-sidebar")
self._view.add_css_class("background")
self.set_child(self._view)
def __activate(self, view: Gtk.ListView, position: int) -> None:
treeitem = self._selection[position]
item = treeitem.get_item()
if isinstance(item, tree.XfstestsRun):
self.runid = item.runid
elif treeitem.props.expanded:
treeitem.props.expanded = False
else:
treeitem.props.expanded = True
self.runid = item.get_earliest_run().runid
def __notify_model(self, sidebar: typing.Self,
param: GObject.ParamSpec) -> None:
model = None if self.model is None else self.model.treemodel
self._selection.props.model = model
class CalendarView(Gtk.Box):
"""Our calendar view for seleting an xfstests run by date."""
runid = GObject.Property(type=int)
sql = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self, sql: sqlite.Connection) -> None:
"""Initialize a CalendarView instance."""
super().__init__(sql=sql, spacing=6,
orientation=Gtk.Orientation.VERTICAL)
today = datetime.date.today()
self._calendar = Gtk.Calendar()
self._view = RunidView(model=tree.DateDeviceList(sql, today))
self._view.bind_property("runid", self, "runid")
self._calendar.connect("day-selected", self.__day_selected)
self._calendar.connect("next-month", self.__date_changed)
self._calendar.connect("next-year", self.__date_changed)
self._calendar.connect("prev-month", self.__date_changed)
self._calendar.connect("prev-year", self.__date_changed)
self.__mark_days(today)
self.append(self._calendar)
self.append(self._view)
def __day_selected(self, calendar: Gtk.Calendar) -> None:
self.__select_day(datetime.date(*calendar.get_date().get_ymd()))
def __date_changed(self, calendar: Gtk.Calendar) -> None:
date = datetime.date(*calendar.get_date().get_ymd())
self.__mark_days(date)
self.__select_day(date)
def __select_day(self, date: datetime.date) -> None:
self._view.model = tree.DateDeviceList(self.sql, date)
def __mark_days(self, date: datetime.date) -> None:
min = datetime.datetime.combine(date.replace(day=1), datetime.time())
max = (min + datetime.timedelta(days=40)).replace(day=1)
self._calendar.clear_marks()
for stamp in self.sql("""SELECT DISTINCT timestamp FROM tagged_runs
WHERE timestamp >= ? AND timestamp < ?""",
min, max).fetchall():
ts = datetime.datetime.fromisoformat(stamp['timestamp'])
self._calendar.mark_day(ts.day)
class Sidebar(Gtk.Box):
"""Our sidebar for seleting an xfstests run."""
runid = GObject.Property(type=int)
sql = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self, sql: sqlite.Connection) -> None:
"""Initialize a CalendarView instance."""
animation = Gtk.StackTransitionType.OVER_LEFT_RIGHT
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL)
self._stack = Gtk.Stack(transition_type=animation)
self._switcher = Gtk.StackSwitcher(stack=self._stack,
margin_top=6, margin_bottom=6,
margin_start=80, margin_end=80)
self._calendar = CalendarView(sql)
self._tags = RunidView(model=tree.TagList(sql))
self._calendar.bind_property("runid", self, "runid")
self._tags.bind_property("runid", self, "runid")
self.__add_page(self._calendar, "Calendar", "month-symbolic")
self.__add_page(self._tags, "Tags", "tag-symbolic")
self._switcher.add_css_class("large-icons")
self.append(self._switcher)
self.append(Gtk.Separator())
self.append(self._stack)
def __add_page(self, page: Gtk.Widget, title: str, icon: str) -> None:
pg = self._stack.add_titled(page, title.lower(), title)
pg.set_icon_name(icon)

224
xfstestsdb/gtk/tree.py Normal file
View File

@ -0,0 +1,224 @@
# Copyright 2023 (c) Anna Schumaker.
"""Our tree model for selecting an xfstests run to view."""
import bisect
import datetime
import typing
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .. import sqlite
class XfstestsRun(GObject.GObject):
"""A single XfstessRun."""
timestamp = GObject.Property(type=GObject.TYPE_PYOBJECT)
runid = GObject.Property(type=int)
ftime = GObject.Property(type=str)
def __init__(self, runid: int, timestamp: datetime.datetime,
ftime: str = "%T") -> None:
"""Initialize an XfstestsRun instance."""
super().__init__(timestamp=timestamp, runid=runid, ftime=ftime)
def __gt__(self, rhs: typing.Self) -> bool:
"""Compare the timestamps an runids of two XfstestRun objects."""
return (self.timestamp, self.runid) > (rhs.timestamp, rhs.runid)
def __lt__(self, rhs: typing.Self) -> bool:
"""Compare the timestamps and runids of two XfstestsRun objects."""
return (self.timestamp, self.runid) < (rhs.timestamp, rhs.runid)
def __str__(self) -> str:
"""Get a string representation of this run."""
return f"#{self.runid}: {self.timestamp.strftime(self.ftime)}"
class DeviceRunsList(GObject.GObject, Gio.ListModel):
"""Contains a single test device with multiple runs."""
name = GObject.Property(type=str)
n_items = GObject.Property(type=int)
def __init__(self, name: str) -> None:
"""Initialize our DeviceRunsList."""
super().__init__(name=name)
self.__runs = []
def __lt__(self, rhs: typing.Self) -> bool:
"""Compare two DeviceRunsList objects."""
return self.name < rhs.name
def __str__(self) -> str:
"""Get the name of this device."""
return self.name
def do_get_item_type(self) -> GObject.GType:
"""Get the type of objects in the list."""
return XfstestsRun.__gtype__
def do_get_n_items(self) -> int:
"""Get the number of runs added to this device."""
return self.n_items
def do_get_item(self, n: int) -> XfstestsRun:
"""Get the run at the nth position."""
return self.__runs[n]
def add_run(self, runid: int, timestamp: datetime.datetime,
ftime: str = "%T") -> None:
"""Add a run to the Device's list."""
bisect.insort(self.__runs, XfstestsRun(runid, timestamp, ftime))
self.n_items = len(self.__runs)
def get_earliest_run(self) -> XfstestsRun | None:
"""Get the earliest XfstestsRun that we know about."""
return self.__runs[0] if self.n_items > 0 else None
class DateDeviceList(GObject.GObject, Gio.ListModel):
"""A list containing test devices used on a specific day."""
date = GObject.Property(type=GObject.TYPE_PYOBJECT)
n_items = GObject.Property(type=int)
treemodel = GObject.Property(type=Gtk.TreeListModel)
def __init__(self, sql: sqlite.Connection, date: datetime.date) -> None:
"""Initialize our DateDeviceList."""
super().__init__(date=date)
devices = {}
for row in sql("""SELECT DISTINCT runid, device, timestamp
FROM tagged_runs
WHERE timestamp >= ? AND timestamp < ?""",
*self.get_range(date)).fetchall():
if (dev := devices.get(row['device'])) is None:
devices[row['device']] = dev = DeviceRunsList(row['device'])
ts = datetime.datetime.fromisoformat(row['timestamp'])
dev.add_run(row['runid'], ts)
self.__items = sorted(devices.values())
self.n_items = len(self.__items)
self.treemodel = Gtk.TreeListModel.new(root=self, passthrough=False,
autoexpand=True,
create_func=self.__create_func)
def __create_func(self, item: GObject.GObject) -> Gio.ListModel:
if isinstance(item, DeviceRunsList):
return item
return None
def do_get_item_type(self) -> GObject.GType:
"""Get the type of objects in the list."""
return DeviceRunsList.__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) -> DeviceRunsList:
"""Get a specific item in the list."""
return self.__items[n]
def get_range(self, date: datetime.date) -> tuple[datetime.datetime,
datetime.datetime]:
"""Get the minimum and maximum timestamps for the date."""
min = datetime.datetime.combine(date, datetime.time())
max = min + datetime.timedelta(days=1)
return (min, max)
class TagDeviceList(GObject.GObject, Gio.ListModel):
"""Contains a single tag with multiple devices and runs."""
name = GObject.Property(type=str)
n_items = GObject.Property(type=int)
def __init__(self, name: str) -> None:
"""Initialize our TagDeviceList."""
super().__init__(name=name)
self.__devices = []
def __lt__(self, rhs: typing.Self) -> bool:
"""Compare two TagDeviceList objects."""
lhs_run = self.get_earliest_run()
rhs_run = rhs.get_earliest_run()
if lhs_run.runid == rhs_run.runid:
return self.name > rhs.name
return lhs_run > rhs_run
def __str__(self) -> str:
"""Get the name of this tag."""
return self.name
def do_get_item_type(self) -> GObject.GType:
"""Get the type of objects in the list."""
return DeviceRunsList.__gtype__
def do_get_n_items(self) -> int:
"""Get the number of devices added to this tag."""
return self.n_items
def do_get_item(self, n: int) -> DeviceRunsList:
"""Get the device at the nth position."""
return self.__devices[n]
def add_run(self, runid: int, device: str,
timestamp: datetime.datetime) -> None:
"""Add a run to the Tag's list."""
pos = bisect.bisect_left(self.__devices, device, key=str)
if pos < len(self.__devices) and self.__devices[pos].name == device:
dev = self.__devices[pos]
else:
dev = DeviceRunsList(device)
self.__devices.insert(pos, dev)
dev.add_run(runid, timestamp, "%c")
self.n_items = len(self.__devices)
def get_earliest_run(self) -> XfstestsRun | None:
"""Get the earliest run added to the tag."""
runs = [d.get_earliest_run() for d in self.__devices]
return min(runs, default=None)
class TagList(GObject.GObject, Gio.ListModel):
"""A list containing all tagged xfstests runs."""
n_items = GObject.Property(type=int)
treemodel = GObject.Property(type=Gtk.TreeListModel)
def __init__(self, sql: sqlite.Connection) -> None:
"""Initialize our TagList."""
super().__init__()
tags = {}
for row in sql("""SELECT DISTINCT runid, device, timestamp, tag
FROM tagged_runs WHERE tag IS NOT NULL""").fetchall():
if (tag := tags.get(row['tag'])) is None:
tags[row['tag']] = tag = TagDeviceList(row['tag'])
ts = datetime.datetime.fromisoformat(row['timestamp'])
tag.add_run(row['runid'], row['device'], ts)
self.__items = sorted(tags.values())
self.n_items = len(self.__items)
self.treemodel = Gtk.TreeListModel.new(root=self, passthrough=False,
autoexpand=False,
create_func=self.__create_func)
def __create_func(self, item: GObject.GObject) -> Gio.ListModel:
if isinstance(item, TagDeviceList | DeviceRunsList):
return item
return None
def do_get_item_type(self) -> GObject.GType:
"""Get the type of objects in the list."""
return TagDeviceList.__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) -> TagDeviceList:
"""Get a specific item in the list."""
return self.__items[n]

View File

@ -33,6 +33,8 @@ class XunitView(Gtk.ScrolledWindow):
hexpand=True),
**kwargs)
self.props.child.get_model().set_model(self.filtermodel)
self.add_css_class("undershoot-top")
self.add_css_class("undershoot-bottom")
self.add_css_class("card")
def do_make_factory(self, xunit: str) -> row.XunitFactory:
@ -242,11 +244,11 @@ class MessagesView(Gtk.Box):
def __init__(self):
"""Initialize a MessagesView."""
icon = "go-previous-symbolic"
icon = "down-large-symbolic"
super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=24,
margin_start=24, margin_end=24, margin_bottom=24)
self._back = Gtk.Button(child=Adw.ButtonContent(icon_name=icon,
label="back"))
label="done"))
self._title = Adw.WindowTitle()
self._stdout = MessageView("stdout")
self._stderr = MessageView("stderr")
@ -299,7 +301,7 @@ class XfstestsView(Gtk.Box):
def __init__(self):
"""Initialize an XfstestsView."""
animation = Gtk.StackTransitionType.OVER_LEFT_RIGHT
animation = Gtk.StackTransitionType.OVER_UP_DOWN
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self._environview = EnvironmentView()
self._propertyview = PropertyView()

View File

@ -10,9 +10,11 @@ class Window(Adw.Window):
"""Our customized Window displayed to the user."""
child = GObject.Property(type=Gtk.Widget)
sidebar = GObject.Property(type=Gtk.Widget)
headerbar = GObject.Property(type=Adw.HeaderBar)
title = GObject.Property(type=Adw.WindowTitle)
runid = GObject.Property(type=int)
show_sidebar = GObject.Property(type=bool, default=False)
def __init__(self, **kwargs):
"""Set up our Window."""
@ -20,13 +22,26 @@ class Window(Adw.Window):
content=Gtk.Box(orientation=Gtk.Orientation.VERTICAL),
title=Adw.WindowTitle(title="xfstestsdb gtk"),
headerbar=Adw.HeaderBar(), **kwargs)
self._splitview = Adw.OverlaySplitView(content=self.child,
sidebar=self.sidebar,
collapsed=True,
show_sidebar=self.show_sidebar)
self._show_sidebar = Gtk.ToggleButton(icon_name="sidebar-show",
active=self.show_sidebar)
self.headerbar.pack_start(self._show_sidebar)
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.props.content.append(self._splitview)
self._show_sidebar.bind_property("active", self, "show-sidebar",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("show-sidebar", self._splitview, "show-sidebar")
self.bind_property("sidebar", self._splitview, "sidebar")
self.bind_property("child", self._splitview, "content")
self.bind_property("child", self.headerbar.get_next_sibling(), "child")
self.connect("notify::runid", self.__notify_runid)
if __debug__: