blob: a73b77967782463bbe87a789dd4575258fd1c52d [file] [edit]
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
#include "lldb/Target/TargetAPIMutex.h"
#include "Plugins/Platform/Linux/PlatformLinux.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/HostInfo.h"
#include "lldb/Target/Platform.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/Policy.h"
#include "gtest/gtest.h"
#include <thread>
using namespace lldb_private;
using namespace lldb;
TEST(TargetAPIMutexTest, DefaultConstructedIsANoOp) {
// No synchronization primitive is touched at all in this state, so
// there is no pairing requirement: try_lock() always succeeds, and
// lock()/unlock() are callable with no invariant to violate.
TargetAPIMutex lock;
EXPECT_TRUE(lock.try_lock());
lock.lock();
lock.unlock();
lock.lock();
lock.unlock();
}
namespace {
class TargetAPIMutexTargetTest : public ::testing::Test {
public:
void SetUp() override {
FileSystem::Initialize();
HostInfo::Initialize();
platform_linux::PlatformLinux::Initialize();
}
void TearDown() override {
platform_linux::PlatformLinux::Terminate();
HostInfo::Terminate();
FileSystem::Terminate();
}
};
TargetSP CreateTarget() {
ArchSpec arch("x86_64-pc-linux");
Platform::SetHostPlatform(
platform_linux::PlatformLinux::CreateInstance(true, &arch));
DebuggerSP debugger_sp = Debugger::CreateInstance();
TargetSP target_sp;
PlatformSP platform_sp;
Status error = debugger_sp->GetTargetList().CreateTarget(
*debugger_sp, "", arch, eLoadDependentsNo, platform_sp, target_sp);
return target_sp;
}
} // namespace
TEST_F(TargetAPIMutexTargetTest, WrapsTheTargetMutex) {
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
TargetAPIMutex lock(target_sp);
lock.lock();
// Recursive reentrancy is delegated straight to the underlying
// std::recursive_mutex: a second handle over the same target, locked
// from the same thread, must not block.
TargetAPIMutex second_lock(target_sp);
EXPECT_TRUE(second_lock.try_lock());
second_lock.unlock();
lock.unlock();
std::thread t([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_TRUE(background_lock.try_lock());
background_lock.unlock();
});
t.join();
}
TEST_F(TargetAPIMutexTargetTest, RealMutexBlocksOtherThreads) {
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
TargetAPIMutex lock(target_sp);
lock.lock();
std::thread t([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_FALSE(background_lock.try_lock());
});
t.join();
lock.unlock();
}
TEST_F(TargetAPIMutexTargetTest, BareHandleDoesNotAutoUnlockOnDestruction) {
// TargetAPIMutex carries no RAII of its own -- exactly like
// std::recursive_mutex, a bare handle going out of scope without an
// explicit unlock() leaves the real mutex held. Callers that want
// scope-based release must wrap it in std::lock_guard/std::unique_lock.
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
{
TargetAPIMutex lock(target_sp);
lock.lock();
}
std::thread t([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_FALSE(background_lock.try_lock());
});
t.join();
// Unlock the original locked mutex.
// Calling try_lock() resolves the underlying mutex and re-enters it on this
// thread (incrementing the recursive count), so we unlock twice to fully
// release both acquisitions.
TargetAPIMutex cleanup_lock(target_sp);
ASSERT_TRUE(cleanup_lock.try_lock());
cleanup_lock.unlock();
cleanup_lock.unlock();
}
TEST_F(TargetAPIMutexTargetTest, LockGuardReleasesOnScopeExit) {
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
{
TargetAPIMutex lock(target_sp);
std::lock_guard<TargetAPIMutex> guard(lock);
}
std::thread t([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_TRUE(background_lock.try_lock());
background_lock.unlock();
});
t.join();
}
TEST_F(TargetAPIMutexTargetTest, MoveTransfersOwnership) {
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
TargetAPIMutex lock(target_sp);
lock.lock();
TargetAPIMutex moved(std::move(lock));
// The moved-from handle no longer references the real mutex: unlocking
// it is a no-op, so the mutex stays held until `moved` releases it.
lock.unlock();
std::thread contended([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_FALSE(background_lock.try_lock());
});
contended.join();
moved.unlock();
std::thread released([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_TRUE(background_lock.try_lock());
background_lock.unlock();
});
released.join();
}
TEST_F(TargetAPIMutexTargetTest, ResolvesFreshOnEachLockCall) {
// lock()/try_lock() re-resolve the real mutex on every call rather
// than caching a single resolution for the handle's lifetime: a
// handle can be locked, unlocked, and locked again, each time
// correctly contending with other threads for the same target mutex.
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
TargetAPIMutex lock(target_sp);
lock.lock();
lock.unlock();
std::thread t([target_sp]() {
TargetAPIMutex background_lock(target_sp);
background_lock.lock();
background_lock.unlock();
});
t.join();
lock.lock();
std::thread contended([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_FALSE(background_lock.try_lock());
});
contended.join();
lock.unlock();
}
TEST_F(TargetAPIMutexTargetTest,
UnlockReplaysLockResolutionAcrossPolicyChange) {
// lock() and unlock() must agree on which mutex they touch even if the
// calling thread's policy changes in between: unlock() replays lock()'s
// resolution rather than re-resolving from the current policy.
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
TargetAPIMutex lock(target_sp);
lock.lock();
// If unlock() re-resolved here it would see the bypass and skip releasing
// the mutex it actually locked.
{
PolicyStack::Guard guard = PolicyStack::Get().PushScriptedExtensionCall();
lock.unlock();
}
// The real mutex must have actually been released: a fresh acquisition
// from a different thread (outside the bypass policy) must succeed
// immediately. A same-thread try_lock() would pass even if unlock() had
// incorrectly no-op'd, since std::recursive_mutex lets the same thread
// reenter a lock it still holds.
std::thread t([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_TRUE(background_lock.try_lock());
background_lock.unlock();
});
t.join();
}
TEST_F(TargetAPIMutexTargetTest, BypassPolicyMakesTryLockANoOp) {
TargetSP target_sp = CreateTarget();
ASSERT_TRUE(target_sp);
TargetAPIMutex holder(target_sp);
holder.lock();
// The contention has to come from another thread: std::recursive_mutex lets
// the owning thread reenter a lock it already holds, so a same-thread
// try_lock() would succeed whether or not the bypass is in effect.
std::thread contended([target_sp]() {
TargetAPIMutex background_lock(target_sp);
EXPECT_FALSE(background_lock.try_lock());
});
contended.join();
// The bypass touches no primitive, so the same acquisition succeeds while
// the real mutex is held elsewhere.
std::thread bypassed([target_sp]() {
PolicyStack::Guard guard = PolicyStack::Get().PushScriptedExtensionCall();
TargetAPIMutex background_lock(target_sp);
EXPECT_TRUE(background_lock.try_lock());
background_lock.unlock();
});
bypassed.join();
holder.unlock();
}