xfstestsdb: Create the `xfstestsdb show` command

This command generates a table from the testcases that have been added
to a single run so the results of each testcase can be seen
side-by-side.

Implements: #9 (`xfstestsdb show`)
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
This commit is contained in:
Anna Schumaker 2023-02-10 15:27:37 -05:00
parent a9a2c2bf7d
commit 1996f3b798
3 changed files with 290 additions and 0 deletions

185
tests/test_show.py Normal file
View File

@ -0,0 +1,185 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests the `xfstestsdb testcase show` command."""
import errno
import io
import unittest
import unittest.mock
import xfstestsdb.show
import tests.xunit
class TestTestCaseShow(unittest.TestCase):
"""Tests the `xfstestsdb testcase show` command."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.show = self.xfstestsdb.commands["show"]
def setup_run(self, mock_stdout: io.StringIO):
"""Set up runs in the database and clear stdout."""
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"])
mock_stdout.seek(0)
mock_stdout.truncate(0)
def test_init(self):
"""Check that the show command was set up properly."""
self.assertIsInstance(xfstestsdb.show.TestCaseTable(["col"]),
xfstestsdb.table.Table)
self.assertIsInstance(self.show, xfstestsdb.commands.Command)
self.assertIsInstance(self.show, xfstestsdb.show.Command)
self.assertEqual(self.xfstestsdb.subparser.choices["show"],
self.show.parser)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_empty(self, mock_stdout: io.StringIO):
"""Test printing out an empty list."""
self.setup_run(mock_stdout)
self.xfstestsdb.sql("DELETE FROM testcases")
self.assertEqual(mock_stdout.getvalue(), "")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_run(self, mock_stdout: io.StringIO):
"""Test showing a run with no filters."""
self.setup_run(mock_stdout)
print()
self.xfstestsdb.run(["show", "1", "--color", "none"])
self.assertEqual(mock_stdout.getvalue(),
"""
+----------+---------+---------+
| testcase | test-1 | test-2 |
+----------+---------+---------+
| test/01 | passed | passed |
| test/02 | skipped | skipped |
| test/03 | skipped | skipped |
| test/04 | passed | passed |
| test/05 | passed | passed |
| test/06 | passed | passed |
| test/07 | skipped | skipped |
| test/08 | passed | passed |
| test/09 | failure | failure |
| test/10 | passed | passed |
+----------+---------+---------+
""")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_filter_testcase(self, mock_stdout: io.StringIO):
"""Test showing a run filtering by testcase."""
self.setup_run(mock_stdout)
print()
self.xfstestsdb.run(["show", "1", "--testcase", "test/0[45]",
"--color", "none"])
self.assertEqual(mock_stdout.getvalue(),
"""
+----------+--------+--------+
| testcase | test-1 | test-2 |
+----------+--------+--------+
| test/04 | passed | passed |
| test/05 | passed | passed |
+----------+--------+--------+
""")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_filter_passed(self, mock_stdout: io.StringIO):
"""Test showing a run filtering for passing tests."""
self.setup_run(mock_stdout)
print()
self.xfstestsdb.run(["show", "1", "--passed", "--color", "none"])
self.assertEqual(mock_stdout.getvalue(),
"""
+----------+--------+--------+
| testcase | test-1 | test-2 |
+----------+--------+--------+
| test/01 | passed | passed |
| test/04 | passed | passed |
| test/05 | passed | passed |
| test/06 | passed | passed |
| test/08 | passed | passed |
| test/10 | passed | passed |
+----------+--------+--------+
""")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_filter_skipped(self, mock_stdout: io.StringIO):
"""Test showing a run filtering for skipped testss."""
self.setup_run(mock_stdout)
print()
self.xfstestsdb.run(["show", "1", "--skipped", "--color", "none"])
self.assertEqual(mock_stdout.getvalue(),
"""
+----------+---------+---------+
| testcase | test-1 | test-2 |
+----------+---------+---------+
| test/02 | skipped | skipped |
| test/03 | skipped | skipped |
| test/07 | skipped | skipped |
+----------+---------+---------+
""")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_filter_failure(self, mock_stdout: io.StringIO):
"""Test showing a run filtering for failing testss."""
self.setup_run(mock_stdout)
print()
self.xfstestsdb.run(["show", "1", "--failure", "--color", "none"])
self.assertEqual(mock_stdout.getvalue(),
"""
+----------+---------+---------+
| testcase | test-1 | test-2 |
+----------+---------+---------+
| test/09 | failure | failure |
+----------+---------+---------+
""")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_uneven_xunits(self, mock_stdout: io.StringIO):
"""Test showing a run where xunits have different testcase sets."""
self.setup_run(mock_stdout)
self.xfstestsdb.sql("DELETE FROM testcases WHERE "
"xunitid=? AND testcase=?", 1, "test/03")
self.xfstestsdb.sql("DELETE FROM testcases WHERE "
"xunitid=? AND testcase=?", 2, "test/08")
print()
self.xfstestsdb.run(["show", "1", "--color", "none"])
self.assertEqual(mock_stdout.getvalue(),
"""
+----------+---------+---------+
| testcase | test-1 | test-2 |
+----------+---------+---------+
| test/01 | passed | passed |
| test/02 | skipped | skipped |
| test/03 | | skipped |
| test/04 | passed | passed |
| test/05 | passed | passed |
| test/06 | passed | passed |
| test/07 | skipped | skipped |
| test/08 | passed | |
| test/09 | failure | failure |
| test/10 | passed | passed |
+----------+---------+---------+
""")
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
def test_show_error(self, mock_stderr: io.StringIO):
"""Test the `xfstestsdb show` command with invalid input."""
with self.assertRaises(SystemExit):
self.xfstestsdb.run(["testcase", "show"])
self.assertRegex(mock_stderr.getvalue(),
"error: the following arguments are required: runid")
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_show_enoent(self, mock_stdout: io.StringIO,
mock_stderr: io.StringIO):
"""Test the `xfstestsdb show` command with an invalid runid."""
self.setup_run(mock_stderr)
with self.assertRaises(SystemExit) as sys_exit:
self.xfstestsdb.run(["show", "2"])
self.assertEqual(sys_exit.exception.code, errno.ENOENT)
self.assertEqual(mock_stderr.getvalue(),
"error: run #2 does not exist\n")

