Compare commits

...

73 Commits

Author SHA1 Message Date
4600258721 new: Add a --terse option
This causes `xfstestsdb new` to only print out the new runid, making it
easier for scripting.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2024-04-25 17:05:51 -04:00
79fae64f37 gtk: Run the application in the done() function handler
I still build the arguments to the application in the main do_command()
handler, but delay running it so the database won't be locked allowing
other commands to still work.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 15:50:29 -05:00
2e85870c87 list: Don't show empty xunits or tags
Up until now, we've been appending an extra column to the listed output
but allowing it to have empty entries. I'm changing this to filter out
NULL entries, since it turns out that's what I want when I use the
--tags option. I also updated the --xunits option to be consistent.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 15:14:45 -05:00
7e6f944cde gc: Vacuum the database after garbage collecting
This patch adds a "vacuum()" function to our sqlite Connection that is
called if we detect that rows have been deleted during `xfstestsdb gc`

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 15:02:35 -05:00
dc6f5f54c3 command: Add a function to be called after the main function
The main function is run inside a sqlite transaction context, but there
are a few rare cases where we want to call a function after the main
function and outside of a transaction (such as using the sqlite 'VACUUM'
operation). This gives us a way to do that.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 14:52:42 -05:00
06d10cf883 xfstestsdb 1.6
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 14:11:51 -05:00
3cc3412c6d gtk: Add a TagList to the Sidebar
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-11-07 10:46:02 -05:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
528444cab6 xfstestsdb 1.5
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-31 15:52:28 -04:00
952889687f gtk: Add a MessagesView to the XfstestsView
This is placed in a Gtk.Stack with a nice transition to show the failed
tests log.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-31 15:35:28 -04:00
ea2913429c gtk: Create a MessagesView
The MessagesView combines two MessageViews into a split-pane card. This
lets us display stdout and stderr side-by-side to the user so they can
see what is going on. I also add a 'back' button that the user can click
to signal that they are done reviewing the output.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-31 15:34:18 -04:00
5fb9bd6221 gtk: Create a MessageView
The message view will be used to display either stdout or stderr
messages to the user. It has built-in 'diff' detection, and adds nice
colors to the diff output if we are asked to display one.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-31 15:31:09 -04:00
54be8f5ce3 gtk: Give the TestCaseView a show-messages signal
This simply passes on the signal from the underlying factory to be used
in a higher up layer.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-31 13:16:17 -04:00
5a25935fed gtk: Emit a signal when a specific test result is clicked
I only do this for failing tests that have text set in their stdout or
stderr properties. I also update the CSS for failed tests to give an
indication when users hover their mouse over them.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-31 13:16:16 -04:00
40b1d1789d gtk: Add the EnvironmentView to the XfstestsView
And set the environment property from the application after creating a
property list.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 17:17:34 -04:00
f44e064c89 gtk: Create an EnvironmentView
The EnvironmentView is a ColumnView configured to display environment
properties to the user. It doesn't use the firstcol property at all, so
some extra handling of the model property was needed.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 17:16:39 -04:00
02452069da gtk: Create an EnvironmentFactory
This is different from our other xunit factories. Instead of displaying
values for each xunit, it looks into what we determine to be environment
properties and displays those instead.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 17:15:17 -04:00
7c7e279648 model: Give the PropertyList "environment" detection
I define the envorionment as properties that are the same across each
added xunit, as long as there are more than 1 added xunits. I add an
'environment' property to make it easy to get the current set of
environment properties.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 16:13:07 -04:00
c861c49564 gtk: Add an XunitView base class
The XunitView implements most of the work needed by our various views to
show xfstests results.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 15:34:27 -04:00
a5d1ae4607 gtk: Create an XunitFactory base class
And convert most of our Factory implementations to inherit from it. This
lets me set up the xunit property in one single place, and soon I'll be
using this factory for a XunitView base class.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 15:27:50 -04:00
2deb484754 xfstestsdb 1.4
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-29 10:43:41 -04:00
af1ab81ea2 gtk: Increase the default Window size
Now that we've added properties, we need a slightly larger window to
display everything clearly.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-16 10:34:21 -04:00
3dc8179624 gtk: Add a PropertyView to the XfstestsView
And have the application create a PropertyList that is passed to the
View.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-16 10:34:21 -04:00
c145a67ae6 gtk: Add a PropertyView
The PropertyView displays the properties of each Xunit added to the
displayed Xfstests run.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-16 10:34:21 -04:00
7ae246677b gtk: Add a PropertyFactory
The PropertyFactory is used to display the individual property rows from
each xunit.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-16 10:34:21 -04:00
59cb699bd9 gtk: Add a PropertyList model
Along with Property, PropertyValue, and PropertyFilter objects that are used
togther to show specific properties to the user when placed in a
Gtk.ColumnView.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-16 10:34:20 -04:00
4838889c56 gtk: Create a Factory base class
And convert our other Factory instances to inherit from it. This lets me
set up a Gtk.Inscription() in a singe way that is used everywere, and
abstract out some other binding code to make implementing new factories
easier.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-15 16:37:55 -04:00
5dc7735ba7 gtk: Create an XunitList base class
The XunitList implements the Gio.ListModel interface, and builds in some
virtual functions to query and fill out the rows in the list in a
standard way. I also update the TestCaseList and SummaryList to both
inherit from this class.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-15 13:56:27 -04:00
c45ec1909e gtk: Create an XunitRow base class
This Object is intended to be returned by a Gio.ListModel to show
Xfstests xunit information. I change the TestCase and Summary classes to
inherit from this class, removing a lot of duplicated code.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-15 13:01:24 -04:00
ad08357121 gtk: Create a XunitCell object and rename the XunitResult object
I use the XunitCell as a base class for the (renamed) TestResult and
SettingsValue objects. This lets us combine common values and
functionality in one reusable place.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-15 13:00:51 -04:00
4d3425bb57 xfstestsdb 1.3
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-08-15 10:57:27 -04:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
bea49c5eae xfstestsdb 1.2
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-26 11:27:32 -04:00
14654dcf23 gc: Create the xfstestsdb gc command
This command is used to garbage collect the xfstestsdb sqlite database.
It removes xfstests runs that have no added xunits, and runs older than
180 days that have not been tagged.

Implements: #16 (`xfstestsdb gc`)
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-26 11:26:09 -04:00
68a00ea94d xunit: Create the xfstestsdb xunit gc command
This command is used to garbage collect the xunit table by removing
xunits that have no testcases. This could be expanded on later to add
more removal conditions, such as xunits older than some age.

Implements: #15 (`xfstestsdb xunit gc`)
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-26 11:26:08 -04:00
c3eb740fb5 xunit delete: Xunit property performance improvements
I found that deleting an xunit from the database was very, very slow.
This patch addresses the issue by adding additional indexes on the
link_xunit_properties table to make it easier to find properties that
are still in use. I also rework the `cleanup_xunit_properties` trigger
to only check propids that were used by the deleted xunit.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-25 12:42:30 -04:00
a168a7f84b xunit delete: Testcase message performance improvements
I found that deleting an xunit from the database was very, very slow.
This patch addresses the issue by adding additional indexes on the
testcases table to make it easier to find messages that are still in
use. I also rework the `cleanup_testcase_messages` trigger to only check
messageids that were used by the deleted testcase.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-25 10:41:58 -04:00
2082b904a0 sqlite: Add an upgrade script to database version 2
Right now it doesn't do any extra changes to the database besides
bumping the user_version to '2'. I'll fill it out over the next several
commits.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-25 10:03:32 -04:00
3f61adc941 sqlite: Give the Connection an executescript() function
This function builds on the built-in Python function, and adds in
opening and reading the file in a way that can be used for running
generic scripts on the database.

I also take this chance to move SQL scripts into a subdirectory to keep
them together.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-07-25 09:54:31 -04:00
14b848bddd xfstestsdb v1.1
- Updates for recent xfstests xunit generation changes
- Fix the `xunit delete` command
- Make sure SQLite foreign_keys are always enabled

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-04-17 15:58:00 -04:00
5d141bd436 sqlite: Make sure foreign_keys are enabled
SQLite wants foreign_keys to be enabled for each connection, and doesn't
save this setting.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-04-17 15:57:47 -04:00
8a70fa6427 xunit: Fix xunit delete
xunit delete was incorrectly passing "rowid=" instead of "runid=" to the
sql DELETE statement.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-04-17 14:35:29 -04:00
ee6c758943 xunit: Update read.py for the recent xunit changes
The xfstests xunit generation has been updated to include an xml
namespace and add some extra attributes to the root element. This broke
my xunit parsing code, which is fixed with this patch.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
2023-04-17 14:15:44 -04:00
53 changed files with 4564 additions and 31 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)

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

