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 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