diff --git a/CMakeLists.txt b/CMakeLists.txt index 26beb96..bd07444 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,8 +4,11 @@ project(YAVL VERSION 0.1.0 LANGUAGES C ) +set(YAVL_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}") + include(GNUInstallDirs) include(CMakePackageConfigHelpers) +include(CTest) find_package(Doxygen OPTIONAL_COMPONENTS doxygen) @@ -142,6 +145,10 @@ foreach(header "${${PROJECT_NAME}_GEN_HEADERS}") ) endforeach() +# Tests +# ===== +add_subdirectory("test/") + # Arch PKGBUILD # ============= configure_file( diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..bbc6614 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 4.2) +set(OLD_PROJECT_NAME "${PROJECT_NAME}") +project("${OLD_PROJECT_NAME}Tests" C CXX) + +# Collect tests +file(GLOB_RECURSE "${OLD_PROJECT_NAME}_TESTS" + LIST_DIRECTORIES false + CONFIGURE_DEPENDS + RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" + vec/**/*.c +) + +list(JOIN "${OLD_PROJECT_NAME}_TESTS" " " testrunner_str) +create_test_sourcelist( + "${OLD_PROJECT_NAME}_TEST_SRC" + TestRunner.c + ${${OLD_PROJECT_NAME}_TESTS} +) + +# Additional libraries +add_subdirectory("lib/") + +# Test runner +add_executable("${OLD_PROJECT_NAME}TestRunner" TestRunner.c ${${OLD_PROJECT_NAME}_TEST_SRC}) +add_executable("${OLD_PROJECT_NAME}::TestRunner" ALIAS "${OLD_PROJECT_NAME}TestRunner") +add_executable("${PROJECT_NAME}::Runner" ALIAS "${OLD_PROJECT_NAME}TestRunner") +target_link_libraries("${OLD_PROJECT_NAME}TestRunner" PRIVATE "${OLD_PROJECT_NAME}" "${PROJECT_NAME}RefLib") +target_include_directories("${OLD_PROJECT_NAME}TestRunner" PRIVATE "include/") +set_target_properties("${OLD_PROJECT_NAME}TestRunner" PROPERTIES + CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/test") + +# Check multithreading +if(NOT DEFINED TRY_STDC_THREADS) + message(CHECK_START "Test if is compiling") + try_compile(TRY_STDC_THREADS + SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/compiler/stdc_threads.c" + ) + if(TRY_STDC_THREADS) + message(CHECK_PASS "Success") + else() + message(CHECK_FAIL "Failure") + endif() +endif() + +if(TRY_STDC_THREADS) + set(THREAD_IMPL "C") + set(THREAD_IMPL_ENUM 1) +else(TRY_STDC_THREADS) + #POSIX + set(THREADS_PREFER_PTHREAD_FLAG TRUE) + find_package(Threads) + if(CMAKE_USE_PTHREADS_INIT AND NOT DEFINED TRY_POSIX_THREADS) + try_compile(TRY_POSIX_THREADS + SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/compiler/stdc_threads.c" + LINK_LIBRARIES Threads::Threads + ) + endif(CMAKE_USE_PTHREADS_INIT AND NOT DEFINED TRY_POSIX_THREADS) + if(TRY_POSIX_THREADS) + set(THREAD_IMPL "POSIX") + set(THREAD_IMPL_ENUM 2) + target_link_libraries("${OLD_PROJECT_NAME}TestRunner" PRIVATE Threads::Threads) + else(TRY_POSIX_THREADS) + set(THREAD_IMPL "") + set(THREAD_IMPL_ENUM 0) + endif(TRY_POSIX_THREADS) +endif(TRY_STDC_THREADS) + +message(STATUS "Threads implementation: ${THREAD_IMPL}") +target_compile_definitions("${OLD_PROJECT_NAME}TestRunner" + PRIVATE "DETECTED_THREAD_IMPL=${THREAD_IMPL_ENUM}") + +foreach(test IN LISTS "${OLD_PROJECT_NAME}_TESTS") + get_filename_component(TName "${test}" NAME_WE) + get_filename_component(TPath "${test}" DIRECTORY) + string(REPLACE "/" "->" TNamespace "${TPath}") + string(REPLACE "/" ";" TLabels "${TPath}") + string(REGEX MATCH "^should(_not)?" TKind "${TName}") + add_test( + NAME "${TNamespace}.${TName}" + COMMAND "${OLD_PROJECT_NAME}TestRunner" "${TPath}/${TName}" + ) + if(TKind STREQUAL "should_not") + set_property( + TEST + "${TNamespace}.${TName}" + PROPERTY + WILL_FAIL true + ) + endif() + set_property( + TEST + "${TNamespace}.${TName}" + PROPERTY + SKIP_RETURN_CODE 2 + ) + set_property( + TEST + "${TNamespace}.${TName}" + PROPERTY + LABELS ${TLabels} + ) +endforeach() diff --git a/test/compiler/posix_threads.c b/test/compiler/posix_threads.c new file mode 100644 index 0000000..b7978c4 --- /dev/null +++ b/test/compiler/posix_threads.c @@ -0,0 +1,15 @@ +#include +#include + +void* thr_fun(void* pass) { + return pass; +} + +int main() { + pthread_t thr; + pthread_create(&thr, NULL, thr_fun, (void*) 0xADD); + void* var=NULL; + pthread_join(thr, &var); + assert(var==0xADD); + return 0; +} \ No newline at end of file diff --git a/test/compiler/stdc_threads.c b/test/compiler/stdc_threads.c new file mode 100644 index 0000000..ba08d80 --- /dev/null +++ b/test/compiler/stdc_threads.c @@ -0,0 +1,15 @@ +#include +#include + +int thr_fun(void*_) { + return 0xADD; +} + +int main() { + thrd_t thr; + thrd_create(&thr, thr_fun, NULL); + int var; + thrd_join(thr, &var); + assert(var==0xADD); + return 0; +} \ No newline at end of file diff --git a/test/include/TestRunner.h b/test/include/TestRunner.h new file mode 100644 index 0000000..76e9cca --- /dev/null +++ b/test/include/TestRunner.h @@ -0,0 +1,3 @@ +#include "TestRunner/prefix.h" +#include "TestRunner/fun.h" +#include "TestRunner/res.h" diff --git a/test/include/TestRunner/fun.h b/test/include/TestRunner/fun.h new file mode 100644 index 0000000..5ae38f5 --- /dev/null +++ b/test/include/TestRunner/fun.h @@ -0,0 +1,4 @@ +// Macros +#define is(S) (code = code || !(S)) +#define test_if(S) if((code = code || !(S))) +#define countof(A) (sizeof(A)/sizeof(A[0])) diff --git a/test/include/TestRunner/prefix.h b/test/include/TestRunner/prefix.h new file mode 100644 index 0000000..fb6eb89 --- /dev/null +++ b/test/include/TestRunner/prefix.h @@ -0,0 +1,34 @@ +#ifdef PREFIX + +// Macro backups +#pragma push_macro("vec_api") +#pragma push_macro("vec_perf") + +// Map names to numbers +#define vec_api 1L +#define vec_perf 2L + +// Compare macro to macro by name +#if PREFIX == vec_api +#define PRIVATE_DEP_should(T,K) vec_api##K##T +#elif PREFIX == vec_perf +#define PRIVATE_DEP_should(T,K) vec_perf##K##T +#else /* PREFIX == vec_api */ +#error Unhandled PREFIX case +#endif /* PREFIX == vec_api */ + +// Macro restore +#undef vec_perf +#undef vec_api +#pragma pop_macro("vec_perf") +#pragma pop_macro("vec_api") + +// Static macros +#define it_should(T) int PRIVATE_DEP_should(T,_should_)(int argc,char** argv) +#define it_should_not(T) int PRIVATE_DEP_should(T,_should_not_)(int argc,char** argv) +#define _tostr(...) #__VA_ARGS__ +#define tostr(...) _tostr(__VA_ARGS__) +#define todo(...) { _Pragma(_tostr(message("Unimplemented test case: " #__VA_ARGS__))) return 2; } +#define it_todo(T) int PRIVATE_DEP_should(T,_should_)(int argc,char** argv) todo +#undef PREFIX +#endif diff --git a/test/include/TestRunner/res.h b/test/include/TestRunner/res.h new file mode 100644 index 0000000..144a210 --- /dev/null +++ b/test/include/TestRunner/res.h @@ -0,0 +1,7 @@ +#pragma once +#ifndef TESTRUNNER_H +#define TESTRUNNER_H +// Shared resources across tests +static char A[] = {1,2,3,4}; +static char B[] = {5,6,7,8,9,10}; +#endif diff --git a/test/lib/CMakeLists.txt b/test/lib/CMakeLists.txt new file mode 100644 index 0000000..8e09372 --- /dev/null +++ b/test/lib/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory("cppvec") +#add_subdirectory("bench") #TODO! diff --git a/test/lib/cppvec/CMakeLists.txt b/test/lib/cppvec/CMakeLists.txt new file mode 100644 index 0000000..d7778c2 --- /dev/null +++ b/test/lib/cppvec/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library("${PROJECT_NAME}RefLib" STATIC src/lib.cpp) +add_library("${PROJECT_NAME}::RefLib" ALIAS "${PROJECT_NAME}RefLib") +target_include_directories("${PROJECT_NAME}RefLib" PUBLIC "include/") diff --git a/test/lib/cppvec/include/RefLib.h b/test/lib/cppvec/include/RefLib.h new file mode 100644 index 0000000..926b1e1 --- /dev/null +++ b/test/lib/cppvec/include/RefLib.h @@ -0,0 +1,26 @@ +#ifdef __cplusplus +#include +extern "C" { +#define cpp_vector(T,...) std::vector +#else +#define cpp_vector(T,...) struct vector_##T +/// Type definition +cpp_vector(char); +#endif +#include +#include +// C++ API maps +cpp_vector(char) *const cpp_vec_new(); +void cpp_vec_free(cpp_vector(char) *const vec); +void cpp_vec_push(cpp_vector(char) *const vec, const char data); +char cpp_vec_pop(cpp_vector(char) *const vec, const char data); +char* cpp_vec_ref(cpp_vector(char) *const vec, size_t idx); +size_t cpp_vec_get_len(cpp_vector(char) *const vec); +size_t cpp_vec_get_reservd(cpp_vector(char) *const vec); +size_t cpp_vec_get_reservd_max(cpp_vector(char) *const vec); +void cpp_vec_set_reservd(cpp_vector(char) *const vec, size_t new_reservd); +char cpp_vec_get_el(cpp_vector(char) *const vec, size_t idx); +char cpp_vec_set_el(cpp_vector(char) *const vec, size_t idx, char v); +#ifdef __cplusplus +} +#endif diff --git a/test/lib/cppvec/src/lib.cpp b/test/lib/cppvec/src/lib.cpp new file mode 100644 index 0000000..7c24831 --- /dev/null +++ b/test/lib/cppvec/src/lib.cpp @@ -0,0 +1,41 @@ +#include + +// Bindings for C++ std::vec to C. + +extern "C" { + cpp_vector(char) *const cpp_vec_new() { + return new cpp_vector(char); + } + void cpp_vec_free(cpp_vector(char) *const vec) { + delete vec; + } + void cpp_vec_push(cpp_vector(char) *const vec, const char data) { + vec->push_back(data); + } + char cpp_vec_pop(cpp_vector(char) *const vec, const char data) { + char last = vec->back(); + vec->pop_back(); + return last; + } + char* cpp_vec_ref(cpp_vector(char) *const vec, size_t idx) { + return vec->data()+idx; + } + size_t cpp_vec_get_len(cpp_vector(char) *const vec) { + return vec->size(); + } + size_t cpp_vec_get_reservd(cpp_vector(char) *const vec) { + return vec->capacity(); + } + size_t cpp_vec_get_reservd_max(cpp_vector(char) *const vec) { + return vec->max_size(); + } + void cpp_vec_set_reservd(cpp_vector(char) *const vec, size_t new_reservd) { + vec->reserve(new_reservd); + } + char cpp_vec_get_el(cpp_vector(char) *const vec, size_t idx) { + return (*vec)[idx]; + } + char cpp_vec_set_el(cpp_vector(char) *const vec, size_t idx, char v) { + return (*vec)[idx] = v; + } +} diff --git a/test/vec/api/prefix.h b/test/vec/api/prefix.h new file mode 100644 index 0000000..f0e0c1f --- /dev/null +++ b/test/vec/api/prefix.h @@ -0,0 +1 @@ +#define PREFIX vec_api diff --git a/test/vec/api/should_concat_arrays.c b/test/vec/api/should_concat_arrays.c new file mode 100644 index 0000000..51890b3 --- /dev/null +++ b/test/vec/api/should_concat_arrays.c @@ -0,0 +1,59 @@ +#include +#include +#include + +#include "prefix.h" +#include +#include + +it_should(concat_arrays) { + yavl_vec_t vec = YAVL_VEC_T_ALLOCATOR; + struct Array { + const size_t len; + char *const A; + } arrs[] = { + (struct Array){countof(A),&A[0]}, + (struct Array){countof(B),&B[0]} + }; + unsigned char arrInd[][countof(arrs)] = { + {0,1}, + {1,0} + }; + size_t total = 0; + for(size_t i=0; i +#include +#include + +it_should(convert_arrays) { + char *const heaparr = calloc(5,sizeof(char)); + int code=0; + if(heaparr==NULL) return 2; // OOM is not our problem + for(char i=0;i<4;++i) heaparr[i]=i; + yavl_vec_t vec = YAVL_VEC_T_ALLOCATOR; + printf("0. Prepare...\n"); + test_if(yavl_vec_fromarray(&vec, heaparr, sizeof(char), 4) == YAVL_VEC_RES_OK) + return code; + printf("1. Arr -> Vec\n"); + // from that point, YAVL took pointer ownership + test_if(yavl_vec_scale(&vec, 10) == YAVL_VEC_RES_OK) + goto exit; + printf("2. Vec[5] -> Vec[10]\n"); + test_if(yavl_vec_push(&vec, &B[0], sizeof(B)/sizeof(B[0])) == YAVL_VEC_RES_OK) + goto exit; + printf("3. Vec[4-10] = B\n"); + test_if(vec.len == vec.reservd + && vec.reservd == sizeof(B)/sizeof(B[0])+4) + goto exit; + printf("4. ok(Vec.len,Vec.reservd)\n"); + // check data + size_t i=0; + for(;i<4;++i) { + char j; + yavl_vec_get(&vec, i, &j); + test_if(heaparr[i]==j) printf("Bug: read: %i!=%i\n",heaparr[i],j); + printf(" * %2zu | %2i\n",i,j); + test_if(j==i) goto exit; + } + for(;i<10;++i) { + char j; + yavl_vec_get(&vec, i, &j); + printf(" * %2i | %2i\n",B[i-4],j); + test_if(j==B[i-4]) break; + } + printf("5. Data check\n"); + exit: yavl_vec_free(&vec); + return code; +} diff --git a/test/vec/api/should_push_many.c b/test/vec/api/should_push_many.c new file mode 100644 index 0000000..ccc04cf --- /dev/null +++ b/test/vec/api/should_push_many.c @@ -0,0 +1,29 @@ +#include +#include +#include + +#include "prefix.h" +#include + +it_should(push_many) { + yavl_vec_t vec = YAVL_VEC_T_ALLOCATOR; + char testcase = -1; + int code = 0; + + test_if(yavl_vec_init(&vec, sizeof(A[0]), countof(A)) == YAVL_VEC_RES_OK) + return code; + test_if(yavl_vec_push(&vec, A, countof(A)) == YAVL_VEC_RES_OK) + goto exit; + test_if(vec.len == vec.reservd && vec.reservd == countof(A)) + goto exit; + for(size_t i = 0; i < countof(A); ++i) { + test_if(yavl_vec_get(&vec, i, &testcase) == YAVL_VEC_RES_OK) + break; + test_if(testcase == A[i]) { + break; + } + } + + exit: is(yavl_vec_free(&vec) == YAVL_VEC_RES_OK); + return code; +} diff --git a/test/vec/api/should_push_one.c b/test/vec/api/should_push_one.c new file mode 100644 index 0000000..a1838e1 --- /dev/null +++ b/test/vec/api/should_push_one.c @@ -0,0 +1,29 @@ +#include +#include +#include + +#include "prefix.h" +#include + +it_should(push_one) { + yavl_vec_t vec = YAVL_VEC_T_ALLOCATOR; + char testcase = -1; + int code = 0; + + printf("1. Init\n"); + test_if(yavl_vec_init(&vec, sizeof(A[0]), 1) == YAVL_VEC_RES_OK) + return code; + printf("2. Push\n"); + test_if(yavl_vec_push(&vec, &A[0], 1) == YAVL_VEC_RES_OK) + goto exit; + printf("3. Get\n"); + test_if(yavl_vec_get(&vec, 0, &testcase) == YAVL_VEC_RES_OK) + goto exit; + printf("4. Compares\n"); + test_if(testcase == A[0]) printf(" - tetcase (%i/%i)\n",testcase,A[0]); + test_if(vec.len == vec.reservd) printf(" - len (%zu/%zu)\n",vec.len,vec.reservd); + is(vec.reservd == 1); + + exit: is(yavl_vec_free(&vec) == YAVL_VEC_RES_OK); + return code; +} diff --git a/test/vec/perf/prefix.h b/test/vec/perf/prefix.h new file mode 100644 index 0000000..ac52b15 --- /dev/null +++ b/test/vec/perf/prefix.h @@ -0,0 +1 @@ +#define PREFIX vec_perf diff --git a/test/vec/perf/should_push_not_slower_than_cpp.c b/test/vec/perf/should_push_not_slower_than_cpp.c new file mode 100644 index 0000000..fb0d46f --- /dev/null +++ b/test/vec/perf/should_push_not_slower_than_cpp.c @@ -0,0 +1,191 @@ +#include "YAVL/vec.h" +#include "prefix.h" +#include +#include + +#include +#include +#include + +#if !defined(__STDC_NO_THREADS__) && DETECTED_THREAD_IMPL == 1 // C +#include +#define THREADS_INCLUDED 1 +#define THREAD_RET int +#define THREAD_RET_OK thrd_success +#elif DETECTED_THREAD_IMPL == 2 // POSIX +#include +#define PTHREAD_INCLUDED 1 +#define THREAD_RET void* +#define THREAD_RET_OK NULL +#else +#define THREAD_RET int +#define THREAD_RET_OK 0 +#endif + +static size_t default_reservd = 0; + +typedef struct { + double res_time; + const char *const restrict test_data; + size_t test_data_len; +} bench_t; + +static THREAD_RET bench_cpp(void* userdata) { + bench_t *const data = userdata; + struct timespec t[2] = {}; + cpp_vector(char) *cpp = NULL; + data->res_time=0; + + for(unsigned char b=0;b<=20;++b) { + cpp = cpp_vec_new(); + timespec_get(&t[0], TIME_UTC); + for(size_t i=0; itest_data_len; ++i) + cpp_vec_push(cpp,data->test_data[i]); + timespec_get(&t[1], TIME_UTC); + cpp_vec_free(cpp); + + t[1].tv_nsec-=t[0].tv_nsec; + t[1].tv_sec-=t[0].tv_sec; + if(b) + data->res_time += (((double)t[1].tv_nsec)/1e9)+(t[1].tv_sec); + } + return THREAD_RET_OK; +} + +static THREAD_RET bench_yavl(void* userdata) { + bench_t *const data = userdata; + struct timespec t[2] = {}; + yavl_vec_t yavl = YAVL_VEC_T_ALLOCATOR; + data->res_time=0; + + for(unsigned char b=0;b<=20;++b) { + yavl_vec_init(&yavl,sizeof(char),default_reservd); + timespec_get(&t[0], TIME_UTC); + for(size_t i=0; itest_data_len; ++i) + yavl_vec_push(&yavl, data->test_data+i, 1); + timespec_get(&t[1], TIME_UTC); + yavl_vec_free(&yavl); + t[1].tv_nsec-=t[0].tv_nsec; + t[1].tv_sec-=t[0].tv_sec; + if(b) + data->res_time += ((double)t[1].tv_nsec/1e9)+(t[1].tv_sec); + } + return THREAD_RET_OK; +} + +static THREAD_RET bench_inline(void* userdata) { + bench_t *const data = userdata; + struct timespec t[2] = {}; + data->res_time=0; + + for(unsigned char b=0;b<=20;++b) { + char* res = malloc(1*sizeof(char)); + size_t l = 1; + timespec_get(&t[0], TIME_UTC); + size_t i=0; + for(; itest_data_len; ++i) { + if(i>l) { + l=l<<1; + void *new = realloc(res,l*sizeof(char)); + if(new) res=new; + } + res[i]=data->test_data[i]; + } + { + void *new = realloc(res,(l=i)*sizeof(char)); + if(new) res=new; + } + timespec_get(&t[1], TIME_UTC); + free(res); + t[1].tv_nsec-=t[0].tv_nsec; + t[1].tv_sec-=t[0].tv_sec; + if(b) + data->res_time += ((double)t[1].tv_nsec/1e9)+(t[1].tv_sec); + } + return THREAD_RET_OK; +} + +it_should(push_not_slower_than_cpp) { + // RNG + srand(time(NULL)); + + // Default reservd from C++ vector + { + cpp_vector(char) *cpp = cpp_vec_new(); + default_reservd=cpp_vec_get_reservd(cpp); + cpp_vec_free(cpp); + } + + // Prepare data + printf("Preparing data array...\n"); + size_t tdata_len = 100L*1024L*1024L; + char *const restrict tdata = calloc(tdata_len,sizeof(tdata[0])); + if(tdata == NULL) return 0; + for(size_t i=0; i0;--i) { + char j = rand() % (i + 1); + char temp = ind[i]; + ind[i] = ind[j]; + ind[j] = temp; + } + + #if THREADS_INCLUDED || PTHREAD_INCLUDED + printf("Running benchmarks in their own threads...\n"); + // Random thread creation, ordered thread collection + for(size_t i=0;ibdata[1].res_time; +}