This commit is contained in:
Drew DeVault 2016-06-19 17:53:56 +00:00 committed by GitHub
commit 8ca1f50323
17 changed files with 618 additions and 5 deletions

1
.gitignore vendored
View file

@ -7,7 +7,6 @@ install_manifest.txt
*.o *.o
*.a *.a
bin/ bin/
test/
build/ build/
.lvimrc .lvimrc
config-debug config-debug

View file

@ -18,9 +18,11 @@ arch:
- cairo - cairo
- gdk-pixbuf2 - gdk-pixbuf2
- wlc-git - wlc-git
- cmocka
script: script:
- "cmake ." - "cmake ."
- "make" - "make"
- "make check"
script: script:
- "curl -s https://raw.githubusercontent.com/mikkeloscar/arch-travis/master/arch-travis.sh | bash" - "curl -s https://raw.githubusercontent.com/mikkeloscar/arch-travis/master/arch-travis.sh | bash"

66
CMake/FindCMocka.cmake Normal file
View file

@ -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 <asn@cryptomilk.org>
#
# 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)

53
CMake/Test.cmake Normal file
View file

@ -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()

View file

@ -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(enable-binding-event "Enables binding event subscription" YES)
option(zsh-completions "Zsh shell completions" NO) option(zsh-completions "Zsh shell completions" NO)
option(default-wallpaper "Installs the default wallpaper" YES) 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(JsonC REQUIRED)
find_package(PCRE REQUIRED) find_package(PCRE REQUIRED)
find_package(WLC REQUIRED) find_package(WLC REQUIRED)
find_package(Wayland REQUIRED) find_package(Wayland REQUIRED)
find_package(XKBCommon REQUIRED) find_package(XKBCommon REQUIRED)
find_package(LibInput REQUIRED)
find_package(Cairo REQUIRED) find_package(Cairo REQUIRED)
find_package(Pango REQUIRED) find_package(Pango REQUIRED)
find_package(GdkPixbuf) find_package(GdkPixbuf)
find_package(PAM) find_package(PAM)
find_package(CMocka)
find_package(LibInput REQUIRED)
find_package(Backtrace) find_package(Backtrace)
if(Backtrace_FOUND) if(Backtrace_FOUND)
@ -71,6 +73,7 @@ endif()
include(FeatureSummary) include(FeatureSummary)
include(Manpage) include(Manpage)
include(Test)
include(GNUInstallDirs) include(GNUInstallDirs)
if (enable-gdk-pixbuf) if (enable-gdk-pixbuf)
@ -130,6 +133,14 @@ install(
COMPONENT data 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) if(default-wallpaper)
install( install(
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/assets/ DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/assets/

View file

@ -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 4. git push -u origin add-so-and-so-feature
5. Make pull request from your feature branch 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 ## Coding Style
Sway is written in C. The style guidelines is [kernel Sway is written in C. The style guidelines is [kernel

View file

@ -69,6 +69,11 @@ On systems without logind, you need to suid the sway binary:
sudo chmod a+s /usr/local/bin/sway 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 ## Configuration
If you already use i3, then copy your i3 config to `~/.config/sway/config` and If you already use i3, then copy your i3 config to `~/.config/sway/config` and

View file

@ -10,7 +10,7 @@ char *read_line(FILE *file) {
return NULL; return NULL;
} }
while (1) { while (1) {
int c = getc(file); int c = fgetc(file);
if (c == '\n' && lastChar == '\\'){ if (c == '\n' && lastChar == '\\'){
--length; // Ignore last character. --length; // Ignore last character.
lastChar = '\0'; lastChar = '\0';
@ -51,7 +51,7 @@ char *read_line_buffer(FILE *file, char *string, size_t string_len) {
return NULL; return NULL;
} }
while (1) { while (1) {
int c = getc(file); int c = fgetc(file);
if (c == EOF || c == '\n' || c == '\0') { if (c == EOF || c == '\n' || c == '\0') {
break; break;
} }

23
include/tests.h Normal file
View file

@ -0,0 +1,23 @@
#ifndef __TESTS_H
#define __TESTS_H
#include <stddef.h>
#include <setjmp.h>
#include <stdarg.h>
#include <cmocka.h>
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

22
test/CMakeLists.txt Normal file
View file

@ -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()

View file

@ -0,0 +1,2 @@
add_subdirectory(list)
add_subdirectory(readline)

View file

@ -0,0 +1,7 @@
configure_test(
SUBPROJECT common
NAME list
SOURCES
${PROJECT_SOURCE_DIR}/common/list.c
list.c
)

187
test/common/list/list.c Normal file
View file

@ -0,0 +1,187 @@
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#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);
}

View file

@ -0,0 +1,9 @@
configure_test(
SUBPROJECT common
NAME readline
SOURCES
${PROJECT_SOURCE_DIR}/common/readline.c
readline.c
WRAPPERS
fgetc
)

View file

@ -0,0 +1,61 @@
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdio.h>
#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);
}

27
test/runner Executable file
View file

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

106
test/util.c Normal file
View file

@ -0,0 +1,106 @@
#include <stdbool.h>
#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);
}
}