@ -0,0 +1,490 @@
# 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 TestXunitCell(unittest.TestCase):
"""Test case for our base XunitCell object."""
def test_cell(self):
"""Test creating a Cell instance."""
cell = xfstestsdb.gtk.model.XunitCell(name="my xunit name")
self.assertIsInstance(cell, GObject.GObject)
self.assertEqual(cell.name, "my xunit name")
self.assertEqual(str(cell), "my xunit name")
class TestXunitRow(unittest.TestCase):
"""Test case for our base XunitRow object."""
def setUp(self):
"""Set up common variables."""
self.row = xfstestsdb.gtk.model.XunitRow(name="row-name")
def test_init(self):
"""Test that the XunitRow is set up properly."""
self.assertIsInstance(self.row, GObject.GObject)
self.assertEqual(self.row.name, "row-name")
def test_compare(self):
"""Test the less-than operator on XunitRows."""
row2 = xfstestsdb.gtk.model.XunitRow(name="row-name-2")
self.assertTrue(self.row < row2)
self.assertFalse(row2 < self.row)
self.assertFalse(self.row < self.row)
def test_xunits(self):
"""Test adding xunits to a XunitRow."""
self.assertIsNone(self.row["xunit-1"])
self.row.add_xunit("xunit-1")
self.assertSetEqual(self.row.get_results(), {"xunit-1"})
self.assertListEqual(self.row.get_xunits(), ["xunit-1"])
xunit = self.row["xunit-1"]
self.assertIsInstance(xunit, xfstestsdb.gtk.model.XunitCell)
self.assertEqual(xunit.name, "xunit-1")
self.row.add_xunit("xunit-2")
self.assertSetEqual(self.row.get_results(), {"xunit-1", "xunit-2"})
self.assertListEqual(self.row.get_xunits(), ["xunit-1", "xunit-2"])
xunit = self.row["xunit-2"]
self.assertIsInstance(xunit, xfstestsdb.gtk.model.XunitCell)
self.assertEqual(xunit.name, "xunit-2")
class TestXunitList(unittest.TestCase):
"""Test case for our base XunitList object."""
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.xulist = xfstestsdb.gtk.model.XunitList(self.xfstestsdb.sql, 1)
def test_init(self):
"""Test that the XunitList was set up properly."""
self.assertIsInstance(self.xulist, GObject.GObject)
self.assertIsInstance(self.xulist, Gio.ListModel)
self.assertEqual(self.xulist.runid, 1)
def test_get_item_type(self):
"""Test the get_item_type() function."""
self.assertEqual(self.xulist.get_item_type(),
xfstestsdb.gtk.model.XunitRow.__gtype__)
def test_get_n_items(self):
"""Test the get_n_items() function."""
self.assertEqual(self.xulist.get_n_items(), 0)
self.assertEqual(self.xulist.n_items, 0)
self.xulist.n_items = 2
self.assertEqual(self.xulist.get_n_items(), 2)
def test_get_item(self):
"""Test the get_item() function."""
self.assertIsNone(self.xulist.get_item(0))
def test_get_xunits(self):
"""Test the get_xunits() function."""
self.assertListEqual(self.xulist.get_xunits(), ["xunit-1", "xunit-2"])
class TestPropertyValue(unittest.TestCase):
"""Tests a single Xunit Property instance."""
def test_xunit_property(self):
"""Test creating an xunit property instance."""
property = xfstestsdb.gtk.model.PropertyValue(name="my xunit name",
key="key", value="123")
self.assertIsInstance(property, xfstestsdb.gtk.model.XunitCell)
self.assertEqual(property.name, "my xunit name")
self.assertEqual(property.key, "key")
self.assertEqual(property.value, "123")
self.assertEqual(str(property), "key = 123")
class TestProperty(unittest.TestCase):
"""Tests our Property GObject."""
def setUp(self):
"""Set up common variables."""
self.property = xfstestsdb.gtk.model.Property(name="property")
def test_init(self):
"""Check that the Property is set up properly."""
self.assertIsInstance(self.property, xfstestsdb.gtk.model.XunitRow)
self.assertEqual(self.property.name, "property")
def test_xunits(self):
"""Test adding xunits to a Property."""
self.assertIsNone(self.property["xunit-1"])
self.assertIsNone(self.property.get_value())
self.assertFalse(self.property.all_same_value())
self.property.add_xunit("xunit-1", "PLATFORM", "linux-123")
property = self.property["xunit-1"]
self.assertIsInstance(property, xfstestsdb.gtk.model.PropertyValue)
self.assertEqual(property.name, "xunit-1")
self.assertEqual(property.key, "PLATFORM")
self.assertEqual(property.value, "linux-123")
self.assertFalse(self.property.all_same_value())
self.assertIsNone(self.property.get_value())
self.property.add_xunit("xunit-2", "PLATFORM", "linux-123")
property = self.property["xunit-2"]
self.assertIsInstance(property, xfstestsdb.gtk.model.PropertyValue)
self.assertEqual(property.name, "xunit-2")
self.assertEqual(property.key, "PLATFORM")
self.assertEqual(property.value, "linux-123")
self.assertTrue(self.property.all_same_value())
self.assertEqual(self.property.get_value(), "linux-123")
self.property.add_xunit("xunit-3", "PLATFORM", "linux-456")
self.assertFalse(self.property.all_same_value())
self.assertIsNone(self.property.get_value())
class TestPropertyList(unittest.TestCase):
"""Tests our PropertyList GObject."""
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_2)])
self.prlist = xfstestsdb.gtk.model.PropertyList(self.xfstestsdb.sql, 1)
def test_init(self):
"""Test that the PropertyList was set up properly."""
self.assertIsInstance(self.prlist, xfstestsdb.gtk.model.XunitList)
self.assertEqual(self.prlist.runid, 1)
self.assertEqual(self.prlist.n_items, 15)
def test_environment(self):
"""Test environment handling."""
self.assertIsInstance(self.prlist.environment, Gio.ListStore)
self.assertEqual(len(self.prlist.environment), 1)
self.assertEqual(self.prlist.environment[0], self.prlist)
env = self.prlist.get_environment()
self.assertDictEqual(env, {"FSTYP": "myfs",
"CHECK_OPTIONS": "-r -R xunit -g quick"})
class TestPropertyFilter(unittest.TestCase):
"""Tests our Gtk.Filter customized for filtering Properties."""
def setUp(self):
"""Set up common variables."""
self.filter = xfstestsdb.gtk.model.PropertyFilter()
def test_init(self):
"""Test that the TestCaseFilter is set up properly."""
self.assertIsInstance(self.filter, Gtk.Filter)
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
def test_hidden_properties(self):
"""Test the hidden properties global variable."""
self.assertSetEqual(xfstestsdb.gtk.model.HIDDEN_PROPERTIES,
{"CPUS", "HOST_OPTIONS", "LOAD_FACTOR",
"MEM_KB", "NUMA_NODES", "OVL_LOWER",
"OVL_UPPER", "OVL_WORK", "PLATFORM",
"SECTION", "SWAP_KB", "TIME_FACTOR"})
def test_match(self):
"""Test matching Properties with the Filter."""
property = xfstestsdb.gtk.model.Property("name")
self.assertTrue(self.filter.match(property))
property.add_xunit("xunit-1", "name", "my name")
self.assertTrue(self.filter.match(property))
property.add_xunit("xunit-2", "name", "my name")
self.assertFalse(self.filter.match(property))
property.add_xunit("xunit-3", "name", "my other name")
self.assertTrue(self.filter.match(property))
for prop in xfstestsdb.gtk.model.HIDDEN_PROPERTIES:
with self.subTest(property=prop):
property.name = prop
self.assertFalse(self.filter.match(property))
class TestTestResult(unittest.TestCase):
"""Tests a single TestCase Xunit instance."""
def test_xunit_result(self):
"""Test creating an xunit instance."""
xunit = xfstestsdb.gtk.model.TestResult(name="my xunit name",
status="passed", time=123,
message="my message",
stdout="my stdout",
stderr="my stderr")
self.assertIsInstance(xunit, xfstestsdb.gtk.model.XunitCell)
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")
self.assertEqual(str(xunit), "passed")
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, xfstestsdb.gtk.model.XunitRow)
self.assertEqual(self.testcase.name, "test-case")
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.TestResult)
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.TestResult)
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, "")
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, xfstestsdb.gtk.model.XunitList)
self.assertEqual(self.tclist.runid, 1)
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))
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, xfstestsdb.gtk.model.XunitCell)
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, xfstestsdb.gtk.model.XunitRow)
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")
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, xfstestsdb.gtk.model.XunitList)
self.assertEqual(self.summary.runid, 1)
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)

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

