Skip to content

Commit 3c3051f

Browse files
authored
Merge pull request #144 from timlnx/hypothesis-fuzzing
Add Hypothesis property tests (Scorecard Fuzzing check)
2 parents 5def329 + ed8858d commit 3c3051f

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
bandit
2+
hypothesis
23
pycodestyle
34
pylint
45
pytest
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# -*- coding: utf-8 -*-
2+
# SPDX-License-Identifier: MIT
3+
# The MIT License (MIT)
4+
#
5+
# SPDX-FileCopyrightText: 2014-2026 Tim Case <bitmath@lnx.cx>
6+
#
7+
# Permission is hereby granted, free of charge, to any person
8+
# obtaining a copy of this software and associated documentation files
9+
# (the "Software"), to deal in the Software without restriction,
10+
# including without limitation the rights to use, copy, modify, merge,
11+
# publish, distribute, sublicense, and/or sell copies of the Software,
12+
# and to permit persons to whom the Software is furnished to do so,
13+
# subject to the following conditions:
14+
#
15+
# The above copyright notice and this permission notice shall be
16+
# included in all copies or substantial portions of the Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
22+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
23+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
# SOFTWARE.
26+
27+
"""
28+
Hypothesis-driven property tests for the bitmath parser and unit
29+
conversions. These are fuzz-style tests: hypothesis generates inputs
30+
across the value/unit space and shrinks any counterexample it finds.
31+
32+
Three invariants are checked:
33+
34+
1. Roundtrip: an instance formatted as "value unit_singular" and then
35+
re-parsed produces a Bitmath with the same underlying bit count.
36+
2. Lossless conversion: x.to_DST().to_SRC() preserves x.bits across
37+
every (SRC, DST) pair in ALL_UNIT_TYPES.
38+
3. Parser exception contract: parse_string raises only ValueError on
39+
bad input. A leaked IndexError, AttributeError, KeyError, or
40+
TypeError would be a bug.
41+
"""
42+
43+
import math
44+
45+
import pytest
46+
47+
# Downstream packagers (Fedora/EPEL RPM builds) run %check without
48+
# installing test-only dependencies. Skip the whole module when
49+
# hypothesis isn't on the path rather than failing collection.
50+
pytest.importorskip("hypothesis")
51+
52+
from hypothesis import given, settings, strategies as st # noqa: E402
53+
54+
import bitmath # noqa: E402
55+
56+
57+
# Bracketed to avoid values that Python's str() renders in scientific
58+
# notation. The parser splits a value/unit string on the first
59+
# alphabetic character, which mis-handles the 'e' in '1e+16' or
60+
# '1e-05'. Scientific-notation file sizes aren't a realistic user
61+
# input, so the test space stays in the regular-notation range
62+
# (approximately [1e-4, 1e16)), plus exactly zero.
63+
nonneg_finite = st.one_of(
64+
st.just(0.0),
65+
st.floats(
66+
min_value=1e-4,
67+
max_value=1e15,
68+
allow_nan=False,
69+
allow_infinity=False,
70+
),
71+
)
72+
73+
unit_names = list(bitmath.ALL_UNIT_TYPES)
74+
75+
76+
@given(value=nonneg_finite, unit=st.sampled_from(unit_names))
77+
@settings(max_examples=200)
78+
def test_parse_string_roundtrip(value, unit):
79+
cls = getattr(bitmath, unit)
80+
original = cls(value)
81+
formatted = original.format("{value} {unit_singular}")
82+
parsed = bitmath.parse_string(formatted)
83+
assert math.isclose(parsed.bits, original.bits, rel_tol=1e-9, abs_tol=1e-9)
84+
85+
86+
@given(
87+
value=nonneg_finite,
88+
src=st.sampled_from(unit_names),
89+
dst=st.sampled_from(unit_names),
90+
)
91+
@settings(max_examples=300)
92+
def test_unit_conversion_lossless(value, src, dst):
93+
src_cls = getattr(bitmath, src)
94+
original = src_cls(value)
95+
via = getattr(original, f"to_{dst}")()
96+
back = getattr(via, f"to_{src}")()
97+
assert math.isclose(back.bits, original.bits, rel_tol=1e-9, abs_tol=1e-9)
98+
99+
100+
@given(garbage=st.text())
101+
@settings(max_examples=500)
102+
def test_parse_string_strict_only_raises_value_error(garbage):
103+
try:
104+
result = bitmath.parse_string(garbage)
105+
except ValueError:
106+
return
107+
assert isinstance(result, bitmath.Bitmath)
108+
109+
110+
@given(garbage=st.text())
111+
@settings(max_examples=500)
112+
def test_parse_string_unsafe_only_raises_value_error(garbage):
113+
try:
114+
result = bitmath.parse_string(garbage, strict=False)
115+
except ValueError:
116+
return
117+
assert isinstance(result, bitmath.Bitmath)

0 commit comments

Comments
 (0)