Skip to content

Commit 62d8d36

Browse files
Евгений БлиновЕвгений Блинов
authored andcommitted
Basic unit tests
1 parent 1d1a3b0 commit 62d8d36

4 files changed

Lines changed: 698 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import math
5+
from unittest.mock import MagicMock
6+
7+
import pytest
8+
9+
from microbenchmark import BenchmarkResult, Scenario
10+
11+
12+
def make_result(
13+
durations: tuple[float, ...],
14+
scenario: Scenario | None = None,
15+
is_primary: bool = True,
16+
) -> BenchmarkResult:
17+
if scenario is None:
18+
scenario = Scenario(lambda: None, name='test', number=len(durations) or 1)
19+
return BenchmarkResult(scenario=scenario, durations=durations, is_primary=is_primary)
20+
21+
22+
class TestBenchmarkResultFields:
23+
def test_all_fields_stored(self) -> None:
24+
scenario = Scenario(lambda: None, name='s', number=3)
25+
result = BenchmarkResult(
26+
scenario=scenario,
27+
durations=(0.1, 0.2, 0.3),
28+
is_primary=True,
29+
)
30+
assert result.scenario is scenario
31+
assert result.durations == (0.1, 0.2, 0.3)
32+
assert result.is_primary is True
33+
34+
def test_durations_is_tuple(self) -> None:
35+
result = make_result((0.1, 0.2, 0.3))
36+
assert isinstance(result.durations, tuple)
37+
38+
def test_mean_computed_correctly(self) -> None:
39+
result = make_result((1.0, 2.0, 3.0))
40+
expected = math.fsum([1.0, 2.0, 3.0]) / 3
41+
assert result.mean == expected
42+
43+
def test_mean_uses_fsum_precision(self) -> None:
44+
# floating point: sum of many small numbers may differ from fsum
45+
durations = tuple(0.1 for _ in range(10))
46+
result = make_result(durations)
47+
expected = math.fsum(durations) / len(durations)
48+
assert result.mean == expected
49+
50+
def test_best_is_min(self) -> None:
51+
result = make_result((3.0, 1.0, 2.0))
52+
assert result.best == 1.0
53+
54+
def test_worst_is_max(self) -> None:
55+
result = make_result((3.0, 1.0, 2.0))
56+
assert result.worst == 3.0
57+
58+
def test_is_primary_true_by_default(self) -> None:
59+
result = make_result((1.0,))
60+
assert result.is_primary is True
61+
62+
def test_is_primary_false(self) -> None:
63+
result = make_result((1.0,), is_primary=False)
64+
assert result.is_primary is False
65+
66+
def test_single_duration(self) -> None:
67+
result = make_result((0.5,))
68+
assert result.best == 0.5
69+
assert result.worst == 0.5
70+
assert result.mean == 0.5
71+
72+
def test_all_equal_durations(self) -> None:
73+
result = make_result((0.1, 0.1, 0.1))
74+
assert result.best == 0.1
75+
assert result.worst == 0.1
76+
assert result.mean == pytest.approx(0.1)
77+
78+
def test_scenario_identity(self) -> None:
79+
scenario = Scenario(lambda: None, name='check')
80+
result = BenchmarkResult(scenario=scenario, durations=(0.1,), is_primary=True)
81+
assert result.scenario is scenario
82+
83+
def test_scenario_none(self) -> None:
84+
result = BenchmarkResult(scenario=None, durations=(0.1,), is_primary=True)
85+
assert result.scenario is None
86+
87+
88+
class TestPercentile:
89+
def test_percentile_returns_benchmark_result(self) -> None:
90+
result = make_result(tuple(float(i) for i in range(1, 11)))
91+
trimmed = result.percentile(90)
92+
assert isinstance(trimmed, BenchmarkResult)
93+
94+
def test_percentile_is_primary_false(self) -> None:
95+
result = make_result(tuple(float(i) for i in range(1, 11)))
96+
trimmed = result.percentile(90)
97+
assert trimmed.is_primary is False
98+
99+
def test_percentile_count_nearest_rank(self) -> None:
100+
import math
101+
result = make_result(tuple(float(i) for i in range(1, 101)))
102+
trimmed = result.percentile(95)
103+
expected_count = math.ceil(100 * 95 / 100)
104+
assert len(trimmed.durations) == expected_count
105+
106+
def test_percentile_contains_fastest(self) -> None:
107+
result = make_result((5.0, 1.0, 3.0, 2.0, 4.0))
108+
trimmed = result.percentile(60)
109+
import math
110+
k = math.ceil(5 * 60 / 100)
111+
assert len(trimmed.durations) == k
112+
# should be the smallest k values
113+
sorted_original = sorted(result.durations)
114+
assert set(trimmed.durations) == set(sorted_original[:k])
115+
116+
def test_percentile_100_returns_all(self) -> None:
117+
result = make_result((1.0, 2.0, 3.0))
118+
trimmed = result.percentile(100)
119+
assert len(trimmed.durations) == 3
120+
121+
def test_percentile_small_number(self) -> None:
122+
result = make_result((1.0, 2.0, 3.0))
123+
trimmed = result.percentile(50)
124+
import math
125+
expected = math.ceil(3 * 50 / 100)
126+
assert len(trimmed.durations) == expected
127+
128+
def test_percentile_99(self) -> None:
129+
result = make_result(tuple(float(i) for i in range(1, 101)))
130+
trimmed = result.percentile(99)
131+
import math
132+
assert len(trimmed.durations) == math.ceil(100 * 99 / 100)
133+
134+
def test_percentile_mean_recomputed(self) -> None:
135+
result = make_result((1.0, 2.0, 3.0, 4.0, 10.0))
136+
trimmed = result.percentile(80)
137+
expected_durations = sorted(result.durations)[:4]
138+
expected_mean = math.fsum(expected_durations) / 4
139+
assert trimmed.mean == pytest.approx(expected_mean)
140+
141+
def test_percentile_on_derived_result(self) -> None:
142+
result = make_result(tuple(float(i) for i in range(1, 101)))
143+
derived = result.percentile(90).percentile(50)
144+
assert isinstance(derived, BenchmarkResult)
145+
assert derived.is_primary is False
146+
147+
def test_percentile_scenario_preserved(self) -> None:
148+
scenario = Scenario(lambda: None, name='s')
149+
result = BenchmarkResult(scenario=scenario, durations=(1.0, 2.0, 3.0), is_primary=True)
150+
trimmed = result.percentile(100)
151+
assert trimmed.scenario is scenario
152+
153+
def test_percentile_0_raises(self) -> None:
154+
result = make_result((1.0, 2.0, 3.0))
155+
with pytest.raises(ValueError, match='percentile'):
156+
result.percentile(0)
157+
158+
def test_percentile_negative_raises(self) -> None:
159+
result = make_result((1.0, 2.0, 3.0))
160+
with pytest.raises(ValueError, match='percentile'):
161+
result.percentile(-5)
162+
163+
def test_percentile_above_100_raises(self) -> None:
164+
result = make_result((1.0, 2.0, 3.0))
165+
with pytest.raises(ValueError, match='percentile'):
166+
result.percentile(101)
167+
168+
def test_percentile_preserves_fsum_mean(self) -> None:
169+
durations = tuple(0.1 * i for i in range(1, 11))
170+
result = make_result(durations)
171+
trimmed = result.percentile(80)
172+
sorted_d = sorted(durations)
173+
import math
174+
k = math.ceil(10 * 80 / 100)
175+
expected = math.fsum(sorted_d[:k]) / k
176+
assert trimmed.mean == pytest.approx(expected)
177+
178+
179+
class TestCachedProperties:
180+
def test_p95_returns_benchmark_result(self) -> None:
181+
result = make_result(tuple(float(i) for i in range(1, 101)))
182+
assert isinstance(result.p95, BenchmarkResult)
183+
184+
def test_p99_returns_benchmark_result(self) -> None:
185+
result = make_result(tuple(float(i) for i in range(1, 101)))
186+
assert isinstance(result.p99, BenchmarkResult)
187+
188+
def test_p95_is_cached(self) -> None:
189+
result = make_result(tuple(float(i) for i in range(1, 101)))
190+
assert result.p95 is result.p95
191+
192+
def test_p99_is_cached(self) -> None:
193+
result = make_result(tuple(float(i) for i in range(1, 101)))
194+
assert result.p99 is result.p99
195+
196+
def test_p95_count(self) -> None:
197+
import math
198+
result = make_result(tuple(float(i) for i in range(1, 101)))
199+
assert len(result.p95.durations) == math.ceil(100 * 95 / 100)
200+
201+
def test_p99_count(self) -> None:
202+
import math
203+
result = make_result(tuple(float(i) for i in range(1, 101)))
204+
assert len(result.p99.durations) == math.ceil(100 * 99 / 100)
205+
206+
def test_p95_is_primary_false(self) -> None:
207+
result = make_result(tuple(float(i) for i in range(1, 101)))
208+
assert result.p95.is_primary is False
209+
210+
def test_p99_is_primary_false(self) -> None:
211+
result = make_result(tuple(float(i) for i in range(1, 101)))
212+
assert result.p99.is_primary is False
213+
214+
215+
class TestSerialization:
216+
def test_to_json_returns_string(self) -> None:
217+
result = make_result((0.1, 0.2))
218+
assert isinstance(result.to_json(), str)
219+
220+
def test_to_json_valid_json(self) -> None:
221+
result = make_result((0.1, 0.2))
222+
data = json.loads(result.to_json())
223+
assert isinstance(data, dict)
224+
225+
def test_to_json_contains_durations(self) -> None:
226+
result = make_result((0.1, 0.2, 0.3))
227+
data = json.loads(result.to_json())
228+
assert 'durations' in data
229+
230+
def test_to_json_contains_is_primary(self) -> None:
231+
result = make_result((0.1,))
232+
data = json.loads(result.to_json())
233+
assert 'is_primary' in data
234+
235+
def test_to_json_contains_scenario_metadata(self) -> None:
236+
s = Scenario(lambda: None, name='myname', doc='mydoc', number=42)
237+
result = BenchmarkResult(scenario=s, durations=(0.1,), is_primary=True)
238+
data = json.loads(result.to_json())
239+
assert data['scenario']['name'] == 'myname'
240+
assert data['scenario']['doc'] == 'mydoc'
241+
assert data['scenario']['number'] == 42
242+
243+
def test_to_json_derived_is_primary_false(self) -> None:
244+
result = make_result((1.0, 2.0, 3.0), is_primary=False)
245+
data = json.loads(result.to_json())
246+
assert data['is_primary'] is False
247+
248+
def test_from_json_round_trip_durations(self) -> None:
249+
result = make_result((0.1, 0.2, 0.3))
250+
restored = BenchmarkResult.from_json(result.to_json())
251+
assert restored.durations == result.durations
252+
253+
def test_from_json_round_trip_mean(self) -> None:
254+
result = make_result((0.1, 0.2, 0.3))
255+
restored = BenchmarkResult.from_json(result.to_json())
256+
assert restored.mean == result.mean
257+
258+
def test_from_json_round_trip_best_worst(self) -> None:
259+
result = make_result((0.1, 0.2, 0.3))
260+
restored = BenchmarkResult.from_json(result.to_json())
261+
assert restored.best == result.best
262+
assert restored.worst == result.worst
263+
264+
def test_from_json_scenario_is_none(self) -> None:
265+
result = make_result((0.1, 0.2))
266+
restored = BenchmarkResult.from_json(result.to_json())
267+
assert restored.scenario is None
268+
269+
def test_from_json_preserves_is_primary(self) -> None:
270+
result = make_result((0.1,), is_primary=False)
271+
restored = BenchmarkResult.from_json(result.to_json())
272+
assert restored.is_primary is False
273+
274+
def test_from_json_primary_preserved(self) -> None:
275+
result = make_result((0.1,), is_primary=True)
276+
restored = BenchmarkResult.from_json(result.to_json())
277+
assert restored.is_primary is True
278+
279+
def test_from_json_fp_precision(self) -> None:
280+
durations = tuple(0.1 * i for i in range(1, 6))
281+
result = make_result(durations)
282+
restored = BenchmarkResult.from_json(result.to_json())
283+
assert restored.mean == pytest.approx(result.mean)
284+
285+
def test_from_json_invalid_json_raises(self) -> None:
286+
with pytest.raises(json.JSONDecodeError):
287+
BenchmarkResult.from_json('{not valid json}')
288+
289+
def test_from_json_missing_fields_raises(self) -> None:
290+
with pytest.raises(ValueError, match='required fields'):
291+
BenchmarkResult.from_json('{}')
292+
293+
def test_from_json_missing_durations_raises(self) -> None:
294+
data = json.dumps({'is_primary': True, 'scenario': {'name': 'x', 'doc': '', 'number': 1}})
295+
with pytest.raises(ValueError, match='required fields'):
296+
BenchmarkResult.from_json(data)

tests/units/test_init.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
import microbenchmark
4+
from microbenchmark import BenchmarkResult, Scenario, ScenarioGroup
5+
6+
7+
class TestPublicImports:
8+
def test_scenario_importable(self) -> None:
9+
assert Scenario is not None
10+
11+
def test_scenario_group_importable(self) -> None:
12+
assert ScenarioGroup is not None
13+
14+
def test_benchmark_result_importable(self) -> None:
15+
assert BenchmarkResult is not None
16+
17+
def test_all_defined(self) -> None:
18+
assert hasattr(microbenchmark, '__all__')
19+
20+
def test_all_contains_scenario(self) -> None:
21+
assert 'Scenario' in microbenchmark.__all__
22+
23+
def test_all_contains_scenario_group(self) -> None:
24+
assert 'ScenarioGroup' in microbenchmark.__all__
25+
26+
def test_all_contains_benchmark_result(self) -> None:
27+
assert 'BenchmarkResult' in microbenchmark.__all__
28+
29+
def test_all_contains_exactly_three_items(self) -> None:
30+
assert set(microbenchmark.__all__) == {'Scenario', 'ScenarioGroup', 'BenchmarkResult'}

0 commit comments

Comments
 (0)