roma-debug 0.1.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.
- roma_debug/__init__.py +3 -0
- roma_debug/config.py +79 -0
- roma_debug/core/__init__.py +5 -0
- roma_debug/core/engine.py +423 -0
- roma_debug/core/models.py +313 -0
- roma_debug/main.py +753 -0
- roma_debug/parsers/__init__.py +21 -0
- roma_debug/parsers/base.py +189 -0
- roma_debug/parsers/python_ast_parser.py +268 -0
- roma_debug/parsers/registry.py +196 -0
- roma_debug/parsers/traceback_patterns.py +314 -0
- roma_debug/parsers/treesitter_parser.py +598 -0
- roma_debug/prompts.py +153 -0
- roma_debug/server.py +247 -0
- roma_debug/tracing/__init__.py +28 -0
- roma_debug/tracing/call_chain.py +278 -0
- roma_debug/tracing/context_builder.py +672 -0
- roma_debug/tracing/dependency_graph.py +298 -0
- roma_debug/tracing/error_analyzer.py +399 -0
- roma_debug/tracing/import_resolver.py +315 -0
- roma_debug/tracing/project_scanner.py +569 -0
- roma_debug/utils/__init__.py +5 -0
- roma_debug/utils/context.py +422 -0
- roma_debug-0.1.0.dist-info/METADATA +34 -0
- roma_debug-0.1.0.dist-info/RECORD +36 -0
- roma_debug-0.1.0.dist-info/WHEEL +5 -0
- roma_debug-0.1.0.dist-info/entry_points.txt +2 -0
- roma_debug-0.1.0.dist-info/licenses/LICENSE +201 -0
- roma_debug-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_context.py +208 -0
- tests/test_engine.py +296 -0
- tests/test_parsers.py +534 -0
- tests/test_project_scanner.py +275 -0
- tests/test_traceback_patterns.py +222 -0
- tests/test_tracing.py +296 -0
tests/test_tracing.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Tests for import resolution and dependency tracing."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import pytest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from roma_debug.core.models import Language, Import, FileContext
|
|
9
|
+
from roma_debug.tracing.import_resolver import ImportResolver
|
|
10
|
+
from roma_debug.tracing.dependency_graph import DependencyGraph
|
|
11
|
+
from roma_debug.tracing.call_chain import CallChainAnalyzer, CallChain
|
|
12
|
+
from roma_debug.tracing.context_builder import ContextBuilder
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestImportResolver:
|
|
16
|
+
"""Tests for import resolution."""
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def temp_project(self):
|
|
20
|
+
"""Create a temporary project structure."""
|
|
21
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
|
+
# Create directory structure
|
|
23
|
+
src = Path(tmpdir) / "src"
|
|
24
|
+
src.mkdir()
|
|
25
|
+
|
|
26
|
+
# Create main.py
|
|
27
|
+
(src / "main.py").write_text("""
|
|
28
|
+
from utils import helper
|
|
29
|
+
from .local import something
|
|
30
|
+
import os
|
|
31
|
+
""")
|
|
32
|
+
|
|
33
|
+
# Create utils.py
|
|
34
|
+
(src / "utils.py").write_text("""
|
|
35
|
+
def helper():
|
|
36
|
+
return 42
|
|
37
|
+
""")
|
|
38
|
+
|
|
39
|
+
# Create local.py
|
|
40
|
+
(src / "local.py").write_text("""
|
|
41
|
+
something = "value"
|
|
42
|
+
""")
|
|
43
|
+
|
|
44
|
+
# Create __init__.py
|
|
45
|
+
(src / "__init__.py").write_text("")
|
|
46
|
+
|
|
47
|
+
yield tmpdir
|
|
48
|
+
|
|
49
|
+
def test_resolve_absolute_import(self, temp_project):
|
|
50
|
+
"""Test resolving absolute imports."""
|
|
51
|
+
resolver = ImportResolver(temp_project)
|
|
52
|
+
|
|
53
|
+
imp = Import(
|
|
54
|
+
module_name="src.utils",
|
|
55
|
+
language=Language.PYTHON,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
resolved = resolver.resolve_import(imp, Path(temp_project) / "src" / "main.py")
|
|
59
|
+
assert resolved.resolved_path is not None
|
|
60
|
+
assert "utils.py" in resolved.resolved_path
|
|
61
|
+
|
|
62
|
+
def test_resolve_relative_import(self, temp_project):
|
|
63
|
+
"""Test resolving relative imports."""
|
|
64
|
+
resolver = ImportResolver(temp_project)
|
|
65
|
+
|
|
66
|
+
imp = Import(
|
|
67
|
+
module_name="local",
|
|
68
|
+
is_relative=True,
|
|
69
|
+
relative_level=1,
|
|
70
|
+
language=Language.PYTHON,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
resolved = resolver.resolve_import(imp, Path(temp_project) / "src" / "main.py")
|
|
74
|
+
assert resolved.resolved_path is not None
|
|
75
|
+
assert "local.py" in resolved.resolved_path
|
|
76
|
+
|
|
77
|
+
def test_unresolvable_import(self, temp_project):
|
|
78
|
+
"""Test that unresolvable imports return None."""
|
|
79
|
+
resolver = ImportResolver(temp_project)
|
|
80
|
+
|
|
81
|
+
imp = Import(
|
|
82
|
+
module_name="nonexistent_module",
|
|
83
|
+
language=Language.PYTHON,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
resolved = resolver.resolve_import(imp, Path(temp_project) / "src" / "main.py")
|
|
87
|
+
assert resolved.resolved_path is None
|
|
88
|
+
|
|
89
|
+
def test_caching(self, temp_project):
|
|
90
|
+
"""Test that resolved paths are cached."""
|
|
91
|
+
resolver = ImportResolver(temp_project)
|
|
92
|
+
|
|
93
|
+
imp = Import(
|
|
94
|
+
module_name="src.utils",
|
|
95
|
+
language=Language.PYTHON,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
source = Path(temp_project) / "src" / "main.py"
|
|
99
|
+
resolved1 = resolver.resolve_import(imp, source)
|
|
100
|
+
resolved2 = resolver.resolve_import(imp, source)
|
|
101
|
+
|
|
102
|
+
# Should be cached
|
|
103
|
+
assert resolved1.resolved_path == resolved2.resolved_path
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestDependencyGraph:
|
|
107
|
+
"""Tests for dependency graph building."""
|
|
108
|
+
|
|
109
|
+
def test_add_file(self):
|
|
110
|
+
"""Test adding a file to the graph."""
|
|
111
|
+
graph = DependencyGraph()
|
|
112
|
+
|
|
113
|
+
imports = [
|
|
114
|
+
Import(module_name="utils", resolved_path="/app/utils.py", language=Language.PYTHON),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
graph.add_file("/app/main.py", Language.PYTHON, imports)
|
|
118
|
+
|
|
119
|
+
assert "/app/main.py" in graph.get_all_files() or any(
|
|
120
|
+
"main.py" in f for f in graph.get_all_files()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def test_get_dependencies(self):
|
|
124
|
+
"""Test getting dependencies of a file."""
|
|
125
|
+
graph = DependencyGraph()
|
|
126
|
+
|
|
127
|
+
imports = [
|
|
128
|
+
Import(module_name="utils", resolved_path="/app/utils.py", language=Language.PYTHON),
|
|
129
|
+
Import(module_name="helpers", resolved_path="/app/helpers.py", language=Language.PYTHON),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
graph.add_file("/app/main.py", Language.PYTHON, imports)
|
|
133
|
+
|
|
134
|
+
deps = graph.get_dependencies("/app/main.py")
|
|
135
|
+
assert len(deps) == 2
|
|
136
|
+
|
|
137
|
+
def test_get_dependents(self):
|
|
138
|
+
"""Test getting files that depend on a given file."""
|
|
139
|
+
graph = DependencyGraph()
|
|
140
|
+
|
|
141
|
+
# main.py imports utils.py
|
|
142
|
+
graph.add_file("/app/main.py", Language.PYTHON, [
|
|
143
|
+
Import(module_name="utils", resolved_path="/app/utils.py", language=Language.PYTHON),
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
# other.py also imports utils.py
|
|
147
|
+
graph.add_file("/app/other.py", Language.PYTHON, [
|
|
148
|
+
Import(module_name="utils", resolved_path="/app/utils.py", language=Language.PYTHON),
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
dependents = graph.get_dependents("/app/utils.py")
|
|
152
|
+
assert len(dependents) == 2
|
|
153
|
+
|
|
154
|
+
def test_transitive_dependencies(self):
|
|
155
|
+
"""Test getting transitive dependencies."""
|
|
156
|
+
graph = DependencyGraph()
|
|
157
|
+
|
|
158
|
+
# main -> utils -> helpers
|
|
159
|
+
graph.add_file("/app/main.py", Language.PYTHON, [
|
|
160
|
+
Import(module_name="utils", resolved_path="/app/utils.py", language=Language.PYTHON),
|
|
161
|
+
])
|
|
162
|
+
graph.add_file("/app/utils.py", Language.PYTHON, [
|
|
163
|
+
Import(module_name="helpers", resolved_path="/app/helpers.py", language=Language.PYTHON),
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
transitive = graph.get_transitive_dependencies("/app/main.py")
|
|
167
|
+
# Should include both utils.py and helpers.py
|
|
168
|
+
assert len(transitive) == 2
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestCallChainAnalyzer:
|
|
172
|
+
"""Tests for call chain analysis."""
|
|
173
|
+
|
|
174
|
+
@pytest.fixture
|
|
175
|
+
def sample_contexts(self):
|
|
176
|
+
"""Create sample file contexts."""
|
|
177
|
+
return [
|
|
178
|
+
FileContext(
|
|
179
|
+
filepath="/app/main.py",
|
|
180
|
+
line_number=10,
|
|
181
|
+
context_type="ast",
|
|
182
|
+
content="def main():\n result = process()",
|
|
183
|
+
function_name="main",
|
|
184
|
+
language=Language.PYTHON,
|
|
185
|
+
),
|
|
186
|
+
FileContext(
|
|
187
|
+
filepath="/app/utils.py",
|
|
188
|
+
line_number=25,
|
|
189
|
+
context_type="ast",
|
|
190
|
+
content="def process():\n return compute()",
|
|
191
|
+
function_name="process",
|
|
192
|
+
language=Language.PYTHON,
|
|
193
|
+
),
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
def test_analyze_from_contexts(self, sample_contexts):
|
|
197
|
+
"""Test building call chain from contexts."""
|
|
198
|
+
analyzer = CallChainAnalyzer()
|
|
199
|
+
chain = analyzer.analyze_from_contexts(sample_contexts)
|
|
200
|
+
|
|
201
|
+
assert len(chain.sites) == 2
|
|
202
|
+
assert chain.sites[0].function_name == "main"
|
|
203
|
+
assert chain.sites[1].function_name == "process"
|
|
204
|
+
|
|
205
|
+
def test_call_chain_string(self, sample_contexts):
|
|
206
|
+
"""Test call chain string representation."""
|
|
207
|
+
analyzer = CallChainAnalyzer()
|
|
208
|
+
chain = analyzer.analyze_from_contexts(sample_contexts)
|
|
209
|
+
|
|
210
|
+
chain_str = str(chain)
|
|
211
|
+
assert "main" in chain_str
|
|
212
|
+
assert "process" in chain_str
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestContextBuilder:
|
|
216
|
+
"""Tests for the context builder."""
|
|
217
|
+
|
|
218
|
+
@pytest.fixture
|
|
219
|
+
def temp_project(self):
|
|
220
|
+
"""Create a temporary project with sample files."""
|
|
221
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
222
|
+
# Create a Python file
|
|
223
|
+
main_py = Path(tmpdir) / "main.py"
|
|
224
|
+
main_py.write_text("""
|
|
225
|
+
import utils
|
|
226
|
+
|
|
227
|
+
def main():
|
|
228
|
+
result = utils.process(None)
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
main()
|
|
233
|
+
""")
|
|
234
|
+
|
|
235
|
+
utils_py = Path(tmpdir) / "utils.py"
|
|
236
|
+
utils_py.write_text("""
|
|
237
|
+
def process(data):
|
|
238
|
+
return data.strip() # This will fail with None
|
|
239
|
+
""")
|
|
240
|
+
|
|
241
|
+
yield tmpdir
|
|
242
|
+
|
|
243
|
+
def test_build_context_from_python_traceback(self, temp_project):
|
|
244
|
+
"""Test building context from a Python traceback."""
|
|
245
|
+
error_log = f'''
|
|
246
|
+
Traceback (most recent call last):
|
|
247
|
+
File "{temp_project}/main.py", line 5, in main
|
|
248
|
+
result = utils.process(None)
|
|
249
|
+
File "{temp_project}/utils.py", line 3, in process
|
|
250
|
+
return data.strip()
|
|
251
|
+
AttributeError: 'NoneType' object has no attribute 'strip'
|
|
252
|
+
'''
|
|
253
|
+
builder = ContextBuilder(project_root=temp_project)
|
|
254
|
+
ctx = builder.build_analysis_context(error_log)
|
|
255
|
+
|
|
256
|
+
assert ctx.primary_context is not None
|
|
257
|
+
assert ctx.primary_context.filepath.endswith("utils.py")
|
|
258
|
+
assert len(ctx.traceback_contexts) >= 1
|
|
259
|
+
|
|
260
|
+
def test_get_context_for_prompt(self, temp_project):
|
|
261
|
+
"""Test formatting context for AI prompt."""
|
|
262
|
+
error_log = f'''
|
|
263
|
+
Traceback (most recent call last):
|
|
264
|
+
File "{temp_project}/main.py", line 5, in main
|
|
265
|
+
result = utils.process(None)
|
|
266
|
+
AttributeError: 'NoneType' object has no attribute 'strip'
|
|
267
|
+
'''
|
|
268
|
+
builder = ContextBuilder(project_root=temp_project)
|
|
269
|
+
ctx = builder.build_analysis_context(error_log)
|
|
270
|
+
|
|
271
|
+
prompt = builder.get_context_for_prompt(ctx)
|
|
272
|
+
|
|
273
|
+
assert "PRIMARY ERROR" in prompt
|
|
274
|
+
assert "main.py" in prompt
|
|
275
|
+
assert "Language:" in prompt
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TestUpstreamContext:
|
|
279
|
+
"""Tests for upstream context building."""
|
|
280
|
+
|
|
281
|
+
def test_upstream_context_to_prompt(self):
|
|
282
|
+
"""Test formatting upstream context."""
|
|
283
|
+
from roma_debug.core.models import UpstreamContext
|
|
284
|
+
|
|
285
|
+
ctx = UpstreamContext(
|
|
286
|
+
call_chain=["main.main", "utils.process", "compute.calc"],
|
|
287
|
+
relevant_definitions={"helper": "def helper(): pass"},
|
|
288
|
+
dependency_summary="3 files analyzed",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
text = ctx.to_prompt_text()
|
|
292
|
+
|
|
293
|
+
assert "CALL CHAIN" in text
|
|
294
|
+
assert "main.main" in text
|
|
295
|
+
assert "RELEVANT DEFINITIONS" in text
|
|
296
|
+
assert "helper" in text
|