Compare commits

...

5 Commits

Author SHA1 Message Date
Anna Schumaker 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
Anna Schumaker 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
Anna Schumaker 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
Anna Schumaker 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
Anna Schumaker 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
13 changed files with 81 additions and 28 deletions

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())

View File

@ -8,6 +8,8 @@ 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."""
@ -31,24 +33,26 @@ class TestGC(unittest.TestCase):
mock_stdout.seek(0)
mock_stdout.truncate(0)
def test_init(self):
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)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_empty(self, mock_stdout: io.StringIO):
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()
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_no_testcases(self, mock_stdout: io.StringIO):
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)
@ -61,9 +65,10 @@ class TestGC(unittest.TestCase):
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [1])
mock_vacuum.assert_called()
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_expired(self, mock_stdout: io.StringIO):
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)])
@ -87,9 +92,10 @@ class TestGC(unittest.TestCase):
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [2])
mock_vacuum.assert_called()
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_expired_tagged(self, mock_stdout: io.StringIO):
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)])
@ -109,9 +115,10 @@ class TestGC(unittest.TestCase):
cur = self.xfstestsdb.sql("SELECT runid FROM xfstests_runs")
self.assertListEqual([row['runid'] for row in cur.fetchall()], [1])
mock_vacuum.assert_called()
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_gc_dry_run(self, mock_stdout: io.StringIO):
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"])
@ -121,3 +128,4 @@ class TestGC(unittest.TestCase):
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()

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

@ -114,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

@ -37,16 +37,18 @@ class TestXfstestsdb(unittest.TestCase):
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):

View File

@ -24,7 +24,8 @@ 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")
@ -55,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."""

View File

@ -25,3 +25,10 @@ class Command(commands.Command):
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()

View File

@ -100,17 +100,19 @@ class Command(commands.Command):
def do_command(self, args: argparse.Namespace) -> None:
"""Run the Gtk UI."""
app_args = []
args.app_args = []
if args.runid is not None:
if self.sql("SELECT 1 FROM xfstests_runs WHERE runid=?",
args.runid).fetchone():
app_args.append(f"runid={args.runid}")
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:
app_args.append("show-sidebar")
args.app_args.append("show-sidebar")
Application(self.sql).run(app_args)
def do_done(self, args: argparse.Namespace) -> None:
"""Run the application outside of a transaction."""
Application(self.sql).run(args.app_args)

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

@ -76,3 +76,7 @@ class Connection:
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")