test: ProjectConf: Add unit tests

Add unit tests for the new ProjectConf module covering:

- Basic string and list value retrieval (get_str(), get_str_or_none(), get_list(), get_list_or_none()) - Quoted values with preserved spaces and comment delimiters - Inline comments outside quotes - Comma-separated lists with quoted commas - Line continuations - Multiple sections - Error cases: empty key, missing key/section, malformed sections, unfinished continuations, unclosed quotes - Error class is a subclass of ValueError

Also include a Makefile for running tests via `make test`.

Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-06-29 18:28:47 +02:00 committed by Jan Lindemann
commit 2ca66e34ba
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
2 changed files with 260 additions and 0 deletions

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,253 @@
import tempfile
from jw.pkg.lib.ProjectConf import ProjectConf
# -- Helper: create temp file and read it --
def _load(text: str) -> ProjectConf:
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f:
f.write(text)
f.flush()
return ProjectConf.read(f.name)
# -- Basic construction --
pc = _load('[section]\nkey = value\n')
assert pc.get_str('section', 'key') == 'value'
# -- get_section returns raw section text --
pc = _load('[meta]\n# header comment\nkey = val\n\n')
section = pc.get_section('meta')
assert section is not None
assert '# header comment' in section
assert 'key = val' in section
# -- get_section returns None for missing section --
pc = _load('[section]\nkey = value\n')
assert pc.get_section('nonexistent') is None
# -- get_str returns unquoted value --
pc = _load('[section]\nkey = "hello world"\n')
assert pc.get_str('section', 'key') == 'hello world'
# -- get_str returns raw value when not quoted --
pc = _load('[section]\nkey = plain value\n')
assert pc.get_str('section', 'key') == 'plain value'
# -- get_str raises Error when key missing --
pc = _load('[section]\nother = val\n')
try:
pc.get_str('section', 'missing')
assert False, 'Should have raised'
except ProjectConf.Error:
pass
# -- get_str raises Error when section missing --
pc = _load('[section]\nkey = val\n')
try:
pc.get_str('nonexistent', 'key')
assert False, 'Should have raised'
except ProjectConf.Error:
pass
# -- get_str_or_none returns None for missing key --
pc = _load('[section]\nother = val\n')
assert pc.get_str_or_none('section', 'missing') is None
# -- get_str_or_none returns None for missing section --
pc = _load('[section]\nkey = val\n')
assert pc.get_str_or_none('nonexistent', 'key') is None
# -- get_str returns string with trailing whitespace stripped --
pc = _load('[section]\nkey = value \n')
assert pc.get_str('section', 'key') == 'value'
# -- Quoted value with spaces preserved --
pc = _load('[section]\nkey = " spaced "\n')
assert pc.get_str('section', 'key') == ' spaced '
# -- Quoted hash inside quotes is preserved --
pc = _load('[section]\nkey = "value # not a comment"\n')
assert pc.get_str('section', 'key') == 'value # not a comment'
# -- Inline comment outside quotes --
pc = _load('[section]\nkey = value # this is a comment\n')
assert pc.get_str('section', 'key') == 'value'
# -- Quoted value containing an inline comment delimiter --
pc = _load('[section]\nkey = "has # inside"\n')
assert pc.get_str('section', 'key') == 'has # inside'
# -- get_list returns split values --
pc = _load('[section]\nlist = a, b, c\n')
assert pc.get_list('section', 'list') == ['a', 'b', 'c']
# -- get_list returns single value --
pc = _load('[section]\nlist = single\n')
assert pc.get_list('section', 'list') == ['single']
# -- get_list_or_none returns None for missing key --
pc = _load('[section]\nother = val\n')
assert pc.get_list_or_none('section', 'missing') is None
# -- get_list raises Error when missing key --
pc = _load('[section]\nother = val\n')
try:
pc.get_list('section', 'missing')
assert False, 'Should have raised'
except ProjectConf.Error:
pass
# -- get_list handles quoted commas --
pc = _load('[section]\nlist = "a, b", c, "d, e"\n')
assert pc.get_list('section', 'list') == ['a, b', 'c', 'd, e']
# -- get_list returns empty list for empty value --
pc = _load('[section]\nlist = \n')
assert pc.get_list('section', 'list') == []
# -- get_list_or_none returns empty list for empty value --
pc = _load('[section]\nlist = \n')
assert pc.get_list_or_none('section', 'list') == []
# -- Unclosed quote in list raises --
pc = _load('[section]\nlist = "unclosed, b\n')
try:
pc.get_list('section', 'list')
assert False, 'Should have raised'
except ProjectConf.Error as exc:
assert 'Unclosed quote' in str(exc)
# -- Line continuation --
pc = _load('[section]\nkey = first \\\n second \\\n third\n')
assert pc.get_str('section', 'key') == 'first second third'
# -- Line continuation with inline comment at end --
pc = _load('[section]\nkey = first \\\n second # comment\n')
assert pc.get_str('section', 'key') == 'first second'
# -- get_section returns None for non-existent section --
pc = _load('[section]\nkey = val\n')
assert pc.get_section('does_not_exist') is None
# -- Multiple keys in same section --
pc = _load('[section]\nfirst = one\nsecond = two\nthird = three\n')
assert pc.get_str('section', 'first') == 'one'
assert pc.get_str('section', 'second') == 'two'
assert pc.get_str('section', 'third') == 'three'
# -- get_str_or_none does not raise for missing section --
pc = _load('[section]\nkey = val\n')
assert pc.get_str_or_none('nope', 'key') is None
# -- get_list_or_none does not raise for missing section --
pc = _load('[section]\nkey = val\n')
assert pc.get_list_or_none('nope', 'key') is None
# -- get_list_or_none does not raise for missing key --
pc = _load('[section]\nother = val\n')
assert pc.get_list_or_none('section', 'nope') is None
# -- Empty key raises --
pc = _load('[section]\n= value\n')
try:
pc.get_str('section', '')
assert False, 'Should have raised'
except ProjectConf.Error:
pass
# -- Not an assignment raises --
pc = _load('[section]\nnot an assignment\n')
try:
pc.get_str('section', 'not an assignment')
assert False, 'Should have raised'
except ProjectConf.Error:
pass
# -- Unfinished continuation raises --
pc = _load('[section]\nkey = value \\\n')
try:
pc.get_str('section', 'key')
assert False, 'Should have raised'
except ProjectConf.Error:
pass
# -- Error class is ValueError --
pc = _load('[section]\nkey = val\n')
try:
pc.get_str('missing', 'key')
except ValueError as e:
assert isinstance(e, ValueError)
else:
assert False, 'Should have raised ValueError'
# -- Whitespace around key and value --
pc = _load('[section]\n key = value \n')
assert pc.get_str('section', 'key') == 'value'
# -- Whitespace around key in list --
pc = _load('[section]\nlist = a , b , c \n')
assert pc.get_list('section', 'list') == ['a', 'b', 'c']
# -- Multiple sections --
pc = _load('[one]\nkey = val1\n[two]\nkey = val2\n')
assert pc.get_str('one', 'key') == 'val1'
assert pc.get_str('two', 'key') == 'val2'
# -- Section with only comments and blanks --
pc = _load('[empty]\n# just a comment\n\n')
section = pc.get_section('empty')
assert section is not None
assert pc.get_str_or_none('empty', 'key') is None
# -- Section with trailing comment on section header line --
pc = _load('[section] # inline comment\nkey = val\n')
assert pc.get_str('section', 'key') == 'val'
# -- Value that looks like a section header --
pc = _load('[section]\nnot_a_key = [bracket]\n')
assert pc.get_str('section', 'not_a_key') == '[bracket]'
# -- Multiple values with trailing backslash comments --
pc = _load('[section]\nlist = "a", "b", "c"\n')
assert pc.get_list('section', 'list') == ['a', 'b', 'c']
print('All ProjectConf tests passed')