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.
428 lines
10 KiB
C++
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);
|
|
}
|
|
}
|