haoline 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haoline/.streamlit/config.toml +10 -0
- haoline/__init__.py +248 -0
- haoline/analyzer.py +935 -0
- haoline/cli.py +2712 -0
- haoline/compare.py +811 -0
- haoline/compare_visualizations.py +1564 -0
- haoline/edge_analysis.py +525 -0
- haoline/eval/__init__.py +131 -0
- haoline/eval/adapters.py +844 -0
- haoline/eval/cli.py +390 -0
- haoline/eval/comparison.py +542 -0
- haoline/eval/deployment.py +633 -0
- haoline/eval/schemas.py +833 -0
- haoline/examples/__init__.py +15 -0
- haoline/examples/basic_inspection.py +74 -0
- haoline/examples/compare_models.py +117 -0
- haoline/examples/hardware_estimation.py +78 -0
- haoline/format_adapters.py +1001 -0
- haoline/formats/__init__.py +123 -0
- haoline/formats/coreml.py +250 -0
- haoline/formats/gguf.py +483 -0
- haoline/formats/openvino.py +255 -0
- haoline/formats/safetensors.py +273 -0
- haoline/formats/tflite.py +369 -0
- haoline/hardware.py +2307 -0
- haoline/hierarchical_graph.py +462 -0
- haoline/html_export.py +1573 -0
- haoline/layer_summary.py +769 -0
- haoline/llm_summarizer.py +465 -0
- haoline/op_icons.py +618 -0
- haoline/operational_profiling.py +1492 -0
- haoline/patterns.py +1116 -0
- haoline/pdf_generator.py +265 -0
- haoline/privacy.py +250 -0
- haoline/pydantic_models.py +241 -0
- haoline/report.py +1923 -0
- haoline/report_sections.py +539 -0
- haoline/risks.py +521 -0
- haoline/schema.py +523 -0
- haoline/streamlit_app.py +2024 -0
- haoline/tests/__init__.py +4 -0
- haoline/tests/conftest.py +123 -0
- haoline/tests/test_analyzer.py +868 -0
- haoline/tests/test_compare_visualizations.py +293 -0
- haoline/tests/test_edge_analysis.py +243 -0
- haoline/tests/test_eval.py +604 -0
- haoline/tests/test_format_adapters.py +460 -0
- haoline/tests/test_hardware.py +237 -0
- haoline/tests/test_hardware_recommender.py +90 -0
- haoline/tests/test_hierarchical_graph.py +326 -0
- haoline/tests/test_html_export.py +180 -0
- haoline/tests/test_layer_summary.py +428 -0
- haoline/tests/test_llm_patterns.py +540 -0
- haoline/tests/test_llm_summarizer.py +339 -0
- haoline/tests/test_patterns.py +774 -0
- haoline/tests/test_pytorch.py +327 -0
- haoline/tests/test_report.py +383 -0
- haoline/tests/test_risks.py +398 -0
- haoline/tests/test_schema.py +417 -0
- haoline/tests/test_tensorflow.py +380 -0
- haoline/tests/test_visualizations.py +316 -0
- haoline/universal_ir.py +856 -0
- haoline/visualizations.py +1086 -0
- haoline/visualize_yolo.py +44 -0
- haoline/web.py +110 -0
- haoline-0.3.0.dist-info/METADATA +471 -0
- haoline-0.3.0.dist-info/RECORD +70 -0
- haoline-0.3.0.dist-info/WHEEL +4 -0
- haoline-0.3.0.dist-info/entry_points.txt +5 -0
- haoline-0.3.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# Copyright (c) 2025 HaoLine Contributors
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Unit tests for the schema module (JSON schema validation).
|
|
6
|
+
|
|
7
|
+
Tests cover both Pydantic validation (preferred) and jsonschema fallback.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import onnx
|
|
18
|
+
import pytest
|
|
19
|
+
from onnx import TensorProto, helper
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
22
|
+
from ..report import ModelInspector
|
|
23
|
+
from ..schema import (
|
|
24
|
+
PYDANTIC_AVAILABLE,
|
|
25
|
+
ValidationError,
|
|
26
|
+
get_schema,
|
|
27
|
+
validate_report,
|
|
28
|
+
validate_report_strict,
|
|
29
|
+
validate_with_pydantic,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Check if jsonschema is available (fallback)
|
|
33
|
+
try:
|
|
34
|
+
from jsonschema import Draft7Validator # noqa: F401
|
|
35
|
+
|
|
36
|
+
JSONSCHEMA_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
JSONSCHEMA_AVAILABLE = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_simple_model() -> onnx.ModelProto:
|
|
42
|
+
"""Create a simple ONNX model for testing."""
|
|
43
|
+
X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 64])
|
|
44
|
+
Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 64])
|
|
45
|
+
|
|
46
|
+
W = helper.make_tensor(
|
|
47
|
+
"W",
|
|
48
|
+
TensorProto.FLOAT,
|
|
49
|
+
[64, 64],
|
|
50
|
+
np.random.randn(64, 64).astype(np.float32).flatten().tolist(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
matmul = helper.make_node("MatMul", ["X", "W"], ["matmul_out"], name="matmul")
|
|
54
|
+
relu = helper.make_node("Relu", ["matmul_out"], ["Y"], name="relu")
|
|
55
|
+
|
|
56
|
+
graph = helper.make_graph([matmul, relu], "simple_model", [X], [Y], [W])
|
|
57
|
+
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)])
|
|
58
|
+
return model
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestSchemaDefinition:
|
|
62
|
+
"""Tests for the JSON schema definition."""
|
|
63
|
+
|
|
64
|
+
def test_schema_has_required_fields(self):
|
|
65
|
+
"""Verify schema has the required top-level structure."""
|
|
66
|
+
schema = get_schema()
|
|
67
|
+
# Note: Pydantic-generated schemas don't include $schema field
|
|
68
|
+
# but do include all the structural elements we need
|
|
69
|
+
assert "properties" in schema
|
|
70
|
+
assert "required" in schema
|
|
71
|
+
assert "title" in schema # Pydantic includes title
|
|
72
|
+
|
|
73
|
+
def test_schema_required_fields(self):
|
|
74
|
+
"""Verify required fields are defined."""
|
|
75
|
+
schema = get_schema()
|
|
76
|
+
required = schema["required"]
|
|
77
|
+
assert "metadata" in required
|
|
78
|
+
assert "generated_at" in required
|
|
79
|
+
assert "autodoc_version" in required
|
|
80
|
+
|
|
81
|
+
def test_schema_has_all_sections(self):
|
|
82
|
+
"""Verify schema includes all report sections."""
|
|
83
|
+
schema = get_schema()
|
|
84
|
+
props = schema["properties"]
|
|
85
|
+
|
|
86
|
+
expected_sections = [
|
|
87
|
+
"metadata",
|
|
88
|
+
"generated_at",
|
|
89
|
+
"autodoc_version",
|
|
90
|
+
"graph_summary",
|
|
91
|
+
"param_counts",
|
|
92
|
+
"flop_counts",
|
|
93
|
+
"memory_estimates",
|
|
94
|
+
"detected_blocks",
|
|
95
|
+
"architecture_type",
|
|
96
|
+
"risk_signals",
|
|
97
|
+
"hardware_profile",
|
|
98
|
+
"hardware_estimates",
|
|
99
|
+
"llm_summary",
|
|
100
|
+
"dataset_info",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for section in expected_sections:
|
|
104
|
+
assert section in props, f"Missing schema section: {section}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.skipif(
|
|
108
|
+
not PYDANTIC_AVAILABLE and not JSONSCHEMA_AVAILABLE,
|
|
109
|
+
reason="Neither pydantic nor jsonschema installed",
|
|
110
|
+
)
|
|
111
|
+
class TestSchemaValidation:
|
|
112
|
+
"""Tests for schema validation (Pydantic preferred, jsonschema fallback)."""
|
|
113
|
+
|
|
114
|
+
def test_valid_report_passes_validation(self):
|
|
115
|
+
"""A properly generated report should pass validation."""
|
|
116
|
+
model = create_simple_model()
|
|
117
|
+
|
|
118
|
+
with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
|
|
119
|
+
onnx.save(model, f.name)
|
|
120
|
+
model_path = Path(f.name)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
inspector = ModelInspector()
|
|
124
|
+
report = inspector.inspect(model_path)
|
|
125
|
+
|
|
126
|
+
is_valid, errors = report.validate()
|
|
127
|
+
assert is_valid, f"Validation failed: {errors}"
|
|
128
|
+
assert len(errors) == 0
|
|
129
|
+
finally:
|
|
130
|
+
model_path.unlink()
|
|
131
|
+
|
|
132
|
+
def test_validate_report_function(self):
|
|
133
|
+
"""Test the validate_report function directly."""
|
|
134
|
+
model = create_simple_model()
|
|
135
|
+
|
|
136
|
+
with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
|
|
137
|
+
onnx.save(model, f.name)
|
|
138
|
+
model_path = Path(f.name)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
inspector = ModelInspector()
|
|
142
|
+
report = inspector.inspect(model_path)
|
|
143
|
+
report_dict = report.to_dict()
|
|
144
|
+
|
|
145
|
+
is_valid, errors = validate_report(report_dict)
|
|
146
|
+
assert is_valid
|
|
147
|
+
assert len(errors) == 0
|
|
148
|
+
finally:
|
|
149
|
+
model_path.unlink()
|
|
150
|
+
|
|
151
|
+
def test_invalid_report_fails_validation(self):
|
|
152
|
+
"""An invalid report should fail validation."""
|
|
153
|
+
# Missing required fields
|
|
154
|
+
invalid_report = {
|
|
155
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
156
|
+
# Missing metadata and autodoc_version
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
is_valid, errors = validate_report(invalid_report)
|
|
160
|
+
assert not is_valid
|
|
161
|
+
assert len(errors) > 0
|
|
162
|
+
assert any("metadata" in e for e in errors)
|
|
163
|
+
|
|
164
|
+
def test_invalid_metadata_fails_validation(self):
|
|
165
|
+
"""Invalid metadata should fail validation."""
|
|
166
|
+
invalid_report = {
|
|
167
|
+
"metadata": {
|
|
168
|
+
"path": "/test/model.onnx",
|
|
169
|
+
"ir_version": "not_an_integer", # Should be int
|
|
170
|
+
"producer_name": "test",
|
|
171
|
+
"opsets": {"": 17},
|
|
172
|
+
},
|
|
173
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
174
|
+
"autodoc_version": "0.1.0",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
is_valid, errors = validate_report(invalid_report)
|
|
178
|
+
assert not is_valid
|
|
179
|
+
assert any("ir_version" in e for e in errors)
|
|
180
|
+
|
|
181
|
+
def test_validate_strict_raises_on_invalid(self):
|
|
182
|
+
"""validate_report_strict should raise ValidationError."""
|
|
183
|
+
invalid_report = {"not_valid": True}
|
|
184
|
+
|
|
185
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
186
|
+
validate_report_strict(invalid_report)
|
|
187
|
+
|
|
188
|
+
assert len(exc_info.value.errors) > 0
|
|
189
|
+
|
|
190
|
+
def test_report_validate_strict_method(self):
|
|
191
|
+
"""Test the validate_strict method on InspectionReport."""
|
|
192
|
+
model = create_simple_model()
|
|
193
|
+
|
|
194
|
+
with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
|
|
195
|
+
onnx.save(model, f.name)
|
|
196
|
+
model_path = Path(f.name)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
inspector = ModelInspector()
|
|
200
|
+
report = inspector.inspect(model_path)
|
|
201
|
+
|
|
202
|
+
# Should not raise
|
|
203
|
+
report.validate_strict()
|
|
204
|
+
finally:
|
|
205
|
+
model_path.unlink()
|
|
206
|
+
|
|
207
|
+
def test_architecture_type_enum_validation(self):
|
|
208
|
+
"""Architecture type should be one of the allowed values."""
|
|
209
|
+
valid_report = {
|
|
210
|
+
"metadata": {
|
|
211
|
+
"path": "/test/model.onnx",
|
|
212
|
+
"ir_version": 9,
|
|
213
|
+
"producer_name": "test",
|
|
214
|
+
"producer_version": "1.0",
|
|
215
|
+
"domain": "",
|
|
216
|
+
"model_version": 0,
|
|
217
|
+
"doc_string": "",
|
|
218
|
+
"opsets": {"": 17},
|
|
219
|
+
},
|
|
220
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
221
|
+
"autodoc_version": "0.1.0",
|
|
222
|
+
"architecture_type": "invalid_type", # Not in enum
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
is_valid, errors = validate_report(valid_report)
|
|
226
|
+
assert not is_valid
|
|
227
|
+
assert any("architecture_type" in e for e in errors)
|
|
228
|
+
|
|
229
|
+
def test_risk_signal_severity_enum(self):
|
|
230
|
+
"""Risk signal severity should be one of the allowed values."""
|
|
231
|
+
valid_report = {
|
|
232
|
+
"metadata": {
|
|
233
|
+
"path": "/test/model.onnx",
|
|
234
|
+
"ir_version": 9,
|
|
235
|
+
"producer_name": "test",
|
|
236
|
+
"producer_version": "",
|
|
237
|
+
"domain": "",
|
|
238
|
+
"model_version": 0,
|
|
239
|
+
"doc_string": "",
|
|
240
|
+
"opsets": {"": 17},
|
|
241
|
+
},
|
|
242
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
243
|
+
"autodoc_version": "0.1.0",
|
|
244
|
+
"risk_signals": [
|
|
245
|
+
{
|
|
246
|
+
"id": "test_risk",
|
|
247
|
+
"severity": "critical", # Invalid - should be info/warning/high
|
|
248
|
+
"description": "Test risk",
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
is_valid, errors = validate_report(valid_report)
|
|
254
|
+
assert not is_valid
|
|
255
|
+
assert any("severity" in e for e in errors)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="pydantic not installed")
|
|
259
|
+
class TestPydanticValidation:
|
|
260
|
+
"""Tests specific to Pydantic validation."""
|
|
261
|
+
|
|
262
|
+
def test_validate_with_pydantic_returns_model(self):
|
|
263
|
+
"""validate_with_pydantic should return a Pydantic model instance."""
|
|
264
|
+
model = create_simple_model()
|
|
265
|
+
|
|
266
|
+
with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
|
|
267
|
+
onnx.save(model, f.name)
|
|
268
|
+
model_path = Path(f.name)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
inspector = ModelInspector()
|
|
272
|
+
report = inspector.inspect(model_path)
|
|
273
|
+
report_dict = report.to_dict()
|
|
274
|
+
|
|
275
|
+
pydantic_model = validate_with_pydantic(report_dict)
|
|
276
|
+
assert pydantic_model is not None
|
|
277
|
+
assert hasattr(pydantic_model, "metadata")
|
|
278
|
+
assert hasattr(pydantic_model, "generated_at")
|
|
279
|
+
assert hasattr(pydantic_model, "autodoc_version")
|
|
280
|
+
finally:
|
|
281
|
+
model_path.unlink()
|
|
282
|
+
|
|
283
|
+
def test_validate_with_pydantic_invalid_raises(self):
|
|
284
|
+
"""validate_with_pydantic should raise ValidationError on invalid input."""
|
|
285
|
+
invalid_report = {"not_valid": True}
|
|
286
|
+
|
|
287
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
288
|
+
validate_with_pydantic(invalid_report)
|
|
289
|
+
|
|
290
|
+
assert len(exc_info.value.errors) > 0
|
|
291
|
+
|
|
292
|
+
def test_pydantic_model_has_correct_types(self):
|
|
293
|
+
"""Pydantic model should have correct field types after parsing."""
|
|
294
|
+
model = create_simple_model()
|
|
295
|
+
|
|
296
|
+
with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
|
|
297
|
+
onnx.save(model, f.name)
|
|
298
|
+
model_path = Path(f.name)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
inspector = ModelInspector()
|
|
302
|
+
report = inspector.inspect(model_path)
|
|
303
|
+
report_dict = report.to_dict()
|
|
304
|
+
|
|
305
|
+
pydantic_model = validate_with_pydantic(report_dict)
|
|
306
|
+
assert pydantic_model is not None
|
|
307
|
+
|
|
308
|
+
# Check metadata structure
|
|
309
|
+
assert pydantic_model.metadata is not None
|
|
310
|
+
assert isinstance(pydantic_model.metadata.path, str)
|
|
311
|
+
assert isinstance(pydantic_model.metadata.ir_version, int)
|
|
312
|
+
|
|
313
|
+
# Check autodoc_version is a string
|
|
314
|
+
assert isinstance(pydantic_model.autodoc_version, str)
|
|
315
|
+
finally:
|
|
316
|
+
model_path.unlink()
|
|
317
|
+
|
|
318
|
+
def test_pydantic_schema_matches_structure(self):
|
|
319
|
+
"""Pydantic-generated schema should have expected structure."""
|
|
320
|
+
schema = get_schema()
|
|
321
|
+
|
|
322
|
+
# Pydantic schema should have these elements
|
|
323
|
+
assert "properties" in schema
|
|
324
|
+
assert "required" in schema
|
|
325
|
+
|
|
326
|
+
# Check key properties exist
|
|
327
|
+
props = schema["properties"]
|
|
328
|
+
assert "metadata" in props
|
|
329
|
+
assert "generated_at" in props
|
|
330
|
+
assert "autodoc_version" in props
|
|
331
|
+
|
|
332
|
+
def test_pydantic_validation_error_messages_are_readable(self):
|
|
333
|
+
"""Pydantic validation errors should be human-readable."""
|
|
334
|
+
invalid_report = {
|
|
335
|
+
"metadata": {
|
|
336
|
+
"path": "/test/model.onnx",
|
|
337
|
+
"ir_version": "invalid", # Should be int
|
|
338
|
+
"producer_name": "test",
|
|
339
|
+
"opsets": {"": 17},
|
|
340
|
+
},
|
|
341
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
342
|
+
"autodoc_version": "0.1.0",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
is_valid, errors = validate_report(invalid_report)
|
|
346
|
+
assert not is_valid
|
|
347
|
+
assert len(errors) > 0
|
|
348
|
+
# Error message should contain the field path
|
|
349
|
+
error_str = " ".join(errors)
|
|
350
|
+
assert "ir_version" in error_str.lower()
|
|
351
|
+
|
|
352
|
+
def test_pydantic_validates_nested_objects(self):
|
|
353
|
+
"""Pydantic should validate nested objects correctly."""
|
|
354
|
+
# Valid nested structure
|
|
355
|
+
valid_report = {
|
|
356
|
+
"metadata": {
|
|
357
|
+
"path": "/test/model.onnx",
|
|
358
|
+
"ir_version": 9,
|
|
359
|
+
"producer_name": "test",
|
|
360
|
+
"producer_version": "",
|
|
361
|
+
"domain": "",
|
|
362
|
+
"model_version": 0,
|
|
363
|
+
"doc_string": "",
|
|
364
|
+
"opsets": {"": 17},
|
|
365
|
+
},
|
|
366
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
367
|
+
"autodoc_version": "0.1.0",
|
|
368
|
+
"param_counts": {
|
|
369
|
+
"total": 1000,
|
|
370
|
+
"trainable": 1000,
|
|
371
|
+
"non_trainable": 0,
|
|
372
|
+
"by_op_type": {"Conv": 500, "MatMul": 500},
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
is_valid, errors = validate_report(valid_report)
|
|
377
|
+
assert is_valid, f"Validation failed: {errors}"
|
|
378
|
+
|
|
379
|
+
def test_pydantic_rejects_wrong_nested_types(self):
|
|
380
|
+
"""Pydantic should reject wrong types in nested objects."""
|
|
381
|
+
invalid_report = {
|
|
382
|
+
"metadata": {
|
|
383
|
+
"path": "/test/model.onnx",
|
|
384
|
+
"ir_version": 9,
|
|
385
|
+
"producer_name": "test",
|
|
386
|
+
"producer_version": "",
|
|
387
|
+
"domain": "",
|
|
388
|
+
"model_version": 0,
|
|
389
|
+
"doc_string": "",
|
|
390
|
+
"opsets": {"": 17},
|
|
391
|
+
},
|
|
392
|
+
"generated_at": "2025-01-01T00:00:00Z",
|
|
393
|
+
"autodoc_version": "0.1.0",
|
|
394
|
+
"param_counts": {
|
|
395
|
+
"total": "not_a_number", # Should be int
|
|
396
|
+
"trainable": 1000,
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
is_valid, errors = validate_report(invalid_report)
|
|
401
|
+
assert not is_valid
|
|
402
|
+
error_str = " ".join(errors)
|
|
403
|
+
assert "total" in error_str.lower() or "param_counts" in error_str.lower()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class TestSchemaWithoutJsonschema:
|
|
407
|
+
"""Tests for behavior when jsonschema is not installed."""
|
|
408
|
+
|
|
409
|
+
def test_get_schema_always_works(self):
|
|
410
|
+
"""get_schema should work regardless of jsonschema."""
|
|
411
|
+
schema = get_schema()
|
|
412
|
+
assert isinstance(schema, dict)
|
|
413
|
+
assert "properties" in schema
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
pytest.main([__file__, "-v"])
|