This is an automated email from the ASF dual-hosted git repository.
JosiahWI pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 04ee11ac10 Add unit tests for `Action` logic (#13332)
04ee11ac10 is described below
commit 04ee11ac10ff3987348bb0f4358f544a76cad725
Author: JosiahWI <[email protected]>
AuthorDate: Tue Jul 28 06:06:04 2026 -0500
Add unit tests for `Action` logic (#13332)
* Add unit tests for `Action` logic
Authored with Claude Sonnet and Claude Opus.
---
src/iocore/eventsystem/CMakeLists.txt | 4 +
src/iocore/eventsystem/unit_tests/test_Action.cc | 254 +++++++++++++++++++++++
2 files changed, 258 insertions(+)
diff --git a/src/iocore/eventsystem/CMakeLists.txt
b/src/iocore/eventsystem/CMakeLists.txt
index 4d4a8c50c5..a6f4d6fc43 100644
--- a/src/iocore/eventsystem/CMakeLists.txt
+++ b/src/iocore/eventsystem/CMakeLists.txt
@@ -68,6 +68,10 @@ if(BUILD_TESTING)
target_link_libraries(test_Continuation ts::inkevent configmanager
Catch2::Catch2WithMain)
add_catch2_test(NAME test_Continuation COMMAND test_Continuation)
+ add_executable(test_Action unit_tests/test_Action.cc)
+ target_link_libraries(test_Action ts::inkevent configmanager
Catch2::Catch2WithMain)
+ add_catch2_test(NAME test_Action COMMAND test_Action)
+
endif()
clang_tidy_check(inkevent)
diff --git a/src/iocore/eventsystem/unit_tests/test_Action.cc
b/src/iocore/eventsystem/unit_tests/test_Action.cc
new file mode 100644
index 0000000000..7f8890ad54
--- /dev/null
+++ b/src/iocore/eventsystem/unit_tests/test_Action.cc
@@ -0,0 +1,254 @@
+/** @file
+
+ Catch2 unit tests for the Action boundary contract.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include "inkevent_test_fixtures.h"
+
+#include <catch2/generators/catch_generators.hpp>
+
+#include <atomic>
+
+using inkevent_test::CountingContinuation;
+using inkevent_test::EventProcessorListener;
+
+CATCH_REGISTER_LISTENER(EventProcessorListener)
+
+namespace
+{
+
+class RecordingAction : public Action
+{
+public:
+ using Action::operator=;
+
+ std::atomic<int> derived_cancel_calls{0};
+
+ void
+ cancel(Continuation *c = nullptr) override
+ {
+ derived_cancel_calls.fetch_add(1, std::memory_order_release);
+ Action::cancel(c);
+ }
+};
+
+} // namespace
+
+TEST_CASE("A default-constructed Action has a null continuation, a null mutex
reference, and a false cancelled flag",
+ "[inkevent][action]")
+{
+ Action a;
+ REQUIRE(a.continuation == nullptr);
+ REQUIRE(a.mutex.get() == nullptr);
+ REQUIRE(a.cancelled == false);
+}
+
+TEST_CASE("Action::operator= with a non-null Continuation pins its observable
post-conditions", "[inkevent][action]")
+{
+ Ptr<ProxyMutex> mutex{new_ProxyMutex()};
+ CountingContinuation cont{mutex.get()};
+ int const before = mutex->refcount();
+ Action a;
+
+ Continuation *result = (a = &cont);
+
+ SECTION("the operator returns its argument unchanged so callers can chain
assignments")
+ {
+ REQUIRE(result == &cont);
+ }
+
+ SECTION("the Action stores the assigned Continuation pointer and adopts that
Continuation's ProxyMutex")
+ {
+ REQUIRE(a.continuation == &cont);
+ REQUIRE(a.mutex.get() == mutex.get());
+ }
+
+ SECTION("the Action retains an additional reference to the bound ProxyMutex")
+ {
+ REQUIRE(mutex->refcount() == before + 1);
+ }
+
+ SECTION("the Action's cancelled flag is unchanged by binding a Continuation")
+ {
+ REQUIRE(a.cancelled == false);
+ }
+}
+
+TEST_CASE("Action::operator= with nullptr clears the bound continuation and
drops the retained ProxyMutex reference",
+ "[inkevent][action]")
+{
+ Ptr<ProxyMutex> mutex{new_ProxyMutex()};
+ CountingContinuation cont{mutex.get()};
+ Action a;
+ a = &cont;
+ int const before_clear = mutex->refcount();
+
+ a = nullptr;
+
+ REQUIRE(a.continuation == nullptr);
+ REQUIRE(a.mutex.get() == nullptr);
+ REQUIRE(mutex->refcount() == before_clear - 1);
+}
+
+TEST_CASE(
+ "Reassigning Action::operator= to a different Continuation drops the
previous ProxyMutex reference and adopts the new one",
+ "[inkevent][action]")
+{
+ Ptr<ProxyMutex> mutex1{new_ProxyMutex()};
+ Ptr<ProxyMutex> mutex2{new_ProxyMutex()};
+ CountingContinuation cont1{mutex1.get()};
+ CountingContinuation cont2{mutex2.get()};
+
+ Action a;
+ a = &cont1;
+ int const ref1_one = mutex1->refcount();
+ int const ref2_one = mutex2->refcount();
+
+ a = &cont2;
+
+ REQUIRE(a.continuation == &cont2);
+ REQUIRE(a.mutex.get() == mutex2.get());
+ REQUIRE(mutex1->refcount() == ref1_one - 1);
+ REQUIRE(mutex2->refcount() == ref2_one + 1);
+}
+
+TEST_CASE(
+ "Action::operator= with a Continuation that has a null mutex stores the
Continuation pointer and leaves Action's mutex null",
+ "[inkevent][action]")
+{
+ CountingContinuation cont{nullptr};
+ Action a;
+
+ a = &cont;
+
+ REQUIRE(a.continuation == &cont);
+ REQUIRE(a.mutex.get() == nullptr);
+}
+
+TEST_CASE("Action::cancel flips the cancelled flag to true whether the bound
Continuation is passed explicitly or omitted",
+ "[inkevent][action]")
+{
+ bool const pass_continuation = GENERATE(true, false);
+
+ Ptr<ProxyMutex> mutex{new_ProxyMutex()};
+ CountingContinuation cont{mutex.get()};
+ Action a;
+ a = &cont;
+ REQUIRE(a.cancelled == false);
+
+ {
+ SCOPED_MUTEX_LOCK(lock, mutex, this_ethread());
+ a.cancel(pass_continuation ? &cont : nullptr);
+ }
+
+ REQUIRE(a.cancelled == true);
+}
+
+TEST_CASE("Action::cancel dispatches virtually so a derived Action's
overridden cancel runs through an Action* base pointer",
+ "[inkevent][action]")
+{
+ Ptr<ProxyMutex> mutex{new_ProxyMutex()};
+ CountingContinuation cont{mutex.get()};
+ RecordingAction derived;
+ derived = &cont;
+ Action *base = &derived;
+
+ {
+ SCOPED_MUTEX_LOCK(lock, mutex, this_ethread());
+ base->cancel(&cont);
+ }
+
+ REQUIRE(derived.derived_cancel_calls.load() == 1);
+ REQUIRE(derived.cancelled == true);
+}
+
+TEST_CASE("Action::cancel_action sets the cancelled flag without invoking a
derived class's overridden cancel",
+ "[inkevent][action]")
+{
+ Ptr<ProxyMutex> mutex{new_ProxyMutex()};
+ CountingContinuation cont{mutex.get()};
+ RecordingAction derived;
+ derived = &cont;
+
+ {
+ SCOPED_MUTEX_LOCK(lock, mutex, this_ethread());
+ derived.cancel_action(&cont);
+ }
+
+ REQUIRE(derived.cancelled == true);
+ REQUIRE(derived.derived_cancel_calls.load() == 0);
+}
+
+TEST_CASE("Destroying an Action that is bound to a Continuation drops its
retained ProxyMutex reference", "[inkevent][action]")
+{
+ Ptr<ProxyMutex> mutex{new_ProxyMutex()};
+ CountingContinuation cont{mutex.get()};
+ int const before = mutex->refcount();
+
+ {
+ Action a;
+ a = &cont;
+ REQUIRE(mutex->refcount() == before + 1);
+ }
+
+ REQUIRE(mutex->refcount() == before);
+}
+
+TEST_CASE("MAKE_ACTION_RESULT encodes a small integer with the low bit set so
callers can distinguish sentinels from real Actions",
+ "[inkevent][action]")
+{
+ Action *s = MAKE_ACTION_RESULT(7);
+ REQUIRE((reinterpret_cast<uintptr_t>(s) & 1u) == 1u);
+ REQUIRE(reinterpret_cast<uintptr_t>(s) == ((7u << 1) + 1u));
+}
+
+TEST_CASE("ACTION_RESULT_DONE and ACTION_IO_ERROR are distinct sentinels with
the low bit set", "[inkevent][action]")
+{
+ REQUIRE((reinterpret_cast<uintptr_t>(ACTION_RESULT_DONE) & 1u) == 1u);
+ REQUIRE((reinterpret_cast<uintptr_t>(ACTION_IO_ERROR) & 1u) == 1u);
+ REQUIRE(ACTION_RESULT_DONE != ACTION_IO_ERROR);
+}
+
+TEST_CASE("A Continuation scheduled into the future and cancelled before its
deadline receives no dispatched event",
+ "[inkevent][action][multithread]")
+{
+ Ptr<ProxyMutex> target_mutex{new_ProxyMutex()};
+ CountingContinuation target{target_mutex.get()};
+ CountingContinuation barrier{new_ProxyMutex()};
+
+ {
+ SCOPED_MUTEX_LOCK(lock, target_mutex, this_ethread());
+ Action *a = eventProcessor.schedule_in(&target, HRTIME_MSECONDS(20),
ET_CALL);
+ REQUIRE(a != nullptr);
+ a->cancel(&target);
+ }
+
+ // Schedule a barrier with a strictly later deadline than target's. The
+ // EThread cannot dispatch the barrier without first visiting target's slot
+ // (PriorityEventQueue dequeues by deadline), so awaiting the barrier proves
+ // the EThread actually ran past target's deadline — without which the
+ // count() == 0 assertion below would be vacuous on a stalled EThread.
+ Event *b = eventProcessor.schedule_in(&barrier, HRTIME_MSECONDS(150),
ET_CALL);
+ REQUIRE(b != nullptr);
+ REQUIRE(barrier.wait_until_at_least(1));
+
+ REQUIRE(target.count() == 0);
+}