""" PYTEST_DONT_REWRITE """ import enum import logging from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from test.utils import GraphHelper from test.utils.dawg_manifest import ManifestEntry from test.utils.namespace import EARL, MF, RDFT from typing import ( TYPE_CHECKING, Callable, Dict, Generator, List, Optional, OrderedDict, Set, Tuple, TypeVar, cast, ) import _pytest.config.argparsing import pytest from pytest import Item from rdflib import RDF, BNode, Graph, Literal, URIRef from rdflib.namespace import DC, DOAP, FOAF from rdflib.plugins.stores.memory import Memory if TYPE_CHECKING: from _pytest.main import Session from _pytest.python import CallSpec2 from _pytest.reports import TestReport from _pytest.runner import CallInfo from pluggy._result import _Result logger = logging.getLogger(__name__) if TYPE_CHECKING: from rdflib.graph import _TripleType TEST_REPORTS_DIR = Path(__file__).parent.parent.parent / "test_reports" RDFLIB_PROJECT_IRI = URIRef("https://github.com/RDFLib/rdflib") @dataclass(eq=False) class EARLReport: """ This is a helper class for building an EARL report graph. """ reporter: "EARLReporter" output_file: Path assertions: List[Tuple[URIRef, Set["_TripleType"]]] = field( init=False, default_factory=list, repr=False ) def add_test_outcome( self, test_id: URIRef, outcome: URIRef, info: Optional[Literal] = None ): triples: Set["_TripleType"] = set() assertion = BNode(f"{test_id}") triples.add((assertion, RDF.type, EARL.Assertion)) triples.add((assertion, EARL.test, test_id)) triples.add((assertion, EARL.subject, self.reporter.project_iri)) triples.add((assertion, EARL.mode, EARL.automatic)) triples.add((assertion, EARL.assertedBy, self.reporter.assertor_iri)) result = BNode() triples.add((assertion, EARL.result, result)) triples.add((result, RDF.type, EARL.TestResult)) if self.reporter.add_datetime: triples.add((result, DC.date, self.reporter.asserted_at)) triples.add((result, EARL.outcome, outcome)) if info: triples.add((result, EARL.info, info)) self.assertions.append((test_id, triples)) def write(self) -> None: sorted_assertion = sorted( self.assertions, key=lambda assertion: f"{assertion[0]}" ) if sorted_assertion: logger.debug("sorted_assertion[-1] = %r", sorted_assertion[-1]) graph = self.reporter.make_report_graph() for assertion in sorted_assertion: for triple in assertion[1]: graph.add(triple) self.output_file.parent.mkdir(parents=True, exist_ok=True) graph.serialize(format="turtle", destination=self.output_file) def pytest_addoption(parser: _pytest.config.argparsing.Parser): group = parser.getgroup("terminal reporting") group.addoption( "--earl-output-dir", action="store", dest="earl_output_dir", metavar="dir", default=f"{TEST_REPORTS_DIR}", help="create EARL reports in the given directory.", ) group.addoption( "--earl-output-file", action="store", dest="earl_output_file", metavar="path", default=None, help="write EARL report in the given file", ) group.addoption( "--earl-output-suffix", action="store", dest="earl_output_suffix", metavar="path", default="-HEAD", help="suffix to use for prefix-defined test reports, defaults to '-HEAD'", ) group.addoption( "--earl-assertor-iri", action="store", dest="earl_assertor_iri", metavar="iri", default=RDFLIB_PROJECT_IRI, help=f"Set the EARL assertor IRI, defaults to the assertor homepage if that is set, or to {RDFLIB_PROJECT_IRI} if no homepage is set.", ) group.addoption( "--earl-assertor-homepage", action="store", dest="earl_assertor_homepage", metavar="URL", default=None, help="Set the EARL assertor homepage.", ) group.addoption( "--earl-assertor-name", action="store", dest="earl_assertor_name", metavar="name", default=None, help="Set the EARL assertor name.", ) group.addoption( "--earl-log-level", action="store", dest="earl_log_level", metavar="level", default=None, help="log level for EARL plugin itself", ) group.addoption( "--earl-add-datetime", action="store_true", dest="earl_add_datetime", default=False, help="Don't write datetime to result", ) FromT = TypeVar("FromT") ToT = TypeVar("ToT") def convert_optional( optional: Optional[FromT], converter: Callable[[FromT], ToT] ) -> Optional[ToT]: if optional is not None: return converter(optional) return None PYTEST_PLUGIN_NAME = "rdflib_earl_reporter" def pytest_configure(config: pytest.Config): if config.option.earl_log_level is not None: log_level = config.option.earl_log_level logger.setLevel(log_level) earl_reporter = EARLReporter( assertor_iri=URIRef(config.option.earl_assertor_iri), output_dir=Path(config.option.earl_output_dir), output_suffix=config.option.earl_output_suffix, output_file=convert_optional(config.option.earl_output_file, Path), assertor_homepage=convert_optional( config.option.earl_assertor_homepage, URIRef ), assertor_name=convert_optional(config.option.earl_assertor_name, Literal), add_datetime=config.option.earl_add_datetime, ) logger.debug("registering earl_reporter = %s", earl_reporter) config.pluginmanager.register(earl_reporter, PYTEST_PLUGIN_NAME) def pytest_unconfigure(config: pytest.Config): earl_reporter: Optional[EARLReporter] = config.pluginmanager.get_plugin( PYTEST_PLUGIN_NAME ) logger.debug("earl_reporter = %s", earl_reporter) if earl_reporter: config.pluginmanager.unregister(earl_reporter, PYTEST_PLUGIN_NAME) # https://docs.pytest.org/en/latest/reference.html#pytest.hookspec.pytest_runtest_protocol class TestResult(enum.Enum): PASS = enum.auto() FAIL = enum.auto() ERROR = enum.auto() SKIP = enum.auto() class TestReportHelper: @classmethod def get_rdf_test_uri(cls, report: "TestReport") -> Optional[URIRef]: return next( ( cast(URIRef, item[1]) for item in report.user_properties if item[0] == RDFT.Test ), None, ) @classmethod def get_manifest_entry(cls, report: "TestReport") -> Optional[ManifestEntry]: return next( ( cast(ManifestEntry, item[1]) for item in report.user_properties if item[0] == MF.ManifestEntry ), None, ) @dataclass(eq=False) class EARLReporter: """ This class is a pytest plugin that will write a EARL report with results for every pytest which has a rdf_test_uri parameter that is a string or an URIRef. """ assertor_iri: URIRef output_dir: Path output_suffix: str output_file: Optional[Path] = None assertor_name: Optional[Literal] = None assertor_homepage: Optional[URIRef] = None add_datetime: bool = True extra_triples: Set["_TripleType"] = field(default_factory=set) prefix_reports: Dict[str, EARLReport] = field(init=True, default_factory=dict) report: Optional[EARLReport] = field(init=True, default=None) def __post_init__(self) -> None: if self.assertor_homepage is not None: self.assertor_iri = self.assertor_homepage if self.assertor_name: self.extra_triples.add((self.assertor_iri, FOAF.name, self.assertor_name)) if self.assertor_homepage: self.extra_triples.add( (self.assertor_iri, FOAF.homepage, URIRef(self.assertor_homepage)) ) self.project_iri = RDFLIB_PROJECT_IRI self.extra_triples.add((self.project_iri, DOAP.homepage, self.project_iri)) self.extra_triples.add((self.project_iri, DOAP.name, Literal("RDFLib"))) self.extra_triples.add((self.project_iri, RDF.type, DOAP.Project)) self.extra_triples.add( (self.project_iri, DOAP["programming-language"], Literal("Python")) ) self.extra_triples.add( ( self.project_iri, DOAP.description, Literal( ( "RDFLib is a Python library for working with RDF, " "a simple yet powerful language for representing information." ), lang="en", ), ) ) self.asserted_at = Literal(datetime.now()) if self.output_file: self.report = EARLReport(self, self.output_file) def setup_report_graph(self, graph: Graph) -> None: GraphHelper.add_triples(graph, self.extra_triples) graph.bind("foaf", FOAF) graph.bind("earl", EARL) graph.bind("doap", DOAP) graph.bind("dc", DC) def make_report_graph(self) -> Graph: graph = Graph(store=OrderedMemory()) self.setup_report_graph(graph) return graph def make_report_with_prefix(self, report_prefix: str) -> EARLReport: output_file = self.output_dir / f"{report_prefix}{self.output_suffix}.ttl" return EARLReport(self, output_file) def get_report_for(self, entry: Optional[ManifestEntry]) -> Optional[EARLReport]: if self.report: return self.report if entry is None: return None manifest = entry.manifest logger.debug("manifest = %s", manifest) if manifest.report_prefix is None: return None report = self.prefix_reports.get(manifest.report_prefix) if report is None: report = self.prefix_reports[ manifest.report_prefix ] = self.make_report_with_prefix(manifest.report_prefix) return report @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport( self, item: Item, call: "CallInfo[None]" ) -> Generator[None, "_Result", None]: result = yield report: "TestReport" = result.get_result() if not hasattr(item, "callspec"): return callspec: "CallSpec2" = getattr(item, "callspec") rdf_test_uri = callspec.params.get("rdf_test_uri") if rdf_test_uri is not None: if isinstance(rdf_test_uri, str): rdf_test_uri = URIRef(rdf_test_uri) if isinstance(rdf_test_uri, URIRef): report.user_properties.append((RDFT.Test, rdf_test_uri)) else: logger.warning( "rdf_test_uri parameter is not a URIRef or a str, ignoring it" ) manifest_entry = callspec.params.get("manifest_entry") if manifest_entry is not None: report.user_properties.append((MF.ManifestEntry, manifest_entry)) @classmethod def get_rdf_test_uri( cls, rdf_test_uri: Optional[URIRef], manifest_entry: Optional[ManifestEntry] ) -> Optional[URIRef]: if rdf_test_uri is not None: return rdf_test_uri if manifest_entry is not None: return manifest_entry.identifier return None def append_result(self, report: "TestReport", test_result: TestResult) -> None: rdf_test_uri = TestReportHelper.get_rdf_test_uri(report) manifest_entry = TestReportHelper.get_manifest_entry(report) rdf_test_uri = self.get_rdf_test_uri(rdf_test_uri, manifest_entry) logger.debug( "rdf_test_uri = %s, manifest_entry = %s", rdf_test_uri, manifest_entry ) if rdf_test_uri is None: # nothing to report with return earl_report = self.get_report_for(manifest_entry) logger.debug("earl_report = %s, test_result = %s", earl_report, test_result) if earl_report is None: return if test_result is TestResult.PASS: earl_report.add_test_outcome(rdf_test_uri, EARL.passed) elif test_result is TestResult.FAIL: earl_report.add_test_outcome(rdf_test_uri, EARL.failed) elif test_result is TestResult.SKIP: earl_report.add_test_outcome(rdf_test_uri, EARL.untested) else: earl_report.add_test_outcome(rdf_test_uri, EARL.cantTell) def pytest_runtest_logreport(self, report: "TestReport") -> None: logger.debug( "report: passed = %s, failed = %s, skipped = %s, when = %s, outcome = %s, keywords = %s", report.passed, report.failed, report.skipped, report.when, report.outcome, report.keywords, ) if report.passed: if report.when == "call": # ignore setup/teardown self.append_result(report, TestResult.PASS) elif report.failed: if report.when == "call": # ignore setup/teardown self.append_result(report, TestResult.FAIL) else: self.append_result(report, TestResult.ERROR) elif report.skipped: if "skip" in report.keywords: self.append_result(report, TestResult.SKIP) elif "xfail" in report.keywords: self.append_result(report, TestResult.FAIL) else: self.append_result(report, TestResult.ERROR) def pytest_sessionfinish(self, session: "Session"): if self.report is not None: self.report.write() for report in self.prefix_reports.values(): report.write() def make_report(self, output_file: Path) -> EARLReport: return EARLReport(self, output_file) class OrderedMemory(Memory): def __init__(self, configuration=None, identifier=None): super().__init__(configuration, identifier) self.__spo = OrderedDict() self.__pos = OrderedDict() self.__osp = OrderedDict() self.__namespace = OrderedDict() self.__prefix = OrderedDict() self.__context_obj_map = OrderedDict()