Skip to content

Commit 01de619

Browse files
committed
tests: add CMake-integrated poller unit coverage
1 parent 63c49b4 commit 01de619

6 files changed

Lines changed: 209 additions & 63 deletions

File tree

‎.github/workflows/cmake.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Configure CMake
3434
env:
3535
CC: ${{ matrix.compiler }}
36-
run: cmake -D CMAKE_BUILD_TYPE=Debug -D PYTHON3_EXE=${{github.workspace}}/venv/bin/python3 -B ${{github.workspace}}/
36+
run: cmake -D CMAKE_BUILD_TYPE=Debug -D BUILD_TESTING=ON -D PYTHON3_EXE=${{github.workspace}}/venv/bin/python3 -B ${{github.workspace}}/
3737

3838
- name: Build
3939
env:

‎CMakeLists.txt‎

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,34 @@ install(FILES ${CMAKE_BINARY_DIR}/${TARGET_NAME}.service
192192

193193
# TESTING
194194

195-
enable_testing()
196-
add_test(NAME dns_poller_watchers
197-
COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_C_COMPILER}"
198-
sh ${CMAKE_SOURCE_DIR}/tests/unit/run_dns_poller_test.sh)
199-
200-
find_program(
201-
PYTHON3_EXE
202-
NAMES "python3"
203-
DOC "Path to python3 executable"
204-
)
205-
if(NOT PYTHON3_EXE)
206-
message(STATUS "python3 not found, robot testing not possible")
207-
else()
208-
message(STATUS "python3 found: ${PYTHON3_EXE}")
195+
option(BUILD_TESTING "Build tests" OFF)
196+
include(CTest)
197+
if(BUILD_TESTING)
198+
add_executable(dns_poller_watchers_test
199+
tests/unit/test_dns_poller.c
200+
src/logging.c
201+
src/ring_buffer.c)
202+
set_property(SOURCE tests/unit/test_dns_poller.c APPEND
203+
PROPERTY COMPILE_DEFINITIONS __FILENAME__="dns_poller_test")
204+
target_link_libraries(dns_poller_watchers_test cares ev)
205+
set_property(TARGET dns_poller_watchers_test PROPERTY C_STANDARD 11)
206+
add_test(NAME dns_poller_watchers COMMAND dns_poller_watchers_test)
207+
set_tests_properties(dns_poller_watchers PROPERTIES LABELS unit)
209208

210-
# Robot framework tests
211-
add_test(NAME robot COMMAND ${PYTHON3_EXE} -m robot.run functional_tests.robot
212-
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/robot)
209+
find_program(
210+
PYTHON3_EXE
211+
NAMES "python3"
212+
DOC "Path to python3 executable"
213+
)
214+
if(NOT PYTHON3_EXE)
215+
message(STATUS "python3 not found, robot testing not possible")
216+
else()
217+
message(STATUS "python3 found: ${PYTHON3_EXE}")
218+
219+
# Robot framework tests
220+
add_test(NAME robot COMMAND ${PYTHON3_EXE} -m robot.run functional_tests.robot
221+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/robot)
222+
endif()
213223
endif()
214224

215225
# Clean target (removes entire build directory)

‎tests/docker/Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ WORKDIR /src
2424

2525
# Default command: build and run tests
2626
# Symlink needed because Robot test expects binary at project root
27-
CMD ["bash", "-c", "cmake -S . -B build && cmake --build build && ln -sf build/https_dns_proxy https_dns_proxy && ctest --test-dir build --output-on-failure"]
27+
CMD ["bash", "-c", "cmake -S . -B build -DBUILD_TESTING=ON && cmake --build build && ln -sf build/https_dns_proxy https_dns_proxy && ctest --test-dir build --output-on-failure"]

‎tests/unit/run_dns_poller_test.sh‎

Lines changed: 0 additions & 10 deletions
This file was deleted.

‎tests/unit/test_dns_poller.c‎

