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_parsers.py ADDED
@@ -0,0 +1,534 @@
1
+ """Tests for the multi-language parser system."""
2
+
3
+ import pytest
4
+ from roma_debug.core.models import Language, Symbol, Import
5
+ from roma_debug.parsers.base import BaseParser
6
+ from roma_debug.parsers.registry import detect_language, get_parser, get_registry
7
+ from roma_debug.parsers.python_ast_parser import PythonAstParser
8
+
9
+
10
+ class TestLanguageDetection:
11
+ """Tests for language detection from file extensions."""
12
+
13
+ def test_python_extensions(self):
14
+ assert detect_language("main.py") == Language.PYTHON
15
+ assert detect_language("script.pyw") == Language.PYTHON
16
+ assert detect_language("types.pyi") == Language.PYTHON
17
+
18
+ def test_javascript_extensions(self):
19
+ assert detect_language("app.js") == Language.JAVASCRIPT
20
+ assert detect_language("module.mjs") == Language.JAVASCRIPT
21
+ assert detect_language("common.cjs") == Language.JAVASCRIPT
22
+ assert detect_language("component.jsx") == Language.JAVASCRIPT
23
+
24
+ def test_typescript_extensions(self):
25
+ assert detect_language("app.ts") == Language.TYPESCRIPT
26
+ assert detect_language("component.tsx") == Language.TYPESCRIPT
27
+ assert detect_language("module.mts") == Language.TYPESCRIPT
28
+
29
+ def test_go_extension(self):
30
+ assert detect_language("main.go") == Language.GO
31
+
32
+ def test_rust_extension(self):
33
+ assert detect_language("lib.rs") == Language.RUST
34
+
35
+ def test_java_extension(self):
36
+ assert detect_language("Main.java") == Language.JAVA
37
+
38
+ def test_unknown_extension(self):
39
+ assert detect_language("file.xyz") == Language.UNKNOWN
40
+ assert detect_language("noextension") == Language.UNKNOWN
41
+
42
+
43
+ class TestPythonAstParser:
44
+ """Tests for the Python AST parser."""
45
+
46
+ def test_parse_simple_function(self):
47
+ source = '''
48
+ def hello(name):
49
+ """Say hello."""
50
+ return f"Hello, {name}!"
51
+ '''
52
+ parser = PythonAstParser()
53
+ assert parser.parse(source, "test.py") is True
54
+ assert parser.is_parsed
55
+
56
+ def test_find_enclosing_function(self):
57
+ source = '''
58
+ def outer():
59
+ x = 1
60
+ def inner():
61
+ y = 2
62
+ return y
63
+ return inner()
64
+ '''
65
+ parser = PythonAstParser()
66
+ parser.parse(source, "test.py")
67
+
68
+ # Line 5 is inside inner()
69
+ symbol = parser.find_enclosing_symbol(5)
70
+ assert symbol is not None
71
+ assert symbol.name == "inner"
72
+ assert symbol.kind == "function"
73
+
74
+ # Line 3 is inside outer()
75
+ symbol = parser.find_enclosing_symbol(3)
76
+ assert symbol is not None
77
+ assert symbol.name == "outer"
78
+
79
+ def test_find_enclosing_class(self):
80
+ source = '''
81
+ class MyClass:
82
+ """A test class."""
83
+
84
+ def method(self):
85
+ return 42
86
+ '''
87
+ parser = PythonAstParser()
88
+ parser.parse(source, "test.py")
89
+
90
+ # Line 5 is inside method()
91
+ symbol = parser.find_enclosing_symbol(5)
92
+ assert symbol is not None
93
+ assert symbol.name == "method"
94
+ assert symbol.kind == "method"
95
+
96
+ # Class should be on line 2
97
+ symbol = parser.find_enclosing_symbol(2)
98
+ assert symbol is not None
99
+ assert symbol.name == "MyClass"
100
+ assert symbol.kind == "class"
101
+
102
+ def test_extract_imports(self):
103
+ source = '''
104
+ import os
105
+ import sys as system
106
+ from pathlib import Path
107
+ from typing import Optional, List
108
+ from . import local_module
109
+ from ..parent import something
110
+ '''
111
+ parser = PythonAstParser()
112
+ parser.parse(source, "test.py")
113
+
114
+ imports = parser.extract_imports()
115
+ assert len(imports) == 6
116
+
117
+ # Check import os
118
+ os_import = next((i for i in imports if i.module_name == "os"), None)
119
+ assert os_import is not None
120
+ assert os_import.alias is None
121
+
122
+ # Check import sys as system
123
+ sys_import = next((i for i in imports if i.module_name == "sys"), None)
124
+ assert sys_import is not None
125
+ assert sys_import.alias == "system"
126
+
127
+ # Check from pathlib import Path
128
+ pathlib_import = next((i for i in imports if i.module_name == "pathlib"), None)
129
+ assert pathlib_import is not None
130
+ assert "Path" in pathlib_import.imported_names
131
+
132
+ # Check relative import
133
+ relative_import = next((i for i in imports if i.is_relative and i.relative_level == 1), None)
134
+ assert relative_import is not None
135
+
136
+ def test_async_function(self):
137
+ source = '''
138
+ async def fetch_data(url):
139
+ response = await client.get(url)
140
+ return response.json()
141
+ '''
142
+ parser = PythonAstParser()
143
+ parser.parse(source, "test.py")
144
+
145
+ symbol = parser.find_enclosing_symbol(3)
146
+ assert symbol is not None
147
+ assert symbol.name == "fetch_data"
148
+ assert symbol.kind == "async_function"
149
+
150
+ def test_decorators(self):
151
+ source = '''
152
+ @staticmethod
153
+ @some_decorator
154
+ def decorated_function():
155
+ pass
156
+ '''
157
+ parser = PythonAstParser()
158
+ parser.parse(source, "test.py")
159
+
160
+ symbols = parser.find_all_symbols()
161
+ assert len(symbols) == 1
162
+ assert "staticmethod" in symbols[0].decorators
163
+ assert "some_decorator" in symbols[0].decorators
164
+
165
+ def test_syntax_error_returns_false(self):
166
+ source = '''
167
+ def broken(
168
+ # missing closing paren and body
169
+ '''
170
+ parser = PythonAstParser()
171
+ assert parser.parse(source, "test.py") is False
172
+ assert not parser.is_parsed
173
+
174
+ def test_format_snippet(self):
175
+ source = "line1\nline2\nline3\nline4\nline5"
176
+ parser = PythonAstParser()
177
+ parser.parse(source, "test.py")
178
+
179
+ snippet = parser.format_snippet(2, 4, highlight_line=3)
180
+ assert ">> " in snippet # Highlight marker
181
+ assert "line2" in snippet
182
+ assert "line3" in snippet
183
+ assert "line4" in snippet
184
+
185
+
186
+ class TestParserRegistry:
187
+ """Tests for the parser registry."""
188
+
189
+ def test_get_python_parser(self):
190
+ parser = get_parser(Language.PYTHON)
191
+ assert parser is not None
192
+ assert parser.language == Language.PYTHON
193
+
194
+ def test_get_parser_by_filepath(self):
195
+ parser = get_parser("test.py")
196
+ assert parser is not None
197
+ assert parser.language == Language.PYTHON
198
+
199
+ def test_registry_caches_parsers(self):
200
+ parser1 = get_parser(Language.PYTHON, create_new=False)
201
+ parser2 = get_parser(Language.PYTHON, create_new=False)
202
+ assert parser1 is parser2
203
+
204
+ def test_create_new_parser(self):
205
+ parser1 = get_parser(Language.PYTHON, create_new=True)
206
+ parser2 = get_parser(Language.PYTHON, create_new=True)
207
+ assert parser1 is not parser2
208
+
209
+ def test_registry_supports_language(self):
210
+ registry = get_registry()
211
+ assert registry.supports_language(Language.PYTHON)
212
+
213
+ def test_unsupported_language_returns_none(self):
214
+ parser = get_parser(Language.UNKNOWN)
215
+ assert parser is None
216
+
217
+
218
+ class TestSymbol:
219
+ """Tests for the Symbol class."""
220
+
221
+ def test_contains_line(self):
222
+ symbol = Symbol(
223
+ name="test",
224
+ kind="function",
225
+ start_line=10,
226
+ end_line=20,
227
+ )
228
+ assert symbol.contains_line(10)
229
+ assert symbol.contains_line(15)
230
+ assert symbol.contains_line(20)
231
+ assert not symbol.contains_line(9)
232
+ assert not symbol.contains_line(21)
233
+
234
+ def test_qualified_name_no_parent(self):
235
+ symbol = Symbol(name="func", kind="function", start_line=1, end_line=5)
236
+ assert symbol.qualified_name == "func"
237
+
238
+ def test_qualified_name_with_parent(self):
239
+ parent = Symbol(name="MyClass", kind="class", start_line=1, end_line=20)
240
+ child = Symbol(name="method", kind="method", start_line=5, end_line=10, parent=parent)
241
+ assert child.qualified_name == "MyClass.method"
242
+
243
+
244
+ class TestImport:
245
+ """Tests for the Import class."""
246
+
247
+ def test_python_import_string(self):
248
+ imp = Import(
249
+ module_name="os",
250
+ language=Language.PYTHON,
251
+ )
252
+ assert imp.full_import_string == "import os"
253
+
254
+ def test_python_import_with_alias(self):
255
+ imp = Import(
256
+ module_name="numpy",
257
+ alias="np",
258
+ language=Language.PYTHON,
259
+ )
260
+ assert imp.full_import_string == "import numpy as np"
261
+
262
+ def test_python_from_import(self):
263
+ imp = Import(
264
+ module_name="pathlib",
265
+ imported_names=["Path", "PurePath"],
266
+ language=Language.PYTHON,
267
+ )
268
+ assert "from pathlib import" in imp.full_import_string
269
+ assert "Path" in imp.full_import_string
270
+
271
+ def test_python_relative_import(self):
272
+ imp = Import(
273
+ module_name="utils",
274
+ imported_names=["helper"],
275
+ is_relative=True,
276
+ relative_level=2,
277
+ language=Language.PYTHON,
278
+ )
279
+ assert "from ..utils import" in imp.full_import_string
280
+
281
+ def test_javascript_import_string(self):
282
+ imp = Import(
283
+ module_name="./utils",
284
+ alias="utils",
285
+ language=Language.JAVASCRIPT,
286
+ )
287
+ assert "import utils from" in imp.full_import_string
288
+
289
+ def test_javascript_named_import(self):
290
+ imp = Import(
291
+ module_name="lodash",
292
+ imported_names=["map", "filter"],
293
+ language=Language.JAVASCRIPT,
294
+ )
295
+ assert "import {" in imp.full_import_string
296
+ assert "map" in imp.full_import_string
297
+
298
+
299
+ class TestTreeSitterParser:
300
+ """Tests for the TreeSitter multi-language parser."""
301
+
302
+ def test_tree_sitter_available(self):
303
+ """Verify tree-sitter is installed and available."""
304
+ from roma_debug.parsers.treesitter_parser import TREE_SITTER_AVAILABLE
305
+ assert TREE_SITTER_AVAILABLE, "tree-sitter should be available"
306
+
307
+ def test_javascript_parse_function(self):
308
+ """Test parsing JavaScript function."""
309
+ source = '''
310
+ function greet(name) {
311
+ console.log("Hello, " + name);
312
+ return true;
313
+ }
314
+
315
+ function multiply(x, y) {
316
+ return x * y;
317
+ }
318
+ '''
319
+ parser = get_parser(Language.JAVASCRIPT, create_new=True)
320
+ assert parser is not None
321
+ assert parser.parse(source, "test.js") is True
322
+
323
+ # Find symbols
324
+ symbols = parser.find_all_symbols()
325
+ names = [s.name for s in symbols]
326
+ assert "greet" in names
327
+ assert "multiply" in names
328
+
329
+ def test_javascript_find_enclosing_symbol(self):
330
+ """Test finding enclosing function in JavaScript."""
331
+ source = '''function outer() {
332
+ let x = 1;
333
+ function inner() {
334
+ let y = 2;
335
+ return y;
336
+ }
337
+ return inner();
338
+ }'''
339
+ parser = get_parser(Language.JAVASCRIPT, create_new=True)
340
+ parser.parse(source, "test.js")
341
+
342
+ # Line 4 is inside inner()
343
+ symbol = parser.find_enclosing_symbol(4)
344
+ assert symbol is not None
345
+ assert symbol.name == "inner"
346
+
347
+ def test_javascript_extract_imports(self):
348
+ """Test extracting JavaScript imports."""
349
+ source = '''
350
+ import React from 'react';
351
+ import { useState, useEffect } from 'react';
352
+ import * as utils from './utils';
353
+ const fs = require('fs');
354
+ '''
355
+ parser = get_parser(Language.JAVASCRIPT, create_new=True)
356
+ parser.parse(source, "test.js")
357
+
358
+ imports = parser.extract_imports()
359
+ module_names = [i.module_name for i in imports]
360
+ assert "react" in module_names or "'react'" in module_names
361
+
362
+ def test_go_parse_function(self):
363
+ """Test parsing Go function."""
364
+ source = '''
365
+ package main
366
+
367
+ import "fmt"
368
+
369
+ func greet(name string) string {
370
+ return fmt.Sprintf("Hello, %s!", name)
371
+ }
372
+
373
+ func main() {
374
+ msg := greet("World")
375
+ fmt.Println(msg)
376
+ }
377
+ '''
378
+ parser = get_parser(Language.GO, create_new=True)
379
+ assert parser is not None
380
+ assert parser.parse(source, "main.go") is True
381
+
382
+ symbols = parser.find_all_symbols()
383
+ names = [s.name for s in symbols]
384
+ assert "greet" in names
385
+ assert "main" in names
386
+
387
+ def test_go_find_enclosing_symbol(self):
388
+ """Test finding enclosing function in Go."""
389
+ source = '''package main
390
+
391
+ func process(data []int) int {
392
+ sum := 0
393
+ for _, v := range data {
394
+ sum += v
395
+ }
396
+ return sum
397
+ }'''
398
+ parser = get_parser(Language.GO, create_new=True)
399
+ parser.parse(source, "main.go")
400
+
401
+ # Line 5 is inside for loop in process()
402
+ symbol = parser.find_enclosing_symbol(5)
403
+ assert symbol is not None
404
+ assert symbol.name == "process"
405
+
406
+ def test_go_extract_imports(self):
407
+ """Test extracting Go imports."""
408
+ source = '''
409
+ package main
410
+
411
+ import (
412
+ "fmt"
413
+ "os"
414
+ "strings"
415
+ )
416
+
417
+ func main() {}
418
+ '''
419
+ parser = get_parser(Language.GO, create_new=True)
420
+ parser.parse(source, "main.go")
421
+
422
+ imports = parser.extract_imports()
423
+ module_names = [i.module_name for i in imports]
424
+ assert any("fmt" in m for m in module_names)
425
+
426
+ def test_rust_parse_function(self):
427
+ """Test parsing Rust function."""
428
+ source = '''
429
+ fn greet(name: &str) -> String {
430
+ format!("Hello, {}!", name)
431
+ }
432
+
433
+ pub fn main() {
434
+ let msg = greet("World");
435
+ println!("{}", msg);
436
+ }
437
+
438
+ struct Point {
439
+ x: i32,
440
+ y: i32,
441
+ }
442
+
443
+ impl Point {
444
+ fn new(x: i32, y: i32) -> Self {
445
+ Point { x, y }
446
+ }
447
+ }
448
+ '''
449
+ parser = get_parser(Language.RUST, create_new=True)
450
+ assert parser is not None
451
+ assert parser.parse(source, "main.rs") is True
452
+
453
+ symbols = parser.find_all_symbols()
454
+ names = [s.name for s in symbols]
455
+ assert "greet" in names
456
+ assert "main" in names
457
+ assert "Point" in names
458
+
459
+ def test_rust_find_enclosing_symbol(self):
460
+ """Test finding enclosing function in Rust."""
461
+ source = '''fn process(data: Vec<i32>) -> i32 {
462
+ let mut sum = 0;
463
+ for v in data {
464
+ sum += v;
465
+ }
466
+ sum
467
+ }'''
468
+ parser = get_parser(Language.RUST, create_new=True)
469
+ parser.parse(source, "main.rs")
470
+
471
+ # Line 3 is inside for loop
472
+ symbol = parser.find_enclosing_symbol(3)
473
+ assert symbol is not None
474
+ assert symbol.name == "process"
475
+
476
+ def test_java_parse_class(self):
477
+ """Test parsing Java class and methods."""
478
+ source = '''
479
+ public class Calculator {
480
+ private int value;
481
+
482
+ public Calculator() {
483
+ this.value = 0;
484
+ }
485
+
486
+ public int add(int x) {
487
+ this.value += x;
488
+ return this.value;
489
+ }
490
+
491
+ public static void main(String[] args) {
492
+ Calculator calc = new Calculator();
493
+ System.out.println(calc.add(5));
494
+ }
495
+ }
496
+ '''
497
+ parser = get_parser(Language.JAVA, create_new=True)
498
+ assert parser is not None
499
+ assert parser.parse(source, "Calculator.java") is True
500
+
501
+ symbols = parser.find_all_symbols()
502
+ names = [s.name for s in symbols]
503
+ assert "Calculator" in names
504
+ assert "add" in names
505
+ assert "main" in names
506
+
507
+ def test_format_snippet_with_highlight(self):
508
+ """Test snippet formatting with highlight line."""
509
+ source = "line1\nline2\nline3\nline4\nline5"
510
+ parser = get_parser(Language.JAVASCRIPT, create_new=True)
511
+ parser.parse(source, "test.js")
512
+
513
+ snippet = parser.format_snippet(2, 4, highlight_line=3)
514
+ assert "line2" in snippet
515
+ assert "line3" in snippet
516
+ assert "line4" in snippet
517
+ # Should have highlight marker on line 3
518
+ lines = snippet.split("\n")
519
+ line3 = [l for l in lines if "line3" in l][0]
520
+ assert ">>" in line3
521
+
522
+ def test_parser_fallback_gracefully(self):
523
+ """Test that parsers handle invalid input gracefully."""
524
+ parser = get_parser(Language.JAVASCRIPT, create_new=True)
525
+
526
+ # Parse valid code first
527
+ valid = "function test() { return 1; }"
528
+ assert parser.parse(valid, "test.js") is True
529
+
530
+ # Now parse invalid/partial code - should still attempt parse
531
+ partial = "function incomplete("
532
+ result = parser.parse(partial, "broken.js")
533
+ # Tree-sitter is lenient, may still produce partial AST
534
+ # The important thing is it doesn't crash