# Licensed under a 3-clause BSD style license - see LICENSE.rst # STDLIB import contextlib from math import ceil import os import re # ASTROPY from astropy.utils.xml.writer import XMLWriter, xml_escape from astropy import online_docs_root # VO from astropy.io.votable import exceptions html_header = """ """ default_style = """ body { font-family: sans-serif } a { text-decoration: none } .highlight { color: red; font-weight: bold; text-decoration: underline; } .green { background-color: #ddffdd } .red { background-color: #ffdddd } .yellow { background-color: #ffffdd } tr:hover { background-color: #dddddd } table { border-width: 1px; border-spacing: 0px; border-style: solid; border-color: gray; border-collapse: collapse; background-color: white; padding: 5px; } table th { border-width: 1px; padding: 5px; border-style: solid; border-color: gray; } table td { border-width: 1px; padding: 5px; border-style: solid; border-color: gray; } """ @contextlib.contextmanager def make_html_header(w): w.write(html_header) with w.tag('html', xmlns="http://www.w3.org/1999/xhtml", lang="en-US"): with w.tag('head'): w.element('title', 'VO Validation results') w.element('style', default_style) with w.tag('body'): yield def write_source_line(w, line, nchar=0): part1 = xml_escape(line[:nchar].decode('utf-8')) char = xml_escape(line[nchar:nchar+1].decode('utf-8')) part2 = xml_escape(line[nchar+1:].decode('utf-8')) w.write(' ') w.write(part1) w.write(f'{char}') w.write(part2) w.write('\n\n') def write_warning(w, line, xml_lines): warning = exceptions.parse_vowarning(line) if not warning['is_something']: w.data(line) else: w.write(f"Line {warning['nline']:d}: ") if warning['warning']: w.write('{}: '.format( online_docs_root, warning['doc_url'], warning['warning'])) msg = warning['message'] if not isinstance(warning['message'], str): msg = msg.decode('utf-8') w.write(xml_escape(msg)) w.write('\n') if 1 <= warning['nline'] < len(xml_lines): write_source_line(w, xml_lines[warning['nline'] - 1], warning['nchar']) def write_votlint_warning(w, line, xml_lines): match = re.search(r"(WARNING|ERROR|INFO) \(l.(?P[0-9]+), c.(?P[0-9]+)\): (?P.*)", line) if match: w.write('Line {:d}: {}\n'.format( int(match.group('line')), xml_escape(match.group('rest')))) write_source_line( w, xml_lines[int(match.group('line')) - 1], int(match.group('column')) - 1) else: w.data(line) w.data('\n') def write_result(result): if 'network_error' in result and result['network_error'] is not None: return xml = result.get_xml_content() xml_lines = xml.splitlines() path = os.path.join(result.get_dirpath(), 'index.html') with open(path, 'w', encoding='utf-8') as fd: w = XMLWriter(fd) with make_html_header(w): with w.tag('p'): with w.tag('a', href='vo.xml'): w.data(result.url.decode('ascii')) w.element('hr') with w.tag('pre'): w._flush() for line in result['warnings']: write_warning(w, line, xml_lines) if result['xmllint'] is False: w.element('hr') w.element('p', 'xmllint results:') content = result['xmllint_content'] if not isinstance(content, str): content = content.decode('ascii') content = content.replace(result.get_dirpath() + '/', '') with w.tag('pre'): w.data(content) if 'votlint' in result: if result['votlint'] is False: w.element('hr') w.element('p', 'votlint results:') content = result['votlint_content'] if not isinstance(content, str): content = content.decode('ascii') with w.tag('pre'): w._flush() for line in content.splitlines(): write_votlint_warning(w, line, xml_lines) def write_result_row(w, result): with w.tag('tr'): with w.tag('td'): if ('network_error' in result and result['network_error'] is not None): w.data(result.url.decode('ascii')) else: w.element('a', result.url.decode('ascii'), href=f'{result.get_htmlpath()}/index.html') if 'network_error' in result and result['network_error'] is not None: w.element('td', str(result['network_error']), attrib={'class': 'red'}) w.element('td', '-') w.element('td', '-') w.element('td', '-') w.element('td', '-') else: w.element('td', '-', attrib={'class': 'green'}) if result['nexceptions']: cls = 'red' msg = 'Fatal' elif result['nwarnings']: cls = 'yellow' msg = str(result['nwarnings']) else: cls = 'green' msg = '-' w.element('td', msg, attrib={'class': cls}) msg = result['version'] if result['xmllint'] is None: cls = '' elif result['xmllint'] is False: cls = 'red' else: cls = 'green' w.element('td', msg, attrib={'class': cls}) if result['expected'] == 'good': cls = 'green' msg = '-' elif result['expected'] == 'broken': cls = 'red' msg = 'net' elif result['expected'] == 'incorrect': cls = 'yellow' msg = 'invalid' w.element('td', msg, attrib={'class': cls}) if 'votlint' in result: if result['votlint']: cls = 'green' msg = 'Passed' else: cls = 'red' msg = 'Failed' else: cls = '' msg = '?' w.element('td', msg, attrib={'class': cls}) def write_table(basename, name, results, root="results", chunk_size=500): def write_page_links(j): if npages <= 1: return with w.tag('center'): if j > 0: w.element('a', '<< ', href=f'{basename}_{j - 1:02d}.html') for i in range(npages): if i == j: w.data(str(i+1)) else: w.element( 'a', str(i+1), href=f'{basename}_{i:02d}.html') w.data(' ') if j < npages - 1: w.element('a', '>>', href=f'{basename}_{j + 1:02d}.html') npages = int(ceil(float(len(results)) / chunk_size)) for i, j in enumerate(range(0, max(len(results), 1), chunk_size)): subresults = results[j:j+chunk_size] path = os.path.join(root, f'{basename}_{i:02d}.html') with open(path, 'w', encoding='utf-8') as fd: w = XMLWriter(fd) with make_html_header(w): write_page_links(i) w.element('h2', name) with w.tag('table'): with w.tag('tr'): w.element('th', 'URL') w.element('th', 'Network') w.element('th', 'Warnings') w.element('th', 'Schema') w.element('th', 'Expected') w.element('th', 'votlint') for result in subresults: write_result_row(w, result) write_page_links(i) def add_subset(w, basename, name, subresults, inside=['p'], total=None): with w.tag('tr'): subresults = list(subresults) if total is None: total = len(subresults) if total == 0: # pragma: no cover percentage = 0.0 else: percentage = (float(len(subresults)) / total) with w.tag('td'): for element in inside: w.start(element) w.element('a', name, href=f'{basename}_00.html') for element in reversed(inside): w.end(element) numbers = f'{len(subresults):d} ({percentage:.2%})' with w.tag('td'): w.data(numbers) def write_index(subsets, results, root='results'): path = os.path.join(root, 'index.html') with open(path, 'w', encoding='utf-8') as fd: w = XMLWriter(fd) with make_html_header(w): w.element('h1', 'VO Validation results') with w.tag('table'): for subset in subsets: add_subset(w, *subset, total=len(results)) def write_index_table(root, basename, name, subresults, inside=None, total=None, chunk_size=500): if total is None: total = len(subresults) percentage = (float(len(subresults)) / total) numbers = f'{len(subresults):d} ({percentage:.2%})' write_table(basename, name + ' ' + numbers, subresults, root, chunk_size)