#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import json
import os
try:
# Python 3
import urllib.request, urllib.parse, urllib.error
url_quote_func = urllib.parse.quote
except:
# Fallback to Python 2
import urllib
url_quote_func = urllib.quote
from xml.etree import ElementTree
from collections import namedtuple
from aztest.common import get_module_name_from_xml_filename, ScanResult
from aztest.errors import InvalidUseError, RunnerReturnCodes
AggregateResult = namedtuple("AggregateResult", ("tests", "failures", "disabled", "errors", "success_rate", "duration"))
def xml_contains_failure(xml_filename):
try:
with open(xml_filename, 'r') as fx:
tree = ElementTree.parse(fx)
testsuites = tree.getroot()
if int(testsuites.attrib['failures']) + int(testsuites.attrib['errors']) > 0:
return True
except:
pass
return False
def get_scan_results_from_json(json_path):
"""
Gathers ScanResult information from a JSON file.
:param json_path: path to the JSON file to read
:return: list of ScanResults with the appropriate data or an empty list if file has nothing in it
"""
scan_results = []
try:
with open(json_path, 'r') as f:
scan_results_json = json.load(f)
except IOError:
return scan_results # If no file exists, we just move on
if 'scan_results' in scan_results_json:
for scan_result in scan_results_json['scan_results']:
scan_results.append(ScanResult(path=scan_result['path'], xml_path=scan_result['xml_path'],
return_code=scan_result['return_code'], error_msg=scan_result['error_msg']))
return scan_results
def get_scan_result_from_module(xml_path):
"""
Gathers ScanResult information from an already existing XML file. It does two checks:
1. Check the testsuites element for an ErrorCode property, if present then set ScanResult to that error code
2. If there is no ErrorCode, determine if tests passed or failed based on attributes in testsuites
:param xml_path: path to the XML file to read
:return: ScanResult with the appropriate data or None if file cannot be read
"""
try:
with open(xml_path, 'r') as fx:
tree = ElementTree.parse(fx)
except IOError:
return None
testsuites = tree.getroot()
if testsuites is None:
return None
# Default to success code
return_code = 0
# Search for the ErrorCode property
properties = testsuites.find("properties")
if properties is not None:
for property in properties.findall("property"):
if property.get('name') == "ErrorCode":
return_code = int(property.get('value'))
# If no error code yet, check for failures
if not return_code:
failures = int(testsuites.get('failures') or 0)
if failures:
return_code = 1
path = get_module_name_from_xml_filename(xml_path)
error_msg = RunnerReturnCodes.to_string(return_code)
return ScanResult(path=path, xml_path=xml_path, return_code=return_code, error_msg=error_msg)
def aggregate_module(xml_path):
""" Compute the aggregate test results for a single module's output.
:param xml_path:
:return: AggregateResult
"""
try:
with open(xml_path, 'r') as fx:
tree = ElementTree.parse(fx)
except IOError:
return AggregateResult(tests=0, failures=0, disabled=0, errors=1, success_rate=0.0, duration=0.0)
testsuites = tree.getroot()
if testsuites is None:
return None
tests = int(testsuites.attrib.get('tests') or 0)
failures = int(testsuites.attrib.get('failures') or 0)
disabled = int(testsuites.attrib.get('disabled') or 0)
errors = int(testsuites.attrib.get('errors') or 0)
duration = float(testsuites.attrib.get('time') or 0.0)
success_rate = (tests - failures) / float(tests) if tests else 0.0
return AggregateResult(tests=tests, failures=failures, disabled=disabled, errors=errors, success_rate=success_rate,
duration=duration)
def aggregate_testsuite(testsuite):
""" Compute aggregate results for a single test suite (ElemTree node)
:param testsuite: ElemTree XML node for a testsuite
:return: AggregateResult
"""
if testsuite is None:
return None
tests = int(testsuite.attrib.get('tests') or 0)
failures = int(testsuite.attrib.get('failures') or 0)
disabled = int(testsuite.attrib.get('disabled') or 0)
errors = int(testsuite.attrib.get('errors') or 0)
duration = float(testsuite.attrib.get('time') or 0.0)
success_rate = (tests - failures) / float(tests) if tests else 0.0
return AggregateResult(tests=tests, failures=failures, disabled=disabled, errors=errors, success_rate=success_rate,
duration=duration)
def aggregate_results(scan_results):
""" Compute aggregate results for all of the test runs.
:param scan_results:
:return: AggregateResult
"""
tests = 0
failures = 0
disabled = 0
errors = 0
duration = 0.0
for result in scan_results:
if result.return_code not in [RunnerReturnCodes.TESTS_SUCCEEDED, RunnerReturnCodes.TESTS_FAILED,
RunnerReturnCodes.MODULE_SKIPPED]:
errors += 1
continue
if result.return_code == RunnerReturnCodes.MODULE_SKIPPED:
continue
module_agg = aggregate_module(result.xml_path)
if module_agg:
tests += module_agg.tests
failures += module_agg.failures
disabled += module_agg.disabled
errors += module_agg.errors
duration += module_agg.duration
else:
errors += 1
success_rate = (tests - failures) / float(tests) if tests else 0.0
return AggregateResult(tests=tests, failures=failures, disabled=disabled, errors=errors, success_rate=success_rate,
duration=duration)
def get_stylesheet():
""" Get the inline stylesheet used for the report. """
css = """
.fail {
background-color:orange;
}
.error {
background-color:red;
}
.module_stub {
background-color:magenta;
}
table {
border: 1px solid black;
}
tr.header {
background-color:#AAA;
}
tr.testsuite {
background-color:#CCC;
}
td {
padding: 5px;
border: 1px solid black;
}
td.testcase {
padding-left: 50px;
}
div.module {
margin-top: 20px;
margin-bottom: 5px;
font-weight: bold;
font-size: 20px;
}
"""
return css
def write_module(scan_result, f, failures_only):
""" Write HTML report for a single module.
:param scan_result: ScanResult for module
:param f: open file handle to write report
:param failures_only: When True, only writes failures. Otherwise, writes all results.
"""
def has_failures(testcase):
failures = testcase.findall('failure')
return len(failures) > 0
def has_errors(testcase):
errors = testcase.findall('error')
return len(errors) > 0
def newline_to_br(s):
x = s.replace("\n", "
")
return x
try:
# skip modules with missing XML files
if not os.path.isfile(scan_result.xml_path):
return
# parse module XML output
with open(scan_result.xml_path, 'r') as fx:
tree = ElementTree.parse(fx)
f.write('
Test | Result | Duration | |
{} | |||
{test} | {result} | {duration} msec | |
{} |