feat: patch FIT creator as Edge 1030 Plus
This commit is contained in:
150
tests/fit/test_rewriter_patching.py
Normal file
150
tests/fit/test_rewriter_patching.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.fit.models import DeviceFieldValue, FitConversionResult, GarminDevice
|
||||
from app.fit.rewriter import (
|
||||
FitFormatError,
|
||||
_iter_data_fields,
|
||||
convert_fit_device,
|
||||
read_device_field_values,
|
||||
)
|
||||
from tests.fit.builders import data, definition, make_fit
|
||||
|
||||
FILE_ID_MESG_NUM = 0
|
||||
DEVICE_INFO_MESG_NUM = 23
|
||||
|
||||
|
||||
def _build_fixture() -> bytes:
|
||||
# file_id: manufacturer(1/u16), product(2/u16)
|
||||
file_def = definition(0, FILE_ID_MESG_NUM, [(1, 2, 0x84), (2, 2, 0x84)])
|
||||
file_data = data(0, struct.pack("<HH", 255, 999))
|
||||
|
||||
# device_info: device_index(0/u8), manufacturer(2/u16), product(4/u16)
|
||||
device_def = definition(1, DEVICE_INFO_MESG_NUM, [(0, 1, 0x02), (2, 2, 0x84), (4, 2, 0x84)])
|
||||
creator = data(1, struct.pack("<BHH", 0, 255, 999))
|
||||
sensor = data(1, struct.pack("<BHH", 1, 32, 1234))
|
||||
|
||||
records = file_def + file_data + device_def + creator + sensor
|
||||
return make_fit(records)
|
||||
|
||||
|
||||
def _build_no_device_index_fixture() -> bytes:
|
||||
"""device_info record entirely missing field 0 (device_index) must be left untouched."""
|
||||
file_def = definition(0, FILE_ID_MESG_NUM, [(1, 2, 0x84), (2, 2, 0x84)])
|
||||
file_data = data(0, struct.pack("<HH", 255, 999))
|
||||
|
||||
# device_info WITHOUT device_index field at all: manufacturer(2/u16), product(4/u16)
|
||||
device_def = definition(1, DEVICE_INFO_MESG_NUM, [(2, 2, 0x84), (4, 2, 0x84)])
|
||||
no_index_device = data(1, struct.pack("<HH", 32, 1234))
|
||||
|
||||
records = file_def + file_data + device_def + no_index_device
|
||||
return make_fit(records)
|
||||
|
||||
|
||||
def _read_device_info_records(path: Path) -> list[dict[int, int]]:
|
||||
"""Parse raw bytes and return one dict of {field_num: value} per device_info record,
|
||||
in file order, so creator and sensor records can be distinguished positionally."""
|
||||
raw = bytearray(path.read_bytes())
|
||||
records: list[dict[int, int]] = []
|
||||
for local_def, field_offsets in _iter_data_fields(raw):
|
||||
if local_def.global_message_num != DEVICE_INFO_MESG_NUM:
|
||||
continue
|
||||
values: dict[int, int] = {}
|
||||
for field, offset in field_offsets:
|
||||
raw_bytes = bytes(raw[offset : offset + field.size])
|
||||
if field.size == 1:
|
||||
values[field.num] = raw_bytes[0]
|
||||
elif field.size == 2:
|
||||
values[field.num] = struct.unpack(f"{local_def.endian}H", raw_bytes)[0]
|
||||
records.append(values)
|
||||
return records
|
||||
|
||||
|
||||
def test_convert_fit_device_patches_file_id(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source.fit"
|
||||
source.write_bytes(_build_fixture())
|
||||
output = tmp_path / "output.fit"
|
||||
|
||||
result = convert_fit_device(source, output)
|
||||
|
||||
assert isinstance(result, FitConversionResult)
|
||||
assert result.source_path == source
|
||||
assert result.output_path == output
|
||||
assert result.file_crc is not None
|
||||
assert result.patched_field_count > 0
|
||||
|
||||
values = read_device_field_values(output)
|
||||
values_by_key = {(v.global_message_num, v.field_num): v.value for v in values}
|
||||
|
||||
assert values_by_key[(FILE_ID_MESG_NUM, 1)] == 1
|
||||
assert values_by_key[(FILE_ID_MESG_NUM, 2)] == 3570
|
||||
|
||||
|
||||
def test_convert_fit_device_patches_creator_and_leaves_sensor_untouched(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source = tmp_path / "source.fit"
|
||||
source.write_bytes(_build_fixture())
|
||||
output = tmp_path / "output.fit"
|
||||
|
||||
convert_fit_device(source, output)
|
||||
|
||||
device_info_records = _read_device_info_records(output)
|
||||
assert len(device_info_records) == 2
|
||||
creator_values, sensor_values = device_info_records
|
||||
|
||||
# creator (device_index=0) patched to Edge 1030 Plus
|
||||
assert creator_values[0] == 0
|
||||
assert creator_values[2] == 1
|
||||
assert creator_values[4] == 3570
|
||||
|
||||
# sensor (device_index=1) must remain completely untouched
|
||||
assert sensor_values[0] == 1
|
||||
assert sensor_values[2] == 32
|
||||
assert sensor_values[4] == 1234
|
||||
|
||||
|
||||
def test_convert_fit_device_leaves_device_info_without_device_index_untouched(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source = tmp_path / "source.fit"
|
||||
source.write_bytes(_build_no_device_index_fixture())
|
||||
output = tmp_path / "output.fit"
|
||||
|
||||
convert_fit_device(source, output)
|
||||
|
||||
device_info_records = _read_device_info_records(output)
|
||||
assert len(device_info_records) == 1
|
||||
values = device_info_records[0]
|
||||
|
||||
# device_info without a device_index field must remain completely untouched
|
||||
assert values[2] == 32
|
||||
assert values[4] == 1234
|
||||
|
||||
|
||||
def test_convert_fit_device_defaults_to_edge_1030_plus() -> None:
|
||||
device = GarminDevice()
|
||||
assert device.manufacturer_id == 1
|
||||
assert device.product_id == 3570
|
||||
assert device.product_name == "Edge 1030 Plus"
|
||||
|
||||
|
||||
def test_convert_fit_device_rejects_invalid_container(tmp_path: Path) -> None:
|
||||
source = tmp_path / "bad.fit"
|
||||
source.write_bytes(b"not a fit file")
|
||||
output = tmp_path / "output.fit"
|
||||
|
||||
with pytest.raises(FitFormatError):
|
||||
convert_fit_device(source, output)
|
||||
|
||||
|
||||
def test_read_device_field_values_returns_device_field_value_instances(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source.fit"
|
||||
source.write_bytes(_build_fixture())
|
||||
|
||||
values = read_device_field_values(source)
|
||||
|
||||
assert all(isinstance(v, DeviceFieldValue) for v in values)
|
||||
assert any(v.global_message_num == FILE_ID_MESG_NUM for v in values)
|
||||
Reference in New Issue
Block a user