View File

@ -6,6 +6,7 @@ from . import delete
from . import list
from . import new
from . import rename
from . import show
from . import tag
from . import testcase
from . import untag
@ -30,6 +31,7 @@ class Command:
"list": list.Command(self.subparser, self.sql),
"new": new.Command(self.subparser, self.sql),
"rename": rename.Command(self.subparser, self.sql),
"show": show.Command(self.subparser, self.sql),
"tag": tag.Command(self.subparser, self.sql),
"testcase": testcase.Command(self.subparser,
self.sql),

103
xfstestsdb/show.py Normal file
View File

@ -0,0 +1,103 @@
# Copyright 2023 (c) Anna Schumaker.
"""The `xfstestsdb testcase show` command."""
import argparse
import errno
import sys
from . import colors
from . import commands
from . import sqlite
from . import table
class TestCaseTable(table.Table):
"""A custom Table for coloring cells based on status."""
def do_format_cell(self, rownum: int, text: str, color: str,
**kwargs) -> str:
"""Set cell text color based on testcase status."""
match text.strip():
case "passed": color = "fg-passed"
case "skipped": color = "fg-skipped"
case "failure": color = "fg-failure"
case _: color = "fg-testcase"
return super().do_format_cell(rownum, text, color, **kwargs)
class Command(commands.Command):
"""The `xfstestsdb testcase show` command."""
def __init__(self, subparser: argparse.Action,
sql: sqlite.Connection) -> None:
"""Set up the testcase show command."""
super().__init__(subparser, sql, "show",
help="show an xfstests run")
self.parser.add_argument("runid", metavar="runid", nargs=1,
help="runid of the testcase to show")
self.parser.add_argument("--color", metavar="color", nargs="?",
choices=["light", "dark", "none"],
const=colors.get_default_colors(),
default=colors.get_default_colors(),
help="show with color output "
f"[default={colors.get_default_colors()}]")
self.parser.add_argument("--failure", dest="status",
action="append_const", const="failure",
help="show failing testcases")
self.parser.add_argument("--passed", dest="status",
action="append_const", const="passed",
help="show passing testcases")
self.parser.add_argument("--skipped", dest="status",
action="append_const", const="skipped",
help="show skipped testcases")
self.parser.add_argument("--testcase", metavar="testcase", nargs=1,
help="show testcases matching "
"the given pattern")
def do_command(self, args: argparse.Namespace) -> None:
"""Create a new row in the xfstestsdb_runs table."""
rows = self.sql("""SELECT name FROM xunits_view
WHERE runid=? ORDER BY name""",
args.runid[0]).fetchall()
if len(rows) == 0:
print(f"error: run #{args.runid[0]} does not exist",
file=sys.stderr)
sys.exit(errno.ENOENT)
xunits = [row["name"] for row in rows]
tbl = TestCaseTable(["testcase"] + xunits,
["r"] + ["l"] * len(rows), args.color)
select = [f"xunit{i}.status as xunit{i}" for i in range(len(xunits))]
query = [(f"SELECT testcase, {','.join(select)} FROM")]
query.append("(SELECT DISTINCT runid, testcase FROM testcases_view "
"WHERE runid=:runid) ")
sql_args = {"runid": args.runid[0]}
where = []
for i, xunit in enumerate(xunits):
xunit_select = "SELECT testcase, status FROM testcases_view " + \
f"WHERE runid=:runid AND xunit=:xunit{i}"
query.append(f"FULL JOIN ({xunit_select}) AS xunit{i} "
"USING (testcase)")
sql_args[f"xunit{i}"] = xunit
if args.status:
xunit_list = ",".join([f"xunit{i}" for i in range(len(xunits))])
for status in args.status:
where.append(f":{status} IN ({xunit_list})")
sql_args[status] = status
if args.testcase:
where.append("testcase GLOB :testcase")
sql_args["testcase"] = args.testcase[0]
if len(where) > 0:
query.append(f"WHERE {' AND '.join(where)}")
query.append("ORDER BY testcase")
cur = self.sql("\n".join(query), **sql_args)
for row in cur.fetchall():
tbl.add_row(*row)
if len(tbl.rows) > 0:
print(tbl)