xunit: Create the xfstestsdb xunit read command

This command reads an xunit file generated by passing "-R xunit" to
xfstests `./check`. Multiple xunit files can be added to a single
xfstests run, and will be shown side-by-side in columns when printed
out.

Implements: #3 (`xfstestsdb xunit read`)
Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
This commit is contained in:
Anna Schumaker 2023-02-03 19:35:35 -05:00
parent 35faa3a781
commit 16399f375e
7 changed files with 249 additions and 2 deletions

5
tests/xunit/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# Copyright 2023 (c) Anna Schumaker.
"""Set up a path to the test xunit files."""
import pathlib
XUNIT_1 = pathlib.Path(__file__).parent / "test-1.xunit"

57
tests/xunit/test-1.xunit Normal file
View File

@ -0,0 +1,57 @@
<?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" >
<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,mountopt2"/>
<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/test"/>
<property name="TEST_DEV" value="/dev/vdb1"/>
<property name="SCRATCH_DEV" value="/dev/vdb2"/>
<property name="SCRATCH_MNT" value="/mnt/scratch"/>
<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>

94
tests/xunit/test_read.py Normal file
View File

@ -0,0 +1,94 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests the `xfstestsdb xunit read` command."""
import errno
import io
import unittest
import unittest.mock
import xfstestsdb.xunit.read
import tests.xunit
class TestXunitRead(unittest.TestCase):
"""Tests the `xfstestsdb xunit read` command."""
def setUp(self):
"""Set up common variables."""
self.xfstestsdb = xfstestsdb.Command()
self.xunit = self.xfstestsdb.commands["xunit"]
self.read = self.xunit.commands["read"]
def test_init(self):
"""Check that the xunit read command was set up properly."""
self.assertIsInstance(self.read, xfstestsdb.commands.Command)
self.assertIsInstance(self.read, xfstestsdb.xunit.read.Command)
self.assertEqual(self.xunit.subparser.choices["read"],
self.read.parser)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_read_file(self, mock_stdout: io.StringIO):
"""Test reading a file into the xfstestsdb_xunit database."""
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "1", str(tests.xunit.XUNIT_1)])
self.assertRegex(mock_stdout.getvalue(),
"added 'test-1' with 10 testcases to run #1")
row = self.xfstestsdb.sql("SELECT * FROM xunits_view").fetchone()
self.assertEqual(row["runid"], 1)
self.assertEqual(row["name"], "test-1")
self.assertEqual(row["hostname"], "myhost")
self.assertEqual(row["timestamp"], "2023-01-31 14:14:14")
self.assertEqual(row["tests"], 10)
self.assertEqual(row["failed"], 1)
self.assertEqual(row["skipped"], 3)
self.assertEqual(row["passed"], 6)
self.assertEqual(row["time"], 43)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_read_file_named(self, mock_stdout: io.StringIO):
"""Test reading a file with the --name= option."""
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "--name", "testname",
"1", str(tests.xunit.XUNIT_1)])
self.assertRegex(mock_stdout.getvalue(),
"added 'testname' with 10 testcases to run #1")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
def test_read_duplicate_file(self, mock_stdout: io.StringIO,
mock_stderr: io.StringIO):
"""Test reading a duplicate file into the xfstestsdb_xunit database."""
self.xfstestsdb.run(["new", "/dev/vda1"])
self.xfstestsdb.run(["xunit", "read", "1", str(tests.xunit.XUNIT_1)])
with self.assertRaises(SystemExit) as sys_exit:
self.xfstestsdb.run(["xunit", "read", "1",
str(tests.xunit.XUNIT_1)])
self.assertEqual(sys_exit.exception.code, errno.EEXIST)
self.assertRegex(mock_stderr.getvalue(),
"error: run #1 already has xunit file 'test-1'")
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
def test_read_error(self, mock_stderr: io.StringIO):
"""Test the `xfstestsdb xunit read` command with invalid input."""
with self.assertRaises(SystemExit):
self.xfstestsdb.run(["xunit", "read"])
self.assertRegex(mock_stderr.getvalue(),
"error: the following arguments are required: runid")
with self.assertRaises(SystemExit):
self.xfstestsdb.run(["xunit", "read", "1"])
self.assertRegex(mock_stderr.getvalue(),
"error: the following arguments are required: file")
@unittest.mock.patch("sys.stderr", new_callable=io.StringIO)
def test_read_enoent(self, mock_stderr: io.StringIO):
"""Test the `xfstestsdb xunit read` command with an invalid runid."""
with self.assertRaises(SystemExit) as sys_exit:
self.xfstestsdb.run(["xunit", "read", "2",
str(tests.xunit.XUNIT_1)])
self.assertEqual(sys_exit.exception.code, errno.ENOENT)
self.assertRegex(mock_stderr.getvalue(),
"error: run #2 does not exist")

View File

@ -1,5 +1,6 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests the `xfstestsdb xunit` command."""
import argparse
import io
import unittest
import unittest.mock
@ -18,6 +19,7 @@ class TestXunit(unittest.TestCase):
"""Check that the xunit command was set up properly."""
self.assertIsInstance(self.xunit, xfstestsdb.commands.Command)
self.assertIsInstance(self.xunit, xfstestsdb.xunit.Command)
self.assertIsInstance(self.xunit.subparser, argparse.Action)
self.assertEqual(self.xfstestsdb.subparser.choices["xunit"],
self.xunit.parser)
@ -26,4 +28,4 @@ class TestXunit(unittest.TestCase):
"""Test calling `xfstestdb xunit` with an empty argument list."""
self.xfstestsdb.run(["xunit"])
self.assertRegex(mock_stdout.getvalue(),
r"^usage: .*? xunit \[\-h\]")
r"^usage: .*? xunit \[\-h\] \{.*?} ...$")