Lines changed: 128 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,135 @@
1-
#include <assert.h>
21
#include <sys/socket.h>
32
#include <unistd.h>
3+
4+
#include "test_harness.h"
45
#include "../../src/dns_poller.c"
5-
int main(void) {
6-
dns_poller_t d = {0};
7-
struct ev_loop *loop = ev_loop_new(0);
8-
assert(loop);
9-
dns_poller_init(&d, loop, "127.0.0.1,127.0.0.2", 60, NULL,
6+
7+
enum {
8+
TEST_SOCKET_COUNT = 40,
9+
TEST_FD_CAPACITY = 48
10+
};
11+
12+
static dns_poller_t poller;
13+
static struct ev_loop *loop;
14+
static int poller_initialized;
15+
static int tracked_fds[TEST_FD_CAPACITY];
16+
static unsigned tracked_fd_count;
17+
18+
static void setUp(void) {
19+
poller_initialized = 0;
20+
tracked_fd_count = 0;
21+
loop = NULL;
22+
memset(&poller, 0, sizeof(poller));
23+
loop = ev_loop_new(0);
24+
if (!loop) {
25+
test_fail(__FILE__, __LINE__, "ev_loop_new(0)");
26+
return;
27+
}
28+
dns_poller_init(&poller, loop, "127.0.0.1,127.0.0.2", 60, NULL,
1029
"example.com", AF_INET, NULL, NULL);
11-
ev_timer_stop(loop, &d.timer);
12-
int fds[40];
13-
ev_io *first = NULL;
14-
for (int i = 0; i < 40; i++) {
15-
fds[i] = socket(AF_INET, SOCK_DGRAM, 0);
16-
assert(fds[i] >= 0);
17-
sock_state_cb(&d, fds[i], 1, 0);
18-
assert(get_io_event(&d, fds[i]));
19-
if (!i) first = get_io_event(&d, fds[i]);
20-
assert(get_io_event(&d, fds[0]) == first);
30+
poller_initialized = 1;
31+
ev_timer_stop(loop, &poller.timer);
32+
}
33+
34+
static void tearDown(void) {
35+
if (poller_initialized) {
36+
dns_poller_cleanup(&poller);
37+
poller_initialized = 0;
38+
}
39+
for (unsigned i = 0; i < tracked_fd_count; i++) {
40+
close(tracked_fds[i]);
2141
}
22-
ev_io *reused = get_io_event(&d, fds[10]);
23-
sock_state_cb(&d, fds[10], 0, 0);
24-
assert(!get_io_event(&d, fds[10]));
25-
sock_state_cb(&d, fds[10], 1, 1);
26-
assert(get_io_event(&d, fds[10]) == reused);
27-
for (int i = 0; i < 40; i++) {
28-
sock_state_cb(&d, fds[i], 0, 0);
29-
close(fds[i]);
42+
if (loop) {
43+
ev_loop_destroy(loop);
44+
loop = NULL;
3045
}
31-
// Also exercise cleanup with a registered watcher still active.
32-
int remaining = socket(AF_INET, SOCK_DGRAM, 0);
33-
assert(remaining >= 0);
34-
sock_state_cb(&d, remaining, 1, 0);
35-
dns_poller_cleanup(&d);
36-
close(remaining);
37-
ev_run(loop, EVRUN_NOWAIT);
38-
ev_loop_destroy(loop);
39-
puts("PASS: 40 sockets / 2 servers; stable addresses; update/reuse/cleanup");
40-
return 0;
46+
}
47+
48+
static int open_test_socket(void) {
49+
int fd = socket(AF_INET, SOCK_DGRAM, 0);
50+
if (fd < 0) {
51+
return fd;
52+
}
53+
if (tracked_fd_count >= TEST_FD_CAPACITY) {
54+
close(fd);
55+
return -1;
56+
}
57+
tracked_fds[tracked_fd_count++] = fd;
58+
return fd;
59+
}
60+
61+
static int watcher_matches(ev_io *watcher, int fd, int events) {
62+
return watcher && ev_is_active(watcher) && !ev_is_pending(watcher) &&
63+
watcher->fd == fd && watcher->events == events &&
64+
watcher->data == &poller;
65+
}
66+
67+
static void test_allocates_watchers_beyond_nameserver_count(void) {
68+
ev_io *first = NULL;
69+
int first_fd = ARES_SOCKET_BAD;
70+
71+
for (int i = 0; i < TEST_SOCKET_COUNT; i++) {
72+
int fd = open_test_socket();
73+
TEST_ASSERT(fd >= 0);
74+
sock_state_cb(&poller, fd, 1, 0);
75+
TEST_ASSERT(watcher_matches(get_io_event(&poller, fd), fd, EV_READ));
76+
if (i == 0) {
77+
first = get_io_event(&poller, fd);
78+
first_fd = fd;
79+
}
80+
TEST_ASSERT_EQUAL_PTR(first, get_io_event(&poller, first_fd));
81+
}
82+
}
83+
84+
static void test_reuses_released_watcher(void) {
85+
int fd = open_test_socket();
86+
TEST_ASSERT(fd >= 0);
87+
sock_state_cb(&poller, fd, 1, 0);
88+
ev_io *watcher = get_io_event(&poller, fd);
89+
TEST_ASSERT(watcher_matches(watcher, fd, EV_READ));
90+
91+
sock_state_cb(&poller, fd, 0, 0);
92+
TEST_ASSERT(!get_io_event(&poller, fd));
93+
TEST_ASSERT(!ev_is_active(watcher));
94+
TEST_ASSERT(!ev_is_pending(watcher));
95+
TEST_ASSERT(watcher->fd == ARES_SOCKET_BAD);
96+
sock_state_cb(&poller, fd, 1, 1);
97+
TEST_ASSERT_EQUAL_PTR(watcher, get_io_event(&poller, fd));
98+
TEST_ASSERT(watcher_matches(watcher, fd, EV_READ | EV_WRITE));
99+
}
100+
101+
static void test_fd_zero_does_not_alias_free_watcher(void) {
102+
sock_state_cb(&poller, 0, 1, 0);
103+
ev_io *fd_zero_watcher = get_io_event(&poller, 0);
104+
TEST_ASSERT(watcher_matches(fd_zero_watcher, 0, EV_READ));
105+
106+
int fd = open_test_socket();
107+
TEST_ASSERT(fd > 0);
108+
sock_state_cb(&poller, fd, 1, 0);
109+
TEST_ASSERT(watcher_matches(get_io_event(&poller, fd), fd, EV_READ));
110+
TEST_ASSERT_EQUAL_PTR(fd_zero_watcher, get_io_event(&poller, 0));
111+
TEST_ASSERT(get_io_event(&poller, fd) != fd_zero_watcher);
112+
}
113+
114+
static void test_cleanup_stops_active_watchers(void) {
115+
int fd = open_test_socket();
116+
TEST_ASSERT(fd >= 0);
117+
sock_state_cb(&poller, fd, 1, 0);
118+
ev_io *watcher = get_io_event(&poller, fd);
119+
TEST_ASSERT(watcher_matches(watcher, fd, EV_READ));
120+
ev_feed_event(loop, watcher, EV_READ);
121+
TEST_ASSERT(ev_is_pending(watcher));
122+
123+
dns_poller_cleanup(&poller);
124+
poller_initialized = 0;
125+
TEST_ASSERT(!poller.io_events);
126+
TEST_ASSERT(ev_run(loop, EVRUN_NOWAIT) == 0);
127+
}
128+
129+
int main(void) {
130+
TEST_RUN(test_allocates_watchers_beyond_nameserver_count);
131+
TEST_RUN(test_reuses_released_watcher);
132+
TEST_RUN(test_fd_zero_does_not_alias_free_watcher);
133+
TEST_RUN(test_cleanup_stops_active_watchers);
134+
return test_summary();
41135
}

‎tests/unit/test_harness.h‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#ifndef TEST_HARNESS_H
2+
#define TEST_HARNESS_H
3+
4+
#include <stdio.h>
5+
6+
static unsigned test_count;
7+
static unsigned test_failures;
8+
static int test_failed;
9+
10+
static void test_fail(const char *file, int line, const char *expression) {
11+
(void)fprintf(stderr, "%s:%d: assertion failed: %s\n", file, line, expression);
12+
test_failed = 1;
13+
}
14+
15+
#define TEST_ASSERT(condition) do { \
16+
if (!(condition)) { \
17+
test_fail(__FILE__, __LINE__, #condition); \
18+
return; \
19+
} \
20+
} while (0)
21+
22+
#define TEST_ASSERT_EQUAL_PTR(expected, actual) do { \
23+
const void *expected_ptr = (expected); \
24+
const void *actual_ptr = (actual); \
25+
if (expected_ptr != actual_ptr) { \
26+
test_fail(__FILE__, __LINE__, #expected " == " #actual); \
27+
return; \
28+
} \
29+
} while (0)
30+
31+
#define TEST_RUN(test) do { \
32+
test_failed = 0; \
33+
setUp(); \
34+
if (!test_failed) { \
35+
test(); \
36+
} \
37+
tearDown(); \
38+
test_count++; \
39+
if (test_failed) { \
40+
test_failures++; \
41+
(void)printf("FAIL: %s\n", #test); \
42+
} else { \
43+
(void)printf("PASS: %s\n", #test); \
44+
} \
45+
} while (0)
46+
47+
static int test_summary(void) {
48+
(void)printf("%u tests, %u failures\n", test_count, test_failures);
49+
return test_failures == 0 ? 0 : 1;
50+
}
51+
52+
#endif

0 commit comments

Comments
 (0)