roampal 0.1.4__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.
- roampal/__init__.py +29 -0
- roampal/__main__.py +6 -0
- roampal/backend/__init__.py +1 -0
- roampal/backend/modules/__init__.py +1 -0
- roampal/backend/modules/memory/__init__.py +43 -0
- roampal/backend/modules/memory/chromadb_adapter.py +623 -0
- roampal/backend/modules/memory/config.py +102 -0
- roampal/backend/modules/memory/content_graph.py +543 -0
- roampal/backend/modules/memory/context_service.py +455 -0
- roampal/backend/modules/memory/embedding_service.py +96 -0
- roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
- roampal/backend/modules/memory/memory_bank_service.py +433 -0
- roampal/backend/modules/memory/memory_types.py +296 -0
- roampal/backend/modules/memory/outcome_service.py +400 -0
- roampal/backend/modules/memory/promotion_service.py +473 -0
- roampal/backend/modules/memory/routing_service.py +444 -0
- roampal/backend/modules/memory/scoring_service.py +324 -0
- roampal/backend/modules/memory/search_service.py +646 -0
- roampal/backend/modules/memory/tests/__init__.py +1 -0
- roampal/backend/modules/memory/tests/conftest.py +12 -0
- roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
- roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
- roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
- roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
- roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
- roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
- roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
- roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
- roampal/backend/modules/memory/unified_memory_system.py +1277 -0
- roampal/cli.py +638 -0
- roampal/hooks/__init__.py +16 -0
- roampal/hooks/session_manager.py +587 -0
- roampal/hooks/stop_hook.py +176 -0
- roampal/hooks/user_prompt_submit_hook.py +103 -0
- roampal/mcp/__init__.py +7 -0
- roampal/mcp/server.py +611 -0
- roampal/server/__init__.py +7 -0
- roampal/server/main.py +744 -0
- roampal-0.1.4.dist-info/METADATA +179 -0
- roampal-0.1.4.dist-info/RECORD +44 -0
- roampal-0.1.4.dist-info/WHEEL +5 -0
- roampal-0.1.4.dist-info/entry_points.txt +2 -0
- roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
- roampal-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit Tests for UnifiedMemorySystem (Core)
|
|
3
|
+
|
|
4
|
+
Tests the Core UMS which is more monolithic than Desktop's facade pattern.
|
|
5
|
+
Core UMS handles collections and services internally.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '..')))
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
from roampal.backend.modules.memory.unified_memory_system import UnifiedMemorySystem
|
|
17
|
+
from roampal.backend.modules.memory.config import MemoryConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestUnifiedMemorySystemInit:
|
|
21
|
+
"""Test initialization."""
|
|
22
|
+
|
|
23
|
+
def test_init_creates_data_dir(self, tmp_path):
|
|
24
|
+
"""Should create data directory."""
|
|
25
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
26
|
+
assert (tmp_path / "data").exists()
|
|
27
|
+
|
|
28
|
+
def test_init_with_custom_config(self, tmp_path):
|
|
29
|
+
"""Should use custom config."""
|
|
30
|
+
config = MemoryConfig(promotion_score_threshold=0.8)
|
|
31
|
+
ums = UnifiedMemorySystem(
|
|
32
|
+
data_path=str(tmp_path / "data"),
|
|
33
|
+
config=config
|
|
34
|
+
)
|
|
35
|
+
assert ums.config.promotion_score_threshold == 0.8
|
|
36
|
+
|
|
37
|
+
def test_init_not_initialized(self, tmp_path):
|
|
38
|
+
"""Should not be initialized until initialize() called."""
|
|
39
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
40
|
+
assert not ums.initialized
|
|
41
|
+
|
|
42
|
+
def test_init_loads_kg(self, tmp_path):
|
|
43
|
+
"""Should load knowledge graph on init."""
|
|
44
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
45
|
+
assert "routing_patterns" in ums.knowledge_graph
|
|
46
|
+
assert "context_action_effectiveness" in ums.knowledge_graph
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestInitialize:
|
|
50
|
+
"""Test initialization process."""
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def ums(self, tmp_path):
|
|
54
|
+
"""Create UMS instance."""
|
|
55
|
+
return UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_initialize_creates_collections(self, ums):
|
|
59
|
+
"""Should create all collections."""
|
|
60
|
+
await ums.initialize()
|
|
61
|
+
|
|
62
|
+
assert "books" in ums.collections
|
|
63
|
+
assert "working" in ums.collections
|
|
64
|
+
assert "history" in ums.collections
|
|
65
|
+
assert "patterns" in ums.collections
|
|
66
|
+
assert "memory_bank" in ums.collections
|
|
67
|
+
|
|
68
|
+
@pytest.mark.asyncio
|
|
69
|
+
async def test_initialize_creates_services(self, ums):
|
|
70
|
+
"""Should initialize all services."""
|
|
71
|
+
await ums.initialize()
|
|
72
|
+
|
|
73
|
+
assert ums._embedding_service is not None
|
|
74
|
+
assert ums._scoring_service is not None
|
|
75
|
+
assert ums._promotion_service is not None
|
|
76
|
+
assert ums._outcome_service is not None
|
|
77
|
+
assert ums._memory_bank_service is not None
|
|
78
|
+
assert ums._context_service is not None
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_initialize_only_once(self, ums):
|
|
82
|
+
"""Should only initialize once."""
|
|
83
|
+
await ums.initialize()
|
|
84
|
+
first_embedding = ums._embedding_service
|
|
85
|
+
|
|
86
|
+
await ums.initialize()
|
|
87
|
+
assert ums._embedding_service is first_embedding
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestStoreWorking:
|
|
91
|
+
"""Test store_working functionality."""
|
|
92
|
+
|
|
93
|
+
@pytest.fixture
|
|
94
|
+
def mock_ums(self, tmp_path):
|
|
95
|
+
"""Create UMS with mocked embedding."""
|
|
96
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
97
|
+
ums.initialized = True
|
|
98
|
+
|
|
99
|
+
# Mock collections
|
|
100
|
+
working = MagicMock()
|
|
101
|
+
working.upsert_vectors = AsyncMock()
|
|
102
|
+
ums.collections = {"working": working}
|
|
103
|
+
|
|
104
|
+
# Mock embedding
|
|
105
|
+
ums._embedding_service = MagicMock()
|
|
106
|
+
ums._embedding_service.embed_text = AsyncMock(return_value=[0.1] * 384)
|
|
107
|
+
|
|
108
|
+
return ums
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
async def test_store_working_generates_doc_id(self, mock_ums):
|
|
112
|
+
"""Should generate document ID."""
|
|
113
|
+
doc_id = await mock_ums.store_working("test text")
|
|
114
|
+
|
|
115
|
+
assert doc_id.startswith("working_")
|
|
116
|
+
mock_ums.collections["working"].upsert_vectors.assert_called_once()
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
async def test_store_working_embeds_text(self, mock_ums):
|
|
120
|
+
"""Should embed the text."""
|
|
121
|
+
await mock_ums.store_working("test text")
|
|
122
|
+
|
|
123
|
+
mock_ums._embedding_service.embed_text.assert_called_with("test text")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestSearch:
|
|
127
|
+
"""Test search functionality."""
|
|
128
|
+
|
|
129
|
+
@pytest.fixture
|
|
130
|
+
def mock_ums(self, tmp_path):
|
|
131
|
+
"""Create UMS with search mocks."""
|
|
132
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
133
|
+
ums.initialized = True
|
|
134
|
+
|
|
135
|
+
# Mock embedding
|
|
136
|
+
ums._embedding_service = MagicMock()
|
|
137
|
+
ums._embedding_service.embed_text = AsyncMock(return_value=[0.1] * 384)
|
|
138
|
+
|
|
139
|
+
# Mock scoring
|
|
140
|
+
ums._scoring_service = MagicMock()
|
|
141
|
+
ums._scoring_service.calculate_final_score = MagicMock(return_value={
|
|
142
|
+
"final_rank_score": 0.8,
|
|
143
|
+
"wilson_score": 0.7,
|
|
144
|
+
"embedding_similarity": 0.9,
|
|
145
|
+
"learned_score": 0.7,
|
|
146
|
+
"embedding_weight": 0.5,
|
|
147
|
+
"learned_weight": 0.5
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
# Mock collections with query results
|
|
151
|
+
mock_collection = MagicMock()
|
|
152
|
+
mock_collection.hybrid_query = AsyncMock(return_value=[
|
|
153
|
+
{"id": "doc_1", "text": "result 1", "distance": 0.5, "metadata": {"score": 0.7, "uses": 3}},
|
|
154
|
+
{"id": "doc_2", "text": "result 2", "distance": 0.8, "metadata": {"score": 0.5, "uses": 1}},
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
ums.collections = {
|
|
158
|
+
"working": mock_collection,
|
|
159
|
+
"history": mock_collection,
|
|
160
|
+
"patterns": mock_collection,
|
|
161
|
+
"books": mock_collection,
|
|
162
|
+
"memory_bank": mock_collection,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return ums
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_search_returns_results(self, mock_ums):
|
|
169
|
+
"""Should return search results."""
|
|
170
|
+
results = await mock_ums.search("test query")
|
|
171
|
+
|
|
172
|
+
# Returns list (may be empty if mocked collections return nothing)
|
|
173
|
+
assert isinstance(results, list)
|
|
174
|
+
|
|
175
|
+
@pytest.mark.asyncio
|
|
176
|
+
async def test_search_generates_embedding(self, mock_ums):
|
|
177
|
+
"""Should generate embedding for query."""
|
|
178
|
+
await mock_ums.search("test query")
|
|
179
|
+
|
|
180
|
+
mock_ums._embedding_service.embed_text.assert_called()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class TestRecordOutcome:
|
|
184
|
+
"""Test outcome recording."""
|
|
185
|
+
|
|
186
|
+
@pytest.fixture
|
|
187
|
+
def mock_ums(self, tmp_path):
|
|
188
|
+
"""Create UMS with outcome mock."""
|
|
189
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
190
|
+
ums.initialized = True
|
|
191
|
+
|
|
192
|
+
# Mock outcome service
|
|
193
|
+
ums._outcome_service = MagicMock()
|
|
194
|
+
ums._outcome_service.record_outcome = AsyncMock(return_value={"score": 0.7})
|
|
195
|
+
|
|
196
|
+
return ums
|
|
197
|
+
|
|
198
|
+
@pytest.mark.asyncio
|
|
199
|
+
async def test_record_outcome_delegates(self, mock_ums):
|
|
200
|
+
"""Should delegate to outcome service."""
|
|
201
|
+
# Core's record_outcome takes doc_ids (list), not doc_id
|
|
202
|
+
await mock_ums.record_outcome(
|
|
203
|
+
doc_ids=["working_test123"],
|
|
204
|
+
outcome="worked"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
mock_ums._outcome_service.record_outcome.assert_called()
|
|
208
|
+
|
|
209
|
+
@pytest.mark.asyncio
|
|
210
|
+
async def test_record_outcome_with_reason(self, mock_ums):
|
|
211
|
+
"""Should pass failure reason."""
|
|
212
|
+
# Core's record_outcome takes doc_ids (list), not doc_id
|
|
213
|
+
await mock_ums.record_outcome(
|
|
214
|
+
doc_ids=["working_test123"],
|
|
215
|
+
outcome="failed",
|
|
216
|
+
failure_reason="Test failure"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Check failure_reason was passed
|
|
220
|
+
mock_ums._outcome_service.record_outcome.assert_called()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestMemoryBankAPI:
|
|
224
|
+
"""Test memory bank API."""
|
|
225
|
+
|
|
226
|
+
@pytest.fixture
|
|
227
|
+
def mock_ums(self, tmp_path):
|
|
228
|
+
"""Create UMS with memory bank mock."""
|
|
229
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
230
|
+
ums.initialized = True
|
|
231
|
+
|
|
232
|
+
# Mock memory bank service
|
|
233
|
+
ums._memory_bank_service = MagicMock()
|
|
234
|
+
ums._memory_bank_service.store = AsyncMock(return_value="memory_bank_123")
|
|
235
|
+
ums._memory_bank_service.update = AsyncMock(return_value="memory_bank_123")
|
|
236
|
+
ums._memory_bank_service.archive = AsyncMock(return_value=True)
|
|
237
|
+
ums._memory_bank_service.search = AsyncMock(return_value=[])
|
|
238
|
+
|
|
239
|
+
return ums
|
|
240
|
+
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_store_memory_bank(self, mock_ums):
|
|
243
|
+
"""Should delegate to memory bank service."""
|
|
244
|
+
doc_id = await mock_ums.store_memory_bank(
|
|
245
|
+
text="User prefers dark mode",
|
|
246
|
+
tags=["preference"]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
assert doc_id == "memory_bank_123"
|
|
250
|
+
mock_ums._memory_bank_service.store.assert_called_once()
|
|
251
|
+
|
|
252
|
+
@pytest.mark.asyncio
|
|
253
|
+
async def test_update_memory_bank(self, mock_ums):
|
|
254
|
+
"""Should delegate update."""
|
|
255
|
+
await mock_ums.update_memory_bank(
|
|
256
|
+
old_content="old text",
|
|
257
|
+
new_content="new text"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
mock_ums._memory_bank_service.update.assert_called_once()
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
async def test_archive_memory_bank(self, mock_ums):
|
|
264
|
+
"""Should delegate archive."""
|
|
265
|
+
result = await mock_ums.archive_memory_bank("some content to archive")
|
|
266
|
+
assert result is True
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestContextAPI:
|
|
270
|
+
"""Test context analysis API."""
|
|
271
|
+
|
|
272
|
+
@pytest.fixture
|
|
273
|
+
def mock_ums(self, tmp_path):
|
|
274
|
+
"""Create UMS with mocked collections for context analysis."""
|
|
275
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
276
|
+
ums.initialized = True
|
|
277
|
+
|
|
278
|
+
# Mock embedding
|
|
279
|
+
ums._embedding_service = MagicMock()
|
|
280
|
+
ums._embedding_service.embed_text = AsyncMock(return_value=[0.1] * 384)
|
|
281
|
+
|
|
282
|
+
# Mock collections
|
|
283
|
+
mock_collection = MagicMock()
|
|
284
|
+
mock_collection.hybrid_query = AsyncMock(return_value=[])
|
|
285
|
+
|
|
286
|
+
ums.collections = {
|
|
287
|
+
"working": mock_collection,
|
|
288
|
+
"history": mock_collection,
|
|
289
|
+
"patterns": mock_collection,
|
|
290
|
+
"books": mock_collection,
|
|
291
|
+
"memory_bank": mock_collection,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return ums
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_analyze_context(self, mock_ums):
|
|
298
|
+
"""Should return context analysis result."""
|
|
299
|
+
# Core's analyze_conversation_context does work internally
|
|
300
|
+
context = await mock_ums.analyze_conversation_context(
|
|
301
|
+
current_message="test",
|
|
302
|
+
recent_conversation=[],
|
|
303
|
+
conversation_id="conv123"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Core returns these keys
|
|
307
|
+
assert "relevant_patterns" in context
|
|
308
|
+
assert "past_outcomes" in context
|
|
309
|
+
assert "topic_continuity" in context
|
|
310
|
+
assert "proactive_insights" in context
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class TestKGAccess:
|
|
314
|
+
"""Test Knowledge Graph access."""
|
|
315
|
+
|
|
316
|
+
@pytest.fixture
|
|
317
|
+
def ums(self, tmp_path):
|
|
318
|
+
"""Create UMS instance."""
|
|
319
|
+
return UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
320
|
+
|
|
321
|
+
def test_knowledge_graph_property(self, ums):
|
|
322
|
+
"""Should expose knowledge graph."""
|
|
323
|
+
kg = ums.knowledge_graph
|
|
324
|
+
|
|
325
|
+
assert "routing_patterns" in kg
|
|
326
|
+
assert "context_action_effectiveness" in kg
|
|
327
|
+
assert "problem_solutions" in kg
|
|
328
|
+
|
|
329
|
+
def test_knowledge_graph_is_dict(self, ums):
|
|
330
|
+
"""Knowledge graph should be a dict."""
|
|
331
|
+
assert isinstance(ums.knowledge_graph, dict)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestStats:
|
|
335
|
+
"""Test statistics retrieval."""
|
|
336
|
+
|
|
337
|
+
@pytest.fixture
|
|
338
|
+
def mock_ums(self, tmp_path):
|
|
339
|
+
"""Create UMS with collection mocks."""
|
|
340
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
341
|
+
ums.initialized = True
|
|
342
|
+
|
|
343
|
+
# Mock collections
|
|
344
|
+
mock_collection = MagicMock()
|
|
345
|
+
mock_collection.collection = MagicMock()
|
|
346
|
+
mock_collection.collection.count = MagicMock(return_value=10)
|
|
347
|
+
|
|
348
|
+
ums.collections = {
|
|
349
|
+
"working": mock_collection,
|
|
350
|
+
"history": mock_collection,
|
|
351
|
+
"patterns": mock_collection,
|
|
352
|
+
"books": mock_collection,
|
|
353
|
+
"memory_bank": mock_collection,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return ums
|
|
357
|
+
|
|
358
|
+
def test_get_stats(self, mock_ums):
|
|
359
|
+
"""Should return statistics."""
|
|
360
|
+
stats = mock_ums.get_stats()
|
|
361
|
+
|
|
362
|
+
assert "initialized" in stats
|
|
363
|
+
assert "data_path" in stats
|
|
364
|
+
assert "collections" in stats
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class TestConceptExtraction:
|
|
368
|
+
"""Test concept extraction."""
|
|
369
|
+
|
|
370
|
+
@pytest.fixture
|
|
371
|
+
def ums(self, tmp_path):
|
|
372
|
+
"""Create UMS instance."""
|
|
373
|
+
return UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
374
|
+
|
|
375
|
+
def test_extract_concepts(self, ums):
|
|
376
|
+
"""Should extract concepts from text."""
|
|
377
|
+
concepts = ums._extract_concepts("Python programming language")
|
|
378
|
+
|
|
379
|
+
assert "python" in concepts
|
|
380
|
+
assert "programming" in concepts
|
|
381
|
+
|
|
382
|
+
def test_extract_concepts_filters_short(self, ums):
|
|
383
|
+
"""Should filter short words."""
|
|
384
|
+
concepts = ums._extract_concepts("Go is a fun language")
|
|
385
|
+
|
|
386
|
+
# Short words filtered
|
|
387
|
+
assert "go" not in concepts
|
|
388
|
+
assert "is" not in concepts
|
|
389
|
+
assert "language" in concepts
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class TestTierRecommendations:
|
|
393
|
+
"""Test tier recommendation logic."""
|
|
394
|
+
|
|
395
|
+
@pytest.fixture
|
|
396
|
+
def ums(self, tmp_path):
|
|
397
|
+
"""Create UMS with routing patterns."""
|
|
398
|
+
ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
|
|
399
|
+
ums.knowledge_graph["routing_patterns"] = {
|
|
400
|
+
"python": {
|
|
401
|
+
"best_collection": "patterns",
|
|
402
|
+
"collections_used": {"patterns": {"total": 10, "successes": 8}}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return ums
|
|
406
|
+
|
|
407
|
+
def test_get_tier_recommendations(self, ums):
|
|
408
|
+
"""Should return recommendations based on KG."""
|
|
409
|
+
recs = ums.get_tier_recommendations(["python"])
|
|
410
|
+
|
|
411
|
+
# Core returns these keys
|
|
412
|
+
assert "top_collections" in recs
|
|
413
|
+
assert "match_count" in recs
|
|
414
|
+
assert "confidence_level" in recs
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
if __name__ == "__main__":
|
|
418
|
+
pytest.main([__file__, "-v"])
|