Skip to content

Commit 486d816

Browse files
Ken KundertKen Kundert
authored andcommitted
extend proposed tests to cover all interfaces of load/loads and dump/dumps
1 parent f2f6cc1 commit 486d816

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

proposed_tests/test_nt.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from voluptuous import Schema, Required, Any
77
from base64 import b64decode
88
import nestedtext as nt
9+
import os
10+
import pytest
911

1012
# GLOBALS {{{1
1113
TEST_SUITE = Path('tests.json')
@@ -74,6 +76,98 @@ def fail_message(self):
7476

7577
return '\n'.join([desc, expected, result])
7678

79+
# HELPER FUNCTIONS FOR FILE INTERFACE TESTING {{{1
80+
def prepare_load_input(content_bytes, input_type, tmp_path):
81+
"""
82+
Convert test content to specified input type for nt.load() or nt.loads().
83+
84+
Returns: (input_object, cleanup_function, loader_function)
85+
- loader_function: nt.loads or nt.load depending on input type
86+
"""
87+
cleanup = lambda: None # default no-op cleanup
88+
89+
if input_type == "string":
90+
# Return content directly for nt.loads()
91+
return content_bytes, cleanup, nt.loads
92+
93+
elif input_type == "str_path":
94+
path = tmp_path / "input.nt"
95+
path.write_bytes(content_bytes)
96+
return str(path), cleanup, nt.load
97+
98+
elif input_type == "pathlib":
99+
path = tmp_path / "input.nt"
100+
path.write_bytes(content_bytes)
101+
return path, cleanup, nt.load
102+
103+
elif input_type == "file_handle":
104+
path = tmp_path / "input.nt"
105+
path.write_bytes(content_bytes)
106+
fh = open(path, 'r', encoding='utf-8-sig') # utf-8-sig strips BOM
107+
cleanup = lambda: fh.close()
108+
return fh, cleanup, nt.load
109+
110+
elif input_type == "fd":
111+
path = tmp_path / "input.nt"
112+
path.write_bytes(content_bytes)
113+
fd = os.open(str(path), os.O_RDONLY)
114+
# nt.load() closes the FD, so cleanup should handle already-closed FD
115+
def cleanup_fd():
116+
try:
117+
os.close(fd)
118+
except OSError:
119+
pass # FD already closed by nt.load()
120+
return fd, cleanup_fd, nt.load
121+
122+
elif input_type == "iterator":
123+
# Write to file and read with universal newlines to match file behavior
124+
path = tmp_path / "input.nt"
125+
path.write_bytes(content_bytes)
126+
fh = open(path, 'r', encoding='utf-8-sig') # utf-8-sig strips BOM
127+
# Create an iterator from the file handle (mimics file iteration with proper universal newlines)
128+
cleanup = lambda: fh.close()
129+
return iter(fh), cleanup, nt.load
130+
131+
132+
def prepare_dump_output(output_type, tmp_path):
133+
"""
134+
Prepare output destination for nt.dump() or indicate nt.dumps().
135+
136+
Returns: (dest_object, result_path, cleanup_function, dumper_function)
137+
- dumper_function: nt.dumps or nt.dump depending on output type
138+
- For dumps: dest_object and result_path will be None
139+
"""
140+
cleanup = lambda: None # default no-op cleanup
141+
142+
if output_type == "string":
143+
# Return None for nt.dumps() (returns string directly)
144+
return None, None, cleanup, nt.dumps
145+
146+
elif output_type == "str_path":
147+
path = tmp_path / "output.nt"
148+
return str(path), path, cleanup, nt.dump
149+
150+
elif output_type == "pathlib":
151+
path = tmp_path / "output.nt"
152+
return path, path, cleanup, nt.dump
153+
154+
elif output_type == "file_handle":
155+
path = tmp_path / "output.nt"
156+
fh = open(path, 'w', encoding='utf-8')
157+
cleanup = lambda: fh.close()
158+
return fh, path, cleanup, nt.dump
159+
160+
elif output_type == "fd":
161+
path = tmp_path / "output.nt"
162+
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT, 0o644)
163+
# nt.dump() may close the FD, so cleanup should handle already-closed FD
164+
def cleanup_fd():
165+
try:
166+
os.close(fd)
167+
except OSError:
168+
pass # FD already closed by nt.dump()
169+
return fd, path, cleanup_fd, nt.dump
170+
77171
# TESTS {{{1
78172
@parametrize(
79173
path = TEST_DIR / TEST_SUITE,
@@ -140,3 +234,135 @@ def test_nt(tmp_path, load_in, load_out, load_err, encoding, types, request):
140234
checker.check(load_out, result, "re-loading")
141235
except nt.NestedTextError:
142236
checker.check(None, result, "re-loading")
237+
238+
239+
# NEW INTERFACE TESTS {{{1
240+
@parametrize(
241+
path = TEST_DIR / TEST_SUITE,
242+
key = "load_tests",
243+
schema = schema,
244+
)
245+
@pytest.mark.parametrize("input_type", ["string", "str_path", "pathlib", "file_handle", "fd", "iterator"])
246+
def test_nt_load_interfaces(tmp_path, input_type, load_in, load_out, load_err, encoding, types, request):
247+
"""Test nt.load() and nt.loads() with all supported input interface types"""
248+
checker = Checker(f"{request.node.callspec.id}/{input_type}")
249+
250+
# Prepare input in specified format
251+
content = b64decode(load_in.encode('ascii'))
252+
input_obj, cleanup, loader = prepare_load_input(content, input_type, tmp_path)
253+
254+
try:
255+
# Test load or loads using the returned loader function
256+
result = loader(input_obj, top=any)
257+
258+
if load_err:
259+
checker.check("@@@ an error @@@", result, f"loading via {input_type}")
260+
return
261+
else:
262+
checker.check(load_out, result, f"loading via {input_type}")
263+
264+
except nt.NestedTextError as e:
265+
result = dict(
266+
message = e.get_message(),
267+
line = e.line,
268+
lineno = e.lineno,
269+
colno = e.colno
270+
)
271+
checker.check(cull(load_err), cull(result), f"loading via {input_type}")
272+
return
273+
274+
except UnicodeDecodeError as e:
275+
# Handle unicode errors (same logic as test_nt)
276+
problematic = e.object[e.start:e.end]
277+
prefix = e.object[:e.start]
278+
lineno = prefix.count(b'\n')
279+
_, _, bol = prefix.rpartition(b'\n')
280+
eol, _, _ = e.object[e.start:].partition(b'\n')
281+
line = bol + eol
282+
colno = line.index(problematic)
283+
284+
if encoding != 'bytes':
285+
line = line.decode(encoding)
286+
else:
287+
line = line.decode('ascii', errors='backslashreplace')
288+
load_err['line'] = load_err['line'].encode(
289+
'ascii', errors='backslashreplace'
290+
).decode('ascii')
291+
292+
result = dict(
293+
message = e.reason,
294+
line = line,
295+
lineno = lineno,
296+
colno = colno,
297+
)
298+
checker.check(load_err, result, f"loading via {input_type}")
299+
return
300+
301+
finally:
302+
cleanup()
303+
304+
305+
@parametrize(
306+
path = TEST_DIR / TEST_SUITE,
307+
key = "load_tests",
308+
schema = schema,
309+
)
310+
@pytest.mark.parametrize("output_type", ["string", "str_path", "pathlib", "file_handle", "fd"])
311+
def test_nt_dump_interfaces(tmp_path, output_type, load_in, load_out, load_err, encoding, types, request):
312+
"""Test nt.dump() and nt.dumps() with all supported output interface types"""
313+
checker = Checker(f"{request.node.callspec.id}/{output_type}")
314+
315+
# Skip if test expects a load error (nothing to dump)
316+
if load_err:
317+
pytest.skip("Test case expects load error, nothing to dump")
318+
319+
# First load the data using loads()
320+
content = b64decode(load_in.encode('ascii'))
321+
obj = nt.loads(content, top=any)
322+
323+
# Prepare output destination
324+
dest, result_path, cleanup, dumper = prepare_dump_output(output_type, tmp_path)
325+
326+
try:
327+
# Dump using the returned dumper function
328+
if dumper is nt.dumps:
329+
dumped_content = dumper(obj)
330+
else:
331+
dumper(obj, dest)
332+
# For file handles/descriptors, need to close before reading
333+
cleanup()
334+
# Read back the dumped content
335+
dumped_content = result_path.read_text(encoding='utf-8')
336+
# Verify trailing newline is present (dump() adds it, dumps() doesn't)
337+
assert dumped_content.endswith('\n'), f"dump() should add trailing newline via {output_type}"
338+
339+
# Verify content can be loaded back and matches original
340+
reloaded = nt.loads(dumped_content, top=any)
341+
checker.check(load_out, reloaded, f"dump via {output_type} then reload")
342+
343+
except nt.NestedTextError:
344+
# Some data structures may not be dumpable
345+
checker.check(None, obj, f"dumping via {output_type}")
346+
347+
348+
def test_nt_dump_trailing_newline(tmp_path):
349+
"""Verify that dump() adds trailing newline while dumps() does not"""
350+
test_cases = [
351+
{"key": "value"},
352+
["item1", "item2"],
353+
"simple string"
354+
]
355+
356+
for data in test_cases:
357+
# Get dumps() result (no trailing newline unless already in content)
358+
dumps_result = nt.dumps(data)
359+
360+
# Test with string path
361+
path = tmp_path / "test_newline.nt"
362+
nt.dump(data, str(path))
363+
file_content = path.read_text()
364+
365+
# Verify dump() adds exactly one trailing newline
366+
assert file_content.endswith('\n'), "dump() should add trailing newline"
367+
assert not file_content.endswith('\n\n'), "dump() should not add double newline"
368+
assert file_content == dumps_result + '\n', "dump() should be dumps() + newline"

0 commit comments

Comments
 (0)