mirror of
https://github.com/esphome/esphome.git
synced 2025-10-29 22:24:26 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -173,3 +173,66 @@ sensor:
|
||||
timeout: 1000ms
|
||||
value: [42.0]
|
||||
- multiply: 2.0
|
||||
|
||||
# CalibrateLinearFilter - piecewise linear calibration
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Linear Two Points"
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 0.0 -> 0.0
|
||||
- 100.0 -> 100.0
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Linear Multiple Segments"
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 0.0 -> 0.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Linear Least Squares"
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
method: least_squares
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
# CalibratePolynomialFilter - polynomial calibration
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Polynomial Degree 2"
|
||||
filters:
|
||||
- calibrate_polynomial:
|
||||
degree: 2
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Polynomial Degree 3"
|
||||
filters:
|
||||
- calibrate_polynomial:
|
||||
degree: 3
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 25.0 -> 26.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
# OrFilter - filter branching
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Or Filter with Multiple Branches"
|
||||
filters:
|
||||
- or:
|
||||
- multiply: 2.0
|
||||
- offset: 10.0
|
||||
- lambda: return x * 3.0;
|
||||
|
||||
112
tests/integration/fixtures/host_mode_climate_basic_state.yaml
Normal file
112
tests/integration/fixtures/host_mode_climate_basic_state.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
esphome:
|
||||
name: host-climate-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
id: dual_mode_thermostat
|
||||
name: Dual-mode Thermostat
|
||||
sensor: host_thermostat_temperature_sensor
|
||||
humidity_sensor: host_thermostat_humidity_sensor
|
||||
humidity_hysteresis: 1.0
|
||||
min_cooling_off_time: 20s
|
||||
min_cooling_run_time: 20s
|
||||
max_cooling_run_time: 30s
|
||||
supplemental_cooling_delta: 3.0
|
||||
min_heating_off_time: 20s
|
||||
min_heating_run_time: 20s
|
||||
max_heating_run_time: 30s
|
||||
supplemental_heating_delta: 3.0
|
||||
min_fanning_off_time: 20s
|
||||
min_fanning_run_time: 20s
|
||||
min_idle_time: 10s
|
||||
visual:
|
||||
min_humidity: 20%
|
||||
max_humidity: 70%
|
||||
min_temperature: 15.0
|
||||
max_temperature: 32.0
|
||||
temperature_step: 0.1
|
||||
default_preset: home
|
||||
preset:
|
||||
- name: "away"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
- name: "home"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
auto_mode:
|
||||
- logger.log: "AUTO mode set"
|
||||
heat_cool_mode:
|
||||
- logger.log: "HEAT_COOL mode set"
|
||||
cool_action:
|
||||
- switch.turn_on: air_cond
|
||||
supplemental_cooling_action:
|
||||
- switch.turn_on: air_cond_2
|
||||
heat_action:
|
||||
- switch.turn_on: heater
|
||||
supplemental_heating_action:
|
||||
- switch.turn_on: heater_2
|
||||
dry_action:
|
||||
- switch.turn_on: air_cond
|
||||
fan_only_action:
|
||||
- switch.turn_on: fan_only
|
||||
idle_action:
|
||||
- switch.turn_off: air_cond
|
||||
- switch.turn_off: air_cond_2
|
||||
- switch.turn_off: heater
|
||||
- switch.turn_off: heater_2
|
||||
- switch.turn_off: fan_only
|
||||
humidity_control_humidify_action:
|
||||
- switch.turn_on: humidifier
|
||||
humidity_control_off_action:
|
||||
- switch.turn_off: humidifier
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: host_thermostat_humidity_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s
|
||||
- platform: template
|
||||
id: host_thermostat_temperature_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 22.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
id: air_cond
|
||||
name: Air Conditioner
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: air_cond_2
|
||||
name: Air Conditioner 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: fan_only
|
||||
name: Fan
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater
|
||||
name: Heater
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater_2
|
||||
name: Heater 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: dehumidifier
|
||||
name: Dehumidifier
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: humidifier
|
||||
name: Humidifier
|
||||
optimistic: true
|
||||
108
tests/integration/fixtures/host_mode_climate_control.yaml
Normal file
108
tests/integration/fixtures/host_mode_climate_control.yaml
Normal file
@@ -0,0 +1,108 @@
|
||||
esphome:
|
||||
name: host-climate-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
id: dual_mode_thermostat
|
||||
name: Dual-mode Thermostat
|
||||
sensor: host_thermostat_temperature_sensor
|
||||
humidity_sensor: host_thermostat_humidity_sensor
|
||||
humidity_hysteresis: 1.0
|
||||
min_cooling_off_time: 20s
|
||||
min_cooling_run_time: 20s
|
||||
max_cooling_run_time: 30s
|
||||
supplemental_cooling_delta: 3.0
|
||||
min_heating_off_time: 20s
|
||||
min_heating_run_time: 20s
|
||||
max_heating_run_time: 30s
|
||||
supplemental_heating_delta: 3.0
|
||||
min_fanning_off_time: 20s
|
||||
min_fanning_run_time: 20s
|
||||
min_idle_time: 10s
|
||||
visual:
|
||||
min_humidity: 20%
|
||||
max_humidity: 70%
|
||||
min_temperature: 15.0
|
||||
max_temperature: 32.0
|
||||
temperature_step: 0.1
|
||||
default_preset: home
|
||||
preset:
|
||||
- name: "away"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
- name: "home"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
auto_mode:
|
||||
- logger.log: "AUTO mode set"
|
||||
heat_cool_mode:
|
||||
- logger.log: "HEAT_COOL mode set"
|
||||
cool_action:
|
||||
- switch.turn_on: air_cond
|
||||
supplemental_cooling_action:
|
||||
- switch.turn_on: air_cond_2
|
||||
heat_action:
|
||||
- switch.turn_on: heater
|
||||
supplemental_heating_action:
|
||||
- switch.turn_on: heater_2
|
||||
dry_action:
|
||||
- switch.turn_on: air_cond
|
||||
fan_only_action:
|
||||
- switch.turn_on: fan_only
|
||||
idle_action:
|
||||
- switch.turn_off: air_cond
|
||||
- switch.turn_off: air_cond_2
|
||||
- switch.turn_off: heater
|
||||
- switch.turn_off: heater_2
|
||||
- switch.turn_off: fan_only
|
||||
humidity_control_humidify_action:
|
||||
- switch.turn_on: humidifier
|
||||
humidity_control_off_action:
|
||||
- switch.turn_off: humidifier
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: host_thermostat_humidity_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s
|
||||
- platform: template
|
||||
id: host_thermostat_temperature_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 22.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
id: air_cond
|
||||
name: Air Conditioner
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: air_cond_2
|
||||
name: Air Conditioner 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: fan_only
|
||||
name: Fan
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater
|
||||
name: Heater
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater_2
|
||||
name: Heater 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: humidifier
|
||||
name: Humidifier
|
||||
optimistic: true
|
||||
@@ -210,7 +210,15 @@ sensor:
|
||||
name: "Test Sensor 50"
|
||||
lambda: return 50.0;
|
||||
update_interval: 0.1s
|
||||
# Temperature sensor for the thermostat
|
||||
# Sensors for the thermostat
|
||||
- platform: template
|
||||
name: "Humidity Sensor"
|
||||
id: humidity_sensor
|
||||
lambda: return 35.0;
|
||||
unit_of_measurement: "%"
|
||||
device_class: humidity
|
||||
state_class: measurement
|
||||
update_interval: 5s
|
||||
- platform: template
|
||||
name: "Temperature Sensor"
|
||||
id: temp_sensor
|
||||
@@ -295,6 +303,11 @@ valve:
|
||||
- logger.log: "Valve stopping"
|
||||
|
||||
output:
|
||||
- platform: template
|
||||
id: humidifier_output
|
||||
type: binary
|
||||
write_action:
|
||||
- logger.log: "Humidifier output changed"
|
||||
- platform: template
|
||||
id: heater_output
|
||||
type: binary
|
||||
@@ -305,18 +318,31 @@ output:
|
||||
type: binary
|
||||
write_action:
|
||||
- logger.log: "Cooler output changed"
|
||||
- platform: template
|
||||
id: fan_output
|
||||
type: binary
|
||||
write_action:
|
||||
- logger.log: "Fan output changed"
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
name: "Test Thermostat"
|
||||
sensor: temp_sensor
|
||||
humidity_sensor: humidity_sensor
|
||||
default_preset: Home
|
||||
on_boot_restore_from: default_preset
|
||||
min_heating_off_time: 1s
|
||||
min_heating_run_time: 1s
|
||||
min_cooling_off_time: 1s
|
||||
min_cooling_run_time: 1s
|
||||
min_fan_mode_switching_time: 1s
|
||||
min_idle_time: 1s
|
||||
visual:
|
||||
min_humidity: 20%
|
||||
max_humidity: 70%
|
||||
min_temperature: 15.0
|
||||
max_temperature: 32.0
|
||||
temperature_step: 0.1
|
||||
heat_action:
|
||||
- output.turn_on: heater_output
|
||||
cool_action:
|
||||
@@ -324,6 +350,14 @@ climate:
|
||||
idle_action:
|
||||
- output.turn_off: heater_output
|
||||
- output.turn_off: cooler_output
|
||||
humidity_control_humidify_action:
|
||||
- output.turn_on: humidifier_output
|
||||
humidity_control_off_action:
|
||||
- output.turn_off: humidifier_output
|
||||
fan_mode_auto_action:
|
||||
- output.turn_off: fan_output
|
||||
fan_mode_on_action:
|
||||
- output.turn_on: fan_output
|
||||
preset:
|
||||
- name: Home
|
||||
default_target_temperature_low: 20
|
||||
|
||||
49
tests/integration/test_host_mode_climate_basic_state.py
Normal file
49
tests/integration/test_host_mode_climate_basic_state.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Integration test for Host mode with climate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_mode_climate_basic_state(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test basic climate state reporting."""
|
||||
loop = asyncio.get_running_loop()
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
states: dict[int, EntityState] = {}
|
||||
climate_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and not climate_future.done()
|
||||
):
|
||||
climate_future.set_result(state)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
try:
|
||||
climate_state = await asyncio.wait_for(climate_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Climate state not received within 5 seconds")
|
||||
|
||||
assert isinstance(climate_state, aioesphomeapi.ClimateState)
|
||||
assert climate_state.mode == ClimateMode.OFF
|
||||
assert climate_state.action == ClimateAction.OFF
|
||||
assert climate_state.current_temperature == 22.0
|
||||
assert climate_state.target_temperature_low == 18.0
|
||||
assert climate_state.target_temperature_high == 24.0
|
||||
assert climate_state.preset == ClimatePreset.HOME
|
||||
assert climate_state.current_humidity == 42.0
|
||||
assert climate_state.target_humidity == 20.0
|
||||
76
tests/integration/test_host_mode_climate_control.py
Normal file
76
tests/integration/test_host_mode_climate_control.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Integration test for Host mode with climate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateInfo, ClimateMode, EntityState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_mode_climate_control(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test climate mode control."""
|
||||
loop = asyncio.get_running_loop()
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
states: dict[int, EntityState] = {}
|
||||
climate_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and state.mode == ClimateMode.HEAT
|
||||
and state.target_temperature_low == 21.5
|
||||
and state.target_temperature_high == 26.5
|
||||
and not climate_future.done()
|
||||
):
|
||||
climate_future.set_result(state)
|
||||
|
||||
# Get entities and set up state synchronization
|
||||
entities, services = await client.list_entities_services()
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
|
||||
|
||||
# Subscribe with the wrapper that filters initial states
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states to be broadcast
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
test_climate = next(
|
||||
(c for c in climate_infos if c.name == "Dual-mode Thermostat"), None
|
||||
)
|
||||
assert test_climate is not None, (
|
||||
"Dual-mode Thermostat thermostat climate not found"
|
||||
)
|
||||
|
||||
# Adjust setpoints
|
||||
client.climate_command(
|
||||
test_climate.key,
|
||||
mode=ClimateMode.HEAT,
|
||||
target_temperature_low=21.5,
|
||||
target_temperature_high=26.5,
|
||||
)
|
||||
|
||||
try:
|
||||
climate_state = await asyncio.wait_for(climate_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Climate state not received within 5 seconds")
|
||||
|
||||
assert isinstance(climate_state, aioesphomeapi.ClimateState)
|
||||
assert climate_state.mode == ClimateMode.HEAT
|
||||
assert climate_state.target_temperature_low == 21.5
|
||||
assert climate_state.target_temperature_high == 26.5
|
||||
@@ -5,7 +5,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import (
|
||||
ClimateFanMode,
|
||||
ClimateFeature,
|
||||
ClimateInfo,
|
||||
ClimateMode,
|
||||
DateInfo,
|
||||
DateState,
|
||||
DateTimeInfo,
|
||||
@@ -121,6 +124,46 @@ async def test_host_mode_many_entities(
|
||||
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
|
||||
|
||||
climate_info = climate_infos[0]
|
||||
|
||||
# Verify feature flags set as expected
|
||||
assert climate_info.feature_flags == (
|
||||
ClimateFeature.SUPPORTS_ACTION
|
||||
| ClimateFeature.SUPPORTS_CURRENT_HUMIDITY
|
||||
| ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
|
||||
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
|
||||
| ClimateFeature.SUPPORTS_TARGET_HUMIDITY
|
||||
)
|
||||
|
||||
# Verify modes
|
||||
assert climate_info.supported_modes == [
|
||||
ClimateMode.OFF,
|
||||
ClimateMode.COOL,
|
||||
ClimateMode.HEAT,
|
||||
], f"Expected modes [OFF, COOL, HEAT], got {climate_info.supported_modes}"
|
||||
|
||||
# Verify visual parameters
|
||||
assert climate_info.visual_min_temperature == 15.0, (
|
||||
f"Expected min_temperature=15.0, got {climate_info.visual_min_temperature}"
|
||||
)
|
||||
assert climate_info.visual_max_temperature == 32.0, (
|
||||
f"Expected max_temperature=32.0, got {climate_info.visual_max_temperature}"
|
||||
)
|
||||
assert climate_info.visual_target_temperature_step == 0.1, (
|
||||
f"Expected temperature_step=0.1, got {climate_info.visual_target_temperature_step}"
|
||||
)
|
||||
assert climate_info.visual_min_humidity == 20.0, (
|
||||
f"Expected min_humidity=20.0, got {climate_info.visual_min_humidity}"
|
||||
)
|
||||
assert climate_info.visual_max_humidity == 70.0, (
|
||||
f"Expected max_humidity=70.0, got {climate_info.visual_max_humidity}"
|
||||
)
|
||||
|
||||
# Verify fan modes
|
||||
assert climate_info.supported_fan_modes == [
|
||||
ClimateFanMode.ON,
|
||||
ClimateFanMode.AUTO,
|
||||
], f"Expected fan modes [ON, AUTO], got {climate_info.supported_fan_modes}"
|
||||
|
||||
# Verify the thermostat has presets
|
||||
assert len(climate_info.supported_presets) > 0, (
|
||||
"Expected climate to have presets"
|
||||
|
||||
@@ -96,17 +96,34 @@ def test_main_all_tests_should_run(
|
||||
mock_should_run_clang_format.return_value = True
|
||||
mock_should_run_python_linters.return_value = True
|
||||
|
||||
# Mock list-components.py output (now returns JSON with --changed-with-deps)
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": ["wifi", "api"], "all_changed": ["wifi", "api", "sensor"]}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||
# Memory impact only runs when component C++ files change
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/config.py",
|
||||
"esphome/helpers.py",
|
||||
]
|
||||
|
||||
# Run main function with mocked argv
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_changed_components",
|
||||
return_value=["wifi", "api", "sensor"],
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: ["wifi", "api"]
|
||||
if not deps
|
||||
else ["wifi", "api", "sensor"],
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
@@ -130,9 +147,9 @@ def test_main_all_tests_should_run(
|
||||
# changed_cpp_file_count should be present
|
||||
assert "changed_cpp_file_count" in output
|
||||
assert isinstance(output["changed_cpp_file_count"], int)
|
||||
# memory_impact should be present
|
||||
# memory_impact should be false (no component C++ files changed)
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false" # No files changed
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
|
||||
|
||||
def test_main_no_tests_should_run(
|
||||
@@ -154,13 +171,18 @@ def test_main_no_tests_should_run(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock empty list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
# Mock changed_files to return no component files
|
||||
mock_changed_files.return_value = []
|
||||
|
||||
# Run main function with mocked argv
|
||||
with patch("sys.argv", ["determine-jobs.py"]):
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=[]),
|
||||
patch.object(determine_jobs, "filter_component_files", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=[]
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
# Check output
|
||||
@@ -226,16 +248,22 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = True
|
||||
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": ["mqtt"], "all_changed": ["mqtt"]}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||
# Memory impact only runs when component C++ files change
|
||||
mock_changed_files.return_value = ["esphome/config.py"]
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["script.py", "-b", "main"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=["mqtt"]),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=["mqtt"]
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
@@ -245,13 +273,6 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_clang_format.assert_called_once_with("main")
|
||||
mock_should_run_python_linters.assert_called_once_with("main")
|
||||
|
||||
# Check that list-components.py was called with branch
|
||||
mock_subprocess_run.assert_called_once()
|
||||
call_args = mock_subprocess_run.call_args[0][0]
|
||||
assert "--changed-with-deps" in call_args
|
||||
assert "-b" in call_args
|
||||
assert "main" in call_args
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = json.loads(captured.out)
|
||||
@@ -272,7 +293,7 @@ def test_main_with_branch_argument(
|
||||
# changed_cpp_file_count should be present
|
||||
assert "changed_cpp_file_count" in output
|
||||
assert isinstance(output["changed_cpp_file_count"], int)
|
||||
# memory_impact should be present
|
||||
# memory_impact should be false (no component C++ files changed)
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
|
||||
@@ -500,16 +521,11 @@ def test_main_filters_components_without_tests(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock list-components.py output with 3 components
|
||||
# wifi: has tests, sensor: has tests, airthings_ble: no tests
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{
|
||||
"directly_changed": ["wifi", "sensor"],
|
||||
"all_changed": ["wifi", "sensor", "airthings_ble"],
|
||||
}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
# Mock changed_files to return component files
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/components/wifi/wifi.cpp",
|
||||
"esphome/components/sensor/sensor.h",
|
||||
]
|
||||
|
||||
# Create test directory structure
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
@@ -533,6 +549,23 @@ def test_main_filters_components_without_tests(
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(helpers, "root_path", str(tmp_path)),
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_changed_components",
|
||||
return_value=["wifi", "sensor", "airthings_ble"],
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: ["wifi", "sensor"]
|
||||
if not deps
|
||||
else ["wifi", "sensor", "airthings_ble"],
|
||||
),
|
||||
):
|
||||
# Clear the cache since we're mocking root_path
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
@@ -788,15 +821,18 @@ def test_clang_tidy_mode_full_scan(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
# Mock changed_files to return no component files
|
||||
mock_changed_files.return_value = []
|
||||
|
||||
# Mock full scan (hash changed)
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=True),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=[]),
|
||||
patch.object(determine_jobs, "filter_component_files", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=[]
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
@@ -853,12 +889,10 @@ def test_clang_tidy_mode_targeted_scan(
|
||||
# Create component names
|
||||
components = [f"comp{i}" for i in range(component_count)]
|
||||
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": components, "all_changed": components}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
# Mock changed_files to return component files
|
||||
mock_changed_files.return_value = [
|
||||
f"esphome/components/{comp}/file.cpp" for comp in components
|
||||
]
|
||||
|
||||
# Mock git_ls_files to return files for each component
|
||||
cpp_files = {
|
||||
@@ -875,6 +909,15 @@ def test_clang_tidy_mode_targeted_scan(
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(determine_jobs, "git_ls_files", side_effect=mock_git_ls_files),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=components),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=components
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user