@ -0,0 +1,442 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our row widgets and factories."""
import datetime
import unittest
import xfstestsdb.gtk.row
import tests.xunit
from gi.repository import Gtk
from gi.repository import Adw
class TestFactory(unittest.TestCase):
"""Tests our base Gtk.Factory to make Gtk.Inscriptions."""
def setUp(self):
"""Set up common variables."""
self.xunitrow = xfstestsdb.gtk.model.XunitRow("xunit/row")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.xunitrow)
self.factory = xfstestsdb.gtk.row.Factory()
def test_init(self):
"""Test that the Factory was initialized correctly."""
self.assertIsInstance(self.factory, Gtk.SignalListItemFactory)
def test_setup(self):
"""Test that the factory implements the 'setup' signal."""
with unittest.mock.patch.object(self.factory,
"do_setup") as mock_setup:
self.factory.emit("setup", self.listitem)
self.assertIsInstance(self.listitem.get_child(), Gtk.Inscription)
self.assertEqual(self.listitem.get_child().props.xalign, 0.5)
self.assertEqual(self.listitem.get_child().props.nat_chars, 18)
self.assertEqual(self.listitem.get_child().props.text_overflow,
Gtk.InscriptionOverflow.ELLIPSIZE_END)
self.assertTrue(self.listitem.get_child().has_css_class("numeric"))
mock_setup.assert_called_with(self.listitem.get_child())
def test_bind(self):
"""Test that the factory implements the 'bind' signal."""
with unittest.mock.patch.object(self.factory, "do_bind") as mock_bind:
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
mock_bind.assert_called_with(self.xunitrow,
self.listitem.get_child())
def test_unbind(self):
"""Test that the factory implements the 'unbind' signal."""
with unittest.mock.patch.object(self.factory,
"do_unbind") as mock_unbind:
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.listitem.get_child().set_text("text")
self.factory.emit("unbind", self.listitem)
self.assertIsNone(self.listitem.get_child().get_text())
mock_unbind.assert_called_with(self.xunitrow,
self.listitem.get_child())
def test_teardown(self):
"""Test that the factory implements the 'teardown' signal."""
with unittest.mock.patch.object(self.factory,
"do_teardown") as mock_teardown:
self.factory.emit("setup", self.listitem)
child = self.listitem.get_child()
self.factory.emit("teardown", self.listitem)
self.assertIsNone(self.listitem.get_child())
mock_teardown.assert_called_with(child)
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(property="name")
self.group = xfstestsdb.gtk.row.LabelFactory.group
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, xfstestsdb.gtk.row.Factory)
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.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.assertIsNone(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.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 TestEnvironmentFactory(unittest.TestCase):
"""Tests our Gtk.Factory to show environment properties."""
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.props = xfstestsdb.gtk.model.PropertyList(self.xfstestsdb.sql, 1)
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.props)
self.factory = xfstestsdb.gtk.row.EnvironmentFactory(property="FSTYP")
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, xfstestsdb.gtk.row.Factory)
self.assertEqual(self.factory.property, "FSTYP")
def test_bind(self):
"""Test binding to a property."""
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_xalign(), 0)
self.assertEqual(self.listitem.get_child().get_text(), "myfs")
self.assertEqual(self.listitem.get_child().get_tooltip_text(), "myfs")
class TestXunitFactory(unittest.TestCase):
"""Tests our XunitFactory base class."""
def setUp(self):
"""Set up common variables."""
self.factory = xfstestsdb.gtk.row.XunitFactory(xunit="xunit-1")
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, xfstestsdb.gtk.row.Factory)
self.assertEqual(self.factory.xunit, "xunit-1")
class TestPropertyFactory(unittest.TestCase):
"""Tests our Gtk.Factory to show xunit properties."""
def setUp(self):
"""Set up common variables."""
self.property = xfstestsdb.gtk.model.Property("property")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.property)
self.factory = xfstestsdb.gtk.row.PropertyFactory(xunit="xunit-1")
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, xfstestsdb.gtk.row.XunitFactory)
def test_bind(self):
"""Test binding to a property."""
self.property.add_xunit("xunit-1", "property", "value")
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertEqual(self.listitem.get_child().get_text(), "value")
self.assertEqual(self.listitem.get_child().get_tooltip_text(), "value")
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="xunit-1")
self.factory.connect("setup", self.setup_parent)
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, xfstestsdb.gtk.row.XunitFactory)
def test_setup_click(self):
"""Test that we setup a GestureClick on the child widget."""
self.factory.emit("setup", self.listitem)
child = self.listitem.get_child()
click = getattr(child, "click")
self.assertIsInstance(click, Gtk.GestureClick)
self.assertIn(click, child.observe_controllers())
self.factory.emit("teardown", self.listitem)
self.assertIsNone(getattr(child, "click"))
self.assertNotIn(click, child.observe_controllers())
def test_bind_passed(self):
"""Test binding to a passing test."""
show_messages = unittest.mock.Mock()
self.testcase.add_xunit("xunit-1", "passed", 3, "", None, None)
self.factory.connect("show-messages", show_messages)
self.factory.emit("setup", self.listitem)
child = self.listitem.get_child()
self.factory.emit("bind", self.listitem)
self.assertEqual(child.get_text(), "3 seconds")
self.assertIsNone(child.get_tooltip_text())
self.assertTrue(self.parent.has_css_class("passed"))
child.click.emit("released", 1, 0, 0)
show_messages.assert_not_called()
self.factory.emit("unbind", self.listitem)
self.assertFalse(self.parent.has_css_class("passed"))
def test_bind_skipped(self):
"""Test binding to a skipped test."""
show_messages = unittest.mock.Mock()
self.testcase.add_xunit("xunit-1", "skipped", 0,
"test skipped for ... reasons", None, None)
self.factory.connect("show-messages", show_messages)
self.factory.emit("setup", self.listitem)
child = self.listitem.get_child()
self.factory.emit("bind", self.listitem)
self.assertEqual(child.get_text(), "skipped")
self.assertEqual(child.get_tooltip_text(),
"test skipped for ... reasons")
self.assertTrue(self.parent.has_css_class("skipped"))
child.click.emit("released", 1, 0, 0)
show_messages.assert_not_called()
self.factory.emit("unbind", self.listitem)
self.assertFalse(self.parent.has_css_class("skipped"))
def test_bind_failed(self):
"""Test binding to a failed test."""
show_messages = unittest.mock.Mock()
self.testcase.add_xunit("xunit-1", "failure", 8,
"- failed. see output", "stdout message",
"stderr message")
self.factory.connect("show-messages", show_messages)
self.factory.emit("setup", self.listitem)
child = self.listitem.get_child()
self.factory.emit("bind", self.listitem)
self.assertEqual(child.get_text(), "failure")
self.assertEqual(child.get_tooltip_text(), "failed. see output")
self.assertTrue(self.parent.has_css_class("failure"))
child.click.emit("released", 1, 0, 0)
show_messages.assert_called_with(self.factory, "test/case", "xunit-1",
"stdout message", "stderr message")
self.factory.emit("unbind", self.listitem)
self.assertFalse(self.parent.has_css_class("failure"))
child.click.emit("released", 1, 0, 0)
show_messages.assert_called_once()
def test_bind_missing(self):
"""Test binding to a missing test."""
self.factory.emit("setup", self.listitem)
self.factory.emit("bind", self.listitem)
self.assertIsNone(self.listitem.get_child().get_text())
self.factory.emit("unbind", self.listitem)
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="xunit-1")
def test_init(self):
"""Test that the factory was initialized correctly."""
self.assertIsInstance(self.factory, xfstestsdb.gtk.row.XunitFactory)
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.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.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.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.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")

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

@ -0,0 +1,575 @@
# 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
from gi.repository import Adw
class TestXunitView(unittest.TestCase):
"""Tests the XunitView base 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(["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.XunitList(self.xfstestsdb.sql, 1)
self.view = xfstestsdb.gtk.view.XunitView("title")
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):
"""Test that we set up the ColumnView child 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_filtermodel(self):
"""Test that we set up the Gtk.FilterModel correctly."""
self.assertIsInstance(self.view.filtermodel, Gtk.FilterListModel)
self.assertIsNone(self.view.filtermodel.props.filter)
self.assertEqual(self.view.props.child.get_model().get_model(),
self.view.filtermodel)
def test_firstcol(self):
"""Test that we set up the first column correctly."""
self.assertIsInstance(self.view.firstcol, Gtk.ColumnViewColumn)
self.assertIsInstance(self.view.firstcol.props.factory,
xfstestsdb.gtk.row.LabelFactory)
self.assertEqual(self.view.firstcol.props.factory.property, "name")
self.assertEqual(self.view.firstcol.props.title, "title")
self.assertFalse(self.view.firstcol.props.expand)
def test_model(self):
"""Test the model property."""
columns = self.view.props.child.get_columns()
self.assertIsNone(self.view.model)
self.assertEqual(len(columns), 0)
self.view.model = self.model
self.assertEqual(self.view.filtermodel.props.model, self.model)
self.assertEqual(len(columns), 3)
self.assertEqual(columns[0], self.view.firstcol)
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.XunitFactory)
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(columns), 0)
class EnvironmentView(unittest.TestCase):
"""Tests the EnvironmentView."""
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.props = xfstestsdb.gtk.model.PropertyList(self.xfstestsdb.sql, 1)
self.model = self.props.environment
self.view = xfstestsdb.gtk.view.EnvironmentView()
def test_init(self):
"""Test that we created the EnvironmentView correctly."""
self.assertIsInstance(self.view, xfstestsdb.gtk.view.XunitView)
self.assertIsNone(self.view.filtermodel.props.filter)
self.assertIsNone(self.view.firstcol)
self.assertIsNone(self.view.model)
self.assertTrue(self.view.props.child.has_css_class("data-table"))
def test_make_factory(self):
"""Test the do_make_factory() implementation."""
factory = self.view.do_make_factory("property")
self.assertIsInstance(factory, xfstestsdb.gtk.row.EnvironmentFactory)
self.assertEqual(factory.property, "property")
def test_model(self):
"""Test the model property."""
columns = self.view.props.child.get_columns()
self.assertIsNone(self.view.model)
self.assertEqual(len(columns), 0)
self.assertFalse(self.view.props.visible)
self.view.model = self.model
self.assertEqual(self.view.filtermodel.props.model, self.model)
self.assertEqual(len(columns), 7)
self.assertTrue(self.view.props.visible)
self.view.model = None
self.assertEqual(len(columns), 0)
self.assertFalse(self.view.props.visible)
class TestPropertyView(unittest.TestCase):
"""Tests the PropertyView."""
def setUp(self):
"""Set up common variables."""
self.view = xfstestsdb.gtk.view.PropertyView()
def test_init(self):
"""Test that we created the ProeprtyView correctly."""
self.assertIsInstance(self.view, xfstestsdb.gtk.view.XunitView)
self.assertIsInstance(self.view.filtermodel.props.filter,
xfstestsdb.gtk.model.PropertyFilter)
self.assertIsNone(self.view.model)
self.assertEqual(self.view.props.vscrollbar_policy,
Gtk.PolicyType.NEVER)
self.assertEqual(self.view.firstcol.props.title, "property")
self.assertTrue(self.view.props.child.has_css_class("data-table"))
def test_make_factory(self):
"""Test the do_make_factory() implementation."""
factory = self.view.do_make_factory("xunit-1")
self.assertIsInstance(factory, xfstestsdb.gtk.row.PropertyFactory)
self.assertEqual(factory.xunit, "xunit-1")
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.view = xfstestsdb.gtk.view.TestCaseView()
def test_init(self):
"""Test that we create the TestCaseView correctly."""
self.assertIsInstance(self.view, xfstestsdb.gtk.view.XunitView)
self.assertIsInstance(self.view.filtermodel.props.filter,
xfstestsdb.gtk.model.TestCaseFilter)
self.assertIsNone(self.view.model)
self.assertEqual(self.view.firstcol.props.title, "testcase")
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.filterbuttons,
xfstestsdb.gtk.view.FilterButtons)
self.assertFalse(self.view.filtermodel.props.filter.passed)
self.view.filterbuttons.passed = True
self.assertTrue(self.view.filtermodel.props.filter.passed)
self.assertFalse(self.view.filtermodel.props.filter.skipped)
self.view.filterbuttons.skipped = True
self.assertTrue(self.view.filtermodel.props.filter.skipped)
self.assertTrue(self.view.filtermodel.props.filter.failure)
self.view.filterbuttons.failure = False
self.assertFalse(self.view.filtermodel.props.filter.failure)
def test_make_factory(self):
"""Test the do_make_factory() implementation."""
show_messages = unittest.mock.Mock()
self.view.connect("show-messages", show_messages)
factory = self.view.do_make_factory("xunit-1")
self.assertIsInstance(factory, xfstestsdb.gtk.row.ResultFactory)
self.assertEqual(factory.xunit, "xunit-1")
factory.emit("show-messages", "testcase", "xunit", "stdout", "stderr")
show_messages.assert_called_with(self.view, "testcase", "xunit",
"stdout", "stderr")
class TestMessageView(unittest.TestCase):
"""Test the MessageView."""
def setUp(self):
"""Set up common variables."""
self.view = xfstestsdb.gtk.view.MessageView("title")
def test_init(self):
"""Check that the MessageView was set up correctly."""
self.assertIsInstance(self.view, Gtk.Box)
self.assertEqual(self.view.props.orientation, Gtk.Orientation.VERTICAL)
self.assertTrue(self.view.has_css_class("view"))
def test_detect_diff(self):
"""Check detecting if input test looks like a diff."""
self.assertFalse(self.view.detect_diff("not a diff"))
lines = ["+++ /some/file"]
self.assertFalse(self.view.detect_diff("\n".join(lines)))
lines.append("--- /some/other/file")
self.assertFalse(self.view.detect_diff("\n".join(lines)))
lines.append("@@ 12,34,5 @@")
self.assertFalse(self.view.detect_diff("\n".join(lines)))
lines.append(" some context line")
self.assertTrue(self.view.detect_diff("\n".join(lines)))
lines[-1] = "+an added line"
self.assertTrue(self.view.detect_diff("\n".join(lines)))
lines[-1] = "-a removed line"
self.assertTrue(self.view.detect_diff("\n".join(lines)))
def test_markup_diff(self):
"""Check colorizing lines with diff colors."""
self.assertEqual(self.view.markup_diff("abcde"), "abcde")
self.assertEqual(self.view.markup_diff("+++ /some/file"),
"<span color='#26a269'>+++ /some/file</span>")
self.assertEqual(self.view.markup_diff("--- /some/other/file"),
"<span color='#c01c28'>--- /some/other/file</span>")
self.assertEqual(self.view.markup_diff("@@ 12,34,5 @@"),
"<span color='#1c71d8'>@@ 12,34,5 @@</span>")
self.assertEqual(self.view.markup_diff(" a context line"),
"<span color='#77767b'> a context line</span>")
self.assertEqual(self.view.markup_diff("+an added line"),
"<span color='#26a269'>+an added line</span>")
self.assertEqual(self.view.markup_diff("-a removed line"),
"<span color='#c01c28'>-a removed line</span>")
def test_title(self):
"""Test the title widgets."""
self.assertIsInstance(self.view._label, Gtk.Label)
self.assertIsInstance(self.view._label.get_next_sibling(),
Gtk.Separator)
self.assertEqual(self.view.get_first_child(), self.view._label)
self.assertEqual(self.view.title, "title")
self.assertEqual(self.view._label.props.label, "title")
self.assertEqual(self.view._label.props.margin_top, 6)
self.assertTrue(self.view._label.has_css_class("large-title"))
def test_text(self):
"""Test the text property."""
win = self.view.get_last_child()
self.assertIsInstance(win, Gtk.ScrolledWindow)
self.assertIsInstance(self.view._textview, Gtk.TextView)
self.assertEqual(win.props.child, self.view._textview)
self.assertTrue(win.props.vexpand)
self.assertFalse(self.view._textview.props.editable)
self.assertTrue(self.view._textview.props.monospace)
buffer = self.view._textview.get_buffer()
self.assertEqual(self.view.text, "")
with unittest.mock.patch.object(buffer, "set_text",
wraps=buffer.set_text) as mock_set:
self.view.text = "text"
self.assertEqual(buffer.get_text(buffer.get_start_iter(),
buffer.get_end_iter(), True),
"text")
mock_set.assert_called_with("text")
self.assertEqual(self.view.text, "text")
def test_text_diff(self):
"""Test setting the text property to a diff string."""
buffer = self.view._textview.get_buffer()
diff = ["+++ /some/file", "--- /some/other/file", "@@ 12,34,5 @@",
" context line", "-removed line", "+added line"]
with unittest.mock.patch.object(buffer, "set_text",
wraps=buffer.set_text) as mock_set:
self.view.text = "\n".join(diff)
mock_set.assert_not_called()
self.assertEqual(self.view.text, "\n".join(diff))
self.view.text = "\n".join(diff)
self.assertEqual(self.view.text, "\n".join(diff))
class MessagesView(unittest.TestCase):
"""Test the MessagesView."""
def setUp(self):
"""Set up common variables."""
self.view = xfstestsdb.gtk.view.MessagesView()
def test_init(self):
"""Check that the MessagesView was set up correctly."""
self.assertIsInstance(self.view, Gtk.Box)
self.assertIsInstance(self.view.get_first_child(), Gtk.CenterBox)
self.assertIsInstance(self.view.get_last_child(), Gtk.Paned)
self.assertTrue(self.view.get_first_child().has_css_class("toolbar"))
self.assertTrue(self.view.has_css_class("card"))
self.assertEqual(self.view.props.orientation, Gtk.Orientation.VERTICAL)
self.assertEqual(self.view.props.margin_start, 24)
self.assertEqual(self.view.props.margin_end, 24)
self.assertEqual(self.view.props.margin_top, 24)
self.assertEqual(self.view.props.margin_bottom, 24)
def test_back_button(self):
"""Check that the back button was set up correctly."""
self.assertIsInstance(self.view._back, Gtk.Button)
self.assertIsInstance(self.view._back.props.child, Adw.ButtonContent)
self.assertEqual(self.view.get_first_child().props.start_widget,
self.view._back)
self.assertEqual(self.view._back.props.child.props.icon_name,
"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"))
go_back = unittest.mock.Mock()
self.view.connect("go-back", go_back)
self.view._back.emit("clicked")
go_back.assert_called()
def test_title(self):
"""Check that the view title was set up correctly."""
self.assertIsInstance(self.view._title, Adw.WindowTitle)
self.assertEqual(self.view.get_first_child().props.center_widget,
self.view._title)
self.assertEqual(self.view.testcase, "")
self.view.testcase = "test/case"
self.assertEqual(self.view._title.props.title, "test/case")
self.assertEqual(self.view.xunit, "")
self.view.xunit = "xunit-1"
self.assertEqual(self.view._title.props.subtitle, "xunit-1")
def test_stdout(self):
"""Check that the stdout window was set up properly."""
self.assertIsInstance(self.view._stdout,
xfstestsdb.gtk.view.MessageView)
self.assertEqual(self.view.get_last_child().props.start_child,
self.view._stdout)
self.assertEqual(self.view._stdout.title, "stdout")
self.assertEqual(self.view.stdout, "")
self.view.stdout = "stdout text"
self.assertEqual(self.view._stdout.text, "stdout text")
def test_stderr(self):
"""Check that the stderr window was set up properly."""
self.assertIsInstance(self.view._stderr,
xfstestsdb.gtk.view.MessageView)
self.assertEqual(self.view.get_last_child().props.end_child,
self.view._stderr)
self.assertEqual(self.view._stderr.title, "stderr")
self.assertEqual(self.view.stderr, "")
self.view.stderr = "stderr text"
self.assertEqual(self.view._stderr.text, "stderr text")
class TestSummaryView(unittest.TestCase):
"""Tests the SummaryView."""
def setUp(self):
"""Set up common variables."""
self.view = xfstestsdb.gtk.view.SummaryView()
def test_init(self):
"""Test that we created the SummaryView correctly."""
self.assertIsInstance(self.view, xfstestsdb.gtk.view.XunitView)
self.assertIsNone(self.view.filtermodel.props.filter)
self.assertIsNone(self.view.model)
self.assertEqual(self.view.props.vscrollbar_policy,
Gtk.PolicyType.NEVER)
self.assertEqual(self.view.firstcol.props.title, "summary")
self.assertTrue(self.view.props.child.has_css_class("data-table"))
def test_make_factory(self):
"""Test the do_make_factory() implementation."""
factory = self.view.do_make_factory("xunit-1")
self.assertIsInstance(factory, xfstestsdb.gtk.row.SummaryFactory)
self.assertEqual(factory.xunit, "xunit-1")
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.props = xfstestsdb.gtk.model.PropertyList(self.xfstestsdb.sql, 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_environment_view(self):
"""Check that the XfstestsView sets up an EnvironmentView correctly."""
self.assertIsInstance(self.view._environview,
xfstestsdb.gtk.view.EnvironmentView)
self.assertEqual(self.view.get_first_child(), self.view._environview)
def test_property_view(self):
"""Check that the XfstestsView sets up a PropertyView correctly."""
sep = self.view._environview.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertIsInstance(self.view._propertyview,
xfstestsdb.gtk.view.PropertyView)
self.assertEqual(sep.get_next_sibling(), self.view._propertyview)
def test_stack(self):
"""Check that the XfstestsView sets u a Gtk.Stack correctly."""
sep = self.view._propertyview.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertIsInstance(self.view._stack, Gtk.Stack)
self.assertEqual(self.view._stack.props.transition_type,
Gtk.StackTransitionType.OVER_UP_DOWN)
self.assertEqual(sep.get_next_sibling(), self.view._stack)
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._stack.get_child_by_name("testcases"),
self.view._testcaseview)
def test_messages_view(self):
"""Check that the XfstestsView sets up a MessagesView correctly."""
self.assertIsInstance(self.view._messagesview,
xfstestsdb.gtk.view.MessagesView)
self.assertEqual(self.view._stack.get_child_by_name("messages"),
self.view._messagesview)
def test_summary_view(self):
"""Check that the XfstestsView sets up a SummaryView correctly."""
sep = self.view._stack.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_environment(self):
"""Test the XfstestsView 'environment' property."""
self.assertIsNone(self.view.environment)
self.view.environment = self.props.environment
self.assertEqual(self.view._environview.model, self.props.environment)
def test_properties(self):
"""Test the XfstestsView 'properties' property."""
self.assertIsNone(self.view.properties)
self.view.properties = self.props
self.assertEqual(self.view._propertyview.model, self.props)
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)
def test_messages(self):
"""Test displaying messages to the user."""
self.assertEqual(self.view._stack.get_visible_child_name(),
"testcases")
self.view._testcaseview.emit("show-messages", "testcase",
"xunit", "stdout", "stderr")
self.assertEqual(self.view._stack.get_visible_child_name(),
"messages")
self.assertEqual(self.view._messagesview.testcase, "testcase")
self.assertEqual(self.view._messagesview.xunit, "xunit")
self.assertEqual(self.view._messagesview.stdout, "stdout")
self.assertEqual(self.view._messagesview.stderr, "stderr")
self.view._messagesview.emit("go-back")
self.assertEqual(self.view._stack.get_visible_child_name(),
"testcases")

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

@ -0,0 +1,106 @@
# 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, 1000)
self.assertEqual(self.window.props.default_width, 1600)
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_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._splitview.props.content,
self.window.child)
label = Gtk.Label()
window2 = xfstestsdb.gtk.window.Window(child=label)
self.assertEqual(window2.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."""
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, "")

