Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-24 15:58:32 -05:00
22 changed files with 495 additions and 157 deletions
@@ -13,17 +13,18 @@ namespace api {
class APIConnection;
} // namespace api
namespace uart {
enum class UARTFlushResult : uint8_t {
UART_FLUSH_RESULT_SUCCESS,
UART_FLUSH_RESULT_ASSUMED_SUCCESS,
UART_FLUSH_RESULT_TIMEOUT,
UART_FLUSH_RESULT_FAILED,
};
} // namespace uart
namespace serial_proxy {
enum class SerialProxyResult : uint8_t {
SERIAL_PROXY_RESULT_OK,
SERIAL_PROXY_RESULT_ASSUMED_SUCCESS,
SERIAL_PROXY_RESULT_PORT_IN_USE,
SERIAL_PROXY_RESULT_INVALID_ARGUMENT,
SERIAL_PROXY_RESULT_ERROR,
SERIAL_PROXY_RESULT_TIMEOUT,
SERIAL_PROXY_RESULT_NOT_SUPPORTED,
};
class SerialProxy {
public:
void set_instance_index(uint32_t index) { this->instance_index_ = index; }
@@ -31,13 +32,20 @@ class SerialProxy {
const char *get_name() const { return ""; }
api::enums::SerialProxyPortType get_port_type() const { return {}; }
api::APIConnection *get_api_connection() { return nullptr; }
void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {}
void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
uint32_t stop_bits, uint32_t data_size) {}
SerialProxyResult serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
SerialProxyResult configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
uint8_t stop_bits, uint8_t data_size) {
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {}
void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {}
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
uint32_t get_modem_pins() const { return 0; }
uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; }
uint32_t get_configured_modem_pins() const { return 0; }
SerialProxyResult flush_port(api::APIConnection *api_connection) { return SerialProxyResult::SERIAL_PROXY_RESULT_OK; }
protected:
uint32_t instance_index_{0};
@@ -15,7 +15,9 @@ namespace zwave_proxy {
class ZWaveProxy {
public:
api::APIConnection *get_api_connection() { return nullptr; }
void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {}
api::enums::ZWaveProxyStatus zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {
return api::enums::ZWAVE_PROXY_STATUS_OK;
}
void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length) {}
void api_connection_authenticated(api::APIConnection *conn) {}
uint32_t get_feature_flags() const { return 0; }
@@ -0,0 +1,61 @@
#include <gtest/gtest.h>
#include <optional>
#include "esphome/components/runtime_image/runtime_image.h"
namespace esphome::runtime_image::testing {
TEST(RuntimeImageMime, FormatForKnownMimeTypes) {
EXPECT_EQ(get_format_for_mime_type("image/bmp"), BMP);
EXPECT_EQ(get_format_for_mime_type("image/x-ms-bmp"), BMP);
EXPECT_EQ(get_format_for_mime_type("image/x-bmp"), BMP);
EXPECT_EQ(get_format_for_mime_type("image/png"), PNG);
EXPECT_EQ(get_format_for_mime_type("image/x-png"), PNG);
#ifdef USE_RUNTIME_IMAGE_JPEG
EXPECT_EQ(get_format_for_mime_type("image/jpeg"), JPEG);
EXPECT_EQ(get_format_for_mime_type("image/jpg"), JPEG);
#endif // USE_RUNTIME_IMAGE_JPEG
}
TEST(RuntimeImageMime, FormatMatchingIsCaseInsensitive) {
EXPECT_EQ(get_format_for_mime_type("Image/PNG"), PNG);
EXPECT_EQ(get_format_for_mime_type("IMAGE/BMP"), BMP);
}
TEST(RuntimeImageMime, FormatMatchesContentTypeWithParameters) {
// Content-Type headers may carry parameters after the media type
EXPECT_EQ(get_format_for_mime_type("image/png; charset=binary"), PNG);
EXPECT_EQ(get_format_for_mime_type("image/bmp;name=\"a.bmp\""), BMP);
}
TEST(RuntimeImageMime, UnknownMimeTypeHasNoFormat) {
EXPECT_EQ(get_format_for_mime_type("text/html"), std::nullopt);
EXPECT_EQ(get_format_for_mime_type("application/octet-stream"), std::nullopt);
EXPECT_EQ(get_format_for_mime_type("image/*"), std::nullopt);
EXPECT_EQ(get_format_for_mime_type(""), std::nullopt);
EXPECT_EQ(get_format_for_mime_type(nullptr), std::nullopt);
}
TEST(RuntimeImageMime, MimeTypeForFormatRoundTrip) {
EXPECT_STREQ(get_mime_type_for_format(BMP), "image/bmp");
EXPECT_STREQ(get_mime_type_for_format(PNG), "image/png");
#ifdef USE_RUNTIME_IMAGE_JPEG
EXPECT_STREQ(get_mime_type_for_format(JPEG), "image/jpeg");
#endif // USE_RUNTIME_IMAGE_JPEG
// AUTO has no single MIME type and falls back to the wildcard
EXPECT_STREQ(get_mime_type_for_format(AUTO), "image/*");
// Every decodable format must resolve back to itself through its MIME type
for (ImageFormat format : {
BMP,
PNG,
#ifdef USE_RUNTIME_IMAGE_JPEG
JPEG,
#endif // USE_RUNTIME_IMAGE_JPEG
}) {
EXPECT_EQ(get_format_for_mime_type(get_mime_type_for_format(format)), format) << format;
}
}
} // namespace esphome::runtime_image::testing
@@ -263,6 +263,17 @@ def test_device_capabilities_response_has_id_150() -> None:
)
def test_z_wave_proxy_request_response_has_id_151() -> None:
body = _extract_proto_message(PROTO_TEXT, "ZWaveProxyRequestResponse")
match = re.search(r"option \(id\) = (\d+);", body)
assert match is not None, "ZWaveProxyRequestResponse is missing `option (id)`"
assert int(match.group(1)) == 151, (
f"ZWaveProxyRequestResponse has id {match.group(1)}, expected 151. "
"Message ids are part of the wire protocol and must not change once "
"assigned."
)
def test_superseded_fields_are_not_marked_deprecated_in_proto() -> None:
"""The six superseded fields must not carry `[deprecated = true]` in
api.proto, or the generator drops them and old clients stop receiving
+49
View File
@@ -299,10 +299,13 @@ def test_run_compile_restamps_cmakecache_after_discovery(setup_core: Path) -> No
_setup_build(setup_core)
config = {CONF_ESPHOME: {}}
cmakecache = CORE.relative_build_path("build/CMakeCache.txt")
build_ninja = CORE.relative_build_path("build/build.ninja")
cmakecache.parent.mkdir(parents=True, exist_ok=True)
cmakecache.write_text("")
build_ninja.write_text("")
old = cmakecache.stat().st_mtime - 100
os.utime(cmakecache, (old, old))
os.utime(build_ninja, (old, old))
with (
patch.object(toolchain, "need_reconfigure", return_value=True),
@@ -314,6 +317,8 @@ def test_run_compile_restamps_cmakecache_after_discovery(setup_core: Path) -> No
assert toolchain.run_compile(config, verbose=False) == 0
assert cmakecache.stat().st_mtime > old
# build.ninja must not be older than the cache or ninja re-runs cmake
assert build_ninja.stat().st_mtime >= cmakecache.stat().st_mtime
def test_run_compile_discovery_without_cmakecache(setup_core: Path) -> None:
@@ -334,6 +339,50 @@ def test_run_compile_discovery_without_cmakecache(setup_core: Path) -> None:
assert not CORE.relative_build_path("build/CMakeCache.txt").exists()
def test_run_compile_reconfigures_after_full_write_outside_testing_mode(
setup_core: Path,
) -> None:
"""The full CMakeLists write is followed by a reconfigure (#18682); a
failure there stops the build and leaves the cache unstamped."""
_setup_build(setup_core)
config = {CONF_ESPHOME: {}}
cmakecache = CORE.relative_build_path("build/CMakeCache.txt")
cmakecache.parent.mkdir(parents=True, exist_ok=True)
cmakecache.write_text("")
old = cmakecache.stat().st_mtime - 100
os.utime(cmakecache, (old, old))
calls: list[tuple] = []
reconfigures = 0
def record_write(minimal: bool = False) -> None:
calls.append(("write_project", minimal))
def record_reconfigure() -> int:
nonlocal reconfigures
reconfigures += 1
calls.append(("run_reconfigure",))
return 1 if reconfigures == 2 else 0
with (
patch.object(toolchain, "need_reconfigure", return_value=True),
patch("esphome.build_gen.espidf.write_project", side_effect=record_write),
patch.object(toolchain, "run_reconfigure", side_effect=record_reconfigure),
patch.object(toolchain, "run_idf_py", return_value=0) as mock_build,
patch.object(toolchain, "print_summary"),
):
assert not CORE.testing_mode
assert toolchain.run_compile(config, verbose=False) == 1
assert calls == [
("write_project", True),
("run_reconfigure",),
("write_project", False),
("run_reconfigure",),
]
mock_build.assert_not_called()
assert cmakecache.stat().st_mtime == old
def test_run_compile_passes_compile_process_limit(setup_core: Path) -> None:
"""compile_process_limit is forwarded to run_idf_py as the job limit."""
_setup_build(setup_core)