From 2ca66e34baff8cfb40e1e2b5a94e39679387aeab Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 29 Jun 2026 18:28:47 +0200 Subject: [PATCH] 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 --- .../python/jw/pkg/lib/ProjectConf/Makefile | 7 + .../python/jw/pkg/lib/ProjectConf/test.py | 253 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 test/unit/python/jw/pkg/lib/ProjectConf/Makefile create mode 100644 test/unit/python/jw/pkg/lib/ProjectConf/test.py diff --git a/test/unit/python/jw/pkg/lib/ProjectConf/Makefile b/test/unit/python/jw/pkg/lib/ProjectConf/Makefile new file mode 100644 index 00000000..de693650 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/ProjectConf/Makefile @@ -0,0 +1,7 @@ +TOPDIR = ../../../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/py-run.mk + +all: +test: run diff --git a/test/unit/python/jw/pkg/lib/ProjectConf/test.py b/test/unit/python/jw/pkg/lib/ProjectConf/test.py new file mode 100644 index 00000000..36a3f558 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/ProjectConf/test.py @@ -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')