//===- 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 #include #include #include 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 &DetachOpIdx, std::optional &ShutdownOpIdx, size_t &OpIdx, move_only_function 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 &DetachOpIdx; std::optional &ShutdownOpIdx; size_t &OpIdx; move_only_function GenResult; }; class ConfigurableService : public Service { public: ConfigurableService(int ConstructorOption) {} /// Fallible named constructor for testing tryCreateService. static Expected> Create(bool Fail) { if (Fail) return make_error("failed to create service"); return std::make_unique(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; /// 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)>; 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> Create(Session &S, bool Fail, PostFn Post = {}, OnConnectFn OnConnect = {}) { if (Fail) return make_error("failed to create controller access"); return std::make_shared(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 ToDrain; { std::unique_lock 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 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(T); Fn(reinterpret_cast(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 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 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 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 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(S)->returnFromController( CallId, WrapperFunctionBuffer(ResultBytes)); } PostFn Post; std::mutex M; bool Shutdown = false; size_t CallId = 0; size_t Outstanding = 0; std::unordered_map PendingOut; std::unordered_map 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 Work) { Q.push_back(std::move(Work)); }; } void waitForShutdown(Session &S) { std::promise 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("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("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 DetachOpIdx; std::optional ShutdownOpIdx; { Session S(mockExecutorProcessInfo(), noDispatch, noErrors); S.addService( std::make_unique(DetachOpIdx, ShutdownOpIdx, OpIdx)); } EXPECT_EQ(OpIdx, 2U); EXPECT_EQ(DetachOpIdx, 0U); EXPECT_EQ(ShutdownOpIdx, 1U); } TEST(SessionTest, MultipleServices) { size_t OpIdx = 0; std::optional DetachOpIdx[3]; std::optional ShutdownOpIdx[3]; { Session S(mockExecutorProcessInfo(), noDispatch, noErrors); for (size_t I = 0; I != 3; ++I) S.addService(std::make_unique(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 DetachOpIdx; std::optional ShutdownOpIdx; bool SessionShutdownComplete = false; { QueueingRunner<>::WorkQueue Tasks; Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors); S.addService( std::make_unique(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 DetachOpIdx; std::optional ShutdownOpIdx; S.createService(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 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 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 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(42)); CS.doMoreConfig(1); } TEST(SessionTest, CreateServiceAndUseRef) { Session S(mockExecutorProcessInfo(), noDispatch, noErrors); auto &CS = S.createService(42); CS.doMoreConfig(1); } TEST(SessionTest, TryCreateServiceSuccess) { Session S(mockExecutorProcessInfo(), noDispatch, noErrors); auto CS = S.tryCreateService(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(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(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(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::handle( S, ArgBytes, Return, CallId, [](move_only_function 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(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(BootstrapInfo(S), postOnto(Tasks)); CAPICallControllerResult R; orc_rt_Session_callController( wrap(&S), reinterpret_cast(&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(BootstrapInfo(S), postOnto(Tasks)); int32_t Result = 0; SPSWrapperFunction::call( S.controllerCaller( reinterpret_cast(add_sps_wrapper)), [&](Expected 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::call( S.controllerCaller( reinterpret_cast(add_sps_wrapper)), [&](Expected 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(BootstrapInfo(S), postOnto(Tasks)); S.detach(); Error Err = Error::success(); SPSWrapperFunction::call( S.controllerCaller( reinterpret_cast(add_sps_wrapper)), [&](Expected 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(BootstrapInfo(S), postOnto(Tasks), MockControllerAccess::OnConnectFn{}, &CA); int32_t Result = 0; SPSWrapperFunction::call( CallFromController(*CA, add_sps_wrapper), [&](Expected 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::handle( S, ArgBytes, Return, CallId, [](move_only_function Return, ExecutorAddr P) { *P.toPtr *>() = 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(BootstrapInfo(S), postOnto(Tasks), MockControllerAccess::OnConnectFn{}, &CA); move_only_function DeferredReturn; bool GotResult = false; SPSWrapperFunction::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( std::move(BI), MockControllerAccess::PostFn{}, [&](BootstrapInfo &BI) { return make_error(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 TestSyms[] = { {SymName, static_cast(&Sym)}}; cantFail(BI.symbols().addUnique(TestSyms)); BI.values()[SecretKey] = SecretValue; bool OnConnectRan = false; S.attach( std::move(BI), MockControllerAccess::PostFn{}, [&](BootstrapInfo &BI) { EXPECT_EQ(BI.symbols().at(SymName), static_cast(&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. QueueingRunner<>::WorkQueue Tasks; Session S(mockExecutorProcessInfo(), QueueingRunner(Tasks), noErrors); cantFail(S.tryAttach(BootstrapInfo(S), /*Fail=*/false, postOnto(Tasks))); int32_t Result = 0; SPSWrapperFunction::call( S.controllerCaller( reinterpret_cast(add_sps_wrapper)), [&](Expected 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(BootstrapInfo(S), /*Fail=*/true); ASSERT_TRUE(static_cast(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::call( S.controllerCaller( reinterpret_cast(add_sps_wrapper)), [&](Expected R) { ErrorAsOutParameter _(CallErr); CallErr = R.takeError(); }, 41, 1); EXPECT_EQ(toString(std::move(CallErr)), "no controller attached"); }