./manage.sh test # Unit tests with AddressSanitizer
./manage.sh valgrind # Unit tests with Valgrind
./manage.sh coverage # Coverage report + functional tests
./manage.sh integration # End-to-end against Dockerized Dovecot (requires Docker)There is no mechanism to run a single test in isolation — all unit tests run
together via build/tests/unit/test-runner.
Each source module has a corresponding test file:
| Test file | Module under test |
|---|---|
test_fs_util.c |
libemail/src/core/fs_util.c |
test_logger.c |
libemail/src/core/logger.c |
test_config.c |
libemail/src/infrastructure/config_store.c |
test_wizard.c |
libemail/src/infrastructure/setup_wizard.c |
test_local_store.c |
libemail/src/infrastructure/local_store.c |
test_imap_client.c |
libemail/src/infrastructure/imap_client.c |
test_mime.c |
libemail/src/core/mime_util.c |
test_imap_util.c |
libemail/src/core/imap_util.c |
test_email_service.c |
libemail/src/domain/email_service.c |
test_html_parser.c |
libemail/src/core/html_parser.c |
test_html_render.c |
libemail/src/core/html_render.c |
test_input_line.c |
libemail/src/core/input_line.c |
test_path_complete.c |
libemail/src/core/path_complete.c |
test_platform.c |
libemail/src/platform/ |
- Add a
void test_foo(void)function in the appropriatetest_*.cfile. - Use
ASSERT(condition, "message")for each assertion. - Register it with
RUN_TEST(test_foo)intest_runner.c.
void test_foo(void) {
int result = foo(42);
ASSERT(result == 0, "foo(42) should return 0");
}Always initialize stack-allocated structs with = {0} to avoid Valgrind
uninitialized-value warnings:
Config cfg = {0}; // correct
Config cfg; // wrong — cfg.ssl_no_verify is garbagesetup_wizard_run() reads from stdin. To test it, redirect stdin via a pipe:
int pipefd[2];
pipe(pipefd);
ssize_t n = write(pipefd[1], input, strlen(input));
ASSERT(n > 0, "write to pipe should succeed");
close(pipefd[1]);
int saved = dup(STDIN_FILENO);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
Config *cfg = setup_wizard_run();
dup2(saved, STDIN_FILENO);
close(saved);The mock IMAP server (mock_imap_server.c) listens on TCP port 9993 and
implements a minimal IMAP state machine:
- Accepts multiple sequential connections (SEARCH uses one connection, each FETCH uses a separate connection).
- Always authenticates any credentials.
- Returns
* SEARCH 1(one message exists). - Returns a hardcoded test email body on FETCH.
Run via ./tests/functional/run_functional.sh. The script:
- Compiles and starts the mock server in background.
- Runs
email-cliwith a temporary config pointing atimap://localhost:9993. - Checks 5 assertions on the output.
- Kills the server on exit (via trap).
Requires Docker. Runs a real Dovecot IMAP server with:
- Self-signed TLS certificate (openssl,
ssl_min_protocol = TLSv1.2) - Two seed emails pre-loaded into the mailbox
./manage.sh integration # start Dovecot if needed, run test, assert output
./manage.sh imap-down # stop container (volume preserved)
./manage.sh imap-clean # stop + remove volumelibptytest is a reusable C library for automated testing of terminal programs.
It opens a pseudo-terminal, forks/execs the program under test, sends keystrokes,
and inspects a virtual screen buffer.
cd libs/libptytest
cc -std=c11 -Wall -Wextra -Werror -o test_ptytest \
test_ptytest.c pty_session.c pty_screen.c pty_sync.c -lutil -I.
./test_ptytestPtySession *s = pty_open(80, 24);
const char *argv[] = { "bin/email-cli", NULL };
pty_run(s, argv);
pty_wait_for(s, "message(s)", 2000); /* wait for text, max 2s */
pty_row_contains(s, 23, "↑↓=step"); /* check last row */
pty_cell_attr(s, 23, 2) & PTY_ATTR_REVERSE /* check reverse video */
pty_send_key(s, PTY_KEY_ESC);
pty_close(s);The parser handles cursor positioning (CSI H), screen/line erase
(CSI 2J, CSI K), SGR attributes (bold, dim, reverse), and 24-bit
colour sequences (parsed but not stored). This covers the escape
sequences used by email-cli's TUI.
ASSERT_ROW_CONTAINS(s, row, "text");
ASSERT_CELL_ATTR(s, row, col, PTY_ATTR_REVERSE);
ASSERT_SCREEN_CONTAINS(s, "text");
ASSERT_WAIT_FOR(s, "text", timeout_ms);| Scope | Target |
|---|---|
libemail/src/core/ + libemail/src/infrastructure/ combined |
>90% line coverage |
libemail/src/domain/ |
best-effort |
src/main.c |
best-effort (wiring code) |
Known uncoverable lines in setup_wizard.c: TTY manipulation branches
(tcgetattr/tcsetattr inside hide && is_tty) require a PTY to test.
These 6 lines are excluded from the >90% target by convention.