View File

@ -38,3 +38,30 @@ CREATE TABLE tags (
ON DELETE CASCADE
ON UPDATE CASCADE
);
/************************************************
* *
* Table for managing xunit files *
* *
************************************************/
CREATE TABLE xunits (
xunitid INTEGER PRIMARY KEY,
runid INTEGER NOT NULL,
timestamp TIMESTAMP,
name TEXT NOT NULL,
hostname TEXT NOT NULL,
tests INTEGER NOT NULL,
failed INTEGER NOT NULL,
skipped INTEGER NOT NULL,
time INTEGER NOT NULL,
UNIQUE (runid, name),
FOREIGN KEY (runid) REFERENCES xfstests_runs (runid)
);
CREATE VIEW xunits_view AS
SELECT runid, name, hostname, tests, failed, skipped, time,
(tests - (skipped + failed)) as passed,
datetime(timestamp, 'localtime') as timestamp
FROM xunits;

View File

@ -3,6 +3,7 @@
import argparse
from .. import commands
from .. import sqlite
from . import read
class Command(commands.Command):
@ -12,7 +13,9 @@ class Command(commands.Command):
sql: sqlite.Connection) -> None:
"""Set up the xunit command."""
super().__init__(subparser, sql, "xunit",
help="xfstestsdb xunit operations")
help="xfstestsdb xunit commands")
self.subparser = self.parser.add_subparsers(title="xunit commands")
self.commands = {"read": read.Command(self.subparser, sql)}
def do_command(self, args: argparse.Namespace) -> None:
"""Print help text for the xunit subcommand."""

59
xfstestsdb/xunit/read.py Normal file
View File

@ -0,0 +1,59 @@
# Copyright 2023 (c) Anna Schumaker.
"""The `xfstestsdb xunit read` command."""
import argparse
import datetime
import dateutil.tz
import errno
import pathlib
import sys
import xml.etree.ElementTree
from .. import commands
from .. import sqlite
class Command(commands.Command):
"""The `xfstestsdb xunit read` command."""
def __init__(self, subparser: argparse.Action,
sql: sqlite.Connection) -> None:
"""Set up the xunit read command."""
super().__init__(subparser, sql, "read", help="read in an xunit file")
self.parser.add_argument("--name", metavar="name", nargs="?", type=str,
help="a name for the xunit file")
self.parser.add_argument("runid", metavar="runid", nargs=1, type=int,
help="runid of the xfstests run "
"associated with the file")
self.parser.add_argument("file", metavar="file", nargs=1,
type=argparse.FileType('r'),
help="path to the xunit file to be read")
def do_command(self, args: argparse.Namespace) -> None:
"""Read in an xunit file."""
if self.sql("SELECT rowid FROM xfstests_runs WHERE rowid=?",
args.runid[0]).fetchone() is None:
print(f"error: run #{args.runid[0]} does not exist",
file=sys.stderr)
sys.exit(errno.ENOENT)
if (xunitname := args.name) is None:
xunitname = pathlib.Path(args.file[0].name).stem
root = xml.etree.ElementTree.parse(args.file[0]).getroot()
timestamp = datetime.datetime.fromisoformat(root.attrib["timestamp"])
timestamp = timestamp.astimezone(dateutil.tz.tzutc())
timestamp = timestamp.replace(tzinfo=None)
cur = self.sql("""INSERT INTO xunits (runid, timestamp, name, hostname,
tests, failed, skipped, time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
args.runid[0], timestamp, xunitname,
root.attrib["hostname"], root.attrib["tests"],
root.attrib["failures"], root.attrib["skipped"],
root.attrib["time"])
if cur is None:
print(f"error: run #{args.runid[0]} already has "
f"xunit file '{xunitname}'")
sys.exit(errno.EEXIST)
print(f"added '{xunitname}' with {root.attrib['tests']} testcases "
f"to run #{args.runid[0]}")