Files
impala/lib/python/impala_py_lib/jenkins/generate_junitxml.py
Joe McDonnell 2b550634d2 IMPALA-11952 (part 2): Fix print function syntax
Python 3 now treats print as a function and requires
the parenthesis in invocation.

print "Hello World!"
is now:
print("Hello World!")

This fixes all locations to use the function
invocation. This is more complicated when the output
is being redirected to a file or when avoiding the
usual newline.

print >> sys.stderr , "Hello World!"
is now:
print("Hello World!", file=sys.stderr)

To support this properly and guarantee equivalent behavior
between python 2 and python 3, all files that use print
now add this import:
from __future__ import print_function

This also fixes random flake8 issues that intersect with
the changes.

Testing:
 - check-python-syntax.sh shows no errors related to print

Change-Id: Ib634958369ad777a41e72d80c8053b74384ac351
Reviewed-on: http://gerrit.cloudera.org:8080/19552
Reviewed-by: Joe McDonnell <joemcdonnell@cloudera.com>
Reviewed-by: Michael Smith <michael.smith@cloudera.com>
Tested-by: Michael Smith <michael.smith@cloudera.com>
2023-02-28 17:11:50 +00:00

274 lines
9.8 KiB
Python
Executable File

#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
A script for generating arbitrary junit XML reports while building Impala.
These files will be consumed by jenkins.impala.io to generate reports for
easier triaging of build and setup errors.
"""
from __future__ import print_function
import argparse
import codecs
import errno
import os
import textwrap
from xml.dom import minidom
from xml.etree import ElementTree as ET
from datetime import datetime as dt
IMPALA_HOME = os.getenv('IMPALA_HOME', '.')
SCRIPT_NAME, _ = os.path.splitext(os.path.basename(__file__))
JUNITXML_LOGDIR = os.path.join(os.getenv("IMPALA_LOGS_DIR", "."), 'extra_junit_xml_logs')
class JunitReport(object):
"""A Junit XML style report parseable by Jenkins for reporting build status.
Generally, a caller who invokes this script from bash doesn't need to do
more than supply the necessary command line parameters. The JunitReport
class is instantiated using those initial inputs, and a timestamped XML
file is output to the $IMPALA_HOME/logs/extra_junit_xml_logs/.
Log files are timestamped, so they will not overwrite previous files containing
output of the same step.
For use from within a python script (must be invoked with impala-python), an
example might look like:
>>> from impala_py_lib.jenkins.generate_junitxml import JunitReport
>>> report = JunitReport(phase='load_data', step='load_hbase', error_msg='oops')
>>> report.tofile()
For now, the class does not support adding more than one step (analogous to a
test case) to the same phase (analogous to a test suite). Each report should
be unique for a given junit XML file. This may be enhanced at some point.
"""
def __init__(self, phase, step, error_msg=None, stdout=None, stderr=None,
elapsed_time=0):
self.root_element = None
self.testsuite_element = None
self.testcase_element = None
self.phase = phase
self.step = step
self.error_msg = error_msg
self.stdout = stdout
self.stderr = stderr
self.elapsed_time = elapsed_time
self.utc_time = dt.utcnow()
self.create_root_element()
self.add_testsuite_element()
self.add_testcase_element()
if self.error_msg is not None:
self.set_error()
if self.stdout is not None:
self.add_output('out', self.stdout)
if self.stderr is not None:
self.add_output('err', self.stderr)
def create_root_element(self):
"""Create the testsuites root element."""
self.root_element = ET.Element("testsuites")
self.root_element.set("time", "{0:.1f}".format(float(self.elapsed_time)))
self.root_element.set("tests", "1")
self.root_element.set("failures", "0")
self.root_element.set("errors", "0")
def add_testsuite_element(self):
"""Create the testsuite element."""
self.testsuite_element = ET.SubElement(self.root_element, "testsuite")
self.testsuite_element.set("name", "{name}.{phase}.{step}".format(
name=SCRIPT_NAME, phase=self.phase, step=self.step))
self.testsuite_element.set(
"timestamp", "{ts}+00:00".format(ts=self.utc_time.strftime('%Y-%m-%d %H:%M:%S')))
self.testsuite_element.set("disabled", "0")
self.testsuite_element.set("errors", "0")
self.testsuite_element.set("failures", "0")
self.testsuite_element.set("skipped", "0")
self.testsuite_element.set("tests", "1")
self.testsuite_element.set("time", "0")
self.testsuite_element.set("file", "None")
self.testsuite_element.set("log", "None")
self.testsuite_element.set("url", "None")
def add_testcase_element(self):
"""Create the testcase element."""
self.testcase_element = ET.SubElement(self.testsuite_element, "testcase")
self.testcase_element.set("classname", "{name}.{phase}".format(
name=SCRIPT_NAME, phase=self.phase))
self.testcase_element.set("name", self.step)
def set_error(self):
"""Set an error msg if the step failed, and increment necessary error attributes."""
error = ET.SubElement(self.testcase_element, "error")
error.set("message", self.error_msg)
error.set("type", "error")
self.testsuite_element.set("errors", "1")
self.root_element.set("errors", "1")
def add_output(self, output_type, file_or_string):
"""
Add stdout or stderr content to testcase element.
Args:
output_type: [string] either out or err
file_or_string: a path to a file containing the content, or a plain string
"""
output = ET.SubElement(self.testcase_element,
"system-{output_type}".format(output_type=output_type))
output.text = JunitReport.get_xml_content(file_or_string)
def to_file(self, junitxml_logdir=JUNITXML_LOGDIR):
"""
Create a timestamped XML report file.
Args:
junitxml_logdir: path to directory where the file will be created
Return:
junit_log_file: path to the generated file
"""
# The equivalent of mkdir -p
try:
os.makedirs(junitxml_logdir)
except OSError as e:
if e.errno == errno.EEXIST and os.path.isdir(junitxml_logdir):
pass
else:
raise
filename = '{name}.{ts}.xml'.format(
name=self.testsuite_element.attrib['name'],
ts=self.utc_time.strftime('%Y%m%d_%H_%M_%S')
)
junit_log_file = os.path.join(junitxml_logdir, filename)
with codecs.open(junit_log_file, encoding="UTF-8", mode='w') as f:
f.write(unicode(self))
return junit_log_file
@staticmethod
def get_xml_content(file_or_string=None):
"""
Derive additional content for the XML report.
If the supplied parameter is the path to a file, the contents will be inserted
into the XML report. If the parameter is just plain string, use that as the
content for the report. For a file or a string passed in on the commandline,
this assumes it could contain Unicode content and converts it to a Unicode
object.
Args:
file_or_string: a path to a file, or a plain string
Returns:
content as a unicode object
"""
if file_or_string is None:
content = u''
elif os.path.exists(file_or_string):
with codecs.open(file_or_string, encoding="UTF-8", mode='r') as f:
content = f.read()
else:
# This is a string passed in on the command line. Make sure to return it as
# a unicode string.
content = unicode(file_or_string, encoding="UTF-8")
return content
def __unicode__(self):
"""
Generate and return a pretty-printable XML unicode string
"""
root_node_unicode = ET.tostring(self.root_element)
root_node_dom = minidom.parseString(root_node_unicode)
return root_node_dom.toprettyxml(indent=' ' * 4)
def get_options():
"""Parse and return command line options."""
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# Required options
parser.add_argument("--phase",
default="buildall",
help="General build phase or script.")
parser.add_argument("--step",
required=True,
help=textwrap.dedent(
"""Specific build step or child script being run.
Each step must be unique for the given build phase.""")
)
parser.add_argument("-t", "--time",
type=float,
default=0,
help="If known, the elapsed time in seconds for this step.")
parser.add_argument("--stdout",
help=textwrap.dedent(
"""Standard output to include in the XML report. Can be
either a string or the path to a file.""")
)
parser.add_argument("--stderr",
help=textwrap.dedent(
"""Standard error to include in the XML report. Can be
either a string or the path to a file.""")
)
parser.add_argument("--error",
help=textwrap.dedent(
"""If specified, the XML report will mark this as an error.
This should be a brief explanation for the error.""")
)
return parser.parse_args()
def main():
"""
Create a report for each invocation of the script, and output the results
of the test case to an XML file within $IMPALA_HOME/logs/extra_junit_xml_logs.
The log file name will use "phase" and "step" values provided on the command
line to structure the report. The XML report filename will follow the form:
junitxml_logger.<phase>.<step>.<time_stamp>.xml
Phase can be repeated in a given test run, but the step leaf node, which is
equivalent to a "test case", must be unique within each phase.
"""
options = get_options()
junit_report = JunitReport(phase=options.phase,
step=options.step,
error_msg=options.error,
stdout=options.stdout,
stderr=options.stderr,
elapsed_time=options.time)
junit_log_file = junit_report.to_file()
print("Generated: {0}".format(junit_log_file))
if "__main__" == __name__:
main()