[core] Remove deprecated std::string scheduler/timer overloads

This commit is contained in:
J. Nick Koston
2026-06-21 10:23:43 -05:00
parent dbdf125ec8
commit 8aa06c9d15
19 changed files with 43 additions and 1492 deletions

View File

@@ -85,24 +85,10 @@ void Component::setup() {}
void Component::loop() {} void Component::loop() {}
void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&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<void()> &&f) { // NOLINT void Component::set_interval(const char *name, uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, name, interval, std::move(f)); 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 bool Component::cancel_interval(const char *name) { // NOLINT
return App.scheduler.cancel_interval(this, name); return App.scheduler.cancel_interval(this, name);
} }
@@ -137,24 +123,10 @@ bool Component::cancel_retry(const char *name) { // NOLINT
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&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<void()> &&f) { // NOLINT void Component::set_timeout(const char *name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, timeout, std::move(f)); 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 bool Component::cancel_timeout(const char *name) { // NOLINT
return App.scheduler.cancel_timeout(this, name); return App.scheduler.cancel_timeout(this, name);
} }
@@ -319,21 +291,9 @@ void Component::reset_to_construction_state() {
void Component::defer(std::function<void()> &&f) { // NOLINT void Component::defer(std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), 0, std::move(f)); App.scheduler.set_timeout(this, static_cast<const char *>(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 bool Component::cancel_defer(const char *name) { // NOLINT
return App.scheduler.cancel_timeout(this, name); return App.scheduler.cancel_timeout(this, name);
} }
void Component::defer(const std::string &name, std::function<void()> &&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<void()> &&f) { // NOLINT void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f)); App.scheduler.set_timeout(this, name, 0, std::move(f));
} }

View File

