entity per world
This commit is contained in:
1 parent
16ba641d8a
commit
b70e2d21d2
15 files changed
+694
-19
No files matched your search
@@ -21,6 +21,48 @@ internal void serialize(Archive& ar, uint16 /*version*/, Entity& entity)
|
||||
SERIALIZE(ar, position, entity.position);
|
||||
}
|
||||
|
||||
String format_entity_id(NonNullPtr<Arena> arena, EntityID entity_id)
|
||||
{
|
||||
// Format:
|
||||
// U = UserID
|
||||
// LC = Local Entity Counter
|
||||
// UU_LCLCLCLCLCLCLC
|
||||
uint8 user_id = get_user_id(entity_id);
|
||||
uint64 counter = get_local_counter(entity_id);
|
||||
|
||||
constexpr size_t kBufferSize = 18;
|
||||
char* buffer = ArenaPushArray<char>(arena, kBufferSize);
|
||||
Assert(buffer);
|
||||
|
||||
juliet_snprintf(buffer, kBufferSize, "%02X_%014llX", (unsigned int)user_id, counter);
|
||||
return { buffer, kBufferSize - 1 };
|
||||
}
|
||||
|
||||
bool parse_entity_id(String str, EntityID& out_id)
|
||||
{
|
||||
bool result = false;
|
||||
out_id = kInvalid_entity_id;
|
||||
|
||||
if (!(str.Size != 17 || str.Str == nullptr || str.Str[2] != '_'))
|
||||
{
|
||||
char user_hex[3]{ str.Str[0], str.Str[1], '\0' };
|
||||
char* end = user_hex;
|
||||
uint8 user_id = strtoul(user_hex, &end, 16);
|
||||
if (end != user_hex && user_id != kInvalid_user_id)
|
||||
{
|
||||
char* counter_hex = str.Str + 3;
|
||||
end = counter_hex;
|
||||
uint64 counter = strtoull(counter_hex, &end, 16);
|
||||
if (end != counter_hex && counter < kEntityID_user_id_mask)
|
||||
{
|
||||
out_id = make_entity_id(user_id, counter);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const Class* resolve_entity_class(uint8 kind, uint32 crc)
|
||||
{
|
||||
Assert(kind <= ENTITY(Count));
|
||||
|
||||
@@ -30,6 +30,32 @@ concept EntityConcept = requires(EntityType entity) {
|
||||
requires std::same_as<decltype(entity.base), Entity*>;
|
||||
};
|
||||
|
||||
inline constexpr EntityID make_entity_id(uint8 user_id, uint64 local_counter)
|
||||
{
|
||||
Assert(user_id != kInvalid_user_id);
|
||||
Assert((local_counter & kEntityID_user_id_mask) == kInvalid_entity_id);
|
||||
|
||||
EntityID result = kInvalid_user_id;
|
||||
result = ((EntityID)user_id << kEntityID_user_id_shift) | (local_counter & kEntityID_user_id_mask);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline constexpr uint8 get_user_id(EntityID entity_id)
|
||||
{
|
||||
return (((uint8)entity_id & kEntityID_user_id_mask) << kEntityID_user_id_shift);
|
||||
}
|
||||
|
||||
inline constexpr uint64 get_local_counter(EntityID entity_id)
|
||||
{
|
||||
return (uint64)entity_id & kEntityID_user_id_mask;
|
||||
}
|
||||
|
||||
inline constexpr bool is_entity_id_valid(EntityID entity_id)
|
||||
{
|
||||
bool result = entity_id != kInvalid_entity_id;
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename EntityType>
|
||||
requires EntityConcept<EntityType>
|
||||
[[nodiscard]] bool IsA(const Entity* entity)
|
||||
@@ -66,6 +92,12 @@ template <typename EntityType>
|
||||
|
||||
void serialize(Archive& ar, NonNullPtr<Entity> entity);
|
||||
|
||||
JULIET_API uint8 get_local_user_id();
|
||||
|
||||
JULIET_API String format_entity_id(NonNullPtr<Arena> arena, EntityID id);
|
||||
|
||||
JULIET_API bool parse_entity_id(String str, EntityID& outId);
|
||||
|
||||
[[nodiscard]] const Class* find_class_by_name(String name);
|
||||
|
||||
[[nodiscard]] const Class* resolve_entity_class(uint8 kind, uint32 crc);
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <Entity/entity_types.h>
|
||||
#include <game.h>
|
||||
#include <Graphics/MeshRenderer.h>
|
||||
#include "Engine/user.h"
|
||||
|
||||
EntityID EntityManager::ID = 0;
|
||||
|
||||
@@ -45,6 +46,17 @@ EntityManager& GetEntityManager()
|
||||
return *entityManager;
|
||||
}
|
||||
|
||||
EntityID generate_next_entity_id(EntityManager& manager)
|
||||
{
|
||||
UserID user = get_local_user_id();
|
||||
Assert(user != kInvalid_user_id);
|
||||
Assert(manager.next_local_counter != kEntityID_user_id_mask);
|
||||
|
||||
EntityID new_id = make_entity_id(user, manager.next_local_counter);
|
||||
manager.next_local_counter += 1ULL;
|
||||
return new_id;
|
||||
}
|
||||
|
||||
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity)
|
||||
{
|
||||
base->ID = EntityManager::ID++;
|
||||
@@ -143,3 +155,16 @@ Entity* deserialize_entity(Archive& ar, EntityManager& manager)
|
||||
|
||||
return entity_base;
|
||||
}
|
||||
|
||||
void update_local_counter(EntityManager& manager, EntityID entity_id)
|
||||
{
|
||||
uint8 user_id = get_local_user_id();
|
||||
if (get_user_id(entity_id) == user_id)
|
||||
{
|
||||
uint64 counter = get_local_counter(entity_id);
|
||||
if (counter >= manager.next_local_counter)
|
||||
{
|
||||
manager.next_local_counter = counter + 1ULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ struct typed_entity_array
|
||||
struct EntityManager
|
||||
{
|
||||
static EntityID ID;
|
||||
uint64 next_local_counter = 1ULL;
|
||||
|
||||
// TODO: Should be a pool
|
||||
VectorArena<Entity, 100'000> Entities;
|
||||
@@ -31,4 +32,5 @@ EntityManager& GetEntityManager();
|
||||
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
|
||||
[[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<const Class> derived_type_class);
|
||||
void UpdateEntityManager(EntityManager& manager);
|
||||
[[nodisacrd]] Entity* deserialize_entity(Archive& archive, EntityManager& manager);
|
||||
[[nodiscard]] Entity* deserialize_entity(Archive& archive, EntityManager& manager);
|
||||
void update_local_counter(EntityManager& manager, EntityID entity_id);
|
||||
@@ -7,6 +7,12 @@ struct Class;
|
||||
using DerivedType = void*;
|
||||
using EntityID = uint64_t;
|
||||
|
||||
constexpr uint8 kInvalid_user_id = 0U;
|
||||
constexpr EntityID kInvalid_entity_id = 0ULL;
|
||||
|
||||
constexpr EntityID kEntityID_user_id_shift = 56ULL;
|
||||
constexpr EntityID kEntityID_user_id_mask = 0xFF00'0000'0000'0000ULL;
|
||||
|
||||
// clang-format off
|
||||
#define ENTITY_TYPE_LIST(X) X(Inert),
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <Core/Common/String.h>
|
||||
|
||||
@@ -33,3 +33,10 @@ JULIET_API Path fs_create_clean_path(NonNullPtr<Arena> arena, String path);
|
||||
JULIET_API bool fs_create_directory(String path, bool recursive);
|
||||
|
||||
JULIET_API bool fs_path_exists(String path);
|
||||
|
||||
JULIET_API bool fs_move_file(Path old_path, Path new_path);
|
||||
|
||||
JULIET_API bool fs_delete_file(String path);
|
||||
|
||||
using fs_iterate_director_callback = void (*)(Path path, bool is_director, void* user_payload);
|
||||
JULIET_API void fs_iterate_directory(Path directory_path, String extension, void* user_payload, fs_iterate_director_callback callback);
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Core/Common/NonNullPtr.h>
|
||||
#include <Core/Common/String.h>
|
||||
#include <Juliet.h>
|
||||
#include "Core/HAL/Filesystem/Filesystem.h"
|
||||
|
||||
// Opaque type
|
||||
struct IOStream;
|
||||
@@ -52,15 +53,17 @@ extern JULIET_API IOStream* IOFromInterface(NonNullPtr<Arena> arena, NonNullPtr<
|
||||
NonNullPtr<IOStreamDataPayload> payload);
|
||||
|
||||
// Write formatted string into the stream.
|
||||
extern JULIET_API size_t IOPrintf(NonNullPtr<IOStream> stream, _Printf_format_string_ const char* format, ...);
|
||||
extern JULIET_API size_t IOWrite(NonNullPtr<IOStream> stream, ByteBuffer inBuffer);
|
||||
JULIET_API size_t IOPrintf(NonNullPtr<IOStream> stream, _Printf_format_string_ const char* format, ...);
|
||||
JULIET_API size_t IOWrite(NonNullPtr<IOStream> stream, ByteBuffer inBuffer);
|
||||
|
||||
extern JULIET_API size_t IORead(NonNullPtr<IOStream> stream, void* ptr, size_t size);
|
||||
extern JULIET_API int64 IOSeek(NonNullPtr<IOStream> stream, int64 offset, IOStreamSeekPivot pivot);
|
||||
JULIET_API size_t IORead(NonNullPtr<IOStream> stream, void* ptr, size_t size);
|
||||
JULIET_API int64 IOSeek(NonNullPtr<IOStream> stream, int64 offset, IOStreamSeekPivot pivot);
|
||||
|
||||
extern JULIET_API int64 IOSize(NonNullPtr<IOStream> stream);
|
||||
JULIET_API int64 IOSize(NonNullPtr<IOStream> stream);
|
||||
|
||||
extern JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, String filename);
|
||||
extern JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, NonNullPtr<IOStream> stream, bool closeStreamWhenDone);
|
||||
JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, String filename);
|
||||
JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, NonNullPtr<IOStream> stream, bool closeStreamWhenDone);
|
||||
|
||||
extern JULIET_API bool IOClose(NonNullPtr<IOStream> stream);
|
||||
JULIET_API size_t IO_atomic_file_write(Path filepath, ByteBuffer file_content);
|
||||
|
||||
JULIET_API bool IOClose(NonNullPtr<IOStream> stream);
|
||||
@@ -5,4 +5,4 @@ using UserID = uint8;
|
||||
void initialize_user_system();
|
||||
void shutdown_user_system();
|
||||
|
||||
uint8 get_local_user_id();
|
||||
JULIET_API uint8 get_local_user_id();
|
||||
@@ -1,4 +1,4 @@
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||
#include <Core/HAL/Filesystem/Filesystem_Platform.h>
|
||||
@@ -106,6 +106,27 @@ bool fs_path_exists(String path)
|
||||
return Platform::fs_path_exists(path);
|
||||
}
|
||||
|
||||
bool fs_move_file(Path old_path, Path new_path)
|
||||
{
|
||||
return Platform::fs_move_file(old_path, new_path);
|
||||
}
|
||||
|
||||
bool fs_delete_file(String path)
|
||||
{
|
||||
Assert(IsValid(path));
|
||||
return Platform::fs_delete_file(path);
|
||||
}
|
||||
|
||||
void fs_iterate_directory(Path directory_path, String extension, void* user_payload, fs_iterate_director_callback callback)
|
||||
{
|
||||
Assert(IsValid(directory_path));
|
||||
Assert(callback);
|
||||
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
scratch_end(scratch);
|
||||
}
|
||||
|
||||
void InitFilesystem(NonNullPtr<Arena> arena)
|
||||
{
|
||||
CachedBasePath = Platform::GetBasePath(arena);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||
|
||||
@@ -9,4 +9,6 @@ namespace Platform
|
||||
Path fs_create_clean_path(NonNullPtr<Arena> arena, String path);
|
||||
bool fs_create_directory(String path, bool recursive);
|
||||
bool fs_path_exists(String path);
|
||||
bool fs_move_file(Path old_path, Path new_path);
|
||||
bool fs_delete_file(String path);
|
||||
} // namespace Platform
|
||||
@@ -1,4 +1,4 @@
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/HAL/Filesystem/Filesystem_Platform.h>
|
||||
#include <Core/HAL/Win32.h>
|
||||
#include <Core/Logging/LogManager.h>
|
||||
@@ -167,10 +167,9 @@ namespace Platform
|
||||
if (!create_result)
|
||||
{
|
||||
const DWORD error = GetLastError();
|
||||
if (error != ERROR_ALREADY_EXISTS)
|
||||
if (error == ERROR_ALREADY_EXISTS)
|
||||
{
|
||||
LogDebug(LogCategory::Core, "Folder already exists");
|
||||
result = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -191,4 +190,20 @@ namespace Platform
|
||||
const bool result = (attribs != INVALID_FILE_ATTRIBUTES);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool fs_move_file(Path old_path, Path new_path)
|
||||
{
|
||||
bool result = MoveFileExA(CStr(old_path.FullPath), CStr(new_path.FullPath), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool fs_delete_file(String path)
|
||||
{
|
||||
if (!DeleteFileA(CStr(path)))
|
||||
{
|
||||
LogError(LogCategory::Core, "Failed to delete file %s. Error: %lu", CStr(path), GetLastError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace Platform
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <Core/Logging/LogManager.h>
|
||||
#include <Core/Logging/LogTypes.h>
|
||||
#include <Core/Thread/Thread.h>
|
||||
#include "Core/Thread/ThreadContext.h"
|
||||
|
||||
IOStream* IOFromFile(NonNullPtr<Arena> arena, String filename, String mode)
|
||||
{
|
||||
@@ -212,6 +213,51 @@ ByteBuffer LoadFile(NonNullPtr<Arena> arena, NonNullPtr<IOStream> stream, bool c
|
||||
return resultBuffer;
|
||||
}
|
||||
|
||||
size_t IO_atomic_file_write(Path filepath, ByteBuffer file_content)
|
||||
{
|
||||
size_t written_size = 0;
|
||||
|
||||
TempArena temp_arena = scratch_begin(nullptr, 0);
|
||||
|
||||
size_t temp_size = filepath.FullPath.Size + sizeof(".tmp");
|
||||
char* tmp_str = (char*)ArenaPushSize(temp_arena.Arena, temp_size, 8, true JULIET_DEBUG_PARAM("Temp path"));
|
||||
juliet_snprintf(tmp_str, temp_size, "%s.tmp", CStr(filepath.FullPath));
|
||||
Path temp_path = fs_create_clean_path(temp_arena.Arena, WrapString(tmp_str));
|
||||
|
||||
if (IOStream* temp_stream = IOFromFile(temp_arena.Arena, temp_path.FullPath, ConstString("w+")))
|
||||
{
|
||||
written_size = IOWrite(temp_stream, file_content);
|
||||
bool closed = IOClose(temp_stream);
|
||||
if (closed && written_size == file_content.Size)
|
||||
{
|
||||
if (!fs_move_file(temp_path, filepath))
|
||||
{
|
||||
LogError(LogCategory::Core, "IO_atomic_file_write [%s] failed to move from temp to final file. Aborting",
|
||||
CStr(filepath.FullPath));
|
||||
written_size = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError(LogCategory::Core, "IO_atomic_file_write [%s] failed to write into temporary file. Aborting",
|
||||
CStr(filepath.FullPath));
|
||||
}
|
||||
|
||||
if (written_size == 0)
|
||||
{
|
||||
fs_delete_file(temp_path.FullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError(LogCategory::Core, "IO_atomic_file_write [%s] failed creating temporary file. Aborting", CStr(filepath.FullPath));
|
||||
}
|
||||
|
||||
scratch_end(temp_arena);
|
||||
|
||||
return written_size;
|
||||
}
|
||||
|
||||
bool IOClose(NonNullPtr<IOStream> stream)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include <Engine/Engine.h>
|
||||
#include <Engine/Engine.h>
|
||||
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <Core/HAL/Event/SystemEvent.h>
|
||||
@@ -154,12 +154,12 @@ void InitializeEngine(JulietInit_Flags flags)
|
||||
|
||||
InitializeLogManager();
|
||||
|
||||
InitFilesystem(EngineInstance.PlatformArena);
|
||||
|
||||
#if JULIET_DEBUG
|
||||
UnitTest::RunUnitTests();
|
||||
#endif
|
||||
|
||||
InitFilesystem(EngineInstance.PlatformArena);
|
||||
|
||||
initialize_user_system();
|
||||
|
||||
JulietInit(flags);
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace UnitTest
|
||||
void TestMemoryArena();
|
||||
void SerializationUnitTest();
|
||||
void os_unit_test();
|
||||
void iostream_unit_test();
|
||||
|
||||
void RunUnitTests()
|
||||
{
|
||||
@@ -21,6 +22,7 @@ namespace UnitTest
|
||||
TestMemoryArena();
|
||||
VectorUnitTest();
|
||||
SerializationUnitTest();
|
||||
iostream_unit_test();
|
||||
|
||||
LogMessage(LogCategory::Core, "Unit Tests Completed Successfully.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
#if JULIET_DEBUG
|
||||
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||
#include <Core/HAL/IO/IOStream.h>
|
||||
#include <Core/Logging/LogManager.h>
|
||||
#include <Core/Logging/LogTypes.h>
|
||||
#include <Core/Memory/MemoryArena.h>
|
||||
#include <Core/Memory/Utils.h>
|
||||
#include <Core/Thread/ThreadContext.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
struct IOStreamTestMemoryPayload : IOStreamDataPayload
|
||||
{
|
||||
Byte* data;
|
||||
size_t capacity;
|
||||
size_t size;
|
||||
size_t position;
|
||||
bool is_closed;
|
||||
};
|
||||
|
||||
internal IOStreamTestMemoryPayload* iostream_test_get_payload(NonNullPtr<IOStreamDataPayload> payload)
|
||||
{
|
||||
return static_cast<IOStreamTestMemoryPayload*>(payload.Get());
|
||||
}
|
||||
|
||||
internal int64 iostream_test_mem_size(NonNullPtr<IOStreamDataPayload> payload)
|
||||
{
|
||||
return static_cast<int64>(iostream_test_get_payload(payload)->size);
|
||||
}
|
||||
|
||||
internal int64 iostream_test_mem_seek(NonNullPtr<IOStreamDataPayload> payload, int64 offset, IOStreamSeekPivot pivot)
|
||||
{
|
||||
auto* memory = iostream_test_get_payload(payload);
|
||||
|
||||
int64 base = 0;
|
||||
switch (pivot)
|
||||
{
|
||||
case IOStreamSeekPivot::Begin: base = 0; break;
|
||||
case IOStreamSeekPivot::Current: base = static_cast<int64>(memory->position); break;
|
||||
case IOStreamSeekPivot::End: base = static_cast<int64>(memory->size); break;
|
||||
case IOStreamSeekPivot::Count: return -1;
|
||||
}
|
||||
|
||||
int64 new_position = base + offset;
|
||||
if ((new_position < 0) || (new_position > static_cast<int64>(memory->size)))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
memory->position = static_cast<size_t>(new_position);
|
||||
return new_position;
|
||||
}
|
||||
|
||||
internal size_t iostream_test_mem_read(NonNullPtr<IOStreamDataPayload> payload, void* out_buffer, size_t size,
|
||||
NonNullPtr<IOStreamStatus> /*status*/)
|
||||
{
|
||||
Assert(out_buffer != nullptr);
|
||||
|
||||
auto* memory = iostream_test_get_payload(payload);
|
||||
size_t bytes_left = memory->size - memory->position;
|
||||
size_t to_read = Min(size, bytes_left);
|
||||
MemCopy(out_buffer, memory->data + memory->position, to_read);
|
||||
memory->position += to_read;
|
||||
return to_read;
|
||||
}
|
||||
|
||||
internal size_t iostream_test_mem_write(NonNullPtr<IOStreamDataPayload> payload, ByteBuffer in_buffer,
|
||||
NonNullPtr<IOStreamStatus> /*status*/)
|
||||
{
|
||||
Assert(IsValid(in_buffer));
|
||||
|
||||
auto* memory = iostream_test_get_payload(payload);
|
||||
size_t space_left = memory->capacity - memory->position;
|
||||
size_t to_write = Min(in_buffer.Size, space_left);
|
||||
MemCopy(memory->data + memory->position, in_buffer.Data, to_write);
|
||||
memory->position += to_write;
|
||||
memory->size = Max(memory->size, memory->position);
|
||||
return to_write;
|
||||
}
|
||||
|
||||
internal bool iostream_test_mem_flush(NonNullPtr<IOStreamDataPayload> /*payload*/, NonNullPtr<IOStreamStatus> /*status*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool iostream_test_mem_close(NonNullPtr<IOStreamDataPayload> payload)
|
||||
{
|
||||
iostream_test_get_payload(payload)->is_closed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal IOStreamInterface iostream_test_make_full_interface()
|
||||
{
|
||||
IOStreamInterface stream_interface = {};
|
||||
stream_interface.Version = sizeof(IOStreamInterface);
|
||||
stream_interface.Size = iostream_test_mem_size;
|
||||
stream_interface.Seek = iostream_test_mem_seek;
|
||||
stream_interface.Read = iostream_test_mem_read;
|
||||
stream_interface.Write = iostream_test_mem_write;
|
||||
stream_interface.Flush = iostream_test_mem_flush;
|
||||
stream_interface.Close = iostream_test_mem_close;
|
||||
return stream_interface;
|
||||
}
|
||||
|
||||
internal IOStreamTestMemoryPayload iostream_test_make_payload(NonNullPtr<Arena> arena, size_t capacity)
|
||||
{
|
||||
Assert(capacity > 0);
|
||||
|
||||
IOStreamTestMemoryPayload payload = {};
|
||||
payload.data = ArenaPushArray<Byte>(arena, capacity);
|
||||
payload.capacity = capacity;
|
||||
Assert(payload.data != nullptr);
|
||||
return payload;
|
||||
}
|
||||
|
||||
internal ByteBuffer iostream_test_buffer_from_string(String str)
|
||||
{
|
||||
Assert(IsValid(str));
|
||||
return { .Data = reinterpret_cast<Byte*>(str.Str), .Size = str.Size };
|
||||
}
|
||||
|
||||
internal bool iostream_test_buffer_equals(ByteBuffer buffer, String expected)
|
||||
{
|
||||
return (buffer.Size == expected.Size) && (MemCompare(buffer.Data, expected.Str, expected.Size) == 0);
|
||||
}
|
||||
|
||||
internal String iostream_test_append_suffix(NonNullPtr<Arena> arena, String str, String suffix)
|
||||
{
|
||||
Assert(IsValid(str));
|
||||
Assert(IsValid(suffix));
|
||||
|
||||
size_t total_size = str.Size + suffix.Size;
|
||||
char* buffer = ArenaPushArray<char>(arena, total_size + 1);
|
||||
Assert(buffer != nullptr);
|
||||
|
||||
MemCopy(buffer, str.Str, str.Size);
|
||||
MemCopy(buffer + str.Size, suffix.Str, suffix.Size);
|
||||
buffer[total_size] = '\0';
|
||||
return { buffer, total_size };
|
||||
}
|
||||
|
||||
internal void iostream_test_delete_file(String filename)
|
||||
{
|
||||
Assert(IsValid(filename));
|
||||
if (fs_path_exists(filename))
|
||||
{
|
||||
Assert(fs_delete_file(filename));
|
||||
}
|
||||
Assert(!fs_path_exists(filename));
|
||||
}
|
||||
|
||||
// Test files live in <repo>/Intermediate/UnitTest/ (exe is in <repo>/bin/x64-<Config>/)
|
||||
internal Path iostream_test_make_path(NonNullPtr<Arena> arena, String filename)
|
||||
{
|
||||
Assert(IsValid(filename));
|
||||
|
||||
Path base_path = GetBasePath();
|
||||
String raw_directory = iostream_test_append_suffix(arena, base_path.Directories,
|
||||
ConstString("..\\..\\Intermediate\\UnitTest\\"));
|
||||
Path test_directory = fs_create_clean_path(arena, raw_directory);
|
||||
Assert(IsValid(test_directory));
|
||||
|
||||
if (!fs_path_exists(test_directory.FullPath))
|
||||
{
|
||||
Assert(fs_create_directory(test_directory.FullPath, true));
|
||||
}
|
||||
|
||||
Path file_path = fs_create_clean_path(arena, iostream_test_append_suffix(arena, test_directory.FullPath, filename));
|
||||
Assert(IsValid(file_path));
|
||||
return file_path;
|
||||
}
|
||||
|
||||
internal void iostream_test_write_file(String filename, String mode, String content)
|
||||
{
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
IOStream* stream = IOFromFile(scratch.Arena, filename, mode);
|
||||
Assert(stream != nullptr);
|
||||
Assert(IOWrite(stream, iostream_test_buffer_from_string(content)) == content.Size);
|
||||
Assert(IOClose(stream));
|
||||
|
||||
scratch_end(scratch);
|
||||
}
|
||||
|
||||
// IOFromInterface + IOWrite / IOPrintf / IOSize / IOSeek / IORead / IOClose on a full interface
|
||||
internal void iostream_test_memory_stream()
|
||||
{
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
IOStreamInterface stream_interface = iostream_test_make_full_interface();
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 256);
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
String hello = ConstString("Hello");
|
||||
Assert(IOWrite(stream, iostream_test_buffer_from_string(hello)) == hello.Size);
|
||||
Assert(IOWrite(stream, {}) == 0);
|
||||
Assert(IOPrintf(stream, " %s %d", "World", 42) == 9);
|
||||
|
||||
String expected = ConstString("Hello World 42");
|
||||
Assert(IOSize(stream) == static_cast<int64>(expected.Size));
|
||||
|
||||
Assert(IOSeek(stream, 0, IOStreamSeekPivot::Begin) == 0);
|
||||
char read_buffer[64] = {};
|
||||
Assert(IORead(stream, read_buffer, expected.Size) == expected.Size);
|
||||
Assert(MemCompare(read_buffer, expected.Str, expected.Size) == 0);
|
||||
|
||||
Assert(IORead(stream, read_buffer, sizeof(read_buffer)) == 0);
|
||||
Assert(IORead(stream, read_buffer, 0) == 0);
|
||||
|
||||
Assert(IOSeek(stream, -2, IOStreamSeekPivot::End) == static_cast<int64>(expected.Size - 2));
|
||||
Assert(IORead(stream, read_buffer, 2) == 2);
|
||||
Assert(MemCompare(read_buffer, "42", 2) == 0);
|
||||
|
||||
Assert(IOSeek(stream, -8, IOStreamSeekPivot::Current) == 6);
|
||||
Assert(IORead(stream, read_buffer, 5) == 5);
|
||||
Assert(MemCompare(read_buffer, "World", 5) == 0);
|
||||
|
||||
Assert(!payload.is_closed);
|
||||
Assert(IOClose(stream));
|
||||
Assert(payload.is_closed);
|
||||
|
||||
scratch_end(scratch);
|
||||
LogMessage(LogCategory::Core, "iostream_test_memory_stream passed.");
|
||||
}
|
||||
|
||||
// Behavior when the interface is missing some functions
|
||||
internal void iostream_test_capability_fallbacks()
|
||||
{
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
String content = ConstString("Hello World 42");
|
||||
|
||||
// Read only, no seek, no size, no close
|
||||
{
|
||||
IOStreamInterface read_only_interface = {};
|
||||
read_only_interface.Version = sizeof(IOStreamInterface);
|
||||
read_only_interface.Read = iostream_test_mem_read;
|
||||
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
|
||||
MemCopy(payload.data, content.Str, content.Size);
|
||||
payload.size = content.Size;
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &read_only_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
Assert(IOWrite(stream, iostream_test_buffer_from_string(content)) == 0);
|
||||
Assert(IOSeek(stream, 0, IOStreamSeekPivot::Begin) == -1);
|
||||
Assert(IOSize(stream) == -1);
|
||||
|
||||
char read_buffer[64] = {};
|
||||
Assert(IORead(stream, read_buffer, content.Size) == content.Size);
|
||||
Assert(MemCompare(read_buffer, content.Str, content.Size) == 0);
|
||||
|
||||
Assert(IOClose(stream));
|
||||
Assert(!payload.is_closed);
|
||||
}
|
||||
|
||||
// Write only
|
||||
{
|
||||
IOStreamInterface write_only_interface = {};
|
||||
write_only_interface.Version = sizeof(IOStreamInterface);
|
||||
write_only_interface.Write = iostream_test_mem_write;
|
||||
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &write_only_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
Assert(IOWrite(stream, iostream_test_buffer_from_string(content)) == content.Size);
|
||||
|
||||
char read_buffer[64] = {};
|
||||
Assert(IORead(stream, read_buffer, content.Size) == 0);
|
||||
Assert(IOClose(stream));
|
||||
}
|
||||
|
||||
// No Size function: IOSize falls back on Seek and restores the position
|
||||
{
|
||||
IOStreamInterface seek_interface = iostream_test_make_full_interface();
|
||||
seek_interface.Size = nullptr;
|
||||
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
|
||||
MemCopy(payload.data, content.Str, content.Size);
|
||||
payload.size = content.Size;
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &seek_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
Assert(IOSeek(stream, 3, IOStreamSeekPivot::Begin) == 3);
|
||||
Assert(IOSize(stream) == static_cast<int64>(content.Size));
|
||||
Assert(IOSeek(stream, 0, IOStreamSeekPivot::Current) == 3);
|
||||
Assert(IOClose(stream));
|
||||
}
|
||||
|
||||
scratch_end(scratch);
|
||||
LogMessage(LogCategory::Core, "iostream_test_capability_fallbacks passed.");
|
||||
}
|
||||
|
||||
// LoadFile(arena, stream, closeStreamWhenDone) with known size and chunked fallback
|
||||
internal void iostream_test_load_file_from_stream()
|
||||
{
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
String content = ConstString("Juliet LoadFile content");
|
||||
|
||||
// Known size, stream closed when done
|
||||
{
|
||||
IOStreamInterface stream_interface = iostream_test_make_full_interface();
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
|
||||
MemCopy(payload.data, content.Str, content.Size);
|
||||
payload.size = content.Size;
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
ByteBuffer loaded = LoadFile(scratch.Arena, stream, true);
|
||||
Assert(iostream_test_buffer_equals(loaded, content));
|
||||
Assert(loaded.Data[loaded.Size] == Byte{ 0 });
|
||||
Assert(payload.is_closed);
|
||||
}
|
||||
|
||||
// Known size, stream kept open
|
||||
{
|
||||
IOStreamInterface stream_interface = iostream_test_make_full_interface();
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
|
||||
MemCopy(payload.data, content.Str, content.Size);
|
||||
payload.size = content.Size;
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
ByteBuffer loaded = LoadFile(scratch.Arena, stream, false);
|
||||
Assert(iostream_test_buffer_equals(loaded, content));
|
||||
Assert(!payload.is_closed);
|
||||
Assert(IOClose(stream));
|
||||
}
|
||||
|
||||
// Unknown size (no Size / Seek): loaded chunk by chunk, bigger than one chunk
|
||||
{
|
||||
constexpr size_t k_big_content_size = 3000;
|
||||
|
||||
IOStreamInterface stream_interface = iostream_test_make_full_interface();
|
||||
stream_interface.Size = nullptr;
|
||||
stream_interface.Seek = nullptr;
|
||||
|
||||
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, k_big_content_size);
|
||||
for (size_t index = 0; index < k_big_content_size; ++index)
|
||||
{
|
||||
payload.data[index] = static_cast<Byte>('a' + (index % 26));
|
||||
}
|
||||
payload.size = k_big_content_size;
|
||||
|
||||
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
|
||||
Assert(stream != nullptr);
|
||||
|
||||
ByteBuffer loaded = LoadFile(scratch.Arena, stream, true);
|
||||
Assert(loaded.Size == k_big_content_size);
|
||||
Assert(MemCompare(loaded.Data, payload.data, k_big_content_size) == 0);
|
||||
Assert(loaded.Data[loaded.Size] == Byte{ 0 });
|
||||
Assert(payload.is_closed);
|
||||
}
|
||||
|
||||
scratch_end(scratch);
|
||||
LogMessage(LogCategory::Core, "iostream_test_load_file_from_stream passed.");
|
||||
}
|
||||
|
||||
// IOFromFile + LoadFile(arena, filename) on a real file
|
||||
internal void iostream_test_file_stream()
|
||||
{
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
Path file_path = iostream_test_make_path(scratch.Arena, ConstString("juliet_iostream_unit_test.bin"));
|
||||
Assert(IsValid(file_path));
|
||||
iostream_test_delete_file(file_path.FullPath);
|
||||
|
||||
Assert(IOFromFile(scratch.Arena, {}, ConstString("rb")) == nullptr);
|
||||
Assert(IOFromFile(scratch.Arena, file_path.FullPath, {}) == nullptr);
|
||||
Assert(IOFromFile(scratch.Arena, file_path.FullPath, ConstString("rb")) == nullptr);
|
||||
|
||||
String first_part = ConstString("First part.");
|
||||
String second_part = ConstString(" Second part.");
|
||||
String full = ConstString("First part. Second part.");
|
||||
|
||||
iostream_test_write_file(file_path.FullPath, ConstString("wb"), first_part);
|
||||
Assert(fs_path_exists(file_path.FullPath));
|
||||
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), first_part));
|
||||
|
||||
iostream_test_write_file(file_path.FullPath, ConstString("ab"), second_part);
|
||||
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), full));
|
||||
|
||||
// Seek / Size / partial read on a file stream
|
||||
{
|
||||
IOStream* stream = IOFromFile(scratch.Arena, file_path.FullPath, ConstString("rb"));
|
||||
Assert(stream != nullptr);
|
||||
Assert(IOSize(stream) == static_cast<int64>(full.Size));
|
||||
Assert(IOSeek(stream, static_cast<int64>(first_part.Size), IOStreamSeekPivot::Begin) ==
|
||||
static_cast<int64>(first_part.Size));
|
||||
|
||||
char read_buffer[64] = {};
|
||||
Assert(IORead(stream, read_buffer, second_part.Size) == second_part.Size);
|
||||
Assert(MemCompare(read_buffer, second_part.Str, second_part.Size) == 0);
|
||||
Assert(IORead(stream, read_buffer, sizeof(read_buffer)) == 0);
|
||||
Assert(IOWrite(stream, iostream_test_buffer_from_string(first_part)) == 0);
|
||||
Assert(IOClose(stream));
|
||||
}
|
||||
|
||||
// "w" truncates
|
||||
iostream_test_write_file(file_path.FullPath, ConstString("wb"), second_part);
|
||||
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), second_part));
|
||||
|
||||
iostream_test_delete_file(file_path.FullPath);
|
||||
|
||||
scratch_end(scratch);
|
||||
LogMessage(LogCategory::Core, "iostream_test_file_stream passed.");
|
||||
}
|
||||
|
||||
// IO_atomic_file_write: writes to <file>.tmp then moves it to <file>
|
||||
internal void iostream_test_atomic_file_write()
|
||||
{
|
||||
TempArena scratch = scratch_begin(nullptr, 0);
|
||||
|
||||
Path file_path = iostream_test_make_path(scratch.Arena, ConstString("juliet_iostream_atomic_unit_test.bin"));
|
||||
Assert(IsValid(file_path));
|
||||
String temp_filename = iostream_test_append_suffix(scratch.Arena, file_path.FullPath, ConstString(".tmp"));
|
||||
|
||||
iostream_test_delete_file(file_path.FullPath);
|
||||
iostream_test_delete_file(temp_filename);
|
||||
|
||||
// Create a new file
|
||||
String first_content = ConstString("Atomic content that is long enough");
|
||||
Assert(IO_atomic_file_write(file_path, iostream_test_buffer_from_string(first_content)) == first_content.Size);
|
||||
Assert(fs_path_exists(file_path.FullPath));
|
||||
Assert(!fs_path_exists(temp_filename));
|
||||
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), first_content));
|
||||
|
||||
// Replace an existing file with shorter content
|
||||
String second_content = ConstString("Shorter");
|
||||
Assert(IO_atomic_file_write(file_path, iostream_test_buffer_from_string(second_content)) == second_content.Size);
|
||||
Assert(!fs_path_exists(temp_filename));
|
||||
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), second_content));
|
||||
|
||||
// A leftover temp file from a previous crash must not break the write
|
||||
iostream_test_write_file(temp_filename, ConstString("wb"), ConstString("Stale temp data"));
|
||||
Assert(IO_atomic_file_write(file_path, iostream_test_buffer_from_string(first_content)) == first_content.Size);
|
||||
Assert(!fs_path_exists(temp_filename));
|
||||
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), first_content));
|
||||
|
||||
iostream_test_delete_file(file_path.FullPath);
|
||||
|
||||
scratch_end(scratch);
|
||||
LogMessage(LogCategory::Core, "iostream_test_atomic_file_write passed.");
|
||||
}
|
||||
|
||||
void iostream_unit_test()
|
||||
{
|
||||
LogMessage(LogCategory::Core, "=== Running IOStream Unit Tests ===");
|
||||
|
||||
iostream_test_memory_stream();
|
||||
iostream_test_capability_fallbacks();
|
||||
iostream_test_load_file_from_stream();
|
||||
iostream_test_file_stream();
|
||||
iostream_test_atomic_file_write();
|
||||
|
||||
LogMessage(LogCategory::Core, "=== All IOStream Unit Tests Passed Successfully ===");
|
||||
}
|
||||
} // namespace UnitTest
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user