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.
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