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.
Files changed (70) hide show
  1. haoline/.streamlit/config.toml +10 -0
  2. haoline/__init__.py +248 -0
  3. haoline/analyzer.py +935 -0
  4. haoline/cli.py +2712 -0
  5. haoline/compare.py +811 -0
  6. haoline/compare_visualizations.py +1564 -0
  7. haoline/edge_analysis.py +525 -0
  8. haoline/eval/__init__.py +131 -0
  9. haoline/eval/adapters.py +844 -0
  10. haoline/eval/cli.py +390 -0
  11. haoline/eval/comparison.py +542 -0
  12. haoline/eval/deployment.py +633 -0
  13. haoline/eval/schemas.py +833 -0
  14. haoline/examples/__init__.py +15 -0
  15. haoline/examples/basic_inspection.py +74 -0
  16. haoline/examples/compare_models.py +117 -0
  17. haoline/examples/hardware_estimation.py +78 -0
  18. haoline/format_adapters.py +1001 -0
  19. haoline/formats/__init__.py +123 -0
  20. haoline/formats/coreml.py +250 -0
  21. haoline/formats/gguf.py +483 -0
  22. haoline/formats/openvino.py +255 -0
  23. haoline/formats/safetensors.py +273 -0
  24. haoline/formats/tflite.py +369 -0
  25. haoline/hardware.py +2307 -0
  26. haoline/hierarchical_graph.py +462 -0
  27. haoline/html_export.py +1573 -0
  28. haoline/layer_summary.py +769 -0
  29. haoline/llm_summarizer.py +465 -0
  30. haoline/op_icons.py +618 -0
  31. haoline/operational_profiling.py +1492 -0
  32. haoline/patterns.py +1116 -0
  33. haoline/pdf_generator.py +265 -0
  34. haoline/privacy.py +250 -0
  35. haoline/pydantic_models.py +241 -0
  36. haoline/report.py +1923 -0
  37. haoline/report_sections.py +539 -0
  38. haoline/risks.py +521 -0
  39. haoline/schema.py +523 -0
  40. haoline/streamlit_app.py +2024 -0
  41. haoline/tests/__init__.py +4 -0
  42. haoline/tests/conftest.py +123 -0
  43. haoline/tests/test_analyzer.py +868 -0
  44. haoline/tests/test_compare_visualizations.py +293 -0
  45. haoline/tests/test_edge_analysis.py +243 -0
  46. haoline/tests/test_eval.py +604 -0
  47. haoline/tests/test_format_adapters.py +460 -0
  48. haoline/tests/test_hardware.py +237 -0
  49. haoline/tests/test_hardware_recommender.py +90 -0
  50. haoline/tests/test_hierarchical_graph.py +326 -0
  51. haoline/tests/test_html_export.py +180 -0
  52. haoline/tests/test_layer_summary.py +428 -0
  53. haoline/tests/test_llm_patterns.py +540 -0
  54. haoline/tests/test_llm_summarizer.py +339 -0
  55. haoline/tests/test_patterns.py +774 -0
  56. haoline/tests/test_pytorch.py +327 -0
  57. haoline/tests/test_report.py +383 -0
  58. haoline/tests/test_risks.py +398 -0
  59. haoline/tests/test_schema.py +417 -0
  60. haoline/tests/test_tensorflow.py +380 -0
  61. haoline/tests/test_visualizations.py +316 -0
  62. haoline/universal_ir.py +856 -0
  63. haoline/visualizations.py +1086 -0
  64. haoline/visualize_yolo.py +44 -0
  65. haoline/web.py +110 -0
  66. haoline-0.3.0.dist-info/METADATA +471 -0
  67. haoline-0.3.0.dist-info/RECORD +70 -0
  68. haoline-0.3.0.dist-info/WHEEL +4 -0
  69. haoline-0.3.0.dist-info/entry_points.txt +5 -0
  70. 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"])