Skip to content

Commit 77c5c12

Browse files
committed
Add simple testing framework
1 parent d490d81 commit 77c5c12

6 files changed

Lines changed: 134 additions & 26 deletions

File tree

src/hsd/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"""
88
Toolbox for reading, writing and manipulating HSD-data.
99
"""
10+
from .common import HSD_ATTRIB_LINE, HSD_ATTRIB_EQUAL, HSD_ATTRIB_SUFFIX,\
11+
HSD_ATTRIB_NAME, HsdError
1012
from .dict import HsdDictBuilder, HsdDictWalker
1113
from .eventhandler import HsdEventHandler, HsdEventPrinter
1214
from .formatter import HsdFormatter

src/hsd/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def unquote(txt):
3535
HSD_ATTRIB_SUFFIX = ".hsdattrib"
3636

3737
# HSD attribute containing the original tag name
38-
HSD_ATTRIB_TAG = "tag"
38+
HSD_ATTRIB_NAME = "name"
3939

4040
# HSD attribute containing the line number
4141
HSD_ATTRIB_LINE = "line"

src/hsd/formatter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99

1010
from typing import List, TextIO, Union
11-
from hsd.common import HSD_ATTRIB_EQUAL, HSD_ATTRIB_TAG
11+
from hsd.common import HSD_ATTRIB_EQUAL, HSD_ATTRIB_NAME
1212
from hsd.eventhandler import HsdEventHandler
1313

1414

@@ -62,7 +62,7 @@ def open_tag(self, tagname: str, attrib: str, hsdattrib: dict):
6262
indentstr = self._indent_level * _INDENT_STR
6363

6464
if self._use_hsd_attribs and hsdattrib is not None:
65-
tagname = hsdattrib.get(HSD_ATTRIB_TAG, tagname)
65+
tagname = hsdattrib.get(HSD_ATTRIB_NAME, tagname)
6666

6767
self._fobj.write(f"{indentstr}{tagname}{attribstr}")
6868

src/hsd/io.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88
"""
99
import io
1010
from typing import Union, TextIO
11-
1211
from .dict import HsdDictWalker, HsdDictBuilder
1312
from .formatter import HsdFormatter
14-
1513
from .parser import HsdParser
1614

1715

@@ -94,14 +92,14 @@ def load_string(
9492
... }
9593
... \"\"\"
9694
>>> hsd.load_string(hsdstr)
97-
{'Dftb': {'Scc': True, 'Filling': {'Fermi': {'Temperature.attrib': 'Kelvin', 'Temperature': 100}}}}
95+
{'Dftb': {'Scc': True, 'Filling': {'Fermi': {'Temperature': 100, 'Temperature.attrib': 'Kelvin'}}}}
9896
9997
In order to ease the case-insensitive handling of the input, the tag
10098
names can be converted to lower case during reading using the
10199
``lower_tag_names`` option.
102100
103101
>>> hsd.load_string(hsdstr, lower_tag_names=True)
104-
{'dftb': {'scc': True, 'filling': {'fermi': {'temperature.attrib': 'Kelvin', 'temperature': 100}}}}
102+
{'dftb': {'scc': True, 'filling': {'fermi': {'temperature': 100, 'temperature.attrib': 'Kelvin'}}}}
105103
106104
The original tag names (together with additional information like the
107105
line number of a tag) can be recorded, if the ``include_hsd_attribs``
@@ -113,7 +111,7 @@ def load_string(
113111
with the recorded data:
114112
115113
>>> data["dftb.hsdattrib"]
116-
{'line': 1, 'tag': 'Dftb'}
114+
{'line': 1, 'name': 'Dftb'}
117115
118116
This additional data can be then also used to format the tags in the
119117
original style, when writing the data in HSD-format again. Compare:

src/hsd/parser.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class HsdParser:
4949
... \"\"\")
5050
>>> parser.parse(hsdfile)
5151
>>> dictbuilder.hsddict
52-
{'Hamiltonian': {'Dftb': {'Scc': True, 'Filling': {'Fermi': {'Temperature.attrib': 'Kelvin', 'Temperature': 100}}}}}
52+
{'Hamiltonian': {'Dftb': {'Scc': True, 'Filling': {'Fermi': {'Temperature': 100, 'Temperature.attrib': 'Kelvin'}}}}}
5353
"""
5454

