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);
|
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)
|
const Class* resolve_entity_class(uint8 kind, uint32 crc)
|
||||||
{
|
{
|
||||||
Assert(kind <= ENTITY(Count));
|
Assert(kind <= ENTITY(Count));
|
||||||
|
|||||||
@@ -30,6 +30,32 @@ concept EntityConcept = requires(EntityType entity) {
|
|||||||
requires std::same_as<decltype(entity.base), 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>
|
template <typename EntityType>
|
||||||
requires EntityConcept<EntityType>
|
requires EntityConcept<EntityType>
|
||||||
[[nodiscard]] bool IsA(const Entity* entity)
|
[[nodiscard]] bool IsA(const Entity* entity)
|
||||||
@@ -66,6 +92,12 @@ template <typename EntityType>
|
|||||||
|
|
||||||
void serialize(Archive& ar, NonNullPtr<Entity> entity);
|
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* find_class_by_name(String name);
|
||||||
|
|
||||||
[[nodiscard]] const Class* resolve_entity_class(uint8 kind, uint32 crc);
|
[[nodiscard]] const Class* resolve_entity_class(uint8 kind, uint32 crc);
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <Entity/entity_types.h>
|
#include <Entity/entity_types.h>
|
||||||
#include <game.h>
|
#include <game.h>
|
||||||
#include <Graphics/MeshRenderer.h>
|
#include <Graphics/MeshRenderer.h>
|
||||||
|
#include "Engine/user.h"
|
||||||
|
|
||||||
EntityID EntityManager::ID = 0;
|
EntityID EntityManager::ID = 0;
|
||||||
|
|
||||||
@@ -45,6 +46,17 @@ EntityManager& GetEntityManager()
|
|||||||
return *entityManager;
|
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)
|
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity)
|
||||||
{
|
{
|
||||||
base->ID = EntityManager::ID++;
|
base->ID = EntityManager::ID++;
|
||||||
@@ -143,3 +155,16 @@ Entity* deserialize_entity(Archive& ar, EntityManager& manager)
|
|||||||
|
|
||||||
return entity_base;
|
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
|
struct EntityManager
|
||||||
{
|
{
|
||||||
static EntityID ID;
|
static EntityID ID;
|
||||||
|
uint64 next_local_counter = 1ULL;
|
||||||
|
|
||||||
// TODO: Should be a pool
|
// TODO: Should be a pool
|
||||||
VectorArena<Entity, 100'000> Entities;
|
VectorArena<Entity, 100'000> Entities;
|
||||||
@@ -31,4 +32,5 @@ EntityManager& GetEntityManager();
|
|||||||
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
|
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
|
||||||
[[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<const Class> derived_type_class);
|
[[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<const Class> derived_type_class);
|
||||||
void UpdateEntityManager(EntityManager& manager);
|
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 DerivedType = void*;
|
||||||
using EntityID = uint64_t;
|
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
|
// clang-format off
|
||||||
#define ENTITY_TYPE_LIST(X) X(Inert),
|
#define ENTITY_TYPE_LIST(X) X(Inert),
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Core/Common/String.h>
|
#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_create_directory(String path, bool recursive);
|
||||||
|
|
||||||
JULIET_API bool fs_path_exists(String path);
|
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/NonNullPtr.h>
|
||||||
#include <Core/Common/String.h>
|
#include <Core/Common/String.h>
|
||||||
#include <Juliet.h>
|
#include <Juliet.h>
|
||||||
|
#include "Core/HAL/Filesystem/Filesystem.h"
|
||||||
|
|
||||||
// Opaque type
|
// Opaque type
|
||||||
struct IOStream;
|
struct IOStream;
|
||||||
@@ -52,15 +53,17 @@ extern JULIET_API IOStream* IOFromInterface(NonNullPtr<Arena> arena, NonNullPtr<
|
|||||||
NonNullPtr<IOStreamDataPayload> payload);
|
NonNullPtr<IOStreamDataPayload> payload);
|
||||||
|
|
||||||
// Write formatted string into the stream.
|
// Write formatted string into the stream.
|
||||||
extern JULIET_API size_t IOPrintf(NonNullPtr<IOStream> stream, _Printf_format_string_ const char* format, ...);
|
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 IOWrite(NonNullPtr<IOStream> stream, ByteBuffer inBuffer);
|
||||||
|
|
||||||
extern JULIET_API size_t IORead(NonNullPtr<IOStream> stream, void* ptr, size_t size);
|
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 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);
|
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, 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 initialize_user_system();
|
||||||
void shutdown_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/Common/String.h>
|
||||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||||
#include <Core/HAL/Filesystem/Filesystem_Platform.h>
|
#include <Core/HAL/Filesystem/Filesystem_Platform.h>
|
||||||
@@ -106,6 +106,27 @@ bool fs_path_exists(String path)
|
|||||||
return Platform::fs_path_exists(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)
|
void InitFilesystem(NonNullPtr<Arena> arena)
|
||||||
{
|
{
|
||||||
CachedBasePath = Platform::GetBasePath(arena);
|
CachedBasePath = Platform::GetBasePath(arena);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||||
|
|
||||||
@@ -9,4 +9,6 @@ namespace Platform
|
|||||||
Path fs_create_clean_path(NonNullPtr<Arena> arena, String path);
|
Path fs_create_clean_path(NonNullPtr<Arena> arena, String path);
|
||||||
bool fs_create_directory(String path, bool recursive);
|
bool fs_create_directory(String path, bool recursive);
|
||||||
bool fs_path_exists(String path);
|
bool fs_path_exists(String path);
|
||||||
|
bool fs_move_file(Path old_path, Path new_path);
|
||||||
|
bool fs_delete_file(String path);
|
||||||
} // namespace Platform
|
} // 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/Filesystem/Filesystem_Platform.h>
|
||||||
#include <Core/HAL/Win32.h>
|
#include <Core/HAL/Win32.h>
|
||||||
#include <Core/Logging/LogManager.h>
|
#include <Core/Logging/LogManager.h>
|
||||||
@@ -167,10 +167,9 @@ namespace Platform
|
|||||||
if (!create_result)
|
if (!create_result)
|
||||||
{
|
{
|
||||||
const DWORD error = GetLastError();
|
const DWORD error = GetLastError();
|
||||||
if (error != ERROR_ALREADY_EXISTS)
|
if (error == ERROR_ALREADY_EXISTS)
|
||||||
{
|
{
|
||||||
LogDebug(LogCategory::Core, "Folder already exists");
|
LogDebug(LogCategory::Core, "Folder already exists");
|
||||||
result = false;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -191,4 +190,20 @@ namespace Platform
|
|||||||
const bool result = (attribs != INVALID_FILE_ATTRIBUTES);
|
const bool result = (attribs != INVALID_FILE_ATTRIBUTES);
|
||||||
return result;
|
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
|
} // namespace Platform
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <Core/Logging/LogManager.h>
|
#include <Core/Logging/LogManager.h>
|
||||||
#include <Core/Logging/LogTypes.h>
|
#include <Core/Logging/LogTypes.h>
|
||||||
#include <Core/Thread/Thread.h>
|
#include <Core/Thread/Thread.h>
|
||||||
|
#include "Core/Thread/ThreadContext.h"
|
||||||
|
|
||||||
IOStream* IOFromFile(NonNullPtr<Arena> arena, String filename, String mode)
|
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;
|
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 IOClose(NonNullPtr<IOStream> stream)
|
||||||
{
|
{
|
||||||
bool result = true;
|
bool result = true;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#include <Engine/Engine.h>
|
#include <Engine/Engine.h>
|
||||||
|
|
||||||
#include <Core/Common/CoreUtils.h>
|
#include <Core/Common/CoreUtils.h>
|
||||||
#include <Core/HAL/Event/SystemEvent.h>
|
#include <Core/HAL/Event/SystemEvent.h>
|
||||||
@@ -154,12 +154,12 @@ void InitializeEngine(JulietInit_Flags flags)
|
|||||||
|
|
||||||
InitializeLogManager();
|
InitializeLogManager();
|
||||||
|
|
||||||
|
InitFilesystem(EngineInstance.PlatformArena);
|
||||||
|
|
||||||
#if JULIET_DEBUG
|
#if JULIET_DEBUG
|
||||||
UnitTest::RunUnitTests();
|
UnitTest::RunUnitTests();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
InitFilesystem(EngineInstance.PlatformArena);
|
|
||||||
|
|
||||||
initialize_user_system();
|
initialize_user_system();
|
||||||
|
|
||||||
JulietInit(flags);
|
JulietInit(flags);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace UnitTest
|
|||||||
void TestMemoryArena();
|
void TestMemoryArena();
|
||||||
void SerializationUnitTest();
|
void SerializationUnitTest();
|
||||||
void os_unit_test();
|
void os_unit_test();
|
||||||
|
void iostream_unit_test();
|
||||||
|
|
||||||
void RunUnitTests()
|
void RunUnitTests()
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,7 @@ namespace UnitTest
|
|||||||
TestMemoryArena();
|
TestMemoryArena();
|
||||||
VectorUnitTest();
|
VectorUnitTest();
|
||||||
SerializationUnitTest();
|
SerializationUnitTest();
|
||||||
|
iostream_unit_test();
|
||||||
|
|
||||||
LogMessage(LogCategory::Core, "Unit Tests Completed Successfully.");
|
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