1
1
from graphlib import CycleError
2
- from typing import Any
2
+ from typing import Any , AsyncGenerator
3
3
4
4
from port_ocean .clients .port .client import PortClient
5
5
from port_ocean .core .utils .entity_topological_sorter import EntityTopologicalSorter
25
25
from dataclasses import dataclass
26
26
from typing import List , Optional
27
27
from port_ocean .tests .core .conftest import create_entity , no_op_event_context
28
+ from port_ocean .helpers .metric .metric import Metrics
29
+ from port_ocean .config .settings import MetricsSettings , IntegrationSettings
28
30
29
31
30
32
@pytest .fixture
@@ -37,7 +39,47 @@ def mock_sync_raw_mixin(
37
39
sync_raw_mixin ._entity_processor = mock_entity_processor
38
40
sync_raw_mixin ._entities_state_applier = mock_entities_state_applier
39
41
sync_raw_mixin ._port_app_config_handler = mock_port_app_config_handler
40
- sync_raw_mixin ._get_resource_raw_results = AsyncMock (return_value = ([{}], [])) # type: ignore
42
+
43
+ # Create raw data that matches the entity structure
44
+ raw_data = [
45
+ {
46
+ "id" : "entity_1" ,
47
+ "name" : "Entity 1" ,
48
+ "service" : "entity_3" ,
49
+ "web_url" : "https://example.com/entity1" ,
50
+ },
51
+ {
52
+ "id" : "entity_2" ,
53
+ "name" : "Entity 2" ,
54
+ "service" : "entity_4" ,
55
+ "web_url" : "https://example.com/entity2" ,
56
+ },
57
+ {
58
+ "id" : "entity_3" ,
59
+ "name" : "Entity 3" ,
60
+ "service" : "" ,
61
+ "web_url" : "https://example.com/entity3" ,
62
+ },
63
+ {
64
+ "id" : "entity_4" ,
65
+ "name" : "Entity 4" ,
66
+ "service" : "entity_3" ,
67
+ "web_url" : "https://example.com/entity4" ,
68
+ },
69
+ {
70
+ "id" : "entity_5" ,
71
+ "name" : "Entity 5" ,
72
+ "service" : "entity_1" ,
73
+ "web_url" : "https://example.com/entity5" ,
74
+ },
75
+ ]
76
+
77
+ # Create an async generator that yields the raw data
78
+ async def raw_results_generator () -> AsyncGenerator [list [dict [str , Any ]], None ]:
79
+ yield raw_data
80
+
81
+ # Return a list containing the async generator and an empty error list
82
+ sync_raw_mixin ._get_resource_raw_results = AsyncMock (return_value = ([raw_results_generator ()], [])) # type: ignore
41
83
sync_raw_mixin ._entity_processor .parse_items = AsyncMock (return_value = MagicMock ()) # type: ignore
42
84
43
85
return sync_raw_mixin
@@ -62,9 +104,15 @@ def mock_ocean(mock_port_client: PortClient) -> Ocean:
62
104
ocean_mock .config .port = MagicMock ()
63
105
ocean_mock .config .port .port_app_config_cache_ttl = 60
64
106
ocean_mock .port_client = mock_port_client
65
- ocean_mock .metrics = MagicMock ()
66
- ocean_mock .metrics .post_metrics = AsyncMock ()
67
- ocean_mock .metrics .flush = AsyncMock ()
107
+
108
+ # Create real metrics instance
109
+ metrics_settings = MetricsSettings (enabled = True )
110
+ integration_settings = IntegrationSettings (type = "test" , identifier = "test" )
111
+ ocean_mock .metrics = Metrics (
112
+ metrics_settings = metrics_settings ,
113
+ integration_configuration = integration_settings ,
114
+ port_client = mock_port_client ,
115
+ )
68
116
69
117
return ocean_mock
70
118
@@ -81,13 +129,17 @@ async def test_sync_raw_mixin_self_dependency(
81
129
entities = [create_entity (* entity_param ) for entity_param in entities_params ]
82
130
83
131
calc_result_mock = MagicMock ()
84
- calc_result_mock .entity_selector_diff .passed = entities
85
- calc_result_mock .errors = []
132
+ calc_result_mock .entity_selector_diff = EntitySelectorDiff (
133
+ passed = entities , failed = [] # No failed entities in this test case
134
+ )
135
+ calc_result_mock .errors = [] # No errors in this test case
136
+ calc_result_mock .number_of_transformed_entities = len (
137
+ entities
138
+ ) # Add this to match real behavior
139
+ calc_result_mock .misonfigured_entity_keys = {} # Add this to match real behavior
86
140
87
141
mock_sync_raw_mixin .entity_processor .parse_items = AsyncMock (return_value = calc_result_mock ) # type: ignore
88
- mock_ocean .metrics .report_sync_metrics = AsyncMock (return_value = None ) # type: ignore
89
- mock_ocean .metrics .report_kind_sync_metrics = AsyncMock (return_value = None ) # type: ignore
90
- mock_ocean .metrics .send_metrics_to_webhook = AsyncMock (return_value = None ) # type: ignore
142
+
91
143
mock_order_by_entities_dependencies = MagicMock (
92
144
side_effect = EntityTopologicalSorter .order_by_entities_dependencies
93
145
)
@@ -126,6 +178,50 @@ async def test_sync_raw_mixin_self_dependency(
126
178
for call in mock_order_by_entities_dependencies .call_args_list
127
179
] == [entity for entity in entities if entity .identifier == "entity_1" ]
128
180
181
+ # Add assertions for actual metrics
182
+ metrics = mock_ocean .metrics .generate_metrics ()
183
+ assert len (metrics ) == 2
184
+
185
+ # Verify object counts
186
+ for metric in metrics :
187
+ if metric ["kind" ] == "project" :
188
+ assert (
189
+ metric ["metrics" ]["phase" ]["extract" ]["object_count_type" ][
190
+ "raw_extracted"
191
+ ]["object_count" ]
192
+ == 5
193
+ )
194
+ assert (
195
+ metric ["metrics" ]["phase" ]["transform" ][
196
+ "object_count_type"
197
+ ]["transformed" ]["object_count" ]
198
+ == 2
199
+ )
200
+ assert (
201
+ metric ["metrics" ]["phase" ]["transform" ][
202
+ "object_count_type"
203
+ ]["filtered_out" ]["object_count" ]
204
+ == 3
205
+ )
206
+ assert (
207
+ metric ["metrics" ]["phase" ]["load" ]["object_count_type" ][
208
+ "failed"
209
+ ]["object_count" ]
210
+ == 1
211
+ )
212
+ assert (
213
+ metric ["metrics" ]["phase" ]["load" ]["object_count_type" ][
214
+ "loaded"
215
+ ]["object_count" ]
216
+ == 1
217
+ )
218
+
219
+ # Verify success
220
+ assert metric ["metrics" ]["phase" ]["resync" ]["success" ] == 1
221
+
222
+ # Verify sync state
223
+ assert metric ["syncState" ] == "completed"
224
+
129
225
130
226
@pytest .mark .asyncio
131
227
async def test_sync_raw_mixin_circular_dependency (
@@ -138,13 +234,17 @@ async def test_sync_raw_mixin_circular_dependency(
138
234
entities = [create_entity (* entity_param ) for entity_param in entities_params ]
139
235
140
236
calc_result_mock = MagicMock ()
141
- calc_result_mock .entity_selector_diff .passed = entities
142
- calc_result_mock .errors = []
237
+ calc_result_mock .entity_selector_diff = EntitySelectorDiff (
238
+ passed = entities , failed = [] # No failed entities in this test case
239
+ )
240
+ calc_result_mock .errors = [] # No errors in this test case
241
+ calc_result_mock .number_of_transformed_entities = len (
242
+ entities
243
+ ) # Add this to match real behavior
244
+ calc_result_mock .misonfigured_entity_keys = {} # Add this to match real behavior
143
245
144
246
mock_sync_raw_mixin .entity_processor .parse_items = AsyncMock (return_value = calc_result_mock ) # type: ignore
145
- mock_ocean .metrics .report_sync_metrics = AsyncMock (return_value = None ) # type: ignore
146
- mock_ocean .metrics .report_kind_sync_metrics = AsyncMock (return_value = None ) # type: ignore
147
- mock_ocean .metrics .send_metrics_to_webhook = AsyncMock (return_value = None ) # type: ignore
247
+
148
248
mock_order_by_entities_dependencies = MagicMock (
149
249
side_effect = EntityTopologicalSorter .order_by_entities_dependencies
150
250
)
@@ -206,11 +306,56 @@ def handle_failed_wrapper(*args: Any, **kwargs: Any) -> Any:
206
306
== 2
207
307
)
208
308
309
+ # Add assertions for actual metrics
310
+ metrics = mock_ocean .metrics .generate_metrics ()
311
+ assert len (metrics ) == 2
312
+
313
+ # Verify object counts
314
+ for metric in metrics :
315
+ if metric ["kind" ] == "project" :
316
+ assert (
317
+ metric ["metrics" ]["phase" ]["extract" ]["object_count_type" ][
318
+ "raw_extracted"
319
+ ]["object_count" ]
320
+ == 5
321
+ )
322
+ assert (
323
+ metric ["metrics" ]["phase" ]["transform" ][
324
+ "object_count_type"
325
+ ]["transformed" ]["object_count" ]
326
+ == 2
327
+ )
328
+ assert (
329
+ metric ["metrics" ]["phase" ]["transform" ][
330
+ "object_count_type"
331
+ ]["filtered_out" ]["object_count" ]
332
+ == 3
333
+ )
334
+ assert (
335
+ metric ["metrics" ]["phase" ]["load" ]["object_count_type" ][
336
+ "failed"
337
+ ]["object_count" ]
338
+ == 2
339
+ )
340
+ assert (
341
+ metric ["metrics" ]["phase" ]["load" ]["object_count_type" ][
342
+ "loaded"
343
+ ]["object_count" ]
344
+ == 0
345
+ )
346
+
347
+ # Verify success
348
+ assert metric ["metrics" ]["phase" ]["resync" ]["success" ] == 1
349
+
350
+ # Verify sync state
351
+ assert metric ["syncState" ] == "completed"
352
+
209
353
210
354
@pytest .mark .asyncio
211
355
async def test_sync_raw_mixin_dependency (
212
356
mock_sync_raw_mixin : SyncRawMixin , mock_ocean : Ocean
213
357
) -> None :
358
+ # Create entities with more realistic data
214
359
entities_params = [
215
360
("entity_1" , "service" , {"service" : "entity_3" }, True ),
216
361
("entity_2" , "service" , {"service" : "entity_4" }, True ),
@@ -219,13 +364,19 @@ async def test_sync_raw_mixin_dependency(
219
364
("entity_5" , "service" , {"service" : "entity_1" }, True ),
220
365
]
221
366
entities = [create_entity (* entity_param ) for entity_param in entities_params ]
222
- mock_ocean .metrics .report_sync_metrics = AsyncMock (return_value = None ) # type: ignore
223
- mock_ocean .metrics .report_kind_sync_metrics = AsyncMock (return_value = None ) # type: ignore
224
- mock_ocean .metrics .send_metrics_to_webhook = AsyncMock (return_value = None ) # type: ignore
367
+
368
+ # Create a more realistic CalculationResult mock
225
369
calc_result_mock = MagicMock ()
226
- calc_result_mock .entity_selector_diff .passed = entities
227
- calc_result_mock .errors = []
370
+ calc_result_mock .entity_selector_diff = EntitySelectorDiff (
371
+ passed = entities , failed = [] # No failed entities in this test case
372
+ )
373
+ calc_result_mock .errors = [] # No errors in this test case
374
+ calc_result_mock .number_of_transformed_entities = len (
375
+ entities
376
+ ) # Add this to match real behavior
377
+ calc_result_mock .misonfigured_entity_keys = {} # Add this to match real behavior
228
378
379
+ # Mock the parse_items method to return our realistic mock
229
380
mock_sync_raw_mixin .entity_processor .parse_items = AsyncMock (return_value = calc_result_mock ) # type: ignore
230
381
231
382
mock_order_by_entities_dependencies = MagicMock (
@@ -295,6 +446,44 @@ def get_entities_wrapper(*args: Any, **kwargs: Any) -> Any:
295
446
"entity_3-entity_1-entity_4-entity_5-entity_2" ,
296
447
)
297
448
449
+ # Add assertions for actual metrics
450
+ metrics = mock_ocean .metrics .generate_metrics ()
451
+ assert len (metrics ) == 2
452
+
453
+ # Verify object counts
454
+ for metric in metrics :
455
+ if metric ["kind" ] == "project" :
456
+ assert (
457
+ metric ["metrics" ]["phase" ]["extract" ]["object_count_type" ][
458
+ "raw_extracted"
459
+ ]["object_count" ]
460
+ == 5
461
+ )
462
+ assert (
463
+ metric ["metrics" ]["phase" ]["transform" ][
464
+ "object_count_type"
465
+ ]["transformed" ]["object_count" ]
466
+ == 5
467
+ )
468
+ assert (
469
+ metric ["metrics" ]["phase" ]["transform" ][
470
+ "object_count_type"
471
+ ]["filtered_out" ]["object_count" ]
472
+ == 0
473
+ )
474
+ assert (
475
+ metric ["metrics" ]["phase" ]["load" ]["object_count_type" ][
476
+ "failed"
477
+ ]["object_count" ]
478
+ == 5
479
+ )
480
+
481
+ # Verify success
482
+ assert metric ["metrics" ]["phase" ]["resync" ]["success" ] == 1
483
+
484
+ # Verify sync state
485
+ assert metric ["syncState" ] == "completed"
486
+
298
487
299
488
@pytest .mark .asyncio
300
489
async def test_register_raw (
0 commit comments