Merge branch 'esp8266-native-framework-installer' into esp8266-native-library-backend

This commit is contained in:
J. Nick Koston
2026-08-22 12:15:39 -05:00
246 changed files with 2473 additions and 663 deletions
@@ -1,5 +1,13 @@
"""Tests for the deep sleep component."""
import pytest
from esphome import config_validation as cv
from esphome.components import deep_sleep
from esphome.const import CONF_WAKEUP_PIN, PlatformFramework
from ..types import SetCoreConfigCallable
def test_deep_sleep_setup(generate_main):
"""
@@ -83,3 +91,35 @@ def test_deep_sleep_run_duration_dictionary(generate_main):
" .gpio_cause = 30000,\n"
"});"
) in main_cpp
def test_deep_sleep_bk72xx_wakeup_pin_mode_at_both_levels_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""On BK72xx, wakeup_pin_mode at the top level and under the pin entry is an error."""
set_core_config(PlatformFramework.BK72XX_ARDUINO)
config = {
CONF_WAKEUP_PIN: [
{"pin": "GPIO12", deep_sleep.CONF_WAKEUP_PIN_MODE: "KEEP_AWAKE"}
],
deep_sleep.CONF_WAKEUP_PIN_MODE: "INVERT_WAKEUP",
}
with pytest.raises(cv.Invalid, match="not both"):
deep_sleep.validate_config(config)
def test_deep_sleep_bk72xx_top_level_wakeup_pin_mode_moved_onto_single_pin(
set_core_config: SetCoreConfigCallable,
) -> None:
"""On BK72xx, a top-level wakeup_pin_mode is moved onto the only pin entry."""
set_core_config(PlatformFramework.BK72XX_ARDUINO)
config = {
CONF_WAKEUP_PIN: [{"pin": "GPIO12"}],
deep_sleep.CONF_WAKEUP_PIN_MODE: "INVERT_WAKEUP",
}
result = deep_sleep.validate_config(config)
assert deep_sleep.CONF_WAKEUP_PIN_MODE not in result
assert (
result[CONF_WAKEUP_PIN][0][deep_sleep.CONF_WAKEUP_PIN_MODE] == "INVERT_WAKEUP"
)
@@ -0,0 +1,100 @@
"""Tests for emontx sensor tag defaults."""
import pytest
from esphome.components import sensor
from esphome.components.emontx.sensor import CONFIG_SCHEMA, apply_tag_defaults
from esphome.const import (
CONF_ACCURACY_DECIMALS,
CONF_STATE_CLASS,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
)
def _resolve_via_config_schema(tag: str) -> dict:
"""Run a minimal config through the real CONFIG_SCHEMA pipeline, the
same path a user's YAML goes through."""
return CONFIG_SCHEMA(
{"tag_name": tag, "emontx_id": "my_emontx", "name": f"{tag} sensor"}
)
def test_config_schema_applies_tag_default_state_class():
"""If sensor_schema(state_class=...) is reintroduced, the schema-level
default wins over apply_tag_defaults' per-prefix value, and E1 would
resolve to measurement instead of total_increasing. Driving the real
CONFIG_SCHEMA (not just apply_tag_defaults) catches that, since
sensor_schema() runs before apply_tag_defaults in the cv.All() chain.
"""
result = _resolve_via_config_schema("E1")
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(
STATE_CLASS_TOTAL_INCREASING
)
def test_config_schema_applies_tag_default_accuracy_decimals():
"""Same root cause as the state_class regression: reintroducing
sensor_schema(accuracy_decimals=...) would make V1 resolve to the
schema-level default instead of the prefix-specific value of 2.
"""
result = _resolve_via_config_schema("V1")
assert result[CONF_ACCURACY_DECIMALS] == 2
def _make_config(tag: str) -> dict:
"""Minimal config dict with only tag_name set — no overrides."""
return {"tag_name": tag}
@pytest.mark.parametrize(
("tag", "expected_state_class", "expected_decimals"),
[
# Known numeric-index prefixes
("E1", STATE_CLASS_TOTAL_INCREASING, 0),
("E12", STATE_CLASS_TOTAL_INCREASING, 0),
("P1", STATE_CLASS_MEASUREMENT, 0),
("V1", STATE_CLASS_MEASUREMENT, 2),
("I1", STATE_CLASS_MEASUREMENT, 2),
("T1", STATE_CLASS_MEASUREMENT, 2),
# Known patterns
("PULSE1", STATE_CLASS_TOTAL_INCREASING, 0),
("PULSE12", STATE_CLASS_TOTAL_INCREASING, 0),
("PF1", STATE_CLASS_MEASUREMENT, 2),
# Unknown / free-form tags fall back to generic defaults
("CUSTOM1", STATE_CLASS_MEASUREMENT, 0),
("X", STATE_CLASS_MEASUREMENT, 0),
],
)
def test_apply_tag_defaults(tag, expected_state_class, expected_decimals):
"""apply_tag_defaults must inject the correct state_class and accuracy_decimals
for each tag type when no user overrides are present."""
config = _make_config(tag)
result = apply_tag_defaults(config)
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(expected_state_class)
assert result[CONF_ACCURACY_DECIMALS] == expected_decimals
@pytest.mark.parametrize(
("tag", "user_state_class", "user_decimals"),
[
# User overrides must not be clobbered by defaults
("E1", STATE_CLASS_MEASUREMENT, 3),
("PULSE1", STATE_CLASS_MEASUREMENT, 1),
("V1", STATE_CLASS_TOTAL_INCREASING, 0),
("CUSTOM1", STATE_CLASS_TOTAL_INCREASING, 4),
],
)
def test_apply_tag_defaults_respects_user_overrides(
tag, user_state_class, user_decimals
):
"""apply_tag_defaults must not overwrite values already set by the user."""
config = _make_config(tag)
config[CONF_STATE_CLASS] = sensor.validate_state_class(user_state_class)
config[CONF_ACCURACY_DECIMALS] = user_decimals
result = apply_tag_defaults(config)
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(user_state_class)
assert result[CONF_ACCURACY_DECIMALS] == user_decimals
@@ -0,0 +1,10 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# The scan list helper is header-only and needs none of the component's real
# dependencies. Pulling them in breaks the host build: web_server_base
# includes ESPAsyncWebServer.h and ota.web_server includes md5/md5.h, neither
# of which exists there.
manifest.dependencies = []
manifest.auto_load = []
@@ -0,0 +1,130 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <string>
#include <vector>
#include "esphome/components/captive_portal/scan_list.h"
namespace esphome::captive_portal::testing {
namespace {
// Stand-in for wifi::WiFiScanResult, which does not compile on the host.
struct Entry {
std::string ssid;
int8_t rssi;
bool with_auth{true};
bool is_hidden{false};
// Compares length and bytes like CompactString does, so an embedded NUL counts.
bool ssid_equals(const Entry &other) const { return this->ssid == other.ssid; }
int8_t get_rssi() const { return this->rssi; }
bool get_with_auth() const { return this->with_auth; }
bool get_is_hidden() const { return this->is_hidden; }
};
// One row as the portal would emit it.
struct Row {
std::string ssid;
int8_t rssi;
bool lock;
bool operator==(const Row &rhs) const { return ssid == rhs.ssid && rssi == rhs.rssi && lock == rhs.lock; }
};
// Walk the results the way handle_config does and collect the rows that survive.
std::vector<Row> rows(const std::vector<Entry> &results) {
std::vector<Row> out;
for (size_t i = 0; i < results.size(); i++) {
bool with_auth = false;
if (!should_show_scan_entry(results, results[i], with_auth))
continue;
out.push_back({results[i].ssid, results[i].rssi, with_auth});
}
return out;
}
} // namespace
TEST(ScanList, SingleEntryShown) {
std::vector<Entry> results = {{"Home", -60}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -60, true}}));
}
TEST(ScanList, DistinctSsidsAllShownInOrder) {
std::vector<Entry> results = {{"Home", -60}, {"Guest", -70}, {"Cafe", -40}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -60, true}, {"Guest", -70, true}, {"Cafe", -40, true}}));
}
// Results are ordered by connection preference, not RSSI, so the strongest entry
// can sit anywhere in the list.
TEST(ScanList, SameSsidKeepsStrongest) {
std::vector<Entry> results = {{"Home", -70}, {"Home", -50}, {"Home", -60}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -50, true}}));
}
TEST(ScanList, EqualRssiKeepsFirst) {
std::vector<Entry> results = {{"Home", -60}, {"Home", -60}, {"Home", -60}};
bool with_auth = false;
EXPECT_TRUE(should_show_scan_entry(results, results[0], with_auth));
EXPECT_FALSE(should_show_scan_entry(results, results[1], with_auth));
EXPECT_FALSE(should_show_scan_entry(results, results[2], with_auth));
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -60, true}}));
}
// with_auth is an out-parameter that must only be written for a shown entry.
TEST(ScanList, WithAuthUntouchedWhenNotShown) {
std::vector<Entry> results = {{"Home", -50, false}, {"Home", -70, true}};
bool with_auth = false;
EXPECT_FALSE(should_show_scan_entry(results, results[1], with_auth));
EXPECT_FALSE(with_auth);
}
TEST(ScanList, DuplicatesInterleavedWithOtherNetworks) {
std::vector<Entry> results = {{"Home", -70}, {"Guest", -55}, {"Home", -50}, {"Guest", -65}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Guest", -55, true}, {"Home", -50, true}}));
}
// Hidden networks scan with an empty SSID. They are never listed and do not
// collapse into each other or into anything else.
TEST(ScanList, HiddenEntriesNeverShown) {
std::vector<Entry> results = {{"", -40, true, true}, {"Home", -70}, {"", -30, true, true}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -70, true}}));
}
// On ESP8266 the hidden flag comes from the driver alongside a real SSID, so a
// hidden access point can share its name with a visible one. It must not
// outrank that visible entry and leave the network unlisted.
TEST(ScanList, HiddenEntryDoesNotSuppressVisibleSameSsid) {
std::vector<Entry> results = {{"Home", -40, true, true}, {"Home", -70}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -70, true}}));
}
// An open access point and a secured one sharing an SSID collapse to one row that
// still asks for a password, whichever of them is strongest.
TEST(ScanList, LockSetWhenAnyEntryRequiresAuth) {
std::vector<Entry> open_stronger = {{"Home", -50, false}, {"Home", -70, true}};
EXPECT_EQ(rows(open_stronger), (std::vector<Row>{{"Home", -50, true}}));
std::vector<Entry> secured_stronger = {{"Home", -70, false}, {"Home", -50, true}};
EXPECT_EQ(rows(secured_stronger), (std::vector<Row>{{"Home", -50, true}}));
}
TEST(ScanList, LockClearWhenEveryEntryIsOpen) {
std::vector<Entry> results = {{"Cafe", -60, false}, {"Cafe", -50, false}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Cafe", -50, false}}));
}
// The auth flag of an unrelated network must not leak into another SSID's row.
TEST(ScanList, LockIsPerSsid) {
std::vector<Entry> results = {{"Cafe", -60, false}, {"Home", -50, true}};
EXPECT_EQ(rows(results), (std::vector<Row>{{"Cafe", -60, false}, {"Home", -50, true}}));
}
TEST(ScanList, EmptyListShowsNothing) {
std::vector<Entry> results;
EXPECT_TRUE(rows(results).empty());
}
} // namespace esphome::captive_portal::testing
+46
View File
@@ -83,4 +83,50 @@ TEST(StaticVectorTest, ConvertingConstructorSameSize) {
EXPECT_EQ(dst[2], 3);
}
TEST(StringContainsIgnoreCaseTest, NullPointerAlwaysFalse) {
const char *haystack = nullptr;
const char *needle = nullptr;
EXPECT_FALSE(str_contains_ignore_case(haystack, needle));
EXPECT_FALSE(str_contains_ignore_case("Hello World", needle));
EXPECT_FALSE(str_contains_ignore_case(haystack, "anything"));
}
TEST(StringContainsIgnoreCaseTest, EmptySearchMatches) {
const char *haystack = "Hello World";
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, ""));
}
TEST(StringContainsIgnoreCaseTest, MiscCaseMatches) {
const char *haystack = "Hello World";
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hello"));
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hello"));
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "HELLO"));
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hELLO"));
}
TEST(StringContainsIgnoreCaseTest, MiscNotMatching) {
const char *haystack = "Hello World";
// Expected to match
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hell"));
// Expected not to match
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Heaven"));
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Hello!"));
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "world!"));
}
TEST(StringContainsIgnoreCaseTest, FallbackMatchesLibc) {
const char *haystack = "Hello World";
for (const char *needle : {"", "Hello", "hELLO", "Hell", "world", "Heaven", "Hello!", "d"}) {
EXPECT_EQ(str_contains_ignore_case_fallback(haystack, needle), str_contains_ignore_case(haystack, needle))
<< "needle: " << needle;
}
EXPECT_EQ(str_contains_ignore_case_fallback("", ""), str_contains_ignore_case("", ""));
EXPECT_EQ(str_contains_ignore_case_fallback("ab", "abc"), str_contains_ignore_case("ab", "abc"));
}
} // namespace esphome
+1 -2
View File
@@ -1,4 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
<<: !include common.yaml
emontx: !include common.yaml
@@ -1,4 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
<<: !include common.yaml
emontx: !include common.yaml
+1 -2
View File
@@ -1,4 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
<<: !include common.yaml
emontx: !include common.yaml
@@ -0,0 +1,73 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
emontx: !include common.yaml
# Validate that each sensor type gets the correct default state_class,
# unit_of_measurement, device_class, and accuracy_decimals when NO overrides
# are provided. The values are intentionally omitted so apply_tag_defaults is
# exercised, not the user-override path.
sensor:
# Energy sensor (E prefix): expects state_class=total_increasing, unit=Wh,
# device_class=energy, accuracy_decimals=0
- platform: emontx
tag_name: E1
name: Energy 1
emontx_id: test_emontx
# Power sensor (P prefix): expects state_class=measurement, unit=W,
# device_class=power, accuracy_decimals=0
- platform: emontx
tag_name: P1
name: Power 1
emontx_id: test_emontx
# Voltage sensor (V prefix): expects state_class=measurement, unit=V,
# device_class=voltage, accuracy_decimals=2
- platform: emontx
tag_name: V1
name: Voltage 1
emontx_id: test_emontx
# Current sensor (I prefix): expects state_class=measurement, unit=A,
# device_class=current, accuracy_decimals=2
- platform: emontx
tag_name: I1
name: Current 1
emontx_id: test_emontx
# Temperature sensor (T prefix): expects state_class=measurement, unit=°C,
# device_class=temperature, accuracy_decimals=2
- platform: emontx
tag_name: T1
name: Temperature 1
emontx_id: test_emontx
# Pulse sensor (PULSE pattern): expects state_class=total_increasing,
# unit=pulses, device_class=energy, accuracy_decimals=0
- platform: emontx
tag_name: PULSE1
name: Pulse 1
emontx_id: test_emontx
# Power factor sensor (PF pattern): expects state_class=measurement,
# device_class=power_factor, accuracy_decimals=2
- platform: emontx
tag_name: PF1
name: Power Factor 1
emontx_id: test_emontx
# Unknown tag: no prefix match, falls back to state_class=measurement,
# accuracy_decimals=0
- platform: emontx
tag_name: CUSTOM1
name: Custom sensor
emontx_id: test_emontx
# User override: verify that explicit values are respected and not clobbered
- platform: emontx
tag_name: E2
name: Energy 2 (user override)
emontx_id: test_emontx
state_class: measurement
accuracy_decimals: 3
@@ -6,3 +6,6 @@ update:
type: embedded
path: $component_dir/test_firmware.bin
sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31
on_update_available:
then:
- logger.log: "Coprocessor update available"
@@ -8,3 +8,6 @@ update:
type: http
source: https://esphome.github.io/esp-hosted-firmware/manifest/esp32c6.json
update_interval: 6h
on_update_available:
then:
- logger.log: "Coprocessor update available"
@@ -1,7 +1,67 @@
#include <array>
#include <utility>
#include "../common.h"
namespace esphome::mitsubishi_cn105::testing {
TEST(MitsubishiCN105ClimateTests, CelsiusTemperatureMappingAndTraitsMatchExpectedValues) {
TestableMitsubishiCN105Climate sut;
const auto mapping = TemperatureMapping();
for (int temperature = 16; temperature <= 31; ++temperature) {
EXPECT_EQ(mapping.to_mitsubishi(temperature), temperature);
EXPECT_EQ(mapping.from_mitsubishi(temperature), temperature);
}
const auto traits = sut.traits();
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::CELSIUS);
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 16.0f);
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 31.0f);
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f);
EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 0.5f);
}
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingAndTraitsMatchExpectedValues) {
TestableMitsubishiCN105Climate sut;
auto mapping = TemperatureMapping();
mapping.set_use_fahrenheit(true);
sut.set_use_fahrenheit(true);
const std::array cases{
std::pair{61, 16.0f}, std::pair{62, 16.5f}, std::pair{63, 17.0f}, std::pair{64, 17.5f}, std::pair{65, 18.0f},
std::pair{66, 18.5f}, std::pair{67, 19.0f}, std::pair{68, 20.0f}, std::pair{69, 21.0f}, std::pair{70, 21.5f},
std::pair{71, 22.0f}, std::pair{72, 22.5f}, std::pair{73, 23.0f}, std::pair{74, 23.5f}, std::pair{75, 24.0f},
std::pair{76, 24.5f}, std::pair{77, 25.0f}, std::pair{78, 25.5f}, std::pair{79, 26.0f}, std::pair{80, 26.5f},
std::pair{81, 27.0f}, std::pair{82, 27.5f}, std::pair{83, 28.0f}, std::pair{84, 28.5f}, std::pair{85, 29.0f},
std::pair{86, 29.5f}, std::pair{87, 30.0f}, std::pair{88, 30.5f},
};
for (const auto &[fahrenheit, mitsubishi_celsius] : cases) {
EXPECT_FLOAT_EQ(mapping.to_mitsubishi(fahrenheit), mitsubishi_celsius);
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(mitsubishi_celsius), fahrenheit);
}
const auto traits = sut.traits();
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT);
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 61.0f);
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 88.0f);
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f);
EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 1.0f);
}
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingUsesLinearConversionOutsideSetpointRange) {
auto mapping = TemperatureMapping();
mapping.set_use_fahrenheit(true);
const std::array cases{
std::pair{0.0f, 32.0f}, std::pair{10.0f, 50.0f}, std::pair{15.5f, 59.9f},
std::pair{31.0f, 87.8f}, std::pair{35.0f, 95.0f}, std::pair{40.0f, 104.0f},
};
for (const auto &[celsius, fahrenheit] : cases) {
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(celsius), fahrenheit);
}
}
TEST(MitsubishiCN105ClimateTests, SupportedSwingModeOffLeavesTraitsEmpty) {
TestableMitsubishiCN105Climate sut;
@@ -73,6 +73,7 @@ class TestableMitsubishiCN105Climate : public MitsubishiCN105Climate {
using MitsubishiCN105Climate::last_non_swing_wide_vane_mode_;
MitsubishiCN105::Status &status() { return const_cast<MitsubishiCN105::Status &>(this->component_.status()); }
void set_use_fahrenheit(bool value) { this->component_.set_use_fahrenheit(value); }
protected:
MitsubishiCN105Component component_;
@@ -3,6 +3,7 @@ mitsubishi_cn105:
uart_id: uart_bus
update_interval: 30s
telemetry_request_min_interval: 120s
use_fahrenheit: true
vane:
on_state:
- logger.log:
@@ -0,0 +1,7 @@
remote_transmitter:
id: xmitr
pin: GPIO26
carrier_duty_percent: 50%
packages:
buttons: !include common-buttons.yaml
@@ -0,0 +1,7 @@
remote_transmitter:
id: xmitr
pin: GPIO12
carrier_duty_percent: 50%
packages:
buttons: !include common-buttons.yaml
@@ -0,0 +1,15 @@
from esphome.components.runtime_image import enable_format
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# to_code is suppressed in cpptest builds; formats are normally enabled by
# process_runtime_image_config(). Enable all formats so the format-switch
# tests have two decoder types and every retained decoder is under test.
async def to_code_testing(config: ConfigType) -> None:
enable_format("BMP")
enable_format("PNG")
enable_format("JPEG")
manifest.to_code = to_code_testing
@@ -0,0 +1,336 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cstring>
#include <vector>
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
namespace esphome::runtime_image::testing {
// 3x2 24bpp BMP, every pixel a unique color (rows padded to 4 bytes)
static const uint8_t BMP_24BPP[] = {
0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x22, 0x11, 0x77, 0x88, 0x99, 0xEF, 0xCD, 0xAB, 0x00,
0x00, 0x00, 0x20, 0x10, 0xE0, 0x40, 0xC0, 0x30, 0xA0, 0x60, 0x50, 0x00, 0x00, 0x00,
};
static const uint8_t BMP_24BPP_EXPECTED[2][3][3] = {
{{0xE0, 0x10, 0x20}, {0x30, 0xC0, 0x40}, {0x50, 0x60, 0xA0}},
{{0x11, 0x22, 0x33}, {0x99, 0x88, 0x77}, {0xAB, 0xCD, 0xEF}},
};
// 3x2 8bpp BMP with a 4-entry color table
static const uint8_t BMP_8BPP[] = {
0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x20, 0x10, 0x00, 0xD0, 0xE0, 0xF0, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00,
};
static const uint8_t BMP_8BPP_EXPECTED[2][3][3] = {
{{0x10, 0x20, 0x30}, {0xF0, 0xE0, 0xD0}, {0x00, 0xFF, 0x00}},
{{0xFF, 0x00, 0xFF}, {0x00, 0xFF, 0x00}, {0xF0, 0xE0, 0xD0}},
};
// 3x2 8bpp BMP with an 8-entry color table, all colors distinct from BMP_8BPP's
static const uint8_t BMP_8BPP_BIG[] = {
0x42, 0x4D, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x03,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x18, 0x08,
0x00, 0xA8, 0xB8, 0xC8, 0x00, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x55, 0x99,
0x11, 0x00, 0xCC, 0x00, 0x66, 0x00, 0x44, 0x22, 0xEE, 0x00, 0x01, 0x06, 0x04, 0x00, 0x07, 0x05, 0x03, 0x00,
};
static const uint8_t BMP_8BPP_BIG_EXPECTED[2][3][3] = {
{{0xEE, 0x22, 0x44}, {0x11, 0x99, 0x55}, {0x80, 0xFF, 0x00}},
{{0xC8, 0xB8, 0xA8}, {0x66, 0x00, 0xCC}, {0xFF, 0x80, 0x00}},
};
// 4x4 RGB PNG, every pixel a unique color
static const uint8_t PNG_RGB[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x04, 0x08, 0x02, 0x00, 0x00, 0x00, 0x26, 0x93, 0x09, 0x29, 0x00, 0x00, 0x00, 0x38, 0x49,
0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x60, 0x64, 0x62, 0x16, 0x50, 0x30, 0x58, 0xB0, 0xE1, 0xC0, 0xFF, 0xFF, 0x0C,
0x0C, 0x0E, 0x0C, 0x50, 0xEC, 0xE0, 0xE0, 0xC0, 0x50, 0xCF, 0xF0, 0x9F, 0xA1, 0xFE, 0xFF, 0xFF, 0x7A, 0x86, 0xFA,
0xFF, 0x0C, 0x0C, 0x42, 0x26, 0x61, 0xA9, 0xCE, 0x8A, 0xFF, 0xEE, 0xEC, 0x5A, 0x7D, 0xF6, 0x3D, 0x00, 0x81, 0xCB,
0x12, 0x4D, 0xB3, 0xFB, 0xD4, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};
static const uint8_t PNG_RGB_EXPECTED[4][4][3] = {
{{0x01, 0x02, 0x03}, {0x10, 0x20, 0x30}, {0xA0, 0xB0, 0xC0}, {0xFF, 0xFF, 0x00}},
{{0x40, 0x00, 0x00}, {0x00, 0x40, 0x00}, {0x00, 0x00, 0x40}, {0x40, 0x40, 0x40}},
{{0x7F, 0x00, 0xFF}, {0x00, 0x7F, 0xFF}, {0xFF, 0x7F, 0x00}, {0x7F, 0xFF, 0x00}},
{{0x12, 0x34, 0x56}, {0x65, 0x43, 0x21}, {0xFE, 0xDC, 0xBA}, {0xAB, 0xCD, 0xEF}},
};
/// Exposes the protected decoder machinery so reuse and eviction can be observed directly.
class TestableRuntimeImage : public RuntimeImage {
public:
explicit TestableRuntimeImage(ImageFormat format)
: RuntimeImage(format, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, nullptr, false, 0, 0) {}
ImageDecoder *decoder() { return this->decoder_.get(); }
/// Simulates the state a dynamic-format producer (PR #16337) would leave behind:
/// a cached decoder whose format no longer matches the image's format.
/// TODO: once #16337 adds a public way to change the format, drive the mismatch
/// through it and delete this seam.
void plant_decoder(ImageFormat format) { this->decoder_ = this->create_decoder_(format); }
};
/// Runs one full decode session. Returns true when every stage succeeded.
static bool decode_all(TestableRuntimeImage &img, const uint8_t *data, size_t len) {
std::vector<uint8_t> buffer(data, data + len); // feed_data needs mutable bytes
if (!img.begin_decode(len)) {
return false;
}
size_t offset = 0;
while (offset < len) {
int consumed = img.feed_data(buffer.data() + offset, len - offset);
if (consumed <= 0) {
return false; // decode error, or no progress despite full data
}
offset += consumed;
}
return img.end_decode();
}
/// Feeds the image the way online_image's download loop does: append a small
/// chunk to a window, feed the window, drop what was consumed, repeat. A zero
/// return mid-stream means "need more data" and grows the window.
static bool decode_chunked(TestableRuntimeImage &img, const uint8_t *data, size_t len, size_t chunk_size) {
if (!img.begin_decode(len)) {
return false;
}
std::vector<uint8_t> window;
size_t supplied = 0;
while (supplied < len || !window.empty()) {
if (supplied < len) {
size_t take = std::min(chunk_size, len - supplied);
window.insert(window.end(), data + supplied, data + supplied + take);
supplied += take;
}
int consumed = img.feed_data(window.data(), window.size());
if (consumed < 0 || (consumed == 0 && supplied >= len)) {
return false; // decode error, or stuck with all data supplied
}
window.erase(window.begin(), window.begin() + consumed);
}
return img.end_decode();
}
template<size_t H, size_t W> static void expect_pixels(TestableRuntimeImage &img, const uint8_t (&expected)[H][W][3]) {
ASSERT_EQ(img.get_width(), static_cast<int>(W));
ASSERT_EQ(img.get_height(), static_cast<int>(H));
for (size_t y = 0; y < H; y++) {
for (size_t x = 0; x < W; x++) {
SCOPED_TRACE(::testing::Message() << "pixel (" << x << "," << y << ")");
Color color = img.get_pixel(x, y);
EXPECT_THAT((std::array<uint8_t, 3>{color.r, color.g, color.b}), ::testing::ElementsAreArray(expected[y][x]));
}
}
}
TEST(RuntimeImageDecoder, DecoderStaysWarmAcrossDecodes) {
TestableRuntimeImage img(BMP);
ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP)));
expect_pixels(img, BMP_24BPP_EXPECTED);
ImageDecoder *first = img.decoder();
ASSERT_NE(first, nullptr);
ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP)));
expect_pixels(img, BMP_24BPP_EXPECTED);
EXPECT_EQ(img.decoder(), first) << "decoder must be reused, not reallocated";
}
TEST(RuntimeImageDecoder, SecondDecodeStartsClean) {
TestableRuntimeImage img(BMP);
// Palettized decode, then a 24bpp decode, then palettized again, all on the
// same decoder: each session must produce correct pixels for its own image.
ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP)));
expect_pixels(img, BMP_8BPP_EXPECTED);
ImageDecoder *first = img.decoder();
ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP)));
expect_pixels(img, BMP_24BPP_EXPECTED);
EXPECT_EQ(img.decoder(), first);
ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP)));
expect_pixels(img, BMP_8BPP_EXPECTED);
EXPECT_EQ(img.decoder(), first);
}
TEST(RuntimeImageDecoder, ColorTableGrowsAndShrinksAcrossReuse) {
TestableRuntimeImage img(BMP);
// Small palette first: the retained table is allocated at 4 entries.
ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP)));
expect_pixels(img, BMP_8BPP_EXPECTED);
ImageDecoder *first = img.decoder();
// Growing to 8 entries on the reused decoder must reallocate, not overflow.
ASSERT_TRUE(decode_all(img, BMP_8BPP_BIG, sizeof(BMP_8BPP_BIG)));
expect_pixels(img, BMP_8BPP_BIG_EXPECTED);
EXPECT_EQ(img.decoder(), first);
// Shrinking back must not surface stale colors from the larger table.
ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP)));
expect_pixels(img, BMP_8BPP_EXPECTED);
EXPECT_EQ(img.decoder(), first);
}
TEST(RuntimeImageDecoder, ChunkedFeedDecodesLikeDownloadLoop) {
TestableRuntimeImage img(BMP);
ASSERT_TRUE(decode_chunked(img, BMP_24BPP, sizeof(BMP_24BPP), 16));
expect_pixels(img, BMP_24BPP_EXPECTED);
ImageDecoder *first = img.decoder();
// Chunked again on the warm decoder: the cross-call resume state
// (current_index_ / paint_index_) must have been fully reset.
ASSERT_TRUE(decode_chunked(img, BMP_24BPP, sizeof(BMP_24BPP), 16));
expect_pixels(img, BMP_24BPP_EXPECTED);
EXPECT_EQ(img.decoder(), first);
}
TEST(RuntimeImageDecoder, FormatSwitchEvictsMismatchedDecoder) {
// PNG image holding a stale BMP decoder: begin_decode must evict and recreate.
TestableRuntimeImage png_img(PNG);
png_img.plant_decoder(BMP);
ASSERT_NE(png_img.decoder(), nullptr);
ASSERT_EQ(png_img.decoder()->get_format(), BMP);
ASSERT_TRUE(decode_all(png_img, PNG_RGB, sizeof(PNG_RGB)));
EXPECT_EQ(png_img.decoder()->get_format(), PNG);
expect_pixels(png_img, PNG_RGB_EXPECTED);
// And the other direction: BMP image holding a stale PNG decoder.
TestableRuntimeImage bmp_img(BMP);
bmp_img.plant_decoder(PNG);
ASSERT_NE(bmp_img.decoder(), nullptr);
ASSERT_EQ(bmp_img.decoder()->get_format(), PNG);
ASSERT_TRUE(decode_all(bmp_img, BMP_24BPP, sizeof(BMP_24BPP)));
EXPECT_EQ(bmp_img.decoder()->get_format(), BMP);
expect_pixels(bmp_img, BMP_24BPP_EXPECTED);
}
TEST(RuntimeImageDecoder, ReleaseKeepsDecoderWarm) {
TestableRuntimeImage img(PNG);
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB)));
ImageDecoder *first = img.decoder();
ASSERT_NE(first, nullptr);
img.release();
EXPECT_EQ(img.decoder(), first) << "release() must keep the decoder for reuse";
EXPECT_FALSE(img.is_decoding());
EXPECT_EQ(img.get_width(), 0);
EXPECT_EQ(img.get_height(), 0);
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB)));
expect_pixels(img, PNG_RGB_EXPECTED);
EXPECT_EQ(img.decoder(), first);
}
TEST(RuntimeImageDecoder, FailedDecodeRecovers) {
TestableRuntimeImage img(BMP);
uint8_t garbage[32];
memset(garbage, 'X', sizeof(garbage));
ASSERT_TRUE(img.begin_decode(sizeof(garbage)));
EXPECT_LT(img.feed_data(garbage, sizeof(garbage)), 0) << "garbage must fail to decode";
img.release();
ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP)));
expect_pixels(img, BMP_24BPP_EXPECTED);
}
#ifdef USE_RUNTIME_IMAGE_JPEG
// 8x8 gradient JPEG (quality 90). JPEG is lossy, so the test asserts that a
// reused decoder reproduces the exact same pixels, not absolute colors.
static const uint8_t JPEG_GRADIENT[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03,
0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A,
0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15,
0x15, 0x15, 0x0C, 0x0F, 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x03,
0x04, 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0D, 0x0B, 0x0D, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62,
0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A,
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85,
0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6,
0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00,
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09,
0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A,
0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xE5, 0x3E, 0x0B, 0xFE,
0xC8, 0x7F, 0xEA, 0x3F, 0xD0, 0xBD, 0x3F, 0x86, 0x8A, 0x28, 0xAA, 0xC2, 0x62, 0x6A, 0xFB, 0x25, 0xA9, 0xD5, 0xC0,
0x7C, 0x6B, 0x9D, 0x7F, 0x62, 0xD3, 0xFD, 0xEF, 0xF5, 0xF7, 0x9F, 0xFF, 0xD9,
};
static std::vector<uint8_t> pixel_bytes(TestableRuntimeImage &img) {
const uint8_t *start = img.get_data_start();
return std::vector<uint8_t>(start, start + img.get_width_stride() * img.get_height());
}
TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
TestableRuntimeImage img(JPEG);
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_EQ(img.get_width(), 8);
ASSERT_EQ(img.get_height(), 8);
std::vector<uint8_t> first_pixels = pixel_bytes(img);
ImageDecoder *first = img.decoder();
ASSERT_NE(first, nullptr);
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
EXPECT_EQ(img.decoder(), first);
EXPECT_EQ(pixel_bytes(img), first_pixels) << "reused decoder must reproduce identical pixels";
}
#endif // USE_RUNTIME_IMAGE_JPEG
TEST(RuntimeImageDecoder, SessionFlagsTrackLifecycle) {
TestableRuntimeImage img(BMP);
std::vector<uint8_t> buffer(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP));
ASSERT_TRUE(img.begin_decode(buffer.size()));
EXPECT_TRUE(img.is_decoding());
EXPECT_FALSE(img.is_decode_finished());
ASSERT_EQ(img.feed_data(buffer.data(), buffer.size()), static_cast<int>(buffer.size()));
EXPECT_TRUE(img.is_decode_finished()) << "all pixel data consumed";
ASSERT_TRUE(img.end_decode());
EXPECT_FALSE(img.is_decoding()) << "end_decode() must close the session";
EXPECT_FALSE(img.is_decode_finished()) << "no session means nothing is 'finished'";
}
} // namespace esphome::runtime_image::testing
+1 -1
View File
@@ -1,6 +1,6 @@
# `sendspin.switch` action enables the controller role, so we use a standalone test
packages:
base: !include common.yaml
sendspin: !include common.yaml
wifi:
on_connect:
@@ -0,0 +1,5 @@
packages:
sendspin_hub: !include common-hub.yaml
ethernet:
type: OPENETH
@@ -0,0 +1,6 @@
psram:
mode: quad
sendspin:
id: sendspin_hub_id
task_stack_in_psram: true
@@ -1,4 +1,5 @@
<<: !include common.yaml
packages:
sendspin: !include common.yaml
media_player:
- platform: sendspin
@@ -1,4 +1,5 @@
<<: !include common.yaml
packages:
sendspin: !include common.yaml
media_source:
- platform: sendspin
+2 -1
View File
@@ -1,4 +1,5 @@
<<: !include common.yaml
packages:
sendspin: !include common.yaml
sensor:
- platform: sendspin
@@ -1,4 +1,5 @@
<<: !include common.yaml
packages:
sendspin: !include common.yaml
text_sensor:
- platform: sendspin
+3 -7
View File
@@ -1,9 +1,5 @@
packages:
sendspin_hub: !include common-hub.yaml
wifi:
ap:
psram:
mode: quad
sendspin:
id: sendspin_hub_id
task_stack_in_psram: true
@@ -1 +1,2 @@
<<: !include common-action.yaml
packages:
sendspin: !include common-action.yaml
@@ -1,9 +1,2 @@
ethernet:
type: OPENETH
psram:
mode: quad
sendspin:
id: sendspin_hub_id
task_stack_in_psram: true
packages:
sendspin: !include common-ethernet.yaml
@@ -1 +1,2 @@
<<: !include common-media_player.yaml
packages:
sendspin: !include common-media_player.yaml
@@ -1 +1,2 @@
<<: !include common-media_source.yaml
packages:
sendspin: !include common-media_source.yaml
@@ -1 +1,2 @@
<<: !include common-sensor.yaml
packages:
sendspin: !include common-sensor.yaml
@@ -1 +1,2 @@
<<: !include common-text_sensor.yaml
packages:
sendspin: !include common-text_sensor.yaml
@@ -1 +1,2 @@
<<: !include common.yaml
packages:
sendspin: !include common.yaml