Skip to content

Commit 1e8bcb3

Browse files
authored
Merge pull request #137 from nlothian/fix-kilocode/103-implement-iso-char-io-predicates
feat(io): add get_char/1, get_char/2, put_char/1, put_char/2 with tests and docs
2 parents c8b7460 + b5b976a commit 1e8bcb3

3 files changed

Lines changed: 471 additions & 2 deletions

File tree

FEATURES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@
107107
-`read_from_chars/2` – Parse term from character list/string
108108
- ⚠️ `write_term_to_chars/3` – Write term to character list with options (basic implementation, operator handling incomplete)
109109
-`read/1`, `read/2` – Read term from input streams
110-
- `get_char/1` – Read character from input
111-
- `put_char/1` – Write character to output
110+
- `get_char/1` – Read character from input
111+
- `put_char/1` – Write character to output
112112
-`open/3` – Open file stream
113113
-`close/1` – Close stream
114114
-`current_input/1` – Get current input stream

tests/test_char_io.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
"""Tests for character I/O predicates (get_char/1-2, put_char/1-2)."""
2+
3+
import io
4+
import tempfile
5+
import pytest
6+
from vibeprolog import PrologInterpreter
7+
from vibeprolog.exceptions import PrologThrow
8+
9+
10+
@pytest.fixture
11+
def prolog() -> PrologInterpreter:
12+
return PrologInterpreter()
13+
14+
15+
class TestGetChar:
16+
"""Tests for get_char/1 and get_char/2 predicates."""
17+
18+
def test_get_char_from_string_stream(self, prolog: PrologInterpreter):
19+
"""Test reading characters from a string-based stream."""
20+
# Create a temporary file with test content
21+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
22+
f.write("abc")
23+
temp_file = f.name
24+
25+
try:
26+
# Open the file for reading
27+
open_result = prolog.query_once(f"open('{temp_file}', read, Stream)")
28+
assert open_result is not None
29+
stream_handle = open_result["Stream"]
30+
31+
# Read first character
32+
result1 = prolog.query_once(f"get_char({stream_handle}, Char1)")
33+
assert result1 is not None
34+
assert result1["Char1"] == "a"
35+
36+
# Read second character
37+
result2 = prolog.query_once(f"get_char({stream_handle}, Char2)")
38+
assert result2 is not None
39+
assert result2["Char2"] == "b"
40+
41+
# Read third character
42+
result3 = prolog.query_once(f"get_char({stream_handle}, Char3)")
43+
assert result3 is not None
44+
assert result3["Char3"] == "c"
45+
46+
# Read EOF
47+
result4 = prolog.query_once(f"get_char({stream_handle}, Char4)")
48+
assert result4 is not None
49+
assert result4["Char4"] == "end_of_file"
50+
51+
# Close stream
52+
prolog.query_once(f"close({stream_handle})")
53+
finally:
54+
import os
55+
os.unlink(temp_file)
56+
57+
def test_get_char_current_input_fails(self, prolog: PrologInterpreter):
58+
"""Test that get_char/1 fails on current input (stdin) with permission_error."""
59+
# This should fail because stdin is captured by pytest
60+
result = prolog.query_once("catch(get_char(Char), Error, true)")
61+
assert result is not None
62+
error_term = result["Error"]
63+
assert "error" in error_term
64+
permission_error = error_term["error"][0]
65+
assert "permission_error" in permission_error
66+
args = permission_error["permission_error"]
67+
assert args[0] == "input"
68+
assert args[1] == "stream"
69+
70+
def test_get_char_invalid_stream(self, prolog: PrologInterpreter):
71+
"""Test get_char/2 with invalid stream raises existence_error."""
72+
result = prolog.query_once("catch(get_char(invalid_stream, Char), Error, true)")
73+
assert result is not None
74+
error_term = result["Error"]
75+
assert "error" in error_term
76+
existence_error = error_term["error"][0]
77+
assert "existence_error" in existence_error
78+
args = existence_error["existence_error"]
79+
assert args[0] == "stream"
80+
81+
def test_get_char_write_only_stream(self, prolog: PrologInterpreter):
82+
"""Test get_char on write-only stream raises permission_error."""
83+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
84+
temp_file = f.name
85+
86+
try:
87+
# Open for writing
88+
open_result = prolog.query_once(f"open('{temp_file}', write, Stream)")
89+
assert open_result is not None
90+
stream_handle = open_result["Stream"]
91+
92+
# Try to read from write-only stream
93+
result = prolog.query_once(f"catch(get_char({stream_handle}, Char), Error, true)")
94+
assert result is not None
95+
error_term = result["Error"]
96+
assert "error" in error_term
97+
permission_error = error_term["error"][0]
98+
assert "permission_error" in permission_error
99+
args = permission_error["permission_error"]
100+
assert args[0] == "input"
101+
assert args[1] == "stream"
102+
103+
prolog.query_once(f"close({stream_handle})")
104+
finally:
105+
import os
106+
os.unlink(temp_file)
107+
108+
109+
class TestPutChar:
110+
"""Tests for put_char/1 and put_char/2 predicates."""
111+
112+
def test_put_char_to_file(self, prolog: PrologInterpreter):
113+
"""Test writing characters to a file stream."""
114+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
115+
temp_file = f.name
116+
117+
try:
118+
# Open the file for writing
119+
open_result = prolog.query_once(f"open('{temp_file}', write, Stream)")
120+
assert open_result is not None
121+
stream_handle = open_result["Stream"]
122+
123+
# Write characters
124+
assert prolog.query_once(f"put_char({stream_handle}, 'H')") == {}
125+
assert prolog.query_once(f"put_char({stream_handle}, 'e')") == {}
126+
assert prolog.query_once(f"put_char({stream_handle}, 'l')") == {}
127+
assert prolog.query_once(f"put_char({stream_handle}, 'l')") == {}
128+
assert prolog.query_once(f"put_char({stream_handle}, 'o')") == {}
129+
130+
# Close stream
131+
prolog.query_once(f"close({stream_handle})")
132+
133+
# Verify content
134+
with open(temp_file, 'r') as f:
135+
content = f.read()
136+
assert content == "Hello"
137+
finally:
138+
import os
139+
os.unlink(temp_file)
140+
141+
def test_put_char_invalid_character(self, prolog: PrologInterpreter):
142+
"""Test put_char with invalid character raises type_error."""
143+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
144+
temp_file = f.name
145+
146+
try:
147+
open_result = prolog.query_once(f"open('{temp_file}', write, Stream)")
148+
assert open_result is not None
149+
stream_handle = open_result["Stream"]
150+
151+
# Try to write multi-character atom
152+
result = prolog.query_once(f"catch(put_char({stream_handle}, 'ab'), Error, true)")
153+
assert result is not None
154+
error_term = result["Error"]
155+
assert "error" in error_term
156+
type_error = error_term["error"][0]
157+
assert "type_error" in type_error
158+
args = type_error["type_error"]
159+
assert args[0] == "in_character"
160+
161+
# Try to write non-atom
162+
result2 = prolog.query_once(f"catch(put_char({stream_handle}, 123), Error2, true)")
163+
assert result2 is not None
164+
error_term2 = result2["Error2"]
165+
assert "error" in error_term2
166+
type_error2 = error_term2["error"][0]
167+
assert "type_error" in type_error2
168+
169+
prolog.query_once(f"close({stream_handle})")
170+
finally:
171+
import os
172+
os.unlink(temp_file)
173+
174+
def test_put_char_invalid_stream(self, prolog: PrologInterpreter):
175+
"""Test put_char/2 with invalid stream raises existence_error."""
176+
result = prolog.query_once("catch(put_char(invalid_stream, 'a'), Error, true)")
177+
assert result is not None
178+
error_term = result["Error"]
179+
assert "error" in error_term
180+
existence_error = error_term["error"][0]
181+
assert "existence_error" in existence_error
182+
args = existence_error["existence_error"]
183+
assert args[0] == "stream"
184+
185+
def test_put_char_read_only_stream(self, prolog: PrologInterpreter):
186+
"""Test put_char on read-only stream raises permission_error."""
187+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
188+
f.write("test")
189+
temp_file = f.name
190+
191+
try:
192+
# Open for reading
193+
open_result = prolog.query_once(f"open('{temp_file}', read, Stream)")
194+
assert open_result is not None
195+
stream_handle = open_result["Stream"]
196+
197+
# Try to write to read-only stream
198+
result = prolog.query_once(f"catch(put_char({stream_handle}, 'a'), Error, true)")
199+
assert result is not None
200+
error_term = result["Error"]
201+
assert "error" in error_term
202+
permission_error = error_term["error"][0]
203+
assert "permission_error" in permission_error
204+
args = permission_error["permission_error"]
205+
assert args[0] == "output"
206+
assert args[1] == "stream"
207+
208+
prolog.query_once(f"close({stream_handle})")
209+
finally:
210+
import os
211+
os.unlink(temp_file)
212+
213+
def test_put_char_uninstantiated_args(self, prolog: PrologInterpreter):
214+
"""Test put_char with uninstantiated arguments raises instantiation_error."""
215+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
216+
temp_file = f.name
217+
218+
try:
219+
open_result = prolog.query_once(f"open('{temp_file}', write, Stream)")
220+
assert open_result is not None
221+
stream_handle = open_result["Stream"]
222+
223+
# Uninstantiated character
224+
result = prolog.query_once(f"catch(put_char({stream_handle}, Char), Error, true)")
225+
assert result is not None
226+
error_term = result["Error"]
227+
assert "error" in error_term
228+
instantiation_error = error_term["error"][0]
229+
assert "instantiation_error" in instantiation_error
230+
231+
# Uninstantiated stream
232+
result2 = prolog.query_once("catch(put_char(Stream, 'a'), Error2, true)")
233+
assert result2 is not None
234+
error_term2 = result2["Error2"]
235+
assert "error" in error_term2
236+
instantiation_error2 = error_term2["error"][0]
237+
assert "instantiation_error" in instantiation_error2
238+
239+
prolog.query_once(f"close({stream_handle})")
240+
finally:
241+
import os
242+
os.unlink(temp_file)
243+
244+
245+
class TestCharIOEdgeCases:
246+
"""Tests for edge cases in character I/O."""
247+
248+
def test_get_char_newline(self, prolog: PrologInterpreter):
249+
"""Test reading newline character."""
250+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
251+
f.write("a\nb")
252+
temp_file = f.name
253+
254+
try:
255+
open_result = prolog.query_once(f"open('{temp_file}', read, Stream)")
256+
assert open_result is not None
257+
stream_handle = open_result["Stream"]
258+
259+
result1 = prolog.query_once(f"get_char({stream_handle}, Char1)")
260+
assert result1["Char1"] == "a"
261+
262+
result2 = prolog.query_once(f"get_char({stream_handle}, Char2)")
263+
assert result2["Char2"] == "\n"
264+
265+
result3 = prolog.query_once(f"get_char({stream_handle}, Char3)")
266+
assert result3["Char3"] == "b"
267+
268+
prolog.query_once(f"close({stream_handle})")
269+
finally:
270+
import os
271+
os.unlink(temp_file)
272+
273+
def test_put_char_special_chars(self, prolog: PrologInterpreter):
274+
"""Test writing special characters."""
275+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
276+
temp_file = f.name
277+
278+
try:
279+
open_result = prolog.query_once(f"open('{temp_file}', write, Stream)")
280+
assert open_result is not None
281+
stream_handle = open_result["Stream"]
282+
283+
# Write various special characters
284+
assert prolog.query_once(f"put_char({stream_handle}, '\\n')") == {}
285+
assert prolog.query_once(f"put_char({stream_handle}, '\\t')") == {}
286+
assert prolog.query_once(f"put_char({stream_handle}, ' ')") == {}
287+
288+
prolog.query_once(f"close({stream_handle})")
289+
290+
with open(temp_file, 'r') as f:
291+
content = f.read()
292+
assert content == "\n\t "
293+
finally:
294+
import os
295+
os.unlink(temp_file)

0 commit comments

Comments
 (0)