Skip to content

Commit ec7e938

Browse files
committed
Added a rich-based serializer
--- - fixes #23
1 parent 3cab892 commit ec7e938

File tree

6 files changed

+179
-48
lines changed

6 files changed

+179
-48
lines changed

src/cite_runner/config.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
import jinja2
4+
import pydantic
45
from pydantic_settings import (
56
BaseSettings,
67
SettingsConfigDict,
@@ -17,6 +18,7 @@ class CiteRunnerSettings(BaseSettings):
1718
)
1819
default_json_serializer: str = "cite_runner.serializers.simple.to_json"
1920
default_markdown_serializer: str = "cite_runner.serializers.simple.to_markdown"
21+
default_console_serializer: str = "cite_runner.serializers.console.to_console"
2022
default_parser: str = "cite_runner.parsers.earl.parse_test_suite_result"
2123
extra_templates_path: str | None = None
2224

@@ -27,6 +29,16 @@ class CiteRunnerSettings(BaseSettings):
2729
simple_serializer_template: str = "test-suite-result.md"
2830

2931

32+
class CiteRunnerContext(pydantic.BaseModel):
33+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
34+
35+
debug: bool = False
36+
jinja_environment: jinja2.Environment = jinja2.Environment()
37+
network_timeout_seconds: int = 20
38+
rich_console: "Console"
39+
settings: CiteRunnerSettings
40+
41+
3042
def get_settings() -> CiteRunnerSettings:
3143
return CiteRunnerSettings()
3244

