Skip to content

Commit 53674bd

Browse files
committed
test(map): add scale and drawables permutations and pixel validation
1 parent 63e06b9 commit 53674bd

3 files changed

Lines changed: 143 additions & 28 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dev = [
6767
"syrupy>=4.9.1,<6",
6868
"pdoc>=15.0.4,<17",
6969
"pytest-cov>=7.0.0",
70+
"pytest-benchmark>=5.1.0",
7071
]
7172

7273
[tool.hatch.build.targets.sdist]

‎tests/map/test_benchmarks.py‎

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
"""Benchmarks for Roborock V1 and Q10 map parsing and rendering.
22
3+
Includes permutations for:
4+
- Different scaling values (scale 1, 2, 4)
5+
- Drawable overlay variations (none, default, all)
6+
- Real V1 S5 and S6 map payloads
7+
- Q10 wire unpack and multi-room composite rendering
8+
- Pixel output validation (dimensions, format, bounding box)
9+
310
Can be run via pytest:
411
uv run pytest tests/map/test_benchmarks.py
512
uv run pytest tests/map/test_benchmarks.py --codspeed
@@ -19,30 +26,36 @@
1926
from typing import TYPE_CHECKING, Any
2027

2128
import pytest
29+
from PIL import Image
30+
from vacuum_map_parser_base.config.drawable import Drawable
2231

2332
from roborock.map.b01_q10_map_parser import (
2433
B01Q10MapParser,
34+
B01Q10MapParserConfig,
2535
Q10MapPacket,
2636
Q10MapPacketKind,
2737
Q10Room,
2838
parse_map_packet,
2939
)
30-
from roborock.map.map_parser import MapParser, MapParserConfig
40+
from roborock.map.map_parser import MapParser, MapParserConfig, ParsedMapData
3141

3242
if TYPE_CHECKING:
33-
from pytest_codspeed import BenchmarkFixture
43+
from pytest_benchmark.fixture import BenchmarkFixture
3444
else:
3545
try:
36-
from pytest_codspeed import BenchmarkFixture
46+
from pytest_benchmark.fixture import BenchmarkFixture
3747
except ImportError:
38-
BenchmarkFixture = Any
48+
try:
49+
from pytest_codspeed import BenchmarkFixture
50+
except ImportError:
51+
BenchmarkFixture = Any
3952

40-
@pytest.fixture
41-
def benchmark() -> Callable[..., Any]:
42-
def _runner(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
43-
return func(*args, **kwargs)
53+
@pytest.fixture
54+
def benchmark() -> Callable[..., Any]:
55+
def _runner(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
56+
return func(*args, **kwargs)
4457

45-
return _runner
58+
return _runner
4659

4760

4861
_TESTDATA_DIR = Path(__file__).resolve().parent / "testdata"
@@ -83,22 +96,78 @@ def _build_synthetic_q10_grid(width: int = 200, height: int = 200) -> Q10MapPack
8396
)
8497

8598

99+
def _validate_image(
100+
result: ParsedMapData | None,
101+
expected_size: tuple[int, int] | None = None,
102+
) -> None:
103+
"""Validate rendered PNG image integrity and pixel bounds outside the timed benchmark."""
104+
assert result is not None
105+
assert result.image_content is not None
106+
assert len(result.image_content) > 0
107+
image = Image.open(io.BytesIO(result.image_content))
108+
assert image.format == "PNG"
109+
assert image.mode == "RGBA"
110+
assert image.getbbox() is not None # Image has non-empty drawn content
111+
if expected_size is not None:
112+
assert image.size == expected_size
113+
114+
115+
# ---------------------------------------------------------------------------
116+
# V1 Map Benchmarks
117+
# ---------------------------------------------------------------------------
118+
119+
86120
def test_benchmark_v1_s5_map_parse(benchmark: BenchmarkFixture) -> None:
87-
"""Benchmark parsing a real Roborock V1 S5 map with 2 rooms."""
121+
"""Benchmark parsing a real Roborock V1 S5 map with 2 rooms (default scale 4)."""
88122
raw_data = _V1_S5_MAP.read_bytes()
89123
parser = MapParser(MapParserConfig())
90124
result = benchmark(parser.parse, raw_data)
91-
assert result is not None
92-
assert result.image_content is not None
125+
_validate_image(result, (1024, 964))
93126

94127

95-
def test_benchmark_v1_s6_map_parse(benchmark: BenchmarkFixture) -> None:
96-
"""Benchmark parsing a real Roborock V1 S6 map with 6 rooms and active segment."""
128+
def test_benchmark_v1_s6_map_scale_4_default(benchmark: BenchmarkFixture) -> None:
129+
"""Benchmark parsing a real Roborock V1 S6 map at default scale 4 (1644x1344 px)."""
97130
raw_data = _V1_S6_MAP.read_bytes()
98-
parser = MapParser(MapParserConfig())
131+
parser = MapParser(MapParserConfig(map_scale=4))
99132
result = benchmark(parser.parse, raw_data)
100-
assert result is not None
101-
assert result.image_content is not None
133+
_validate_image(result, (1644, 1344))
134+
135+
136+
def test_benchmark_v1_s6_map_scale_2(benchmark: BenchmarkFixture) -> None:
137+
"""Benchmark parsing a real Roborock V1 S6 map at scale 2 (822x672 px)."""
138+
raw_data = _V1_S6_MAP.read_bytes()
139+
parser = MapParser(MapParserConfig(map_scale=2))
140+
result = benchmark(parser.parse, raw_data)
141+
_validate_image(result, (822, 672))
142+
143+
144+
def test_benchmark_v1_s6_map_scale_1(benchmark: BenchmarkFixture) -> None:
145+
"""Benchmark parsing a real Roborock V1 S6 map at scale 1 (411x336 px)."""
146+
raw_data = _V1_S6_MAP.read_bytes()
147+
parser = MapParser(MapParserConfig(map_scale=1))
148+
result = benchmark(parser.parse, raw_data)
149+
_validate_image(result, (411, 336))
150+
151+
152+
def test_benchmark_v1_s6_map_no_drawables(benchmark: BenchmarkFixture) -> None:
153+
"""Benchmark parsing a real Roborock V1 S6 map with all drawables disabled."""
154+
raw_data = _V1_S6_MAP.read_bytes()
155+
parser = MapParser(MapParserConfig(map_scale=4, drawables=[]))
156+
result = benchmark(parser.parse, raw_data)
157+
_validate_image(result, (1644, 1344))
158+
159+
160+
def test_benchmark_v1_s6_map_all_drawables(benchmark: BenchmarkFixture) -> None:
161+
"""Benchmark parsing a real Roborock V1 S6 map with all available drawables enabled."""
162+
raw_data = _V1_S6_MAP.read_bytes()
163+
parser = MapParser(MapParserConfig(map_scale=4, drawables=list(Drawable)))
164+
result = benchmark(parser.parse, raw_data)
165+
_validate_image(result, (1644, 1344))
166+
167+
168+
# ---------------------------------------------------------------------------
169+
# Q10 Map Benchmarks
170+
# ---------------------------------------------------------------------------
102171

103172

104173
def test_benchmark_q10_map_packet_unpack(benchmark: BenchmarkFixture) -> None:
@@ -114,15 +183,23 @@ def test_benchmark_q10_map_parse_and_render(benchmark: BenchmarkFixture) -> None
114183
raw_data = _Q10_MAP.read_bytes()
115184
parser = B01Q10MapParser()
116185
result = benchmark(parser.parse, raw_data)
117-
assert result.image_content is not None
186+
_validate_image(result, (32, 24))
118187

119188

120-
def test_benchmark_q10_map_full_scale_render(benchmark: BenchmarkFixture) -> None:
121-
"""Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan."""
189+
def test_benchmark_q10_map_full_scale_render_scale_1(benchmark: BenchmarkFixture) -> None:
190+
"""Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan at scale 1."""
122191
packet = _build_synthetic_q10_grid(200, 200)
123-
parser = B01Q10MapParser()
192+
parser = B01Q10MapParser(B01Q10MapParserConfig(map_scale=1))
124193
result = benchmark(parser.parse_packet, packet)
125-
assert result.image_content is not None
194+
_validate_image(result, (200, 200))
195+
196+
197+
def test_benchmark_q10_map_full_scale_render_scale_4(benchmark: BenchmarkFixture) -> None:
198+
"""Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan at scale 4."""
199+
packet = _build_synthetic_q10_grid(200, 200)
200+
parser = B01Q10MapParser(B01Q10MapParserConfig(map_scale=4))
201+
result = benchmark(parser.parse_packet, packet)
202+
_validate_image(result, (800, 800))
126203

127204

128205
# ---------------------------------------------------------------------------
@@ -136,15 +213,28 @@ def _run_benchmarks(iterations: int, warmup: int, profile: bool) -> None:
136213
q10_data = _Q10_MAP.read_bytes()
137214
q10_packet_200 = _build_synthetic_q10_grid(200, 200)
138215

139-
v1_parser = MapParser(MapParserConfig())
140-
q10_parser = B01Q10MapParser()
216+
p_v1_s5 = MapParser(MapParserConfig())
217+
p_v1_s6_scale4 = MapParser(MapParserConfig(map_scale=4))
218+
p_v1_s6_scale2 = MapParser(MapParserConfig(map_scale=2))
219+
p_v1_s6_scale1 = MapParser(MapParserConfig(map_scale=1))
220+
p_v1_s6_no_draw = MapParser(MapParserConfig(map_scale=4, drawables=[]))
221+
p_v1_s6_all_draw = MapParser(MapParserConfig(map_scale=4, drawables=list(Drawable)))
222+
223+
p_q10_small = B01Q10MapParser()
224+
p_q10_scale1 = B01Q10MapParser(B01Q10MapParserConfig(map_scale=1))
225+
p_q10_scale4 = B01Q10MapParser(B01Q10MapParserConfig(map_scale=4))
141226

142227
benchmarks: list[tuple[str, Callable[[], Any]]] = [
143-
("V1 S5 Map (Parse & Render)", lambda: v1_parser.parse(v1_s5_data)),
144-
("V1 S6 Map (Parse & Render)", lambda: v1_parser.parse(v1_s6_data)),
228+
("V1 S5 Map (Scale 4, Default)", lambda: p_v1_s5.parse(v1_s5_data)),
229+
("V1 S6 Map (Scale 4, Default)", lambda: p_v1_s6_scale4.parse(v1_s6_data)),
230+
("V1 S6 Map (Scale 2)", lambda: p_v1_s6_scale2.parse(v1_s6_data)),
231+
("V1 S6 Map (Scale 1)", lambda: p_v1_s6_scale1.parse(v1_s6_data)),
232+
("V1 S6 Map (No Drawables)", lambda: p_v1_s6_no_draw.parse(v1_s6_data)),
233+
("V1 S6 Map (All Drawables)", lambda: p_v1_s6_all_draw.parse(v1_s6_data)),
145234
("Q10 Wire Packet (Unpack only)", lambda: parse_map_packet(q10_data)),
146-
("Q10 Map (Parse & Render)", lambda: q10_parser.parse(q10_data)),
147-
("Q10 200x200 Map (Composite Render)", lambda: q10_parser.parse_packet(q10_packet_200)),
235+
("Q10 Small Map (Scale 1)", lambda: p_q10_small.parse(q10_data)),
236+
("Q10 200x200 Map (Scale 1)", lambda: p_q10_scale1.parse_packet(q10_packet_200)),
237+
("Q10 200x200 Map (Scale 4)", lambda: p_q10_scale4.parse_packet(q10_packet_200)),
148238
]
149239

150240
print(f"\nRunning {len(benchmarks)} benchmarks ({warmup} warmup, {iterations} timed iterations)...\n")

‎uv.lock‎

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)