@@ -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 * 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. * 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<void()> &&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. * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
* This means the name should be: * This means the name should be:
@@ -391,7 +379,7 @@ class Component {
* - A static const char* variable * - A static const char* variable
* - A pointer with lifetime >= the scheduled task * - 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 name The identifier for this interval function (must have static lifetime)
* @param interval The interval in ms * @param interval The interval in ms
@@ -416,12 +404,9 @@ class Component {
* @param name The identifier for this interval function. * @param name The identifier for this interval function.
* @return Whether an interval functions was deleted. * @return Whether an interval functions was deleted.
*/ */
// Remove before 2026.7.0 bool cancel_interval(const char *name); // NOLINT
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(uint32_t id); // NOLINT
bool cancel_interval(const std::string &name); // 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. /// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0.
// Remove before 2026.8.0 // Remove before 2026.8.0
@@ -465,18 +450,6 @@ class Component {
* IMPORTANT: Do not rely on this having correct timing. This is only called from * 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 delay. If you need exact timing please
* use hardware timers. * 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<void()> &&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. * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
* This means the name should be: * This means the name should be:
@@ -484,7 +457,9 @@ class Component {
* - A static const char* variable * - A static const char* variable
* - A pointer with lifetime >= the timeout duration * - 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 name The identifier for this timeout function (must have static lifetime)
* @param timeout The timeout in ms * @param timeout The timeout in ms
@@ -509,25 +484,13 @@ class Component {
* @param name The identifier for this timeout function. * @param name The identifier for this timeout function.
* @return Whether a timeout functions was deleted. * @return Whether a timeout functions was deleted.
*/ */
// Remove before 2026.7.0 bool cancel_timeout(const char *name); // NOLINT
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(uint32_t id); // NOLINT
bool cancel_timeout(const std::string &name); // NOLINT bool cancel_timeout(InternalSchedulerID id); // 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<void()> &&f); // NOLINT
/** Defer a callback to the next loop() call with a const char* name. /** 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. * IMPORTANT: The provided name pointer must remain valid for the lifetime of the deferred task.
* This means the name should be: * This means the name should be:
@@ -535,7 +498,7 @@ class Component {
* - A static const char* variable * - A static const char* variable
* - A pointer with lifetime >= the deferred execution * - 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 name The name of the defer function (must have static lifetime)
* @param f The callback * @param f The callback
@@ -549,11 +512,8 @@ class Component {
void defer(uint32_t id, std::function<void()> &&f); // NOLINT void defer(uint32_t id, std::function<void()> &&f); // NOLINT
/// Cancel a defer callback using the specified name, name must not be empty. /// Cancel a defer callback using the specified name, name must not be empty.
// Remove before 2026.7.0 bool cancel_defer(const char *name); // NOLINT
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_defer(uint32_t id); // NOLINT
bool cancel_defer(const std::string &name); // NOLINT
bool cancel_defer(const char *name); // NOLINT
bool cancel_defer(uint32_t id); // NOLINT
void status_clear_warning_slow_path_(); void status_clear_warning_slow_path_();
void status_clear_error_slow_path_(); void status_clear_error_slow_path_();

View File

@@ -254,30 +254,16 @@ void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t
std::move(func)); std::move(func));
} }
void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
std::function<void()> &&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<void()> &&func) { void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout, this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout,
std::move(func)); 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) { bool HOT Scheduler::cancel_timeout(Component *component, const char *name) {
return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::TIMEOUT); return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::TIMEOUT);
} }
bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) { bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) {
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::TIMEOUT); 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<void()> &&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, void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval,
std::function<void()> &&func) { std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::STATIC_STRING, name, 0, interval, 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, this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID, nullptr, id, interval,
std::move(func)); 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) { bool HOT Scheduler::cancel_interval(Component *component, const char *name) {
return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::INTERVAL); return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::INTERVAL);
} }

View File

@@ -31,11 +31,6 @@ class Scheduler {
template<typename... Ts> friend class DelayAction; template<typename... Ts> friend class DelayAction;
public: 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<void()> &&func);
/** Set a timeout with a const char* name. /** Set a timeout with a const char* name.
* *
* IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
@@ -53,8 +48,6 @@ class Scheduler {
static_cast<uint32_t>(id), timeout, std::move(func)); static_cast<uint32_t>(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, const char *name);
bool cancel_timeout(Component *component, uint32_t id); bool cancel_timeout(Component *component, uint32_t id);
bool cancel_timeout(Component *component, InternalSchedulerID id) { bool cancel_timeout(Component *component, InternalSchedulerID id) {
@@ -62,9 +55,6 @@ class Scheduler {
SchedulerItem::TIMEOUT); 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<void()> &&func);
/** Set an interval with a const char* name. /** Set an interval with a const char* name.
* *
* IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
@@ -82,8 +72,6 @@ class Scheduler {
static_cast<uint32_t>(id), interval, std::move(func)); static_cast<uint32_t>(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, const char *name);
bool cancel_interval(Component *component, uint32_t id); bool cancel_interval(Component *component, uint32_t id);
bool cancel_interval(Component *component, InternalSchedulerID id) { bool cancel_interval(Component *component, InternalSchedulerID id) {

View File

@@ -8,14 +8,23 @@ static const char *const TAG = "bulk_cleanup";
void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); } 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() { void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
ESP_LOGI(TAG, "Starting bulk cleanup test..."); ESP_LOGI(TAG, "Starting bulk cleanup test...");
// Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10) // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10)
ESP_LOGI(TAG, "Scheduling 25 timeouts..."); ESP_LOGI(TAG, "Scheduling 25 timeouts...");
for (int i = 0; i < 25; i++) { for (int i = 0; i < 25; i++) {
std::string name = "bulk_timeout_" + std::to_string(i); App.scheduler.set_timeout(this, BULK_TIMEOUT_NAMES[i], 2500, [i]() {
App.scheduler.set_timeout(this, name, 2500, [i]() {
// These should never execute as we'll cancel them // These should never execute as we'll cancel them
ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i); ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i);
}); });
@@ -25,8 +34,7 @@ void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup..."); ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup...");
int cancelled_count = 0; int cancelled_count = 0;
for (int i = 0; i < 25; i++) { for (int i = 0; i < 25; i++) {
std::string name = "bulk_timeout_" + std::to_string(i); if (App.scheduler.cancel_timeout(this, BULK_TIMEOUT_NAMES[i])) {
if (App.scheduler.cancel_timeout(this, name)) {
cancelled_count++; cancelled_count++;
} }
} }
@@ -56,8 +64,7 @@ void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
// Also schedule some normal timeouts to ensure scheduler keeps working after cleanup // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup
static int post_cleanup_count = 0; static int post_cleanup_count = 0;
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
std::string name = "post_cleanup_" + std::to_string(i); App.scheduler.set_timeout(this, POST_CLEANUP_NAMES[i], 50 + i * 25, [i]() {
App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() {
ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i);
post_cleanup_count++; post_cleanup_count++;
if (post_cleanup_count >= 5) { if (post_cleanup_count >= 5) {

View File

@@ -4,12 +4,18 @@
#include <vector> #include <vector>
#include <chrono> #include <chrono>
#include <random> #include <random>
#include <sstream>
namespace esphome::scheduler_rapid_cancellation_component { namespace esphome::scheduler_rapid_cancellation_component {
static const char *const TAG = "scheduler_rapid_cancellation"; 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::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); }
void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { 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++) { for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
// Use modulo to ensure multiple threads use the same names // Use modulo to ensure multiple threads use the same names
int name_index = i % NUM_NAMES; int name_index = i % NUM_NAMES;
std::stringstream ss; const char *name = SHARED_TIMEOUT_NAMES[name_index];
ss << "shared_timeout_" << name_index;
std::string name = ss.str();
// All threads schedule timeouts - this will implicitly cancel existing ones // All threads schedule timeouts - this will implicitly cancel existing ones
this->set_timeout(name, 150, [this, name]() { this->set_timeout(name, 150, [this, name]() {
this->total_executed_.fetch_add(1); 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); this->total_scheduled_.fetch_add(1);

View File

@@ -1,9 +1,9 @@
#include "simultaneous_callbacks_component.h" #include "simultaneous_callbacks_component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes>
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <chrono> #include <chrono>
#include <sstream>
namespace esphome::scheduler_simultaneous_callbacks_component { 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)); std::this_thread::sleep_until(start_time + std::chrono::microseconds(100));
for (int i = 0; i < CALLBACKS_PER_THREAD; i++) { for (int i = 0; i < CALLBACKS_PER_THREAD; i++) {
// Create unique name for each callback // Unique numeric ID for each callback (zero heap allocation, no name collisions)
std::stringstream ss; uint32_t callback_id = static_cast<uint32_t>(thread_id) * CALLBACKS_PER_THREAD + i;
ss << "thread_" << thread_id << "_cb_" << i;
std::string name = ss.str();
// Schedule callback for exactly DELAY_MS from now // 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 // Increment concurrent counter atomically
int current = this->callbacks_at_once_.fetch_add(1) + 1; 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 // 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 // Simulate some minimal work
std::atomic<int> work{0}; std::atomic<int> work{0};
@@ -73,7 +71,7 @@ void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test()
}); });
this->total_scheduled_.fetch_add(1); 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); ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id);

View File

@@ -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)

View File

@@ -1,260 +0,0 @@
#include "string_lifetime_component.h"
#include "esphome/core/log.h"
#include <memory>
#include <thread>
#include <chrono>
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<std::string> 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<std::string>("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

View File

@@ -1,35 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include <vector>
#include <string>
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

View File

@@ -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)

View File

@@ -1,108 +0,0 @@
#include "string_name_stress_component.h"
#include "esphome/core/log.h"
#include <thread>
#include <atomic>
#include <vector>
#include <chrono>
#include <string>
#include <sstream>
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<std::thread> 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<std::chrono::milliseconds>(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

View File

@@ -1,20 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include <atomic>
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<int> total_callbacks_{0};
std::atomic<int> executed_callbacks_{0};
};
} // namespace esphome::scheduler_string_name_stress_component

View File

@@ -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();

View File

@@ -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"

View File

@@ -1,310 +0,0 @@
esphome:
name: scheduler-string-test
on_boot:
priority: -100
then:
- logger.log: "Starting scheduler string tests"
debug_scheduler: true # Enable scheduler debug logging
host:
api:
logger:
level: VERBOSE
globals:
- id: timeout_counter
type: int
initial_value: '0'
- 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'
- id: dynamic_tests_done
type: bool
initial_value: 'false'
- id: results_reported
type: bool
initial_value: 'false'
- id: edge_tests_done
type: bool
initial_value: 'false'
- id: empty_cancel_failed
type: bool
initial_value: 'false'
script:
- id: test_static_strings
then:
- logger.log: "Testing static string timeouts and intervals"
- lambda: |-
auto *component1 = id(test_sensor1);
// Test 1: Static string literals with set_timeout
App.scheduler.set_timeout(component1, "static_timeout_1", 50, []() {
ESP_LOGI("test", "Static timeout 1 fired");
id(timeout_counter) += 1;
});
// Test 2: Static const char* with set_timeout
static const char* TIMEOUT_NAME = "static_timeout_2";
App.scheduler.set_timeout(component1, TIMEOUT_NAME, 100, []() {
ESP_LOGI("test", "Static timeout 2 fired");
id(timeout_counter) += 1;
});
// Test 3: Static string literal with set_interval
App.scheduler.set_interval(component1, "static_interval_1", 200, []() {
ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter));
id(interval_counter) += 1;
if (id(interval_counter) >= 3) {
App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1");
ESP_LOGI("test", "Cancelled static interval 1");
}
});
// Test 4: Empty string (should be handled safely)
App.scheduler.set_timeout(component1, "", 150, []() {
ESP_LOGI("test", "Empty string timeout fired");
});
// Test 5: Cancel timeout with const char* literal
App.scheduler.set_timeout(component1, "cancel_static_timeout", 5000, []() {
ESP_LOGI("test", "This static timeout should be cancelled");
});
// Cancel using const char* directly
App.scheduler.cancel_timeout(component1, "cancel_static_timeout");
ESP_LOGI("test", "Cancelled static timeout using const char*");
// Test 6 & 7: Test defer with const char* overload using a test component
class TestDeferComponent : public Component {
public:
void test_static_defer() {
// Test 6: Static string literal with defer (const char* overload)
this->defer("static_defer_1", []() {
ESP_LOGI("test", "Static defer 1 fired");
id(timeout_counter) += 1;
});
// Test 7: Static const char* with defer
static const char* DEFER_NAME = "static_defer_2";
this->defer(DEFER_NAME, []() {
ESP_LOGI("test", "Static defer 2 fired");
id(timeout_counter) += 1;
});
}
};
static TestDeferComponent test_defer_component;
test_defer_component.test_static_defer();
- id: test_dynamic_strings
then:
- logger.log: "Testing dynamic string 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, []() {
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());
id(interval_counter) += 1;
if (id(interval_counter) >= 6) {
App.scheduler.cancel_interval(id(test_sensor2), interval_name);
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, []() {
ESP_LOGI("test", "This should be cancelled");
});
// 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)
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());
id(timeout_counter) += 1;
});
}
};
static TestDynamicDeferComponent test_dynamic_defer_component;
test_dynamic_defer_component.test_dynamic_defer();
- id: test_cancellation_edge_cases
then:
- logger.log: "Testing cancellation edge cases"
- lambda: |-
auto *component1 = id(test_sensor1);
// Use a different component for empty string tests to avoid interference
auto *component2 = id(test_sensor2);
// Test 12: Cancel with empty string - regression test for issue #9599
// First create a timeout with empty name on component2 to avoid interference
App.scheduler.set_timeout(component2, "", 500, []() {
ESP_LOGE("test", "ERROR: Empty name timeout fired - it should have been cancelled!");
id(empty_cancel_failed) = true;
});
// Now cancel it - this should work after our fix
bool cancelled_empty = App.scheduler.cancel_timeout(component2, "");
ESP_LOGI("test", "Cancel empty string result: %s (should be true)", cancelled_empty ? "true" : "false");
if (!cancelled_empty) {
ESP_LOGE("test", "ERROR: Failed to cancel empty string timeout!");
id(empty_cancel_failed) = true;
}
// Test 13: Cancel non-existent timeout
bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, "does_not_exist");
ESP_LOGI("test", "Cancel non-existent timeout result: %s",
cancelled_nonexistent ? "true (unexpected!)" : "false (expected)");
// Test 14: Multiple timeouts with same name - only last should execute
for (int i = 0; i < 5; i++) {
App.scheduler.set_timeout(component1, "duplicate_timeout", 200 + i*10, [i]() {
ESP_LOGI("test", "Duplicate timeout %d fired", i);
id(timeout_counter) += 1;
});
}
ESP_LOGI("test", "Created 5 timeouts with same name 'duplicate_timeout'");
// Test 15: Multiple intervals with same name - only last should run
for (int i = 0; i < 3; i++) {
App.scheduler.set_interval(component1, "duplicate_interval", 300, [i]() {
ESP_LOGI("test", "Duplicate interval %d fired", i);
id(interval_counter) += 10; // Large increment to detect multiple
// Cancel after first execution
App.scheduler.cancel_interval(id(test_sensor1), "duplicate_interval");
});
}
ESP_LOGI("test", "Created 3 intervals with same name 'duplicate_interval'");
// Test 16: Cancel with nullptr protection (via empty const char*)
const char* null_name = "";
App.scheduler.set_timeout(component2, null_name, 600, []() {
ESP_LOGE("test", "ERROR: Const char* empty timeout fired - should have been cancelled!");
id(empty_cancel_failed) = true;
});
bool cancelled_const_empty = App.scheduler.cancel_timeout(component2, null_name);
ESP_LOGI("test", "Cancel const char* empty result: %s (should be true)",
cancelled_const_empty ? "true" : "false");
if (!cancelled_const_empty) {
ESP_LOGE("test", "ERROR: Failed to cancel const char* empty timeout!");
id(empty_cancel_failed) = true;
}
// Test 17: Rapid create/cancel/create with same name
App.scheduler.set_timeout(component1, "rapid_test", 5000, []() {
ESP_LOGI("test", "First rapid timeout - should not fire");
id(timeout_counter) += 100;
});
App.scheduler.cancel_timeout(component1, "rapid_test");
App.scheduler.set_timeout(component1, "rapid_test", 250, []() {
ESP_LOGI("test", "Second rapid timeout - should fire");
id(timeout_counter) += 1;
});
// Test 18: Cancel all with a specific name (multiple instances)
// Create multiple with same name
App.scheduler.set_timeout(component1, "multi_cancel", 300, []() {
ESP_LOGI("test", "Multi-cancel timeout 1");
});
App.scheduler.set_timeout(component1, "multi_cancel", 350, []() {
ESP_LOGI("test", "Multi-cancel timeout 2");
});
App.scheduler.set_timeout(component1, "multi_cancel", 400, []() {
ESP_LOGI("test", "Multi-cancel timeout 3 - only this should fire");
id(timeout_counter) += 1;
});
// Note: Each set_timeout with same name cancels the previous one automatically
- id: report_results
then:
- lambda: |-
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d",
id(timeout_counter), id(interval_counter));
// Check if empty string cancellation test passed
if (id(empty_cancel_failed)) {
ESP_LOGE("test", "ERROR: Empty string cancellation test FAILED!");
} else {
ESP_LOGI("test", "Empty string cancellation test PASSED");
}
sensor:
- platform: template
name: Test Sensor 1
id: test_sensor1
lambda: return 1.0;
update_interval: never
- platform: template
name: Test Sensor 2
id: test_sensor2
lambda: return 2.0;
update_interval: never
interval:
# Run static string tests after boot - using script to run once
- interval: 0.1s
then:
- if:
condition:
lambda: 'return id(static_tests_done) == false;'
then:
- lambda: 'id(static_tests_done) = true;'
- script.execute: test_static_strings
- logger.log: "Started static string tests"
# Run dynamic string tests after static tests
- interval: 0.2s
then:
- if:
condition:
lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);'
then:
- lambda: 'id(dynamic_tests_done) = true;'
- delay: 0.2s
- script.execute: test_dynamic_strings
# Run cancellation edge case tests after dynamic tests
- interval: 0.2s
then:
- if:
condition:
lambda: 'return id(dynamic_tests_done) && !id(edge_tests_done);'
then:
- lambda: 'id(edge_tests_done) = true;'
- delay: 0.5s
- script.execute: test_cancellation_edge_cases
# Report results after all tests
- interval: 0.2s
then:
- if:
condition:
lambda: 'return id(edge_tests_done) && !id(results_reported);'
then:
- lambda: 'id(results_reported) = true;'
- delay: 1s
- script.execute: report_results

View File

@@ -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']}"
)

View File

@@ -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"

View File

@@ -1,202 +0,0 @@
"""Test scheduler string optimization with static and dynamic strings."""
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_scheduler_string_test(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that scheduler handles both static and dynamic strings correctly."""
# Track counts
timeout_count = 0
interval_count = 0
# Events for each test completion
static_timeout_1_fired = asyncio.Event()
static_timeout_2_fired = asyncio.Event()
static_interval_fired = asyncio.Event()
static_interval_cancelled = asyncio.Event()
empty_string_timeout_fired = asyncio.Event()
static_timeout_cancelled = asyncio.Event()
static_defer_1_fired = asyncio.Event()
static_defer_2_fired = asyncio.Event()
dynamic_timeout_fired = asyncio.Event()
dynamic_interval_fired = asyncio.Event()
dynamic_defer_fired = asyncio.Event()
cancel_test_done = asyncio.Event()
final_results_logged = asyncio.Event()
# Track interval counts
static_interval_count = 0
dynamic_interval_count = 0
def on_log_line(line: str) -> None:
nonlocal \
timeout_count, \
interval_count, \
static_interval_count, \
dynamic_interval_count
# Strip ANSI color codes
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
# Check for static timeout completions
if "Static timeout 1 fired" in clean_line:
static_timeout_1_fired.set()
timeout_count += 1
elif "Static timeout 2 fired" in clean_line:
static_timeout_2_fired.set()
timeout_count += 1
# Check for static interval
elif "Static interval 1 fired" in clean_line:
match = re.search(r"count: (\d+)", clean_line)
if match:
static_interval_count = int(match.group(1))
static_interval_fired.set()
elif "Cancelled static interval 1" in clean_line:
static_interval_cancelled.set()
# Check for empty string timeout
elif "Empty string timeout fired" in clean_line:
empty_string_timeout_fired.set()
# Check for static timeout cancellation
elif "Cancelled static timeout using const char*" in clean_line:
static_timeout_cancelled.set()
# Check for static defer tests
elif "Static defer 1 fired" in clean_line:
static_defer_1_fired.set()
timeout_count += 1
elif "Static defer 2 fired" in clean_line:
static_defer_2_fired.set()
timeout_count += 1
# Check for dynamic string tests
elif "Dynamic timeout fired" in clean_line:
dynamic_timeout_fired.set()
timeout_count += 1
elif "Dynamic interval fired" in clean_line:
dynamic_interval_count += 1
dynamic_interval_fired.set()
# Check for dynamic defer test
elif "Dynamic defer fired" in clean_line:
dynamic_defer_fired.set()
timeout_count += 1
# Check for cancel test
elif "Cancelled timeout using different string object" in clean_line:
cancel_test_done.set()
# Check for final results
elif "Final results" in clean_line:
match = re.search(r"Timeouts: (\d+), Intervals: (\d+)", clean_line)
if match:
timeout_count = int(match.group(1))
interval_count = int(match.group(2))
final_results_logged.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-test"
# Wait for static string tests
try:
await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5)
except TimeoutError:
pytest.fail("Static timeout 1 did not fire within 0.5 seconds")
try:
await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5)
except TimeoutError:
pytest.fail("Static timeout 2 did not fire within 0.5 seconds")
try:
await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0)
except TimeoutError:
pytest.fail("Static interval did not fire within 1 second")
try:
await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0)
except TimeoutError:
pytest.fail("Static interval was not cancelled within 2 seconds")
# Verify static interval ran at least 3 times
assert static_interval_count >= 2, (
f"Expected static interval to run at least 3 times, got {static_interval_count + 1}"
)
# Verify static timeout was cancelled
assert static_timeout_cancelled.is_set(), (
"Static timeout should have been cancelled"
)
# Wait for static defer tests
try:
await asyncio.wait_for(static_defer_1_fired.wait(), timeout=0.5)
except TimeoutError:
pytest.fail("Static defer 1 did not fire within 0.5 seconds")
try:
await asyncio.wait_for(static_defer_2_fired.wait(), timeout=0.5)
except TimeoutError:
pytest.fail("Static defer 2 did not fire within 0.5 seconds")
# Wait for dynamic string tests
try:
await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0)
except TimeoutError:
pytest.fail("Dynamic timeout did not fire within 1 second")
try:
await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5)
except TimeoutError:
pytest.fail("Dynamic interval did not fire within 1.5 seconds")
# Wait for dynamic defer test
try:
await asyncio.wait_for(dynamic_defer_fired.wait(), timeout=1.0)
except TimeoutError:
pytest.fail("Dynamic defer did not fire within 1 second")
# Wait for cancel test
try:
await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail("Cancel test did not complete within 1 second")
# Wait for final results
try:
await asyncio.wait_for(final_results_logged.wait(), timeout=4.0)
except TimeoutError:
pytest.fail("Final results were not logged within 4 seconds")
# Verify results
assert timeout_count >= 6, (
f"Expected at least 6 timeouts (including defers), got {timeout_count}"
)
assert interval_count >= 3, (
f"Expected at least 3 interval fires, got {interval_count}"
)
# Empty string timeout DOES fire (scheduler accepts empty names)
assert empty_string_timeout_fired.is_set(), "Empty string timeout should fire"