@@ -69,10 +81,10 @@ def get_console() -> Console:
6981
def get_context(
7082
debug: bool,
7183
network_timeout_seconds: int,
72-
) -> models.CiteRunnerContext:
84+
) -> CiteRunnerContext:
7385
settings = get_settings()
7486
console = get_console()
75-
return models.CiteRunnerContext(
87+
return CiteRunnerContext(
7688
debug=debug,
7789
network_timeout_seconds=network_timeout_seconds,
7890
jinja_environment=_get_jinja_environment(settings),

src/cite_runner/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import typer
1212
from rich import print
1313

14-
import src.cite_runner.models
1514
from . import (
1615
config,
1716
exceptions,
@@ -82,8 +81,9 @@ def parse_test_result(
8281
include_skipped_detail: bool = True,
8382
include_passed_detail: bool = False,
8483
):
84+
context: config.CiteRunnerContext = ctx.obj
8585
parsed = teamengine_runner.parse_test_suite_result(
86-
test_suite_result.read_text(), ctx.obj.settings
86+
test_suite_result.read_text(), context.settings
8787
)
8888
serialized = teamengine_runner.serialize_suite_result(
8989
parsed,
@@ -96,7 +96,8 @@ def parse_test_result(
9696
),
9797
context=ctx.obj,
9898
)
99-
print(serialized)
99+
100+
context.rich_console.print(serialized)
100101
raise typer.Exit(_get_exit_code(parsed, exit_with_error_on_suite_failed_result))
101102

102103

@@ -217,7 +218,7 @@ def execute_test_suite(
217218

218219

219220
def _execute_test_suite(
220-
ctx: src.cite_runner.models.CiteRunnerContext,
221+
ctx: config.CiteRunnerContext,
221222
teamengine_base_url: str,
222223
test_suite_identifier: str,
223224
teamengine_username: pydantic.SecretStr,

src/cite_runner/models.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,20 @@
22
import enum
33
from typing import Generator
44

5-
import jinja2
65
import pydantic
7-
from rich import Console
8-
9-
from src.cite_runner.config import CiteRunnerSettings
106

117

128
class OutputFormat(str, enum.Enum):
139
JSON = "json"
1410
MARKDOWN = "markdown"
1511
RAW = "raw"
12+
CONSOLE = "console"
1613

1714

1815
class ParseableOutputFormat(str, enum.Enum):
1916
JSON = "json"
2017
MARKDOWN = "markdown"
18+
CONSOLE = "console"
2119

2220

2321
class TestStatus(enum.Enum):
@@ -83,13 +81,3 @@ class TestSuiteResult(pydantic.BaseModel):
8381
inputs: list[TestSuiteInput]
8482
conformance_class_results: list[ConformanceClassResult]
8583
passed: bool
86-
87-
88-
class CiteRunnerContext(pydantic.BaseModel):
89-
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
90-
91-
debug: bool = False
92-
jinja_environment: jinja2.Environment = jinja2.Environment()
93-
network_timeout_seconds: int = 20
94-
rich_console: Console
95-
settings: CiteRunnerSettings
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import humanize
2+
import rich.box
3+
from rich.panel import Panel
4+
from rich.table import Table
5+
from rich.console import (
6+
Group,
7+
RenderableType,
8+
)
9+
from rich.text import Text
10+
11+
from .. import (
12+
config,
13+
models,
14+
)
15+
16+
17+
def to_console(
18+
parsed_result: models.TestSuiteResult,
19+
serialization_details: models.SerializationDetails,
20+
context: config.CiteRunnerContext,
21+
) -> Group:
22+
overview_message = Text(
23+
f"Test suite has {'passed 🏅' if parsed_result.passed else 'failed ❌'}"
24+
f"\n\n"
25+
f"- Ran {parsed_result.num_tests_total} tests in "
26+
f"{humanize.precisedelta(parsed_result.test_run_duration)}\n"
27+
f"- 🔴 Failed {parsed_result.num_failed_tests} tests\n"
28+
f"- 🟡 Skipped {parsed_result.num_skipped_tests} tests\n"
29+
f"- 🟢 Passed {parsed_result.num_passed_tests} tests\n"
30+
)
31+
contents = [overview_message]
32+
if serialization_details.include_summary:
33+
summary_table = Table(title="Conformance classes", expand=True)
34+
summary_table.add_column("Class")
35+
summary_table.add_column("🔴 Failed")
36+
summary_table.add_column("🟡 Skipped")
37+
summary_table.add_column("🟢 Passed")
38+
for conf_class in parsed_result.conformance_class_results:
39+
summary_table.add_row(
40+
conf_class.title,
41+
str(conf_class.num_failed_tests),
42+
str(conf_class.num_skipped_tests),
43+
str(conf_class.num_passed_tests),
44+
)
45+
summary_contents = Panel(summary_table, box=rich.box.SIMPLE)
46+
contents.append(summary_contents)
47+
if (
48+
serialization_details.include_failed_detail
49+
and parsed_result.num_failed_tests > 0
50+
):
51+
failed_contents = _render_detail_section(
52+
parsed_result, models.TestStatus.FAILED
53+
)
54+
contents.append(failed_contents)
55+
if (
56+
serialization_details.include_skipped_detail
57+
and parsed_result.num_skipped_tests > 0
58+
):
59+
skipped_contents = _render_detail_section(
60+
parsed_result, models.TestStatus.SKIPPED
61+
)
62+
contents.append(skipped_contents)
63+
if (
64+
serialization_details.include_passed_detail
65+
and parsed_result.num_passed_tests > 0
66+
):
67+
passed_contents = _render_detail_section(
68+
parsed_result, models.TestStatus.PASSED
69+
)
70+
contents.append(passed_contents)
71+
panel_group = Group(
72+
Panel(Group(*contents), title=f"Test suite {parsed_result.suite_title}"),
73+
)
74+
return panel_group
75+
76+
77+
def _render_detail_section(
78+
parsed_result: models.TestSuiteResult,
79+
detail_type: models.TestStatus,
80+
) -> RenderableType:
81+
conf_classes_contents = []
82+
title = {
83+
models.TestStatus.PASSED: "🟢 Passed tests",
84+
models.TestStatus.FAILED: "🔴 Failed tests",
85+
models.TestStatus.SKIPPED: "🟡 Skipped tests",
86+
}[detail_type]
87+
for conf_class in parsed_result.conformance_class_results:
88+
comparator, outcome_color, test_case_result_generator = {
89+
models.TestStatus.PASSED: (
90+
conf_class.num_passed_tests,
91+
"green",
92+
conf_class.gen_passed_tests,
93+
),
94+
models.TestStatus.FAILED: (
95+
conf_class.num_failed_tests,
96+
"red",
97+
conf_class.gen_failed_tests,
98+
),
99+
models.TestStatus.SKIPPED: (
100+
conf_class.num_skipped_tests,
101+
"bright_yellow",
102+
conf_class.gen_skipped_tests,
103+
),
104+
}[detail_type]
105+
if comparator > 0:
106+
test_case_group_content = []
107+
for test_case_result in test_case_result_generator():
108+
test_case_group_content.append(
109+
Panel(
110+
Group(
111+
Text.assemble(
112+
("Test case: ", "yellow"), test_case_result.identifier
113+
),
114+
Text.assemble(
115+
("Outcome: ", "yellow"),
116+
(test_case_result.status.value, outcome_color),
117+
),
118+
Text.assemble(
119+
("Description: ", "yellow"),
120+
test_case_result.description or "",
121+
),
122+
Text.assemble(
123+
("Detail: ", "yellow"), test_case_result.detail or ""
124+
),
125+
),
126+
box=rich.box.SIMPLE,
127+
title_align="left",
128+
),
129+
)
130+
conf_classes_contents.append(
131+
Panel(
132+
Group(*test_case_group_content),
133+
title=conf_class.title,
134+
title_align="left",
135+
)
136+
)
137+
failed_contents = Panel(
138+
Group(*conf_classes_contents),
139+
title=title,
140+
title_align="left",
141+
box=rich.box.SIMPLE,
142+
)
143+
return failed_contents
Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
from rich.table import Table
2-
3-
from .. import models
4-
from ..models import CiteRunnerContext
1+
from .. import (
2+
config,
3+
models,
4+
)
55

66

77
def to_markdown(
88
parsed_result: models.TestSuiteResult,
99
serialization_details: models.SerializationDetails,
10-
context: CiteRunnerContext,
10+
context: config.CiteRunnerContext,
1111
) -> str:
1212
"""Serialize parsed test suite results to markdown"""
1313
template = context.jinja_environment.get_template(
@@ -22,25 +22,6 @@ def to_markdown(
2222
def to_json(
2323
parsed_result: models.TestSuiteResult,
2424
serialization_details: models.SerializationDetails,
25-
context: CiteRunnerContext,
25+
context: config.CiteRunnerContext,
2626
) -> str:
2727
return parsed_result.model_dump_json(indent=2)
28-
29-
30-
def to_terminal(
31-
parsed_result: models.TestSuiteResult,
32-
serialization_details: models.SerializationDetails,
33-
context: CiteRunnerContext,
34-
) -> str:
35-
table = Table("Conformance classes")
36-
table.add_column("Title")
37-
table.add_column("Failed")
38-
table.add_column("Skipped")
39-
table.add_column("Passed")
40-
for conformance_class in parsed_result.conformance_class_results:
41-
table.add_row(
42-
conformance_class.title,
43-
conformance_class.num_failed_tests,
44-
conformance_class.num_skipped_tests,
45-
conformance_class.num_passed_tests,
46-
)

src/cite_runner/teamengine_runner.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import httpx
1010
import pydantic
11+
from rich.console import RenderableType
1112

1213
from . import (
1314
config,
@@ -27,8 +28,8 @@ def __call__(
2728
self,
2829
suite_result: models.TestSuiteResult,
2930
serialization_details: models.SerializationDetails,
30-
context: models.CiteRunnerContext,
31-
) -> str: ...
31+
context: config.CiteRunnerContext,
32+
) -> RenderableType: ...
3233

3334

3435
def wait_for_teamengine_to_be_ready(
@@ -102,7 +103,7 @@ def serialize_suite_result(
102103
parsed_suite_result: models.TestSuiteResult,
103104
output_format: models.ParseableOutputFormat,
104105
serialization_details: models.SerializationDetails,
105-
context: models.CiteRunnerContext,
106+
context: config.CiteRunnerContext,
106107
) -> str:
107108
serializer: SuiteSerializerProtocol = _get_suite_result_serializer(
108109
output_format, context.settings, parsed_suite_result.suite_title
@@ -137,6 +138,7 @@ def _get_suite_result_serializer(
137138
serializer_python_path = {
138139
output_format.JSON: settings.default_json_serializer,
139140
output_format.MARKDOWN: settings.default_markdown_serializer,
141+
output_format.CONSOLE: settings.default_console_serializer,
140142
}.get(output_format)
141143
if test_suite_identifier is not None:
142144
settings_key = "_".join(
@@ -156,6 +158,10 @@ def _get_suite_result_serializer(
156158
f"test suite identifier {test_suite_identifier!r} - using "
157159
f"default serializer of {serializer_python_path!r}"
158160
)
161+
if serializer_python_path is None:
162+
raise NotImplementedError(
163+
f"no serializer available for {output_format=} and {test_suite_identifier=}"
164+
)
159165
return _load_python_object(serializer_python_path)
160166

161167

0 commit comments

Comments
 (0)