2026-04-09 08:40:53 +10:00

690 lines
30 KiB
Python

"""
Tests for all parse_* functions in fc_doc_generator.py.
"""
import fc_doc_generator as fcdg
# ---------------------------------------------------------------------------
# get_chip_family
# ---------------------------------------------------------------------------
class TestGetChipFamily:
def test_stm32h7(self, board_stm32h7_all_dshot):
assert fcdg.get_chip_family(board_stm32h7_all_dshot) == "stm32h7"
def test_stm32h7_io(self, board_stm32h7_mixed_io):
assert fcdg.get_chip_family(board_stm32h7_mixed_io) == "stm32h7"
def test_stm32f4(self, board_stm32f4):
assert fcdg.get_chip_family(board_stm32f4) == "stm32f4"
def test_imxrt(self, board_imxrt):
assert fcdg.get_chip_family(board_imxrt) == "imxrt"
def test_missing_defconfig(self, tmp_path):
assert fcdg.get_chip_family(tmp_path) == "unknown"
def test_ppm_shared(self, board_ppm_shared):
assert fcdg.get_chip_family(board_ppm_shared) == "stm32h7"
# ---------------------------------------------------------------------------
# parse_board_config
# ---------------------------------------------------------------------------
class TestParseBoardConfig:
def test_8ch_no_io(self, board_stm32h7_all_dshot):
result = fcdg.parse_board_config(board_stm32h7_all_dshot)
assert result["total_outputs"] == 8
assert result["has_io_board"] is False
assert result["io_outputs"] == 0
def test_9ch_with_io(self, board_stm32h7_mixed_io):
result = fcdg.parse_board_config(board_stm32h7_mixed_io)
assert result["total_outputs"] == 9
assert result["has_io_board"] is True
assert result["io_outputs"] == 8
def test_6ch_no_io(self, board_stm32f4):
result = fcdg.parse_board_config(board_stm32f4)
assert result["total_outputs"] == 6
assert result["has_io_board"] is False
def test_4ch_ppm(self, board_ppm_shared):
result = fcdg.parse_board_config(board_ppm_shared)
assert result["total_outputs"] == 4
def test_missing_file(self, tmp_path):
assert fcdg.parse_board_config(tmp_path) == {}
def test_imxrt_8ch(self, board_imxrt):
result = fcdg.parse_board_config(board_imxrt)
assert result["total_outputs"] == 8
assert result["has_io_board"] is False
# ---------------------------------------------------------------------------
# parse_timer_config
# ---------------------------------------------------------------------------
class TestParseTimerConfig:
def test_stm32h7_timer_count(self, board_stm32h7_all_dshot):
result = fcdg.parse_timer_config(board_stm32h7_all_dshot)
assert len(result["timers"]) == 4
def test_stm32h7_all_timers_dshot(self, board_stm32h7_all_dshot):
result = fcdg.parse_timer_config(board_stm32h7_all_dshot)
assert all(t["dshot"] for t in result["timers"])
def test_stm32h7_channel_count(self, board_stm32h7_all_dshot):
result = fcdg.parse_timer_config(board_stm32h7_all_dshot)
assert len(result["channels"]) == 8
def test_stm32h7_channel_output_indices(self, board_stm32h7_all_dshot):
result = fcdg.parse_timer_config(board_stm32h7_all_dshot)
indices = [ch["output_index"] for ch in result["channels"]]
# output_index is 1-based and sequential
assert indices == list(range(1, len(indices) + 1))
def test_stm32h7_mixed_dma(self, board_stm32h7_mixed_io):
result = fcdg.parse_timer_config(board_stm32h7_mixed_io)
timer_map = {t["timer"]: t["dshot"] for t in result["timers"]}
assert timer_map["Timer5"] is True
assert timer_map["Timer4"] is True
assert timer_map["Timer12"] is False
def test_stm32h7_mixed_channel_count(self, board_stm32h7_mixed_io):
result = fcdg.parse_timer_config(board_stm32h7_mixed_io)
assert len(result["channels"]) == 9
def test_imxrt_derives_timers_from_channels(self, board_imxrt):
result = fcdg.parse_timer_config(board_imxrt)
# iMXRT: io_timers[] is empty, timers are derived from PWM prefix in channels
timer_names = {t["timer"] for t in result["timers"]}
assert "PWM2" in timer_names
assert "PWM4" in timer_names
def test_imxrt_all_channels_dshot(self, board_imxrt):
result = fcdg.parse_timer_config(board_imxrt)
assert all(ch["is_dshot_channel"] for ch in result["channels"])
def test_imxrt_channel_count(self, board_imxrt):
result = fcdg.parse_timer_config(board_imxrt)
assert len(result["channels"]) == 8
def test_capture_channel_count(self, board_stm32h7_capture_channels):
# 8 regular + 8 initIOTimerChannelCapture = 16 total
result = fcdg.parse_timer_config(board_stm32h7_capture_channels)
assert len(result["channels"]) == 16
def test_capture_channels_include_capture_timers(self, board_stm32h7_capture_channels):
result = fcdg.parse_timer_config(board_stm32h7_capture_channels)
timer_names = {ch["timer"] for ch in result["channels"]}
assert "Timer1" in timer_names
assert "Timer8" in timer_names
assert "Timer12" in timer_names
def test_capture_channels_output_indices(self, board_stm32h7_capture_channels):
# All 16 outputs are numbered 1..16 in order
result = fcdg.parse_timer_config(board_stm32h7_capture_channels)
indices = [ch["output_index"] for ch in result["channels"]]
assert indices == list(range(1, 17))
def test_stm32f4_no_dshot(self, board_stm32f4):
result = fcdg.parse_timer_config(board_stm32f4)
assert all(not t["dshot"] for t in result["timers"])
assert len(result["timers"]) == 3
def test_stm32f4_channel_count(self, board_stm32f4):
result = fcdg.parse_timer_config(board_stm32f4)
assert len(result["channels"]) == 6
def test_missing_file(self, tmp_path):
assert fcdg.parse_timer_config(tmp_path) == {}
# ---------------------------------------------------------------------------
# parse_serial_config
# ---------------------------------------------------------------------------
class TestParseSerialConfig:
def test_stm32h7_port_count(self, board_stm32h7_all_dshot):
result = fcdg.parse_serial_config(board_stm32h7_all_dshot)
# USART1, USART2, USART3, UART4, USART6, UART7 = 6 active
assert len(result["serial_ports"]) == 6
def test_stm32h7_labels(self, board_stm32h7_all_dshot):
result = fcdg.parse_serial_config(board_stm32h7_all_dshot)
label_map = {p["device"]: p["label"] for p in result["serial_ports"]}
assert label_map["/dev/ttyS0"] == "TELEM1"
assert label_map["/dev/ttyS1"] == "TELEM2"
assert label_map["/dev/ttyS2"] == "Debug Console" # USART3 is console, no BOARD_SERIAL label
assert label_map["/dev/ttyS3"] == "GPS1"
assert label_map["/dev/ttyS4"] == "RC"
def test_stm32h7_uart_order(self, board_stm32h7_all_dshot):
result = fcdg.parse_serial_config(board_stm32h7_all_dshot)
uarts = [p["uart"] for p in result["serial_ports"]]
# Must follow stm32h7 UART_HW_ORDER
assert uarts[0] == "USART1"
assert uarts[2] == "USART3"
def test_io_board_px4io_rc_combined_label(self, board_stm32h7_mixed_io):
result = fcdg.parse_serial_config(board_stm32h7_mixed_io)
# PX4IO_SERIAL_DEVICE="/dev/ttyS5" and CONFIG_BOARD_SERIAL_RC="/dev/ttyS5"
# → should be labelled "PX4IO/RC"
label_map = {p["device"]: p["label"] for p in result["serial_ports"]}
assert label_map["/dev/ttyS5"] == "PX4IO/RC"
def test_tel_normalized_to_telem(self, board_stm32h7_all_dshot):
result = fcdg.parse_serial_config(board_stm32h7_all_dshot)
labels = [p["label"] for p in result["serial_ports"]]
assert "TELEM1" in labels
assert "TELEM2" in labels
# Raw "TEL1" / "TEL2" must not appear
assert "TEL1" not in labels
assert "TEL2" not in labels
def test_imxrt_lpuart_order(self, board_imxrt):
result = fcdg.parse_serial_config(board_imxrt)
uarts = [p["uart"] for p in result["serial_ports"]]
# LPUART2 comes before LPUART3 etc. (LPUART1 not enabled → skipped)
assert uarts[0] == "LPUART2"
assert "LPUART1" not in uarts
def test_unknown_family_returns_empty(self, tmp_path):
# Board with no defconfig → unknown family → no ports
result = fcdg.parse_serial_config(tmp_path)
assert result == {"serial_ports": []}
def test_flow_control_detected_from_board_h(self, board_stm32h7_all_dshot):
result = fcdg.parse_serial_config(board_stm32h7_all_dshot)
fc_map = {p["uart"]: p["flow_control"] for p in result["serial_ports"]}
# USART1 has both #define GPIO_USART1_RTS and _CTS → True
assert fc_map["USART1"] is True
# USART2 has only #define GPIO_USART2_RTS (no CTS) → False
assert fc_map["USART2"] is False
# UART4 has GPIO_UART4_RTS in a comment only, not a #define → False
assert fc_map["UART4"] is False
def test_flow_control_false_when_no_board_h(self, tmp_path):
# Board with defconfig but no board.h → all flow_control=False
defconfig_dir = tmp_path / "nuttx-config" / "nsh"
defconfig_dir.mkdir(parents=True)
(defconfig_dir / "defconfig").write_text(
"CONFIG_ARCH_CHIP_STM32H7=y\nCONFIG_STM32H7_USART1=y\n"
)
result = fcdg.parse_serial_config(tmp_path)
for p in result["serial_ports"]:
assert p["flow_control"] is False
# ---------------------------------------------------------------------------
# parse_rc_config
# ---------------------------------------------------------------------------
class TestParseRcConfig:
def test_has_rc_input(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_config(board_stm32h7_all_dshot)
assert result["has_rc_input"] is True
assert result["has_common_rc"] is False
def test_rc_serial_device(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_config(board_stm32h7_all_dshot)
assert result["rc_serial_device"] == "/dev/ttyS4"
def test_no_ppm_pin(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_config(board_stm32h7_all_dshot)
assert result["has_ppm_pin"] is False
def test_has_common_rc(self, board_stm32h7_mixed_io):
result = fcdg.parse_rc_config(board_stm32h7_mixed_io)
assert result["has_common_rc"] is True
assert result["has_rc_input"] is False
def test_ppm_pin_present_mixed_io(self, board_stm32h7_mixed_io):
result = fcdg.parse_rc_config(board_stm32h7_mixed_io)
assert result["has_ppm_pin"] is True
def test_ppm_shared_with_rc_serial(self, board_ppm_shared):
result = fcdg.parse_rc_config(board_ppm_shared)
assert result["has_ppm_pin"] is True
assert result["ppm_shared_with_rc_serial"] is True
def test_imxrt_common_rc(self, board_imxrt):
result = fcdg.parse_rc_config(board_imxrt)
assert result["has_common_rc"] is True
def test_no_rc_data(self, tmp_path):
result = fcdg.parse_rc_config(tmp_path)
assert result["has_rc_input"] is False
assert result["has_common_rc"] is False
assert result["has_ppm_pin"] is False
# ---------------------------------------------------------------------------
# parse_gps_config
# ---------------------------------------------------------------------------
class TestParseGpsConfig:
def test_imxrt_pps_capture(self, board_imxrt):
result = fcdg.parse_gps_config(board_imxrt)
assert result["has_pps_capture"] is True
def test_imxrt_safety_switch_and_led(self, board_imxrt):
result = fcdg.parse_gps_config(board_imxrt)
assert result["has_safety_switch"] is True
assert result["has_safety_led"] is True
def test_imxrt_has_buzzer(self, board_imxrt):
result = fcdg.parse_gps_config(board_imxrt)
assert result["has_buzzer"] is True
def test_no_safety_stm32h7(self, board_stm32h7_all_dshot):
result = fcdg.parse_gps_config(board_stm32h7_all_dshot)
assert result["has_safety_switch"] is False
assert result["has_safety_led"] is False
assert result["has_pps_capture"] is False
assert result["has_buzzer"] is False
def test_no_gps_data(self, tmp_path):
result = fcdg.parse_gps_config(tmp_path)
assert result["has_pps_capture"] is False
assert result["has_safety_switch"] is False
assert result["has_buzzer"] is False
# ---------------------------------------------------------------------------
# parse_power_config
# ---------------------------------------------------------------------------
class TestParsePowerConfig:
def test_single_brick_stm32h7(self, board_stm32h7_all_dshot):
result = fcdg.parse_power_config(board_stm32h7_all_dshot)
assert result["num_power_inputs"] == 1
assert result["has_redundant_power"] is False
assert result["has_dual_battery_monitoring"] is False
assert result["power_monitor_type"] == "analog"
def test_dual_brick_stm32h7_mixed_io(self, board_stm32h7_mixed_io):
result = fcdg.parse_power_config(board_stm32h7_mixed_io)
assert result["num_power_inputs"] == 2
assert result["has_redundant_power"] is True
assert result["has_dual_battery_monitoring"] is True
assert result["power_monitor_type"] == "ltc44xx"
def test_digital_brick_imxrt(self, board_imxrt):
result = fcdg.parse_power_config(board_imxrt)
assert result["num_power_inputs"] == 1
assert result["has_redundant_power"] is False
# px4board INA238 driver takes precedence
assert result["power_monitor_type"] == "ina238"
def test_analog_monitor_stm32f4(self, board_stm32f4):
result = fcdg.parse_power_config(board_stm32f4)
assert result["num_power_inputs"] == 1
assert result["power_monitor_type"] == "analog"
def test_missing_board_config_returns_defaults(self, tmp_path):
result = fcdg.parse_power_config(tmp_path)
assert result["num_power_inputs"] == 1
assert result["has_redundant_power"] is False
assert result["has_dual_battery_monitoring"] is False
assert result["power_monitor_type"] is None
def test_dronecan_pattern_a(self, board_stm32h7_mixed_io):
# Pattern A: GPIO_nPOWER_IN_CAN present in board_config.h
result = fcdg.parse_power_config(board_stm32h7_mixed_io)
assert result["has_dronecan_power_input"] is True
def test_dronecan_pattern_b(self, board_imxrt):
# Pattern B: BOARD_NUMBER_DIGITAL_BRICKS + CONFIG_DRIVERS_UAVCAN=y
result = fcdg.parse_power_config(board_imxrt)
assert result["has_dronecan_power_input"] is True
def test_no_dronecan_stm32f4(self, board_stm32f4):
result = fcdg.parse_power_config(board_stm32f4)
assert result["has_dronecan_power_input"] is False
def test_no_dronecan_stm32h7_single(self, board_stm32h7_all_dshot):
result = fcdg.parse_power_config(board_stm32h7_all_dshot)
assert result["has_dronecan_power_input"] is False
# ---------------------------------------------------------------------------
# parse_sd_card_config
# ---------------------------------------------------------------------------
class TestParseSDCardConfig:
def test_has_sd_stm32h7(self, board_stm32h7_all_dshot):
# defconfig has CONFIG_MMCSD_SDIO=y
result = fcdg.parse_sd_card_config(board_stm32h7_all_dshot)
assert result["has_sd_card"] is True
def test_has_sd_mixed_io(self, board_stm32h7_mixed_io):
result = fcdg.parse_sd_card_config(board_stm32h7_mixed_io)
assert result["has_sd_card"] is True
def test_has_sd_imxrt(self, board_imxrt):
result = fcdg.parse_sd_card_config(board_imxrt)
assert result["has_sd_card"] is True
def test_no_sd_stm32f4(self, board_stm32f4):
result = fcdg.parse_sd_card_config(board_stm32f4)
assert result["has_sd_card"] is False
def test_no_sd_ppm_shared(self, board_ppm_shared):
result = fcdg.parse_sd_card_config(board_ppm_shared)
assert result["has_sd_card"] is False
def test_secondary_sd_bench(self, tmp_path):
# No defconfig, but default.px4board has SD bench tool
(tmp_path / 'default.px4board').write_text(
'CONFIG_SYSTEMCMDS_SD_BENCH=y\n'
)
result = fcdg.parse_sd_card_config(tmp_path)
assert result["has_sd_card"] is True
def test_secondary_sd_stress(self, tmp_path):
(tmp_path / 'default.px4board').write_text(
'CONFIG_SYSTEMCMDS_SD_STRESS=y\n'
)
result = fcdg.parse_sd_card_config(tmp_path)
assert result["has_sd_card"] is True
def test_missing_returns_false(self, tmp_path):
result = fcdg.parse_sd_card_config(tmp_path)
assert result["has_sd_card"] is False
# ---------------------------------------------------------------------------
# parse_rc_board_sensors
# ---------------------------------------------------------------------------
class TestParseRcBoardSensors:
def test_basic_sensor_detection(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_board_sensors(board_stm32h7_all_dshot)
assert result['imu'] == ['ICM-42688P']
assert result['baro'] == ['MS5611']
assert result['mag'] == ['IST8310']
assert result['osd'] == []
def test_deduplicates_repeated_driver(self, board_stm32h7_all_dshot):
"""ist8310 started twice (internal + external) → appears exactly once."""
result = fcdg.parse_rc_board_sensors(board_stm32h7_all_dshot)
assert result['mag'].count('IST8310') == 1
def test_ignores_non_sensor_commands(self, board_stm32h7_all_dshot):
"""board_adc and other non-sensor commands are silently ignored."""
result = fcdg.parse_rc_board_sensors(board_stm32h7_all_dshot)
flat = str(result)
assert 'board_adc' not in flat
assert 'start' not in flat
def test_missing_file_returns_empty(self, tmp_path):
"""No rc.board_sensors → empty result, no error."""
result = fcdg.parse_rc_board_sensors(tmp_path)
assert result == {'imu': [], 'baro': [], 'mag': [], 'osd': []}
# ---------------------------------------------------------------------------
# parse_rc_board_sensor_bus
# ---------------------------------------------------------------------------
class TestParseRcBoardSensorBus:
def test_spi_flag_detected(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
imu = result['imu']
assert len(imu) == 1
assert imu[0]['name'] == 'ICM-42688P'
assert imu[0]['bus_type'] == 'SPI'
assert imu[0]['bus_num'] is None
assert imu[0]['external'] is False
def test_i2c_internal_flag_detected(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
# ist8310 -I start → internal I2C
internal = [e for e in result['mag'] if not e['external']]
assert len(internal) == 1
assert internal[0]['bus_type'] == 'I2C'
assert internal[0]['bus_num'] is None
assert internal[0]['external'] is False
def test_external_flag_and_bus_num_detected(self, board_stm32h7_all_dshot):
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
# ist8310 -X -b 1 start → external I2C bus 1
external = [e for e in result['mag'] if e['external']]
assert len(external) == 1
assert external[0]['bus_type'] == 'I2C'
assert external[0]['bus_num'] == 1
assert external[0]['external'] is True
def test_same_chip_different_bus_kept_separate(self, board_stm32h7_all_dshot):
"""IST8310 internal + external → 2 separate entries."""
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
assert len(result['mag']) == 2
def test_missing_file_returns_empty(self, tmp_path):
result = fcdg.parse_rc_board_sensor_bus(tmp_path)
assert result == {'imu': [], 'baro': [], 'mag': [], 'osd': [], 'power_monitor': []}
def test_skips_variant_block(self, board_stm32h7_variant):
"""Sensors inside if ver hwtypecmp blocks must not appear in sensor_bus_info."""
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_variant)
imu_names = [e['name'] for e in result['imu']]
# bmi088 and icm20602 are inside variant blocks → must be absent
assert 'BMI088' not in imu_names
assert 'ICM-20602' not in imu_names
# icm42688p is unconditional → must be present
assert 'ICM-42688P' in imu_names
def test_external_port_label_detected_from_comment(self, board_stm32h7_all_dshot):
"""External sensor preceded by 'GPS1/I2C1' comment gets port_label='GPS1'."""
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
external = [e for e in result['mag'] if e['external']]
assert len(external) == 1
assert external[0]['port_label'] == 'GPS1'
def test_internal_sensor_port_label_none(self, board_stm32h7_all_dshot):
"""Internal sensor preceded by 'Internal compass' comment gets port_label=None."""
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
internal = [e for e in result['mag'] if not e['external']]
assert len(internal) == 1
assert internal[0]['port_label'] is None
def test_spi_sensor_port_label_none(self, board_stm32h7_all_dshot):
"""SPI sensor with no GPS/I2C comment gets port_label=None."""
result = fcdg.parse_rc_board_sensor_bus(board_stm32h7_all_dshot)
assert result['imu'][0]['port_label'] is None
# ---------------------------------------------------------------------------
# parse_i2c_bus_config
# ---------------------------------------------------------------------------
class TestParseI2cBusConfig:
def test_missing_file_returns_empty_dict(self, tmp_path):
"""No src/i2c.cpp → empty dict."""
assert fcdg.parse_i2c_bus_config(tmp_path) == {}
def test_external_buses_parsed(self, tmp_path):
(tmp_path / 'src').mkdir(parents=True)
(tmp_path / 'src' / 'i2c.cpp').write_text(
'constexpr px4_i2c_bus_t buses[] = {\n'
' initI2CBusExternal(1),\n'
' initI2CBusExternal(2),\n'
'};\n'
)
result = fcdg.parse_i2c_bus_config(tmp_path)
assert result['external'] == [1, 2]
assert result['internal'] == []
def test_internal_buses_parsed(self, tmp_path):
(tmp_path / 'src').mkdir(parents=True)
(tmp_path / 'src' / 'i2c.cpp').write_text(
'constexpr px4_i2c_bus_t buses[] = {\n'
' initI2CBusInternal(3),\n'
'};\n'
)
result = fcdg.parse_i2c_bus_config(tmp_path)
assert result['external'] == []
assert result['internal'] == [3]
def test_mixed_buses_parsed_and_sorted(self, tmp_path):
(tmp_path / 'src').mkdir(parents=True)
(tmp_path / 'src' / 'i2c.cpp').write_text(
'constexpr px4_i2c_bus_t buses[] = {\n'
' initI2CBusExternal(1),\n'
' initI2CBusExternal(2),\n'
' initI2CBusInternal(3),\n'
' initI2CBusExternal(4),\n'
'};\n'
)
result = fcdg.parse_i2c_bus_config(tmp_path)
assert result['external'] == [1, 2, 4]
assert result['internal'] == [3]
def test_whitespace_in_args(self, tmp_path):
(tmp_path / 'src').mkdir(parents=True)
(tmp_path / 'src' / 'i2c.cpp').write_text(
'initI2CBusExternal( 1 );\ninitI2CBusInternal( 2 );\n'
)
result = fcdg.parse_i2c_bus_config(tmp_path)
assert 1 in result['external']
assert 2 in result['internal']
def test_empty_file_returns_empty_dict(self, tmp_path):
(tmp_path / 'src').mkdir(parents=True)
(tmp_path / 'src' / 'i2c.cpp').write_text('// no bus entries\n')
assert fcdg.parse_i2c_bus_config(tmp_path) == {}
# ---------------------------------------------------------------------------
# power_monitor category in parse_rc_board_sensor_bus
# ---------------------------------------------------------------------------
class TestPowerMonitorSensorBus:
def test_ina226_classified_as_power_monitor(self, tmp_path):
(tmp_path / 'init').mkdir(parents=True)
(tmp_path / 'init' / 'rc.board_sensors').write_text(
'ina226 -X -b 2 -t 1 -k start\n'
)
result = fcdg.parse_rc_board_sensor_bus(tmp_path)
assert len(result['power_monitor']) == 1
pm = result['power_monitor'][0]
assert pm['name'] == 'INA226'
assert pm['bus_type'] == 'I2C'
assert pm['bus_num'] == 2
assert pm['external'] is True
def test_ina228_classified_as_power_monitor(self, tmp_path):
(tmp_path / 'init').mkdir(parents=True)
(tmp_path / 'init' / 'rc.board_sensors').write_text(
'ina228 -X -b 1 start\n'
)
result = fcdg.parse_rc_board_sensor_bus(tmp_path)
assert result['power_monitor'][0]['name'] == 'INA228'
# ---------------------------------------------------------------------------
# _extract_port_label_from_comment
# ---------------------------------------------------------------------------
class TestExtractPortLabelFromComment:
def test_gps_port_extracted(self):
assert fcdg._extract_port_label_from_comment(
'External compass on GPS1/I2C1: standard puck') == 'GPS1'
def test_gps2_extracted(self):
assert fcdg._extract_port_label_from_comment('compass on GPS2') == 'GPS2'
def test_telem_extracted_when_no_gps(self):
assert fcdg._extract_port_label_from_comment('sensor on TELEM1') == 'TELEM1'
def test_i2c_numbered_extracted_when_no_gps_or_telem(self):
assert fcdg._extract_port_label_from_comment('I2C2 ist8310 magnetometer') == 'I2C2'
def test_i2c_without_digit_returns_none(self):
# "Internal magnetometer on I2C" — no digit suffix
assert fcdg._extract_port_label_from_comment('Internal magnetometer on I2C') is None
def test_none_input_returns_none(self):
assert fcdg._extract_port_label_from_comment(None) is None
def test_empty_comment_returns_none(self):
assert fcdg._extract_port_label_from_comment('') is None
def test_gps_takes_priority_over_i2c(self):
# Comment mentions both GPS1 and I2C1 — GPS should win
assert fcdg._extract_port_label_from_comment(
'External compass on GPS1/I2C1 (the 3rd external bus)') == 'GPS1'
# ---------------------------------------------------------------------------
# parse_sensor_variant_blocks
# ---------------------------------------------------------------------------
class TestParseSensorVariantBlocks:
def test_detects_variant_block(self, board_stm32h7_variant):
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_variant)
assert result['has_variants'] is True
def test_unconditional_sensors_separated(self, board_stm32h7_variant):
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_variant)
uncond = result['unconditional']
imu_names = [e['name'] for e in uncond['imu']]
baro_names = [e['name'] for e in uncond['baro']]
mag_names = [e['name'] for e in uncond['mag']]
assert 'ICM-42688P' in imu_names
assert 'MS5611' in baro_names
assert 'IST8310' in mag_names
# variant-only sensors must NOT be in unconditional
assert 'BMI088' not in imu_names
assert 'ICM-20602' not in imu_names
def test_variant_code_sensors_captured(self, board_stm32h7_variant):
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_variant)
hw1 = result['variants'].get('HW000001', {})
imu_names = [e['name'] for e in hw1.get('imu', [])]
assert 'BMI088' in imu_names
def test_else_block_stored_as_other(self, board_stm32h7_variant):
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_variant)
other = result['variants'].get('__other__', {})
imu_names = [e['name'] for e in other.get('imu', [])]
assert 'ICM-20602' in imu_names
def test_no_variants_returns_has_variants_false(self, board_stm32h7_all_dshot):
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_all_dshot)
assert result['has_variants'] is False
assert result['variants'] == {}
def test_no_variants_unconditional_has_all_sensors(self, board_stm32h7_all_dshot):
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_all_dshot)
uncond = result['unconditional']
assert len(uncond['imu']) == 1
assert uncond['imu'][0]['name'] == 'ICM-42688P'
def test_missing_file_returns_has_variants_false(self, tmp_path):
result = fcdg.parse_sensor_variant_blocks(tmp_path)
assert result['has_variants'] is False
assert result['unconditional'] == {'imu': [], 'baro': [], 'mag': [], 'osd': [], 'power_monitor': []}
assert result['variants'] == {}
def test_graceful_fail_sensors_moved_to_variant(self, board_stm32h7_graceful_fail):
"""Sensors that appear unconditionally AND in a variant block must be removed from
unconditional (PX4 graceful-fail pattern — driver start fails on hardware without chip)."""
result = fcdg.parse_sensor_variant_blocks(board_stm32h7_graceful_fail)
uncond_imu = [e['name'] for e in result['unconditional']['imu']]
# BMI088 is in unconditional block AND VD000000 → must be moved to VD000000 only
assert 'BMI088' not in uncond_imu
# ICM-20689 is unconditional only → stays unconditional
assert 'ICM-20689' in uncond_imu
# ICM-20602 is VD000001 only → in variants, not unconditional
assert 'ICM-20602' not in uncond_imu
# Variants must still hold the sensor
vd0_imu = [e['name'] for e in result['variants'].get('VD000000', {}).get('imu', [])]
assert 'BMI088' in vd0_imu
vd1_imu = [e['name'] for e in result['variants'].get('VD000001', {}).get('imu', [])]
assert 'ICM-20602' in vd1_imu