From bd5b32c493eed54ff97d245635e87501ed946755 Mon Sep 17 00:00:00 2001 From: synqa Date: Mon, 16 Mar 2026 17:27:34 +0900 Subject: [PATCH 1/2] Introducing unit test Add two test suites using CTest. - TestEncodeDecode: A test to verify that data archived using HamCore is stored with the same content. - TestArchiveFormat: A test to verify that the structure of the archive created by HamCore is correct. Each test case uses file filled with random bytes. The test is performed with various file sizes and counts. --- CMakeLists.txt | 6 ++ tests/CMakeLists.txt | 64 ++++++++++++++ tests/RandomFile.c | 114 ++++++++++++++++++++++++ tests/RandomFile.h | 17 ++++ tests/TestArchiveFormat.c | 181 ++++++++++++++++++++++++++++++++++++++ tests/TestEncodeDecode.c | 145 ++++++++++++++++++++++++++++++ tests/Utils.c | 168 +++++++++++++++++++++++++++++++++++ tests/Utils.h | 28 ++++++ 8 files changed, 723 insertions(+) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/RandomFile.c create mode 100644 tests/RandomFile.h create mode 100644 tests/TestArchiveFormat.c create mode 100644 tests/TestEncodeDecode.c create mode 100644 tests/Utils.c create mode 100644 tests/Utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4017ff2..a39135c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,3 +36,9 @@ target_sources(libhamcore find_package(ZLIB REQUIRED) target_link_libraries(libhamcore PRIVATE ZLIB::ZLIB) + +option(BUILD_TESTING "Enable testing" OFF) +if (BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..6225781 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,64 @@ +add_library(TestModule) +target_sources(TestModule + PRIVATE + RandomFile.c + Utils.c +) +target_include_directories(TestModule PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) +target_link_libraries(TestModule PUBLIC libhamcore ZLIB::ZLIB) + +add_executable(TestEncodeDecode) +target_sources(TestEncodeDecode PRIVATE TestEncodeDecode.c) +target_link_libraries(TestEncodeDecode PRIVATE TestModule) + +function(EncodeDecodeTest mode num_files) + set(working_dir ${CMAKE_CURRENT_BINARY_DIR}/EncodeDecodeTest-${mode}-${num_files}) + file(MAKE_DIRECTORY ${working_dir}) + + add_test( + NAME TestEncodeDecode-${mode}-${num_files} + COMMAND TestEncodeDecode ${mode} ${num_files} + WORKING_DIRECTORY ${working_dir} + ) +endfunction() + +EncodeDecodeTest(SMALL 1) +EncodeDecodeTest(SMALL 100) +EncodeDecodeTest(SMALL 1000) +EncodeDecodeTest(MEDIUM 1) +EncodeDecodeTest(MEDIUM 100) +EncodeDecodeTest(MEDIUM 500) +EncodeDecodeTest(LARGE 1) +EncodeDecodeTest(LARGE 100) +EncodeDecodeTest(LARGE 250) +EncodeDecodeTest(MIXED 10) +EncodeDecodeTest(MIXED 250) +EncodeDecodeTest(MIXED 500) + +add_executable(TestArchiveFormat) +target_sources(TestArchiveFormat PRIVATE TestArchiveFormat.c) +target_link_libraries(TestArchiveFormat PRIVATE TestModule) + +function(ArchiveFormatTest mode num_files) + set(working_dir ${CMAKE_CURRENT_BINARY_DIR}/TestArchiveFormat-${mode}-${num_files}) + file(MAKE_DIRECTORY ${working_dir}) + + add_test( + NAME TestArchiveFormat-${mode}-${num_files} + COMMAND TestArchiveFormat ${mode} ${num_files} + WORKING_DIRECTORY ${working_dir} + ) +endfunction() + +ArchiveFormatTest(SMALL 1) +ArchiveFormatTest(SMALL 100) +ArchiveFormatTest(SMALL 1000) +ArchiveFormatTest(MEDIUM 1) +ArchiveFormatTest(MEDIUM 100) +ArchiveFormatTest(MEDIUM 500) +ArchiveFormatTest(LARGE 1) +ArchiveFormatTest(LARGE 100) +ArchiveFormatTest(LARGE 250) +ArchiveFormatTest(MIXED 10) +ArchiveFormatTest(MIXED 250) +ArchiveFormatTest(MIXED 500) diff --git a/tests/RandomFile.c b/tests/RandomFile.c new file mode 100644 index 0000000..c160d73 --- /dev/null +++ b/tests/RandomFile.c @@ -0,0 +1,114 @@ +#include "RandomFile.h" +#include "FileSystem.h" + +#include +#include +#include + +static uint32_t Xorshift32(uint32_t *state) +{ + uint32_t x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +static uint8_t *MakeRandomBytes(const size_t size) +{ + if (size == 0) + { + return NULL; + } + + uint8_t *buffer = malloc(size); + if (!buffer) + { + return NULL; + } + + uint32_t state = 12345; + + size_t i = 0; + for (; i + sizeof(uint32_t) <= size; i += sizeof(uint32_t)) + { + uint32_t random_byte = Xorshift32(&state); + memcpy(buffer + i, &random_byte, sizeof(uint32_t)); + } + for (; i < size; i++) + { + buffer[i] = (uint8_t)(Xorshift32(&state) % 256); + } + + return buffer; +} + +RANDOM_FILE *CreateRandomFile(const char *path, const size_t size) +{ + if (!path || size == 0) + { + return NULL; + } + + RANDOM_FILE *random_file = malloc(sizeof(RANDOM_FILE)); + if (!random_file) + { + return NULL; + } + memset(random_file, 0, sizeof(RANDOM_FILE)); + + random_file->Size = size; + + random_file->Path = malloc(strlen(path) + 1); + if (!random_file->Path) + { + goto FINAL; + } + strcpy(random_file->Path, path); + + uint8_t *random_bytes = MakeRandomBytes(size); + if (!random_bytes) + { + goto FINAL; + } + + FILE *file = Ham_FileOpen(path, true); + if (!file) + { + free(random_bytes); + goto FINAL; + } + + if (! Ham_FileWrite(file, random_bytes, random_file->Size)) + { + free(random_bytes); + Ham_FileClose(file); + goto FINAL; + } + + free(random_bytes); + Ham_FileClose(file); + + return random_file; + +FINAL: + DeleteRandomFile(random_file); + return NULL; +} + +void DeleteRandomFile(RANDOM_FILE *file) +{ + if (!file) + { + return; + } + + if (file->Path) + { + remove(file->Path); + free(file->Path); + } + + free(file); +} diff --git a/tests/RandomFile.h b/tests/RandomFile.h new file mode 100644 index 0000000..c7ec41b --- /dev/null +++ b/tests/RandomFile.h @@ -0,0 +1,17 @@ +#ifndef RANDOMFILE_H +#define RANDOMFILE_H + +#include +#include +#include + +typedef struct RANDOM_FILE +{ + size_t Size; + char *Path; +} RANDOM_FILE; + +RANDOM_FILE *CreateRandomFile(const char *path, const size_t size); +void DeleteRandomFile(RANDOM_FILE *file); + +#endif diff --git a/tests/TestArchiveFormat.c b/tests/TestArchiveFormat.c new file mode 100644 index 0000000..53cec8e --- /dev/null +++ b/tests/TestArchiveFormat.c @@ -0,0 +1,181 @@ +#include "FileSystem.h" +#include "Hamcore.h" +#include "Memory.h" +#include "RandomFile.h" +#include "Utils.h" + +#include +#include +#include +#include + +#include + +#define HAMCORE_NAME "hamcore.se2" + +typedef struct TEST_PARAMS +{ + size_t num_files; + FILESIZE_MODE mode; +} TEST_PARAMS; + +static bool ParseArguments(int argc, char **argv, TEST_PARAMS *parameter) +{ + if (argc != 3) + { + return false; + } + + if (!FilesizeModeFromStr(argv[1], ¶meter->mode)) + { + return false; + } + + int num_files = atoi(argv[2]); + if (num_files <= 0) + { + return false; + } + parameter->num_files = num_files; + + return true; +} + +static bool Test(RANDOM_FILE **files, TEST_PARAMS *params) +{ + if (!files || !params) + { + return false; + } + + HAMCORE *hamcore = HamcoreOpen(HAMCORE_NAME); + if (!hamcore) + { + return false; + } + + FILE *ham_file = hamcore->File; + if (!Ham_FileSeek(ham_file, 0)) + { + goto FINAL; + } + + // Verify magic header matches "HamCore" + uint8_t header[7] = {}; + if (!Ham_FileRead(ham_file, header, sizeof(header))) + { + goto FINAL; + } + assert(memcmp(header, HAMCORE_HEADER_DATA, 7) == 0); + + // Verify file count matches the number of archived files + uint32_t num_files = 0; + if (!Ham_FileRead(ham_file, &num_files, sizeof(uint32_t))) + { + goto FINAL; + } + assert(BigEndian32(num_files) == params->num_files); + + // Check file table + for (int i = 0; i < params->num_files; i++) + { + RANDOM_FILE *file = files[i]; + if (!file) + { + goto FINAL; + } + uint32_t path_actual_len = strlen(file->Path); + + // Verify path length matches (stored as strlen + 1) + uint32_t path_len; + if (!Ham_FileRead(ham_file, &path_len, sizeof(uint32_t))) + { + goto FINAL; + } + assert(BigEndian32(path_len) == path_actual_len + 1); + + // Verify path string matches the original file path + char *path_str = malloc(path_actual_len); + if (!path_str) + { + goto FINAL; + } + if (!Ham_FileRead(ham_file, path_str, path_actual_len)) + { + free(path_str); + goto FINAL; + } + assert(memcmp(path_str, file->Path, path_actual_len) == 0); + free(path_str); + + // Verify original file size matches + uint32_t original_size; + if (!Ham_FileRead(ham_file, &original_size, sizeof(uint32_t))) + { + goto FINAL; + } + assert(BigEndian32(original_size) == file->Size); + + // Verify compressed size does not exceed zlib upper bound + uint32_t compressed_size; + if (!Ham_FileRead(ham_file, &compressed_size, sizeof(uint32_t))) + { + goto FINAL; + } + assert(BigEndian32(compressed_size) <= compressBound(file->Size)); + + // Verify offset is past the header + uint32_t offset; + if (!Ham_FileRead(ham_file, &offset, sizeof(uint32_t))) + { + goto FINAL; + } + assert(BigEndian32(offset) >= HAMCORE_HEADER_SIZE); + } + + // Verify file offsets are contiguous + HAMCORE_FILE *files_list = hamcore->Files.List; + for (int i = 0; i < params->num_files - 1; i++) + { + size_t next_offset = files_list[i].Offset + files_list[i].Size; + assert(next_offset == files_list[i + 1].Offset); + } + + // Verify last file's data extends exactly to end of archive + HAMCORE_FILE *last_file = &hamcore->Files.List[params->num_files - 1]; + assert(last_file->Offset + last_file->Size == Ham_FileSize(HAMCORE_NAME)); + + HamcoreClose(hamcore); + return true; + +FINAL: + HamcoreClose(hamcore); + return false; +} + +int main(int argc, char **argv) +{ + TEST_PARAMS parameters = {0}; + if (!ParseArguments(argc, argv, ¶meters)) + { + return 2; + } + + RANDOM_FILE **random_files = CreateRandomFiles(parameters.mode, parameters.num_files); + if (!random_files) + { + return 1; + } + if (!CreateHamcore(HAMCORE_NAME, random_files, parameters.num_files)) + { + DeleteRandomFiles(random_files, parameters.num_files); + return 1; + } + + bool result = Test(random_files, ¶meters); + + DeleteHamcore(HAMCORE_NAME); + DeleteRandomFiles(random_files, parameters.num_files); + + return !result; +} diff --git a/tests/TestEncodeDecode.c b/tests/TestEncodeDecode.c new file mode 100644 index 0000000..7bc460a --- /dev/null +++ b/tests/TestEncodeDecode.c @@ -0,0 +1,145 @@ +#include "FileSystem.h" +#include "Hamcore.h" +#include "RandomFile.h" +#include "Utils.h" + +#include +#include +#include + +#define HAMCORE_NAME "hamcore.se2" + +typedef struct TEST_PARAMS +{ + size_t num_files; + FILESIZE_MODE mode; +} TEST_PARAMS; + +static bool ParseArguments(int argc, char **argv, TEST_PARAMS *params) +{ + if (argc != 3) + { + return false; + } + + if (!FilesizeModeFromStr(argv[1], ¶ms->mode)) + { + return false; + } + + int num_files = atoi(argv[2]); + if (num_files <= 0) + { + return false; + } + params->num_files = num_files; + + return true; +} + +static bool Test(RANDOM_FILE **files, TEST_PARAMS *params) +{ + if (!files || !params) + { + return false; + } + + HAMCORE *hamcore = HamcoreOpen(HAMCORE_NAME); + if (!hamcore) + { + return false; + } + + // Verify file count matches the number of archived files + assert(hamcore->Files.Num == params->num_files); + + for (int i = 0; i < params->num_files; i++) + { + RANDOM_FILE *random_file = files[i]; + if (!random_file) + { + goto CLEANUP; + } + + const HAMCORE_FILE *hamcore_file = HamcoreFind(hamcore, random_file->Path); + if (!hamcore_file) + { + goto CLEANUP; + } + // Verify that the file size after archiving matches the file size before archiving + assert(hamcore_file->OriginalSize == random_file->Size); + + uint8_t *actual_bytes = malloc(random_file->Size); + if (!actual_bytes) + { + goto CLEANUP; + } + FILE *file = Ham_FileOpen(random_file->Path, false); + if (!file) + { + free(actual_bytes); + goto CLEANUP; + } + if (!Ham_FileRead(file, actual_bytes, random_file->Size)) + { + free(actual_bytes); + Ham_FileClose(file); + goto CLEANUP; + } + Ham_FileClose(file); + + uint8_t *read_bytes = malloc(hamcore_file->OriginalSize); + if (!read_bytes) + { + free(actual_bytes); + goto CLEANUP; + } + if (!HamcoreRead(hamcore, read_bytes, hamcore_file)) + { + free(read_bytes); + free(actual_bytes); + goto CLEANUP; + } + + // Verify that the data after archiving matches the archived data before archiving + assert(memcmp(read_bytes, actual_bytes, random_file->Size) == 0); + + free(read_bytes); + free(actual_bytes); + } + + HamcoreClose(hamcore); + + return true; + +CLEANUP: + HamcoreClose(hamcore); + return false; +} + +int main(int argc, char **argv) +{ + TEST_PARAMS parameters = {0}; + if (!ParseArguments(argc, argv, ¶meters)) + { + return 2; + } + + RANDOM_FILE **random_files = CreateRandomFiles(parameters.mode, parameters.num_files); + if (!random_files) + { + return 1; + } + if (!CreateHamcore(HAMCORE_NAME, random_files, parameters.num_files)) + { + DeleteRandomFiles(random_files, parameters.num_files); + return 1; + } + + bool result = Test(random_files, ¶meters); + + DeleteHamcore(HAMCORE_NAME); + DeleteRandomFiles(random_files, parameters.num_files); + + return !result; +} diff --git a/tests/Utils.c b/tests/Utils.c new file mode 100644 index 0000000..c1f6348 --- /dev/null +++ b/tests/Utils.c @@ -0,0 +1,168 @@ +#include "Utils.h" +#include "Hamcore.h" +#include "RandomFile.h" + +#include +#include + +bool FilesizeModeFromStr(const char *str, FILESIZE_MODE *mode) +{ + if (!str) + { + return false; + } + + if (strcmp(str, "SMALL") == 0) + { + *mode = SMALL; + } + else if (strcmp(str, "MEDIUM") == 0) + { + *mode = MEDIUM; + } + else if (strcmp(str, "LARGE") == 0) + { + *mode = LARGE; + } + else if (strcmp(str, "MIXED") == 0) + { + *mode = MIXED; + } + else + { + return false; + } + + return true; +} + +static bool CreateSequenceFile(const char *format, RANDOM_FILE **files, size_t size, size_t num_files) +{ + if (!format || !files) + { + return false; + } + + char path[64] = {0}; + for (int i = 0; i < num_files; i++) + { + if (snprintf(path, sizeof(path), format, i) < 0) + { + return false; + } + files[i] = CreateRandomFile(path, size); + if (!files[i]) + { + return false; + } + } + + return true; +} + +RANDOM_FILE **CreateRandomFiles(FILESIZE_MODE mode, size_t num_files) +{ + if (num_files == 0) + { + return NULL; + } + + RANDOM_FILE **files = malloc(sizeof(RANDOM_FILE *) * num_files); + if (!files) + { + return NULL; + } + memset(files, 0, sizeof(RANDOM_FILE *) * num_files); + + size_t num_small_files = 0; + size_t num_medium_files = 0; + size_t num_large_files = 0; + if (mode == SMALL) + { + num_small_files = num_files; + } + else if (mode == MEDIUM) + { + num_medium_files = num_files; + } + else if (mode == LARGE) + { + num_large_files = num_files; + } + else if (mode == MIXED) + { + size_t part = num_files / 3; + num_small_files = part + (num_files % 3); + num_medium_files = part; + num_large_files = part; + } + + size_t total = 0; + if (!CreateSequenceFile("small%09d", &files[total], SMALL_FILE_SIZE, num_small_files)) + { + goto FINAL; + } + total += num_small_files; + + if (!CreateSequenceFile("medium%09d", &files[total], MEDIUM_FILE_SIZE, num_medium_files)) + { + goto FINAL; + } + total += num_medium_files; + + if (!CreateSequenceFile("large%09d", &files[total], LARGE_FILE_SIZE, num_large_files)) + { + goto FINAL; + } + total += num_large_files; + + return files; + +FINAL: + DeleteRandomFiles(files, num_files); + return NULL; +} + +void DeleteRandomFiles(RANDOM_FILE **files, size_t num_files) +{ + if (!files || num_files == 0) + { + return; + } + + for (int i = 0; i < num_files; i++) + { + DeleteRandomFile(files[i]); + } + + free(files); +} + +bool CreateHamcore(const char *ham_name, RANDOM_FILE **files, size_t num_files) +{ + if (!ham_name || !files || num_files == 0) + { + return false; + } + + const char **src_paths = malloc(sizeof(char *) * num_files); + if (!src_paths) + { + return false; + } + for (int i = 0; i < num_files; i++) + { + src_paths[i] = files[i]->Path; + } + + bool result = HamcoreBuild(ham_name, NULL, src_paths, num_files); + + free(src_paths); + + return result; +} + +void DeleteHamcore(const char *ham_name) +{ + remove(ham_name); +} diff --git a/tests/Utils.h b/tests/Utils.h new file mode 100644 index 0000000..e5ebcbd --- /dev/null +++ b/tests/Utils.h @@ -0,0 +1,28 @@ +#ifndef UTILS_H +#define UTILS_H + +#include "RandomFile.h" + +#include + +#define SMALL_FILE_SIZE 1 * 1024 +#define MEDIUM_FILE_SIZE 500 * 1024 +#define LARGE_FILE_SIZE 5 * 1024 * 1024 + +typedef enum FILESIZE_MODE +{ + SMALL, + MEDIUM, + LARGE, + MIXED +} FILESIZE_MODE; + +bool FilesizeModeFromStr(const char *str, FILESIZE_MODE *mode); + +RANDOM_FILE **CreateRandomFiles(FILESIZE_MODE mode, size_t num_files); +void DeleteRandomFiles(RANDOM_FILE **files, size_t num_files); + +bool CreateHamcore(const char *ham_name, RANDOM_FILE **files, size_t num_files); +void DeleteHamcore(const char *ham_name); + +#endif From 13e52c6cc0b8f1afac2249c065bd23536d6152d5 Mon Sep 17 00:00:00 2001 From: synqa Date: Sat, 28 Mar 2026 21:16:54 +0900 Subject: [PATCH 2/2] Add CI for unit test --- .github/workflows/build.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05f5818..6c6052a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,8 +7,8 @@ env: VCVARS_PATH: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat" jobs: - build: - name: "Build" + build_and_test: + name: "Build and Test" runs-on: ${{matrix.os}} strategy: fail-fast: false @@ -35,22 +35,32 @@ jobs: with: linux: | mkdir build && cd build - cmake -G Ninja .. + cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DCMAKE_C_FLAGS="-fsanitize=address,leak,undefined -fno-omit-frame-pointer" .. cmake --build . macos: | mkdir build && cd build - cmake -G Ninja .. + cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" .. cmake --build . windows: | mkdir build && cd build call "${{env.VCVARS_PATH}}" - cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE="${{env.TOOLCHAIN_PATH}}" -DVCPKG_TARGET_TRIPLET=x64-windows-static-md .. + cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DCMAKE_TOOLCHAIN_FILE="${{env.TOOLCHAIN_PATH}}" -DVCPKG_TARGET_TRIPLET=x64-windows-static-md -DCMAKE_C_FLAGS="/fsanitize=address" .. cmake --build . windowsShell: cmd + - uses: knicknic/os-specific-run@v1.0.4 + name: Run CTest + with: + linux: ctest -j --output-on-failure --test-dir build + macos: ctest -j --output-on-failure --test-dir build + windows: | + call "${{env.VCVARS_PATH}}" + ctest -j --output-on-failure --test-dir build + windowsShell: cmd + integration_test: name: "Integration Test" - needs: build + needs: build_and_test runs-on: ${{matrix.os}} strategy: fail-fast: false @@ -120,3 +130,4 @@ jobs: archive: false path: | hamcore-${{matrix.os}}.se2 + retention-days: 30