7
tests/test-script.sql Normal file
View File

@ -0,0 +1,7 @@
/* Copyright 2023 (c) Anna Schumaker */
CREATE TABLE test (a INT, b INT);
INSERT INTO test VALUES (1, 2);
INSERT INTO test VALUES (3, 4);
INSERT INTO test VALUES (5, 6);
INSERT INTO test VALUES (7, 8);
INSERT INTO test VALUES (9, 0);

View File

@ -24,9 +24,15 @@ class TestCommand(unittest.TestCase):
self.assertEqual(self.command.parser.description, "description")
self.assertEqual(self.command.parser._defaults["function"],
self.command.do_command)
self.assertEqual(self.command.parser._defaults["done"],
self.command.do_done)
self.assertEqual(self.command.sql, self.sql)
def test_do_command(self):
"""Test the do_command() function."""
with self.assertRaises(NotImplementedError):
self.command.do_command(argparse.Namespace())
def test_do_done(self):
"""Test the do_done() function."""
self.command.do_done(argparse.Namespace())

131
tests/test_gc.py Normal file
View File

@ -0,0 +1,131 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests the `xfstestsdb gc` command."""
import datetime
import io
import unittest
import unittest.mock
import xfstestsdb.xunit.gc
import tests.xunit
@unittest.mock.patch("xfstestsdb.sqlite.Connection.vacuum")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
class TestGC(unittest.TestCase):
"""Tests the `xfstestsdb xunit gc` command."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.xunit = self.xfstestsdb.commands["xunit"]
self.gc = self.xunit.commands["gc"]
def setup_runs(self, mock_stdout: io.StringIO):
"""Set up runs in the database and clear stdout."""
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "3", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.sql("DELETE FROM testcases WHERE xunitid=2")
mock_stdout.seek(0)
mock_stdout.truncate(0)
def test_init(self, mock_stdout: io.StringIO,
mock_vacuum: unittest.mock.Mock):
"""Check that the gc command was set up properly."""
self.assertIsInstance(self.gc, xfstestsdb.commands.Command)
self.assertIsInstance(self.gc, xfstestsdb.xunit.gc.Command)
self.assertEqual(self.xunit.subparser.choices["gc"],
self.gc.parser)
def test_gc_empty(self, mock_stdout: io.StringIO,
mock_vacuum: unittest.mock.Mock):
"""Test garbage collecting an empty database."""
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_gc_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [])
self.xfstestsdb.run(["gc"])
self.assertEqual(mock_stdout.getvalue(), "")
mock_vacuum.assert_not_called()
def test_gc_no_testcases(self, mock_stdout: io.StringIO,
mock_vacuum: unittest.mock.Mock):
"""Test garbage collecting runs with no testcases."""
self.setup_runs(mock_stdout)
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_gc_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [2, 3])
self.xfstestsdb.run(["gc"])
self.assertRegex(mock_stdout.getvalue(),
"run #2 has been deleted\nrun #3 has been deleted")
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [1])
mock_vacuum.assert_called()
def test_gc_expired(self, mock_stdout: io.StringIO,
mock_vacuum: unittest.mock.Mock):
"""Test garbage collecting old runs."""
self.setup_runs(mock_stdout)
self.xfstestsdb.run(["xunit", "read", "2", str(tests.xunit.XUNIT_1)])
utcnow = datetime.datetime.utcnow()
expired = utcnow - datetime.timedelta(days=180, seconds=1)
self.xfstestsdb.sql("""UPDATE xfstests_runs SET timestamp=?
WHERE runid=1""", expired)
not_expired = utcnow - datetime.timedelta(days=180, seconds=-1)
self.xfstestsdb.sql("""UPDATE xfstests_runs SET timestamp=?
WHERE runid=2""", not_expired)
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_gc_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [1, 3])
self.xfstestsdb.run(["gc"])
self.assertRegex(mock_stdout.getvalue(),
"run #1 has been deleted\nrun #3 has been deleted")
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [2])
mock_vacuum.assert_called()
def test_gc_expired_tagged(self, mock_stdout: io.StringIO,
mock_vacuum: unittest.mock.Mock):
"""Test that we don't garbage collect expired runs that are tagged."""
self.setup_runs(mock_stdout)
self.xfstestsdb.run(["xunit", "read", "2", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["tag", "1", "my-tag"])
self.xfstestsdb.run(["tag", "3", "my-tag"])
utcnow = datetime.datetime.utcnow()
expired = utcnow - datetime.timedelta(days=181)
self.xfstestsdb.sql("UPDATE xfstests_runs SET timestamp=?", expired)
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_gc_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [2, 3])
self.xfstestsdb.run(["gc"])
self.assertRegex(mock_stdout.getvalue(),
"run #2 has been deleted\nrun #3 has been deleted")
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [1])
mock_vacuum.assert_called()
def test_gc_dry_run(self, mock_stdout: io.StringIO,
mock_vacuum: unittest.mock.Mock):
"""Test garbage collecting with the --dry-run option."""
self.setup_runs(mock_stdout)
self.xfstestsdb.run(["gc", "--dry-run"])
self.assertRegex(mock_stdout.getvalue(),
"run #2 would be deleted\nrun #3 would be deleted")
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()],
[1, 2, 3])
mock_vacuum.assert_not_called()

200
tests/test_gtk.py Normal file
View File

