|
ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
|
🧪 Unit, integration, and performance tests More...
Files | |
| file | common.c |
| Common test utilities implementation. | |
| file | common.h |
| Common test utilities and environment detection. | |
| file | globals.c |
| 🔗 Global symbol stubs for test executables to satisfy linker dependencies | |
| file | logging.c |
| 🧪 Test utilities for logging: stdout/stderr redirection and capture helpers | |
| file | logging.h |
| Test logging control utilities. | |
| file | test_env.h |
| Test environment detection utilities. | |
Macros | |
| #define | TEST_LOGGING_SETUP_AND_TEARDOWN() |
| Macro to create setup and teardown functions for quiet testing. | |
| #define | TEST_LOGGING_SETUP_AND_TEARDOWN_WITH_LOG_LEVELS(setup_level, restore_level, disable_stdout, disable_stderr) |
| Macro to create setup and teardown functions for quiet testing with custom log level control. | |
| #define | TEST_LOGGING_SETUP_AND_TEARDOWN_WITH_LOG_LEVEL() TEST_LOGGING_SETUP_AND_TEARDOWN_WITH_LOG_LEVELS(LOG_FATAL, LOG_DEBUG, true, true) |
| Macro to create setup and teardown functions for quiet testing with log level control (default levels) | |
| #define | TEST_SUITE_WITH_QUIET_LOGGING_AND_LOG_LEVELS(suite_name, setup_level, restore_level, disable_stdout, disable_stderr, ...) |
| Macro to create a complete test suite with quiet logging and custom log levels. | |
| #define | TEST_SUITE_WITH_QUIET_LOGGING(suite_name, ...) |
| Macro to create a complete test suite with quiet logging (default log levels) | |
| #define | TEST_SUITE_WITH_QUIET_LOGGING_AND_LOG_LEVEL(suite_name, ...) |
| Macro to create a complete test suite with quiet logging and log level control (default levels) | |
| #define | TEST_LOGGING_TEMPORARILY_DISABLE() |
| Macro to temporarily disable logging for a specific test. | |
| #define | TEST_LOGGING_TEMPORARILY_DISABLE_STDOUT() |
| Macro to temporarily disable only stdout for a specific test. | |
| #define | TEST_LOGGING_TEMPORARILY_DISABLE_STDERR() |
| Macro to temporarily disable only stderr for a specific test. | |
| #define | TEST_SUITE_WITH_DEBUG_LOGGING(suite_name, ...) TEST_SUITE_WITH_QUIET_LOGGING_AND_LOG_LEVELS(suite_name, LOG_DEBUG, LOG_DEBUG, false, false, ##__VA_ARGS__) |
| Macro to create a complete test suite with debug logging and stdout/stderr enabled. | |
| #define | TEST_SUITE_WITH_VERBOSE_LOGGING(suite_name, ...) TEST_SUITE_WITH_DEBUG_LOGGING(suite_name, ##__VA_ARGS__) |
| Alias for TEST_SUITE_WITH_DEBUG_LOGGING for verbose output. | |
Functions | |
| const char * | test_get_binary_path (void) |
| Get the path to the ascii-chat binary for integration tests. | |
| int | test_logging_disable (bool disable_stdout, bool disable_stderr) |
| Disable stdout/stderr output for quiet test execution. | |
| int | test_logging_restore (void) |
| Restore stdout/stderr output after test logging disable. | |
| bool | test_logging_is_disabled (void) |
| Check if logging is currently disabled. | |
🧪 Unit, integration, and performance tests
This header provides common utilities and helpers for writing ascii-chat tests. It includes standard test headers, platform detection, and environment checks to help tests run reliably across different environments (CI, Docker, WSL).
This header automatically includes:
Tests that require hardware (like webcam) can use test_is_in_headless_environment() to skip when running in CI/Docker/WSL.
This header provides utilities for controlling logging output during tests. It enables tests to temporarily disable or redirect stdout/stderr for quiet test execution, and provides convenient macros for test suite setup and teardown.
This header provides test environment detection functions that can be used by both test code and production code (to adjust behavior like timeouts).
Unlike common.h, this header has NO Criterion dependency, so it can be safely included by any code that needs to detect if it's running in a test environment.
Welcome! This guide will help you get comfortable with the test suite in ascii-chat. We use Criterion, a modern C testing framework, to make sure everything works correctly. Don't worry if you're new to testing—we'll walk you through everything you need to know.
The test suite is designed to work smoothly across different platforms (Linux, macOS, Windows) using Docker when needed, so you can run tests confidently on your machine.
We chose Criterion because it's modern, feature-rich, and makes writing tests pleasant. If you're coming from other testing frameworks, you'll find it familiar. If you're new to testing in C, don't worry—it's straightforward once you see a few examples.
Test(suite, name) for regular testsParameterizedTest, ParameterizedTestParameters)Theory, TheoryDataPoints)TestSuite, .init, .fini)cr_assert, cr_assert_eq, cr_assert_not_null, etc.).timeout = N in seconds)Heads up! If you're on Windows, there's something important you should know.
Criterion has limited Windows support, and many features don't work correctly on native Windows builds. To make sure tests run reliably for everyone, we've set up Docker-based testing.
tests/scripts/run-docker-tests.ps1 for easy testingThe Docker setup gives you the same Linux environment that works perfectly with Criterion, so your tests will run just like they do on Linux or macOS. No need to worry about platform differences!
We organize tests into three categories, each serving a different purpose. Understanding when to use each type will help you write better tests.
Unit tests check individual pieces of code in isolation. Think of them like checking each ingredient before you cook—you want to make sure each component works correctly on its own.
All unit tests live in tests/unit/. Each module typically has its own test file.
Examples you can look at:
tests/unit/ascii_test.c: Testing ASCII conversion functionstests/unit/buffer_pool_test.c: Testing buffer pool allocationtests/unit/crypto_test.c: Testing cryptographic operationstests/unit/palette_test.c: Testing palette managementIntegration tests check how different pieces work together. Now you're testing the whole recipe, not just individual ingredients.
Integration tests live in tests/integration/.
Examples you can look at:
tests/integration/crypto_handshake_integration_test.c: Testing the full cryptographic handshaketests/integration/crypto_network_integration_test.c: Testing network encryption end-to-endtests/integration/ascii_simd_integration_test.c: Verifying SIMD optimizations work correctlyPerformance tests make sure critical code paths run fast enough. They're like a speedometer for your code—telling you if things are running as fast as they should.
Performance tests live in tests/performance/.
Examples you can look at:
tests/performance/ascii_performance_test.c: Making sure ASCII conversion is fasttests/performance/crypto_performance_test.c: Making sure encryption/decryption is fastWe use a couple of environment variables to make tests run faster and behave appropriately during testing. The test runners set these automatically, so you usually don't need to think about them—but it's good to know what they do.
TESTING=1**: A general flag that says "we're in test mode"CRITERION_TEST=1**: Specifically identifies Criterion test executionCode checks if it's running in a test environment like this:
This lets the code automatically optimize for testing:
Here's how we shorten network timeouts during tests (from lib/network/packet.c):
And here's how we limit memory usage in tests (from tests/unit/compression_test.c):
Network operations can take time, and we don't want tests waiting around forever. So when tests are running (when TESTING=1 or CRITERION_TEST=1 is set), network timeouts get automatically shortened.
This means integration tests that use the network complete in seconds instead of minutes, while still testing real network behavior. Pretty neat!
The timeout logic lives in these network functions:
lib/network/network.c: send_with_timeout(), recv_with_timeout()lib/network/packet.c: calculate_packet_timeout()These functions automatically detect the test environment and use shorter timeouts:
To make sure tests work the same everywhere, we've set up Docker-based testing. This is especially helpful on Windows where Criterion has limited support, but it's also great for ensuring consistent results across all platforms.
Our test Dockerfile (tests/Dockerfile) uses Arch Linux as the base. It's lightweight, stays up-to-date, and has everything Criterion needs.
The Dockerfile structure is pretty straightforward:
The Docker Compose setup (tests/docker-compose.yml) makes it easy to run tests without thinking about Docker details.
ascii-chat-tests service**: The main test containertests/DockerfileTESTING=1: We're in test modeCRITERION_TEST=1: Criterion test flagCCACHE_DIR=/ccache: Where to store compilation cacheASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer: For AddressSanitizer supportOn Windows, we have a convenient PowerShell script that makes Docker testing easy:
You can also use Docker Compose commands directly if you prefer:
Tests are run using CMake's ctest tool, which integrates with Criterion to provide parallel execution, filtering, and XML output for CI.
--parallel 0 to auto-detect CPU coresbuild/Testing/criterion-xml/CMake discovers tests from tests/unit/, tests/integration/, and tests/performance/:
test_{category}_{name} (e.g., test_unit_ascii)cmake --build build --target testscmake --build build --target test_unit_buffer_poolCriterion generates XML output in build/Testing/criterion-xml/:
Sometimes you want to test the same logic with different inputs. Instead of writing five nearly-identical test functions, you can use parameterized tests!
If you need to allocate memory in your parameterized test data, you must use Criterion's special memory functions. Regular malloc() won't work correctly because Criterion needs to track the memory to clean it up properly.
Use these Criterion functions:
cr_malloc(): Allocate memory (Criterion tracks it)cr_calloc(): Allocate zero-initialized memorycr_realloc(): Reallocate memorycr_free(): Free memory allocated with Criterion functionsBut honestly? The easiest approach is to just use static arrays when possible:
Want to see parameterized tests in action? Check these out:
tests/unit/terminal_detect_test.c: Testing different COLORTERM and TERM valuestests/unit/webcam_test.c: Testing different webcam indicestests/unit/simd_scalar_comparison_test.c: Testing different palettesTheorized tests are Criterion's way of doing property-based testing. Instead of testing specific values, you test that a property holds for a whole range of values. It's like mathematical proof by example—if the property holds for all these values, it probably holds in general.
Criterion gives you several ways to generate test values:
TheoryPointsFromRange(min, max, step): Generate a range of valuesTheoryPointsFromArray(array, count): Use your own array of valuesTheoryPointsFromBitfield(bits): Generate all bit combinationsTheorized tests are perfect for checking mathematical properties:
decompress(compress(x)) == x should always be trueWe use theorized tests in lots of places:
tests/unit/compression_test.c: Compression roundtrip propertytests/unit/crypto_test.c: Encryption roundtrip and nonce uniquenesstests/unit/palette_test.c: Palette length and UTF-8 boundary propertiestests/unit/ringbuffer_test.c: FIFO ordering propertytests/unit/mixer_test.c: Audio bounds propertytests/unit/ascii_test.c: Image size propertytests/unit/buffer_pool_test.c: Allocation roundtrip and pool reusetests/unit/aspect_ratio_test.c: Aspect ratio preservationTests can generate a lot of output, which makes it hard to see what actually matters. We've got some helpful macros in lib/tests/logging.h (included via lib/tests/common.h) that let you control logging during tests.
Sometimes you just want to quiet things down for part of a test:
If you need more control, you can set things up manually:
For maximum control, you can call the functions directly:
We use Codecov (https://codecov.io/) to track how much of our code is covered by tests. Coverage reports are automatically generated when code is pushed or pull requests are created, and uploaded to Codecov so you can see what's covered (and what isn't).
Coverage settings are in codecov.yml:
To build and run tests with coverage instrumentation:
Coverage is organized by test type and platform:
ascii-chat-tests-ubuntu-debug: Unit tests on Ubuntu (debug build)ascii-chat-tests-macos-debug: Unit tests on macOS (debug build)ascii-chat-integration-ubuntu: Integration tests on Ubuntuascii-chat-performance-ubuntu: Performance tests on UbuntuDifferent parts of the codebase have different coverage goals:
These are some guidelines we've learned from experience. Following them will help you write tests that are fast, reliable, and easy to maintain.
lib/tests/common.h: It has everything you needtests/unit/{module}_test.c is the pattern-v for verbose output: See what's actually happening--no-parallel for sequential execution: Easier to see what's going onOur test suite is integrated with GitHub Actions, so tests run automatically on every push and pull request. This means you'll know right away if something breaks!
The main test workflow is in .github/workflows/test.yml:
tests/scripts/tests/unit/, tests/integration/, tests/performance/lib/tests/tests/Dockerfile, tests/docker-compose.ymlThe test utilities in lib/tests/ provide common functionality for writing tests:
Files:
These utilities help tests run reliably by providing logging control, environment detection, and common headers. See the individual file documentation for details.
If you're stuck or have questions, feel free to ask! We're here to help. Check out the examples in the codebase—seeing real tests is often the best way to learn.
| #define TEST_LOGGING_SETUP_AND_TEARDOWN | ( | ) |
#include <logging.h>
Macro to create setup and teardown functions for quiet testing.
This macro creates two functions:
Usage:
Definition at line 119 of file tests/logging.h.
| #define TEST_LOGGING_SETUP_AND_TEARDOWN_WITH_LOG_LEVEL | ( | ) | TEST_LOGGING_SETUP_AND_TEARDOWN_WITH_LOG_LEVELS(LOG_FATAL, LOG_DEBUG, true, true) |
#include <logging.h>
Macro to create setup and teardown functions for quiet testing with log level control (default levels)
This macro creates two functions that also control the log level:
Usage:
Definition at line 163 of file tests/logging.h.
| #define TEST_LOGGING_SETUP_AND_TEARDOWN_WITH_LOG_LEVELS | ( | setup_level, | |
| restore_level, | |||
| disable_stdout, | |||
| disable_stderr | |||
| ) |
#include <logging.h>
Macro to create setup and teardown functions for quiet testing with custom log level control.
This macro creates two functions that control the log level:
Usage:
Definition at line 140 of file tests/logging.h.
| #define TEST_LOGGING_TEMPORARILY_DISABLE | ( | ) |
#include <logging.h>
Macro to temporarily disable logging for a specific test.
This macro can be used within a test to temporarily disable logging, with automatic restoration when the test ends.
Usage:
Definition at line 252 of file tests/logging.h.
| #define TEST_LOGGING_TEMPORARILY_DISABLE_STDERR | ( | ) |
#include <logging.h>
Macro to temporarily disable only stderr for a specific test.
This macro can be used within a test to temporarily disable only stderr, keeping stdout available for normal output.
Usage:
Definition at line 293 of file tests/logging.h.
| #define TEST_LOGGING_TEMPORARILY_DISABLE_STDOUT | ( | ) |
#include <logging.h>
Macro to temporarily disable only stdout for a specific test.
This macro can be used within a test to temporarily disable only stdout, keeping stderr available for error messages.
Usage:
Definition at line 273 of file tests/logging.h.
| #define TEST_SUITE_WITH_DEBUG_LOGGING | ( | suite_name, | |
| ... | |||
| ) | TEST_SUITE_WITH_QUIET_LOGGING_AND_LOG_LEVELS(suite_name, LOG_DEBUG, LOG_DEBUG, false, false, ##__VA_ARGS__) |
#include <logging.h>
Macro to create a complete test suite with debug logging and stdout/stderr enabled.
This macro creates unique setup/teardown functions with debug logging enabled and stdout/stderr available for debugging output.
Usage:
Definition at line 311 of file tests/logging.h.
| #define TEST_SUITE_WITH_QUIET_LOGGING | ( | suite_name, | |
| ... | |||
| ) |
#include <logging.h>
Macro to create a complete test suite with quiet logging (default log levels)
This macro creates unique setup/teardown functions and declares the test suite in one go. It supports additional TestSuite options and avoids redefinition errors when multiple test suites are in the same file.
Usage:
Definition at line 204 of file tests/logging.h.
| #define TEST_SUITE_WITH_QUIET_LOGGING_AND_LOG_LEVEL | ( | suite_name, | |
| ... | |||
| ) |
#include <logging.h>
Macro to create a complete test suite with quiet logging and log level control (default levels)
This macro creates unique setup/teardown functions with log level control and declares the test suite in one go. It supports additional TestSuite options and avoids redefinition errors.
Usage:
Definition at line 226 of file tests/logging.h.
| #define TEST_SUITE_WITH_QUIET_LOGGING_AND_LOG_LEVELS | ( | suite_name, | |
| setup_level, | |||
| restore_level, | |||
| disable_stdout, | |||
| disable_stderr, | |||
| ... | |||
| ) |
#include <logging.h>
Macro to create a complete test suite with quiet logging and custom log levels.
This macro creates unique setup/teardown functions with custom log levels and declares the test suite in one go. It supports additional TestSuite options and avoids redefinition errors.
Usage:
Definition at line 178 of file tests/logging.h.
| #define TEST_SUITE_WITH_VERBOSE_LOGGING | ( | suite_name, | |
| ... | |||
| ) | TEST_SUITE_WITH_DEBUG_LOGGING(suite_name, ##__VA_ARGS__) |
#include <logging.h>
Alias for TEST_SUITE_WITH_DEBUG_LOGGING for verbose output.
This macro is an alias for TEST_SUITE_WITH_DEBUG_LOGGING, providing verbose logging output for debugging test failures.
Usage:
Definition at line 325 of file tests/logging.h.
| const char * test_get_binary_path | ( | void | ) |
#include <common.h>
Get the path to the ascii-chat binary for integration tests.
This function finds the ascii-chat binary by trying multiple candidate paths. It handles both direct test invocation from the repo root and ctest invocation from the build directory.
Search order:
Definition at line 16 of file tests/common.c.
References initialized, and safe_snprintf().
#include <logging.c>
Disable stdout/stderr output for quiet test execution.
Redirect stdout and/or stderr to /dev/null for quiet testing.
| disable_stdout | If true, redirect stdout to /dev/null |
| disable_stderr | If true, redirect stderr to /dev/null |
| disable_stdout | If true, redirect stdout to /dev/null |
| disable_stderr | If true, redirect stderr to /dev/null |
Redirects stdout and/or stderr to /dev/null to suppress output during tests. This is useful for tests that produce noisy output or test error handling without cluttering the test output.
Definition at line 34 of file tests/logging.c.
References PLATFORM_O_WRONLY, and platform_open().
| bool test_logging_is_disabled | ( | void | ) |
#include <logging.c>
Check if logging is currently disabled.
Checks whether stdout and/or stderr have been redirected to /dev/null. Useful for conditional logging control or debugging test setup.
Definition at line 133 of file tests/logging.c.
| int test_logging_restore | ( | void | ) |
#include <logging.c>
Restore stdout/stderr output after test logging disable.
Restore stdout and/or stderr to their original state.
Restores stdout and/or stderr to their original file descriptors that were saved when test_logging_disable() was called.
Definition at line 90 of file tests/logging.c.