Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions src/cite_runner/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class CiteRunnerSettings(BaseSettings):
)
default_json_serializer: str = "cite_runner.serializers.simple.to_json"
default_markdown_serializer: str = "cite_runner.serializers.simple.to_markdown"
default_console_serializer: str = "cite_runner.serializers.console.to_console"
default_parser: str = "cite_runner.parsers.earl.parse_test_suite_result"
extra_templates_path: str | None = None

Expand All @@ -28,29 +29,20 @@ class CiteRunnerSettings(BaseSettings):
simple_serializer_template: str = "test-suite-result.md"


class CliContext(pydantic.BaseModel):
class CiteRunnerContext(pydantic.BaseModel):
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)

debug: bool = False
jinja_environment: jinja2.Environment = jinja2.Environment()
network_timeout_seconds: int = 20
rich_console: "Console"
settings: CiteRunnerSettings


def get_settings() -> CiteRunnerSettings:
return CiteRunnerSettings()


def get_context(debug: bool, network_timeout_seconds: int) -> CliContext:
settings = get_settings()
return CliContext(
debug=debug,
network_timeout_seconds=network_timeout_seconds,
jinja_environment=_get_jinja_environment(settings),
settings=settings,
)


def _get_jinja_environment(settings: CiteRunnerSettings) -> jinja2.Environment:
loaders = [
jinja2.PackageLoader("cite_runner", "templates"),
Expand All @@ -71,10 +63,31 @@ def _get_jinja_environment(settings: CiteRunnerSettings) -> jinja2.Environment:
return env


def configure_logging(debug: bool) -> None:
def configure_logging(rich_console: Console, debug: bool) -> None:
logging.basicConfig(
level=logging.DEBUG if debug else logging.WARNING,
handlers=[RichHandler(console=Console(stderr=True), rich_tracebacks=True)],
handlers=[RichHandler(console=rich_console, rich_tracebacks=True)],
)
logging.getLogger("httpcore").setLevel(logging.INFO if debug else logging.WARNING)
logging.getLogger("httpx").setLevel(logging.INFO if debug else logging.WARNING)


def get_console() -> Console:
return Console(
stderr=True,
)


def get_context(
debug: bool,
network_timeout_seconds: int,
) -> CiteRunnerContext:
settings = get_settings()
console = get_console()
return CiteRunnerContext(
debug=debug,
network_timeout_seconds=network_timeout_seconds,
jinja_environment=_get_jinja_environment(settings),
settings=settings,
rich_console=console,
)
19 changes: 10 additions & 9 deletions src/cite_runner/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ def _parse_pydantic_secret_str(value: str) -> pydantic.SecretStr:
def base_callback(
ctx: typer.Context, debug: bool = False, network_timeout: int = 120
) -> None:
config.configure_logging(debug=debug)
ctx.obj = config.get_context(
context = config.get_context(
debug=debug,
network_timeout_seconds=network_timeout,
)
config.configure_logging(rich_console=context.rich_console, debug=debug)
ctx.obj = context


@app.command("parse-result")
Expand All @@ -80,22 +81,23 @@ def parse_test_result(
include_skipped_detail: bool = True,
include_passed_detail: bool = False,
):
context: config.CiteRunnerContext = ctx.obj
parsed = teamengine_runner.parse_test_suite_result(
test_suite_result.read_text(), ctx.obj.settings
test_suite_result.read_text(), context.settings
)
serialized = teamengine_runner.serialize_suite_result(
parsed,
output_format,
ctx.obj.settings,
ctx.obj.jinja_environment,
serialization_details=models.SerializationDetails(
include_summary=include_summary,
include_failed_detail=include_failed_detail,
include_skipped_detail=include_skipped_detail,
include_passed_detail=include_passed_detail,
),
context=ctx.obj,
)
print(serialized)

context.rich_console.print(serialized)
raise typer.Exit(_get_exit_code(parsed, exit_with_error_on_suite_failed_result))


Expand Down Expand Up @@ -216,7 +218,7 @@ def execute_test_suite(


def _execute_test_suite(
ctx: config.CliContext,
ctx: config.CiteRunnerContext,
teamengine_base_url: str,
test_suite_identifier: str,
teamengine_username: pydantic.SecretStr,
Expand Down Expand Up @@ -258,9 +260,8 @@ def _execute_test_suite(
serialized = teamengine_runner.serialize_suite_result(
parsed,
format_to_output,
ctx.settings,
ctx.jinja_environment,
serialization_details,
ctx,
)
return parsed, serialized
else:
Expand Down
2 changes: 2 additions & 0 deletions src/cite_runner/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ class OutputFormat(str, enum.Enum):
JSON = "json"
MARKDOWN = "markdown"
RAW = "raw"
CONSOLE = "console"


class ParseableOutputFormat(str, enum.Enum):
JSON = "json"
MARKDOWN = "markdown"
CONSOLE = "console"


class TestStatus(enum.Enum):
Expand Down
143 changes: 143 additions & 0 deletions src/cite_runner/serializers/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import humanize
import rich.box
from rich.panel import Panel
from rich.table import Table
from rich.console import (
Group,
RenderableType,
)
from rich.text import Text

from .. import (
config,
models,
)


def to_console(
parsed_result: models.TestSuiteResult,
serialization_details: models.SerializationDetails,
context: config.CiteRunnerContext,
) -> Group:
overview_message = Text(
f"Test suite has {'passed 🏅' if parsed_result.passed else 'failed ❌'}"
f"\n\n"
f"- Ran {parsed_result.num_tests_total} tests in "
f"{humanize.precisedelta(parsed_result.test_run_duration)}\n"
f"- 🔴 Failed {parsed_result.num_failed_tests} tests\n"
f"- 🟡 Skipped {parsed_result.num_skipped_tests} tests\n"
f"- 🟢 Passed {parsed_result.num_passed_tests} tests\n"
)
contents = [overview_message]
if serialization_details.include_summary:
summary_table = Table(title="Conformance classes", expand=True)
summary_table.add_column("Class")
summary_table.add_column("🔴 Failed")
summary_table.add_column("🟡 Skipped")
summary_table.add_column("🟢 Passed")
for conf_class in parsed_result.conformance_class_results:
summary_table.add_row(
conf_class.title,
str(conf_class.num_failed_tests),
str(conf_class.num_skipped_tests),
str(conf_class.num_passed_tests),
)
summary_contents = Panel(summary_table, box=rich.box.SIMPLE)
contents.append(summary_contents)
if (
serialization_details.include_failed_detail
and parsed_result.num_failed_tests > 0
):
failed_contents = _render_detail_section(
parsed_result, models.TestStatus.FAILED
)
contents.append(failed_contents)
if (
serialization_details.include_skipped_detail
and parsed_result.num_skipped_tests > 0
):
skipped_contents = _render_detail_section(
parsed_result, models.TestStatus.SKIPPED
)
contents.append(skipped_contents)
if (
serialization_details.include_passed_detail
and parsed_result.num_passed_tests > 0
):
passed_contents = _render_detail_section(
parsed_result, models.TestStatus.PASSED
)
contents.append(passed_contents)
panel_group = Group(
Panel(Group(*contents), title=f"Test suite {parsed_result.suite_title}"),
)
return panel_group


def _render_detail_section(
parsed_result: models.TestSuiteResult,
detail_type: models.TestStatus,
) -> RenderableType:
conf_classes_contents = []
title = {
models.TestStatus.PASSED: "🟢 Passed tests",
models.TestStatus.FAILED: "🔴 Failed tests",
models.TestStatus.SKIPPED: "🟡 Skipped tests",
}[detail_type]
for conf_class in parsed_result.conformance_class_results:
comparator, outcome_color, test_case_result_generator = {
models.TestStatus.PASSED: (
conf_class.num_passed_tests,
"green",
conf_class.gen_passed_tests,
),
models.TestStatus.FAILED: (
conf_class.num_failed_tests,
"red",
conf_class.gen_failed_tests,
),
models.TestStatus.SKIPPED: (
conf_class.num_skipped_tests,
"bright_yellow",
conf_class.gen_skipped_tests,
),
}[detail_type]
if comparator > 0:
test_case_group_content = []
for test_case_result in test_case_result_generator():
test_case_group_content.append(
Panel(
Group(
Text.assemble(
("Test case: ", "yellow"), test_case_result.identifier
),
Text.assemble(
("Outcome: ", "yellow"),
(test_case_result.status.value, outcome_color),
),
Text.assemble(
("Description: ", "yellow"),
test_case_result.description or "",
),
Text.assemble(
("Detail: ", "yellow"), test_case_result.detail or ""
),
),
box=rich.box.SIMPLE,
title_align="left",
),
)
conf_classes_contents.append(
Panel(
Group(*test_case_group_content),
title=conf_class.title,
title_align="left",
)
)
failed_contents = Panel(
Group(*conf_classes_contents),
title=title,
title_align="left",
box=rich.box.SIMPLE,
)
return failed_contents
18 changes: 9 additions & 9 deletions src/cite_runner/serializers/simple.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import jinja2

from .. import models
from ..config import CiteRunnerSettings
from .. import (
config,
models,
)


def to_markdown(
parsed_result: models.TestSuiteResult,
settings: CiteRunnerSettings,
jinja_environment: jinja2.Environment,
serialization_details: models.SerializationDetails,
context: config.CiteRunnerContext,
) -> str:
"""Serialize parsed test suite results to markdown"""
template = jinja_environment.get_template(settings.simple_serializer_template)
template = context.jinja_environment.get_template(
context.settings.simple_serializer_template
)
return template.render(
result=parsed_result,
serialization_details=serialization_details,
Expand All @@ -20,8 +21,7 @@ def to_markdown(

def to_json(
parsed_result: models.TestSuiteResult,
settings: CiteRunnerSettings,
jinja_environment: jinja2.Environment,
serialization_details: models.SerializationDetails,
context: config.CiteRunnerContext,
) -> str:
return parsed_result.model_dump_json(indent=2)
19 changes: 11 additions & 8 deletions src/cite_runner/teamengine_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from lxml import etree

import httpx
import jinja2
import pydantic
from rich.console import RenderableType

from . import (
config,
Expand All @@ -27,10 +27,9 @@ class SuiteSerializerProtocol(typing.Protocol):
def __call__(
self,
suite_result: models.TestSuiteResult,
settings: config.CiteRunnerSettings,
jinja_env: jinja2.Environment,
serialization_details: models.SerializationDetails,
) -> str: ...
context: config.CiteRunnerContext,
) -> RenderableType: ...


def wait_for_teamengine_to_be_ready(
Expand Down Expand Up @@ -103,14 +102,13 @@ def parse_test_suite_result(
def serialize_suite_result(
parsed_suite_result: models.TestSuiteResult,
output_format: models.ParseableOutputFormat,
settings: config.CiteRunnerSettings,
jinja_env: jinja2.Environment,
serialization_details: models.SerializationDetails,
context: config.CiteRunnerContext,
) -> str:
serializer: SuiteSerializerProtocol = _get_suite_result_serializer(
output_format, settings, parsed_suite_result.suite_title
output_format, context.settings, parsed_suite_result.suite_title
)
return serializer(parsed_suite_result, settings, jinja_env, serialization_details)
return serializer(parsed_suite_result, serialization_details, context)


def _sanitize_test_suite_identifier(raw_identifier: str) -> str:
Expand Down Expand Up @@ -140,6 +138,7 @@ def _get_suite_result_serializer(
serializer_python_path = {
output_format.JSON: settings.default_json_serializer,
output_format.MARKDOWN: settings.default_markdown_serializer,
output_format.CONSOLE: settings.default_console_serializer,
}.get(output_format)
if test_suite_identifier is not None:
settings_key = "_".join(
Expand All @@ -159,6 +158,10 @@ def _get_suite_result_serializer(
f"test suite identifier {test_suite_identifier!r} - using "
f"default serializer of {serializer_python_path!r}"
)
if serializer_python_path is None:
raise NotImplementedError(
f"no serializer available for {output_format=} and {test_suite_identifier=}"
)
return _load_python_object(serializer_python_path)


Expand Down