Skip to content

Commit fd2aae8

Browse files
authored
Merge pull request #34 from stonerlab/copilot/increase-test-coverage
Fix Python 3.11 test failures and mask-free CI failure reporting
2 parents 197268f + e3c0f7a commit fd2aae8

9 files changed

Lines changed: 424 additions & 4 deletions

File tree

.github/workflows/run-tests-action.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ jobs:
4242
sudo apt-get install qtbase5-dev
4343
- name: Test with xvfb
4444
run: |
45-
xvfb-run --auto-servernum pytest -n 2 --cov-report= --cov=Stoner --junitxml pytest.xml
45+
xvfb-run --auto-servernum pytest -n 2 --cov-report= --cov=Stoner --junitxml pytest.xml || PYTEST_EXIT=$?
4646
coverage xml
47+
exit ${PYTEST_EXIT:-0}
4748
env:
4849
TZ: Europe/London
4950
LC_CTYPE: en_GB.UTF-8

Stoner/core/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,8 @@ def __getitem__(self, name: Union[str, RegExp]) -> Any:
477477
key = name
478478
(name, typehint) = self._get_name_(name)
479479
name = self.__lookup__(name, True)
480-
value = [super().__getitem__(nm) for nm in name]
480+
_super = super()
481+
value = [_super.__getitem__(nm) for nm in name]
481482
if typehint is not None:
482483
value = [self.__mungevalue(typehint, v) for v in value]
483484
if len(value) == 0: # pylint: disable=len-as-condition

tests/Stoner/core/test_exceptions.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,53 @@
22
"""Test Stoner.core.exceptions module"""
33
import pytest
44

5-
from Stoner.core.exceptions import assertion
5+
from Stoner.core.exceptions import (
6+
StonerAssertionError,
7+
StonerLoadError,
8+
StonerSetasError,
9+
StonerUnrecognisedFormat,
10+
assertion,
11+
)
612

713

814
def test_assertion():
915
with pytest.raises(RuntimeError):
1016
assertion(False, "Triggered an assertion")
1117

1218

19+
def test_assertion_true_does_not_raise():
20+
# Calling assertion with a truthy condition should not raise
21+
assertion(True, "Should not raise")
22+
23+
24+
def test_StonerLoadError_is_exception():
25+
err = StonerLoadError("test load error")
26+
assert isinstance(err, Exception), "StonerLoadError should be an Exception"
27+
with pytest.raises(StonerLoadError):
28+
raise StonerLoadError("could not load file")
29+
30+
31+
def test_StonerUnrecognisedFormat_is_IOError():
32+
err = StonerUnrecognisedFormat("unknown format")
33+
assert isinstance(err, IOError), "StonerUnrecognisedFormat should be an IOError"
34+
with pytest.raises(StonerUnrecognisedFormat):
35+
raise StonerUnrecognisedFormat("no loader found")
36+
37+
38+
def test_StonerSetasError_is_AttributeError():
39+
err = StonerSetasError("setas not set")
40+
assert isinstance(err, AttributeError), "StonerSetasError should be an AttributeError"
41+
with pytest.raises(StonerSetasError):
42+
raise StonerSetasError("column not accessible")
43+
44+
45+
def test_StonerAssertionError_is_RuntimeError():
46+
err = StonerAssertionError("assertion failed")
47+
assert isinstance(err, RuntimeError), "StonerAssertionError should be a RuntimeError"
48+
with pytest.raises(StonerAssertionError):
49+
assertion(False)
50+
51+
1352
if __name__ == "__main__":
1453
pytest.main(["--pdb", __file__])
54+

