Skip to content

Commit

Permalink
Makes generic XMLScanner util
Browse files Browse the repository at this point in the history
1. Add 'run_unit_test' to Remote
2. Add util/xml_scanner.py
3. Remove changes from orchestra/run.py

Signed-off-by: Vallari Agrawal <[email protected]>
  • Loading branch information
VallariAg committed Dec 11, 2022
1 parent bc47d28 commit a3af9a4
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 129 deletions.
5 changes: 3 additions & 2 deletions teuthology/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,9 @@ def __str__(self):
prefix += " ({label})".format(label=self.label)
if self.node:
prefix += " on {node}".format(node=self.node)
return "{prefix} with status {status}: '{message}'".format(
if self.exitstatus:
prefix += " with status {status}".format(status=self.exitstatus)
return "{prefix}: '{message}'".format(
prefix=prefix,
status=self.exitstatus,
message=self.message,
)
23 changes: 22 additions & 1 deletion teuthology/orchestra/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from teuthology.orchestra.opsys import OS
import teuthology.provision
from teuthology import misc
from teuthology.exceptions import CommandFailedError
from teuthology.exceptions import CommandFailedError, UnitTestError
from teuthology.util.xml_scanner import UnitTestScanner
from teuthology.misc import host_shortname
import errno
import time
Expand Down Expand Up @@ -511,6 +512,26 @@ def run(self, **kwargs):
r.remote = self
return r

def run_unit_test(self, unittest_xml=None, **kwargs):
try:
r = self.run(**kwargs)
except CommandFailedError as exc:
log.info("XML_DEBUG: CommandFailedError exception found.")
if unittest_xml:
error_msg = None
try:
error_msg = UnitTestScanner(client=self.ssh).get_error_msg(unittest_xml)
except Exception as scanner_exc:
log.exception(scanner_exc)
if error_msg:
raise UnitTestError(
exitstatus=exc.exitstatus, node=exc.node,
label=exc.label, message=error_msg
)
raise exc
r.remote = self
return r

def _sftp_put_file(self, local_path, remote_path):
"""
Use the paramiko.SFTPClient to put a file. Returns the remote filename.
Expand Down
129 changes: 3 additions & 126 deletions teuthology/orchestra/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
"""

import io
import os
import yaml
import collections

from paramiko import ChannelFile

Expand All @@ -15,11 +12,10 @@
import pipes
import logging
import shutil
from lxml import etree

from teuthology.contextutil import safe_while
from teuthology.exceptions import (CommandCrashedError, CommandFailedError,
ConnectionLostError, UnitTestError)
ConnectionLostError)

log = logging.getLogger(__name__)

Expand All @@ -38,13 +34,12 @@ class RemoteProcess(object):
# for orchestra.remote.Remote to place a backreference
'remote',
'label',
'unittest_xml',
]

deadlock_warning = "Using PIPE for %s without wait=False would deadlock"

