Source code for planemo.galaxy.test.structures

"""Utilities for reasoning about Galaxy test results."""

import os
import shlex
from typing import NamedTuple
from xml.etree import ElementTree as ET

from planemo.io import error
from planemo.test.results import StructuredData as BaseStructuredData

NO_STRUCTURED_FILE = (
    "Warning: Problem with target Galaxy, it did not "
    "produce a structured test results file [%s] - summary "
    "information and planemo reports will be incorrect."
)


[docs]class GalaxyTestCommand: """Abstraction around building a ``run_tests.sh`` command for Galaxy tests.""" def __init__( self, html_report_file, xunit_report_file, structured_report_file, failed=False, installed=False, ): self.html_report_file = html_report_file self.xunit_report_file = xunit_report_file self.structured_report_file = structured_report_file self.failed = failed self.installed = installed
[docs] def build(self): xunit_report_file = self.xunit_report_file sd_report_file = self.structured_report_file cmd = "./run_tests.sh $COMMON_STARTUP_ARGS --report_file %s" % shlex.quote(self.html_report_file) if xunit_report_file: cmd += " --xunit_report_file %s" % shlex.quote(xunit_report_file) if sd_report_file: cmd += " --structured_data_report_file %s" % shlex.quote(sd_report_file) if self.installed: cmd += " -installed" elif self.failed: sd = StructuredData(sd_report_file) tests = " ".join(sd.failed_ids) cmd += " %s" % tests else: cmd += " functional.test_toolbox" return cmd
[docs]class StructuredData(BaseStructuredData): """Abstraction around Galaxy's structured test data output.""" def __init__(self, json_path): if not json_path or not os.path.exists(json_path): error(NO_STRUCTURED_FILE % json_path) super().__init__(json_path)
[docs] def merge_xunit(self, xunit_root): self.has_details = True xunit_attrib = xunit_root.attrib num_tests = int(xunit_attrib.get("tests", 0)) num_failures = int(xunit_attrib.get("failures", 0)) num_errors = int(xunit_attrib.get("errors", 0)) num_skips = int(xunit_attrib.get("skips", 0)) summary = dict( num_tests=num_tests, num_failures=num_failures, num_errors=num_errors, num_skips=num_skips, ) self.structured_data["summary"] = summary for testcase_el in xunit_t_elements_from_root(xunit_root): test = case_id(testcase_el) test_data = self.structured_data_by_id.get(test.id) if not test_data: continue problem_el = None for problem_type in ["skip", "failure", "error"]: problem_el = testcase_el.find(problem_type) if problem_el is not None: break if problem_el is not None: status = problem_el.tag test_data["problem_type"] = problem_el.attrib["type"] test_data["problem_log"] = problem_el.text else: status = "success" test_data["status"] = status
[docs]class GalaxyTestResults: """Class that combine the test-centric xunit output with the Galaxy centric structured data output - and abstracts away the difference (someday). """ def __init__( self, output_json_path, output_xml_path, output_html_path, exit_code, ): self.output_html_path = output_html_path sd = StructuredData(output_json_path) self.sd = sd self.structured_data = sd.structured_data self.structured_data_tests = sd.structured_data_tests self.structured_data_by_id = sd.structured_data_by_id self.xunit_tree = parse_xunit_report(output_xml_path) sd.merge_xunit(self._xunit_root) self.sd.set_exit_code(exit_code) self.sd.read_summary() self.sd.update() @property def exit_code(self): return self.sd.exit_code @property def has_details(self): return self.sd.has_details @property def num_tests(self): return self.sd.num_tests @property def num_problems(self): return self.sd.num_problems @property def _xunit_root(self): return self.xunit_tree.getroot() @property def all_tests_passed(self): return self.sd.num_problems == 0 @property def xunit_testcase_elements(self): return xunit_t_elements_from_root(self._xunit_root)
[docs]def xunit_t_elements_from_root(xunit_root): yield from find_cases(xunit_root)
[docs]def parse_xunit_report(xunit_report_path): return ET.parse(xunit_report_path)
[docs]def find_cases(xunit_root): return xunit_root.findall("testcase")
[docs]def case_id(testcase_el=None, raw_id=None): if raw_id is None: assert testcase_el is not None name_raw = testcase_el.attrib["name"] if "TestForTool_" in name_raw: raw_id = name_raw else: class_name = testcase_el.attrib["classname"] raw_id = f"{class_name}.{name_raw}" name = None num = None if "TestForTool_" in raw_id: tool_and_num = raw_id.split("TestForTool_")[-1] if ".test_tool_" in tool_and_num: name, num_str = tool_and_num.split(".test_tool_", 1) num = _parse_num(num_str) # Tempted to but something human friendly in here like # num + 1 - but then it doesn't match HTML report. else: name = tool_and_num else: name = raw_id return TestId(name, num, raw_id)
def _parse_num(num_str): try: num = int(num_str) except ValueError: num = None return num
[docs]class TestId(NamedTuple): name: str num: int id: str @property def label(self): if self.num is not None: return f"{self.name}[{self.num}]" else: return self.id