tests/Stoner/core/test_utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# -*- coding: utf-8 -*-
2+
"""Tests for Stoner.core.utils"""
3+
4+
import csv
5+
6+
import pytest
7+
8+
from Stoner.core.utils import Tab_Delimited, decode_string
9+
10+
11+
def test_decode_string_simple():
12+
assert decode_string("xxyy") == "xxyy", "decode_string with no patterns should return unchanged"
13+
14+
15+
def test_decode_string_repeated_x():
16+
assert decode_string("3x") == "xxx", "decode_string failed to expand 3x"
17+
18+
19+
def test_decode_string_repeated_y():
20+
assert decode_string("2y") == "yy", "decode_string failed to expand 2y"
21+
22+
23+
def test_decode_string_mixed():
24+
result = decode_string("x2yz")
25+
assert result == "xyyz", "decode_string failed for mixed pattern x2yz"
26+
27+
28+
def test_decode_string_dots_and_dashes():
29+
assert decode_string("3.") == "...", "decode_string failed to expand 3."
30+
assert decode_string("2-") == "--", "decode_string failed to expand 2-"
31+
32+
33+
def test_decode_string_multiple_patterns():
34+
result = decode_string("2x3y")
35+
assert result == "xxyyy", "decode_string failed for multiple patterns 2x3y"
36+
37+
38+
def test_Tab_Delimited_is_csv_dialect():
39+
assert issubclass(Tab_Delimited, csv.Dialect), "Tab_Delimited should subclass csv.Dialect"
40+
assert Tab_Delimited.delimiter == "\t", "Tab_Delimited delimiter should be a tab"
41+
assert Tab_Delimited.quoting == csv.QUOTE_NONE, "Tab_Delimited quoting should be QUOTE_NONE"
42+
assert Tab_Delimited.doublequote is False, "Tab_Delimited doublequote should be False"
43+
assert Tab_Delimited.lineterminator == "\r\n", "Tab_Delimited lineterminator should be CRLF"
44+
45+
46+
def test_Tab_Delimited_roundtrip():
47+
import io
48+
49+
output = io.StringIO()
50+
writer = csv.writer(output, dialect=Tab_Delimited)
51+
writer.writerow(["a", "b", "c"])
52+
output.seek(0)
53+
reader = csv.reader(output, dialect=Tab_Delimited)
54+
row = next(reader)
55+
assert row == ["a", "b", "c"], "Tab_Delimited roundtrip failed"
56+
57+
58+
if __name__ == "__main__":
59+
pytest.main(["--pdb", __file__])

tests/Stoner/tools/test_classes.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from Stoner import Options
7+
from Stoner import Data, Options
88
from Stoner.tools import classes
99

1010

@@ -146,5 +146,72 @@ def test_Options():
146146
assert repr(Options) == opt_repr, "Representation of Options failed"
147147

148148

149+
def test_AttributeStore():
150+
store = classes.AttributeStore({"x": 1, "y": 2})
151+
assert store.x == 1, "AttributeStore getattr failed"
152+
assert store["y"] == 2, "AttributeStore getitem failed"
153+
store.z = 3
154+
assert store["z"] == 3, "AttributeStore setattr failed"
155+
try:
156+
_ = store.missing
157+
except AttributeError:
158+
pass
159+
else:
160+
assert False, "AttributeStore getattr for missing key didn't raise AttributeError"
161+
162+
163+
def test_AttributeStore_init_no_dict():
164+
store = classes.AttributeStore()
165+
store["key"] = "value"
166+
assert store.key == "value", "AttributeStore set via dict key and get via attr failed"
167+
168+
169+
def test_TypedList_missing_methods():
170+
tl = classes.TypedList(int, (1, 2, 3))
171+
# append
172+
tl.append(4)
173+
assert tl == [1, 2, 3, 4], "TypedList append failed"
174+
try:
175+
tl.append("bad")
176+
except TypeError:
177+
pass
178+
else:
179+
assert False, "TypedList append with bad type didn't raise TypeError"
180+
# __len__
181+
assert len(tl) == 4, "TypedList __len__ failed"
182+
# __iter__
183+
items = list(tl)
184+
assert items == [1, 2, 3, 4], "TypedList __iter__ failed"
185+
# count
186+
tl.append(1)
187+
assert tl.count(1) == 2, "TypedList count failed"
188+
# remove
189+
tl.remove(1)
190+
assert tl.count(1) == 1, "TypedList remove failed"
191+
# pop
192+
val = tl.pop()
193+
assert val == 1, "TypedList pop failed"
194+
# reverse
195+
tl2 = classes.TypedList(int, (1, 2, 3))
196+
tl2.reverse()
197+
assert tl2 == [3, 2, 1], "TypedList reverse failed"
198+
# clear
199+
tl2.clear()
200+
assert len(tl2) == 0, "TypedList clear failed"
201+
202+
203+
def test_copy_into():
204+
import numpy as np
205+
206+
src = Data(np.column_stack((np.arange(5), np.arange(5) * 2.0)), column_headers=["x", "y"])
207+
src.setas = "xy"
208+
dest = Data()
209+
result = classes.copy_into(src, dest)
210+
assert result is dest, "copy_into should return the destination object"
211+
assert list(dest.column_headers) == ["x", "y"], "copy_into failed to copy column headers"
212+
assert len(dest) == 5, "copy_into failed to copy data rows"
213+
214+
149215
if __name__ == "__main__":
150216
pytest.main(["--pdb", __file__])
217+