5555
def __init__(self, eventhandler: Optional[HsdEventHandler] = None,
@@ -75,9 +75,10 @@ def __init__(self, eventhandler: Optional[HsdEventHandler] = None,
7575
self._after_equal_sign = False # last tag was opened with equal sign
7676
self._inside_attrib = False # parser inside attrib specification
7777
self._inside_quote = False # parser inside quotation
78-
self._has_child = False
78+
self._has_child = True # Whether current node has a child already
79+
self._has_text = False # whether current node contains text already
7980
self._oldbefore = "" # buffer for tagname
80-
self._lower_tag_names = lower_tag_names
81+
self._lower_tag_names = lower_tag_names # whether tag names should be lowered
8182

8283

8384
def parse(self, fobj: Union[TextIO, str]):
@@ -148,14 +149,13 @@ def _parse(self, line):
148149
# tagname was followed by an attribute -> append
149150
self._oldbefore += before
150151
else:
151-
self._has_child = True
152152
self._hsdattrib[common.HSD_ATTRIB_EQUAL] = True
153153
self._starttag(before, False)
154154
self._after_equal_sign = True
155155

156156
# Opening tag by curly brace
157157
elif sign == "{":
158-
self._has_child = True
158+
#self._has_child = True
159159
self._starttag(before, self._after_equal_sign)
160160
self._buffer = []
161161
self._after_equal_sign = False
@@ -188,7 +188,7 @@ def _parse(self, line):
188188
self._oldbefore = before
189189
self._buffer = []
190190
self._inside_attrib = True
191-
self._opened_tags.append(("[", self._currline, None))
191+
self._opened_tags.append(("[", self._currline, None, None, None))
192192
self._checkstr = _ATTRIB_SPECIALS
193193

194194
# Closing attribute specification
@@ -212,7 +212,7 @@ def _parse(self, line):
212212
self._checkstr = sign
213213
self._inside_quote = True
214214
self._buffer.append(before + sign)
215-
self._opened_tags.append(('"', self._currline, None))
215+
self._opened_tags.append(('"', self._currline, None, None, None))
216216

217217
# Interrupt
218218
elif sign == "<" and not self._after_equal_sign:
@@ -237,13 +237,18 @@ def _parse(self, line):
237237
def _text(self, text):
238238
stripped = text.strip()
239239
if stripped:
240+
if self._has_child:
241+
self._error(SYNTAX_ERROR, (self._currline, self._currline))
240242
self._eventhandler.add_text(stripped)
243+
self._has_text = True
241244

242245

243246
def _starttag(self, tagname, closeprev):
244247
txt = "".join(self._buffer)
245248
if txt:
246249
self._text(txt)
250+
if self._has_text:
251+
self._error(SYNTAX_ERROR, (self._currline, self._currline))
247252
tagname_stripped = tagname.strip()
248253
if self._oldbefore:
249254
if tagname_stripped:
@@ -254,15 +259,15 @@ def _starttag(self, tagname, closeprev):
254259
self._error(SYNTAX_ERROR, (self._currline, self._currline))
255260
self._hsdattrib[common.HSD_ATTRIB_LINE] = self._currline
256261
if self._lower_tag_names:
257-
self._hsdattrib[common.HSD_ATTRIB_TAG] = tagname_stripped
262+
self._hsdattrib[common.HSD_ATTRIB_NAME] = tagname_stripped
258263
tagname_stripped = tagname_stripped.lower()
259264
self._eventhandler.open_tag(tagname_stripped, self._attrib,
260265
self._hsdattrib)
261266
self._opened_tags.append(
262-
(tagname_stripped, self._currline, closeprev, self._has_child))
267+
(tagname_stripped, self._currline, closeprev, True, False))
268+
self._has_child = False
263269
self._buffer = []
264270
self._oldbefore = ""
265-
self._has_child = False
266271
self._attrib = None
267272
self._hsdattrib = {}
268273

@@ -271,7 +276,7 @@ def _closetag(self):
271276
if not self._opened_tags:
272277
self._error(SYNTAX_ERROR, (0, self._currline))
273278
self._buffer = []
274-
tag, _, closeprev, self._has_child = self._opened_tags.pop()
279+
tag, _, closeprev, self._has_child, self._has_text = self._opened_tags.pop()
275280
self._eventhandler.close_tag(tag)
276281
if closeprev:
277282
self._closetag()

test/test_parser.py

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,118 @@
55
# Licensed under the BSD 2-clause license. #
66
#------------------------------------------------------------------------------#
77
#
8+
import io
9+
import pytest
810
import hsd
9-
import os.path as op
1011

12+
_OPEN_TAG_EVENT = 1
13+
_CLOSE_TAG_EVENT = 2
14+
_ADD_TEXT_EVENT = 3
1115

12-
def test_parser():
13-
parser = hsd.HsdParser()
14-
with open(op.join(op.dirname(__file__), "test.hsd"), "r") as fobj:
15-
parser.parse(fobj)
16+
_HSD_LINE = hsd.HSD_ATTRIB_LINE
17+
_HSD_EQUAL = hsd.HSD_ATTRIB_EQUAL
18+
_HSD_NAME = hsd.HSD_ATTRIB_NAME
1619

20+
_VALID_TESTS = [
21+
(
22+
"Simple", (
23+
"""Test {} """,
24+
[
25+
(_OPEN_TAG_EVENT, "Test", None, {_HSD_LINE: 0}),
26+
(_CLOSE_TAG_EVENT, "Test"),
27+
]
28+
)
29+
),
30+
(
31+
"Data with quoted strings", (
32+
"""O = SelectedShells { "s" "p" }""",
33+
[
34+
(_OPEN_TAG_EVENT, "O", None, {_HSD_LINE: 0, _HSD_EQUAL: True}),
35+
(_OPEN_TAG_EVENT, 'SelectedShells', None, {_HSD_LINE: 0}),
36+
(_ADD_TEXT_EVENT, '"s" "p"'),
37+
(_CLOSE_TAG_EVENT, 'SelectedShells'),
38+
(_CLOSE_TAG_EVENT, 'O'),
39+
]
40+
)
41+
),
42+
(
43+
"Attribute containing comma", (
44+
"""PolarRadiusCharge [AA^3,AA,] = {\n1.030000 3.800000 2.820000\n}""",
45+
[
46+
(_OPEN_TAG_EVENT, "PolarRadiusCharge", "AA^3,AA,", {_HSD_LINE: 0, }),
47+
(_ADD_TEXT_EVENT, '1.030000 3.800000 2.820000'),
48+
(_CLOSE_TAG_EVENT, 'PolarRadiusCharge'),
49+
]
50+
)
51+
),
52+
]
1753

18-
if __name__ == '__main__':
19-
test_parser()
54+
_VALID_TEST_NAMES, _VALID_TEST_CASES = zip(*_VALID_TESTS)
55+
56+
57+
_FAILING_TESTS = [
58+
(
59+
"Node-less data", (
60+
"""a = 2\n15\n"""
61+
)
62+
),
63+
(
64+
"Node-less data at start", (
65+
"""15\na = 2\na = 4\n"""
66+
)
67+
),
68+
(
69+
"Node-less data in child", (
70+
"""a {\n12\nb = 5\n}\n"""
71+
)
72+
),
73+
(
74+
"Quoted tag name", (
75+
"""\"mytag\" = 12\n"""
76+
)
77+
),
78+
79+
]
80+
81+
_FAILING_TEST_NAMES, _FAILING_TEST_CASES = zip(*_FAILING_TESTS)
82+
83+
84+
class _TestEventHandler(hsd.HsdEventHandler):
85+
86+
def __init__(self):
87+
self.events = []
88+
89+
def open_tag(self, tagname, attrib, hsdoptions):
90+
self.events.append((_OPEN_TAG_EVENT, tagname, attrib, hsdoptions))
91+
92+
def close_tag(self, tagname):
93+
self.events.append((_CLOSE_TAG_EVENT, tagname))
94+
95+
def add_text(self, text):
96+
self.events.append((_ADD_TEXT_EVENT, text))
97+
98+
99+
@pytest.mark.parametrize(
100+
"hsd_input,expected_events",
101+
_VALID_TEST_CASES,
102+
ids=_VALID_TEST_NAMES
103+
)
104+
def test_valid_parser_events(hsd_input, expected_events):
105+
testhandler = _TestEventHandler()
106+
parser = hsd.HsdParser(eventhandler=testhandler)
107+
hsdfile = io.StringIO(hsd_input)
108+
parser.parse(hsdfile)
109+
assert testhandler.events == expected_events
110+
111+
112+
@pytest.mark.parametrize(
113+
"hsd_input",
114+
_FAILING_TEST_CASES,
115+
ids=_FAILING_TEST_NAMES
116+
)
117+
def test_invalid_parser_events(hsd_input):
118+
testhandler = _TestEventHandler()
119+
parser = hsd.HsdParser(eventhandler=testhandler)
120+
hsdfile = io.StringIO(hsd_input)
121+
with pytest.raises(hsd.HsdError):
122+
parser.parse(hsdfile)

0 commit comments

Comments
 (0)