vectorwave 0.1.5__tar.gz → 0.1.6__tar.gz
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.
- {vectorwave-0.1.5/src/vectorwave.egg-info → vectorwave-0.1.6}/PKG-INFO +1 -1
- {vectorwave-0.1.5 → vectorwave-0.1.6}/pyproject.toml +1 -1
- vectorwave-0.1.6/src/tests/conftest.py +27 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/database/test_db.py +3 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/models/test_db_config.py +36 -1
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/test_tracer.py +144 -68
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/search/test_execution_search.py +12 -7
- vectorwave-0.1.6/src/tests/utils/test_function_cahe.py +126 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/__init__.py +2 -1
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/core/decorator.py +39 -22
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/database/db.py +15 -6
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/database/db_search.py +100 -6
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/models/db_config.py +17 -2
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/tracer.py +140 -18
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/search/execution_search.py +19 -38
- vectorwave-0.1.6/src/vectorwave/utils/__init__.py +0 -0
- vectorwave-0.1.6/src/vectorwave/utils/function_cache.py +73 -0
- vectorwave-0.1.6/src/vectorwave/vectorizer/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6/src/vectorwave.egg-info}/PKG-INFO +1 -1
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/SOURCES.txt +5 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/LICENSE +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/MANIFEST.in +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/NOTICE +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/Readme.md +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/setup.cfg +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/batch/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/batch/test_batch.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/core/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/core/test_decorator.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/database/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/database/test_db_search.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/exception/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/models/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/alert/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/alert/test_alerter.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/test_async_trace.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/prediction/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/search/__init__.py +0 -0
- {vectorwave-0.1.5/src/tests/vectorizer → vectorwave-0.1.6/src/tests/utils}/__init__.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/batch → vectorwave-0.1.6/src/tests/vectorizer}/__init__.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/core → vectorwave-0.1.6/src/vectorwave/batch}/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/batch/batch.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/database → vectorwave-0.1.6/src/vectorwave/core}/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/core/core.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/exception → vectorwave-0.1.6/src/vectorwave/database}/__init__.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/models → vectorwave-0.1.6/src/vectorwave/exception}/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/exception/exceptions.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/monitoring → vectorwave-0.1.6/src/vectorwave/models}/__init__.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/monitoring/alert → vectorwave-0.1.6/src/vectorwave/monitoring}/__init__.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/prediction → vectorwave-0.1.6/src/vectorwave/monitoring/alert}/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/base.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/factory.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/monitoring.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/search → vectorwave-0.1.6/src/vectorwave/prediction}/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/prediction/predictor.py +0 -0
- {vectorwave-0.1.5/src/vectorwave/vectorizer → vectorwave-0.1.6/src/vectorwave/search}/__init__.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/search/extended_search.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/search/rag_search.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/base.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/factory.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/openai_vectorizer.py +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/dependency_links.txt +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/requires.txt +0 -0
- {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
CACHE_FILE_PATH = ".vectorwave_functions_cache.json"
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _delete_cache():
|
|
10
|
+
"""Helper function to remove the cache file if it exists."""
|
|
11
|
+
if os.path.exists(CACHE_FILE_PATH):
|
|
12
|
+
try:
|
|
13
|
+
os.remove(CACHE_FILE_PATH)
|
|
14
|
+
except OSError as e:
|
|
15
|
+
print(f"\n[CacheFixture Error] Failed to remove {CACHE_FILE_PATH}: {e}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True, scope="function")
|
|
19
|
+
def atomic_function_cache():
|
|
20
|
+
# --- SETUP (Before Test) ---
|
|
21
|
+
_delete_cache()
|
|
22
|
+
|
|
23
|
+
# --- Run the test ---
|
|
24
|
+
yield
|
|
25
|
+
|
|
26
|
+
# --- TEARDOWN (After Test) ---
|
|
27
|
+
_delete_cache()
|
|
@@ -355,6 +355,9 @@ def test_create_execution_schema_new(mock_vectorizer_none, test_settings):
|
|
|
355
355
|
assert "status" in passed_props_map
|
|
356
356
|
assert "duration_ms" in passed_props_map
|
|
357
357
|
|
|
358
|
+
assert "return_value" in passed_props_map
|
|
359
|
+
assert passed_props_map["return_value"].dataType == wvc.DataType.TEXT
|
|
360
|
+
|
|
358
361
|
assert collection == mock_new_collection
|
|
359
362
|
|
|
360
363
|
|
|
@@ -148,4 +148,39 @@ def test_get_settings_loads_global_custom_values(mock_env_get, mock_open_file, m
|
|
|
148
148
|
assert settings.global_custom_values["run_id"] == "test-run-123"
|
|
149
149
|
|
|
150
150
|
# "EXPERIMENT_ID" should not be included as os.environ.get returned None.
|
|
151
|
-
assert "experiment_id" not in settings.global_custom_values
|
|
151
|
+
assert "experiment_id" not in settings.global_custom_values
|
|
152
|
+
|
|
153
|
+
@patch('os.path.exists', return_value=False)
|
|
154
|
+
def test_get_settings_parses_sensitive_keys(mock_exists, monkeypatch):
|
|
155
|
+
"""
|
|
156
|
+
Tests whether the SENSITIVE_FIELD_NAMES environment variable is correctly parsed
|
|
157
|
+
and converted into a lowercase set (sensitive_keys).
|
|
158
|
+
"""
|
|
159
|
+
monkeypatch.setenv("SENSITIVE_FIELD_NAMES", "password, Token, api_key,, secret ")
|
|
160
|
+
|
|
161
|
+
get_weaviate_settings.cache_clear()
|
|
162
|
+
|
|
163
|
+
# 2. Act
|
|
164
|
+
settings = get_weaviate_settings()
|
|
165
|
+
|
|
166
|
+
# 3. Assert
|
|
167
|
+
assert isinstance(settings.sensitive_keys, set)
|
|
168
|
+
assert len(settings.sensitive_keys) == 4 # 이제 4개가 맞습니다.
|
|
169
|
+
assert settings.sensitive_keys == {"password", "token", "api_key", "secret"}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# [MODIFIED] @patch('os.environ.get', return_value=None) 제거, monkeypatch 인자 추가
|
|
173
|
+
@patch('os.path.exists', return_value=False)
|
|
174
|
+
def test_get_settings_sensitive_keys_default(mock_exists, monkeypatch): # <-- FIX: monkeypatch 사용
|
|
175
|
+
"""
|
|
176
|
+
Test when there is no SENSITIVE_FIELD_NAMES environment variable set default
|
|
177
|
+
"""
|
|
178
|
+
get_weaviate_settings.cache_clear()
|
|
179
|
+
monkeypatch.delenv("SENSITIVE_FIELD_NAMES", raising=False)
|
|
180
|
+
|
|
181
|
+
# 2. Act
|
|
182
|
+
settings = get_weaviate_settings()
|
|
183
|
+
|
|
184
|
+
# 3. Assert: check default for 5
|
|
185
|
+
assert settings.sensitive_keys == {"password", "api_key", "token", "secret", "auth_token"}
|
|
186
|
+
assert len(settings.sensitive_keys) == 5
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from unittest.mock import MagicMock
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
import time
|
|
4
6
|
|
|
5
|
-
from vectorwave.monitoring.tracer import trace_root, trace_span
|
|
7
|
+
from vectorwave.monitoring.tracer import trace_root, trace_span, _mask_and_serialize
|
|
8
|
+
|
|
6
9
|
from vectorwave.models.db_config import WeaviateSettings
|
|
7
10
|
|
|
8
11
|
# --- Import real functions for cache clearing ---
|
|
9
12
|
from vectorwave.batch.batch import get_batch_manager as real_get_batch_manager
|
|
10
13
|
from vectorwave.database.db import get_cached_client as real_get_cached_client
|
|
11
14
|
from vectorwave.models.db_config import get_weaviate_settings as real_get_settings
|
|
12
|
-
from vectorwave.monitoring.tracer import TraceCollector, current_tracer_var
|
|
15
|
+
from vectorwave.monitoring.tracer import TraceCollector, current_tracer_var, current_span_id_var
|
|
13
16
|
from vectorwave.monitoring.alert.base import BaseAlerter
|
|
14
17
|
|
|
15
18
|
# Module paths to mock (adjust to your project structure if needed)
|
|
@@ -39,7 +42,8 @@ def mock_tracer_deps(monkeypatch):
|
|
|
39
42
|
EXECUTION_COLLECTION_NAME="TestExecutions",
|
|
40
43
|
custom_properties=None, # Not important for this test
|
|
41
44
|
global_custom_values={"run_id": "global-run-abc", "env": "test"},
|
|
42
|
-
failure_mapping={"ValueError": "INVALID_INPUT"}
|
|
45
|
+
failure_mapping={"ValueError": "INVALID_INPUT"},
|
|
46
|
+
sensitive_keys={"password", "token", "api_key"}
|
|
43
47
|
)
|
|
44
48
|
mock_get_settings = MagicMock(return_value=mock_settings)
|
|
45
49
|
|
|
@@ -73,16 +77,18 @@ def mock_tracer_deps(monkeypatch):
|
|
|
73
77
|
|
|
74
78
|
def test_trace_root_and_span_success(mock_tracer_deps):
|
|
75
79
|
"""
|
|
76
|
-
Case 1: Success (Root + Span)
|
|
80
|
+
Case 1: Success (Root + Span)
|
|
81
|
+
[MODIFIED] Now also checks for parent_span_id hierarchy.
|
|
77
82
|
"""
|
|
78
|
-
mock_batch = mock_tracer_deps["batch"]
|
|
83
|
+
mock_batch = mock_tracer_deps["batch"].add_object # Get the add_object mock
|
|
79
84
|
mock_alerter = mock_tracer_deps["alerter"]
|
|
80
85
|
|
|
81
|
-
@trace_span
|
|
86
|
+
@trace_span # Child
|
|
82
87
|
def my_inner_span(x):
|
|
83
88
|
return f"result: {x}"
|
|
84
89
|
|
|
85
|
-
@trace_root()
|
|
90
|
+
@trace_root() # Root (sets up context)
|
|
91
|
+
@trace_span # Root Span (logs itself)
|
|
86
92
|
def my_workflow_root():
|
|
87
93
|
return my_inner_span(x=10)
|
|
88
94
|
|
|
@@ -91,34 +97,48 @@ def test_trace_root_and_span_success(mock_tracer_deps):
|
|
|
91
97
|
|
|
92
98
|
# --- Assert ---
|
|
93
99
|
assert result == "result: 10"
|
|
94
|
-
|
|
100
|
+
# 2 calls: 1 (inner_span) + 1 (workflow_root)
|
|
101
|
+
assert mock_batch.call_count == 2
|
|
95
102
|
mock_alerter.notify.assert_not_called()
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
call_args_list = mock_batch.call_args_list
|
|
105
|
+
props_map = {
|
|
106
|
+
call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
|
|
107
|
+
for call in call_args_list
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
assert "my_inner_span" in props_map
|
|
111
|
+
assert "my_workflow_root" in props_map
|
|
112
|
+
|
|
113
|
+
root_props = props_map["my_workflow_root"]
|
|
114
|
+
inner_props = props_map["my_inner_span"]
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
assert
|
|
102
|
-
assert
|
|
103
|
-
assert
|
|
104
|
-
|
|
105
|
-
assert "trace_id"
|
|
106
|
-
assert
|
|
107
|
-
assert
|
|
116
|
+
# --- [NEW] Hierarchy Validation ---
|
|
117
|
+
assert root_props["parent_span_id"] is None # Root span has no parent
|
|
118
|
+
assert inner_props["parent_span_id"] is not None
|
|
119
|
+
assert inner_props["parent_span_id"] == root_props["span_id"] # Child's parent is Root
|
|
120
|
+
|
|
121
|
+
assert root_props["trace_id"] == inner_props["trace_id"]
|
|
122
|
+
assert root_props["status"] == "SUCCESS"
|
|
123
|
+
assert inner_props["status"] == "SUCCESS"
|
|
124
|
+
assert root_props["run_id"] == "global-run-abc"
|
|
125
|
+
assert inner_props["run_id"] == "global-run-abc"
|
|
108
126
|
|
|
109
127
|
|
|
110
128
|
def test_trace_span_failure(mock_tracer_deps):
|
|
111
129
|
"""
|
|
112
|
-
Case 2: Failure (Root + Failing Span)
|
|
130
|
+
Case 2: Failure (Root + Failing Span)
|
|
131
|
+
Now checks hierarchy and logs both root (success) and child (error).
|
|
113
132
|
"""
|
|
114
|
-
mock_batch = mock_tracer_deps["batch"]
|
|
133
|
+
mock_batch = mock_tracer_deps["batch"].add_object # Get the add_object mock
|
|
115
134
|
mock_alerter = mock_tracer_deps["alerter"]
|
|
116
135
|
|
|
117
|
-
@trace_span
|
|
136
|
+
@trace_span # Child (Failing)
|
|
118
137
|
def my_failing_span():
|
|
119
138
|
raise ValueError("This is a test error")
|
|
120
139
|
|
|
121
|
-
@trace_root()
|
|
140
|
+
@trace_root() # Root (sets up context)
|
|
141
|
+
@trace_span # Root Span (Success)
|
|
122
142
|
def my_workflow_root_fail():
|
|
123
143
|
my_failing_span()
|
|
124
144
|
|
|
@@ -127,34 +147,46 @@ def test_trace_span_failure(mock_tracer_deps):
|
|
|
127
147
|
my_workflow_root_fail()
|
|
128
148
|
|
|
129
149
|
# --- Assert (Log) ---
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
assert db_props["status"] == "ERROR"
|
|
133
|
-
assert "ValueError: This is a test error" in db_props["error_message"]
|
|
134
|
-
assert db_props["error_code"] == "INVALID_INPUT"
|
|
150
|
+
# 2 calls: 1 (root_fail - SUCCESS) + 1 (failing_span - ERROR)
|
|
151
|
+
assert mock_batch.call_count == 2
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
call_args_list = mock_batch.call_args_list
|
|
154
|
+
props_map = {
|
|
155
|
+
call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
|
|
156
|
+
for call in call_args_list
|
|
157
|
+
}
|
|
138
158
|
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
assert "my_workflow_root_fail" in props_map
|
|
160
|
+
assert "my_failing_span" in props_map
|
|
141
161
|
|
|
162
|
+
root_props = props_map["my_workflow_root_fail"]
|
|
163
|
+
failing_props = props_map["my_failing_span"]
|
|
142
164
|
|
|
143
|
-
|
|
165
|
+
# Root Span Asserts (SUCCESS)
|
|
166
|
+
assert root_props["status"] == "ERROR"
|
|
167
|
+
assert root_props["parent_span_id"] is None
|
|
168
|
+
|
|
169
|
+
# Failing Span Asserts (ERROR)
|
|
170
|
+
assert failing_props["status"] == "ERROR"
|
|
171
|
+
assert "ValueError: This is a test error" in failing_props["error_message"]
|
|
172
|
+
assert failing_props["error_code"] == "INVALID_INPUT"
|
|
173
|
+
assert failing_props["parent_span_id"] == root_props["span_id"]
|
|
174
|
+
assert failing_props["run_id"] == "global-run-abc"
|
|
175
|
+
|
|
176
|
+
# Alerter Asserts
|
|
177
|
+
mock_alerter.notify.assert_called_once()
|
|
178
|
+
alert_props = mock_alerter.notify.call_args.args[0]
|
|
179
|
+
|
|
180
|
+
assert alert_props == failing_props # Alerter was called with the failing span's properties
|
|
144
181
|
assert alert_props["status"] == "ERROR"
|
|
145
182
|
assert alert_props["error_code"] == "INVALID_INPUT"
|
|
146
|
-
assert props["status"] == "ERROR"
|
|
147
|
-
assert "ValueError: This is a test error" in props["error_message"]
|
|
148
|
-
assert kwargs["properties"]["error_code"] == "INVALID_INPUT"
|
|
149
|
-
assert props["function_name"] == "my_failing_span"
|
|
150
|
-
assert props["run_id"] == "global-run-abc"
|
|
151
183
|
|
|
152
184
|
|
|
153
185
|
def test_span_without_root_does_nothing(mock_tracer_deps):
|
|
154
186
|
"""
|
|
155
187
|
Case 3: Tracing disabled (Span only) - If there's no Root, nothing should be recorded.
|
|
156
188
|
"""
|
|
157
|
-
mock_batch = mock_tracer_deps["batch"]
|
|
189
|
+
mock_batch = mock_tracer_deps["batch"].add_object
|
|
158
190
|
|
|
159
191
|
@trace_span
|
|
160
192
|
def my_lonely_span():
|
|
@@ -165,60 +197,78 @@ def test_span_without_root_does_nothing(mock_tracer_deps):
|
|
|
165
197
|
|
|
166
198
|
# --- Assert ---
|
|
167
199
|
assert result == "lonely_result"
|
|
168
|
-
mock_batch.
|
|
200
|
+
mock_batch.assert_not_called()
|
|
169
201
|
|
|
170
202
|
|
|
171
203
|
def test_span_captures_attributes_and_overrides_globals(mock_tracer_deps):
|
|
172
204
|
"""
|
|
173
205
|
Case 4/5: Attribute Capturing and Overriding
|
|
206
|
+
Checks hierarchy.
|
|
174
207
|
"""
|
|
175
|
-
mock_batch = mock_tracer_deps["batch"]
|
|
208
|
+
mock_batch = mock_tracer_deps["batch"].add_object
|
|
176
209
|
|
|
177
210
|
class MyObject:
|
|
178
211
|
def __str__(self): return "MyObjectInstance"
|
|
179
212
|
|
|
180
|
-
@trace_span(attributes_to_capture=["team", "priority", "run_id", "user_obj"])
|
|
181
|
-
def my_span_with_attrs(team, priority, run_id, user_obj, other_arg="default"):
|
|
213
|
+
@trace_span(attributes_to_capture=["team", "priority", "run_id", "user_obj", "password"])
|
|
214
|
+
def my_span_with_attrs(team, priority, run_id, user_obj, password, other_arg="default"):
|
|
182
215
|
return "captured"
|
|
183
216
|
|
|
184
217
|
@trace_root()
|
|
218
|
+
@trace_span # Root span
|
|
185
219
|
def my_workflow_root_attrs():
|
|
186
220
|
return my_span_with_attrs(
|
|
187
221
|
team="backend",
|
|
188
222
|
priority=1,
|
|
189
223
|
run_id="override-run-xyz", # <-- This should override "global-run-abc"
|
|
190
224
|
user_obj=MyObject(),
|
|
191
|
-
other_arg="should-be-ignored"
|
|
225
|
+
other_arg="should-be-ignored",
|
|
226
|
+
password="my_secret_password_123"
|
|
192
227
|
)
|
|
193
228
|
|
|
194
229
|
# --- Act ---
|
|
195
230
|
my_workflow_root_attrs()
|
|
196
231
|
|
|
197
232
|
# --- Assert ---
|
|
198
|
-
mock_batch.
|
|
199
|
-
|
|
233
|
+
assert mock_batch.call_count == 2
|
|
234
|
+
|
|
235
|
+
props_map = {
|
|
236
|
+
call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
|
|
237
|
+
for call in mock_batch.call_args_list
|
|
238
|
+
}
|
|
239
|
+
root_props = props_map["my_workflow_root_attrs"]
|
|
240
|
+
child_props = props_map["my_span_with_attrs"]
|
|
241
|
+
|
|
242
|
+
# Hierarchy
|
|
243
|
+
assert root_props["parent_span_id"] is None
|
|
244
|
+
assert child_props["parent_span_id"] == root_props["span_id"]
|
|
200
245
|
|
|
201
|
-
|
|
202
|
-
assert
|
|
203
|
-
assert
|
|
204
|
-
assert
|
|
205
|
-
assert
|
|
206
|
-
assert "
|
|
246
|
+
# Child properties (existing test)
|
|
247
|
+
assert child_props["team"] == "backend"
|
|
248
|
+
assert child_props["priority"] == 1
|
|
249
|
+
assert child_props["user_obj"] == "MyObjectInstance"
|
|
250
|
+
assert child_props["run_id"] == "override-run-xyz" # Overridden
|
|
251
|
+
assert child_props["env"] == "test" # Non-overridden global remains
|
|
252
|
+
assert "other_arg" not in child_props
|
|
253
|
+
|
|
254
|
+
assert "password" in child_props
|
|
255
|
+
assert child_props["password"] == "[MASKED]"
|
|
207
256
|
|
|
208
257
|
|
|
209
258
|
def test_root_accepts_custom_trace_id(mock_tracer_deps):
|
|
210
259
|
"""
|
|
211
260
|
Bonus: Test case for manually providing a 'trace_id'.
|
|
212
|
-
|
|
261
|
+
Checks hierarchy and trace_id propagation.
|
|
213
262
|
"""
|
|
214
|
-
mock_batch = mock_tracer_deps["batch"]
|
|
263
|
+
mock_batch = mock_tracer_deps["batch"].add_object
|
|
215
264
|
|
|
216
265
|
@trace_span
|
|
217
266
|
def my_inner_span():
|
|
218
267
|
pass
|
|
219
268
|
|
|
220
|
-
@trace_root()
|
|
221
|
-
|
|
269
|
+
@trace_root() # Sets up context
|
|
270
|
+
@trace_span # Root span
|
|
271
|
+
def my_workflow_root_custom_id():
|
|
222
272
|
my_inner_span()
|
|
223
273
|
|
|
224
274
|
# --- Act ---
|
|
@@ -226,16 +276,30 @@ def test_root_accepts_custom_trace_id(mock_tracer_deps):
|
|
|
226
276
|
my_workflow_root_custom_id(trace_id="my-custom-trace-id-123")
|
|
227
277
|
|
|
228
278
|
# --- Assert ---
|
|
229
|
-
mock_batch.
|
|
230
|
-
|
|
279
|
+
assert mock_batch.call_count == 2
|
|
280
|
+
|
|
281
|
+
props_map = {
|
|
282
|
+
call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
|
|
283
|
+
for call in mock_batch.call_args_list
|
|
284
|
+
}
|
|
285
|
+
root_props = props_map["my_workflow_root_custom_id"]
|
|
286
|
+
child_props = props_map["my_inner_span"]
|
|
231
287
|
|
|
232
288
|
# Check if the trace_id was popped and injected correctly
|
|
233
|
-
assert
|
|
289
|
+
assert root_props["trace_id"] == "my-custom-trace-id-123"
|
|
290
|
+
assert child_props["trace_id"] == "my-custom-trace-id-123"
|
|
291
|
+
|
|
292
|
+
# Check hierarchy
|
|
293
|
+
assert root_props["parent_span_id"] is None
|
|
294
|
+
assert child_props["parent_span_id"] == root_props["span_id"]
|
|
234
295
|
|
|
235
296
|
|
|
236
297
|
def test_trace_span_error_code_priority_1_custom_attr(mock_tracer_deps):
|
|
237
|
-
"""
|
|
238
|
-
|
|
298
|
+
"""
|
|
299
|
+
Test (Priority 1) custom e.error_code attribute.
|
|
300
|
+
Also sets and checks parent_span_id.
|
|
301
|
+
"""
|
|
302
|
+
mock_batch = mock_tracer_deps["batch"].add_object
|
|
239
303
|
tracer = TraceCollector(trace_id="test_trace_p1")
|
|
240
304
|
tracer.settings = mock_tracer_deps["settings"]
|
|
241
305
|
|
|
@@ -243,21 +307,29 @@ def test_trace_span_error_code_priority_1_custom_attr(mock_tracer_deps):
|
|
|
243
307
|
def my_custom_fail_function():
|
|
244
308
|
raise CustomErrorWithCode("Test", "PAYMENT_FAILED_001")
|
|
245
309
|
|
|
246
|
-
|
|
310
|
+
# Set up mock parent context
|
|
311
|
+
tracer_token = current_tracer_var.set(tracer)
|
|
312
|
+
parent_span_token = current_span_id_var.set("mock-parent-id-123")
|
|
313
|
+
|
|
247
314
|
try:
|
|
248
315
|
with pytest.raises(CustomErrorWithCode):
|
|
249
316
|
my_custom_fail_function()
|
|
250
317
|
finally:
|
|
251
|
-
|
|
318
|
+
current_span_id_var.reset(parent_span_token)
|
|
319
|
+
current_tracer_var.reset(tracer_token)
|
|
252
320
|
|
|
253
|
-
args, kwargs = mock_batch.
|
|
321
|
+
args, kwargs = mock_batch.call_args
|
|
254
322
|
assert kwargs["properties"]["status"] == "ERROR"
|
|
255
323
|
assert kwargs["properties"]["error_code"] == "PAYMENT_FAILED_001"
|
|
324
|
+
assert kwargs["properties"]["parent_span_id"] == "mock-parent-id-123"
|
|
256
325
|
|
|
257
326
|
|
|
258
327
|
def test_trace_span_error_code_priority_3_default_class_name(mock_tracer_deps):
|
|
259
|
-
"""
|
|
260
|
-
|
|
328
|
+
"""
|
|
329
|
+
Test (Priority 3) default class name (KeyError is not in mapping).
|
|
330
|
+
Also sets and checks parent_span_id.
|
|
331
|
+
"""
|
|
332
|
+
mock_batch = mock_tracer_deps["batch"].add_object
|
|
261
333
|
tracer = TraceCollector(trace_id="test_trace_p3")
|
|
262
334
|
tracer.settings = mock_tracer_deps["settings"]
|
|
263
335
|
|
|
@@ -265,13 +337,17 @@ def test_trace_span_error_code_priority_3_default_class_name(mock_tracer_deps):
|
|
|
265
337
|
def my_key_error_function():
|
|
266
338
|
_ = {}["missing_key"] # Raises KeyError
|
|
267
339
|
|
|
268
|
-
|
|
340
|
+
tracer_token = current_tracer_var.set(tracer)
|
|
341
|
+
parent_span_token = current_span_id_var.set("mock-parent-id-456")
|
|
342
|
+
|
|
269
343
|
try:
|
|
270
344
|
with pytest.raises(KeyError):
|
|
271
345
|
my_key_error_function()
|
|
272
346
|
finally:
|
|
273
|
-
|
|
347
|
+
current_span_id_var.reset(parent_span_token)
|
|
348
|
+
current_tracer_var.reset(tracer_token)
|
|
274
349
|
|
|
275
|
-
args, kwargs = mock_batch.
|
|
350
|
+
args, kwargs = mock_batch.call_args
|
|
276
351
|
assert kwargs["properties"]["status"] == "ERROR"
|
|
277
352
|
assert kwargs["properties"]["error_code"] == "KeyError"
|
|
353
|
+
assert kwargs["properties"]["parent_span_id"] == "mock-parent-id-456"
|
|
@@ -66,17 +66,22 @@ MockDateTime.fromisoformat = MagicMock(side_effect=datetime.fromisoformat)
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
@patch('vectorwave.search.execution_search.datetime', MockDateTime)
|
|
69
|
-
@patch('vectorwave.search.execution_search.find_executions'
|
|
70
|
-
def test_find_recent_errors():
|
|
69
|
+
@patch('vectorwave.search.execution_search.find_executions')
|
|
70
|
+
def test_find_recent_errors(mock_find_executions):
|
|
71
71
|
"""
|
|
72
|
-
Tests if find_recent_errors
|
|
72
|
+
Tests if find_recent_errors calls find_executions with the correct filters.
|
|
73
|
+
(The test no longer checks manual filtering, as filtering is delegated to the DB).
|
|
73
74
|
"""
|
|
74
75
|
# 1. Run test
|
|
75
|
-
|
|
76
|
+
find_recent_errors(minutes_ago=10, error_codes=["INVALID_INPUT"])
|
|
77
|
+
|
|
78
|
+
# 2. Verify find_executions was called correctly
|
|
79
|
+
call_args = mock_find_executions.call_args
|
|
80
|
+
filters_arg = call_args.kwargs['filters']
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
assert
|
|
79
|
-
assert
|
|
82
|
+
assert filters_arg['status'] == 'ERROR'
|
|
83
|
+
assert filters_arg['error_code'] == 'INVALID_INPUT'
|
|
84
|
+
assert filters_arg['timestamp_utc__gte'] == expected_time_limit_iso
|
|
80
85
|
|
|
81
86
|
|
|
82
87
|
@patch('vectorwave.search.execution_search.find_executions')
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import json
|
|
3
|
+
from unittest.mock import patch, mock_open, ANY, MagicMock
|
|
4
|
+
from vectorwave.utils.function_cache import FunctionCacheManager
|
|
5
|
+
|
|
6
|
+
# --- 1. Tests for calculate_content_hash static method ---
|
|
7
|
+
|
|
8
|
+
def test_calculate_content_hash_consistency():
|
|
9
|
+
"""Tests that identical inputs always return the same hash."""
|
|
10
|
+
props1 = {"a": 1, "b": "test", "c": [1, 2, 3]}
|
|
11
|
+
props2 = {"a": 1, "b": "test", "c": [1, 2, 3]}
|
|
12
|
+
|
|
13
|
+
hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
|
|
14
|
+
hash2 = FunctionCacheManager.calculate_content_hash("id1", props2)
|
|
15
|
+
|
|
16
|
+
assert hash1 == hash2
|
|
17
|
+
assert len(hash1) == 64 # SHA256
|
|
18
|
+
|
|
19
|
+
def test_calculate_content_hash_sensitivity_props():
|
|
20
|
+
"""Tests that different properties return different hashes."""
|
|
21
|
+
props1 = {"a": 1, "b": "test"}
|
|
22
|
+
props2 = {"a": 1, "b": "changed"}
|
|
23
|
+
|
|
24
|
+
hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
|
|
25
|
+
hash2 = FunctionCacheManager.calculate_content_hash("id1", props2)
|
|
26
|
+
|
|
27
|
+
assert hash1 != hash2
|
|
28
|
+
|
|
29
|
+
def test_calculate_content_hash_sensitivity_id():
|
|
30
|
+
"""Tests that different identifiers return different hashes."""
|
|
31
|
+
props1 = {"a": 1}
|
|
32
|
+
|
|
33
|
+
hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
|
|
34
|
+
hash2 = FunctionCacheManager.calculate_content_hash("id2", props1)
|
|
35
|
+
|
|
36
|
+
assert hash1 != hash2
|
|
37
|
+
|
|
38
|
+
def test_calculate_content_hash_order_invariance():
|
|
39
|
+
"""
|
|
40
|
+
Tests that different dictionary key orders return the same hash,
|
|
41
|
+
due to json.dumps(sort_keys=True).
|
|
42
|
+
"""
|
|
43
|
+
props1 = {"a": 1, "b": 2}
|
|
44
|
+
props2 = {"b": 2, "a": 1} # Changed order
|
|
45
|
+
|
|
46
|
+
hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
|
|
47
|
+
hash2 = FunctionCacheManager.calculate_content_hash("id1", props2)
|
|
48
|
+
|
|
49
|
+
assert hash1 == hash2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- 2. Tests for __init__ and _load_cache ---
|
|
53
|
+
|
|
54
|
+
@patch("os.path.exists", return_value=False)
|
|
55
|
+
def test_init_cache_file_not_found(mock_exists):
|
|
56
|
+
"""Tests that self.cache is initialized as an empty dict ({}) when the cache file is not found."""
|
|
57
|
+
manager = FunctionCacheManager()
|
|
58
|
+
|
|
59
|
+
mock_exists.assert_called_once_with("./.vectorwave_functions_cache.json")
|
|
60
|
+
assert manager.cache == {}
|
|
61
|
+
|
|
62
|
+
@patch("os.path.exists", return_value=True)
|
|
63
|
+
@patch("builtins.open", new_callable=mock_open, read_data='{"uuid_abc": "hash_123"}')
|
|
64
|
+
def test_init_cache_file_loaded(mock_file, mock_exists):
|
|
65
|
+
"""Tests that the cache file is loaded successfully."""
|
|
66
|
+
manager = FunctionCacheManager()
|
|
67
|
+
|
|
68
|
+
mock_exists.assert_called_once_with("./.vectorwave_functions_cache.json")
|
|
69
|
+
mock_file.assert_called_once_with("./.vectorwave_functions_cache.json", 'r', encoding='utf-8')
|
|
70
|
+
assert manager.cache == {"uuid_abc": "hash_123"}
|
|
71
|
+
|
|
72
|
+
@patch("os.path.exists", return_value=True)
|
|
73
|
+
@patch("builtins.open", new_callable=mock_open, read_data='{invalid_json')
|
|
74
|
+
@patch("logging.Logger.warning") # Mock the logger
|
|
75
|
+
def test_init_cache_invalid_json(mock_logger, mock_file, mock_exists):
|
|
76
|
+
"""Tests that a warning is logged and cache is an empty dict ({}) if JSON parsing fails."""
|
|
77
|
+
manager = FunctionCacheManager()
|
|
78
|
+
|
|
79
|
+
assert manager.cache == {}
|
|
80
|
+
mock_logger.assert_called_once()
|
|
81
|
+
assert "Failed to load static function cache" in mock_logger.call_args[0][0]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- 3. Tests for logic and saving ---
|
|
85
|
+
|
|
86
|
+
@patch("os.path.exists", return_value=False)
|
|
87
|
+
def test_is_cached_and_unchanged_logic(mock_exists):
|
|
88
|
+
"""Tests the True/False logic of is_cached_and_unchanged."""
|
|
89
|
+
manager = FunctionCacheManager()
|
|
90
|
+
manager.cache = {
|
|
91
|
+
"uuid_A": "hash_A_123",
|
|
92
|
+
"uuid_B": "hash_B_123"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# 1. Cache Hit (Both UUID and hash match)
|
|
96
|
+
assert manager.is_cached_and_unchanged("uuid_A", "hash_A_123") == True
|
|
97
|
+
|
|
98
|
+
# 2. Cache Miss (UUID matches, but hash is different)
|
|
99
|
+
assert manager.is_cached_and_unchanged("uuid_B", "hash_B_456_NEW") == False
|
|
100
|
+
|
|
101
|
+
# 3. Cache Miss (UUID does not exist)
|
|
102
|
+
assert manager.is_cached_and_unchanged("uuid_C_NEW", "hash_C_123") == False
|
|
103
|
+
|
|
104
|
+
@patch("os.path.exists", return_value=False)
|
|
105
|
+
@patch("json.dump") # Mock json.dump
|
|
106
|
+
@patch("builtins.open", new_callable=mock_open)
|
|
107
|
+
def test_update_cache_calls_save(mock_file, mock_json_dump, mock_exists):
|
|
108
|
+
"""Tests if update_cache updates self.cache and calls _save_cache."""
|
|
109
|
+
manager = FunctionCacheManager()
|
|
110
|
+
assert manager.cache == {}
|
|
111
|
+
|
|
112
|
+
manager.update_cache("uuid_new", "hash_new_123")
|
|
113
|
+
|
|
114
|
+
# 1. Check if the internal cache was updated
|
|
115
|
+
assert manager.cache == {"uuid_new": "hash_new_123"}
|
|
116
|
+
|
|
117
|
+
# 2. Check if _save_cache was called and attempted to write to the file
|
|
118
|
+
mock_file.assert_called_once_with("./.vectorwave_functions_cache.json", 'w', encoding='utf-8')
|
|
119
|
+
|
|
120
|
+
# 3. Check if json.dump was called with the correct arguments (indent, sort_keys)
|
|
121
|
+
mock_json_dump.assert_called_once_with(
|
|
122
|
+
{"uuid_new": "hash_new_123"}, # 1. The updated cache
|
|
123
|
+
ANY, # 2. The mock_file handle
|
|
124
|
+
indent=4,
|
|
125
|
+
sort_keys=True
|
|
126
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .core.decorator import vectorize
|
|
2
2
|
|
|
3
3
|
from .database.db import initialize_database
|
|
4
|
-
from .database.db_search import search_functions, search_executions
|
|
4
|
+
from .database.db_search import search_functions, search_executions, search_errors_by_message
|
|
5
5
|
from .monitoring.tracer import trace_span
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
@@ -9,5 +9,6 @@ __all__ = [
|
|
|
9
9
|
'initialize_database',
|
|
10
10
|
'search_functions',
|
|
11
11
|
'search_executions',
|
|
12
|
+
'search_errors_by_message',
|
|
12
13
|
'trace_span'
|
|
13
14
|
]
|