diff --git a/CMake/Test.cmake b/CMake/Test.cmake index 58097529..a1c27be2 100644 --- a/CMake/Test.cmake +++ b/CMake/Test.cmake @@ -5,8 +5,6 @@ function(configure_test) set(oneValueArgs NAME SUBPROJECT) set(multiValueArgs WRAPPERS SOURCES INCLUDES LIBRARIES) cmake_parse_arguments(CONFIGURE_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - message("${CONFIGURE_TEST_SOURCES}") - message("${CONFIGURE_TEST_WRAPPERS}") include_directories( ${CMOCKA_INCLUDE_DIR} @@ -19,7 +17,12 @@ function(configure_test) ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test/${CONFIGURE_TEST_SUBPROJECT}/${CONFIGURE_TEST_NAME} ) - add_executable(${CONFIGURE_TEST_NAME}_test ${CONFIGURE_TEST_SOURCES}) + add_executable(${CONFIGURE_TEST_NAME}_test + ${CMAKE_SOURCE_DIR}/test/util.c + ${CONFIGURE_TEST_SOURCES} + ) + + list(APPEND CONFIGURE_TEST_WRAPPERS "malloc" "calloc" "realloc" "free") list(LENGTH CONFIGURE_TEST_WRAPPERS WRAPPED_COUNT) @@ -32,7 +35,6 @@ function(configure_test) "${WRAPPED} \ -Wl,--wrap=${WRAPPER}" ) - message("${WRAPPER}, ${WRAPPED}") endforeach() set_target_properties(${CONFIGURE_TEST_NAME}_test @@ -42,4 +44,6 @@ function(configure_test) endif() target_link_libraries(${CONFIGURE_TEST_NAME}_test ${CMOCKA_LIBRARIES} ${CONFIGURE_TEST_LIBRARIES}) + + set(test_targets ${test_targets} ${CONFIGURE_TEST_NAME}_test PARENT_SCOPE) endfunction() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc29dad0..67694fd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,39 @@ branch. Instead, when you start working on a feature, do this: 4. git push -u origin add-so-and-so-feature 5. Make pull request from your feature branch +## Writing Tests + +Tests are driven by [CMocka](https://cmocka.org/). When testing a given +function, we can "mock" out the functions it relies on to program their behavior +explicitly and test the function in isolation. The directory layout of `test/` +is identical to the global directory layout, but each C file in the parent tree +has its own directory in the test tree, with its own CMakeLists.txt that wires +things up. To add a test, make the appropriate directory in `test/` and add a +CMakeLists.txt that looks something like this made-up example: + +```cmake +configure_test( + SUBPROJECT swaymsg + NAME main + SOURCES + ${PROJECT_SOURCE_DIR}/swaymsg/main.c + swaymsg.c + WRAPPERS + ipc_open_socket + LIBRARIES + ${WLC_LIBRARIES} + INCLUDES + ${WLC_INCLUDES} +) +``` + +This defines a test suite in the swaymsg subproject that tests main. This file +would live at `test/swaymsg/main/CMakeLists.txt`. It specifies that it requires +`swaymsg/main.c` and `test/swaymsg/main/swaymsg.c`, the former being the actual +swaymsg source and the latter being the test suite. It mocks ipc_open_socket and +links against openssl. See the cmocka documentation or read existing tests to +learn more about how mocks work. + ## Coding Style Sway is written in C. The style guidelines is [kernel diff --git a/README.md b/README.md index e9143f0f..c8c531dd 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ On systems without logind, you need to suid the sway binary: sudo chmod a+s /usr/local/bin/sway +## Tests + +Run `make && make check` from the build directory to run tests. The exit code +will be the number of failed tests (0 for success). + ## Configuration If you already use i3, then copy your i3 config to `~/.config/sway/config` and diff --git a/include/tests.h b/include/tests.h index 47b47059..36b68587 100644 --- a/include/tests.h +++ b/include/tests.h @@ -6,4 +6,13 @@ #include #include +enum wrapper_behavior { + WRAPPER_INVOKE_REAL, + WRAPPER_INVOKE_CMOCKA, + WRAPPER_DO_ASSERTIONS, +}; + +int reset_mem_wrappers(void **state); +void memory_behavior(enum wrapper_behavior behavior); + #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e4717b2d..e6024560 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1 +1,7 @@ +set(test_targets "") + add_subdirectory(common) + +add_custom_target(check + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + COMMAND ${CMAKE_SOURCE_DIR}/test/runner) diff --git a/test/common/list/list.c b/test/common/list/list.c index dd66dec7..7cfc2609 100644 --- a/test/common/list/list.c +++ b/test/common/list/list.c @@ -5,15 +5,16 @@ #include "tests.h" #include "list.h" -static void test_test(void **state) { +static void test_create_list(void **state) { + memory_behavior(WRAPPER_INVOKE_CMOCKA); list_t *list = create_list(); - free(list); - assert_true(true); + assert_int_equal(list->length, 0); + list_free(list); } -int main() { +int main(int argc, char **argv) { const struct CMUnitTest tests[] = { - cmocka_unit_test(test_test), + cmocka_unit_test(test_create_list), }; - return cmocka_run_group_tests(tests, NULL, NULL); + return cmocka_run_group_tests(tests, reset_mem_wrappers, NULL); } diff --git a/test/runner b/test/runner new file mode 100755 index 00000000..cee622f9 --- /dev/null +++ b/test/runner @@ -0,0 +1,11 @@ +#!/usr/bin/bash +tests=$(find . -type f -name "*_test") +ret=0 +for test in $tests +do + printf 'Running %s\n' $(basename $test) + $test + ret+=$? +done + +exit $ret diff --git a/test/util.c b/test/util.c new file mode 100644 index 00000000..ef825200 --- /dev/null +++ b/test/util.c @@ -0,0 +1,74 @@ +#include +#include "tests.h" + +void *__real_malloc(size_t size); +void __real_free(void *ptr); +void *__real_calloc(size_t nmemb, size_t size); +void *__real_realloc(void *ptr, size_t size); + +enum wrapper_behavior _memory_behavior = WRAPPER_INVOKE_REAL; + +int reset_mem_wrappers(void **state) { + _memory_behavior = WRAPPER_INVOKE_REAL; + return 0; +} + +void memory_behavior(enum wrapper_behavior behavior) { + _memory_behavior = behavior; +} + +void *__wrap_malloc(size_t size) { + switch (_memory_behavior) { + case WRAPPER_INVOKE_CMOCKA: + return test_malloc(size); + case WRAPPER_DO_ASSERTIONS: + check_expected(size); + return mock_type(void *); + case WRAPPER_INVOKE_REAL: + default: + return __real_malloc(size); + } +} + +void __wrap_free(void *ptr) { + switch (_memory_behavior) { + case WRAPPER_INVOKE_CMOCKA: + test_free(ptr); + break; + case WRAPPER_DO_ASSERTIONS: + check_expected_ptr(ptr); + break; + case WRAPPER_INVOKE_REAL: + default: + __real_free(ptr); + break; + } +} + +void *__wrap_calloc(size_t nmemb, size_t size) { + switch (_memory_behavior) { + case WRAPPER_INVOKE_CMOCKA: + return test_calloc(nmemb, size); + case WRAPPER_DO_ASSERTIONS: + check_expected(nmemb); + check_expected(size); + return mock_type(void *); + case WRAPPER_INVOKE_REAL: + default: + return __real_calloc(nmemb, size); + } +} + +void *__wrap_realloc(void *ptr, size_t size) { + switch (_memory_behavior) { + case WRAPPER_INVOKE_CMOCKA: + return test_realloc(ptr, size); + case WRAPPER_DO_ASSERTIONS: + check_expected_ptr(ptr); + check_expected(size); + return mock_type(void *); + case WRAPPER_INVOKE_REAL: + default: + return __real_realloc(ptr, size); + } +}