Skip to content

Commit 10d7c89

Browse files
authored
Decouple test results from rendering logic via Reporters (#81)
* Add jinja dependencies * Implement `Reporter` abstract class and decouple from `Runner` * Add jinja template * Add tests for `Reporters` * Linting * Add exportable asset serializers * Allow assets to serialize during reports * Add demos and tests * Render additional examples * Add date and package version ot reports * Fix rendering bug * Fix type hinting * Rename example
1 parent b5dff01 commit 10d7c89

File tree

15 files changed

+2397
-146
lines changed

15 files changed

+2397
-146
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Reporter Examples
2+
3+
This page demonstrates how to use the Reporter module.
4+
5+
## Example Code for HTML Reporter
6+
7+
The following example shows how to run QC tests using the HTML and Console reporters:
8+
9+
```python
10+
--8<-- "examples/reporters_demo.py"
11+
```
12+
13+
## Exporting Context Exportable Objects
14+
15+
The following example demonstrates how to export context exportable objects when running QC tests:
16+
17+
```python
18+
--8<-- "examples/image_serialization_demo.py"
19+
```
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"""Demo script for testing ContextExportableObj serialization with images."""
2+
3+
import contraqctor.qc as qc
4+
5+
6+
class ImageTestSuite(qc.Suite):
7+
"""Test suite demonstrating image handling in reports."""
8+
9+
name = "Image Visualization Tests"
10+
11+
def test_matplotlib_figure(self):
12+
"""Test that creates a matplotlib figure."""
13+
try:
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
17+
# Create a simple plot
18+
fig, ax = plt.subplots(figsize=(8, 6))
19+
x = np.linspace(0, 10, 100)
20+
y = np.sin(x)
21+
ax.plot(x, y, label="sin(x)")
22+
ax.set_xlabel("X")
23+
ax.set_ylabel("Y")
24+
ax.set_title("Sine Wave")
25+
ax.legend()
26+
ax.grid(True)
27+
28+
# Add the figure to context using ContextExportableObj
29+
context = qc.ContextExportableObj.as_context(fig)
30+
context["data_points"] = len(x)
31+
32+
plt.close(fig)
33+
34+
return self.pass_test(True, "Matplotlib figure created successfully", context=context)
35+
except ImportError:
36+
return self.skip_test("Matplotlib not available")
37+
38+
def test_pil_image(self):
39+
"""Test that creates a PIL image."""
40+
try:
41+
from PIL import Image, ImageDraw
42+
43+
# Create a simple image
44+
img = Image.new("RGB", (400, 300), color="white")
45+
draw = ImageDraw.Draw(img)
46+
47+
# Draw some shapes
48+
draw.rectangle([50, 50, 350, 250], outline="blue", width=3)
49+
draw.ellipse([100, 100, 300, 200], fill="lightblue", outline="darkblue", width=2)
50+
draw.text((150, 130), "Test Image", fill="black")
51+
52+
# Add the image to context
53+
context = qc.ContextExportableObj.as_context(img)
54+
context["image_size"] = img.size
55+
56+
return self.pass_test(True, "PIL Image created successfully", context=context)
57+
except ImportError:
58+
return self.skip_test("PIL not available")
59+
60+
def test_numpy_array_image(self):
61+
"""Test that creates a numpy array image."""
62+
try:
63+
import numpy as np
64+
65+
# Create a gradient image
66+
img = np.zeros((200, 300, 3), dtype=np.uint8)
67+
for i in range(200):
68+
for j in range(300):
69+
img[i, j] = [int(i / 200 * 255), int(j / 300 * 255), 128]
70+
71+
# Add the image to context
72+
context = qc.ContextExportableObj.as_context(img)
73+
context["array_shape"] = str(img.shape)
74+
context["array_dtype"] = str(img.dtype)
75+
76+
return self.pass_test(True, "Numpy array image created successfully", context=context)
77+
except ImportError:
78+
return self.skip_test("Numpy not available")
79+
80+
def test_multiple_images(self):
81+
"""Test with multiple images in context."""
82+
try:
83+
import matplotlib.pyplot as plt
84+
import numpy as np
85+
86+
# Create two different plots
87+
fig1, ax1 = plt.subplots(figsize=(6, 4))
88+
x = np.linspace(0, 2 * np.pi, 100)
89+
ax1.plot(x, np.sin(x), "r-", label="sin")
90+
ax1.set_title("Sine")
91+
ax1.legend()
92+
93+
fig2, ax2 = plt.subplots(figsize=(6, 4))
94+
ax2.plot(x, np.cos(x), "b-", label="cos")
95+
ax2.set_title("Cosine")
96+
ax2.legend()
97+
98+
# Create context with multiple images (not using ContextExportableObj.as_context
99+
# because it only handles one asset)
100+
context = {
101+
"sine_plot": qc.ContextExportableObj(fig1),
102+
"cosine_plot": qc.ContextExportableObj(fig2),
103+
"note": "Two separate plots",
104+
}
105+
106+
plt.close(fig1)
107+
plt.close(fig2)
108+
109+
return self.pass_test(True, "Multiple images created successfully", context=context)
110+
except ImportError:
111+
return self.skip_test("Matplotlib not available")
112+
113+
def test_failed_with_image(self):
114+
"""Test that fails but includes an image for debugging."""
115+
try:
116+
import matplotlib.pyplot as plt
117+
import numpy as np
118+
119+
# Create a plot showing the failure
120+
fig, ax = plt.subplots(figsize=(8, 6))
121+
x = np.array([1, 2, 3, 4, 5])
122+
expected = np.array([2, 4, 6, 8, 10])
123+
actual = np.array([2, 4, 7, 8, 10]) # Deviation at x=3
124+
125+
ax.plot(x, expected, "g-o", label="Expected")
126+
ax.plot(x, actual, "r-x", label="Actual")
127+
ax.set_xlabel("X")
128+
ax.set_ylabel("Y")
129+
ax.set_title("Data Validation Failure")
130+
ax.legend()
131+
ax.grid(True)
132+
133+
context = qc.ContextExportableObj.as_context(fig)
134+
context["deviation_index"] = 2
135+
context["expected_value"] = 6
136+
context["actual_value"] = 7
137+
138+
plt.close(fig)
139+
140+
return self.fail_test(False, "Data validation failed at index 2", context=context)
141+
except ImportError:
142+
return self.skip_test("Matplotlib not available")
143+
144+
145+
class DataQualitySuite(qc.Suite):
146+
"""Test suite for data quality checks."""
147+
148+
name = "Data Quality"
149+
150+
def test_data_range(self):
151+
"""Test data is within expected range."""
152+
return self.pass_test(True, "Data within range")
153+
154+
def test_no_missing_values(self):
155+
"""Test no missing values."""
156+
return self.pass_test(True, "No missing values found")
157+
158+
def test_data_type_validation(self):
159+
"""Test data types are correct."""
160+
return self.warn_test(True, "Some data types could be optimized")
161+
162+
163+
def main():
164+
"""Run the demo and generate both console and HTML reports."""
165+
print("Running Image Visualization Demo\n")
166+
167+
# Create runner and add test suites
168+
runner = qc.Runner()
169+
runner.add_suite(ImageTestSuite(), "Visualization")
170+
runner.add_suite(DataQualitySuite(), "Data Quality")
171+
172+
# Manually run tests and collect results
173+
from contraqctor.qc.base import ResultsStatistics, _TaggedResult
174+
175+
tagged_results = []
176+
for group, suites in runner.suites.items():
177+
for suite in suites:
178+
for test_method in suite.get_tests():
179+
results_iter = suite.run_test(test_method)
180+
result = next(iter(results_iter))
181+
tagged_results.append(_TaggedResult(suite=suite, group=group, result=result, test=test_method))
182+
183+
stats = ResultsStatistics.from_results([tr.result for tr in tagged_results])
184+
185+
# Display with console reporter (with asset serialization)
186+
print("=" * 80)
187+
print("CONSOLE OUTPUT (with asset serialization enabled)")
188+
print("=" * 80 + "\n")
189+
190+
console_reporter = qc.ConsoleReporter()
191+
console_reporter.report_results(
192+
tagged_results,
193+
stats,
194+
serialize_context_exportable_obj=True,
195+
asset_output_dir="./report/assets",
196+
)
197+
198+
# Generate HTML report WITHOUT serialization (baseline)
199+
print("\n" + "=" * 80)
200+
print("Generating HTML report WITHOUT serialization...")
201+
html_reporter_no_serialize = qc.HtmlReporter("test_report_no_serialize.html")
202+
html_reporter_no_serialize.report_results(
203+
tagged_results,
204+
stats,
205+
serialize_context_exportable_obj=False,
206+
)
207+
print("HTML report (without serialization) saved to: test_report_no_serialize.html")
208+
209+
# Generate HTML report WITH serialization
210+
print("\n" + "=" * 80)
211+
print("Generating HTML report WITH serialization...")
212+
html_reporter_with_serialize = qc.HtmlReporter("test_report_with_serialize.html")
213+
html_reporter_with_serialize.report_results(
214+
tagged_results,
215+
stats,
216+
serialize_context_exportable_obj=True,
217+
)
218+
print("HTML report (with serialization) saved to: test_report_with_serialize.html")
219+
220+
print("\n" + "=" * 80)
221+
print("SUMMARY")
222+
print("=" * 80)
223+
print(f"Total Tests: {stats.total}")
224+
print(f"Passed: {stats.passed}")
225+
print(f"Failed: {stats.failed}")
226+
print(f"Errors: {stats.error}")
227+
print(f"Warnings: {stats.warnings}")
228+
print(f"Skipped: {stats.skipped}")
229+
230+
print("\nGenerated files:")
231+
print(" - ./report/assets/ (serialized image files for console)")
232+
print(" - test_report_no_serialize.html (shows raw context)")
233+
print(" - test_report_with_serialize.html (shows embedded images)")
234+
print("\nOpen the HTML files in your browser to see the difference!")
235+
236+
237+
if __name__ == "__main__":
238+
main()

0 commit comments

Comments
 (0)