mirror of
https://github.com/Laupetin/OpenAssetTools.git
synced 2025-04-19 15:52:53 +00:00
test: add test for KeyValuePairsCompilerT6
This commit is contained in:
parent
16e82f68ca
commit
fc9e6ce14d
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@ -49,6 +49,7 @@ jobs:
|
|||||||
working-directory: ${{ github.workspace }}/build/lib/Release_x86/tests
|
working-directory: ${{ github.workspace }}/build/lib/Release_x86/tests
|
||||||
run: |
|
run: |
|
||||||
./ObjCommonTests
|
./ObjCommonTests
|
||||||
|
./ObjCompilingTests
|
||||||
./ObjLoadingTests
|
./ObjLoadingTests
|
||||||
./ParserTests
|
./ParserTests
|
||||||
./ZoneCodeGeneratorLibTests
|
./ZoneCodeGeneratorLibTests
|
||||||
@ -86,6 +87,8 @@ jobs:
|
|||||||
$combinedExitCode = 0
|
$combinedExitCode = 0
|
||||||
./ObjCommonTests
|
./ObjCommonTests
|
||||||
$combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE)
|
$combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE)
|
||||||
|
./ObjCompilingTests
|
||||||
|
$combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE)
|
||||||
./ObjLoadingTests
|
./ObjLoadingTests
|
||||||
$combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE)
|
$combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE)
|
||||||
./ParserTests
|
./ParserTests
|
||||||
|
@ -172,6 +172,7 @@ group ""
|
|||||||
-- ========================
|
-- ========================
|
||||||
include "test/ObjCommonTestUtils.lua"
|
include "test/ObjCommonTestUtils.lua"
|
||||||
include "test/ObjCommonTests.lua"
|
include "test/ObjCommonTests.lua"
|
||||||
|
include "test/ObjCompilingTests.lua"
|
||||||
include "test/ObjLoadingTests.lua"
|
include "test/ObjLoadingTests.lua"
|
||||||
include "test/ParserTestUtils.lua"
|
include "test/ParserTestUtils.lua"
|
||||||
include "test/ParserTests.lua"
|
include "test/ParserTests.lua"
|
||||||
@ -182,6 +183,7 @@ include "test/ZoneCommonTests.lua"
|
|||||||
group "Tests"
|
group "Tests"
|
||||||
ObjCommonTestUtils:project()
|
ObjCommonTestUtils:project()
|
||||||
ObjCommonTests:project()
|
ObjCommonTests:project()
|
||||||
|
ObjCompilingTests:project()
|
||||||
ObjLoadingTests:project()
|
ObjLoadingTests:project()
|
||||||
ParserTestUtils:project()
|
ParserTestUtils:project()
|
||||||
ParserTests:project()
|
ParserTests:project()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Game/T6/CommonT6.h"
|
#include "Game/T6/CommonT6.h"
|
||||||
#include "Game/T6/T6.h"
|
#include "Game/T6/T6.h"
|
||||||
|
#include "KeyValuePairs/KeyValuePairsCreator.h"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <format>
|
#include <format>
|
||||||
@ -9,29 +10,31 @@
|
|||||||
|
|
||||||
using namespace T6;
|
using namespace T6;
|
||||||
|
|
||||||
KeyValuePairsCompiler::KeyValuePairsCompiler(MemoryManager& memory,
|
namespace
|
||||||
const Zone& zone,
|
{
|
||||||
const ZoneDefinition& zoneDefinition,
|
class KeyValuePairsCompiler final : public IAssetCreator
|
||||||
ZoneAssetCreationStateContainer& zoneStates)
|
{
|
||||||
|
public:
|
||||||
|
KeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates)
|
||||||
: m_memory(memory),
|
: m_memory(memory),
|
||||||
m_zone(zone),
|
m_zone(zone),
|
||||||
m_zone_definition(zoneDefinition),
|
m_zone_definition(zoneDefinition),
|
||||||
m_kvp_creator(zoneStates.GetZoneAssetCreationState<KeyValuePairsCreator>())
|
m_kvp_creator(zoneStates.GetZoneAssetCreationState<KeyValuePairsCreator>())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<asset_type_t> KeyValuePairsCompiler::GetHandlingAssetType() const
|
[[nodiscard]] std::optional<asset_type_t> GetHandlingAssetType() const override
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetCreationResult KeyValuePairsCompiler::CreateAsset(const std::string& assetName, AssetCreationContext& context)
|
AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override
|
||||||
{
|
{
|
||||||
return AssetCreationResult::NoAction();
|
return AssetCreationResult::NoAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyValuePairsCompiler::FinalizeZone(AssetCreationContext& context)
|
void FinalizeZone(AssetCreationContext& context) override
|
||||||
{
|
{
|
||||||
m_kvp_creator.Finalize(m_zone_definition);
|
m_kvp_creator.Finalize(m_zone_definition);
|
||||||
const auto commonKvps = m_kvp_creator.GetFinalKeyValuePairs();
|
const auto commonKvps = m_kvp_creator.GetFinalKeyValuePairs();
|
||||||
if (commonKvps.empty())
|
if (commonKvps.empty())
|
||||||
@ -59,4 +62,21 @@ void KeyValuePairsCompiler::FinalizeZone(AssetCreationContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.AddAsset(AssetRegistration<AssetKeyValuePairs>(m_zone.m_name, gameKvps));
|
context.AddAsset(AssetRegistration<AssetKeyValuePairs>(m_zone.m_name, gameKvps));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
MemoryManager& m_memory;
|
||||||
|
const Zone& m_zone;
|
||||||
|
const ZoneDefinition& m_zone_definition;
|
||||||
|
KeyValuePairsCreator& m_kvp_creator;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace T6
|
||||||
|
{
|
||||||
|
std::unique_ptr<IAssetCreator>
|
||||||
|
CreateKeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates)
|
||||||
|
{
|
||||||
|
return std::make_unique<KeyValuePairsCompiler>(memory, zone, zoneDefinition, zoneStates);
|
||||||
|
}
|
||||||
|
} // namespace T6
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Asset/IAssetCreator.h"
|
#include "Asset/IAssetCreator.h"
|
||||||
#include "Game/T6/T6.h"
|
#include "Asset/IZoneAssetCreationState.h"
|
||||||
#include "KeyValuePairs/KeyValuePairsCreator.h"
|
#include "SearchPath/ISearchPath.h"
|
||||||
#include "Utils/MemoryManager.h"
|
#include "Utils/MemoryManager.h"
|
||||||
#include "Zone/Definition/ZoneDefinition.h"
|
#include "Zone/Definition/ZoneDefinition.h"
|
||||||
#include "Zone/Zone.h"
|
#include "Zone/Zone.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
namespace T6
|
namespace T6
|
||||||
{
|
{
|
||||||
class KeyValuePairsCompiler final : public IAssetCreator
|
std::unique_ptr<IAssetCreator>
|
||||||
{
|
CreateKeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates);
|
||||||
public:
|
|
||||||
KeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates);
|
|
||||||
|
|
||||||
[[nodiscard]] std::optional<asset_type_t> GetHandlingAssetType() const override;
|
|
||||||
AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override;
|
|
||||||
void FinalizeZone(AssetCreationContext& context) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
MemoryManager& m_memory;
|
|
||||||
const Zone& m_zone;
|
|
||||||
const ZoneDefinition& m_zone_definition;
|
|
||||||
KeyValuePairsCreator& m_kvp_creator;
|
|
||||||
};
|
|
||||||
} // namespace T6
|
} // namespace T6
|
||||||
|
@ -20,7 +20,7 @@ namespace
|
|||||||
{
|
{
|
||||||
auto& memory = *zone.GetMemory();
|
auto& memory = *zone.GetMemory();
|
||||||
|
|
||||||
collection.AddAssetCreator(std::make_unique<KeyValuePairsCompiler>(memory, zone, zoneDefinition.m_zone_definition, zoneStates));
|
collection.AddAssetCreator(CreateKeyValuePairsCompiler(memory, zone, zoneDefinition.m_zone_definition, zoneStates));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigurePostProcessors(AssetCreatorCollection& collection,
|
void ConfigurePostProcessors(AssetCreatorCollection& collection,
|
||||||
|
@ -6,6 +6,33 @@
|
|||||||
|
|
||||||
class MemoryManager
|
class MemoryManager
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
|
MemoryManager();
|
||||||
|
virtual ~MemoryManager();
|
||||||
|
MemoryManager(const MemoryManager& other) = delete;
|
||||||
|
MemoryManager(MemoryManager&& other) noexcept = default;
|
||||||
|
MemoryManager& operator=(const MemoryManager& other) = delete;
|
||||||
|
MemoryManager& operator=(MemoryManager&& other) noexcept = default;
|
||||||
|
|
||||||
|
void* AllocRaw(size_t size);
|
||||||
|
char* Dup(const char* str);
|
||||||
|
|
||||||
|
template<typename T> std::add_pointer_t<T> Alloc(const size_t count = 1u)
|
||||||
|
{
|
||||||
|
return static_cast<std::add_pointer_t<T>>(AllocRaw(sizeof(T) * count));
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T, class... ValType> std::add_pointer_t<T> Create(ValType&&... val)
|
||||||
|
{
|
||||||
|
Allocation<T>* allocation = new Allocation<T>(std::forward<ValType>(val)...);
|
||||||
|
m_destructible.emplace_back(allocation, &allocation->m_entry);
|
||||||
|
return &allocation->m_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Free(const void* data);
|
||||||
|
void Delete(const void* data);
|
||||||
|
|
||||||
|
protected:
|
||||||
class IDestructible
|
class IDestructible
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -47,30 +74,4 @@ class MemoryManager
|
|||||||
|
|
||||||
std::vector<void*> m_allocations;
|
std::vector<void*> m_allocations;
|
||||||
std::vector<AllocationInfo> m_destructible;
|
std::vector<AllocationInfo> m_destructible;
|
||||||
|
|
||||||
public:
|
|
||||||
MemoryManager();
|
|
||||||
virtual ~MemoryManager();
|
|
||||||
MemoryManager(const MemoryManager& other) = delete;
|
|
||||||
MemoryManager(MemoryManager&& other) noexcept = default;
|
|
||||||
MemoryManager& operator=(const MemoryManager& other) = delete;
|
|
||||||
MemoryManager& operator=(MemoryManager&& other) noexcept = default;
|
|
||||||
|
|
||||||
void* AllocRaw(size_t size);
|
|
||||||
char* Dup(const char* str);
|
|
||||||
|
|
||||||
template<typename T> std::add_pointer_t<T> Alloc(const size_t count = 1u)
|
|
||||||
{
|
|
||||||
return static_cast<std::add_pointer_t<T>>(AllocRaw(sizeof(T) * count));
|
|
||||||
}
|
|
||||||
|
|
||||||
template<class T, class... ValType> std::add_pointer_t<T> Create(ValType&&... val)
|
|
||||||
{
|
|
||||||
Allocation<T>* allocation = new Allocation<T>(std::forward<ValType>(val)...);
|
|
||||||
m_destructible.emplace_back(allocation, &allocation->m_entry);
|
|
||||||
return &allocation->m_entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Free(const void* data);
|
|
||||||
void Delete(const void* data);
|
|
||||||
};
|
};
|
||||||
|
14
test/ObjCommonTestUtils/Utils/TestMemoryManager.h
Normal file
14
test/ObjCommonTestUtils/Utils/TestMemoryManager.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Utils/MemoryManager.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
class TestMemoryManager : public MemoryManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
[[nodiscard]] size_t GetAllocationCount() const
|
||||||
|
{
|
||||||
|
return m_allocations.size() + m_destructible.size();
|
||||||
|
}
|
||||||
|
};
|
58
test/ObjCompilingTests.lua
Normal file
58
test/ObjCompilingTests.lua
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
ObjCompilingTests = {}
|
||||||
|
|
||||||
|
function ObjCompilingTests:include(includes)
|
||||||
|
if includes:handle(self:name()) then
|
||||||
|
includedirs {
|
||||||
|
path.join(TestFolder(), "ObjCompilingTests")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ObjCompilingTests:link(links)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function ObjCompilingTests:use()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function ObjCompilingTests:name()
|
||||||
|
return "ObjCompilingTests"
|
||||||
|
end
|
||||||
|
|
||||||
|
function ObjCompilingTests:project()
|
||||||
|
local folder = TestFolder()
|
||||||
|
local includes = Includes:create()
|
||||||
|
local links = Links:create()
|
||||||
|
|
||||||
|
project(self:name())
|
||||||
|
targetdir(TargetDirectoryTest)
|
||||||
|
location "%{wks.location}/test/%{prj.name}"
|
||||||
|
kind "ConsoleApp"
|
||||||
|
language "C++"
|
||||||
|
|
||||||
|
files {
|
||||||
|
path.join(folder, "ObjCompilingTests/**.h"),
|
||||||
|
path.join(folder, "ObjCompilingTests/**.cpp")
|
||||||
|
}
|
||||||
|
|
||||||
|
vpaths {
|
||||||
|
["*"] = {
|
||||||
|
path.join(folder, "ObjCompilingTests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self:include(includes)
|
||||||
|
ObjCommonTestUtils:include(includes)
|
||||||
|
ParserTestUtils:include(includes)
|
||||||
|
ObjLoading:include(includes)
|
||||||
|
ObjCompiling:include(includes)
|
||||||
|
catch2:include(includes)
|
||||||
|
|
||||||
|
links:linkto(ObjCommonTestUtils)
|
||||||
|
links:linkto(ParserTestUtils)
|
||||||
|
links:linkto(ObjLoading)
|
||||||
|
links:linkto(ObjCompiling)
|
||||||
|
links:linkto(catch2)
|
||||||
|
links:linkall()
|
||||||
|
end
|
@ -0,0 +1,130 @@
|
|||||||
|
#include "Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.h"
|
||||||
|
|
||||||
|
#include "Game/T6/CommonT6.h"
|
||||||
|
#include "Game/T6/GameAssetPoolT6.h"
|
||||||
|
#include "KeyValuePairs/KeyValuePairsCreator.h"
|
||||||
|
#include "Utils/TestMemoryManager.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
using namespace T6;
|
||||||
|
using namespace std::string_literals;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class TestContext
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TestContext()
|
||||||
|
: m_memory(),
|
||||||
|
m_zone("test", 0, IGame::GetGameById(GameId::T6)),
|
||||||
|
m_zone_definition(),
|
||||||
|
m_zone_states(m_zone),
|
||||||
|
m_creators(m_zone),
|
||||||
|
m_ignored_assets(),
|
||||||
|
m_context(m_zone, &m_creators, &m_ignored_assets),
|
||||||
|
m_kvp_creator(m_zone_states.GetZoneAssetCreationState<KeyValuePairsCreator>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IAssetCreator> CreateSut()
|
||||||
|
{
|
||||||
|
return CreateKeyValuePairsCompiler(m_memory, m_zone, m_zone_definition, m_zone_states);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestMemoryManager m_memory;
|
||||||
|
Zone m_zone;
|
||||||
|
ZoneDefinition m_zone_definition;
|
||||||
|
ZoneAssetCreationStateContainer m_zone_states;
|
||||||
|
AssetCreatorCollection m_creators;
|
||||||
|
IgnoredAssetLookup m_ignored_assets;
|
||||||
|
AssetCreationContext m_context;
|
||||||
|
|
||||||
|
KeyValuePairsCreator& m_kvp_creator;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace test::game::t6::keyvaluepairs
|
||||||
|
{
|
||||||
|
TEST_CASE("KeyValuePairsCompilerT6: Does not handle any asset type", "[keyvaluepairs][t6]")
|
||||||
|
{
|
||||||
|
TestContext testContext;
|
||||||
|
const auto sut = testContext.CreateSut();
|
||||||
|
|
||||||
|
REQUIRE(!sut->GetHandlingAssetType().has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("KeyValuePairsCompilerT6: Does not take any action", "[keyvaluepairs][t6]")
|
||||||
|
{
|
||||||
|
TestContext testContext;
|
||||||
|
const auto sut = testContext.CreateSut();
|
||||||
|
|
||||||
|
REQUIRE(sut->CreateAsset("anyAsset", testContext.m_context).HasTakenAction() == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("KeyValuePairsCompilerT6: Does nothing without any KeyValuePairs", "[keyvaluepairs][t6]")
|
||||||
|
{
|
||||||
|
TestContext testContext;
|
||||||
|
const auto sut = testContext.CreateSut();
|
||||||
|
|
||||||
|
sut->FinalizeZone(testContext.m_context);
|
||||||
|
|
||||||
|
REQUIRE(testContext.m_memory.GetAllocationCount() == 0u);
|
||||||
|
REQUIRE(testContext.m_zone.m_pools->GetTotalAssetCount() == 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("KeyValuePairsCompilerT6: Creates KeyValuePairs asset with identical name to the zone", "[keyvaluepairs][t6]")
|
||||||
|
{
|
||||||
|
TestContext testContext;
|
||||||
|
const auto sut = testContext.CreateSut();
|
||||||
|
|
||||||
|
testContext.m_kvp_creator.AddKeyValuePair(CommonKeyValuePair("ipak_read", "test_ipak"));
|
||||||
|
|
||||||
|
sut->FinalizeZone(testContext.m_context);
|
||||||
|
|
||||||
|
REQUIRE(testContext.m_memory.GetAllocationCount() > 0u);
|
||||||
|
REQUIRE(testContext.m_zone.m_pools->GetTotalAssetCount() == 1u);
|
||||||
|
|
||||||
|
XAssetInfo<KeyValuePairs>* assetInfo = *dynamic_cast<GameAssetPoolT6*>(testContext.m_zone.m_pools.get())->m_key_value_pairs->begin();
|
||||||
|
REQUIRE(assetInfo);
|
||||||
|
REQUIRE(assetInfo->m_name == "test");
|
||||||
|
|
||||||
|
auto* asset = assetInfo->Asset();
|
||||||
|
REQUIRE(asset->name == "test"s);
|
||||||
|
REQUIRE(asset->numVariables == 1u);
|
||||||
|
REQUIRE(asset->keyValuePairs != nullptr);
|
||||||
|
|
||||||
|
REQUIRE(asset->keyValuePairs[0].keyHash == 0x0001bdc1);
|
||||||
|
REQUIRE(asset->keyValuePairs[0].namespaceHash == 0x0000d2d3);
|
||||||
|
REQUIRE(asset->keyValuePairs[0].value == "test_ipak"s);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("KeyValuePairsCompilerT6: Creates KeyValuePairs asset with predefined hash", "[keyvaluepairs][t6]")
|
||||||
|
{
|
||||||
|
TestContext testContext;
|
||||||
|
const auto sut = testContext.CreateSut();
|
||||||
|
|
||||||
|
testContext.m_kvp_creator.AddKeyValuePair(CommonKeyValuePair(0xDDEEFFAA, "hello_there"));
|
||||||
|
|
||||||
|
sut->FinalizeZone(testContext.m_context);
|
||||||
|
|
||||||
|
REQUIRE(testContext.m_memory.GetAllocationCount() > 0u);
|
||||||
|
REQUIRE(testContext.m_zone.m_pools->GetTotalAssetCount() == 1u);
|
||||||
|
|
||||||
|
XAssetInfo<KeyValuePairs>* assetInfo = *dynamic_cast<GameAssetPoolT6*>(testContext.m_zone.m_pools.get())->m_key_value_pairs->begin();
|
||||||
|
REQUIRE(assetInfo);
|
||||||
|
REQUIRE(assetInfo->m_name == "test");
|
||||||
|
|
||||||
|
auto* asset = assetInfo->Asset();
|
||||||
|
REQUIRE(asset->name == "test"s);
|
||||||
|
REQUIRE(asset->numVariables == 1u);
|
||||||
|
REQUIRE(asset->keyValuePairs != nullptr);
|
||||||
|
|
||||||
|
REQUIRE(asset->keyValuePairs[0].keyHash == 0xDDEEFFAA);
|
||||||
|
REQUIRE(asset->keyValuePairs[0].namespaceHash == 0x0000d2d3);
|
||||||
|
REQUIRE(asset->keyValuePairs[0].value == "hello_there"s);
|
||||||
|
}
|
||||||
|
} // namespace test::game::t6::keyvaluepairs
|
Loading…
x
Reference in New Issue
Block a user