diff --git a/tests/testcase/test_show.py b/tests/testcase/test_show.py new file mode 100644 index 0000000..8cff110 --- /dev/null +++ b/tests/testcase/test_show.py @@ -0,0 +1,170 @@ +# Copyright 2023 (c) Anna Schumaker. +"""Tests the `xfstestsdb testcase show` command.""" +import errno +import io +import unittest +import unittest.mock +import xfstestsdb.testcase.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.testcase = self.xfstestsdb.commands["testcase"] + self.show = self.testcase.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.sql("UPDATE messages SET message=? WHERE rowid=6", + "\n".join([f"stderr line {i}" for i in range(10)])) + + mock_stdout.seek(0) + mock_stdout.truncate(0) + + def test_init(self): + """Check that the testcase show command was set up properly.""" + self.assertIsInstance(self.show, xfstestsdb.commands.Command) + self.assertIsInstance(self.show, xfstestsdb.testcase.show.Command) + self.assertEqual(self.testcase.subparser.choices["show"], + self.show.parser) + + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_show_passing(self, mock_stdout: io.StringIO): + """Test showing a passing test case.""" + self.setup_run(mock_stdout) + self.xfstestsdb.run(["testcase", "show", "1", "test-1", "test/01", + "--color", "none"]) + self.assertEqual(mock_stdout.getvalue(), + "test/01: passed, 1 second\n") + + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_show_skipped(self, mock_stdout: io.StringIO): + """Test showing a skipped test case.""" + self.setup_run(mock_stdout) + print() + self.xfstestsdb.run(["testcase", "show", "1", "test-1", "test/02", + "--color", "none"]) + self.assertEqual(mock_stdout.getvalue(), + """ +test/02: skipped, 0 seconds: + skipped on $TEST_DEV +""") + + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_show_failure(self, mock_stdout: io.StringIO): + """Test showing a failing test case.""" + self.setup_run(mock_stdout) + print() + self.xfstestsdb.run(["testcase", "show", "1", "test-1", "test/09", + "--color", "none"]) + self.assertEqual(mock_stdout.getvalue(), + """ +test/09: failure, 9 seconds: + output mismatch (see somefile) + +text printed to system-out: + there was a problem with '$SCRATCH_DEV' + +text printed to system-err: + stderr line 0 + stderr line 1 + stderr line 2 + stderr line 3 + stderr line 4 + stderr line 5 + stderr line 6 + stderr line 7 + stderr line 8 + stderr line 9 +""") + + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_show_maxlines(self, mock_stdout: io.StringIO): + """Test showing a failing test case, with a line limit.""" + self.setup_run(mock_stdout) + print() + self.xfstestsdb.run(["testcase", "show", "1", "test-1", "test/09", + "--maxlines", "5", "--color", "none"]) + self.assertEqual(mock_stdout.getvalue(), + """ +test/09: failure, 9 seconds: + output mismatch (see somefile) + +text printed to system-out: + there was a problem with '$SCRATCH_DEV' + +text printed to system-err: + stderr line 0 + stderr line 1 + stderr line 2 + stderr line 3 + stderr line 4 + ... + [5 additional lines, see `--maxlines=`] +""") + + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_show_negative_maxlines(self, mock_stdout: io.StringIO): + """Test showing a failing test case, with a negative line limit.""" + self.setup_run(mock_stdout) + print() + self.xfstestsdb.run(["testcase", "show", "1", "test-1", "test/09", + "--maxlines", "-1", "--color", "none"]) + self.assertEqual(mock_stdout.getvalue(), + """ +test/09: failure, 9 seconds: + output mismatch (see somefile) + +text printed to system-out: + there was a problem with '$SCRATCH_DEV' + +text printed to system-err: + stderr line 0 + stderr line 1 + stderr line 2 + stderr line 3 + stderr line 4 + stderr line 5 + stderr line 6 + stderr line 7 + stderr line 8 + stderr line 9 +""") + + @unittest.mock.patch("sys.stderr", new_callable=io.StringIO) + def test_show_error(self, mock_stderr: io.StringIO): + """Test the `xfstestsdb testcase 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") + + with self.assertRaises(SystemExit): + self.xfstestsdb.run(["testcase", "show", "1"]) + self.assertRegex(mock_stderr.getvalue(), + "error: the following arguments are required: xunit") + + with self.assertRaises(SystemExit): + self.xfstestsdb.run(["testcase", "show", "1", "xunit-1"]) + self.assertRegex(mock_stderr.getvalue(), + "the following arguments are required: testcase") + + @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 `xfstestsdb testcase show` with an invalid runid.""" + self.setup_run(mock_stderr) + with self.assertRaises(SystemExit) as sys_exit: + self.xfstestsdb.run(["testcase", "show", "2", "test-1", "test/01"]) + + self.assertEqual(sys_exit.exception.code, errno.ENOENT) + self.assertEqual(mock_stderr.getvalue(), + "error: either run #2, xunit 'test-1', " + "or testcase 'test/01' do not exist\n") diff --git a/xfstestsdb/testcase/__init__.py b/xfstestsdb/testcase/__init__.py index 7b197c8..c9a10d9 100644 --- a/xfstestsdb/testcase/__init__.py +++ b/xfstestsdb/testcase/__init__.py @@ -4,6 +4,7 @@ import argparse from .. import commands from .. import sqlite from . import list +from . import show class Command(commands.Command): @@ -15,7 +16,8 @@ class Command(commands.Command): super().__init__(subparser, sql, "testcase", help="xfstestsdb testcase commands") self.subparser = self.parser.add_subparsers(title="testcase commands") - self.commands = {"list": list.Command(self.subparser, sql)} + self.commands = {"list": list.Command(self.subparser, sql), + "show": show.Command(self.subparser, sql)} def do_command(self, args: argparse.Namespace) -> None: """Print help text for the testcase subcommand.""" diff --git a/xfstestsdb/testcase/show.py b/xfstestsdb/testcase/show.py new file mode 100644 index 0000000..b1101d1 --- /dev/null +++ b/xfstestsdb/testcase/show.py @@ -0,0 +1,91 @@ +# 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 + + +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 detailed information" + "about a single testcase") + self.parser.add_argument("--color", metavar="color", nargs="?", + choices=["light", "dark", "none"], + const=colors.get_default_colors(), + default=colors.get_default_colors(), + help="show testcases with color output " + f"[default={colors.get_default_colors()}]") + self.parser.add_argument("--maxlines", metavar="n", type=int, + default=20, + help="show n lines of system-out " + "and system-err logs") + self.parser.add_argument("runid", metavar="runid", nargs=1, + help="runid of the testcase to show") + self.parser.add_argument("xunit", metavar="xunit", nargs=1, + help="xunit of the testcase to show") + self.parser.add_argument("testcase", metavar="testcase", nargs=1, + help="the testcase to show") + + def __print_output(self, colors: colors.ColorScheme, system_log: str, + output: str, maxlines: int, diff: bool = False) -> None: + print() + print(colors.format(f"text printed to {system_log}:", bold=True)) + + lines = output.split("\n") + maxlines = maxlines if maxlines > 0 else len(lines) + + for line in lines[:maxlines]: + color = f"diff-{line[0]}" if diff is True else None + print(" ", colors.format(line, color=color, dark="fg-dark")) + + if (remaining := len(lines) - maxlines) > 0: + ellipsis = " ..." if colors.name == "none" else " ․․․" + print(colors.format(ellipsis, color="fg-skipped", + bold=True, dark="fg-dark")) + print(colors.format(f" [{remaining} additional lines, " + "see `--maxlines=`]", color="fg-header", + bold=True, dark="fg-dark")) + + def do_command(self, args: argparse.Namespace) -> None: + """Show details about a specific testcase.""" + scheme = colors.get_colors(args.color) + cur = self.sql("""SELECT * FROM testcases_view + WHERE runid=? AND xunit=? AND testcase=?""", + args.runid[0], args.xunit[0], args.testcase[0]) + if (row := cur.fetchone()) is None: + print(f"error: either run #{args.runid[0]}, " + f"xunit '{args.xunit[0]}', or testcase '{args.testcase[0]}' " + "do not exist", file=sys.stderr) + sys.exit(errno.ENOENT) + + time = row['time'] + s_time = "" if time == 1 else "s" + status_color = f"fg-{row['status']}" + + print(scheme.format(f"{args.testcase[0]}:", bold=True), end=" ") + print(scheme.format(row["status"], color=status_color, + bold=True, dark="fg-dark"), end="") + print(scheme.format(f", {row['time']} second{s_time}", bold=True), + end="") + print(scheme.format(":" if row["message"] else "", bold=True)) + + if row["message"]: + print(" ", scheme.format(row["message"].lstrip("- "), + color=status_color, + bold=True, dark="fg-dark")) + + if row["stdout"]: + self.__print_output(scheme, "system-out", row["stdout"], + args.maxlines) + + if row["stderr"]: + self.__print_output(scheme, "system-err", row["stderr"], + args.maxlines, diff=True)