-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathtest_script.py
More file actions
246 lines (203 loc) · 8.92 KB
/
test_script.py
File metadata and controls
246 lines (203 loc) · 8.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""Tests for the QQL script runner (src/qql/script.py)."""
from __future__ import annotations
import pytest
from rich.console import Console
from qql.exceptions import QQLRuntimeError
from qql.executor import ExecutionResult
from qql.lexer import Lexer
from qql.script import run_script, split_statements, strip_comments
# ── Helpers ───────────────────────────────────────────────────────────────────
def tokenize(text: str):
return Lexer().tokenize(text)
def null_console() -> Console:
"""A Console that writes to /dev/null — suppresses output in tests."""
return Console(quiet=True)
# ── strip_comments ────────────────────────────────────────────────────────────
class TestStripComments:
def test_removes_full_line_comment(self):
result = strip_comments("-- this is a comment\nCREATE COLLECTION x")
assert "-- this" not in result
assert "CREATE" in result
def test_removes_inline_comment(self):
result = strip_comments("CREATE COLLECTION x -- inline note")
assert "-- inline" not in result
assert "CREATE COLLECTION x" in result
def test_preserves_non_comment_lines(self):
text = "CREATE COLLECTION x\nSHOW COLLECTIONS"
assert strip_comments(text) == text
def test_empty_string(self):
assert strip_comments("") == ""
def test_only_comments(self):
result = strip_comments("-- line 1\n-- line 2")
assert "line" not in result
def test_comment_at_start_of_line(self):
result = strip_comments(" -- leading spaces then comment\nDROP COLLECTION x")
assert "DROP" in result
assert "leading" not in result
def test_preserves_double_dash_inside_string_literal(self):
text = "INSERT INTO COLLECTION x VALUES {'text': 'hello--world'} -- trailing comment"
result = strip_comments(text)
assert "hello--world" in result
assert "trailing comment" not in result
# ── split_statements ──────────────────────────────────────────────────────────
class TestSplitStatements:
def test_single_statement(self):
tokens = tokenize("CREATE COLLECTION x")
chunks = split_statements(tokens)
assert len(chunks) == 1
def test_two_statements(self):
tokens = tokenize("CREATE COLLECTION x\nSHOW COLLECTIONS")
chunks = split_statements(tokens)
assert len(chunks) == 2
def test_three_statements(self):
tokens = tokenize(
"CREATE COLLECTION x\n"
"INSERT INTO COLLECTION x VALUES {'text': 'hi'}\n"
"SHOW COLLECTIONS"
)
chunks = split_statements(tokens)
assert len(chunks) == 3
def test_bulk_insert_not_split_inside_brackets(self):
"""INSERT keyword inside a VALUES [...] array must NOT start a new chunk."""
tokens = tokenize(
"INSERT BULK INTO COLLECTION x VALUES [\n"
" {'text': 'a'},\n"
" {'text': 'b'}\n"
"]\n"
"SHOW COLLECTIONS"
)
chunks = split_statements(tokens)
# There should be exactly 2 chunks: INSERT BULK and SHOW COLLECTIONS
assert len(chunks) == 2
def test_empty_input(self):
tokens = tokenize("")
chunks = split_statements(tokens)
assert chunks == []
def test_first_chunk_starts_with_create(self):
tokens = tokenize("CREATE COLLECTION x\nDROP COLLECTION x")
chunks = split_statements(tokens)
from qql.lexer import TokenKind
assert chunks[0][0].kind == TokenKind.CREATE
assert chunks[1][0].kind == TokenKind.DROP
def test_recommend_starts_new_top_level_statement(self):
from qql.lexer import TokenKind
tokens = tokenize(
"SEARCH x SIMILAR TO 'stroke' LIMIT 5\n"
"RECOMMEND FROM x POSITIVE IDS ('id-1') LIMIT 3\n"
"SHOW COLLECTIONS"
)
chunks = split_statements(tokens)
assert len(chunks) == 3
assert chunks[1][0].kind == TokenKind.RECOMMEND
def test_scroll_starts_new_top_level_statement(self):
from qql.lexer import TokenKind
tokens = tokenize(
"SHOW COLLECTIONS\n"
"SCROLL FROM x LIMIT 10\n"
"DROP COLLECTION x"
)
chunks = split_statements(tokens)
assert len(chunks) == 3
assert chunks[1][0].kind == TokenKind.SCROLL
def test_select_starts_new_top_level_statement(self):
from qql.lexer import TokenKind
tokens = tokenize(
"SHOW COLLECTIONS\n"
"SELECT * FROM x WHERE id = 'id-1'\n"
"DROP COLLECTION x"
)
chunks = split_statements(tokens)
assert len(chunks) == 3
assert chunks[1][0].kind == TokenKind.SELECT
def test_alter_starts_new_top_level_statement(self):
from qql.lexer import TokenKind
tokens = tokenize(
"CREATE COLLECTION x\n"
"ALTER COLLECTION x WITH HNSW { payload_m: 24 }\n"
"SHOW COLLECTIONS"
)
chunks = split_statements(tokens)
assert len(chunks) == 3
assert chunks[1][0].kind == TokenKind.ALTER
# ── run_script ────────────────────────────────────────────────────────────────
class TestRunScript:
@pytest.fixture
def script_file(self, tmp_path):
"""Factory: write content to a temp .qql file and return its path."""
def _make(content: str) -> str:
p = tmp_path / "test.qql"
p.write_text(content)
return str(p)
return _make
@pytest.fixture
def mock_executor(self, mocker):
ex = mocker.MagicMock()
ex.execute.return_value = ExecutionResult(success=True, message="ok")
return ex
def test_executes_all_statements(self, script_file, mock_executor):
path = script_file(
"CREATE COLLECTION x\n"
"SHOW COLLECTIONS\n"
)
ok, fail = run_script(path, mock_executor, null_console(), null_console())
assert mock_executor.execute.call_count == 2
assert ok == 2
assert fail == 0
def test_continues_on_error_by_default(self, script_file, mock_executor):
mock_executor.execute.side_effect = [
ExecutionResult(success=True, message="ok"),
QQLRuntimeError("boom"),
ExecutionResult(success=True, message="ok"),
]
path = script_file(
"CREATE COLLECTION x\n"
"DROP COLLECTION missing\n"
"SHOW COLLECTIONS\n"
)
ok, fail = run_script(path, mock_executor, null_console(), null_console())
assert ok == 2
assert fail == 1
assert mock_executor.execute.call_count == 3
def test_stops_on_error_when_flag_set(self, script_file, mock_executor):
mock_executor.execute.side_effect = [
QQLRuntimeError("fail fast"),
ExecutionResult(success=True, message="ok"),
]
path = script_file(
"CREATE COLLECTION x\n"
"SHOW COLLECTIONS\n"
)
ok, fail = run_script(
path, mock_executor, null_console(), null_console(), stop_on_error=True
)
assert fail == 1
assert mock_executor.execute.call_count == 1 # stopped after first
def test_empty_script_returns_zero_counts(self, script_file, mock_executor):
path = script_file("-- only comments\n\n")
ok, fail = run_script(path, mock_executor, null_console(), null_console())
assert ok == 0
assert fail == 0
mock_executor.execute.assert_not_called()
def test_comments_are_stripped(self, script_file, mock_executor):
path = script_file(
"-- header comment\n"
"CREATE COLLECTION x -- inline comment\n"
)
ok, fail = run_script(path, mock_executor, null_console(), null_console())
assert ok == 1
assert fail == 0
def test_recommend_statement_executes_from_script(self, script_file, mock_executor):
path = script_file(
"SEARCH x SIMILAR TO 'stroke' LIMIT 5\n"
"RECOMMEND FROM x POSITIVE IDS ('id-1') LIMIT 3\n"
)
ok, fail = run_script(path, mock_executor, null_console(), null_console())
assert ok == 2
assert fail == 0
assert mock_executor.execute.call_count == 2
def test_nonexistent_file_returns_failure(self, mock_executor):
ok, fail = run_script(
"/no/such/file.qql", mock_executor, null_console(), null_console()
)
assert ok == 0
assert fail == 1