def __init__(self, client, args, check_status=True, hostname=None,
label=None, timeout=None, wait=True, logger=None, cwd=None, unittest_xml=None):
label=None, timeout=None, wait=True, logger=None, cwd=None):
"""
Create the object. Does not initiate command execution.
Expand All @@ -63,8 +58,6 @@ def __init__(self, client, args, check_status=True, hostname=None,
:param logger: Alternative logger to use (optional)
:param cwd: Directory in which the command will be executed
(optional)
:param unittest_xml: Absolute path to unit-tests output XML file
(optional)
"""
self.client = client
self.args = args
Expand All @@ -91,7 +84,6 @@ def __init__(self, client, args, check_status=True, hostname=None,
self.returncode = self.exitstatus = None
self._wait = wait
self.logger = logger or log
self.unittest_xml = unittest_xml or ""

def execute(self):
"""
Expand Down Expand Up @@ -186,19 +178,6 @@ def _raise_for_status(self):
# signal; sadly SSH does not tell us which signal
raise CommandCrashedError(command=self.command)
if self.returncode != 0:
log.info("XML_DEBUG: self.unittest_xml " + self.unittest_xml)
if self.unittest_xml:
error_msg = None
try:
error_msg = UnitTestFailure().get_error_msg(self.unittest_xml, self.client)
except Exception as exc:
self.logger.exception(exc)
# self.logger.error('Unable to scan logs, exception occurred: {exc}'.format(exc=repr(exc)))
if error_msg:
raise UnitTestError(
exitstatus=self.returncode, node=self.hostname,
label=self.label, message=error_msg
)
raise CommandFailedError(
command=self.command, exitstatus=self.returncode,
node=self.hostname, label=self.label
Expand Down Expand Up @@ -242,106 +221,6 @@ def __repr__(self):
name=self.hostname,
)

class UnitTestFailure():
def __init__(self) -> None:
self.yaml_data = {}
self.client = None

def get_error_msg(self, xmlfile_path: str, client=None):
"""
Find error message in xml file.
If xmlfile_path is a directory, parse all xml files.
"""
if not xmlfile_path:
return "No XML file path was passed to process!"
self.client = client
error_message = None
log.info("XML_DEBUG: getting message...")

if xmlfile_path[-1] == "/": # directory
(_, stdout, _) = client.exec_command(f'ls -d {xmlfile_path}*.xml', timeout=200)
xml_files = stdout.read().decode().split('\n')
log.info("XML_DEBUG: xml_files are " + " ".join(xml_files))

for file in xml_files:
error = self._parse_xml(file)
if not error_message:
error_message = error
log.info("XML_DEBUG: Parsed all .xml files.")
elif os.path.splitext(xmlfile_path)[1] == ".xml": # xml file
error_message = self._parse_xml(xmlfile_path)

if error_message:
self.write_logs()
return error_message + ' Information store in remote/unittest_failures.yaml'
log.info("XML_DEBUG: no error_message")

def _parse_xml(self, xml_path: str):
"""
Load the XML file
and parse for failures and errors.
Returns information about first failure/error occurance.
"""

if not xml_path:
return None
try:
log.info("XML_DEBUG: open file " + xml_path)
# TODO: change to paramiko function
(_, stdout, _) = self.client.exec_command(f'cat {xml_path}', timeout=200)
if stdout:
tree = etree.parse(stdout)
log.info("XML_DEBUG: parsed.")
failed_testcases = tree.xpath('.//failure/.. | .//error/..')
if len(failed_testcases) == 0:
log.debug("No failures or errors found in unit test's output xml file.")
return None

error_data = collections.defaultdict(dict)
error_message = ""

for testcase in failed_testcases:
testcase_name = testcase.get("name", "test-name")
testcase_suitename = testcase.get("classname", "suite-name")
for child in testcase:
if child.tag in ['failure', 'error']:
fault_kind = child.tag
reason = child.get('message', 'NO MESSAGE FOUND IN XML FILE; CHECK LOGS.')
reason = reason[:reason.find('begin captured')] # remove captured logs/stdout
error_data[testcase_suitename][testcase_name] = {
"kind": fault_kind,
"message": reason,
}
if not error_message:
error_message = f'{fault_kind}: Test `{testcase_name}` of `{testcase_suitename}` because {reason}'

xml_filename = os.path.basename(xml_path)
self.yaml_data[xml_filename] = {
"xml_file": xml_path,
"num_of_failures": len(failed_testcases),
"failures": dict(error_data)
}

return error_message
else:
return f'XML output not found at `{str(xml_path)}`!'
except Exception as exc:
log.exception(exc)
raise Exception("Somthing went wrong while searching for error in XML file: " + repr(exc))

def write_logs(self):
yamlfile = "/home/ubuntu/cephtest/archive/unittest_failures.yaml"
if self.yaml_data:
log.info(self.yaml_data)
try:
sftp = self.client.open_sftp()
remote_yaml_file = sftp.open(yamlfile, "w")
yaml.safe_dump(self.yaml_data, remote_yaml_file, default_flow_style=False)
remote_yaml_file.close()
except Exception as exc:
log.exception(exc)
log.info("XML_DEBUG: write logs error: " + repr(exc))
log.info("XML_DEBUG: yaml_data is empty!")

class Raw(object):

Expand Down Expand Up @@ -515,7 +394,6 @@ def run(
quiet=False,
timeout=None,
cwd=None,
unittest_xml=None,
# omit_sudo is used by vstart_runner.py
omit_sudo=False
):
Expand Down Expand Up @@ -551,7 +429,6 @@ def run(
:param timeout: timeout value for args to complete on remote channel of
paramiko
:param cwd: Directory in which the command should be executed.
:param unittest_xml: Absolute path to unit-tests output XML file.
"""
try:
transport = client.get_transport()
Expand All @@ -569,7 +446,7 @@ def run(
log.info("Running command with timeout %d", timeout)
r = RemoteProcess(client, args, check_status=check_status, hostname=name,
label=label, timeout=timeout, wait=wait, logger=logger,
cwd=cwd, unittest_xml=unittest_xml)
cwd=cwd)
r.execute()
r.setup_stdin(stdin)
r.setup_output_stream(stderr, 'stderr', quiet)
Expand Down
128 changes: 128 additions & 0 deletions teuthology/util/xml_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os
import logging
import yaml
from collections import defaultdict
from lxml import etree

log = logging.Logger(__name__)


class XMLScanner():
def __init__(self, client=None, yaml_path=None) -> None:
self.yaml_data = []
self.yaml_path = yaml_path
self.client = client

def scan_all_files_in_dir(self, dir_path: str, ext: str):
"""
:param dir_path: Scan all files here. Example: /path/to/dir/
:param ext : Extension of files to scan. Example: "xml" or "log"
"""
(_, stdout, _) = self.client.exec_command(f'ls -d {dir_path}*.{ext}', timeout=200)
files = stdout.read().decode().split('\n')
log.info("XML_DEBUG: all file paths are " + " ".join(files))

errors = []
for fpath in files:
error_txt = self.scan_file(fpath)
if error_txt:
errors += [error_txt]

self.write_logs()
return errors

def scan_file(self, path):
if not path:
return None
(_, stdout, _) = self.client.exec_command(f'cat {path}', timeout=200)
if stdout:
xml_tree = etree.parse(stdout)
error_txt, error_data = self.get_error(xml_tree)
if error_data:
error_data["xml_file"] = path
self.yaml_data += [error_data]
return error_txt
log.debug(f'XML output not found at `{str(path)}`!')

def get_error(self):
# defined in inherited classes
pass

def write_logs(self):
yamlfile = self.yaml_path
if self.yaml_data:
try:
sftp = self.client.open_sftp()
remote_yaml_file = sftp.open(yamlfile, "w")
yaml.safe_dump(self.yaml_data, remote_yaml_file, default_flow_style=False)
remote_yaml_file.close()
except Exception as exc:
log.exception(exc)
log.info("XML_DEBUG: write logs error: " + repr(exc))
else:
log.info("XML_DEBUG: yaml_data is empty!")


class UnitTestScanner(XMLScanner):
def __init__(self, client=None) -> None:
super().__init__(client)
self.yaml_path = "/home/ubuntu/cephtest/archive/unittest_failures.yaml"

def get_error_msg(self, xml_path):
try:
if xml_path[-1] == "/":
errors = self.scan_all_files_in_dir(xml_path, "xml")
if errors:
return errors[0]
log.debug("UnitTestScanner: No error found in XML output")
return None
else:
error = self.scan_file(xml_path)
self.write_logs()
return error
except Exception as exc:
log.exception(exc)
log.info("XML_DEBUG: get_error_msg: " + repr(exc))

def get_error(self, xml_tree):
"""
Returns message of first error found.
And stores info of all errors in yaml_data.
"""
root = xml_tree.getroot()
if int(root.get("failures", -1)) == 0 and int(root.get("errors", -1)) == 0:
log.debug("No failures or errors in unit test.")
return None, None

failed_testcases = xml_tree.xpath('.//failure/.. | .//error/..')
if len(failed_testcases) == 0:
log.debug("No failures/errors tags found in xml file.")
return None, None

error_txt, error_data = "", defaultdict(list)
for testcase in failed_testcases:
testcase_name = testcase.get("name", "test-name")
testcase_suitename = testcase.get("classname", "suite-name")
for child in testcase:
if child.tag in ['failure', 'error']:
fault_kind = child.tag
reason = child.get('message', 'No message found in xml output, check logs.')
short_reason = reason[:reason.find('begin captured')] # remove traceback
error_data[testcase_suitename] += [{
"kind": fault_kind,
"testcase": testcase_name,
"message": reason,
}]
if not error_txt:
error_txt = f'{fault_kind.upper()}: Test `{testcase_name}` of `{testcase_suitename}`. Reason: {short_reason}.'

return error_txt, { "failed_testsuites": dict(error_data), "num_of_failures": len(failed_testcases) }


class ValgrindScanner(XMLScanner):
def __init__(self, client=None) -> None:
super().__init__(client)
self.yaml_path = "/home/ubuntu/cephtest/archive/valgrind.yaml"

def get_error(self, xml_tree):
pass

0 comments on commit a3af9a4

Please sign in to comment.