tests/Stoner/tools/test_file.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
"""Tests for Stoner.tools.file (test_is_zip function)."""
3+
4+
import os
5+
import tempfile
6+
import zipfile
7+
8+
import pytest
9+
10+
from Stoner.tools.file import test_is_zip as is_zip_file
11+
12+
13+
def test_is_zip_with_empty_string():
14+
assert is_zip_file("") is False, "test_is_zip should return False for empty string"
15+
16+
17+
def test_is_zip_with_none():
18+
assert is_zip_file(None) is False, "test_is_zip should return False for None"
19+
20+
21+
def test_is_zip_with_bytes_containing_null():
22+
assert is_zip_file(b"data\x00more") is False, "test_is_zip should return False for bytes with null"
23+
24+
25+
def test_is_zip_with_real_zip():
26+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
27+
tmp_name = tmp.name
28+
try:
29+
with zipfile.ZipFile(tmp_name, "w") as zf:
30+
zf.writestr("hello.txt", "Hello, world!")
31+
result = is_zip_file(tmp_name)
32+
assert result is not False, "test_is_zip should detect a real zip file"
33+
assert result[0] == tmp_name, "test_is_zip should return the zip filename"
34+
assert result[1] == "", "test_is_zip should return empty member for direct zip"
35+
finally:
36+
os.unlink(tmp_name)
37+
38+
39+
def test_is_zip_with_non_zip_file():
40+
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as tmp:
41+
tmp.write("Not a zip file")
42+
tmp_name = tmp.name
43+
try:
44+
result = is_zip_file(tmp_name)
45+
assert result is False, "test_is_zip should return False for non-zip file"
46+
finally:
47+
os.unlink(tmp_name)
48+
49+
50+
def test_is_zip_with_path_inside_zip():
51+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
52+
tmp_name = tmp.name
53+
try:
54+
with zipfile.ZipFile(tmp_name, "w") as zf:
55+
zf.writestr("subdir/data.txt", "content")
56+
# Test with a path that includes the zip file + member path
57+
result = is_zip_file(os.path.join(tmp_name, "subdir", "data.txt"))
58+
assert result is not False, "test_is_zip should find zip when path goes through a zip"
59+
assert result[0] == tmp_name, "test_is_zip should find the zip file path"
60+
finally:
61+
os.unlink(tmp_name)
62+
63+
64+
if __name__ == "__main__":
65+
pytest.main(["--pdb", __file__])

