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:
parent
35faa3a781
commit
16399f375e
5
tests/xunit/__init__.py
Normal file
5
tests/xunit/__init__.py
Normal 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
57
tests/xunit/test-1.xunit
Normal 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 "myfs" 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 '/dev/vdb2'
|
||||
]]>
|
||||
</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 '/mnt/scratch'
|
||||
]]>
|
||||
</system-err>
|
||||
|
||||
</testcase>
|
||||
<testcase classname="xfstests.global" name="test/10" time="10">
|
||||
</testcase>
|
||||
</testsuite>
|
94
tests/xunit/test_read.py
Normal file
94
tests/xunit/test_read.py
Normal 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")
|
|
@ -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\] \{.*?} ...$")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
59
xfstestsdb/xunit/read.py
Normal 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]}")
|
Loading…
Reference in New Issue
Block a user