blob: a6d411c1c34ab6d0310072751da146aee100e04b [file] [edit]
//===- SessionTest.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 Session.h APIs.
//
//===----------------------------------------------------------------------===//
#include "orc-rt/Session.h"
#include "orc-rt/QueueingRunner.h"
#include "orc-rt/SPSWrapperFunction.h"
#include "orc-rt-c/Session.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "CommonTestUtils.h"
#include <chrono>
#include <deque>
#include <future>
#include <optional>
using namespace orc_rt;
using ::testing::Eq;
using ::testing::Optional;
class MockService : public Service {
public:
enum class Op { Detach, Shutdown };
static void noop(Op) {}
MockService(std::optional<size_t> &DetachOpIdx,
std::optional<size_t> &ShutdownOpIdx, size_t &OpIdx,
move_only_function<void(Op)> GenResult = noop)
: DetachOpIdx(DetachOpIdx), ShutdownOpIdx(ShutdownOpIdx), OpIdx(OpIdx),
GenResult(std::move(GenResult)) {}
void onDetach(OnCompleteFn OnComplete, bool ShutdownRequested) override {
DetachOpIdx = OpIdx++;
GenResult(Op::Detach);
OnComplete();
}
void onShutdown(OnCompleteFn OnComplete) override {
ShutdownOpIdx = OpIdx++;
GenResult(Op::Shutdown);
OnComplete();
}
private:
std::optional<size_t> &DetachOpIdx;
std::optional<size_t> &ShutdownOpIdx;
size_t &OpIdx;
move_only_function<void(Op)> GenResult;
};
class ConfigurableService : public Service {
public:
ConfigurableService(int ConstructorOption) {}
/// Fallible named constructor for testing tryCreateService.
static Expected<std::unique_ptr<ConfigurableService>> Create(bool Fail) {
if (Fail)
return make_error<StringError>("failed to create service");
return std::make_unique<ConfigurableService>(42);
}
void onDetach(OnCompleteFn OnComplete, bool ShutdownRequested) override {
OnComplete();
}
void onShutdown(OnCompleteFn OnComplete) override { OnComplete(); }
void doMoreConfig(int) noexcept {}
};
class MockControllerAccess : public Session::ControllerAccess {
private:
class ConnectGuard {
public:
ConnectGuard() = default;
ConnectGuard(MockControllerAccess *MCA) : MCA(MCA) {
// Note: Assumes already locked.
++MCA->Outstanding;
}
ConnectGuard(const ConnectGuard &) = delete;
ConnectGuard &operator=(const ConnectGuard &) = delete;
ConnectGuard(ConnectGuard &&Other) : MCA(Other.MCA) { Other.MCA = nullptr; }
ConnectGuard &operator=(ConnectGuard &&Other) {
reset();
MCA = Other.MCA;
Other.MCA = nullptr;
return *this;
};
~ConnectGuard() { reset(); }
private:
void reset() {
if (MCA) {
bool Notify;
{
std::scoped_lock Lock(MCA->M);
--MCA->Outstanding;
Notify = MCA->Shutdown && MCA->Outstanding == 0;
}
if (Notify)
MCA->ShutdownCV.notify_all();
}
}
MockControllerAccess *MCA = nullptr;
};
public:
using OnConnectFn = move_only_function<Error(BootstrapInfo &BI)>;
/// Hook used to defer controller-side work (the simulated controller
/// handler invocation, and the OnComplete callback for completed calls).
/// Tests typically wire this to push onto the same WorkQueue that the
/// Session's QueueingRunner uses, so that a single drain advances both
/// sides.
using PostFn = move_only_function<void(move_only_function<void()>)>;
MockControllerAccess(Session &SS, PostFn Post = {},
OnConnectFn OnConnect = {},
MockControllerAccess **Self = nullptr)
: Session::ControllerAccess(SS), Post(std::move(Post)),
OnConnect(std::move(OnConnect)) {
// Optionally publish this instance so tests that need to drive the
// controller side directly can reach it after attach constructs it.
// (attach constructs the ControllerAccess internally and does not hand
// back a reference, since the object may not outlive the attach call.)
if (Self)
*Self = this;
}
/// Fallible named constructor for testing tryAttach. Returns an error if
/// Fail is true, otherwise a MockControllerAccess forwarding the remaining
/// arguments to the constructor.
static Expected<std::shared_ptr<MockControllerAccess>>
Create(Session &S, bool Fail, PostFn Post = {}, OnConnectFn OnConnect = {}) {
if (Fail)
return make_error<StringError>("failed to create controller access");
return std::make_shared<MockControllerAccess>(S, std::move(Post),
std::move(OnConnect));
}
void connect(BootstrapInfo BI) override {
if (OnConnect) {
if (auto Err = OnConnect(BI)) {
reportError(std::move(Err));
notifyDisconnected();
}
}
}
void disconnect() override {
// Drain any still-pending controller calls before notifying, so their
// handlers are failed rather than silently dropped -- see the
// ControllerAccess::disconnect contract. Shutdown gates callController
// (which bails via failControllerCallInline once it is set), so no new call
// can be registered after the snapshot below.
std::unordered_map<size_t, OnControllerCallReturn> ToDrain;
{
std::unique_lock<std::mutex> Lock(M);
Shutdown = true;
ShutdownCV.wait(Lock, [this]() { return Shutdown && Outstanding == 0; });
ToDrain = std::move(PendingOut);
}
for (auto &[_, OnComplete] : ToDrain)
failPendingControllerCall(std::move(OnComplete));
notifyDisconnected();
}
void callController(OnControllerCallReturn OnComplete,
orc_rt_ControllerHandlerTag T,
WrapperFunctionBuffer ArgBytes) override {
// Simulate a call to the controller by running the requested function via
// the test-supplied Post hook (or inline, if no hook was provided).
ConnectGuard CG;
size_t CId = 0;
bool BailOut = false;
{
std::scoped_lock<std::mutex> Lock(M);
if (!Shutdown) {
CG = ConnectGuard(this);
CId = CallId++;
assert(!PendingOut.count(CId));
PendingOut[CId] = std::move(OnComplete);
} else
BailOut = true;
}
if (BailOut)
return failControllerCallInline(std::move(OnComplete));
runOrPost([this, CId, T, ArgBytes = std::move(ArgBytes)]() mutable {
auto Fn = reinterpret_cast<orc_rt_WrapperFunction>(T);
Fn(reinterpret_cast<orc_rt_SessionRef>(this), ArgBytes.release(),
ccReturn, CId);
});
}
void sendWrapperResult(WrapperFunctionBuffer ResultBytes,
uint64_t CallId) override {
// Respond to a simulated call by the controller.
ConnectGuard CG;
Session::OnControllerCallReturnFn OnComplete;
{
std::scoped_lock<std::mutex> Lock(M);
if (Shutdown) {
assert(PendingIn.empty() && "Shut down but results still pending?");
return;
}
CG = ConnectGuard(this);
auto I = PendingIn.find(CallId);
assert(I != PendingIn.end());
OnComplete = std::move(I->second);
PendingIn.erase(I);
}
runOrPost([OnComplete = std::move(OnComplete),
ResultBytes = std::move(ResultBytes)]() mutable {
OnComplete(std::move(ResultBytes));
});
}
void returnFromController(uint64_t CallId,
WrapperFunctionBuffer ResultBytes) {
ConnectGuard CG;
OnControllerCallReturn OnComplete;
{
std::scoped_lock<std::mutex> Lock(M);
CG = ConnectGuard(this);
auto I = PendingOut.find(CallId);
assert(I != PendingOut.end());
OnComplete = std::move(I->second);
PendingOut.erase(I);
}
handleControllerCallResult(std::move(OnComplete), std::move(ResultBytes));
}
void callFromController(Session::OnControllerCallReturnFn OnComplete,
orc_rt_WrapperFunction Fn,
WrapperFunctionBuffer ArgBytes) {
ConnectGuard CG;
size_t CId = 0;
bool BailOut = false;
{
std::scoped_lock<std::mutex> Lock(M);
if (!Shutdown) {
CG = ConnectGuard(this);
CId = CallId++;
PendingIn[CId] = std::move(OnComplete);
} else
BailOut = true;
}
if (BailOut)
return runOrPost([OnComplete = std::move(OnComplete)]() mutable {
OnComplete(WrapperFunctionBuffer::createOutOfBandError(
"Controller disconnected"));
});
handleWrapperCall(Fn, std::move(ArgBytes), CId);
}
private:
void runOrPost(move_only_function<void()> Work) {
if (Post)
Post(std::move(Work));
else
Work();
}
static void ccReturn(orc_rt_SessionRef S,
orc_rt_WrapperFunctionBuffer ResultBytes,
uint64_t CallId) {
// Abuse "session" to refer to the ControllerAccess object.
// We can just re-use sendFunctionResult for this.
reinterpret_cast<MockControllerAccess *>(S)->returnFromController(
CallId, WrapperFunctionBuffer(ResultBytes));
}
PostFn Post;
std::mutex M;
bool Shutdown = false;
size_t CallId = 0;
size_t Outstanding = 0;
std::unordered_map<size_t, OnControllerCallReturn> PendingOut;
std::unordered_map<size_t, Session::OnControllerCallReturnFn> PendingIn;
std::condition_variable ShutdownCV;
OnConnectFn OnConnect;
};
class CallFromController {
public:
CallFromController(MockControllerAccess &CA, orc_rt_WrapperFunction Fn)
: CA(CA), Fn(Fn) {}
void operator()(Session::OnControllerCallReturnFn OnComplete,
WrapperFunctionBuffer ArgBytes) {
CA.callFromController(std::move(OnComplete), Fn, std::move(ArgBytes));
}
private:
MockControllerAccess &CA;
orc_rt_WrapperFunction Fn;
};
/// Build a PostFn for MockControllerAccess that pushes its work onto the
/// supplied queue. With this, a single QueueingRunner::runFIFOUntilEmpty(Q)
/// call advances both Session-side and controller-side work.
inline MockControllerAccess::PostFn postOnto(QueueingRunner<>::WorkQueue &Q) {
return
[&Q](move_only_function<void()> Work) { Q.push_back(std::move(Work)); };
}
void waitForShutdown(Session &S) {
std::promise<void> P;
auto F = P.get_future();
S.shutdown([P = std::move(P)]() mutable { P.set_value(); });
F.get();
}
TEST(SessionTest, TrivialConstructionAndDestruction) {
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
}
TEST(SessionTest, ReportError) {
Error E = Error::success();
cantFail(std::move(E)); // Force error into checked state.
Session S(mockExecutorProcessInfo(), noDispatch,
[&](Error Err) { E = std::move(Err); });
S.reportError(make_error<StringError>("foo"));
if (E)
EXPECT_EQ(toString(std::move(E)), "foo");
else
ADD_FAILURE() << "Missing error value";
}
TEST(SessionTest, ReportErrorsViaSession) {
Error E = Error::success();
cantFail(std::move(E)); // Force error into checked state.
// Check that the ReportErrorsViaSession utility works as advertised.
Session S(mockExecutorProcessInfo(), noDispatch,
[&](Error Err) { E = std::move(Err); });
(ReportErrorsViaSession(S))(make_error<StringError>("foo"));
if (E)
EXPECT_EQ(toString(std::move(E)), "foo");
else
ADD_FAILURE() << "Missing error value";
}
TEST(SessionTest, SingleService) {
size_t OpIdx = 0;
std::optional<size_t> DetachOpIdx;
std::optional<size_t> ShutdownOpIdx;
{
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
S.addService(
std::make_unique<MockService>(DetachOpIdx, ShutdownOpIdx, OpIdx));
}
EXPECT_EQ(OpIdx, 2U);
EXPECT_EQ(DetachOpIdx, 0U);
EXPECT_EQ(ShutdownOpIdx, 1U);
}
TEST(SessionTest, MultipleServices) {
size_t OpIdx = 0;
std::optional<size_t> DetachOpIdx[3];
std::optional<size_t> ShutdownOpIdx[3];
{
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
for (size_t I = 0; I != 3; ++I)
S.addService(std::make_unique<MockService>(DetachOpIdx[I],
ShutdownOpIdx[I], OpIdx));
}
EXPECT_EQ(OpIdx, 6U);
// Expect shutdown in reverse order.
for (size_t I = 0; I != 3; ++I) {
EXPECT_EQ(DetachOpIdx[I], 2 - I);
EXPECT_EQ(ShutdownOpIdx[I], 5 - I);
}
}
TEST(SessionTest, ScheduleShutdownFromOnDetachHandler) {
// Check that when we schedule a shutdown from an onDetach handler:
// 1. The shutdown is scheduled.
// 2. All onDetach handlers run before any onShutdown handlers.
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
int OnDetachHandlersRun = 0;
bool OnShutdownHandlerRun = false;
S.addOnDetach([&]() { ++OnDetachHandlersRun; });
S.addOnDetach([&]() { S.shutdown(); });
S.addOnDetach([&]() { ++OnDetachHandlersRun; });
S.addOnShutdown([&]() {
EXPECT_EQ(OnDetachHandlersRun, 2);
OnShutdownHandlerRun = true;
});
S.detach();
EXPECT_TRUE(OnShutdownHandlerRun);
}
TEST(SessionTest, RedundantAsyncShutdown) {
// Check that redundant calls to shutdown have their callbacks run.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
// Initiate shutdown here, and wait for the on-shutdown callbacks to start
// running.
waitForShutdown(S);
// Now try to add a new on-shutdown callback and verify that it runs.
bool RedundantCallbackRan = false;
S.shutdown([&]() { RedundantCallbackRan = true; });
EXPECT_TRUE(RedundantCallbackRan);
}
TEST(SessionTest, ExpectedShutdownSequenceWithNoActiveManagedCodeCalls) {
// Check that Session shutdown results in...
// 1. Services being shut down.
// 2. A call to OnShutdownComplete.
size_t OpIdx = 0;
std::optional<size_t> DetachOpIdx;
std::optional<size_t> ShutdownOpIdx;
bool SessionShutdownComplete = false;
{
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
S.addService(
std::make_unique<MockService>(DetachOpIdx, ShutdownOpIdx, OpIdx));
S.shutdown([&]() {
EXPECT_TRUE(ShutdownOpIdx);
EXPECT_EQ(*ShutdownOpIdx, 1);
SessionShutdownComplete = true;
});
}
EXPECT_TRUE(SessionShutdownComplete);
}
TEST(SessionTest, ActiveManagedCallsDelayShutdown) {
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
size_t OpIdx = 0;
std::optional<size_t> DetachOpIdx;
std::optional<size_t> ShutdownOpIdx;
S.createService<MockService>(DetachOpIdx, ShutdownOpIdx, OpIdx);
ASSERT_FALSE(DetachOpIdx);
ASSERT_FALSE(ShutdownOpIdx);
// Take a managed code call token. This should succeed.
auto Tok = TaskGroup::Token(S.managedCodeTokenSource());
ASSERT_TRUE(Tok);
// We expect shutdown to wait for any active managed calls to complete.
bool ShutdownComplete = false;
S.shutdown([&]() { ShutdownComplete = true; });
// Detach should have happened, but shutdown should be waiting on token.
EXPECT_EQ(DetachOpIdx, 0U);
EXPECT_FALSE(ShutdownOpIdx);
EXPECT_FALSE(ShutdownComplete);
// The managed calls code group should have been closed. Assert that we
// can't get a new token.
ASSERT_FALSE(TaskGroup::Token(S.managedCodeTokenSource()));
Tok = TaskGroup::Token(); // Reset token.
EXPECT_EQ(ShutdownOpIdx, 1U);
EXPECT_TRUE(ShutdownComplete);
}
static void managedVoidFunction(int *P) { *P = 42; }
TEST(SessionTest, CallManagedCodeVoidFn) {
// Test calls to a void function while holding a ManagedCodeTaskGroup token.
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
{
// Pre-shutdown we expect token acquisition to succeed and the function to
// run.
int X = 0;
bool CallSucceeded = S.callManagedCode(managedVoidFunction, &X);
EXPECT_TRUE(CallSucceeded);
EXPECT_EQ(X, 42U);
}
waitForShutdown(S);
{
// Post-shutdown we expect token acquisition to fail, and
// callManagedCode to return false.
int X = 0;
bool CallSucceeded = S.callManagedCode(managedVoidFunction, &X);
EXPECT_FALSE(CallSucceeded);
}
}
static int managedNonVoidFunction(int N) { return N + 1; }
TEST(SessionTest, CallManagedCodeNonVoidFn) {
// Test calls to a non-void function while holding a ManagedCodeTaskGroup
// token.
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
{
// Pre-shutdown we expect token acquisition to succeed, the function to be
// run, and the result to be returned.
auto Result = S.callManagedCode(managedNonVoidFunction, 41);
EXPECT_TRUE(Result);
EXPECT_EQ(*Result, 42U);
}
waitForShutdown(S);
{
// Post-shutdown we expect token acquisition to fail, and
// callManagedCode to return std::nullopt.
auto Result = S.callManagedCode(managedNonVoidFunction, 41);
EXPECT_EQ(Result, std::nullopt);
}
}
static void managedAsyncFunction(move_only_function<void(int)> Return, int *P) {
Return(++*P);
}
TEST(SessionTest, CallManagedCodeAsyncFn) {
// Test that calls to managed async functions via callManagedCode work.
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
{
// Pre-shutdown we expect token acquisition to succeed, and the function
// and Return callback to be run.
int N = 41;
std::optional<int> Result;
S.callManagedCode(managedAsyncFunction, [&](int N) { Result = N; }, &N);
EXPECT_TRUE(Result);
EXPECT_EQ(*Result, 42U);
EXPECT_EQ(N, 42U);
}
waitForShutdown(S);
{
// Post-shutdown we expect token acquisition to fail. Return should be
// with `std::nullopt` and the function should not be called.
int N = 41;
std::optional<int> Result;
S.callManagedCode(managedAsyncFunction, [&](int N) { Result = N; }, &N);
EXPECT_EQ(Result, std::nullopt);
EXPECT_EQ(N, 41U);
}
}
TEST(SessionTest, AddServiceAndUseRef) {
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
auto &CS = S.addService(std::make_unique<ConfigurableService>(42));
CS.doMoreConfig(1);
}
TEST(SessionTest, CreateServiceAndUseRef) {
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
auto &CS = S.createService<ConfigurableService>(42);
CS.doMoreConfig(1);
}
TEST(SessionTest, TryCreateServiceSuccess) {
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
auto CS = S.tryCreateService<ConfigurableService>(false);
if (auto Err = CS.takeError()) {
ADD_FAILURE() << "expected service creation to succeed";
consumeError(std::move(Err));
}
}
TEST(SessionTest, TryCreateServiceFailure) {
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
auto CS = S.tryCreateService<ConfigurableService>(true);
if (auto Err = CS.takeError())
consumeError(std::move(Err));
else
ADD_FAILURE() << "expected service creation to fail";
}
TEST(ControllerAccessTest, Basics) {
// Test that we can set the ControllerAccess implementation and still shut
// down as expected.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
S.attach<MockControllerAccess>(BootstrapInfo(S), postOnto(Tasks));
QueueingRunner<>::runFIFOUntilEmpty(Tasks);
}
// A ControllerAccess that is attached but whose controller connection is
// already gone, so every callController fails synchronously. Used to exercise
// the failControllerCallInline path in isolation.
class DeadControllerAccess : public Session::ControllerAccess {
public:
DeadControllerAccess(Session &S) : ControllerAccess(S) {}
void connect(BootstrapInfo) override {}
void disconnect() override { notifyDisconnected(); }
void callController(OnControllerCallReturn OnComplete,
orc_rt_ControllerHandlerTag,
WrapperFunctionBuffer) override {
failControllerCallInline(std::move(OnComplete));
}
void sendWrapperResult(WrapperFunctionBuffer, uint64_t) override {}
};
TEST(ControllerAccessTest, SynchronousCallControllerFailureRunsInline) {
// A controller call that fails synchronously (connection already gone) must
// complete its handler inline -- on the calling thread, before callController
// returns, and WITHOUT going through Dispatch -- with the canonical
// "disconnected" out-of-band error. noDispatch ADD_FAILUREs if anything is
// dispatched, so it doubles as the "ran inline, not via the task queue"
// check.
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
S.attach<DeadControllerAccess>(BootstrapInfo(S));
bool HandlerRan = false;
std::string ErrMsg;
S.callController(
[&](WrapperFunctionBuffer Result) {
HandlerRan = true;
if (const char *E = Result.getOutOfBandError())
ErrMsg = E;
},
/*T=*/nullptr, WrapperFunctionBuffer());
// Set synchronously, during the call above -> the handler ran inline.
EXPECT_TRUE(HandlerRan);
EXPECT_EQ(ErrMsg, "disconnected");
}
static void add_sps_wrapper(orc_rt_SessionRef S,
orc_rt_WrapperFunctionBuffer ArgBytes,
orc_rt_WrapperFunctionReturn Return,
uint64_t CallId) {
SPSWrapperFunction<int32_t(int32_t, int32_t)>::handle(
S, ArgBytes, Return, CallId,
[](move_only_function<void(int32_t)> Return, int32_t X, int32_t Y) {
Return(X + Y);
});
}
// Wrapper handler that echoes its argument bytes straight back as the result.
// Used to exercise the C callController entry point without pulling in SPS
// (de)serialization -- the payload is opaque to the code under test.
static void echo_wrapper(orc_rt_SessionRef S,
orc_rt_WrapperFunctionBuffer ArgBytes,
orc_rt_WrapperFunctionReturn Return, uint64_t CallId) {
Return(S, ArgBytes, CallId);
}
// Captures what orc_rt_Session_callController threads back to its C return
// function, so the test can inspect it after the call completes.
struct CAPICallControllerResult {
bool Ran = false;
orc_rt_SessionRef S = nullptr;
std::string Result;
};
static void capiCallControllerReturn(orc_rt_SessionRef S,
orc_rt_WrapperFunctionBuffer ResultBytes,
void *Ctx) {
auto &R = *static_cast<CAPICallControllerResult *>(Ctx);
R.Ran = true;
R.S = S;
WrapperFunctionBuffer Result(ResultBytes);
R.Result.assign(Result.data(), Result.size());
}
TEST(ControllerAccessTest, CAPICallControllerRoundTrip) {
// Drive a controller call through the C API entry point
// (orc_rt_Session_callController) and confirm that the result bytes, the
// session handle, and the caller-supplied context are all threaded back to
// the C return function.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
S.attach<MockControllerAccess>(BootstrapInfo(S), postOnto(Tasks));
CAPICallControllerResult R;
orc_rt_Session_callController(
wrap(&S), reinterpret_cast<orc_rt_ControllerHandlerTag>(&echo_wrapper),
WrapperFunctionBuffer::copyFrom("hello", 5).release(),
&capiCallControllerReturn, &R);
QueueingRunner<>::runFIFOUntilEmpty(Tasks);
EXPECT_TRUE(R.Ran) << "C return function never fired";
EXPECT_EQ(R.S, wrap(&S)) << "Session handle not threaded through";
EXPECT_EQ(R.Result, "hello") << "Result bytes not round-tripped";
}
TEST(ControllerAccessTest, ValidCallToController) {
// Simulate a call to a controller handler.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
S.attach<MockControllerAccess>(BootstrapInfo(S), postOnto(Tasks));
int32_t Result = 0;
SPSWrapperFunction<int32_t(int32_t, int32_t)>::call(
S.controllerCaller(
reinterpret_cast<orc_rt_ControllerHandlerTag>(add_sps_wrapper)),
[&](Expected<int32_t> R) { Result = cantFail(std::move(R)); }, 41, 1);
QueueingRunner<>::runFIFOUntilEmpty(Tasks);
EXPECT_EQ(Result, 42);
}
TEST(ControllerAccessTest, CallToControllerBeforeAttach) {
// Expect calls to the controller prior to attaching to fail.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
Error Err = Error::success();
SPSWrapperFunction<int32_t(int32_t, int32_t)>::call(
S.controllerCaller(
reinterpret_cast<orc_rt_ControllerHandlerTag>(add_sps_wrapper)),
[&](Expected<int32_t> R) {
ErrorAsOutParameter _(Err);
Err = R.takeError();
},
41, 1);
EXPECT_EQ(toString(std::move(Err)), "no controller attached");
}
TEST(ControllerAccessTest, CallToControllerAfterDetach) {
// Expect calls to the controller prior to attaching to fail.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
S.attach<MockControllerAccess>(BootstrapInfo(S), postOnto(Tasks));
S.detach();
Error Err = Error::success();
SPSWrapperFunction<int32_t(int32_t, int32_t)>::call(
S.controllerCaller(
reinterpret_cast<orc_rt_ControllerHandlerTag>(add_sps_wrapper)),
[&](Expected<int32_t> R) {
ErrorAsOutParameter _(Err);
Err = R.takeError();
},
41, 1);
EXPECT_EQ(toString(std::move(Err)), "no controller attached");
}
TEST(ControllerAccessTest, CallFromController) {
// Simulate a call from the controller.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
MockControllerAccess *CA = nullptr;
S.attach<MockControllerAccess>(BootstrapInfo(S), postOnto(Tasks),
MockControllerAccess::OnConnectFn{}, &CA);
int32_t Result = 0;
SPSWrapperFunction<int32_t(int32_t, int32_t)>::call(
CallFromController(*CA, add_sps_wrapper),
[&](Expected<int32_t> R) { Result = cantFail(std::move(R)); }, 41, 1);
QueueingRunner<>::runFIFOUntilEmpty(Tasks);
EXPECT_EQ(Result, 42);
}
// Stashes Return, via the address passed as the wrapper call's argument,
// instead of calling it -- simulating a wrapper function that defers
// completion past its own return.
static void deferred_wrapper(orc_rt_SessionRef S,
orc_rt_WrapperFunctionBuffer ArgBytes,
orc_rt_WrapperFunctionReturn Return,
uint64_t CallId) {
SPSWrapperFunction<void(SPSExecutorAddr)>::handle(
S, ArgBytes, Return, CallId,
[](move_only_function<void()> Return, ExecutorAddr P) {
*P.toPtr<move_only_function<void()> *>() = std::move(Return);
});
}
TEST(ControllerAccessTest, WrapperCallTokenReleasedWhenFnReturns) {
// A managed-code token acquired for an incoming wrapper call must bracket
// only the (synchronous) span of Fn's execution -- not the whole
// call/response chain -- per the "Managed code execution and shutdown"
// policy in docs/Design.md. Check this by having Fn defer its Return call
// past its own return, then confirming that Session shutdown's drain phase
// does not wait on that deferred call.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
MockControllerAccess *CA = nullptr;
S.attach<MockControllerAccess>(BootstrapInfo(S), postOnto(Tasks),
MockControllerAccess::OnConnectFn{}, &CA);
move_only_function<void()> DeferredReturn;
bool GotResult = false;
SPSWrapperFunction<void(SPSExecutorAddr)>::call(
CallFromController(*CA, deferred_wrapper),
[&](Error Err) {
cantFail(std::move(Err));
GotResult = true;
},
&DeferredReturn);
QueueingRunner<>::runFIFOUntilEmpty(Tasks);
// Fn ran and deferred its Return call, so no result should have been sent
// back yet.
ASSERT_TRUE(DeferredReturn);
EXPECT_FALSE(GotResult);
// Fn has already returned, so its managed-code token should have been
// released even though the call is still logically outstanding. Shutdown's
// drain phase should therefore complete without waiting on the deferred
// Return call.
bool ShutdownComplete = false;
S.shutdown([&] { ShutdownComplete = true; });
EXPECT_TRUE(ShutdownComplete);
}
TEST(ControllerAccessTest, FailConnect) {
// Simulate failure to connect.
bool GotError = false;
std::string ErrMsg = "failed to connect";
Session S(mockExecutorProcessInfo(), noDispatch, [&](Error Err) {
GotError = true;
EXPECT_EQ(toString(std::move(Err)), ErrMsg);
});
BootstrapInfo BI(S);
S.attach<MockControllerAccess>(
std::move(BI), MockControllerAccess::PostFn{},
[&](BootstrapInfo &BI) { return make_error<StringError>(ErrMsg); });
ASSERT_TRUE(GotError);
}
TEST(ControllerAccessTest, BootstrapInfoPassedToConnect) {
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
// Test values.
constexpr const char *SymName = "test_sym";
const char Sym = '.';
constexpr const char *SecretKey = "luggage_combo";
constexpr const char *SecretValue = "12345";
// Build a BootstrapInfo with custom symbols and values.
BootstrapInfo BI(S);
std::pair<const char *, const void *> TestSyms[] = {
{SymName, static_cast<const void *>(&Sym)}};
cantFail(BI.symbols().addUnique(TestSyms));
BI.values()[SecretKey] = SecretValue;
bool OnConnectRan = false;
S.attach<MockControllerAccess>(
std::move(BI), MockControllerAccess::PostFn{}, [&](BootstrapInfo &BI) {
EXPECT_EQ(BI.symbols().at(SymName), static_cast<const void *>(&Sym));
EXPECT_EQ(BI.values().at(SecretKey), SecretValue);
OnConnectRan = true;
return Error::success();
});
ASSERT_TRUE(OnConnectRan);
}
TEST(ControllerAccessTest, TryAttachSuccess) {
// A successful Create attaches the controller, which then services calls
// just like one attached via attach<T>.
QueueingRunner<>::WorkQueue Tasks;
Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors);
cantFail(S.tryAttach<MockControllerAccess>(BootstrapInfo(S), /*Fail=*/false,
postOnto(Tasks)));
int32_t Result = 0;
SPSWrapperFunction<int32_t(int32_t, int32_t)>::call(
S.controllerCaller(
reinterpret_cast<orc_rt_ControllerHandlerTag>(add_sps_wrapper)),
[&](Expected<int32_t> R) { Result = cantFail(std::move(R)); }, 41, 1);
QueueingRunner<>::runFIFOUntilEmpty(Tasks);
EXPECT_EQ(Result, 42);
}
TEST(ControllerAccessTest, TryAttachFailure) {
// A failing Create surfaces its Error and leaves the Session unattached.
Session S(mockExecutorProcessInfo(), noDispatch, noErrors);
auto Err = S.tryAttach<MockControllerAccess>(BootstrapInfo(S), /*Fail=*/true);
ASSERT_TRUE(static_cast<bool>(Err));
EXPECT_EQ(toString(std::move(Err)), "failed to create controller access");
// Since nothing was attached, calls to the controller should fail as they
// would before any attach.
Error CallErr = Error::success();
SPSWrapperFunction<int32_t(int32_t, int32_t)>::call(
S.controllerCaller(
reinterpret_cast<orc_rt_ControllerHandlerTag>(add_sps_wrapper)),
[&](Expected<int32_t> R) {
ErrorAsOutParameter _(CallErr);
CallErr = R.takeError();
},
41, 1);
EXPECT_EQ(toString(std::move(CallErr)), "no controller attached");
}