diff --git a/.gitignore b/.gitignore index 48f8a0cb0..1ba87a37d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ install_manifest.txt *.o *.a bin/ -test/ build/ .lvimrc config-debug diff --git a/.travis.yml b/.travis.yml index a8e292ba2..772590d02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,11 @@ arch: - cairo - gdk-pixbuf2 - wlc-git + - cmocka script: - "cmake ." - "make" + - "make check" script: - "curl -s https://raw.githubusercontent.com/mikkeloscar/arch-travis/master/arch-travis.sh | bash" diff --git a/CMake/FindCMocka.cmake b/CMake/FindCMocka.cmake new file mode 100644 index 000000000..76b4ba74d --- /dev/null +++ b/CMake/FindCMocka.cmake @@ -0,0 +1,66 @@ +# - Try to find CMocka +# Once done this will define +# +# CMOCKA_ROOT_DIR - Set this variable to the root installation of CMocka +# +# Read-Only variables: +# CMOCKA_FOUND - system has CMocka +# CMOCKA_INCLUDE_DIR - the CMocka include directory +# CMOCKA_LIBRARIES - Link these to use CMocka +# CMOCKA_DEFINITIONS - Compiler switches required for using CMocka +# +#============================================================================= +# Copyright (c) 2011-2012 Andreas Schneider +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# + +set(_CMOCKA_ROOT_HINTS +) + +set(_CMOCKA_ROOT_PATHS + "$ENV{PROGRAMFILES}/cmocka" +) + +find_path(CMOCKA_ROOT_DIR + NAMES + include/cmocka.h + HINTS + ${_CMOCKA_ROOT_HINTS} + PATHS + ${_CMOCKA_ROOT_PATHS} +) +mark_as_advanced(CMOCKA_ROOT_DIR) + +find_path(CMOCKA_INCLUDE_DIR + NAMES + cmocka.h + PATHS + ${CMOCKA_ROOT_DIR}/include +) + +find_library(CMOCKA_LIBRARY + NAMES + cmocka + PATHS + ${CMOCKA_ROOT_DIR}/lib +) + +if (CMOCKA_LIBRARY) + set(CMOCKA_LIBRARIES + ${CMOCKA_LIBRARIES} + ${CMOCKA_LIBRARY} + ) +endif (CMOCKA_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(CMocka DEFAULT_MSG CMOCKA_LIBRARIES CMOCKA_INCLUDE_DIR) + +# show the CMOCKA_INCLUDE_DIR and CMOCKA_LIBRARIES variables only in the advanced view +mark_as_advanced(CMOCKA_INCLUDE_DIR CMOCKA_LIBRARIES) diff --git a/CMake/Test.cmake b/CMake/Test.cmake new file mode 100644 index 000000000..18f5f919c --- /dev/null +++ b/CMake/Test.cmake @@ -0,0 +1,53 @@ +function(configure_test) + set(options) + set(oneValueArgs NAME SUBPROJECT) + set(multiValueArgs WRAPPERS SOURCES INCLUDES LIBRARIES) + cmake_parse_arguments(CONFIGURE_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + include_directories( + ${CMOCKA_INCLUDE_DIR} + ${CONFIGURE_TEST_INCLUDES} + ) + add_definitions(${CMOCKA_DEFINITIONS}) + + set( + CMAKE_RUNTIME_OUTPUT_DIRECTORY + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test/${CONFIGURE_TEST_SUBPROJECT}/${CONFIGURE_TEST_NAME} + ) + + add_executable(${CONFIGURE_TEST_NAME}_test + ${CMAKE_SOURCE_DIR}/test/util.c + ${CONFIGURE_TEST_SOURCES} + ) + + list(APPEND CONFIGURE_TEST_WRAPPERS "malloc" "calloc" "realloc" "free") + + if (enable-coverage) + add_definitions(-g -O0 --coverage -fprofile-arcs -ftest-coverage) + endif() + + list(LENGTH CONFIGURE_TEST_WRAPPERS WRAPPED_COUNT) + + if(NOT ${WRAPPED_COUNT} STREQUAL "0") + set(WRAPPED "") + + foreach(WRAPPER ${CONFIGURE_TEST_WRAPPERS}) + string(REGEX REPLACE "\\n" "" WRAPPER "${WRAPPER}") + set(WRAPPED + "${WRAPPED} \ + -Wl,--wrap=${WRAPPER}" + ) + endforeach() + + set_target_properties(${CONFIGURE_TEST_NAME}_test + PROPERTIES + LINK_FLAGS "${WRAPPED}" + ) + endif() + + target_link_libraries(${CONFIGURE_TEST_NAME}_test ${CMOCKA_LIBRARIES} ${CONFIGURE_TEST_LIBRARIES}) + + if (enable-coverage) + target_link_libraries(${CONFIGURE_TEST_NAME}_test gcov) + endif() +endfunction() diff --git a/CMakeLists.txt b/CMakeLists.txt index 813bec4d9..4219bc9eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,18 +48,20 @@ option(enable-gdk-pixbuf "Use Pixbuf to support more image formats" YES) option(enable-binding-event "Enables binding event subscription" YES) option(zsh-completions "Zsh shell completions" NO) option(default-wallpaper "Installs the default wallpaper" YES) +option(enable-tests "Enables test suite" YES) +option(enable-coverage "Enables test coverage" NO) find_package(JsonC REQUIRED) find_package(PCRE REQUIRED) find_package(WLC REQUIRED) find_package(Wayland REQUIRED) find_package(XKBCommon REQUIRED) +find_package(LibInput REQUIRED) find_package(Cairo REQUIRED) find_package(Pango REQUIRED) find_package(GdkPixbuf) find_package(PAM) - -find_package(LibInput REQUIRED) +find_package(CMocka) find_package(Backtrace) if(Backtrace_FOUND) @@ -71,6 +73,7 @@ endif() include(FeatureSummary) include(Manpage) +include(Test) include(GNUInstallDirs) if (enable-gdk-pixbuf) @@ -130,6 +133,14 @@ install( COMPONENT data ) +if(enable-tests) + if (CMOCKA_FOUND) + add_subdirectory(test) + else() + message(WARNING "Not building tests - cmocka is required.") + endif() +endif() + if(default-wallpaper) install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/assets/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc29dad0d..67694fd3d 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 e9143f0fc..c8c531dd1 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/common/readline.c b/common/readline.c index 5106172c3..5815c3f14 100644 --- a/common/readline.c +++ b/common/readline.c @@ -10,7 +10,7 @@ char *read_line(FILE *file) { return NULL; } while (1) { - int c = getc(file); + int c = fgetc(file); if (c == '\n' && lastChar == '\\'){ --length; // Ignore last character. lastChar = '\0'; @@ -51,7 +51,7 @@ char *read_line_buffer(FILE *file, char *string, size_t string_len) { return NULL; } while (1) { - int c = getc(file); + int c = fgetc(file); if (c == EOF || c == '\n' || c == '\0') { break; } diff --git a/include/tests.h b/include/tests.h new file mode 100644 index 000000000..de358106a --- /dev/null +++ b/include/tests.h @@ -0,0 +1,23 @@ +#ifndef __TESTS_H +#define __TESTS_H + +#include +#include +#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); +int malloc_calls(); +int free_calls(); +int calloc_calls(); +int realloc_calls(); +int alloc_calls(); + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 000000000..b6d757a5f --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,22 @@ +set(test_targets "") + +add_subdirectory(common) + +add_custom_target(check + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + COMMAND ${CMAKE_SOURCE_DIR}/test/runner "${CMAKE_BINARY_DIR}") + +if(enable-coverage) + find_program(GCOV_PATH gcov) + find_program(LCOV_PATH lcov) + find_program(GENHTML_PATH genhtml) + if(NOT GCOV_PATH) + MESSAGE(FATAL_ERROR "gcov not found! Aborting...") + endif() + if(NOT LCOV_PATH) + MESSAGE(FATAL_ERROR "lcov not found! Aborting...") + endif() + if(NOT GENHTML_PATH) + MESSAGE(FATAL_ERROR "genhtml not found! Aborting...") + endif() +endif() diff --git a/test/common/CMakeLists.txt b/test/common/CMakeLists.txt new file mode 100644 index 000000000..8226b207b --- /dev/null +++ b/test/common/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(list) +add_subdirectory(readline) diff --git a/test/common/list/CMakeLists.txt b/test/common/list/CMakeLists.txt new file mode 100644 index 000000000..a9b8043b3 --- /dev/null +++ b/test/common/list/CMakeLists.txt @@ -0,0 +1,7 @@ +configure_test( + SUBPROJECT common + NAME list + SOURCES + ${PROJECT_SOURCE_DIR}/common/list.c + list.c +) diff --git a/test/common/list/list.c b/test/common/list/list.c new file mode 100644 index 000000000..105e2e818 --- /dev/null +++ b/test/common/list/list.c @@ -0,0 +1,187 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include "tests.h" +#include "list.h" + +static void assert_list_contents(list_t *list, int contents[], size_t len) { + assert_int_equal(list->length, (int)len); + for (size_t i = 0; i < (size_t)list->length; ++i) { + assert_int_equal(contents[i], *(int *)list->items[i]); + } +} + +static list_t *create_test_list(int contents[], size_t len) { + list_t *l = create_list(); + for (size_t i = 0; i < len; ++i) { + list_add(l, &contents[i]); + } + return l; +} + +static void test_create_and_free(void **state) { + list_t *list = create_list(); + assert_int_equal(list->length, 0); + assert_int_equal(list->capacity, 10); + assert_non_null(list->items); + list_free(list); + assert_int_equal(malloc_calls(), 2); + assert_int_equal(free_calls(), 2); +} + +static void test_add(void **state) { + list_t *list = create_list(); + + int items[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + for (size_t i = 0; i < sizeof(items) / sizeof(int); ++i) { + list_add(list, &items[i]); + assert_int_equal(items[i], *(int *)list->items[i]); + assert_int_equal(list->length, i + 1); + } + assert_list_contents(list, items, 15); + assert_int_equal(list->length, 15); + assert_int_equal(list->capacity, 20); + assert_int_equal(realloc_calls(), 1); + + list_free(list); +} + +static void test_insert(void **state) { + list_t *list = create_list(); + int i = 1, j = 2; + list_add(list, &i); + list_add(list, &i); + list_add(list, &i); + list_insert(list, 0, &j); + assert_int_equal(j, *(int *)list->items[0]); + assert_int_equal(list->length, 4); + list_free(list); +} + +static void test_del(void **state) { + list_t *list = create_list(); + + int items[] = { 1, 2, 3, 4, 5 }; + int new_items[] = { 1, 2, 4, 5 }; + for (size_t i = 0; i < sizeof(items) / sizeof(int); ++i) { + list_add(list, &items[i]); + } + + list_del(list, 2); + assert_list_contents(list, new_items, 4); + list_free(list); +} + +static void test_cat(void **state) { + int items_a[] = { 1, 2, 3, 4 }; + int items_b[] = { 5, 6, 7, 8 }; + int items_final[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + list_t *list_a = create_test_list(items_a, 4); + list_t *list_b = create_test_list(items_b, 4); + + list_cat(list_a, list_b); + + assert_list_contents(list_a, items_final, 8); + list_free(list_a); + list_free(list_b); +} + +static int qsort_compare(const void *left, const void *right) { + return **(int * const *)left - **(int * const *)right; +} + +static void test_qsort(void **state) { + int items_start[] = { 1, 4, 3, 2 }; + int items_final[] = { 1, 2, 3, 4 }; + list_t *list = create_test_list(items_start, 4); + list_qsort(list, qsort_compare); + assert_list_contents(list, items_final, 4); + list_free(list); +} + +static int find_compare(const void *a, const void *b) { + return *(int *)a - *(int *)b; +} + +static void test_seq_find(void **state) { + int items[] = { 1, 2, 3, 4 }; + int expected = 3; + list_t *list = create_test_list(items, 4); + int index = list_seq_find(list, find_compare, &expected); + assert_int_equal(index, 2); + list_free(list); +} + +int foreach_count = 0; + +static void foreach(void *item) { + foreach_count++; + assert_int_equal(*(int *)item, foreach_count); +} + +static void test_foreach(void **state) { + int items[] = { 1, 2, 3, 4 }; + list_t *list = create_test_list(items, 4); + list_foreach(list, foreach); + assert_int_equal(foreach_count, 4); + list_free(list); +} + +struct stable_data { + int id, value; +}; + +static int stable_compare(const void *_a, const void *_b) { + struct stable_data * const *a = _a; + struct stable_data * const *b = _b; + return (*a)->value - (*b)->value; +} + +static void test_stable_sort(void **state) { + struct stable_data initial[] = { + { .id = 0, .value = 0 }, + { .id = 3, .value = 2 }, + { .id = 4, .value = 3 }, + { .id = 1, .value = 1 }, + { .id = 2, .value = 1 }, + { .id = 5, .value = 4 }, + { .id = 7, .value = 5 }, + { .id = 6, .value = 5 }, + }; + struct stable_data expected[] = { + { .id = 0, .value = 0 }, + { .id = 1, .value = 1 }, + { .id = 2, .value = 1 }, + { .id = 3, .value = 2 }, + { .id = 4, .value = 3 }, + { .id = 5, .value = 4 }, + { .id = 7, .value = 5 }, + { .id = 6, .value = 5 }, + }; + list_t *list = create_list(); + for (size_t i = 0; i < sizeof(initial) / sizeof(initial[0]); ++i) { + list_add(list, &initial[i]); + } + list_stable_sort(list, stable_compare); + for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); ++i) { + struct stable_data *item = list->items[i]; + assert_int_equal(item->value, expected[i].value); + assert_int_equal(item->id, expected[i].id); + } + list_free(list); +} + +int main(int argc, char **argv) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_create_and_free), + cmocka_unit_test(test_add), + cmocka_unit_test(test_insert), + cmocka_unit_test(test_del), + cmocka_unit_test(test_cat), + cmocka_unit_test(test_qsort), + cmocka_unit_test(test_seq_find), + cmocka_unit_test(test_foreach), + cmocka_unit_test(test_stable_sort), + }; + return cmocka_run_group_tests(tests, reset_mem_wrappers, NULL); +} diff --git a/test/common/readline/CMakeLists.txt b/test/common/readline/CMakeLists.txt new file mode 100644 index 000000000..0327c4265 --- /dev/null +++ b/test/common/readline/CMakeLists.txt @@ -0,0 +1,9 @@ +configure_test( + SUBPROJECT common + NAME readline + SOURCES + ${PROJECT_SOURCE_DIR}/common/readline.c + readline.c + WRAPPERS + fgetc +) diff --git a/test/common/readline/readline.c b/test/common/readline/readline.c new file mode 100644 index 000000000..ba182fbd9 --- /dev/null +++ b/test/common/readline/readline.c @@ -0,0 +1,61 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include "tests.h" +#include "readline.h" + +int __wrap_fgetc(FILE *stream) { + return mock_type(int); +} + +static void prep_string(const char *str) { + while (*str) { + will_return(__wrap_fgetc, *str++); + } +} + +static void test_eof_line_ending(void **state) { + prep_string("hello"); + will_return(__wrap_fgetc, EOF); + char *line = read_line(NULL); + assert_string_equal(line, "hello"); + free(line); +} + +static void test_newline(void **state) { + prep_string("hello\n"); + char *line = read_line(NULL); + assert_string_equal(line, "hello"); + free(line); +} + +static void test_continuation(void **state) { + prep_string("hello \\\nworld"); + will_return(__wrap_fgetc, EOF); + char *line = read_line(NULL); + assert_string_equal(line, "hello world"); + free(line); +} + +static void test_expand_buffer(void **state) { + const char *test = "This is a very very long string. " + "This string is so long that it may in fact be greater " + "than 128 bytes (or octets) in length, which is suitable " + "for triggering a realloc"; + prep_string(test); + will_return(__wrap_fgetc, EOF); + char *line = read_line(NULL); + assert_string_equal(line, test); + free(line); + assert_int_equal(realloc_calls(), 1); +} + +int main(int argc, char **argv) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_eof_line_ending), + cmocka_unit_test(test_newline), + cmocka_unit_test(test_continuation), + cmocka_unit_test(test_expand_buffer), + }; + return cmocka_run_group_tests(tests, reset_mem_wrappers, NULL); +} diff --git a/test/runner b/test/runner new file mode 100755 index 000000000..7baf032d4 --- /dev/null +++ b/test/runner @@ -0,0 +1,27 @@ +#!/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 + +if (( $ret == 0 )) +then + if grep 'enable-coverage:BOOL=YES' "$1/CMakeCache.txt" > /dev/null + then + echo "Generating coverage reports" + rm -rf "$1/coverage" + mkdir "$1/coverage" + lcov --directory "$1" \ + --capture \ + --output-file "$1/coverage/lcov.info" > /dev/null + lcov --remove "$1/coverage/lcov.info" 'test/*' '/usr/*' \ + --output-file "$1/coverage/lcov.info.clean" > /dev/null + genhtml -o "$1/coverage/" "$1/coverage/lcov.info.clean" + fi +fi + +exit $ret diff --git a/test/util.c b/test/util.c new file mode 100644 index 000000000..837ac3d5d --- /dev/null +++ b/test/util.c @@ -0,0 +1,106 @@ +#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_CMOCKA; +int malloc_callcount = 0, + free_callcount = 0, + calloc_callcount = 0, + realloc_callcount = 0; + +int reset_mem_wrappers(void **state) { + _memory_behavior = WRAPPER_INVOKE_CMOCKA; + malloc_callcount = + free_callcount = + calloc_callcount = + realloc_callcount = 0; + return 0; +} + +void memory_behavior(enum wrapper_behavior behavior) { + _memory_behavior = behavior; +} + +int malloc_calls() { + return malloc_callcount; +} + +int free_calls() { + return free_callcount; +} + +int calloc_calls() { + return calloc_callcount; +} + +int realloc_calls() { + return realloc_callcount; +} + +int alloc_calls() { + return malloc_callcount + calloc_callcount; +} + +void *__wrap_malloc(size_t size) { + ++malloc_callcount; + 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) { + ++free_callcount; + 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) { + ++calloc_callcount; + 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) { + ++realloc_callcount; + 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); + } +}