From 1996f3b798bc983f5f471c01b429c0c5a8f58532 Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Fri, 10 Feb 2023 15:27:37 -0500 Subject: [PATCH] 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 --- tests/test_show.py | 185 +++++++++++++++++++++++++++++++++++++++++ xfstestsdb/__init__.py | 2 + xfstestsdb/show.py | 103 +++++++++++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 tests/test_show.py create mode 100644 xfstestsdb/show.py diff --git a/tests/test_show.py b/tests/test_show.py new file mode 100644 index 0000000..9c02cd3 --- /dev/null +++ b/tests/test_show.py @@ -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") diff --git a/xfstestsdb/__init__.py b/xfstestsdb/__init__.py index ff1e609..2ed9490 100644 --- a/xfstestsdb/__init__.py +++ b/xfstestsdb/__init__.py @@ -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), diff --git a/xfstestsdb/show.py b/xfstestsdb/show.py new file mode 100644 index 0000000..91fe98b --- /dev/null +++ b/xfstestsdb/show.py @@ -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)