@ -0,0 +1,200 @@
# 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.properties)
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.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()
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.properties,
xfstestsdb.gtk.model.PropertyList)
self.assertIsInstance(self.application.model,
xfstestsdb.gtk.model.TestCaseList)
self.assertIsInstance(self.application.summary,
xfstestsdb.gtk.model.SummaryList)
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")
@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.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.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)
self.application.environment = properties.environment
self.assertEqual(self.application.view.environment,
properties.environment)
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."""
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")
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

@ -30,6 +30,7 @@ class TestList(unittest.TestCase):
self.xfstestsdb.run(["tag", "1", "mytag1"])
self.xfstestsdb.run(["tag", "1", "mytag2"])
self.xfstestsdb.run(["tag", "3", "mytag3"])
self.xfstestsdb.run(["tag", "3", "othertag"])
self.xfstestsdb.sql.executemany("""UPDATE xfstests_runs SET timestamp=?
WHERE rowid=?""", (timestamp, 1),
@ -116,13 +117,12 @@ class TestList(unittest.TestCase):
self.xfstestsdb.run(["list", "--color", "none", "--tags"])
self.assertEqual(mock_stdout.getvalue(),
"""
+-----+---------------------+-----------+---------------+
| run | timestamp | device | tags |
+-----+---------------------+-----------+---------------+
| 1 | 2023-01-01 12:59:59 | /dev/vda1 | mytag1,mytag2 |
| 2 | 2023-01-02 12:59:59 | /dev/vda2 | |
| 3 | 2023-01-03 12:59:59 | /dev/vdb1 | mytag3 |
+-----+---------------------+-----------+---------------+
+-----+---------------------+-----------+-----------------+
| run | timestamp | device | tags |
+-----+---------------------+-----------+-----------------+
| 1 | 2023-01-01 12:59:59 | /dev/vda1 | mytag1,mytag2 |
| 3 | 2023-01-03 12:59:59 | /dev/vdb1 | mytag3,othertag |
+-----+---------------------+-----------+-----------------+
""")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
@ -170,7 +170,6 @@ class TestList(unittest.TestCase):
| run | timestamp | device | xunits |
+-----+---------------------+-----------+---------------+
| 1 | 2023-01-01 12:59:59 | /dev/vda1 | test-1,test-2 |
| 2 | 2023-01-02 12:59:59 | /dev/vda2 | |
| 3 | 2023-01-03 12:59:59 | /dev/vdb1 | test-3 |
+-----+---------------------+-----------+---------------+
""")

View File

@ -29,6 +29,12 @@ class TestNew(unittest.TestCase):
r"created run #1 with test device '/dev/vdb1' "
r"\[[\d\-\: ]+\]\n")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_new_terse(self, mock_stdout: io.StringIO):
"""Test running `xfstestsdb new --terse`."""
self.xfstestsdb.run(["new", "--terse", "/dev/vdb1"])
self.assertEqual(mock_stdout.getvalue(), "1\n")
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
def test_new_error(self, mock_stderr: io.StringIO):
"""Test running the `xfstestsdb new` command with invalid input."""

View File

@ -22,8 +22,12 @@ class TestConnection(unittest.TestCase):
data_dir / "xfstestsdb-debug.sqlite3")
self.assertEqual(xfstestsdb.sqlite.DATABASE, ":memory:")
script = pathlib.Path(xfstestsdb.__file__).parent / "xfstestsdb.sql"
self.assertEqual(xfstestsdb.sqlite.SQL_SCRIPT, script)
self.assertEqual(xfstestsdb.sqlite.SQL_SCRIPTS,
pathlib.Path(xfstestsdb.__file__).parent / "scripts")
self.assertEqual(xfstestsdb.sqlite.SQL_V1_SCRIPT,
xfstestsdb.sqlite.SQL_SCRIPTS / "xfstestsdb.sql")
self.assertEqual(xfstestsdb.sqlite.SQL_V2_SCRIPT,
xfstestsdb.sqlite.SQL_SCRIPTS / "upgrade-v2.sql")
def test_foreign_keys(self):
"""Test that foreign key constraints are enabled."""
@ -33,7 +37,7 @@ class TestConnection(unittest.TestCase):
def test_version(self):
"""Test checking the database schema version."""
cur = self.sql("PRAGMA user_version")
self.assertEqual(cur.fetchone()["user_version"], 1)
self.assertEqual(cur.fetchone()["user_version"], 2)
def test_connection(self):
"""Check that the connection manager is initialized properly."""
@ -72,6 +76,17 @@ class TestConnection(unittest.TestCase):
self.assertListEqual([(row["a"], row["b"]) for row in rows],
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 0)])
def test_executescript(self):
"""Test running a sql script."""
script = pathlib.Path(__file__).parent / "test-script.sql"
cur = self.sql.executescript(script)
self.assertIsInstance(cur, sqlite3.Cursor)
rows = self.sql("SELECT * FROM test").fetchall()
self.assertListEqual([(row["a"], row["b"]) for row in rows],
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 0)])
self.assertIsNone(self.sql.executescript(script.parent / "no-script"))
def test_transaction(self):
"""Test that we can manually start a transaction."""
self.assertFalse(self.sql.sql.in_transaction)
@ -99,6 +114,10 @@ class TestConnection(unittest.TestCase):
with self.assertRaises(sqlite3.OperationalError):
self.sql("SELECT COUNT(*) FROM other_table")
def test_vacuum(self):
"""Test vacuuming the database."""
self.assertIsInstance(self.sql.vacuum(), sqlite3.Cursor)
def test_close(self):
"""Check closing the connection."""
self.sql.close()

View File

