From c4abc5476e11acfe3e81bbe4b3894bb881a34a4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jun 2026 16:22:08 -0500 Subject: [PATCH] [core] Remove deprecated std::string scheduler/timer overloads (#17111) --- esphome/core/component.cpp | 40 --- esphome/core/component.h | 78 ++---- esphome/core/scheduler.cpp | 17 -- esphome/core/scheduler.h | 16 +- .../scheduler_bulk_cleanup_component.cpp | 19 +- .../rapid_cancellation_component.cpp | 14 +- .../simultaneous_callbacks_component.cpp | 14 +- .../__init__.py | 21 -- .../string_lifetime_component.cpp | 260 ------------------ .../string_lifetime_component.h | 35 --- .../__init__.py | 21 -- .../string_name_stress_component.cpp | 108 -------- .../string_name_stress_component.h | 20 -- .../integration/fixtures/scheduler_pool.yaml | 12 +- .../fixtures/scheduler_string_lifetime.yaml | 48 ---- .../scheduler_string_name_stress.yaml | 39 --- .../fixtures/scheduler_string_test.yaml | 42 ++- .../test_scheduler_string_lifetime.py | 169 ------------ .../test_scheduler_string_name_stress.py | 116 -------- .../integration/test_scheduler_string_test.py | 2 +- 20 files changed, 74 insertions(+), 1017 deletions(-) delete mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py delete mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp delete mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h delete mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py delete mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp delete mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h delete mode 100644 tests/integration/fixtures/scheduler_string_lifetime.yaml delete mode 100644 tests/integration/fixtures/scheduler_string_name_stress.yaml delete mode 100644 tests/integration/test_scheduler_string_lifetime.py delete mode 100644 tests/integration/test_scheduler_string_name_stress.py diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 7ef5ff50a5..281d7aaecd 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,24 +85,10 @@ void Component::setup() {} void Component::loop() {} -void Component::set_interval(const std::string &name, uint32_t interval, std::function &&f) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_interval(this, name, interval, std::move(f)); -#pragma GCC diagnostic pop -} - void Component::set_interval(const char *name, uint32_t interval, std::function &&f) { // NOLINT App.scheduler.set_interval(this, name, interval, std::move(f)); } -bool Component::cancel_interval(const std::string &name) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return App.scheduler.cancel_interval(this, name); -#pragma GCC diagnostic pop -} - bool Component::cancel_interval(const char *name) { // NOLINT return App.scheduler.cancel_interval(this, name); } @@ -137,24 +123,10 @@ bool Component::cancel_retry(const char *name) { // NOLINT #pragma GCC diagnostic pop } -void Component::set_timeout(const std::string &name, uint32_t timeout, std::function &&f) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_timeout(this, name, timeout, std::move(f)); -#pragma GCC diagnostic pop -} - void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, timeout, std::move(f)); } -bool Component::cancel_timeout(const std::string &name) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return App.scheduler.cancel_timeout(this, name); -#pragma GCC diagnostic pop -} - bool Component::cancel_timeout(const char *name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } @@ -319,21 +291,9 @@ void Component::reset_to_construction_state() { void Component::defer(std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } -bool Component::cancel_defer(const std::string &name) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return App.scheduler.cancel_timeout(this, name); -#pragma GCC diagnostic pop -} bool Component::cancel_defer(const char *name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } -void Component::defer(const std::string &name, std::function &&f) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_timeout(this, name, 0, std::move(f)); -#pragma GCC diagnostic pop -} void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 299a5f72ea..1ae70371a1 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -357,9 +357,9 @@ class Component { /// so once a flag is set, subsequent (potentially different) messages may be suppressed. bool set_status_flag_(uint8_t flag); - /** Set an interval function with a unique name. Empty name means no cancelling possible. + /** Set an interval function with a const char* name. Empty name means no cancelling possible. * - * This will call f every interval ms. Can be cancelled via CancelInterval(). + * This will call f every interval ms. Can be cancelled via cancel_interval(). * Similar to javascript's setInterval(). * * IMPORTANT NOTE: @@ -372,18 +372,6 @@ class Component { * * Note also that the first call to f will not happen immediately, but after a random delay. This is * intended to prevent many interval functions from being called at the same time. - * - * @param name The identifier for this interval function. - * @param interval The interval in ms. - * @param f The function (or lambda) that should be called - * - * @see cancel_interval() - */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT - - /** Set an interval function with a const char* name. * * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. * This means the name should be: @@ -391,7 +379,7 @@ class Component { * - A static const char* variable * - A pointer with lifetime >= the scheduled task * - * For dynamic strings, use the std::string overload instead. + * For dynamic names, use the uint32_t id overload instead. * * @param name The identifier for this interval function (must have static lifetime) * @param interval The interval in ms @@ -416,12 +404,9 @@ class Component { * @param name The identifier for this interval function. * @return Whether an interval functions was deleted. */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - bool cancel_interval(const std::string &name); // NOLINT - bool cancel_interval(const char *name); // NOLINT - bool cancel_interval(uint32_t id); // NOLINT - bool cancel_interval(InternalSchedulerID id); // NOLINT + bool cancel_interval(const char *name); // NOLINT + bool cancel_interval(uint32_t id); // NOLINT + bool cancel_interval(InternalSchedulerID id); // NOLINT /// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0. // Remove before 2026.8.0 @@ -458,25 +443,13 @@ class Component { ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(uint32_t id); // NOLINT - /** Set a timeout function with a unique name. + /** Set a timeout function with a const char* name. * * Similar to javascript's setTimeout(). Empty name means no cancelling possible. * * IMPORTANT: Do not rely on this having correct timing. This is only called from - * loop() and therefore can be significantly delay. If you need exact timing please + * loop() and therefore can be significantly delayed. If you need exact timing please * use hardware timers. - * - * @param name The identifier for this timeout function. - * @param timeout The timeout in ms. - * @param f The function (or lambda) that should be called - * - * @see cancel_timeout() - */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT - - /** Set a timeout function with a const char* name. * * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. * This means the name should be: @@ -484,7 +457,9 @@ class Component { * - A static const char* variable * - A pointer with lifetime >= the timeout duration * - * For dynamic strings, use the std::string overload instead. + * For dynamic names, use the uint32_t id overload instead. + * + * @see cancel_timeout() * * @param name The identifier for this timeout function (must have static lifetime) * @param timeout The timeout in ms @@ -509,25 +484,13 @@ class Component { * @param name The identifier for this timeout function. * @return Whether a timeout functions was deleted. */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - bool cancel_timeout(const std::string &name); // NOLINT - bool cancel_timeout(const char *name); // NOLINT - bool cancel_timeout(uint32_t id); // NOLINT - bool cancel_timeout(InternalSchedulerID id); // NOLINT - - /** Defer a callback to the next loop() call. - * - * If name is specified and a defer() object with the same name exists, the old one is first removed. - * - * @param name The name of the defer function. - * @param f The callback. - */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") - void defer(const std::string &name, std::function &&f); // NOLINT + bool cancel_timeout(const char *name); // NOLINT + bool cancel_timeout(uint32_t id); // NOLINT + bool cancel_timeout(InternalSchedulerID id); // NOLINT /** Defer a callback to the next loop() call with a const char* name. + * + * If name is specified and a defer() object with the same name exists, the old one is first removed. * * IMPORTANT: The provided name pointer must remain valid for the lifetime of the deferred task. * This means the name should be: @@ -535,7 +498,7 @@ class Component { * - A static const char* variable * - A pointer with lifetime >= the deferred execution * - * For dynamic strings, use the std::string overload instead. + * For dynamic names, use the uint32_t id overload instead. * * @param name The name of the defer function (must have static lifetime) * @param f The callback @@ -549,11 +512,8 @@ class Component { void defer(uint32_t id, std::function &&f); // NOLINT /// Cancel a defer callback using the specified name, name must not be empty. - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") - bool cancel_defer(const std::string &name); // NOLINT - bool cancel_defer(const char *name); // NOLINT - bool cancel_defer(uint32_t id); // NOLINT + bool cancel_defer(const char *name); // NOLINT + bool cancel_defer(uint32_t id); // NOLINT void status_clear_warning_slow_path_(); void status_clear_error_slow_path_(); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 15bb9ea239..9c5557bdfc 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -254,30 +254,16 @@ void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t std::move(func)); } -void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function &&func) { - this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), - timeout, std::move(func)); -} void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout, std::move(func)); } -bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { - return this->cancel_item_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), SchedulerItem::TIMEOUT); -} bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::TIMEOUT); } bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) { return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::TIMEOUT); } -void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, - std::function &&func) { - this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), - interval, std::move(func)); -} - void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::STATIC_STRING, name, 0, interval, @@ -287,9 +273,6 @@ void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t int this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID, nullptr, id, interval, std::move(func)); } -bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { - return this->cancel_item_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), SchedulerItem::INTERVAL); -} bool HOT Scheduler::cancel_interval(Component *component, const char *name) { return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::INTERVAL); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 378c0fb94b..c7743e5b2a 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -31,11 +31,6 @@ class Scheduler { template friend class DelayAction; public: - // std::string overload - deprecated, use const char* or uint32_t instead - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function &&func); - /** Set a timeout with a const char* name. * * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. @@ -53,8 +48,6 @@ class Scheduler { static_cast(id), timeout, std::move(func)); } - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - bool cancel_timeout(Component *component, const std::string &name); bool cancel_timeout(Component *component, const char *name); bool cancel_timeout(Component *component, uint32_t id); bool cancel_timeout(Component *component, InternalSchedulerID id) { @@ -62,9 +55,6 @@ class Scheduler { SchedulerItem::TIMEOUT); } - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func); - /** Set an interval with a const char* name. * * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. @@ -82,8 +72,6 @@ class Scheduler { static_cast(id), interval, std::move(func)); } - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - bool cancel_interval(Component *component, const std::string &name); bool cancel_interval(Component *component, const char *name); bool cancel_interval(Component *component, uint32_t id); bool cancel_interval(Component *component, InternalSchedulerID id) { @@ -396,8 +384,8 @@ class Scheduler { inline bool HOT names_match_static_(const char *name1, const char *name2) const { // Check pointer equality first (common for static strings), then string contents // The core ESPHome codebase uses static strings (const char*) for component names, - // making pointer comparison effective. The std::string overloads exist only for - // compatibility with external components but are rarely used in practice. + // making pointer comparison effective. The strcmp fallback covers distinct pointers + // with identical content (e.g. names built into separate static buffers). return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0)); } diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp index f6fd1b1de7..d419694af7 100644 --- a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp @@ -8,14 +8,23 @@ static const char *const TAG = "bulk_cleanup"; void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); } +// Static name tables keep the const char* pointers valid for the lifetime of the scheduled tasks. +static const char *const BULK_TIMEOUT_NAMES[25] = { + "bulk_timeout_0", "bulk_timeout_1", "bulk_timeout_2", "bulk_timeout_3", "bulk_timeout_4", + "bulk_timeout_5", "bulk_timeout_6", "bulk_timeout_7", "bulk_timeout_8", "bulk_timeout_9", + "bulk_timeout_10", "bulk_timeout_11", "bulk_timeout_12", "bulk_timeout_13", "bulk_timeout_14", + "bulk_timeout_15", "bulk_timeout_16", "bulk_timeout_17", "bulk_timeout_18", "bulk_timeout_19", + "bulk_timeout_20", "bulk_timeout_21", "bulk_timeout_22", "bulk_timeout_23", "bulk_timeout_24"}; +static const char *const POST_CLEANUP_NAMES[5] = {"post_cleanup_0", "post_cleanup_1", "post_cleanup_2", + "post_cleanup_3", "post_cleanup_4"}; + void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { ESP_LOGI(TAG, "Starting bulk cleanup test..."); // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10) ESP_LOGI(TAG, "Scheduling 25 timeouts..."); for (int i = 0; i < 25; i++) { - std::string name = "bulk_timeout_" + std::to_string(i); - App.scheduler.set_timeout(this, name, 2500, [i]() { + App.scheduler.set_timeout(this, BULK_TIMEOUT_NAMES[i], 2500, [i]() { // These should never execute as we'll cancel them ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i); }); @@ -24,8 +33,7 @@ void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { // Cancel all of them to mark for removal ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup..."); int cancelled_count = 0; - for (int i = 0; i < 25; i++) { - std::string name = "bulk_timeout_" + std::to_string(i); + for (const char *name : BULK_TIMEOUT_NAMES) { if (App.scheduler.cancel_timeout(this, name)) { cancelled_count++; } @@ -56,8 +64,7 @@ void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup static int post_cleanup_count = 0; for (int i = 0; i < 5; i++) { - std::string name = "post_cleanup_" + std::to_string(i); - App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() { + App.scheduler.set_timeout(this, POST_CLEANUP_NAMES[i], 50 + i * 25, [i]() { ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); post_cleanup_count++; if (post_cleanup_count >= 5) { diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp index 0e5525d265..4971a15dbc 100644 --- a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp @@ -4,12 +4,18 @@ #include #include #include -#include namespace esphome::scheduler_rapid_cancellation_component { static const char *const TAG = "scheduler_rapid_cancellation"; +// Static name table keeps the const char* pointers valid for the lifetime of the scheduled tasks. +// Threads race over this fixed set of names; STATIC_STRING names match by content, so scheduling +// the same name replaces (implicitly cancels) the previous timeout, exactly as before. +static const char *const SHARED_TIMEOUT_NAMES[10] = { + "shared_timeout_0", "shared_timeout_1", "shared_timeout_2", "shared_timeout_3", "shared_timeout_4", + "shared_timeout_5", "shared_timeout_6", "shared_timeout_7", "shared_timeout_8", "shared_timeout_9"}; + void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); } void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { @@ -32,14 +38,12 @@ void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { // Use modulo to ensure multiple threads use the same names int name_index = i % NUM_NAMES; - std::stringstream ss; - ss << "shared_timeout_" << name_index; - std::string name = ss.str(); + const char *name = SHARED_TIMEOUT_NAMES[name_index]; // All threads schedule timeouts - this will implicitly cancel existing ones this->set_timeout(name, 150, [this, name]() { this->total_executed_.fetch_add(1); - ESP_LOGI(TAG, "Executed callback '%s'", name.c_str()); + ESP_LOGI(TAG, "Executed callback '%s'", name); }); this->total_scheduled_.fetch_add(1); diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp index a817b9f508..a3d135527f 100644 --- a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp @@ -1,9 +1,9 @@ #include "simultaneous_callbacks_component.h" #include "esphome/core/log.h" +#include #include #include #include -#include namespace esphome::scheduler_simultaneous_callbacks_component { @@ -41,13 +41,11 @@ void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() std::this_thread::sleep_until(start_time + std::chrono::microseconds(100)); for (int i = 0; i < CALLBACKS_PER_THREAD; i++) { - // Create unique name for each callback - std::stringstream ss; - ss << "thread_" << thread_id << "_cb_" << i; - std::string name = ss.str(); + // Unique numeric ID for each callback (zero heap allocation, no name collisions) + uint32_t callback_id = static_cast(thread_id) * CALLBACKS_PER_THREAD + i; // Schedule callback for exactly DELAY_MS from now - this->set_timeout(name, DELAY_MS, [this, name]() { + this->set_timeout(callback_id, DELAY_MS, [this, callback_id]() { // Increment concurrent counter atomically int current = this->callbacks_at_once_.fetch_add(1) + 1; @@ -57,7 +55,7 @@ void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() // Loop until we successfully update or someone else set a higher value } - ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current); + ESP_LOGV(TAG, "Callback executed: id=%" PRIu32 " (concurrent: %d)", callback_id, current); // Simulate some minimal work std::atomic work{0}; @@ -73,7 +71,7 @@ void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() }); this->total_scheduled_.fetch_add(1); - ESP_LOGV(TAG, "Scheduled callback %s", name.c_str()); + ESP_LOGV(TAG, "Scheduled callback id=%" PRIu32, callback_id); } ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id); diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py deleted file mode 100644 index 3f29a839ef..0000000000 --- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.const import CONF_ID - -scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace( - "scheduler_string_lifetime_component" -) -SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_( - "SchedulerStringLifetimeComponent", cg.Component -) - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent), - } -).extend(cv.COMPONENT_SCHEMA) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp deleted file mode 100644 index cc1b9f7814..0000000000 --- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "string_lifetime_component.h" -#include "esphome/core/log.h" -#include -#include -#include - -namespace esphome::scheduler_string_lifetime_component { - -static const char *const TAG = "scheduler_string_lifetime"; - -void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); } - -void SchedulerStringLifetimeComponent::run_string_lifetime_test() { - ESP_LOGI(TAG, "Starting string lifetime tests"); - - this->tests_passed_ = 0; - this->tests_failed_ = 0; - - // Run each test - test_temporary_string_lifetime(); - test_scope_exit_string(); - test_vector_reallocation(); - test_string_move_semantics(); - test_lambda_capture_lifetime(); -} - -void SchedulerStringLifetimeComponent::run_test1() { - test_temporary_string_lifetime(); - // Wait for all callbacks to execute - this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); }); -} - -void SchedulerStringLifetimeComponent::run_test2() { - test_scope_exit_string(); - // Wait for all callbacks to execute - this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); }); -} - -void SchedulerStringLifetimeComponent::run_test3() { - test_vector_reallocation(); - // Wait for all callbacks to execute - this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); }); -} - -void SchedulerStringLifetimeComponent::run_test4() { - test_string_move_semantics(); - // Wait for all callbacks to execute - this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); }); -} - -void SchedulerStringLifetimeComponent::run_test5() { - test_lambda_capture_lifetime(); - // Wait for all callbacks to execute - this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); }); -} - -void SchedulerStringLifetimeComponent::run_final_check() { - ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); - ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); - - if (this->tests_failed_ == 0) { - ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); - } else { - ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); - } - ESP_LOGI(TAG, "String lifetime tests complete"); -} - -void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() { - ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names"); - - // Test with a temporary string that goes out of scope immediately - { - std::string temp_name = "temp_callback_" + std::to_string(12345); - - // Schedule with temporary string name - scheduler must copy/store this - this->set_timeout(temp_name, 1, [this]() { - ESP_LOGD(TAG, "Callback for temp string name executed"); - this->tests_passed_++; - }); - - // String goes out of scope here, but scheduler should have made a copy - } - - // Test with rvalue string as name - this->set_timeout(std::string("rvalue_test"), 2, [this]() { - ESP_LOGD(TAG, "Rvalue string name callback executed"); - this->tests_passed_++; - }); - - // Test cancelling with reconstructed string - { - std::string cancel_name = "cancel_test_" + std::to_string(999); - this->set_timeout(cancel_name, 100, [this]() { - ESP_LOGE(TAG, "This should have been cancelled!"); - this->tests_failed_++; - }); - } // cancel_name goes out of scope - - // Reconstruct the same string to cancel - std::string cancel_name_2 = "cancel_test_" + std::to_string(999); - bool cancelled = this->cancel_timeout(cancel_name_2); - if (cancelled) { - ESP_LOGD(TAG, "Successfully cancelled with reconstructed string"); - this->tests_passed_++; - } else { - ESP_LOGE(TAG, "Failed to cancel with reconstructed string"); - this->tests_failed_++; - } -} - -void SchedulerStringLifetimeComponent::test_scope_exit_string() { - ESP_LOGI(TAG, "Test 2: Scope exit string names"); - - // Create string names in a limited scope - { - std::string scoped_name = "scoped_timeout_" + std::to_string(555); - - // Schedule with scoped string name - this->set_timeout(scoped_name, 3, [this]() { - ESP_LOGD(TAG, "Scoped name callback executed"); - this->tests_passed_++; - }); - - // scoped_name goes out of scope here - } - - // Test with dynamically allocated string name - { - auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777)); - - this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() { - ESP_LOGD(TAG, "Dynamic string name callback executed"); - this->tests_passed_++; - delete dynamic_name; // Clean up in callback - }); - - // Pointer goes out of scope but string object remains until callback - } - - // Test multiple timeouts with same dynamically created name - for (int i = 0; i < 3; i++) { - std::string loop_name = "loop_timeout_" + std::to_string(i); - this->set_timeout(loop_name, 5 + i * 1, [this, i]() { - ESP_LOGD(TAG, "Loop timeout %d executed", i); - this->tests_passed_++; - }); - // loop_name destroyed and recreated each iteration - } -} - -void SchedulerStringLifetimeComponent::test_vector_reallocation() { - ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names"); - - // Create a vector that will reallocate - std::vector names; - names.reserve(2); // Small initial capacity to force reallocation - - // Schedule callbacks with string names from vector - for (int i = 0; i < 10; i++) { - names.push_back("vector_cb_" + std::to_string(i)); - // Use the string from vector as timeout name - this->set_timeout(names.back(), 8 + i * 1, [this, i]() { - ESP_LOGV(TAG, "Vector name callback %d executed", i); - this->tests_passed_++; - }); - } - - // Force reallocation by adding more elements - // This will move all strings to new memory locations - for (int i = 10; i < 50; i++) { - names.push_back("realloc_trigger_" + std::to_string(i)); - } - - // Add more timeouts after reallocation to ensure old names still work - for (int i = 50; i < 55; i++) { - names.push_back("post_realloc_" + std::to_string(i)); - this->set_timeout(names.back(), 20 + (i - 50), [this]() { - ESP_LOGV(TAG, "Post-reallocation callback executed"); - this->tests_passed_++; - }); - } - - // Clear the vector while timeouts are still pending - names.clear(); - ESP_LOGD(TAG, "Vector cleared - all string names destroyed"); -} - -void SchedulerStringLifetimeComponent::test_string_move_semantics() { - ESP_LOGI(TAG, "Test 4: String move semantics for timeout names"); - - // Test moving string names - std::string original = "move_test_original"; - std::string moved = std::move(original); - - // Schedule with moved string as name - this->set_timeout(moved, 30, [this]() { - ESP_LOGD(TAG, "Moved string name callback executed"); - this->tests_passed_++; - }); - - // original is now empty, try to use it as a different timeout name - original = "reused_after_move"; - this->set_timeout(original, 32, [this]() { - ESP_LOGD(TAG, "Reused string name callback executed"); - this->tests_passed_++; - }); -} - -void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() { - ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios"); - - // Test scheduling with name built in lambda - [this]() { - std::string lambda_name = "lambda_built_name_" + std::to_string(888); - this->set_timeout(lambda_name, 38, [this]() { - ESP_LOGD(TAG, "Lambda-built name callback executed"); - this->tests_passed_++; - }); - }(); // Lambda executes and lambda_name is destroyed - - // Test with shared_ptr name - auto shared_name = std::make_shared("shared_ptr_timeout"); - this->set_timeout(*shared_name, 40, [this, shared_name]() { - ESP_LOGD(TAG, "Shared_ptr name callback executed"); - this->tests_passed_++; - }); - shared_name.reset(); // Release the shared_ptr - - // Test overwriting timeout with same name - std::string overwrite_name = "overwrite_test"; - this->set_timeout(overwrite_name, 1000, [this]() { - ESP_LOGE(TAG, "This should have been overwritten!"); - this->tests_failed_++; - }); - - // Overwrite with shorter timeout - this->set_timeout(overwrite_name, 42, [this]() { - ESP_LOGD(TAG, "Overwritten timeout executed"); - this->tests_passed_++; - }); - - // Test very long string name - std::string long_name; - for (int i = 0; i < 100; i++) { - long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_"; - } - this->set_timeout(long_name, 44, [this]() { - ESP_LOGD(TAG, "Very long name timeout executed"); - this->tests_passed_++; - }); - - // Test empty string as name - this->set_timeout("", 46, [this]() { - ESP_LOGD(TAG, "Empty string name timeout executed"); - this->tests_passed_++; - }); -} - -} // namespace esphome::scheduler_string_lifetime_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h deleted file mode 100644 index 20185f128d..0000000000 --- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include -#include - -namespace esphome::scheduler_string_lifetime_component { - -class SchedulerStringLifetimeComponent : public Component { - public: - void setup() override; - float get_setup_priority() const override { return setup_priority::LATE; } - - void run_string_lifetime_test(); - - // Individual test methods exposed as services - void run_test1(); - void run_test2(); - void run_test3(); - void run_test4(); - void run_test5(); - void run_final_check(); - - private: - void test_temporary_string_lifetime(); - void test_scope_exit_string(); - void test_vector_reallocation(); - void test_string_move_semantics(); - void test_lambda_capture_lifetime(); - - int tests_passed_{0}; - int tests_failed_{0}; -}; - -} // namespace esphome::scheduler_string_lifetime_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py deleted file mode 100644 index 6cc564395c..0000000000 --- a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.const import CONF_ID - -scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace( - "scheduler_string_name_stress_component" -) -SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_( - "SchedulerStringNameStressComponent", cg.Component -) - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent), - } -).extend(cv.COMPONENT_SCHEMA) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp deleted file mode 100644 index 677d371f25..0000000000 --- a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "string_name_stress_component.h" -#include "esphome/core/log.h" -#include -#include -#include -#include -#include -#include - -namespace esphome::scheduler_string_name_stress_component { - -static const char *const TAG = "scheduler_string_name_stress"; - -void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); } - -void SchedulerStringNameStressComponent::run_string_name_stress_test() { - // Use member variables to reset state - this->total_callbacks_ = 0; - this->executed_callbacks_ = 0; - static constexpr int NUM_THREADS = 10; - static constexpr int CALLBACKS_PER_THREAD = 100; - - ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names"); - ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management"); - - // Track start time - auto start_time = std::chrono::steady_clock::now(); - - // Create threads - std::vector threads; - - ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS, - CALLBACKS_PER_THREAD); - - threads.reserve(NUM_THREADS); - for (int i = 0; i < NUM_THREADS; i++) { - threads.emplace_back([this, i]() { - ESP_LOGV(TAG, "Thread %d starting", i); - - // Each thread schedules callbacks with dynamically created string names - for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { - int callback_id = this->total_callbacks_.fetch_add(1); - - // Create a dynamic string name - this will test memory management - std::stringstream ss; - ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id; - std::string dynamic_name = ss.str(); - - ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str()); - - // Capture necessary values for the lambda - auto *component = this; - - // Schedule with std::string name - this tests the string overload - // Use varying delays to stress the heap scheduler - uint32_t delay = 1 + (callback_id % 50); - - // Also test nested scheduling from callbacks - if (j % 10 == 0) { - // Every 10th callback schedules another callback - this->set_timeout(dynamic_name, delay, [component, callback_id]() { - component->executed_callbacks_.fetch_add(1); - ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id); - - // Schedule another timeout from within this callback with a new dynamic name - std::string nested_name = "nested_from_" + std::to_string(callback_id); - component->set_timeout(nested_name, 1, [callback_id]() { - ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id); - }); - }); - } else { - // Regular callback - this->set_timeout(dynamic_name, delay, [component, callback_id]() { - component->executed_callbacks_.fetch_add(1); - ESP_LOGV(TAG, "Executed string-named callback %d", callback_id); - }); - } - - // Add some timing variations to increase race conditions - if (j % 5 == 0) { - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } - } - ESP_LOGV(TAG, "Thread %d finished scheduling", i); - }); - } - - // Wait for all threads to complete scheduling - for (auto &t : threads) { - t.join(); - } - - auto end_time = std::chrono::steady_clock::now(); - auto thread_time = std::chrono::duration_cast(end_time - start_time).count(); - ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time, - this->total_callbacks_.load()); - - // Give some time for callbacks to execute - ESP_LOGI(TAG, "Waiting for callbacks to execute..."); - - // Schedule a final callback to signal completion - this->set_timeout("test_complete", 2000, [this]() { - ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(), - this->total_callbacks_.load()); - }); -} - -} // namespace esphome::scheduler_string_name_stress_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h deleted file mode 100644 index 121bda6204..0000000000 --- a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include - -namespace esphome::scheduler_string_name_stress_component { - -class SchedulerStringNameStressComponent : public Component { - public: - void setup() override; - float get_setup_priority() const override { return setup_priority::LATE; } - - void run_string_name_stress_test(); - - private: - std::atomic total_callbacks_{0}; - std::atomic executed_callbacks_{0}; -}; - -} // namespace esphome::scheduler_string_name_stress_component diff --git a/tests/integration/fixtures/scheduler_pool.yaml b/tests/integration/fixtures/scheduler_pool.yaml index 989c1535b0..a75d9dbcbc 100644 --- a/tests/integration/fixtures/scheduler_pool.yaml +++ b/tests/integration/fixtures/scheduler_pool.yaml @@ -156,9 +156,9 @@ script: // Simulate a burst of defer operations like ratgdo does with state updates // These should execute immediately and recycle quickly to the pool + // Phase-specific id range (0..9) so ids never collide with later phases for (int i = 0; i < 10; i++) { - std::string defer_name = "defer_" + std::to_string(i); - App.scheduler.set_timeout(component, defer_name, 0, [i]() { + App.scheduler.set_timeout(component, static_cast(i), 0, [i]() { ESP_LOGD("test", "Defer %d executed", i); // Force a small delay between defer executions to see recycling if (i == 5) { @@ -207,9 +207,9 @@ script: // Now create 8 new timeouts - they should reuse from pool when available int reuse_test_count = 8; + // Phase-specific id range (100..107) so ids never collide with other phases for (int i = 0; i < reuse_test_count; i++) { - std::string name = "reuse_test_" + std::to_string(i); - App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + App.scheduler.set_timeout(component, static_cast(100 + i), 10 + i * 5, [i]() { ESP_LOGD("test", "Reuse test %d completed", i); }); } @@ -229,9 +229,9 @@ script: auto *component = id(test_sensor); int full_reuse_count = 10; + // Phase-specific id range (200..209) so ids never collide with other phases for (int i = 0; i < full_reuse_count; i++) { - std::string name = "full_reuse_" + std::to_string(i); - App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + App.scheduler.set_timeout(component, static_cast(200 + i), 10 + i * 5, [i]() { ESP_LOGD("test", "Full reuse test %d completed", i); }); } diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml deleted file mode 100644 index 5ae5a1914e..0000000000 --- a/tests/integration/fixtures/scheduler_string_lifetime.yaml +++ /dev/null @@ -1,48 +0,0 @@ -esphome: - debug_scheduler: true # Enable scheduler leak detection - name: scheduler-string-lifetime-test - -external_components: - - source: - type: local - path: EXTERNAL_COMPONENT_PATH - components: [scheduler_string_lifetime_component] - -host: - -logger: - level: DEBUG - -scheduler_string_lifetime_component: - id: string_lifetime - -api: - services: - - service: run_string_lifetime_test - then: - - lambda: |- - id(string_lifetime)->run_string_lifetime_test(); - - service: run_test1 - then: - - lambda: |- - id(string_lifetime)->run_test1(); - - service: run_test2 - then: - - lambda: |- - id(string_lifetime)->run_test2(); - - service: run_test3 - then: - - lambda: |- - id(string_lifetime)->run_test3(); - - service: run_test4 - then: - - lambda: |- - id(string_lifetime)->run_test4(); - - service: run_test5 - then: - - lambda: |- - id(string_lifetime)->run_test5(); - - service: run_final_check - then: - - lambda: |- - id(string_lifetime)->run_final_check(); diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml deleted file mode 100644 index 8f68d1d102..0000000000 --- a/tests/integration/fixtures/scheduler_string_name_stress.yaml +++ /dev/null @@ -1,39 +0,0 @@ -esphome: - debug_scheduler: true # Enable scheduler leak detection - name: sched-string-name-stress - -external_components: - - source: - type: local - path: EXTERNAL_COMPONENT_PATH - components: [scheduler_string_name_stress_component] - -host: - -logger: - level: VERBOSE - -scheduler_string_name_stress_component: - id: string_stress - -api: - services: - - service: run_string_name_stress_test - then: - - lambda: |- - id(string_stress)->run_string_name_stress_test(); - -event: - - platform: template - name: "Test Complete" - id: test_complete - device_class: button - event_types: - - "test_finished" - - platform: template - name: "Test Result" - id: test_result - device_class: button - event_types: - - "passed" - - "failed" diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml index c53ec392df..06e3a4c97c 100644 --- a/tests/integration/fixtures/scheduler_string_test.yaml +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -18,9 +18,6 @@ globals: - id: interval_counter type: int initial_value: '0' - - id: dynamic_counter - type: int - initial_value: '0' - id: static_tests_done type: bool initial_value: 'false' @@ -103,46 +100,43 @@ script: - id: test_dynamic_strings then: - - logger.log: "Testing dynamic string timeouts and intervals" + - logger.log: "Testing const char* timeouts and intervals" - lambda: |- auto *component2 = id(test_sensor2); - // Test 8: Dynamic string with set_timeout (std::string) - std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++); - App.scheduler.set_timeout(component2, dynamic_name, 100, []() { + // Test 8: const char* name with set_timeout + App.scheduler.set_timeout(component2, "dynamic_timeout", 100, []() { ESP_LOGI("test", "Dynamic timeout fired"); id(timeout_counter) += 1; }); - // Test 9: Dynamic string with set_interval - std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++); - App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() { - ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str()); + // Test 9: const char* name with set_interval, cancelled from inside the callback + App.scheduler.set_interval(component2, "dynamic_interval", 250, []() { + ESP_LOGI("test", "Dynamic interval fired"); id(interval_counter) += 1; if (id(interval_counter) >= 6) { - App.scheduler.cancel_interval(id(test_sensor2), interval_name); + App.scheduler.cancel_interval(id(test_sensor2), "dynamic_interval"); ESP_LOGI("test", "Cancelled dynamic interval"); } }); - // Test 10: Cancel with different string object but same content - std::string cancel_name = "cancel_test"; - App.scheduler.set_timeout(component2, cancel_name, 2000, []() { + // Test 10: Cancel with a different pointer but identical content. + // STATIC_STRING names match by content, so a distinct static buffer with the + // same characters still cancels the scheduled timeout. + static const char CANCEL_NAME[] = "cancel_test"; + App.scheduler.set_timeout(component2, CANCEL_NAME, 2000, []() { ESP_LOGI("test", "This should be cancelled"); }); + static const char CANCEL_NAME_2[] = "cancel_test"; + App.scheduler.cancel_timeout(component2, CANCEL_NAME_2); + ESP_LOGI("test", "Cancelled timeout using different buffer with same content"); - // Cancel using a different string object - std::string cancel_name_2 = "cancel_test"; - App.scheduler.cancel_timeout(component2, cancel_name_2); - ESP_LOGI("test", "Cancelled timeout using different string object"); - - // Test 11: Dynamic string with defer (using std::string overload) + // Test 11: const char* name with defer class TestDynamicDeferComponent : public Component { public: void test_dynamic_defer() { - std::string defer_name = "dynamic_defer_" + std::to_string(id(dynamic_counter)++); - this->defer(defer_name, [defer_name]() { - ESP_LOGI("test", "Dynamic defer fired: %s", defer_name.c_str()); + this->defer("dynamic_defer", []() { + ESP_LOGI("test", "Dynamic defer fired"); id(timeout_counter) += 1; }); } diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py deleted file mode 100644 index bfa581129b..0000000000 --- a/tests/integration/test_scheduler_string_lifetime.py +++ /dev/null @@ -1,169 +0,0 @@ -"""String lifetime test - verify scheduler handles string destruction correctly.""" - -import asyncio -from pathlib import Path -import re - -import pytest - -from .types import APIClientConnectedFactory, RunCompiledFunction - - -@pytest.mark.asyncio -async def test_scheduler_string_lifetime( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """Test that scheduler correctly handles string lifetimes when strings go out of scope.""" - - # Get the absolute path to the external components directory - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) - - # Replace the placeholder in the YAML config with the actual path - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) - - # Create events for synchronization - test1_complete = asyncio.Event() - test2_complete = asyncio.Event() - test3_complete = asyncio.Event() - test4_complete = asyncio.Event() - test5_complete = asyncio.Event() - all_tests_complete = asyncio.Event() - - # Track test progress - test_stats = { - "tests_passed": 0, - "tests_failed": 0, - "errors": [], - "current_test": None, - "test_callbacks_executed": {}, - } - - def on_log_line(line: str) -> None: - # Track test-specific events - if "Test 1 complete" in line: - test1_complete.set() - elif "Test 2 complete" in line: - test2_complete.set() - elif "Test 3 complete" in line: - test3_complete.set() - elif "Test 4 complete" in line: - test4_complete.set() - elif "Test 5 complete" in line: - test5_complete.set() - - # Track individual callback executions - callback_match = re.search(r"Callback '(.+?)' executed", line) - if callback_match: - callback_name = callback_match.group(1) - test_stats["test_callbacks_executed"][callback_name] = True - - # Track test results from the C++ test output - if "Tests passed:" in line and "string_lifetime" in line: - # Extract the number from "Tests passed: 32" - match = re.search(r"Tests passed:\s*(\d+)", line) - if match: - test_stats["tests_passed"] = int(match.group(1)) - elif "Tests failed:" in line and "string_lifetime" in line: - match = re.search(r"Tests failed:\s*(\d+)", line) - if match: - test_stats["tests_failed"] = int(match.group(1)) - elif "ERROR" in line and "string_lifetime" in line: - test_stats["errors"].append(line) - - # Check for memory corruption indicators - if any( - indicator in line.lower() - for indicator in [ - "use after free", - "heap corruption", - "segfault", - "abort", - "assertion", - "sanitizer", - "bad memory", - "invalid pointer", - ] - ): - pytest.fail(f"Memory corruption detected: {line}") - - # Check for completion - if "String lifetime tests complete" in line: - all_tests_complete.set() - - async with ( - run_compiled(yaml_config, line_callback=on_log_line), - api_client_connected() as client, - ): - # Verify we can connect - device_info = await client.device_info() - assert device_info is not None - assert device_info.name == "scheduler-string-lifetime-test" - - # List entities and services - _, services = await asyncio.wait_for( - client.list_entities_services(), timeout=5.0 - ) - - # Find our test services - test_services = {} - for service in services: - if service.name == "run_test1": - test_services["test1"] = service - elif service.name == "run_test2": - test_services["test2"] = service - elif service.name == "run_test3": - test_services["test3"] = service - elif service.name == "run_test4": - test_services["test4"] = service - elif service.name == "run_test5": - test_services["test5"] = service - elif service.name == "run_final_check": - test_services["final"] = service - - # Ensure all services are found - required_services = ["test1", "test2", "test3", "test4", "test5", "final"] - for service_name in required_services: - assert service_name in test_services, f"{service_name} service not found" - - # Run tests sequentially, waiting for each to complete - try: - # Test 1 - await client.execute_service(test_services["test1"], {}) - await asyncio.wait_for(test1_complete.wait(), timeout=5.0) - - # Test 2 - await client.execute_service(test_services["test2"], {}) - await asyncio.wait_for(test2_complete.wait(), timeout=5.0) - - # Test 3 - await client.execute_service(test_services["test3"], {}) - await asyncio.wait_for(test3_complete.wait(), timeout=5.0) - - # Test 4 - await client.execute_service(test_services["test4"], {}) - await asyncio.wait_for(test4_complete.wait(), timeout=5.0) - - # Test 5 - await client.execute_service(test_services["test5"], {}) - await asyncio.wait_for(test5_complete.wait(), timeout=5.0) - - # Final check - await client.execute_service(test_services["final"], {}) - await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) - - except TimeoutError: - pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") - - # Check for any errors - assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}" - - # Verify we had the expected number of passing tests - assert test_stats["tests_passed"] == 30, ( - f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}" - ) diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py deleted file mode 100644 index 56b8998c56..0000000000 --- a/tests/integration/test_scheduler_string_name_stress.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Stress test for heap scheduler with std::string names from multiple threads.""" - -import asyncio -from pathlib import Path -import re - -from aioesphomeapi import UserService -import pytest - -from .types import APIClientConnectedFactory, RunCompiledFunction - - -@pytest.mark.asyncio -async def test_scheduler_string_name_stress( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads.""" - - # Get the absolute path to the external components directory - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) - - # Replace the placeholder in the YAML config with the actual path - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) - - # Create a future to signal test completion - loop = asyncio.get_running_loop() - test_complete_future: asyncio.Future[None] = loop.create_future() - - # Track executed callbacks and any crashes - executed_callbacks: set[int] = set() - error_messages: list[str] = [] - - def on_log_line(line: str) -> None: - # Check for crash indicators - if any( - indicator in line.lower() - for indicator in [ - "segfault", - "abort", - "assertion", - "heap corruption", - "use after free", - ] - ): - error_messages.append(line) - if not test_complete_future.done(): - test_complete_future.set_exception(Exception(f"Crash detected: {line}")) - return - - # Track executed callbacks - match = re.search(r"Executed string-named callback (\d+)", line) - if match: - callback_id = int(match.group(1)) - executed_callbacks.add(callback_id) - - # Check for completion - if ( - "String name stress test complete" in line - and not test_complete_future.done() - ): - test_complete_future.set_result(None) - - async with ( - run_compiled(yaml_config, line_callback=on_log_line), - api_client_connected() as client, - ): - # Verify we can connect - device_info = await client.device_info() - assert device_info is not None - assert device_info.name == "sched-string-name-stress" - - # List entities and services - _, services = await asyncio.wait_for( - client.list_entities_services(), timeout=5.0 - ) - - # Find our test service - run_stress_test_service: UserService | None = None - for service in services: - if service.name == "run_string_name_stress_test": - run_stress_test_service = service - break - - assert run_stress_test_service is not None, ( - "run_string_name_stress_test service not found" - ) - - # Call the service to start the test - await client.execute_service(run_stress_test_service, {}) - - # Wait for test to complete or crash - try: - await asyncio.wait_for(test_complete_future, timeout=30.0) - except TimeoutError: - pytest.fail( - f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " - f"This might indicate a deadlock." - ) - - # Verify no errors occurred (crashes already handled by exception) - assert not error_messages, f"Errors detected during test: {error_messages}" - - # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each) - assert len(executed_callbacks) == 1000, ( - f"Expected 1000 callbacks but got {len(executed_callbacks)}" - ) - - # Verify each callback ID was executed exactly once - for i in range(1000): - assert i in executed_callbacks, f"Callback {i} was not executed" diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py index 783ed37c13..3bc3487432 100644 --- a/tests/integration/test_scheduler_string_test.py +++ b/tests/integration/test_scheduler_string_test.py @@ -99,7 +99,7 @@ async def test_scheduler_string_test( timeout_count += 1 # Check for cancel test - elif "Cancelled timeout using different string object" in clean_line: + elif "Cancelled timeout using different buffer with same content" in clean_line: cancel_test_done.set() # Check for final results