mirror-llvm-project/orc-rt/test/unit/TaskGroupTest.cpp
Lang Hames 3c6b487042
[orc-rt] Add TaskGroup::TokenSource; drop raw group accessor (#210240)
Session::managedCodeTaskGroup() returned the raw
std::shared_ptr<TaskGroup> for the managed-code group, exposing the
group's full interface -- including close() and addOnComplete() -- to
callers, even though those operations are reserved for the Session.

Add TaskGroup::TokenSource, a strong handle whose sole role is to serve
as an argument to the TaskGroup::Token constructor. Session now exposes
managedCodeTokenSource() in its place, so clients can acquire tokens (to
keep managed code alive across shutdown) without gaining the ability to
close the group or register completion callbacks.
2026-07-17 14:45:45 +10:00

428 lines
10 KiB
C++

//===- TaskGroupTest.cpp --------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Tests for orc-rt's TaskGroup.h APIs.
//
//===----------------------------------------------------------------------===//
#include "orc-rt/TaskGroup.h"
#include "gtest/gtest.h"
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
using namespace orc_rt;
TEST(TaskGroupTest, TrivialConstructionAndDestruction) {
auto TG = TaskGroup::Create();
}
TEST(TaskGroupTest, SingleTokenThenClose) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
{
TaskGroup::Token T(TG);
EXPECT_TRUE(T);
TG->close();
EXPECT_FALSE(Completed);
}
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, CloseWithNoTokens) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
TG->close();
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, TokenFromClosedGroup) {
auto TG = TaskGroup::Create();
TG->close();
TaskGroup::Token T(TG);
EXPECT_FALSE(T);
}
TEST(TaskGroupTest, TokenFromNullSharedPtr) {
std::shared_ptr<TaskGroup> TG;
TaskGroup::Token T(TG);
EXPECT_FALSE(T);
}
TEST(TaskGroupTest, CopyToken) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
{
TaskGroup::Token T1(TG);
EXPECT_TRUE(T1);
{
TaskGroup::Token T2(T1); // Copy increments count
EXPECT_TRUE(T2);
TG->close();
EXPECT_FALSE(Completed);
}
EXPECT_FALSE(Completed); // T1 still alive
}
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, MoveToken) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
TaskGroup::Token T1(TG);
TaskGroup::Token T2(std::move(T1));
EXPECT_FALSE(T1);
EXPECT_TRUE(T2);
TG->close();
EXPECT_FALSE(Completed);
T2 = TaskGroup::Token(); // Release
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, CopyAssignmentReleasesOld) {
bool Completed1 = false;
bool Completed2 = false;
auto TG1 = TaskGroup::Create();
auto TG2 = TaskGroup::Create();
TG1->addOnComplete([&]() { Completed1 = true; });
TG2->addOnComplete([&]() { Completed2 = true; });
TaskGroup::Token T1(TG1);
TaskGroup::Token T2(TG2);
TG1->close();
TG2->close();
EXPECT_FALSE(Completed1);
EXPECT_FALSE(Completed2);
T1 = T2; // Releases TG1, acquires TG2
EXPECT_TRUE(Completed1); // TG1 should complete
EXPECT_FALSE(Completed2); // TG2 still has T1 and T2
}
TEST(TaskGroupTest, CopyAssignmentFromClosedGroup) {
bool Completed = false;
auto TG1 = TaskGroup::Create();
auto TG2 = TaskGroup::Create();
TG1->addOnComplete([&]() { Completed = true; });
TG2->close();
TaskGroup::Token T1(TG1);
TaskGroup::Token T2(TG2);
EXPECT_TRUE(T1);
EXPECT_FALSE(T2);
TG1->close();
EXPECT_FALSE(Completed);
T1 = T2; // Assign from empty, releases TG1
EXPECT_FALSE(T1);
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, MoveAssignmentReleasesOld) {
bool Completed1 = false;
bool Completed2 = false;
auto TG1 = TaskGroup::Create();
auto TG2 = TaskGroup::Create();
TG1->addOnComplete([&]() { Completed1 = true; });
TG2->addOnComplete([&]() { Completed2 = true; });
TaskGroup::Token T1(TG1);
TaskGroup::Token T2(TG2);
TG1->close();
TG2->close();
EXPECT_FALSE(Completed1);
EXPECT_FALSE(Completed2);
T1 = std::move(T2); // Releases TG1, takes TG2 from T2
EXPECT_TRUE(Completed1); // TG1 should complete
EXPECT_FALSE(Completed2); // TG2 now held by T1
EXPECT_FALSE(T2); // T2 is now empty
}
TEST(TaskGroupTest, SelfCopyAssignment) {
auto TG = TaskGroup::Create();
TaskGroup::Token T(TG);
EXPECT_TRUE(T);
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
#endif
T = T; // Self-assign
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
EXPECT_TRUE(T); // Should still be valid
TG->close();
}
TEST(TaskGroupTest, SelfMoveAssignment) {
auto TG = TaskGroup::Create();
TaskGroup::Token T(TG);
EXPECT_TRUE(T);
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-move"
#endif
T = std::move(T); // Self-move-assign
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
EXPECT_TRUE(T); // Should still be valid
TG->close();
}
TEST(TaskGroupTest, AddOnCompleteAfterCompletion) {
auto TG = TaskGroup::Create();
TG->close();
bool Completed = false;
TG->addOnComplete([&]() { Completed = true; });
EXPECT_TRUE(Completed); // Runs immediately
}
TEST(TaskGroupTest, AddOnCompleteWhileTasksPending) {
auto TG = TaskGroup::Create();
TaskGroup::Token T(TG);
TG->close();
bool Completed = false;
TG->addOnComplete([&]() { Completed = true; });
EXPECT_FALSE(Completed); // Queued, not run yet
T = TaskGroup::Token(); // Release
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, MultipleCallbacks) {
std::vector<int> Order;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Order.push_back(1); });
TG->addOnComplete([&]() { Order.push_back(2); });
TG->addOnComplete([&]() { Order.push_back(3); });
TG->close();
ASSERT_EQ(Order.size(), 3u);
EXPECT_EQ(Order[0], 1);
EXPECT_EQ(Order[1], 2);
EXPECT_EQ(Order[2], 3);
}
TEST(TaskGroupTest, MultipleTokens) {
int CompletionCount = 0;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { CompletionCount++; });
{
TaskGroup::Token T1(TG);
TaskGroup::Token T2(TG);
TaskGroup::Token T3(TG);
TG->close();
EXPECT_EQ(CompletionCount, 0);
}
EXPECT_EQ(CompletionCount, 1); // Only fires once
}
TEST(TaskGroupTest, CloseIsIdempotent) {
int CompletionCount = 0;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { CompletionCount++; });
TG->close();
TG->close();
TG->close();
EXPECT_EQ(CompletionCount, 1);
}
TEST(TaskGroupTest, AcquireAfterCloseViaDirectAPI) {
auto TG = TaskGroup::Create();
EXPECT_TRUE(TG->acquireToken());
TG->close();
EXPECT_FALSE(TG->acquireToken());
TG->releaseToken(); // Release the one we acquired
}
TEST(TaskGroupTest, DirectAPIMatchesRAII) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
TG->acquireToken();
TG->acquireToken();
TG->close();
EXPECT_FALSE(Completed);
TG->releaseToken();
EXPECT_FALSE(Completed);
TG->releaseToken();
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, TokenKeepsTaskGroupAlive) {
TaskGroup::Token T;
bool Completed = false;
{
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
T = TaskGroup::Token(TG);
TG->close();
// TG goes out of scope here, but T holds a shared_ptr
}
EXPECT_FALSE(Completed); // Still pending - T keeps TG alive
T = TaskGroup::Token(); // Release
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, TokenFromTokenSource) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
TaskGroup::TokenSource TS(TG);
{
TaskGroup::Token T(TS);
EXPECT_TRUE(T);
TG->close();
EXPECT_FALSE(Completed);
}
EXPECT_TRUE(Completed);
}
TEST(TaskGroupTest, TokenFromClosedGroupViaTokenSource) {
auto TG = TaskGroup::Create();
TaskGroup::TokenSource TS(TG);
TG->close();
TaskGroup::Token T(TS);
EXPECT_FALSE(T);
}
// The point of the strong handle: it keeps the group alive so a later
// acquisition attempt is well-defined even after the original ref is gone.
TEST(TaskGroupTest, TokenSourceKeepsTaskGroupAlive) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
TaskGroup::TokenSource TS(TG);
TG->close();
TG.reset(); // Only TS holds the group now.
TaskGroup::Token T(TS); // Group still exists; acquire fails only b/c closed.
EXPECT_FALSE(T);
EXPECT_TRUE(Completed); // Nothing kept a token, so it completed.
}
// A TokenSource is not a Token: holding one must not defer completion.
TEST(TaskGroupTest, TokenSourceDoesNotHoldToken) {
bool Completed = false;
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Completed = true; });
TaskGroup::TokenSource TS(TG); // Alive across the close below.
TG->close();
EXPECT_TRUE(Completed); // Completes immediately; TS holds no token.
}
TEST(TaskGroupTest, ConcurrentTokens) {
for (int Iter = 0; Iter < 100; ++Iter) {
std::atomic<int> Count{0};
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { Count++; });
std::vector<std::thread> Threads;
for (int I = 0; I < 10; ++I) {
TaskGroup::Token T(TG);
Threads.emplace_back([T = std::move(T)]() {
std::this_thread::sleep_for(std::chrono::microseconds(10));
});
}
TG->close();
for (auto &T : Threads)
T.join();
EXPECT_EQ(Count, 1);
}
}
TEST(TaskGroupTest, ConcurrentAddOnCompleteAndClose) {
for (int Iter = 0; Iter < 100; ++Iter) {
std::atomic<int> Count{0};
auto TG = TaskGroup::Create();
std::thread Closer([&]() { TG->close(); });
std::thread Registerer([&]() { TG->addOnComplete([&]() { Count++; }); });
Closer.join();
Registerer.join();
// Callback should have run exactly once regardless of order
EXPECT_EQ(Count, 1);
}
}
TEST(TaskGroupTest, ConcurrentAcquireAndClose) {
for (int Iter = 0; Iter < 100; ++Iter) {
std::atomic<int> SuccessfulAcquires{0};
std::atomic<int> CompletionCount{0};
auto TG = TaskGroup::Create();
TG->addOnComplete([&]() { CompletionCount++; });
std::vector<std::thread> Threads;
// Multiple threads trying to acquire
for (int I = 0; I < 5; ++I) {
Threads.emplace_back([&, T = TaskGroup::Token(TG)]() {
if (T)
SuccessfulAcquires++;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
});
}
// One thread closing
Threads.emplace_back([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
TG->close();
});
for (auto &T : Threads)
T.join();
// Should complete exactly once
EXPECT_EQ(CompletionCount, 1);
// At least some acquires should have succeeded before close
EXPECT_EQ(SuccessfulAcquires, 5);
}
}