Skip to content

Commit 494614f

Browse files
authored
better tests (#4)
- Introduces more tests - Introduces the virtual file system test helper - Adds convenience methods to the following classes: - Position: __eq__ and __repr__ - Querier: __repr__
1 parent 4230e7f commit 494614f

File tree

11 files changed

+463
-54
lines changed

11 files changed

+463
-54
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,6 @@ jobs:
2525
uses: actions/setup-python@v2
2626
with:
2727
python-version: "3.11.9"
28-
29-
- name: Setup venv
30-
run: python -m venv .venv
31-
32-
- name: Install Python dependencies
33-
run: |
34-
python -m pip install --upgrade pip
35-
python -m pip install maturin
36-
28+
3729
- name: Run tests
38-
run: |
39-
. .venv/bin/activate
40-
maturin develop
41-
which python
42-
which maturin
43-
python tests/test.py
30+
run: make setup test

Makefile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1+
# Run all commands in a single shell, so that the virtual environment is activated for all commands.
2+
.ONESHELL:
3+
14
.PHONY: setup
25
setup:
36
python -m venv .venv
47
. .venv/bin/activate
58
pip install maturin
9+
pip install pytest
610

711
.PHONY: develop
812
develop:
13+
. .venv/bin/activate
914
maturin develop
1015

11-
1216
test: develop
13-
## TODO: Add actual tests with pytest
14-
python tests/test.py
17+
. .venv/bin/activate
18+
pytest
1519

1620
.PHONY: validate-tags release
1721

py.typed

Whitespace-only changes.

src/classes.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub struct Position {
3030
#[pyclass]
3131
pub struct Querier {
3232
db_reader: SQLiteReader,
33+
db_path: String,
3334
}
3435

3536
#[pymethods]
@@ -38,7 +39,8 @@ impl Querier {
3839
pub fn new(db_path: String) -> Self {
3940
println!("Opening database: {}", db_path);
4041
Querier {
41-
db_reader: SQLiteReader::open(db_path).unwrap(),
42+
db_reader: SQLiteReader::open(db_path.clone()).unwrap(),
43+
db_path: db_path,
4244
}
4345
}
4446

@@ -54,6 +56,10 @@ impl Querier {
5456

5557
Ok(positions)
5658
}
59+
60+
fn __repr__(&self) -> String {
61+
format!("Querier(db_path=\"{}\")", self.db_path)
62+
}
5763
}
5864

5965
// TODO(@nohehf): Indexer class
@@ -64,6 +70,17 @@ impl Position {
6470
fn new(path: String, line: usize, column: usize) -> Self {
6571
Position { path, line, column }
6672
}
73+
74+
fn __eq__(&self, other: &Position) -> bool {
75+
self.path == other.path && self.line == other.line && self.column == other.column
76+
}
77+
78+
fn __repr__(&self) -> String {
79+
format!(
80+
"Position(path=\"{}\", line={}, column={})",
81+
self.path, self.line, self.column
82+
)
83+
}
6784
}
6885

6986
impl Display for Position {

stack_graphs_python.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ class Position:
1212
column: int
1313

1414
def __init__(self, path: str, line: int, column: int) -> None: ...
15+
def __eq__(self, other: object) -> bool: ...
16+
def __repr__(self) -> str: ...
1517

1618
class Querier:
1719
def __init__(self, db_path: str) -> None: ...
1820
def definitions(self, reference: Position) -> list[Position]: ...
21+
def __repr__(self) -> str: ...
1922

2023
def index(paths: list[str], db_path: str, language: Language) -> None: ...

tests/from_dir_test.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
from stack_graphs_python import index, Querier, Position, Language
3+
4+
5+
# index ./js_sample directory
6+
# This test is the same as the one in js_ok_test.py, without using the virtual file system helper
7+
def test_from_dir():
8+
# convert ./js_sample directory to absolute path
9+
dir = os.path.abspath("./tests/js_sample")
10+
db_path = os.path.abspath("./db.sqlite")
11+
12+
print("Indexing directory: ", dir)
13+
print("Database path: ", db_path)
14+
15+
index([dir], db_path, language=Language.JavaScript)
16+
17+
source_reference = Position(path=dir + "/index.js", line=2, column=12)
18+
19+
print("Querying definition for: ", source_reference.path)
20+
21+
querier = Querier(db_path)
22+
23+
results = querier.definitions(source_reference)
24+
25+
print("Results: ", results)
26+
27+
for result in results:
28+
print("Path: ", result.path)
29+
print("Line: ", result.line)
30+
print("Column: ", result.column)
31+
print("\n")
32+
33+
assert len(results) == 2
34+
assert results[0].path.endswith("index.js")
35+
assert results[0].line == 0
36+
assert results[0].column == 9

tests/helpers/virtual_files.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import os
2+
import re
3+
import tempfile
4+
import shutil
5+
from typing import Iterator
6+
from stack_graphs_python import Position
7+
import contextlib
8+
9+
10+
POSITION_MARKER_REGEX = r"( *)\^{([a-zA-Z0-9_]+)}\n"
11+
FILE_SEPARATOR_REGEX = r"\n;---([a-zA-Z0-9\_./]+)---\n"
12+
13+
14+
def _remove_position_markers(string: str) -> str:
15+
return re.subn(POSITION_MARKER_REGEX, "", string)[0]
16+
17+
18+
def _split_files(string: str) -> list[tuple[str, str]]:
19+
"""
20+
Split a string into a list of tuples where each tuple contains the file path and the file content
21+
"""
22+
files = []
23+
matches = [m for m in re.finditer(FILE_SEPARATOR_REGEX, string)]
24+
# Get the file path from match group 0
25+
# Get the file content from the end of the previous match to the start of the current match
26+
for i in range(1, len(matches) + 1):
27+
end = len(string) if i == len(matches) else matches[i].start()
28+
file_path = matches[i - 1].group(1)
29+
file_content = string[matches[i - 1].end() : end]
30+
files.append((file_path, file_content))
31+
32+
return files
33+
34+
35+
def _get_positions_in_file(file_path: str, contents: str) -> dict[str, Position]:
36+
positions = {}
37+
while match := re.search(POSITION_MARKER_REGEX, contents):
38+
identifier = match.group(2)
39+
line = contents.count("\n", 0, match.start()) - 1
40+
# count the number of spaces
41+
column = len(match.group(1))
42+
positions[identifier] = Position(file_path, line, column)
43+
contents = contents[: match.start()] + contents[match.end() :]
44+
45+
return positions
46+
47+
48+
@contextlib.contextmanager
49+
def string_to_virtual_repo(
50+
string: str,
51+
) -> Iterator[tuple[str, dict[str, Position]]]:
52+
"""
53+
# Usage
54+
```py
55+
string = \"""
56+
;---main.py---
57+
from module import module
58+
^{pos1}
59+
60+
;---module.py---
61+
module = "module.py"
62+
^{pos2}
63+
\"""
64+
65+
with string_to_virtual_repo(string) as (repo_path, positions):
66+
...
67+
```
68+
69+
# Description
70+
71+
This is a utility to create a virtual repository from a string
72+
It allows to quickly write tests, as creating those files and getting the correct positions is cumbersome
73+
74+
It parses a string into a list of files and positions in the files
75+
Files separators are defined as ';---{filepath}---'. It should be preceded by a newline and followed by a newline
76+
Positions queries are defined as '^{identifier}' where identifier is a string, prefixed by any number of spaces
77+
Positions queries are stripped from the file content in the temporary filesystem
78+
79+
For example this string:
80+
```py
81+
string = \"""
82+
;---main.py---
83+
from module import module
84+
^{pos1}
85+
import lib
86+
from lib.file import file
87+
88+
print(module)
89+
print(lib.lib)
90+
^{pos2}
91+
print(file)
92+
93+
;---module.py---
94+
module = "module.py"
95+
^{pos3}
96+
97+
;---lib/__init__.py---
98+
lib = "lib/__init__.py"
99+
100+
;---lib/file.py---
101+
file = "lib/file.py"
102+
\"""
103+
```
104+
105+
When parsed via:
106+
```py
107+
with string_to_virtual_repo(string) as (repo_path, positions):
108+
...
109+
```
110+
111+
Will create the following temporary filesystem:
112+
```sh
113+
/
114+
├── main.py
115+
├── module.py
116+
└── lib
117+
├── __init__.py
118+
└── file.py
119+
```
120+
121+
And output the following positions:
122+
```py
123+
{
124+
"pos1": Position("main.py", 0, 6),
125+
"pos2": Position("main.py", 5, 11),
126+
"pos3": Position("module.py", 0, 10),
127+
}
128+
```
129+
"""
130+
files = _split_files(string)
131+
temp_dir = tempfile.mkdtemp()
132+
temp_dir = os.path.abspath(tempfile.mkdtemp())
133+
positions_map = {}
134+
for file_path, file_content in files:
135+
abs_path = os.path.join(temp_dir, file_path)
136+
positions_map.update(_get_positions_in_file(abs_path, file_content))
137+
138+
try:
139+
for file_path, file_content in files:
140+
file_content = _remove_position_markers(file_content)
141+
full_path = os.path.join(temp_dir, file_path)
142+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
143+
with open(full_path, "w") as f:
144+
f.write(file_content)
145+
yield temp_dir, positions_map
146+
except Exception as e:
147+
print(e)
148+
finally:
149+
print("Removing temp dir")
150+
shutil.rmtree(temp_dir)

0 commit comments

Comments
 (0)