@ -32,27 +32,29 @@ 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."""
parser = self.xfstestsdb.subparser.add_parser("test-run", help="help")
test_done = unittest.mock.Mock()
test_passed = False
def test_func(args: argparse.Namespace) -> None:
nonlocal test_passed
self.assertTrue(self.xfstestsdb.sql.sql.in_transaction)
test_passed = True
parser.set_defaults(function=test_func)
parser.set_defaults(function=test_func, done=test_done)
self.xfstestsdb.run(["test-run"])
self.assertTrue(test_passed)
test_done.assert_called()
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_version(self, mock_stdout: io.StringIO):
"""Test printing version information."""
self.assertEqual(xfstestsdb.MAJOR, 1)
self.assertEqual(xfstestsdb.MINOR, 0)
self.assertEqual(xfstestsdb.MINOR, 6)
self.xfstestsdb.run(["--version"])
self.assertEqual(mock_stdout.getvalue(), "xfstestsdb v1.0-debug\n")
self.assertEqual(mock_stdout.getvalue(), "xfstestsdb v1.6-debug\n")

View File

@ -3,3 +3,4 @@
import pathlib
XUNIT_1 = pathlib.Path(__file__).parent / "test-1.xunit"
XUNIT_2 = pathlib.Path(__file__).parent / "test-2.xunit"

View File

@ -1,5 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="xfstests" failures="1" skipped="3" tests="10" time="43" hostname="myhost" timestamp="2023-01-31T14:14:14" >
<testsuite
xmlns="https://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git https://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git/tree/doc/xunit.xsd"
name="xfstests"
failures="1" skipped="3" tests="10" time="43"
hostname="myhost"
start_timestamp="2023-01-31T14:14:14-05:00"
timestamp="2023-01-31T14:14:55-05:00"
report_timestamp="2023-01-31T14:14:57-05:00"
>
<properties>
<property name="SECTION" value="-no-sections-"/>
<property name="FSTYP" value="myfs"/>

68
tests/xunit/test-2.xunit Normal file
View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuite
xmlns="https://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git https://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git/tree/doc/xunit.xsd"
name="xfstests"
failures="1" skipped="3" tests="10" time="43"
hostname="myhost"
start_timestamp="2023-01-31T14:14:14-05:00"
timestamp="2023-01-31T14:14:55-05:00"
report_timestamp="2023-01-31T14:14:57-05:00"
>
<properties>
<property name="SECTION" value="-no-sections-"/>
<property name="FSTYP" value="myfs"/>
<property name="PLATFORM" value="Linux/x86_64 myhost 6.1.8-arch1"/>
<property name="MOUNT_OPTIONS" value="-o mountopt1,mountopt3"/>
<property name="HOST_OPTIONS" value="local.config"/>
<property name="CHECK_OPTIONS" value="-r -R xunit -g quick"/>
<property name="TIME_FACTOR" value="1"/>
<property name="LOAD_FACTOR" value="1"/>
<property name="TEST_DIR" value="/mnt/test2"/>
<property name="TEST_DEV" value="/dev/vdb3"/>
<property name="SCRATCH_DEV" value="/dev/vdb4"/>
<property name="SCRATCH_MNT" value="/mnt/scratch2"/>
<property name="OVL_UPPER" value="ovl-upper"/>
<property name="OVL_LOWER" value="ovl-lower"/>
<property name="OVL_WORK" value="ovl-work"/>
</properties>
<testcase classname="xfstests.global" name="test/01" time="1">
</testcase>
<testcase classname="xfstests.global" name="test/02" time="0">
<skipped message="skipped on /dev/vdb1" />
</testcase>
<testcase classname="xfstests.global" name="test/03" time="0">
<skipped message="skipped on /mnt/test too" />
</testcase>
<testcase classname="xfstests.global" name="test/04" time="4">
</testcase>
<testcase classname="xfstests.global" name="test/05" time="5">
</testcase>
<testcase classname="xfstests.global" name="test/06" time="6">
</testcase>
<testcase classname="xfstests.global" name="test/07" time="0">
<skipped message="fstype &quot;myfs&quot; gets skipped" />
</testcase>
<testcase classname="xfstests.global" name="test/08" time="8">
</testcase>
<testcase classname="xfstests.global" name="test/09" time="9">
<failure message="- output mismatch (see somefile)" type="TestFail" />
<system-out>
<![CDATA[
there was a problem with &apos;/dev/vdb2&apos;
]]>
</system-out>
<system-err>
<![CDATA[
--- test/09.out 2023-01-31 14:14:14.141414 -1414
+++ results/some/sub/dir/test/09.out.bad 2023-02-02 16:16:16.161616 -1616
there was a problem with &apos;/mnt/scratch&apos;
]]>
</system-err>
</testcase>
<testcase classname="xfstests.global" name="test/10" time="10">
</testcase>
</testsuite>

70
tests/xunit/test_gc.py Normal file
View File

@ -0,0 +1,70 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests the `xfstestsdb xunit gc` command."""
import io
import unittest
import unittest.mock
import xfstestsdb.xunit.gc
import tests.xunit
class TestXunitGC(unittest.TestCase):
"""Tests the `xfstestsdb xunit gc` command."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.xunit = self.xfstestsdb.commands["xunit"]
self.gc = self.xunit.commands["gc"]
def setup_runs(self, mock_stdout: io.StringIO):
"""Set up runs in the database and clear stdout."""
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "1", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.run(["xunit", "read", "1", str(tests.xunit.XUNIT_1),
"--name", "test-2"])
self.xfstestsdb.run(["xunit", "read", "2", str(tests.xunit.XUNIT_1)])
self.xfstestsdb.sql("DELETE FROM testcases WHERE xunitid=?", 2)
self.xfstestsdb.sql("DELETE FROM testcases WHERE xunitid=?", 3)
mock_stdout.seek(0)
mock_stdout.truncate(0)
def test_init(self):
"""Check that the xunit gc command was set up properly."""
self.assertIsInstance(self.gc, xfstestsdb.commands.Command)
self.assertIsInstance(self.gc, xfstestsdb.xunit.gc.Command)
self.assertEqual(self.xunit.subparser.choices["gc"],
self.gc.parser)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_empty(self, mock_stdout: io.StringIO):
"""Test garbage collecting an empty database."""
self.xfstestsdb.run(["xunit", "gc"])
self.assertEqual(mock_stdout.getvalue(), "")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_xunits(self, mock_stdout: io.StringIO):
"""Test garbage collecting xunits with default options."""
self.setup_runs(mock_stdout)
self.xfstestsdb.run(["xunit", "gc"])
self.assertRegex(mock_stdout.getvalue(),
"run #1 xunit 'test-2' has been deleted\n"
"run #2 xunit 'test-1' has been deleted")
cur = self.xfstestsdb.sql("SELECT COUNT(*) FROM xunits")
self.assertEqual(cur.fetchone()["COUNT(*)"], 1)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_xunits_dry_run(self, mock_stdout: io.StringIO):
"""Test garbage collecting xunits with the --dry-run option."""
self.setup_runs(mock_stdout)
self.xfstestsdb.run(["xunit", "gc", "--dry-run"])
self.assertRegex(mock_stdout.getvalue(),
"run #1 xunit 'test-2' would be deleted\n"
"run #2 xunit 'test-1' would be deleted")
cur = self.xfstestsdb.sql("SELECT COUNT(*) FROM xunits")
self.assertEqual(cur.fetchone()["COUNT(*)"], 3)

View File

@ -3,6 +3,8 @@
import argparse
from . import sqlite
from . import delete
from . import gc
from . import gtk
from . import list
from . import new
from . import rename
@ -13,7 +15,7 @@ from . import untag
from . import xunit
MAJOR = 1
MINOR = 0
MINOR = 6
class Command:
@ -22,12 +24,15 @@ class Command:
def __init__(self) -> None:
"""Initialize the xfstestsdb command."""
self.parser = argparse.ArgumentParser()
self.parser.set_defaults(function=lambda x: self.parser.print_usage())
self.parser.set_defaults(function=lambda x: self.parser.print_usage(),
done=lambda x: None)
self.parser.add_argument("--version", action="store_true",
help="show version number and exit")
self.subparser = self.parser.add_subparsers(title="commands")
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),
@ -51,3 +56,4 @@ class Command:
else:
with self.sql:
parsed.function(parsed)
parsed.done(parsed)

View File

@ -12,8 +12,12 @@ class Command:
"""Set up the Command."""
self.parser = subparser.add_parser(name, help=help, **kwargs)
self.parser.set_defaults(function=self.do_command)
self.parser.set_defaults(done=self.do_done)
self.sql = sql
def do_command(self, args: argparse.Namespace) -> None:
"""Do something."""
raise NotImplementedError
def do_done(self, args: argparse.Namespace) -> None:
"""Run after the main command, outside of a transaction."""

34
xfstestsdb/gc.py Normal file
View File

@ -0,0 +1,34 @@
# Copyright 2023 (c) Anna Schumaker.
"""The `xfstestsdb gc` command."""
import argparse
from . import commands
from . import sqlite
class Command(commands.Command):
"""The `xfstestsdb gc` command."""
def __init__(self, subparser: argparse.Action,
sql: sqlite.Connection) -> None:
"""Set up the gc command."""
super().__init__(subparser, sql, "gc",
help="garbage collect xfstestsdb entries")
self.parser.add_argument("--dry-run", action="store_true",
help="do not remove the xunit entries")
def do_command(self, args: argparse.Namespace) -> None:
"""Clean up the xunit table."""
how = "would be" if args.dry_run else "has been"
rows = self.sql("SELECT runid FROM xfstests_gc_runs").fetchall()
for runid in sorted([row['runid'] for row in rows]):
if not args.dry_run:
self.sql("DELETE FROM xfstests_runs WHERE runid=?", runid)
print(f"run #{runid} {how} deleted")
args.need_vacuum = len(rows) > 0
def do_done(self, args: argparse.Namespace) -> None:
"""Vacuum the database after deleting."""
if args.need_vacuum and not args.dry_run:
self.sql.vacuum()

118
xfstestsdb/gtk/__init__.py Normal file
View File

@ -0,0 +1,118 @@
# 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 sidebar
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)
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)
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)
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."""
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])
case "show-sidebar":
self.show_sidebar = True
self.activate()
return 0
def do_startup(self) -> None:
"""Handle the Adw.Application::startup signal."""
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, 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")
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='?', type=int,
help="show a specific xfstests run")
def do_command(self, args: argparse.Namespace) -> None:
"""Run the Gtk UI."""
args.app_args = []
if args.runid is not None:
if self.sql("SELECT 1 FROM xfstests_runs WHERE runid=?",
args.runid).fetchone():
args.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:
args.app_args.append("show-sidebar")
def do_done(self, args: argparse.Namespace) -> None:
"""Run the application outside of a transaction."""
Application(self.sql).run(args.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 -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

@ -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,11 @@
<?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>
<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>

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

@ -0,0 +1,295 @@
# Copyright 2023 (c) Anna Schumaker.
"""Our Testcase Gio.ListModel."""
import sqlite3
import typing
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .. import sqlite
class XunitCell(GObject.GObject):
"""Holds a single value for a single Xunit."""
name = GObject.Property(type=str)
def __str__(self) -> str:
"""Get a string representation of this XunitCell."""
return self.name
class XunitRow(GObject.GObject):
"""Collects results for a single row across multiple Xunits."""
name = GObject.Property(type=str)
def __init__(self, name: str) -> None:
"""Initialize an XunitRow."""
super().__init__(name=name)
self.__xunits = {}
def __getitem__(self, xunit: str) -> XunitCell | None:
"""Get the value of a specific Xunit."""
return self.__xunits.get(xunit)
def __lt__(self, rhs: typing.Self) -> bool:
"""Compare the names of two XunitRows."""
return self.name < rhs.name
def add_xunit(self, name: str, *args, **kwargs) -> None:
"""Add an XunitCell to the XunitRow."""
self.__xunits[name] = self.do_make_xunit(name, *args, **kwargs)
def do_make_xunit(self, name: str) -> XunitCell:
"""Create and return a new XunitCell."""
return XunitCell(name=name)
def get_results(self) -> set[str]:
"""Get a set of results for each added xunit."""
return {str(xunit) for xunit in self.__xunits.values()}
def get_xunits(self) -> set[str]:
"""Get a set of xunits added to this row."""
return list(sorted(self.__xunits.keys()))
class XunitList(GObject.GObject, Gio.ListModel):
"""A list of XunitRows 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 an XunitList."""
super().__init__(runid=runid)
self.__xunits = set()
rows = {}
for row in self.do_query(sql).fetchall():
self.do_parse(rows, row)
self.__xunits.add(row["xunit"])
self.__items = sorted(rows.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 XunitRow.__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) -> XunitRow | None:
"""Get a specific item on the list."""
return self.__items[n] if n < self.n_items else None
def do_parse(self, rows: dict[XunitRow], row: sqlite3.Row) -> None:
"""Parse a sqlite3.Row and add it to the rows dict."""
def do_query(self, sql: sqlite.Connection) -> sqlite3.Cursor:
"""Query the database."""
return sql("SELECT name AS xunit FROM xunits WHERE runid=?",
self.runid)
def get_xunits(self) -> list[str]:
"""Get a list of xunits attached to this xfstests run."""
return sorted(self.__xunits)
HIDDEN_PROPERTIES = {"CPUS", "HOST_OPTIONS", "LOAD_FACTOR", "MEM_KB",
"NUMA_NODES", "OVL_LOWER", "OVL_UPPER", "OVL_WORK",
"PLATFORM", "SECTION", "SWAP_KB", "TIME_FACTOR"}
class PropertyValue(XunitCell):
"""A single Property for a specific Xunit."""
key = GObject.Property(type=str)
value = GObject.Property(type=str)
def __str__(self) -> str:
"""Get a string representation of this Property."""
return f"{self.key} = {self.value}"
class Property(XunitRow):
"""Collects one property across multiple xunits."""
def do_make_xunit(self, name: str, key: str, value: str) -> PropertyValue:
"""Add a PropertyValue to the Property."""
return PropertyValue(name=name, key=key, value=value)
def all_same_value(self) -> bool:
"""Check if all the xunits have the same value."""
return len(self.get_results()) == 1 and len(self.get_xunits()) > 1
def get_value(self) -> str | None:
"""Get the value of this row if all xunits have the same value."""
if self.all_same_value():
return self[self.get_xunits()[0]].value
class PropertyList(XunitList):
"""A list of Properties for a specific Xfstests Run."""
environment = GObject.Property(type=Gio.ListStore)
def __init__(self, sql: sqlite.Connection, runid: int) -> None:
"""Initialize an XunitList."""
super().__init__(sql=sql, runid=runid)
self.environment = Gio.ListStore()
self.environment.append(self)
def do_query(self, sql: sqlite.Connection) -> sqlite3.Cursor:
"""Query the database for properties."""
return sql("""SELECT xunit, key, value FROM xunit_properties_view
WHERE runid=?""", self.runid)
def do_parse(self, rows: dict[Property], row: sqlite3.Cursor) -> None:
"""Parse the data in the row and add it to the rows dict."""
property = rows.setdefault(row["key"], Property(row["key"]))
property.add_xunit(row["xunit"], row["key"], row["value"])
def get_environment(self) -> dict[str, str]:
"""Get a dictionary of 'environment' properties."""
return {row.name: row.get_value() for row in self
if row.name not in HIDDEN_PROPERTIES and row.all_same_value()}
class PropertyFilter(Gtk.Filter):
"""A filter for Properties."""
def do_get_strictness(self) -> Gtk.FilterMatch:
"""Get the strictness of the filter."""
return Gtk.FilterMatch.SOME
def do_match(self, property: Property) -> bool:
"""Check if a property matches the filter."""
return property.name not in HIDDEN_PROPERTIES \
and not property.all_same_value()
class TestResult(XunitCell):
"""The results for a single TestCase with a specific Xunit."""
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)
def __str__(self) -> str:
"""Get a string representation of this TestResult."""
return self.status
class TestCase(XunitRow):
"""Collects results for a single TestCase with multiple Xunits."""
def do_make_xunit(self, name: str, status: str, time: int,
message: str | None, stdout: str | None,
stderr: str | None) -> TestResult:
"""Add an xunit result to the TestCase."""
return TestResult(name=name, status=status, time=time,
message=("" if message is None else message),
stdout=("" if stdout is None else stdout),
stderr=("" if stderr is None else stderr))
class TestCaseList(XunitList):
"""A list of TestCases for a specific Xfstests Run."""
def do_query(self, sql: sqlite.Connection) -> sqlite3.Cursor:
"""Query the database for testcase results."""
return sql("""SELECT testcase, xunit, status, time,
message, stdout, stderr
FROM testcases_view WHERE runid=?""", self.runid)
def do_parse(self, rows: dict[TestCase], row: sqlite3.Cursor) -> None:
"""Parse the data in the row and add it to the rows dict."""
testcase = rows.setdefault(row["testcase"], TestCase(row["testcase"]))
testcase.add_xunit(row["xunit"], row["status"], row["time"],
row["message"], row["stdout"], row["stderr"])
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(XunitCell):
"""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(XunitRow):
"""Collects values for each summary field with multiple Xunits."""
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 do_make_xunit(self, name: str, value: int, unit: str) -> SummaryValue:
"""Add an xunit summary to the Summary."""
return SummaryValue(name=name, value=value, unit=unit)
class SummaryList(XunitList):
"""A list summarizing the results of a specific Xfstests Run."""
def do_query(self, sql: sqlite.Connection) -> sqlite3.Cursor:
"""Query the database for xunit summaries."""
return sql("""SELECT name AS xunit, passed, failed, skipped, time
FROM xunits_view WHERE runid=?""", self.runid)
def do_parse(self, rows: dict[Summary], row: sqlite3.Row) -> None:
"""Parse the data in the row and add it to the rows dict."""
for field in ["passed", "failed", "skipped", "time"]:
summary = rows.setdefault(field, Summary(field))
summary.add_xunit(row["xunit"], row[field],
"second" if field == "time" else "testcase")

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

@ -0,0 +1,221 @@
# Copyright 2023 (c) Anna Schumaker.
"""Our TestCase row widgets and factory."""
import typing
from gi.repository import GObject
from gi.repository import Gtk
from . import model
from . import tree
STYLES = {"passed": "success", "failed": "error",
"skipped": "warning", "time": "accent"}
class Factory(Gtk.SignalListItemFactory):
"""Create Gtk.Inscriptions for each Gtk.ListItem."""
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.Inscription(xalign=0.5, nat_chars=18)
child.props.text_overflow = Gtk.InscriptionOverflow.ELLIPSIZE_END
child.add_css_class("numeric")
self.do_setup(child)
listitem.set_child(child)
def __bind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Bind a ListItem to the child widget."""
self.do_bind(listitem.get_item(), listitem.get_child())
def __unbind(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
"""Unbind a ListItem from the child widget."""
self.do_unbind(listitem.get_item(), listitem.get_child())
listitem.get_child().set_text(None)
def __teardown(self, factory: typing.Self, listitem: Gtk.ListItem) -> None:
self.do_teardown(listitem.get_child())
listitem.set_child(None)
def do_setup(self, child: Gtk.Inscription) -> None:
"""Extra factory-specific setup for the child widget."""
def do_bind(self, row: model.XunitRow, child: Gtk.Inscription) -> None:
"""Extra factory-specific binding work for the child widget."""
def do_unbind(self, row: model.XunitRow, child: Gtk.Inscription) -> None:
"""Extra factory-specific unbinding work for the child widget."""
def do_teardown(self, child: Gtk.Inscription) -> None:
"""Extra factory-specific teardown for the child widget."""
class LabelFactory(Factory):
"""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)
def do_setup(self, child: Gtk.Inscription) -> None:
"""Set up a ListItem child widget."""
LabelFactory.group.add_widget(child)
def do_bind(self, row: model.XunitRow, child: Gtk.Inscription) -> None:
"""Bind a ListItem to the child widget."""
text = row.get_property(self.property)
if style := STYLES.get(text):
child.add_css_class(style)
child.set_text(text)
def do_unbind(self, row: model.XunitRow, child: Gtk.Inscription) -> None:
"""Unbind a ListItem from the child widget."""
for style in STYLES.values():
child.remove_css_class(style)
def do_teardown(self, child: Gtk.Inscription) -> None:
"""Clean up a ListItem child widget."""
if child is not None:
LabelFactory.group.remove_widget(child)
class EnvironmentFactory(Factory):
"""Factory for Environment property columns."""
property = GObject.Property(type=str)
def __init__(self, *, property: str):
"""Initialize our Environment Factory."""
super().__init__(property=property)
def do_bind(self, row: model.PropertyList, child: Gtk.Inscription) -> None:
"""Bind an Environment property to the child widget."""
text = row.get_environment()[self.property]
child.set_xalign(0)
child.set_text(text)
child.set_tooltip_text(text)
class XunitFactory(Factory):
"""Factory base class for Xunit columns."""
xunit = GObject.Property(type=str)
def __init__(self, *, xunit: str):
"""Initialize our Xunit Factory."""
super().__init__(xunit=xunit)
class PropertyFactory(XunitFactory):
"""Factory for making property widgets."""
def do_bind(self, row: model.TestCase, child: Gtk.Inscription) -> None:
"""Bind a ListItem to the child widget."""
property = row[self.xunit]
child.set_text(property.value)
child.set_tooltip_text(property.value)
class ResultFactory(XunitFactory):
"""Factory for making test result widgets."""
def __clicked(self, click: Gtk.GestureClick, n_press:
int, x: float, y: float, row: model.TestCase) -> None:
if (result := row[self.xunit]) is not None:
if len(result.stdout) > 0 or len(result.stderr) > 0:
self.emit("show-messages", row.name, self.xunit,
result.stdout, result.stderr)
def do_setup(self, child: Gtk.Inscription) -> None:
"""Set up click handling on the child widget."""
child.click = Gtk.GestureClick()
child.add_controller(child.click)
def do_bind(self, row: model.TestCase, child: Gtk.Inscription) -> None:
"""Bind a ListItem to the child widget."""
if (result := row[self.xunit]) is None:
return
if (text := result.status) == "passed":
text = f"{result.time} seconds"
child.set_text(text)
child.set_tooltip_text(result.message.lstrip(" -"))
child.get_parent().add_css_class(result.status)
child.click.connect("released", self.__clicked, row)
def do_unbind(self, row: model.TestCase, child: Gtk.Inscription) -> None:
"""Unbind a ListItem from the child widget."""
if (result := row[self.xunit]) is not None:
child.get_parent().remove_css_class(result.status)
child.click.disconnect_by_func(self.__clicked)
def do_teardown(self, child: Gtk.Inscription) -> None:
"""Clean up the GestureClick."""
child.remove_controller(child.click)
setattr(child, "click", None)
@GObject.Signal(arg_types=(str, str, str, str))
def show_messages(self, testcase: str, xunit: str,
stdout: str, stderr: str) -> None:
"""Show the selected messages to the user."""
class SummaryFactory(XunitFactory):
"""Factory for making test summary widgets."""
def do_bind(self, row: model.Summary, child: Gtk.Inscription) -> None:
"""Bind a ListItem to the child widget."""
result = row[self.xunit]
child.set_text(str(result))
child.add_css_class(STYLES[row.name])
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]

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

@ -0,0 +1,346 @@
# Copyright 2023 (c) Anna Schumaker.
"""A view widget used to display our TestCaseModel."""
import re
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Adw
from .model import PropertyList
from .model import PropertyFilter
from .model import TestCaseList
from .model import TestCaseFilter
from .model import SummaryList
from .model import XunitList
from . import button
from . import row
class XunitView(Gtk.ScrolledWindow):
"""Our XunitView base class."""
filtermodel = GObject.Property(type=Gtk.FilterListModel)
firstcol = GObject.Property(type=Gtk.ColumnViewColumn)
def __init__(self, title: str, **kwargs):
"""Initialize an XunitView."""
factory = row.LabelFactory("name")
super().__init__(filtermodel=Gtk.FilterListModel(),
firstcol=Gtk.ColumnViewColumn(title=title,
factory=factory),
child=Gtk.ColumnView(model=Gtk.NoSelection(),
show_row_separators=True,
show_column_separators=True,
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:
"""Make an XunitFactory for the given xunit."""
return row.XunitFactory(xunit=xunit)
@GObject.Property(type=XunitList)
def model(self) -> XunitList | None:
"""Get the XunitList shown by the XunitView."""
return self.filtermodel.props.model
@model.setter
def model(self, new: XunitList) -> None:
for col in list(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.firstcol)
for xunit in new.get_xunits():
col = Gtk.ColumnViewColumn(title=xunit, expand=True,
factory=self.do_make_factory(xunit))
self.props.child.append_column(col)
class EnvironmentView(XunitView):
"""Displays our Environment properties to the user."""
def __init__(self):
"""Initialize an EnvironmentView."""
super().__init__("environment", visible=False)
self.firstcol = None
self.props.child.add_css_class("data-table")
def do_make_factory(self, property: str) -> row.EnvironmentFactory:
"""Make a new EnvironmentFactory instance."""
return row.EnvironmentFactory(property=property)
@GObject.Property(type=Gio.ListModel)
def model(self) -> Gio.ListModel | None:
"""Get the ListModel shown by the EnvironmentView."""
return self.filtermodel.props.model
@model.setter
def model(self, new: Gio.ListModel) -> None:
for col in list(self.props.child.get_columns()):
self.props.child.remove_column(col)
self.filtermodel.props.model = new
self.props.visible = new is not None
if new is not None:
environ = new[0].get_environment()
for prop in environ.keys():
col = Gtk.ColumnViewColumn(title=prop, expand=True,
factory=self.do_make_factory(prop))
self.props.child.append_column(col)
class PropertyView(XunitView):
"""Displays our PropertyList model to the user."""
def __init__(self):
"""Initialize a PropertyView."""
super().__init__("property", vscrollbar_policy=Gtk.PolicyType.NEVER)
self.filtermodel.set_filter(PropertyFilter())
self.props.child.add_css_class("data-table")
def do_make_factory(self, xunit: str) -> row.PropertyFactory:
"""Make a new PropertyFactory instance."""
return row.PropertyFactory(xunit=xunit)
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(XunitView):
"""Displays our TestCaseList model to the user."""
filterbuttons = GObject.Property(type=FilterButtons)
def __init__(self):
"""Initialize a TestCaseView."""
super().__init__("testcase", filterbuttons=FilterButtons())
self.filtermodel.props.filter = TestCaseFilter()
for prop in ["passed", "skipped", "failure"]:
self.filterbuttons.bind_property(prop,
self.filtermodel.props.filter,
prop)
self.props.child.set_vexpand(True)
def __show_messages(self, factory: row.ResultFactory, testcase: str,
xunit: str, stdout: str, stderr: str) -> None:
self.emit("show-messages", testcase, xunit, stdout, stderr)
def do_make_factory(self, xunit: str) -> row.ResultFactory:
"""Make a new ResultFactory instance."""
factory = row.ResultFactory(xunit=xunit)
factory.connect("show-messages", self.__show_messages)
return factory
@GObject.Signal(arg_types=(str, str, str, str))
def show_messages(self, testcase: str, xunit: str,
stdout: str, stderr: str) -> None:
"""Signal that the user wants to inspect stdout and stderr messages."""
class MessageView(Gtk.Box):
"""A view for displaying a multiline test result message."""
title = GObject.Property(type=str)
def __init__(self, title: str):
"""Initialize a MessageView."""
super().__init__(title=title, orientation=Gtk.Orientation.VERTICAL)
self._label = Gtk.Label(label=self.title, margin_top=6)
self._textview = Gtk.TextView(monospace=True, editable=False)
self.append(self._label)
self.append(Gtk.Separator())
self.append(Gtk.ScrolledWindow(child=self._textview, vexpand=True))
self._label.add_css_class("large-title")
self.add_css_class("view")
def detect_diff(self, text: str) -> bool:
"""Detect if the given text looks like a diff."""
in_file = out_file = counts = changed = False
for line in text.split("\n"):
if re.match(r"^\+\+\+", line):
in_file = True
elif re.match(r"^---", line):
out_file = True
elif re.match(r"^@@(.*?)@@", line):
counts = True
elif re.match(r"^[\+| |-](.*?)", line):
changed = True
return in_file and out_file and counts and changed
def markup_diff(self, text: str) -> str:
"""Add Pango markup to the input string."""
if re.match(r"^\++(.*?)", text):
return f"<span color='#26a269'>{text}</span>"
elif re.match(r"^-+(.*?)", text):
return f"<span color='#c01c28'>{text}</span>"
elif re.match(r"^@@(.*?)@@", text):
return f"<span color='#1c71d8'>{text}</span>"
elif re.match(r"^ (.*?)", text):
return f"<span color='#77767b'>{text}</span>"
return text
@GObject.Property(type=str)
def text(self) -> str:
"""Get the text displayed in the view."""
buffer = self._textview.props.buffer
return buffer.get_text(buffer.get_start_iter(),
buffer.get_end_iter(), True)
@text.setter
def text(self, new_text: str) -> None:
buffer = self._textview.props.buffer
if self.detect_diff(new_text):
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
for i, line in enumerate(new_text.split("\n")):
text = self.markup_diff(line)
text = f"\n{text}" if i > 0 else text
buffer.insert_markup(buffer.get_end_iter(), text, len(text))
else:
buffer.set_text(new_text)
class MessagesView(Gtk.Box):
"""A view for displaying stdout and stderr messages."""
testcase = GObject.Property(type=str)
xunit = GObject.Property(type=str)
stdout = GObject.Property(type=str)
stderr = GObject.Property(type=str)
def __init__(self):
"""Initialize a MessagesView."""
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="done"))
self._title = Adw.WindowTitle()
self._stdout = MessageView("stdout")
self._stderr = MessageView("stderr")
self.bind_property("testcase", self._title, "title")
self.bind_property("xunit", self._title, "subtitle")
self.bind_property("stdout", self._stdout, "text")
self.bind_property("stderr", self._stderr, "text")
self._back.connect("clicked", self.__back_clicked)
self.append(Gtk.CenterBox(start_widget=self._back,
center_widget=self._title))
self.append(Gtk.Paned(start_child=self._stdout,
end_child=self._stderr, vexpand=True))
self.get_first_child().add_css_class("toolbar")
self._back.add_css_class("suggested-action")
self._back.add_css_class("pill")
self.add_css_class("card")
def __back_clicked(self, button: Gtk.Button) -> None:
self.emit("go-back")
@GObject.Signal
def go_back(self) -> None:
"""Signal that the user wants to go back."""
class SummaryView(XunitView):
"""Displays our SummaryList model to the user."""
def __init__(self):
"""Initialize a SummaryView."""
super().__init__("summary", vscrollbar_policy=Gtk.PolicyType.NEVER)
self.props.child.add_css_class("data-table")
def do_make_factory(self, xunit: str) -> row.SummaryFactory:
"""Make a new SummaryFactory instance."""
return row.SummaryFactory(xunit=xunit)
class XfstestsView(Gtk.Box):
"""A widget to display the results of an Xfstests runs."""
environment = GObject.Property(type=Gio.ListModel)
properties = GObject.Property(type=PropertyList)
model = GObject.Property(type=TestCaseList)
summary = GObject.Property(type=SummaryList)
def __init__(self):
"""Initialize an XfstestsView."""
animation = Gtk.StackTransitionType.OVER_UP_DOWN
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self._environview = EnvironmentView()
self._propertyview = PropertyView()
self._testcaseview = TestCaseView()
self._messagesview = MessagesView()
self._stack = Gtk.Stack(transition_type=animation)
self._summaryview = SummaryView()
self.bind_property("environment", self._environview, "model")
self.bind_property("properties", self._propertyview, "model")
self.bind_property("model", self._testcaseview, "model")
self.bind_property("summary", self._summaryview, "model")
self._testcaseview.connect("show-messages", self.__show_messages)
self._messagesview.connect("go-back", self.__show_testcases)
self._stack.add_named(self._testcaseview, "testcases")
self._stack.add_named(self._messagesview, "messages")
self.append(self._environview)
self.append(Gtk.Separator())
self.append(self._propertyview)
self.append(Gtk.Separator())
self.append(self._stack)
self.append(Gtk.Separator())
self.append(self._summaryview)
def __show_messages(self, view: TestCaseView, testcase: str,
xunit: str, stdout: str, stderr: str) -> None:
self._messagesview.testcase = testcase
self._messagesview.xunit = xunit
self._messagesview.stdout = stdout
self._messagesview.stderr = stderr
self._stack.set_visible_child_name("messages")
def __show_testcases(self, view: MessagesView) -> None:
self._stack.set_visible_child_name("testcases")
@GObject.Property(type=FilterButtons)
def filterbuttons(self) -> FilterButtons:
"""Get the FilterButtons attached to the child TestCaseView."""
return self._testcaseview.filterbuttons

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

@ -0,0 +1,53 @@
# 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)
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."""
super().__init__(default_height=1000, default_width=1600,
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(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.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,33 @@
/* 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;
}
cell.failure:hover {
color: @error_fg_color;
background-color: shade(@error_bg_color, 1.1);
}

View File

@ -52,6 +52,8 @@ class Command(commands.Command):
if isinstance(args.tags, str):
filter.append("tag GLOB ?")
sql_args.append(args.tags)
else:
filter.append("tag IS NOT NULL")
if args.timestamp:
filter.append("timestamp GLOB ?")
@ -62,6 +64,8 @@ class Command(commands.Command):
if isinstance(args.xunits, str):
filter.append("xunit GLOB ?")
sql_args.append(args.xunits)
else:
filter.append("xunit IS NOT NULL")
if len(filter) > 0:
sql_where = " WHERE " + " AND ".join(filter) + " "

View File

@ -16,6 +16,8 @@ class Command(commands.Command):
self.parser.add_argument("device", metavar="device", nargs=1,
help="the test device used for"
"this xfstests run")
self.parser.add_argument("--terse", action="store_true",
help="output only the new runid")
def do_command(self, args: argparse.Namespace) -> None:
"""Create a new row in the xfstestsdb_runs table."""
@ -24,6 +26,9 @@ class Command(commands.Command):
datetime(timestamp, 'localtime') as timestamp""",
args.device[0])
row = cur.fetchone()
print(f"created run #{row['runid']}", end=" ")
print(f"with test device '{row['device']}'", end=" ")
print(f"[{row['timestamp']}]")
if args.terse:
print(row['runid'])
else:
print(f"created run #{row['runid']}", end=" ")
print(f"with test device '{row['device']}'", end=" ")
print(f"[{row['timestamp']}]")

View File

@ -0,0 +1,64 @@
/* Copyright 2023 (c) Anna Schumaker */
PRAGMA user_version = 2;
/*
* The original `cleanup_xunit_properties` trigger was very slow on
* large databases. We can do a few things to improve on it:
* 1. Add an index on the link_xunits_props table to make it easier
* to check if specific properties are still in use.
* 2. Rewrite the `cleanup_xunit_properties` trigger to only operate
* on the propid associated with the deleted xunit.
*/
CREATE INDEX link_xunit_props_propid_index ON link_xunit_props (propid);
DROP TRIGGER cleanup_xunit_properties;
CREATE TRIGGER cleanup_xunit_properties
AFTER DELETE ON link_xunit_props
BEGIN
DELETE FROM xunit_properties
WHERE (propid = OLD.propid)
AND NOT EXISTS (SELECT 1 FROM link_xunit_props
WHERE propid = xunit_properties.propid);
END;
/*
* The original `cleanup_testcase_messages` trigger was very slow. We can
* do a few things to improve upon it:
* 1. Add indexes on the testcases table to make it easier to check
* if specific messageids are still in use.
* 2. Rewrite the `cleanup_testcase_messages` trigger to only operate
* on the messageids associated with the deleted testcase.
*/
CREATE INDEX testcases_messageid_index ON testcases (messageid);
CREATE INDEX testcases_stdoutid_index ON testcases (stdoutid);
CREATE INDEX testcases_stderrid_index ON testcases (stderrid);
DROP TRIGGER cleanup_testcase_messages;
CREATE TRIGGER cleanup_testcase_messages
AFTER DELETE ON testcases
BEGIN
DELETE FROM messages
WHERE (messageid = OLD.messageid
OR messageid = OLD.stdoutid
OR messageid = OLD.stderrid)
AND NOT EXISTS
(SELECT 1 FROM testcases WHERE
messageid = messages.messageid
OR stdoutid = messages.messageid
OR stderrid = messages.messageid);
END;
/*
* Create a view on the xfstestsdb_runs to find garbage collection candidates.
*/
CREATE VIEW xfstests_gc_runs AS
SELECT runid
FROM xfstests_runs
WHERE NOT EXISTS (SELECT 1 FROM testcases
JOIN xunits USING (xunitid)
WHERE runid = xfstests_runs.runid)
OR (timestamp < datetime('now', '-180 days')
AND NOT EXISTS (SELECT 1 FROM tags
WHERE runid = xfstests_runs.runid));

View File

@ -9,7 +9,10 @@ import xdg.BaseDirectory
DATA_DIR = pathlib.Path(xdg.BaseDirectory.save_data_path("xfstestsdb"))
DATA_FILE = DATA_DIR / f"xfstestsdb{'-debug' if __debug__ else ''}.sqlite3"
DATABASE = ":memory:" if "unittest" in sys.modules else DATA_FILE
SQL_SCRIPT = pathlib.Path(__file__).parent / "xfstestsdb.sql"
SQL_SCRIPTS = pathlib.Path(__file__).parent / "scripts"
SQL_V1_SCRIPT = SQL_SCRIPTS / "xfstestsdb.sql"
SQL_V2_SCRIPT = SQL_SCRIPTS / "upgrade-v2.sql"
class Connection:
@ -22,11 +25,13 @@ class Connection:
self.sql.row_factory = sqlite3.Row
self.connected = True
self("PRAGMA foreign_keys = ON")
match self("PRAGMA user_version").fetchone()["user_version"]:
case 0:
with open(SQL_SCRIPT) as f:
self.sql.executescript(f.read())
self.sql.commit()
self.executescript(SQL_V1_SCRIPT)
self.executescript(SQL_V2_SCRIPT)
case 1:
self.executescript(SQL_V2_SCRIPT)
def __call__(self, statement: str,
*args, **kwargs) -> sqlite3.Cursor | None:
@ -63,3 +68,15 @@ class Connection:
return self.sql.executemany(statement, args)
except sqlite3.IntegrityError:
return None
def executescript(self, script: pathlib.Path) -> sqlite3.Cursor | None:
"""Execute a SQL script."""
if script.is_file():
with open(script) as f:
cur = self.sql.executescript(f.read())
self.sql.commit()
return cur
def vacuum(self) -> sqlite3.Cursor | None:
"""Vacuum the database."""
return self.sql.execute("VACUUM")

View File

@ -4,6 +4,7 @@ import argparse
from .. import commands
from .. import sqlite
from . import delete
from . import gc
from . import list
from . import properties
from . import read
@ -20,6 +21,7 @@ class Command(commands.Command):
help="xfstestsdb xunit commands")
self.subparser = self.parser.add_subparsers(title="xunit commands")
self.commands = {"delete": delete.Command(self.subparser, sql),
"gc": gc.Command(self.subparser, sql),
"list": list.Command(self.subparser, sql),
"properties": properties.Command(self.subparser, sql),
"read": read.Command(self.subparser, sql),

View File

@ -22,7 +22,7 @@ class Command(commands.Command):
def do_command(self, args: argparse.Namespace) -> None:
"""Delete a row from the xunits table."""
cur = self.sql("DELETE FROM xunits WHERE rowid=? and name=?",
cur = self.sql("DELETE FROM xunits WHERE runid=? and name=?",
args.runid[0], args.name[0])
if cur.rowcount == 0:
print(f"error: either run #{args.runid[0]} or "

29
xfstestsdb/xunit/gc.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright 2023 (c) Anna Schumaker.
"""The `xfstestsdb xunit gc` command."""
import argparse
from .. import commands
from .. import sqlite
class Command(commands.Command):
"""The `xfstestsdb xunit gc` command."""
def __init__(self, subparser: argparse.Action,
sql: sqlite.Connection) -> None:
"""Set up the xunit gc command."""
super().__init__(subparser, sql, "gc",
help="garbage collect xunit entries")
self.parser.add_argument("--dry-run", action="store_true",
help="do not remove the xunit entries")
def do_command(self, args: argparse.Namespace) -> None:
"""Clean up the xunit table."""
how = "would be" if args.dry_run else "has been"
cur = self.sql("""SELECT xunitid, runid, name FROM xunits
WHERE NOT EXISTS (SELECT 1 FROM testcases
WHERE xunitid = xunits.xunitid)""")
for row in cur.fetchall():
if not args.dry_run:
self.sql("DELETE FROM xunits WHERE xunitid=?", row['xunitid'])
print(f"run #{row['runid']} xunit '{row['name']}' {how} deleted")

View File

@ -39,7 +39,7 @@ class Command(commands.Command):
def elm_testcase(self, runid: int, xunit: str,
elm: xml.etree.ElementTree.Element,
properties: dict) -> tuple:
properties: dict, namespace: str) -> tuple:
"""Extract testcase data for an xml element."""
status = "passed"
message = None
@ -47,6 +47,8 @@ class Command(commands.Command):
stderr = None
for e in elm:
if e.tag.startswith(namespace):
e.tag = e.tag[len(namespace):]
match e.tag:
case "failure" | "skipped": status = e.tag
case "system-out": stdout = html.unescape(e.text)
@ -76,12 +78,20 @@ class Command(commands.Command):
xunitname = pathlib.Path(args.file[0].name).stem
root = xml.etree.ElementTree.parse(args.file[0]).getroot()
match = re.match(r"{.*?}", root.tag)
namespace = match.group(0) if match else ""
properties = {e.attrib["name"]: e.attrib["value"] for e in root[0]}
testcases = [self.elm_testcase(args.runid[0], xunitname,
elm, properties)
elm, properties, namespace)
for elm in root[1:]]
timestamp = datetime.datetime.fromisoformat(root.attrib["timestamp"])
timestamp = root.attrib.get("start_timestamp")
if timestamp is None:
timestamp = root.attrib["timestamp"]
if timestamp[-3] != ":":
timestamp = timestamp[:-2] + ":" + timestamp[-2:]
timestamp = datetime.datetime.fromisoformat(timestamp)
timestamp = timestamp.astimezone(dateutil.tz.tzutc())
timestamp = timestamp.replace(tzinfo=None)