mirror of
https://github.com/esphome/esphome.git
synced 2025-10-30 14:43:51 +00:00
Merge branch 'text_sensor_filters' into integration
This commit is contained in:
31
tests/components/climate/common.yaml
Normal file
31
tests/components/climate/common.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
switch:
|
||||
- platform: template
|
||||
id: climate_heater_switch
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: climate_cooler_switch
|
||||
optimistic: true
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: climate_temperature_sensor
|
||||
lambda: |-
|
||||
return 21.5;
|
||||
update_interval: 60s
|
||||
|
||||
climate:
|
||||
- platform: bang_bang
|
||||
id: climate_test_climate
|
||||
name: Test Climate
|
||||
sensor: climate_temperature_sensor
|
||||
default_target_temperature_low: 18°C
|
||||
default_target_temperature_high: 24°C
|
||||
idle_action:
|
||||
- switch.turn_off: climate_heater_switch
|
||||
- switch.turn_off: climate_cooler_switch
|
||||
cool_action:
|
||||
- switch.turn_on: climate_cooler_switch
|
||||
- switch.turn_off: climate_heater_switch
|
||||
heat_action:
|
||||
- switch.turn_on: climate_heater_switch
|
||||
- switch.turn_off: climate_cooler_switch
|
||||
1
tests/components/climate/test.esp8266-ard.yaml
Normal file
1
tests/components/climate/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -16,3 +16,4 @@ esp32_improv:
|
||||
authorizer: io0_button
|
||||
authorized_duration: 1min
|
||||
status_indicator: built_in_led
|
||||
next_url: "https://example.com/setup?device={{device_name}}&ip={{ip_address}}&version={{esphome_version}}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
espnow:
|
||||
id: espnow_component
|
||||
auto_add_peer: false
|
||||
channel: 1
|
||||
peers:
|
||||
@@ -50,3 +51,26 @@ espnow:
|
||||
- format_mac_address_pretty(info.src_addr).c_str()
|
||||
- format_hex_pretty(data, size).c_str()
|
||||
- info.rx_ctrl->rssi
|
||||
|
||||
packet_transport:
|
||||
- platform: espnow
|
||||
id: transport1
|
||||
espnow_id: espnow_component
|
||||
peer_address: "FF:FF:FF:FF:FF:FF"
|
||||
encryption:
|
||||
key: "0123456789abcdef0123456789abcdef"
|
||||
sensors:
|
||||
- temp_sensor
|
||||
providers:
|
||||
- name: test_provider
|
||||
encryption:
|
||||
key: "0123456789abcdef0123456789abcdef"
|
||||
|
||||
sensor:
|
||||
- platform: internal_temperature
|
||||
id: temp_sensor
|
||||
|
||||
- platform: packet_transport
|
||||
provider: test_provider
|
||||
remote_id: temp_sensor
|
||||
id: remote_temp
|
||||
|
||||
33
tests/components/json/common.yaml
Normal file
33
tests/components/json/common.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
json:
|
||||
|
||||
interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- lambda: |-
|
||||
// Test build_json
|
||||
std::string json_str = esphome::json::build_json([](JsonObject root) {
|
||||
root["sensor"] = "temperature";
|
||||
root["value"] = 23.5;
|
||||
root["unit"] = "°C";
|
||||
});
|
||||
ESP_LOGD("test", "Built JSON: %s", json_str.c_str());
|
||||
|
||||
// Test parse_json
|
||||
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
|
||||
if (root.containsKey("sensor") && root.containsKey("value")) {
|
||||
const char* sensor = root["sensor"];
|
||||
float value = root["value"];
|
||||
ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value);
|
||||
} else {
|
||||
ESP_LOGD("test", "Parsed JSON missing required keys");
|
||||
}
|
||||
});
|
||||
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");
|
||||
|
||||
// Test JsonBuilder class
|
||||
esphome::json::JsonBuilder builder;
|
||||
JsonObject obj = builder.root();
|
||||
obj["test"] = "direct_builder";
|
||||
obj["count"] = 42;
|
||||
std::string result = builder.serialize();
|
||||
ESP_LOGD("test", "JsonBuilder result: %s", result.c_str());
|
||||
1
tests/components/json/test.esp32-idf.yaml
Normal file
1
tests/components/json/test.esp32-idf.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
1
tests/components/json/test.esp8266-ard.yaml
Normal file
1
tests/components/json/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -99,3 +99,77 @@ sensor:
|
||||
window_size: 10
|
||||
send_every: 10
|
||||
send_first_at: 1 # Send after first value
|
||||
|
||||
# ValueListFilter-based filters tests
|
||||
# FilterOutValueFilter - single value
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Filter Out Single Value"
|
||||
filters:
|
||||
- filter_out: 42.0 # Should filter out exactly 42.0
|
||||
|
||||
# FilterOutValueFilter - multiple values
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Filter Out Multiple Values"
|
||||
filters:
|
||||
- filter_out: [0.0, 42.0, 100.0] # List of values to filter
|
||||
|
||||
# FilterOutValueFilter - with NaN
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Filter Out NaN"
|
||||
filters:
|
||||
- filter_out: nan # Filter out NaN values
|
||||
|
||||
# FilterOutValueFilter - mixed values with NaN
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Filter Out Mixed with NaN"
|
||||
filters:
|
||||
- filter_out: [nan, 0.0, 42.0]
|
||||
|
||||
# ThrottleWithPriorityFilter - single priority value
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Throttle with Single Priority"
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 1000ms
|
||||
value: 42.0 # Priority value bypasses throttle
|
||||
|
||||
# ThrottleWithPriorityFilter - multiple priority values
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Throttle with Multiple Priorities"
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 500ms
|
||||
value: [0.0, 42.0, 100.0] # Multiple priority values
|
||||
|
||||
# ThrottleWithPriorityFilter - with NaN priority
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Throttle with NaN Priority"
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 1000ms
|
||||
value: nan # NaN as priority value
|
||||
|
||||
# Combined filters - FilterOutValueFilter + other filters
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Filter Out Then Throttle"
|
||||
filters:
|
||||
- filter_out: [0.0, 100.0]
|
||||
- throttle: 500ms
|
||||
|
||||
# Combined filters - ThrottleWithPriorityFilter + other filters
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Throttle Priority Then Scale"
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 1000ms
|
||||
value: [42.0]
|
||||
- multiply: 2.0
|
||||
|
||||
66
tests/components/text_sensor/common.yaml
Normal file
66
tests/components/text_sensor/common.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "Test Substitute Single"
|
||||
id: test_substitute_single
|
||||
filters:
|
||||
- substitute:
|
||||
- ERROR -> Error
|
||||
|
||||
- platform: template
|
||||
name: "Test Substitute Multiple"
|
||||
id: test_substitute_multiple
|
||||
filters:
|
||||
- substitute:
|
||||
- ERROR -> Error
|
||||
- WARN -> Warning
|
||||
- INFO -> Information
|
||||
- DEBUG -> Debug
|
||||
|
||||
- platform: template
|
||||
name: "Test Substitute Chained"
|
||||
id: test_substitute_chained
|
||||
filters:
|
||||
- substitute:
|
||||
- foo -> bar
|
||||
- to_upper
|
||||
- substitute:
|
||||
- BAR -> baz
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Single"
|
||||
id: test_map_single
|
||||
filters:
|
||||
- map:
|
||||
- ON -> Active
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Multiple"
|
||||
id: test_map_multiple
|
||||
filters:
|
||||
- map:
|
||||
- ON -> Active
|
||||
- OFF -> Inactive
|
||||
- UNKNOWN -> Error
|
||||
- IDLE -> Standby
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Passthrough"
|
||||
id: test_map_passthrough
|
||||
filters:
|
||||
- map:
|
||||
- Good -> Excellent
|
||||
- Bad -> Poor
|
||||
|
||||
- platform: template
|
||||
name: "Test All Filters"
|
||||
id: test_all_filters
|
||||
filters:
|
||||
- to_upper
|
||||
- to_lower
|
||||
- append: " suffix"
|
||||
- prepend: "prefix "
|
||||
- substitute:
|
||||
- prefix -> PREFIX
|
||||
- suffix -> SUFFIX
|
||||
- map:
|
||||
- PREFIX text SUFFIX -> mapped
|
||||
1
tests/components/text_sensor/test.esp8266-ard.yaml
Normal file
1
tests/components/text_sensor/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -11,18 +11,17 @@ time:
|
||||
- 192.168.178.1
|
||||
|
||||
uponor_smatrix:
|
||||
address: 0x110B
|
||||
time_id: sntp_time
|
||||
time_device_address: 0xDE13
|
||||
time_device_address: 0x110BDE13
|
||||
|
||||
climate:
|
||||
- platform: uponor_smatrix
|
||||
address: 0xDE13
|
||||
address: 0x110BDE13
|
||||
name: Thermostat Living Room
|
||||
|
||||
sensor:
|
||||
- platform: uponor_smatrix
|
||||
address: 0xDE13
|
||||
address: 0x110BDE13
|
||||
humidity:
|
||||
name: Thermostat Humidity Living Room
|
||||
temperature:
|
||||
|
||||
332
tests/integration/fixtures/sensor_filters_value_list.yaml
Normal file
332
tests/integration/fixtures/sensor_filters_value_list.yaml
Normal file
@@ -0,0 +1,332 @@
|
||||
esphome:
|
||||
name: test-value-list-filters
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Template sensors - one for each test to avoid cross-test interference
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Source Sensor 1"
|
||||
id: source_sensor_1
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 2"
|
||||
id: source_sensor_2
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 3"
|
||||
id: source_sensor_3
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 4"
|
||||
id: source_sensor_4
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 5"
|
||||
id: source_sensor_5
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 6"
|
||||
id: source_sensor_6
|
||||
accuracy_decimals: 2
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 7"
|
||||
id: source_sensor_7
|
||||
accuracy_decimals: 1
|
||||
|
||||
# FilterOutValueFilter - single value
|
||||
- platform: copy
|
||||
source_id: source_sensor_1
|
||||
name: "Filter Out Single"
|
||||
id: filter_out_single
|
||||
filters:
|
||||
- filter_out: 42.0
|
||||
|
||||
# FilterOutValueFilter - multiple values
|
||||
- platform: copy
|
||||
source_id: source_sensor_2
|
||||
name: "Filter Out Multiple"
|
||||
id: filter_out_multiple
|
||||
filters:
|
||||
- filter_out: [0.0, 42.0, 100.0]
|
||||
|
||||
# FilterOutValueFilter - with NaN
|
||||
- platform: copy
|
||||
source_id: source_sensor_1
|
||||
name: "Filter Out NaN"
|
||||
id: filter_out_nan
|
||||
filters:
|
||||
- filter_out: nan
|
||||
|
||||
# ThrottleWithPriorityFilter - single priority value
|
||||
- platform: copy
|
||||
source_id: source_sensor_3
|
||||
name: "Throttle Priority Single"
|
||||
id: throttle_priority_single
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 200ms
|
||||
value: 42.0
|
||||
|
||||
# ThrottleWithPriorityFilter - multiple priority values
|
||||
- platform: copy
|
||||
source_id: source_sensor_4
|
||||
name: "Throttle Priority Multiple"
|
||||
id: throttle_priority_multiple
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 200ms
|
||||
value: [0.0, 42.0, 100.0]
|
||||
|
||||
# Edge case: Filter Out NaN explicitly
|
||||
- platform: copy
|
||||
source_id: source_sensor_5
|
||||
name: "Filter Out NaN Test"
|
||||
id: filter_out_nan_test
|
||||
filters:
|
||||
- filter_out: nan
|
||||
|
||||
# Edge case: Accuracy decimals - 2 decimals
|
||||
- platform: copy
|
||||
source_id: source_sensor_6
|
||||
name: "Filter Out Accuracy 2"
|
||||
id: filter_out_accuracy_2
|
||||
filters:
|
||||
- filter_out: 42.0
|
||||
|
||||
# Edge case: Throttle with NaN priority
|
||||
- platform: copy
|
||||
source_id: source_sensor_7
|
||||
name: "Throttle Priority NaN"
|
||||
id: throttle_priority_nan
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 200ms
|
||||
value: nan
|
||||
|
||||
# Script to test FilterOutValueFilter
|
||||
script:
|
||||
- id: test_filter_out_single
|
||||
then:
|
||||
# Should pass through: 1.0, 2.0, 3.0
|
||||
# Should filter out: 42.0
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 42.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 2.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 42.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 3.0
|
||||
|
||||
- id: test_filter_out_multiple
|
||||
then:
|
||||
# Should filter out: 0.0, 42.0, 100.0
|
||||
# Should pass through: 1.0, 2.0, 50.0
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 0.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 42.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 2.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 100.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 50.0
|
||||
|
||||
- id: test_throttle_priority_single
|
||||
then:
|
||||
# 42.0 bypasses throttle, other values are throttled
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 1.0 # First value - passes
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 2.0 # Throttled
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 42.0 # Priority - passes immediately
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 3.0 # Throttled
|
||||
- delay: 250ms # Wait for throttle to expire
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 4.0 # Passes after timeout
|
||||
|
||||
- id: test_throttle_priority_multiple
|
||||
then:
|
||||
# 0.0, 42.0, 100.0 bypass throttle
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 1.0 # First value - passes
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 2.0 # Throttled
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 0.0 # Priority - passes
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 3.0 # Throttled
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 42.0 # Priority - passes
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 4.0 # Throttled
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 100.0 # Priority - passes
|
||||
|
||||
- id: test_filter_out_nan
|
||||
then:
|
||||
# NaN should be filtered out, regular values pass
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 1.0 # Pass
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: !lambda 'return NAN;' # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 2.0 # Pass
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: !lambda 'return NAN;' # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 3.0 # Pass
|
||||
|
||||
- id: test_filter_out_accuracy_2
|
||||
then:
|
||||
# With 2 decimal places, 42.00 filtered, 42.01 and 42.15 pass
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: 42.0 # Filtered (rounds to 42.00)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: 42.01 # Pass (rounds to 42.01)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: 42.15 # Pass (rounds to 42.15)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_6
|
||||
state: 42.0 # Filtered (rounds to 42.00)
|
||||
|
||||
- id: test_throttle_priority_nan
|
||||
then:
|
||||
# NaN bypasses throttle, regular values throttled
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_7
|
||||
state: 1.0 # First value - passes
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_7
|
||||
state: 2.0 # Throttled
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_7
|
||||
state: !lambda 'return NAN;' # Priority NaN - passes
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_7
|
||||
state: 3.0 # Throttled
|
||||
- delay: 50ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_7
|
||||
state: !lambda 'return NAN;' # Priority NaN - passes
|
||||
|
||||
# Buttons to trigger each test
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Filter Out Single"
|
||||
id: btn_filter_out_single
|
||||
on_press:
|
||||
- script.execute: test_filter_out_single
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Out Multiple"
|
||||
id: btn_filter_out_multiple
|
||||
on_press:
|
||||
- script.execute: test_filter_out_multiple
|
||||
|
||||
- platform: template
|
||||
name: "Test Throttle Priority Single"
|
||||
id: btn_throttle_priority_single
|
||||
on_press:
|
||||
- script.execute: test_throttle_priority_single
|
||||
|
||||
- platform: template
|
||||
name: "Test Throttle Priority Multiple"
|
||||
id: btn_throttle_priority_multiple
|
||||
on_press:
|
||||
- script.execute: test_throttle_priority_multiple
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Out NaN"
|
||||
id: btn_filter_out_nan
|
||||
on_press:
|
||||
- script.execute: test_filter_out_nan
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Out Accuracy 2"
|
||||
id: btn_filter_out_accuracy_2
|
||||
on_press:
|
||||
- script.execute: test_filter_out_accuracy_2
|
||||
|
||||
- platform: template
|
||||
name: "Test Throttle Priority NaN"
|
||||
id: btn_throttle_priority_nan
|
||||
on_press:
|
||||
- script.execute: test_throttle_priority_nan
|
||||
@@ -281,8 +281,12 @@ async def test_noise_corrupt_encrypted_frame(
|
||||
# Check for signs that the process exited/crashed
|
||||
if "Segmentation fault" in line or "core dumped" in line:
|
||||
process_exited = True
|
||||
# Check for the expected warning about decryption failure
|
||||
# Check for the expected log about decryption failure
|
||||
# This can appear as either a VV-level log from noise or a W-level log from connection
|
||||
if (
|
||||
"[VV][api.noise" in line
|
||||
and "noise_cipherstate_decrypt failed: MAC_FAILURE" in line
|
||||
) or (
|
||||
"[W][api.connection" in line
|
||||
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
|
||||
):
|
||||
@@ -322,9 +326,9 @@ async def test_noise_corrupt_encrypted_frame(
|
||||
assert not process_exited, (
|
||||
"ESPHome process should not crash on corrupt encrypted frames"
|
||||
)
|
||||
# Verify we saw the expected warning message
|
||||
# Verify we saw the expected log message about decryption failure
|
||||
assert cipherstate_failed, (
|
||||
"Expected to see warning about CIPHERSTATE_DECRYPT_FAILED"
|
||||
"Expected to see log about noise_cipherstate_decrypt failure or CIPHERSTATE_DECRYPT_FAILED"
|
||||
)
|
||||
|
||||
# Verify we can still reconnect after handling the corrupt frame
|
||||
|
||||
263
tests/integration/test_sensor_filters_value_list.py
Normal file
263
tests/integration/test_sensor_filters_value_list.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""Test sensor ValueListFilter functionality (FilterOutValueFilter and ThrottleWithPriorityFilter)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_filters_value_list(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that ValueListFilter-based filters work correctly."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track state changes for all sensors
|
||||
sensor_values: dict[str, list[float]] = {
|
||||
"filter_out_single": [],
|
||||
"filter_out_multiple": [],
|
||||
"throttle_priority_single": [],
|
||||
"throttle_priority_multiple": [],
|
||||
"filter_out_nan_test": [],
|
||||
"filter_out_accuracy_2": [],
|
||||
"throttle_priority_nan": [],
|
||||
}
|
||||
|
||||
# Futures for each test
|
||||
filter_out_single_done = loop.create_future()
|
||||
filter_out_multiple_done = loop.create_future()
|
||||
throttle_single_done = loop.create_future()
|
||||
throttle_multiple_done = loop.create_future()
|
||||
filter_out_nan_done = loop.create_future()
|
||||
filter_out_accuracy_2_done = loop.create_future()
|
||||
throttle_nan_done = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
"""Track sensor state updates."""
|
||||
if not isinstance(state, SensorState) or state.missing_state:
|
||||
return
|
||||
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
if sensor_name not in sensor_values:
|
||||
return
|
||||
|
||||
sensor_values[sensor_name].append(state.state)
|
||||
|
||||
# Check completion conditions
|
||||
if (
|
||||
sensor_name == "filter_out_single"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_out_single_done.done()
|
||||
):
|
||||
filter_out_single_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_out_multiple"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_out_multiple_done.done()
|
||||
):
|
||||
filter_out_multiple_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "throttle_priority_single"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not throttle_single_done.done()
|
||||
):
|
||||
throttle_single_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "throttle_priority_multiple"
|
||||
and len(sensor_values[sensor_name]) == 4
|
||||
and not throttle_multiple_done.done()
|
||||
):
|
||||
throttle_multiple_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_out_nan_test"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_out_nan_done.done()
|
||||
):
|
||||
filter_out_nan_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_out_accuracy_2"
|
||||
and len(sensor_values[sensor_name]) == 2
|
||||
and not filter_out_accuracy_2_done.done()
|
||||
):
|
||||
filter_out_accuracy_2_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "throttle_priority_nan"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not throttle_nan_done.done()
|
||||
):
|
||||
throttle_nan_done.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Get entities and build key mapping
|
||||
entities, _ = await client.list_entities_services()
|
||||
key_to_sensor = build_key_to_entity_mapping(
|
||||
entities,
|
||||
{
|
||||
"filter_out_single": "Filter Out Single",
|
||||
"filter_out_multiple": "Filter Out Multiple",
|
||||
"throttle_priority_single": "Throttle Priority Single",
|
||||
"throttle_priority_multiple": "Throttle Priority Multiple",
|
||||
"filter_out_nan_test": "Filter Out NaN Test",
|
||||
"filter_out_accuracy_2": "Filter Out Accuracy 2",
|
||||
"throttle_priority_nan": "Throttle Priority NaN",
|
||||
},
|
||||
)
|
||||
|
||||
# Set up initial state helper with all entities
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
|
||||
# Subscribe to state changes with wrapper
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for initial states
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Find all buttons
|
||||
button_name_map = {
|
||||
"Test Filter Out Single": "filter_out_single",
|
||||
"Test Filter Out Multiple": "filter_out_multiple",
|
||||
"Test Throttle Priority Single": "throttle_priority_single",
|
||||
"Test Throttle Priority Multiple": "throttle_priority_multiple",
|
||||
"Test Filter Out NaN": "filter_out_nan",
|
||||
"Test Filter Out Accuracy 2": "filter_out_accuracy_2",
|
||||
"Test Throttle Priority NaN": "throttle_priority_nan",
|
||||
}
|
||||
buttons = {}
|
||||
for entity in entities:
|
||||
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
|
||||
buttons[button_name_map[entity.name]] = entity.key
|
||||
|
||||
assert len(buttons) == 7, f"Expected 7 buttons, found {len(buttons)}"
|
||||
|
||||
# Test 1: FilterOutValueFilter - single value
|
||||
sensor_values["filter_out_single"].clear()
|
||||
client.button_command(buttons["filter_out_single"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_out_single_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 1 timed out. Values: {sensor_values['filter_out_single']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 2.0, 3.0]
|
||||
assert sensor_values["filter_out_single"] == pytest.approx(expected), (
|
||||
f"Test 1 failed: expected {expected}, got {sensor_values['filter_out_single']}"
|
||||
)
|
||||
|
||||
# Test 2: FilterOutValueFilter - multiple values
|
||||
sensor_values["filter_out_multiple"].clear()
|
||||
filter_out_multiple_done = loop.create_future()
|
||||
client.button_command(buttons["filter_out_multiple"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_out_multiple_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 2 timed out. Values: {sensor_values['filter_out_multiple']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 2.0, 50.0]
|
||||
assert sensor_values["filter_out_multiple"] == pytest.approx(expected), (
|
||||
f"Test 2 failed: expected {expected}, got {sensor_values['filter_out_multiple']}"
|
||||
)
|
||||
|
||||
# Test 3: ThrottleWithPriorityFilter - single priority
|
||||
sensor_values["throttle_priority_single"].clear()
|
||||
throttle_single_done = loop.create_future()
|
||||
client.button_command(buttons["throttle_priority_single"])
|
||||
try:
|
||||
await asyncio.wait_for(throttle_single_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 3 timed out. Values: {sensor_values['throttle_priority_single']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 42.0, 4.0]
|
||||
assert sensor_values["throttle_priority_single"] == pytest.approx(expected), (
|
||||
f"Test 3 failed: expected {expected}, got {sensor_values['throttle_priority_single']}"
|
||||
)
|
||||
|
||||
# Test 4: ThrottleWithPriorityFilter - multiple priorities
|
||||
sensor_values["throttle_priority_multiple"].clear()
|
||||
throttle_multiple_done = loop.create_future()
|
||||
client.button_command(buttons["throttle_priority_multiple"])
|
||||
try:
|
||||
await asyncio.wait_for(throttle_multiple_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 4 timed out. Values: {sensor_values['throttle_priority_multiple']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 0.0, 42.0, 100.0]
|
||||
assert sensor_values["throttle_priority_multiple"] == pytest.approx(expected), (
|
||||
f"Test 4 failed: expected {expected}, got {sensor_values['throttle_priority_multiple']}"
|
||||
)
|
||||
|
||||
# Test 5: FilterOutValueFilter - NaN handling
|
||||
sensor_values["filter_out_nan_test"].clear()
|
||||
filter_out_nan_done = loop.create_future()
|
||||
client.button_command(buttons["filter_out_nan"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_out_nan_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 5 timed out. Values: {sensor_values['filter_out_nan_test']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 2.0, 3.0]
|
||||
assert sensor_values["filter_out_nan_test"] == pytest.approx(expected), (
|
||||
f"Test 5 failed: expected {expected}, got {sensor_values['filter_out_nan_test']}"
|
||||
)
|
||||
|
||||
# Test 6: FilterOutValueFilter - Accuracy decimals (2)
|
||||
sensor_values["filter_out_accuracy_2"].clear()
|
||||
filter_out_accuracy_2_done = loop.create_future()
|
||||
client.button_command(buttons["filter_out_accuracy_2"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_out_accuracy_2_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 6 timed out. Values: {sensor_values['filter_out_accuracy_2']}"
|
||||
)
|
||||
|
||||
expected = [42.01, 42.15]
|
||||
assert sensor_values["filter_out_accuracy_2"] == pytest.approx(expected), (
|
||||
f"Test 6 failed: expected {expected}, got {sensor_values['filter_out_accuracy_2']}"
|
||||
)
|
||||
|
||||
# Test 7: ThrottleWithPriorityFilter - NaN priority
|
||||
sensor_values["throttle_priority_nan"].clear()
|
||||
throttle_nan_done = loop.create_future()
|
||||
client.button_command(buttons["throttle_priority_nan"])
|
||||
try:
|
||||
await asyncio.wait_for(throttle_nan_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 7 timed out. Values: {sensor_values['throttle_priority_nan']}"
|
||||
)
|
||||
|
||||
# First value (1.0) + two NaN priority values
|
||||
# NaN values will be compared using math.isnan
|
||||
assert len(sensor_values["throttle_priority_nan"]) == 3, (
|
||||
f"Test 7 failed: expected 3 values, got {len(sensor_values['throttle_priority_nan'])}"
|
||||
)
|
||||
assert sensor_values["throttle_priority_nan"][0] == pytest.approx(1.0), (
|
||||
f"Test 7 failed: first value should be 1.0, got {sensor_values['throttle_priority_nan'][0]}"
|
||||
)
|
||||
assert math.isnan(sensor_values["throttle_priority_nan"][1]), (
|
||||
f"Test 7 failed: second value should be NaN, got {sensor_values['throttle_priority_nan'][1]}"
|
||||
)
|
||||
assert math.isnan(sensor_values["throttle_priority_nan"][2]), (
|
||||
f"Test 7 failed: third value should be NaN, got {sensor_values['throttle_priority_nan'][2]}"
|
||||
)
|
||||
@@ -107,6 +107,7 @@ def test_main_all_tests_should_run(
|
||||
|
||||
assert output["integration_tests"] is True
|
||||
assert output["clang_tidy"] is True
|
||||
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||
assert output["clang_format"] is True
|
||||
assert output["python_linters"] is True
|
||||
assert output["changed_components"] == ["wifi", "api", "sensor"]
|
||||
@@ -117,6 +118,9 @@ def test_main_all_tests_should_run(
|
||||
assert output["component_test_count"] == len(
|
||||
output["changed_components_with_tests"]
|
||||
)
|
||||
# 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
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false" # No files changed
|
||||
@@ -156,11 +160,14 @@ def test_main_no_tests_should_run(
|
||||
|
||||
assert output["integration_tests"] is False
|
||||
assert output["clang_tidy"] is False
|
||||
assert output["clang_tidy_mode"] == "disabled"
|
||||
assert output["clang_format"] is False
|
||||
assert output["python_linters"] is False
|
||||
assert output["changed_components"] == []
|
||||
assert output["changed_components_with_tests"] == []
|
||||
assert output["component_test_count"] == 0
|
||||
# changed_cpp_file_count should be 0
|
||||
assert output["changed_cpp_file_count"] == 0
|
||||
# memory_impact should be present
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
@@ -239,6 +246,7 @@ def test_main_with_branch_argument(
|
||||
|
||||
assert output["integration_tests"] is False
|
||||
assert output["clang_tidy"] is True
|
||||
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||
assert output["clang_format"] is False
|
||||
assert output["python_linters"] is True
|
||||
assert output["changed_components"] == ["mqtt"]
|
||||
@@ -249,6 +257,9 @@ def test_main_with_branch_argument(
|
||||
assert output["component_test_count"] == len(
|
||||
output["changed_components_with_tests"]
|
||||
)
|
||||
# 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
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
@@ -433,6 +444,40 @@ def test_should_run_clang_format_with_branch() -> None:
|
||||
mock_changed.assert_called_once_with("release")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("changed_files", "expected_count"),
|
||||
[
|
||||
(["esphome/core.cpp"], 1),
|
||||
(["esphome/core.h"], 1),
|
||||
(["test.hpp"], 1),
|
||||
(["test.cc"], 1),
|
||||
(["test.cxx"], 1),
|
||||
(["test.c"], 1),
|
||||
(["test.tcc"], 1),
|
||||
(["esphome/core.cpp", "esphome/core.h"], 2),
|
||||
(["esphome/core.cpp", "esphome/core.h", "test.cc"], 3),
|
||||
(["README.md"], 0),
|
||||
(["esphome/config.py"], 0),
|
||||
(["README.md", "esphome/config.py"], 0),
|
||||
(["esphome/core.cpp", "README.md", "esphome/config.py"], 1),
|
||||
([], 0),
|
||||
],
|
||||
)
|
||||
def test_count_changed_cpp_files(changed_files: list[str], expected_count: int) -> None:
|
||||
"""Test count_changed_cpp_files function."""
|
||||
with patch.object(determine_jobs, "changed_files", return_value=changed_files):
|
||||
result = determine_jobs.count_changed_cpp_files()
|
||||
assert result == expected_count
|
||||
|
||||
|
||||
def test_count_changed_cpp_files_with_branch() -> None:
|
||||
"""Test count_changed_cpp_files with branch argument."""
|
||||
with patch.object(determine_jobs, "changed_files") as mock_changed:
|
||||
mock_changed.return_value = []
|
||||
determine_jobs.count_changed_cpp_files("release")
|
||||
mock_changed.assert_called_once_with("release")
|
||||
|
||||
|
||||
def test_main_filters_components_without_tests(
|
||||
mock_should_run_integration_tests: Mock,
|
||||
mock_should_run_clang_tidy: Mock,
|
||||
@@ -501,6 +546,9 @@ def test_main_filters_components_without_tests(
|
||||
assert set(output["changed_components_with_tests"]) == {"wifi", "sensor"}
|
||||
# component_test_count should be based on components with tests
|
||||
assert output["component_test_count"] == 2
|
||||
# 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
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
@@ -545,7 +593,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non
|
||||
|
||||
|
||||
def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
||||
"""Test memory impact detection with core-only changes (no component changes)."""
|
||||
"""Test memory impact detection with core C++ changes (no component changes)."""
|
||||
# Create test directory structure with fallback component
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
|
||||
@@ -554,7 +602,7 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
||||
api_dir.mkdir(parents=True)
|
||||
(api_dir / "test.esp32-idf.yaml").write_text("test: api")
|
||||
|
||||
# Mock changed_files to return only core files (no component files)
|
||||
# Mock changed_files to return only core C++ files (no component files)
|
||||
with (
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(helpers, "root_path", str(tmp_path)),
|
||||
@@ -574,6 +622,35 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
||||
assert result["use_merged_config"] == "true"
|
||||
|
||||
|
||||
def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> None:
|
||||
"""Test that Python-only core changes don't trigger memory impact analysis."""
|
||||
# Create test directory structure with fallback component
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
|
||||
# api component (fallback component) with esp32-idf test
|
||||
api_dir = tests_dir / "api"
|
||||
api_dir.mkdir(parents=True)
|
||||
(api_dir / "test.esp32-idf.yaml").write_text("test: api")
|
||||
|
||||
# Mock changed_files to return only core Python files (no C++ files)
|
||||
with (
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(helpers, "root_path", str(tmp_path)),
|
||||
patch.object(determine_jobs, "changed_files") as mock_changed_files,
|
||||
):
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/__main__.py",
|
||||
"esphome/config.py",
|
||||
"esphome/core/config.py",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
# Python-only changes should NOT trigger memory impact analysis
|
||||
assert result["should_run"] == "false"
|
||||
|
||||
|
||||
def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
|
||||
"""Test memory impact detection when components have no common platform."""
|
||||
# Create test directory structure
|
||||
|
||||
@@ -8,6 +8,7 @@ substitutions:
|
||||
area: 25
|
||||
numberOne: 1
|
||||
var1: 79
|
||||
double_width: 14
|
||||
test_list:
|
||||
- The area is 56
|
||||
- 56
|
||||
@@ -25,3 +26,4 @@ test_list:
|
||||
- ord("a") = 97
|
||||
- chr(97) = a
|
||||
- len([1,2,3]) = 3
|
||||
- width = 7, double_width = 14
|
||||
|
||||
@@ -8,6 +8,7 @@ substitutions:
|
||||
area: 25
|
||||
numberOne: 1
|
||||
var1: 79
|
||||
double_width: ${width * 2}
|
||||
|
||||
test_list:
|
||||
- "The area is ${width * height}"
|
||||
@@ -23,3 +24,4 @@ test_list:
|
||||
- ord("a") = ${ ord("a") }
|
||||
- chr(97) = ${ chr(97) }
|
||||
- len([1,2,3]) = ${ len([1,2,3]) }
|
||||
- width = ${width}, double_width = ${double_width}
|
||||
|
||||
@@ -17,10 +17,12 @@ from esphome import platformio_api
|
||||
from esphome.__main__ import (
|
||||
Purpose,
|
||||
choose_upload_log_host,
|
||||
command_analyze_memory,
|
||||
command_clean_all,
|
||||
command_rename,
|
||||
command_update_all,
|
||||
command_wizard,
|
||||
detect_external_components,
|
||||
get_port_type,
|
||||
has_ip_address,
|
||||
has_mqtt,
|
||||
@@ -226,13 +228,47 @@ def mock_run_external_process() -> Generator[Mock]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_run_external_command() -> Generator[Mock]:
|
||||
"""Mock run_external_command for testing."""
|
||||
def mock_run_external_command_main() -> Generator[Mock]:
|
||||
"""Mock run_external_command in __main__ module (different from platformio_api)."""
|
||||
with patch("esphome.__main__.run_external_command") as mock:
|
||||
mock.return_value = 0 # Default to success
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_write_cpp() -> Generator[Mock]:
|
||||
"""Mock write_cpp for testing."""
|
||||
with patch("esphome.__main__.write_cpp") as mock:
|
||||
mock.return_value = 0 # Default to success
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_compile_program() -> Generator[Mock]:
|
||||
"""Mock compile_program for testing."""
|
||||
with patch("esphome.__main__.compile_program") as mock:
|
||||
mock.return_value = 0 # Default to success
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_esphome_components() -> Generator[Mock]:
|
||||
"""Mock get_esphome_components for testing."""
|
||||
with patch("esphome.analyze_memory.helpers.get_esphome_components") as mock:
|
||||
mock.return_value = {"logger", "api", "ota"}
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_memory_analyzer_cli() -> Generator[Mock]:
|
||||
"""Mock MemoryAnalyzerCLI for testing."""
|
||||
with patch("esphome.analyze_memory.cli.MemoryAnalyzerCLI") as mock_class:
|
||||
mock_analyzer = MagicMock()
|
||||
mock_analyzer.generate_report.return_value = "Mock Memory Report"
|
||||
mock_class.return_value = mock_analyzer
|
||||
yield mock_class
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_string_default() -> None:
|
||||
"""Test with a single string default device."""
|
||||
setup_core()
|
||||
@@ -839,7 +875,7 @@ def test_upload_program_serial_esp8266_with_file(
|
||||
|
||||
def test_upload_using_esptool_path_conversion(
|
||||
tmp_path: Path,
|
||||
mock_run_external_command: Mock,
|
||||
mock_run_external_command_main: Mock,
|
||||
mock_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test upload_using_esptool properly converts Path objects to strings for esptool.
|
||||
@@ -875,10 +911,10 @@ def test_upload_using_esptool_path_conversion(
|
||||
assert result == 0
|
||||
|
||||
# Verify that run_external_command was called
|
||||
assert mock_run_external_command.call_count == 1
|
||||
assert mock_run_external_command_main.call_count == 1
|
||||
|
||||
# Get the actual call arguments
|
||||
call_args = mock_run_external_command.call_args[0]
|
||||
call_args = mock_run_external_command_main.call_args[0]
|
||||
|
||||
# The first argument should be esptool.main function,
|
||||
# followed by the command arguments
|
||||
@@ -917,7 +953,7 @@ def test_upload_using_esptool_path_conversion(
|
||||
|
||||
def test_upload_using_esptool_with_file_path(
|
||||
tmp_path: Path,
|
||||
mock_run_external_command: Mock,
|
||||
mock_run_external_command_main: Mock,
|
||||
) -> None:
|
||||
"""Test upload_using_esptool with a custom file that's a Path object."""
|
||||
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
|
||||
@@ -934,10 +970,10 @@ def test_upload_using_esptool_with_file_path(
|
||||
assert result == 0
|
||||
|
||||
# Verify that run_external_command was called
|
||||
mock_run_external_command.assert_called_once()
|
||||
mock_run_external_command_main.assert_called_once()
|
||||
|
||||
# Get the actual call arguments
|
||||
call_args = mock_run_external_command.call_args[0]
|
||||
call_args = mock_run_external_command_main.call_args[0]
|
||||
cmd_list = list(call_args[1:]) # Skip the esptool.main function
|
||||
|
||||
# Find the firmware path in the command
|
||||
@@ -2273,3 +2309,226 @@ def test_show_logs_api_mqtt_timeout_fallback(
|
||||
|
||||
# Verify run_logs was called with only the static IP (MQTT failed)
|
||||
mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"])
|
||||
|
||||
|
||||
def test_detect_external_components_no_external(
|
||||
mock_get_esphome_components: Mock,
|
||||
) -> None:
|
||||
"""Test detect_external_components with no external components."""
|
||||
config = {
|
||||
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||
"logger": {},
|
||||
"api": {},
|
||||
}
|
||||
|
||||
result = detect_external_components(config)
|
||||
|
||||
assert result == set()
|
||||
mock_get_esphome_components.assert_called_once()
|
||||
|
||||
|
||||
def test_detect_external_components_with_external(
|
||||
mock_get_esphome_components: Mock,
|
||||
) -> None:
|
||||
"""Test detect_external_components detects external components."""
|
||||
config = {
|
||||
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||
"logger": {}, # Built-in
|
||||
"api": {}, # Built-in
|
||||
"my_custom_sensor": {}, # External
|
||||
"another_custom": {}, # External
|
||||
"external_components": [], # Special key, not a component
|
||||
"substitutions": {}, # Special key, not a component
|
||||
}
|
||||
|
||||
result = detect_external_components(config)
|
||||
|
||||
assert result == {"my_custom_sensor", "another_custom"}
|
||||
mock_get_esphome_components.assert_called_once()
|
||||
|
||||
|
||||
def test_detect_external_components_filters_special_keys(
|
||||
mock_get_esphome_components: Mock,
|
||||
) -> None:
|
||||
"""Test detect_external_components filters out special config keys."""
|
||||
config = {
|
||||
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||
"substitutions": {"key": "value"},
|
||||
"packages": {},
|
||||
"globals": [],
|
||||
"external_components": [],
|
||||
"<<": {}, # YAML merge key
|
||||
}
|
||||
|
||||
result = detect_external_components(config)
|
||||
|
||||
assert result == set()
|
||||
mock_get_esphome_components.assert_called_once()
|
||||
|
||||
|
||||
def test_command_analyze_memory_success(
|
||||
tmp_path: Path,
|
||||
capfd: CaptureFixture[str],
|
||||
mock_write_cpp: Mock,
|
||||
mock_compile_program: Mock,
|
||||
mock_get_idedata: Mock,
|
||||
mock_get_esphome_components: Mock,
|
||||
mock_memory_analyzer_cli: Mock,
|
||||
) -> None:
|
||||
"""Test command_analyze_memory with successful compilation and analysis."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||
|
||||
# Create firmware.elf file
|
||||
firmware_path = (
|
||||
tmp_path / ".esphome" / "build" / "test_device" / ".pioenvs" / "test_device"
|
||||
)
|
||||
firmware_path.mkdir(parents=True, exist_ok=True)
|
||||
firmware_elf = firmware_path / "firmware.elf"
|
||||
firmware_elf.write_text("mock elf file")
|
||||
|
||||
# Mock idedata
|
||||
mock_idedata_obj = MagicMock(spec=platformio_api.IDEData)
|
||||
mock_idedata_obj.firmware_elf_path = str(firmware_elf)
|
||||
mock_idedata_obj.objdump_path = "/path/to/objdump"
|
||||
mock_idedata_obj.readelf_path = "/path/to/readelf"
|
||||
mock_get_idedata.return_value = mock_idedata_obj
|
||||
|
||||
config = {
|
||||
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||
"logger": {},
|
||||
}
|
||||
|
||||
args = MockArgs()
|
||||
|
||||
result = command_analyze_memory(args, config)
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Verify compilation was done
|
||||
mock_write_cpp.assert_called_once_with(config)
|
||||
mock_compile_program.assert_called_once_with(args, config)
|
||||
|
||||
# Verify analyzer was created with correct parameters
|
||||
mock_memory_analyzer_cli.assert_called_once_with(
|
||||
str(firmware_elf),
|
||||
"/path/to/objdump",
|
||||
"/path/to/readelf",
|
||||
set(), # No external components
|
||||
)
|
||||
|
||||
# Verify analysis was run
|
||||
mock_analyzer = mock_memory_analyzer_cli.return_value
|
||||
mock_analyzer.analyze.assert_called_once()
|
||||
mock_analyzer.generate_report.assert_called_once()
|
||||
|
||||
# Verify report was printed
|
||||
captured = capfd.readouterr()
|
||||
assert "Mock Memory Report" in captured.out
|
||||
|
||||
|
||||
def test_command_analyze_memory_with_external_components(
|
||||
tmp_path: Path,
|
||||
mock_write_cpp: Mock,
|
||||
mock_compile_program: Mock,
|
||||
mock_get_idedata: Mock,
|
||||
mock_get_esphome_components: Mock,
|
||||
mock_memory_analyzer_cli: Mock,
|
||||
) -> None:
|
||||
"""Test command_analyze_memory detects external components."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||
|
||||
# Create firmware.elf file
|
||||
firmware_path = (
|
||||
tmp_path / ".esphome" / "build" / "test_device" / ".pioenvs" / "test_device"
|
||||
)
|
||||
firmware_path.mkdir(parents=True, exist_ok=True)
|
||||
firmware_elf = firmware_path / "firmware.elf"
|
||||
firmware_elf.write_text("mock elf file")
|
||||
|
||||
# Mock idedata
|
||||
mock_idedata_obj = MagicMock(spec=platformio_api.IDEData)
|
||||
mock_idedata_obj.firmware_elf_path = str(firmware_elf)
|
||||
mock_idedata_obj.objdump_path = "/path/to/objdump"
|
||||
mock_idedata_obj.readelf_path = "/path/to/readelf"
|
||||
mock_get_idedata.return_value = mock_idedata_obj
|
||||
|
||||
config = {
|
||||
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||
"logger": {},
|
||||
"my_custom_component": {"param": "value"}, # External component
|
||||
"external_components": [{"source": "github://user/repo"}], # Not a component
|
||||
}
|
||||
|
||||
args = MockArgs()
|
||||
|
||||
result = command_analyze_memory(args, config)
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Verify analyzer was created with external components detected
|
||||
mock_memory_analyzer_cli.assert_called_once_with(
|
||||
str(firmware_elf),
|
||||
"/path/to/objdump",
|
||||
"/path/to/readelf",
|
||||
{"my_custom_component"}, # External component detected
|
||||
)
|
||||
|
||||
|
||||
def test_command_analyze_memory_write_cpp_fails(
|
||||
tmp_path: Path,
|
||||
mock_write_cpp: Mock,
|
||||
) -> None:
|
||||
"""Test command_analyze_memory when write_cpp fails."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||
|
||||
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
mock_write_cpp.return_value = 1 # Failure
|
||||
|
||||
result = command_analyze_memory(args, config)
|
||||
|
||||
assert result == 1
|
||||
mock_write_cpp.assert_called_once_with(config)
|
||||
|
||||
|
||||
def test_command_analyze_memory_compile_fails(
|
||||
tmp_path: Path,
|
||||
mock_write_cpp: Mock,
|
||||
mock_compile_program: Mock,
|
||||
) -> None:
|
||||
"""Test command_analyze_memory when compilation fails."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||
|
||||
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
mock_compile_program.return_value = 1 # Compilation failed
|
||||
|
||||
result = command_analyze_memory(args, config)
|
||||
|
||||
assert result == 1
|
||||
mock_write_cpp.assert_called_once_with(config)
|
||||
mock_compile_program.assert_called_once_with(args, config)
|
||||
|
||||
|
||||
def test_command_analyze_memory_no_idedata(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_write_cpp: Mock,
|
||||
mock_compile_program: Mock,
|
||||
mock_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test command_analyze_memory when idedata cannot be retrieved."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||
|
||||
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
mock_get_idedata.return_value = None # Failed to get idedata
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
result = command_analyze_memory(args, config)
|
||||
|
||||
assert result == 1
|
||||
assert "Failed to get IDE data for memory analysis" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user