tests/Stoner/tools/test_formatting.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,62 @@ def test_ordinal():
6767
else:
6868
assert False, "ordinal didn't raise a ValueError for non integer value"
6969
assert formatting.ordinal(11).endswith("th"), "Failed special handling for 11th in ordinal"
70+
assert formatting.ordinal(12).endswith("th"), "Failed special handling for 12th in ordinal"
71+
assert formatting.ordinal(13).endswith("th"), "Failed special handling for 13th in ordinal"
7072
assert formatting.ordinal(21).endswith("st"), "Failed to add st to 21st in ordinal"
73+
assert formatting.ordinal(1).endswith("st"), "Failed to add st to 1st in ordinal"
74+
assert formatting.ordinal(2).endswith("nd"), "Failed to add nd to 2nd in ordinal"
75+
assert formatting.ordinal(3).endswith("rd"), "Failed to add rd to 3rd in ordinal"
76+
assert formatting.ordinal(4).endswith("th"), "Failed to add th to 4th in ordinal"
77+
assert formatting.ordinal(0).endswith("th"), "Failed to add th to 0th in ordinal"
78+
79+
80+
def test_quantize():
81+
assert formatting.quantize(1.23, 0.1) == pytest.approx(1.2), "quantize(1.23, 0.1) failed"
82+
assert formatting.quantize(1.26, 0.1) == pytest.approx(1.3), "quantize(1.26, 0.1) failed"
83+
assert formatting.quantize(7, 2) == pytest.approx(8), "quantize(7, 2) failed"
84+
assert formatting.quantize(6, 2) == pytest.approx(6), "quantize(6, 2) failed"
85+
86+
87+
def test_tex_escape():
88+
assert formatting.tex_escape("&") == r"\&", "tex_escape & failed"
89+
assert formatting.tex_escape("%") == r"\%", "tex_escape % failed"
90+
assert formatting.tex_escape("$") == r"\$", "tex_escape $ failed"
91+
assert formatting.tex_escape("#") == r"\#", "tex_escape # failed"
92+
assert formatting.tex_escape("_") == r"\_", "tex_escape _ failed"
93+
assert formatting.tex_escape("{") == r"\{", "tex_escape { failed"
94+
assert formatting.tex_escape("}") == r"\}", "tex_escape } failed"
95+
assert formatting.tex_escape("~") == r"\textasciitilde{}", "tex_escape ~ failed"
96+
assert formatting.tex_escape("^") == r"\^{}", "tex_escape ^ failed"
97+
assert formatting.tex_escape("\\") == r"\textbackslash{}", "tex_escape \\ failed"
98+
assert formatting.tex_escape("<") == r"\textless", "tex_escape < failed"
99+
assert formatting.tex_escape(">") == r"\textgreater", "tex_escape > failed"
100+
assert formatting.tex_escape("hello") == "hello", "tex_escape plain text should be unchanged"
101+
102+
103+
def test_format_val_modes():
104+
value = 1.2345e-6
105+
# eng mode text
106+
result = formatting.format_val(value, fmt="text", mode="eng")
107+
assert "u" in result or "1" in result, "format_val eng text mode failed"
108+
# sci mode html
109+
result = formatting.format_val(value, fmt="html", mode="sci")
110+
assert "10" in result, "format_val sci html mode failed"
111+
# sci mode latex
112+
result = formatting.format_val(value, fmt="latex", mode="sci")
113+
assert r"\times" in result, "format_val sci latex mode failed"
114+
# float mode (default)
115+
result = formatting.format_val(value, fmt="text", mode="float")
116+
assert "1.2345" in result, "format_val float text mode failed"
117+
# bad mode raises RuntimeError
118+
try:
119+
formatting.format_val(value, fmt="text", mode="bad")
120+
except RuntimeError:
121+
pass
122+
else:
123+
assert False, "format_val bad mode didn't raise RuntimeError"
71124

72125

73126
if __name__ == "__main__":
74127
pytest.main(["--pdb", __file__])
128+

0 commit comments

Comments
 (0)