Compare commits

...

18 Commits

Author SHA1 Message Date
TapTap 3f73012b36 fix: address re-review — coverage build, chunk OOB guard, UBSan loads, srand scope
CI / lint (push) Successful in 8s
CI / lint (pull_request) Successful in 7s
CI / sanitizers (address) (push) Successful in 14s
CI / sanitizers (undefined) (push) Successful in 14s
CI / fuzz-build (push) Successful in 13s
CI / coverage (push) Successful in 10s
CI / build-and-test (push) Successful in 55s
CI / valgrind (push) Successful in 11s
CI / sanitizers (address) (pull_request) Successful in 15s
CI / sanitizers (undefined) (pull_request) Successful in 16s
CI / fuzz-build (pull_request) Successful in 12s
CI / build-and-test (pull_request) Successful in 55s
CI / coverage (pull_request) Successful in 9s
CI / valgrind (pull_request) Successful in 13s
- Remove -DSTRICT_WARNINGS=ON from coverage job (_FORTIFY_SOURCE + -O0 + -Werror fatal)
- Fix chunk.c metadata guard: peek at present flag before calling metadata_from_buf
  so the remaining_size check covers the full sizeof(int)+FILE_METADATA_WIRE_SIZE
- Fix chunk.c UBSan misaligned loads: use memcpy instead of *(size_t*)deref
- Move srand(42) to test_property() top level so all property tests are seeded
2026-07-20 14:53:39 +02:00
TapTap ed6242cfc5 ci: bump CI image to v9 with libclang-rt-18-dev
v8 had cached image without libclang-rt-18-dev on CI runner.
v9 forces a fresh pull and includes all fuzzer runtime libraries.
2026-07-20 14:44:41 +02:00
TapTap a27f13d657 fix: bake CI deps into Docker image, fix chunk use_metadata OOB
- Build and push fastsync-ci:v8 with lcov, valgrind, clang baked in
- Remove all apt-get install steps from CI (v8 has them pre-installed)
- Fix chunk.c use_metadata=true OOB: add remaining_size guard before
  metadata_from_buf reads past the buffer. The bug allowed network-facing
  chunk_deserialize to heap-buffer-overflow on crafted inputs.
- Also guard against unsigned underflow on remaining_size - sizeof(int)
2026-07-20 14:44:41 +02:00
TapTap eb8651e3d5 fix: address PR review — fuzz target fixes, UBSan, CI improvements
Critical fixes:
- Add missing #include <string.h> to 5 fuzz targets (wouldn't compile with GCC14+/Clang16+)
- Fix fuzz_metadata_from_buf.c OOB read: guard size >= sizeof(int) + FILE_METADATA_WIRE_SIZE
- Add Clang compiler check for ENABLE_FUZZ in CMakeLists.txt (fail fast at configure time)
- Add fuzz-build CI job (install clang, build all 6 fuzz targets with ENABLE_FUZZ=ON)

Warning fixes:
- Add UBSan to sanitizer CI matrix (address + undefined)
- Remove tautological test_property_glob_consistency (pure/deterministic function)
- Use fixed seed srand(42) instead of srand(time(NULL)) for reproducible property tests
- Add valid-header + truncated-instructions delta robustness test (exercises instruction-loop error paths)
- Fix lcov --remove to exclude '*/_deps/*' (xxhash coverage pollution)
- Add comment explaining FASTSYNC_UNDER_VALGRIND skip in test_file.c
- Update .gitignore for build-*/ directories
2026-07-20 14:44:41 +02:00
TapTap e9ab84b5ea ci: fix coverage and valgrind jobs, skip fork tests under valgrind
- Install lcov in coverage job (not in fastsync-ci:v7)
- Use lcov 2.x compatible flags (--branch-coverage instead of --rc)
- Remove unused xxhash exclude pattern that lcov 2.x rejects
- Install valgrind in valgrind job
- Skip fork-based file tests under valgrind (pipe timing issues)
- Add FASTSYNC_UNDER_VALGRIND env var for test skip detection
- Move cleanup before assertions in fork tests to prevent leaks
- Add coverage.info to .gitignore
2026-07-20 14:44:41 +02:00
TapTap 7c6e69ad81 ci: install lcov and valgrind in CI jobs
The fastsync-ci:v7 container does not include lcov or valgrind.
Add apt-get install steps to the coverage and valgrind jobs.

Also update lcov flags for lcov 2.x compatibility:
--rc lcov_branch_coverage=1 -> --branch-coverage
2026-07-20 14:44:41 +02:00
TapTap 66d994f01b fix: resolve cppcheck warnings in new test files
- Remove unused ConsumerCtx struct in test_stress.c
- Add const qualifiers to variables only checked for NULL
  in test_metadata.c, test_protocol.c, test_robustness.c
2026-07-20 14:44:35 +02:00
TapTap 44d0f99de9 style: fix clang-format violations in test files
Fix line wrapping and designated initializer alignment in
test_file.c, test_protocol.c, and test_stress.c to pass
CI clang-format check.
2026-07-20 14:44:35 +02:00
TapTap 8f77395b58 testing: add comprehensive test suite for test-driven development
Add unit tests for previously untested modules (protocol, data, metadata,
file, glob), robustness tests for deserialization of malformed inputs,
thread safety stress tests, property-based roundtrip tests, and 6 fuzz
targets. Update CI with CTest integration, coverage reporting, and
valgrind memory checking.

New test files:
- test_data.c: Data type lifecycle (5 tests)
- test_protocol.c: Protocol I/O roundtrips and error paths (9 tests)
- test_metadata.c: Metadata serialization roundtrips (6 tests)
- test_file.c: File operations and send/receive (12 tests)
- test_glob.c: Glob pattern matching (10 tests)
- test_robustness.c: Malformed input handling for chunk/delta/protocol (11 tests)
- test_stress.c: MPMC queue stress, backpressure, rapid create/destroy (3 tests)
- test_property.c: Compress, delta, chunk, glob roundtrip properties (4 tests)
- tests/fuzz/: 6 libFuzzer targets for deserialization functions

Extended existing tests:
- test_config.c: config_send/config_receive roundtrip via fork+pipe
- test_shared_utils.c: mkdir_r, glob_match, delete_extras

Infrastructure:
- CTest integration in CMakeLists.txt
- Coverage support (-DENABLE_COVERAGE=ON)
- Fuzz target support (-DENABLE_FUZZ=ON)
- CI: coverage, valgrind, address sanitizer jobs
- Fixed MPMC stress test race condition (cnd_signal -> cnd_broadcast)
2026-07-20 14:44:35 +02:00
TapTap afa5aeca37 Merge pull request 'feat: update AI automation agents, add reviewer agent, enhance CI' (#26) from feat/ai-automation-updates into main
CI / lint (push) Successful in 7s
CI / build-and-test (push) Successful in 53s
CI / sanitizers (address) (push) Successful in 58s
CI / clang-tidy (push) Successful in 7s
Reviewed-on: #26
2026-07-20 14:38:51 +02:00
TapTap 5c686b0655 fix: address PR review — doc accuracy, clang-tidy info, config leak fix
CI / lint (push) Successful in 7s
CI / lint (pull_request) Successful in 7s
CI / build-and-test (push) Successful in 54s
CI / clang-tidy (push) Successful in 6s
CI / sanitizers (address) (push) Successful in 59s
CI / build-and-test (pull_request) Successful in 55s
CI / clang-tidy (pull_request) Successful in 5s
CI / sanitizers (address) (pull_request) Successful in 57s
Review findings addressed:

Critical:
- cmake-expert.md: replace commented-out sanitizer lines with actual
  SANITIZER cache variable block, fix ENABLE_ASAN/TSAN/UBSAN pattern
- integrator.md: update YAML example to v7, use -DSANITIZER= instead
  of raw flags, add symlink + LSAN suppression steps

Warnings:
- ci.yaml: rename clang-tidy step to 'informational, non-blocking',
  add ::warning:: workflow command for visibility
- client_cli.c: add goto cleanup pattern to free config on error paths
  (skip for multithreaded path where pipeline_context_sender_destroy
  already owns config)

Suggestions:
- test-writer.md: clarify conftest.py is at tests/conftest.py
- .lsan-suppressions.txt: remove config_create suppression (leak fixed)
2026-07-20 14:28:42 +02:00
TapTap c7f34eaf9d ci: symlink build dir for sanitizer integration tests 2026-07-20 14:22:27 +02:00
TapTap bd17f50f9b feat: update AI automation agents, add reviewer agent, fix leaks, enhance CI
Agents:
- cmake-expert: fix stale CMake version (4.1 -> 3.22), add OpenSSL/xxHash deps
- integrator: fix stale test.py references to tests/integration/, update CI docs
- test-writer: update integration test patterns for modular test structure
- reviewer: new comprehensive PR reviewer (code, build, CI, docs, quality)

Bug fixes:
- delta.c: set instructions[i].type = DELTA_INSTR_LITERAL in deserialize
- scanner.c: free ArrayList when directory_scanner_next returns NULL

CI:
- Add sanitizer job (ASan + UBSan) with LSAN suppressions for pre-existing leaks
- Add clang-tidy job with proper warning/error detection
- Remove || true masking (fixes now make sanitizer useful)

Cleanup:
- gitignore build-asan/
- .lsan-suppressions.txt for known CLI config leaks
2026-07-20 14:22:27 +02:00
TapTap a4b35e136b Merge pull request 'docs: add AGENTS.md with custom-image dependency install rule' (#27) from feat/custom-image-deps-rule into main
CI / lint (push) Successful in 12s
CI / sanitizers (address) (push) Successful in 14s
CI / build-and-test (push) Successful in 53s
Reviewed-on: #27
2026-07-19 22:57:15 +02:00
TapTap 02ceb6828b fix: re-review cleanup — deps/conventions/sanitizer sections in cmake-expert, AGENTS.md notes
CI / lint (push) Successful in 8s
CI / lint (pull_request) Successful in 7s
CI / sanitizers (address) (push) Successful in 14s
CI / build-and-test (push) Successful in 54s
CI / build-and-test (pull_request) Successful in 53s
CI / sanitizers (address) (pull_request) Successful in 14s
2026-07-19 22:54:02 +02:00
TapTap 0f9e689e39 fix: address PR review — lcov/valgrind in Dockerfile, correct cmake-expert.md, expand AGENTS.md
CI / lint (push) Successful in 8s
CI / lint (pull_request) Successful in 7s
CI / build-and-test (push) Successful in 53s
CI / sanitizers (address) (push) Successful in 14s
CI / build-and-test (pull_request) Successful in 53s
CI / sanitizers (address) (pull_request) Successful in 14s
2026-07-19 22:37:26 +02:00
TapTap a7bb6454a4 docs: add AGENTS.md with custom-image dependency install rule
CI / lint (push) Successful in 8s
CI / build-and-test (push) Successful in 52s
CI / sanitizers (address) (push) Successful in 13s
CI / lint (pull_request) Successful in 7s
CI / build-and-test (pull_request) Successful in 53s
CI / sanitizers (address) (pull_request) Successful in 14s
2026-07-19 20:43:54 +02:00
TapTap 8a222bffa6 Merge pull request 'ci: add static analysis, sanitizers, and formatting enforcement' (#24) from ci/tier1-review-automation into main
CI / lint (push) Successful in 7s
CI / build-and-test (push) Successful in 53s
CI / sanitizers (address) (push) Successful in 14s
Reviewed-on: #24
2026-07-19 18:10:06 +02:00
36 changed files with 1956 additions and 131 deletions
+63 -7
View File
@@ -5,7 +5,7 @@ on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v7
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -18,7 +18,7 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v7
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
needs: lint
steps:
- name: Checkout
@@ -31,18 +31,18 @@ jobs:
run: cmake --build build -j$(nproc)
- name: Unit Tests
run: ./build/tests
run: ctest --test-dir build --output-on-failure -j$(nproc)
- name: Integration Tests
run: python3 -m pytest tests/ -v --tb=short
run: python3 -m pytest tests/integration/ -v --tb=short
sanitizers:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v7
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
needs: lint
strategy:
matrix:
sanitizer: [address]
sanitizer: [address, undefined]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -54,4 +54,60 @@ jobs:
run: cmake --build build-${{ matrix.sanitizer }} -j$(nproc)
- name: Unit Tests
run: ./build-${{ matrix.sanitizer }}/tests
run: ctest --test-dir build-${{ matrix.sanitizer }} --output-on-failure
fuzz-build:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure (clang + fuzz)
run: CC=clang CXX=clang++ cmake -B build-fuzz -S . -DENABLE_FUZZ=ON
- name: Build fuzz targets
run: cmake --build build-fuzz -j$(nproc)
coverage:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure
run: cmake -B build -S . -DENABLE_COVERAGE=ON
- name: Build
run: cmake --build build -j$(nproc)
- name: Unit Tests
run: ctest --test-dir build --output-on-failure
- name: Coverage Report
run: |
lcov --capture --directory build --output-file coverage.info --branch-coverage --ignore-errors negative
lcov --remove coverage.info '/usr/*' '*/tests/*' '*/_deps/*' --output-file coverage.info --branch-coverage --ignore-errors unused,negative
lcov --list coverage.info
valgrind:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
- name: Build
run: cmake --build build -j$(nproc)
- name: Valgrind Memcheck
run: valgrind --leak-check=full --show-leak-kinds=definite --error-exitcode=1 ./build/tests
env:
FASTSYNC_UNDER_VALGRIND: "1"
+2
View File
@@ -3,3 +3,5 @@ data_copied
test_data/
__pycache__/
build-asan
coverage.info
build-*/
+6
View File
@@ -0,0 +1,6 @@
# LSAN suppressions for FastSync
# Add suppression entries here for known pre-existing leaks that cannot be
# fixed immediately. Remove entries as leaks are fixed.
#
# Example format:
# leak:function_name
+2
View File
@@ -9,6 +9,8 @@ You are a system architect for the FastSync project — a high-performance file
Make high-level design decisions. Evaluate trade-offs, plan module interactions, design data flow, and ensure architectural coherence across the codebase.
> **Environment rule:** dependency installation must always use the project's custom Docker image (repo-root `Dockerfile`, same as CI) — never ad-hoc host package installs. See `AGENTS.md`.
## Project Architecture
### Module Map
+57 -56
View File
@@ -3,7 +3,7 @@ description: Manages the CMake build system for FastSync — adding targets, sou
mode: subagent
---
You are a CMake expert for the FastSync project — a high-performance file synchronization system built with CMake 4.1+ and C11.
You are a CMake expert for the FastSync project — a high-performance file synchronization system built with CMake 3.22+ and C11.
## Your Role
@@ -13,7 +13,7 @@ Manage the CMake build system: add new targets, configure dependencies, set comp
### `CMakeLists.txt` (project root)
```cmake
cmake_minimum_required(VERSION 4.1)
cmake_minimum_required(VERSION 3.22)
project(FastFileTransfer)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -22,30 +22,54 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
add_compile_options(-Wall -g -O3)
include(FetchContent)
FetchContent_Declare(xxhash GIT_REPOSITORY https://github.com/Cyan4973/xxHash GIT_TAG v0.8.3 SOURCE_SUBDIR cmake_unofficial)
FetchContent_MakeAvailable(xxhash)
# Sanitizer option
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)")
set_property(CACHE SANITIZER PROPERTY STRINGS address thread none)
if(SANITIZER STREQUAL "address")
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g)
add_link_options(-fsanitize=address)
elseif(SANITIZER STREQUAL "thread")
add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g)
add_link_options(-fsanitize=thread)
elseif(NOT SANITIZER STREQUAL "none")
message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, none")
endif()
option(STRICT_WARNINGS "Enable strict warnings" OFF)
if(STRICT_WARNINGS)
add_compile_options(-Wextra -Wpedantic -Werror)
endif()
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
find_library(ZSTD_LIBRARY zstd)
# ... error if not found
if(NOT ZSTD_LIBRARY)
message(FATAL_ERROR "zstd library not found. Ensure it is in your nix-shell!")
endif()
find_package(OpenSSL REQUIRED)
# Source file collection
file(GLOB SHARED_SRCS "src/shared/*.c")
file(GLOB SERVER_SRCS "src/server/*.c")
file(GLOB CLIENT_SRCS "src/client/*.c")
file(GLOB TEST_SRCS "tests/*.c")
# Targets
add_executable(server ${SERVER_SRCS} ${SHARED_SRCS})
target_include_directories(server PRIVATE src/shared src/server src/client)
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY})
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS})
target_include_directories(client PRIVATE src/shared src/server src/client)
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY})
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c)
target_include_directories(tests PRIVATE tests src/shared src/server src/client)
target_link_libraries(tests PRIVATE Threads::Threads ${ZSTD_LIBRARY})
target_link_libraries(tests PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
```
### Source Layout
@@ -53,22 +77,26 @@ target_link_libraries(tests PRIVATE Threads::Threads ${ZSTD_LIBRARY})
src/shared/ — shared libraries (globbed as SHARED_SRCS)
src/client/ — client sources (globbed as CLIENT_SRCS)
src/server/ — server sources (globbed as SERVER_SRCS)
tests/ — test sources (globbed as TEST_SRCS)
tests/ — unit test sources (globbed as TEST_SRCS)
tests/integration/ — Python pytest integration tests
```
### Dependencies
- **zstd** — found via `find_library(ZSTD_LIBRARY zstd)`
- **OpenSSL** — found via `find_package(OpenSSL REQUIRED)` (TLS 1.2+ transport)
- **xxHash** — fetched via `FetchContent` from GitHub (delta transfer hashing, v0.8.3)
- **pthreads** — found via `find_package(Threads REQUIRED)`
- **C11 standard** — required
- **CMake 4.1+** — minimum version
- **CMake 3.22+** — minimum version
## Conventions
- Use `file(GLOB ...)` for source collection (existing pattern).
- All targets link `Threads::Threads` and `${ZSTD_LIBRARY}`.
- All targets link `Threads::Threads`, `${ZSTD_LIBRARY}`, `OpenSSL::SSL`, `OpenSSL::Crypto`, and `xxhash`.
- Include directories: `src/shared`, `src/server`, `src/client`, `tests` (for test target).
- Sanitizer support is commented out but present (`-fsanitize=address`).
- Sanitizer support: pass `-DSANITIZER=address` or `-DSANITIZER=thread` to cmake (live option in CMakeLists.txt).
- Build with `cmake -B build -S . && cmake --build build -j$(nproc)`.
- Install dependencies only via the project's custom Docker image (repo-root `Dockerfile`, same image CI uses) — never via host package installs; see `AGENTS.md`.
## When Making Changes
@@ -77,28 +105,21 @@ tests/ — test sources (globbed as TEST_SRCS)
3. Add new dependencies with `find_package` or `find_library`.
4. When adding a new executable target, follow the pattern of existing targets.
5. When adding a new library (static/shared), use `add_library` and follow the project's naming.
6. For sanitizer builds, use the commented-out `-fsanitize=address` lines as reference.
6. For sanitizer builds, pass `-DSANITIZER=address` or `-DSANITIZER=thread` to cmake (matching CI's matrix strategy).
7. Always verify the build compiles after changes.
## Sanitizer Configurations
### AddressSanitizer (memory errors)
Use the project's built-in `-DSANITIZER=` option (matching the CI matrix):
```bash
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake -B build -S . -DSANITIZER=address # AddressSanitizer (memory errors)
cmake --build build -j$(nproc)
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (race conditions)
cmake --build build -j$(nproc)
```
### ThreadSanitizer (race conditions)
```bash
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=thread -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
cmake --build build -j$(nproc)
```
### UndefinedBehaviorSanitizer
For UndefinedBehaviorSanitizer (no `-DSANITIZER=undefined` option in CMakeLists.txt yet), use the manual flag approach:
```bash
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
@@ -106,14 +127,6 @@ cmake -B build -S . \
cmake --build build -j$(nproc)
```
### Combined Sanitizers
```bash
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build build -j$(nproc)
```
### Using ccache (faster rebuilds)
```bash
cmake -B build -S . -DCMAKE_C_COMPILER_LAUNCHER=ccache
@@ -151,31 +164,19 @@ cmake --build build -j$(nproc)
./build/tests
```
## When Adding Sanitizer Support to CMakeLists.txt
## Sanitizer Integration
Use CMake options for cleaner integration:
The project uses a single `SANITIZER` cache variable in `CMakeLists.txt`:
```cmake
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined)
add_link_options(-fsanitize=undefined)
endif()
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)")
set_property(CACHE SANITIZER PROPERTY STRINGS address thread none)
```
Supported values: `address`, `thread`, `none`. Unknown values trigger `FATAL_ERROR`.
Then build with:
Build with:
```bash
cmake -B build -S . -DENABLE_ASAN=ON
cmake -B build -S . -DSANITIZER=address
cmake --build build -j$(nproc)
```
To add support for a new sanitizer (e.g., UBSan), add an `elseif(SANITIZER STREQUAL "undefined")` block following the existing `address`/`thread` pattern.
+18 -17
View File
@@ -16,12 +16,12 @@ Design integration tests that verify the full transfer pipeline works end-to-end
- Custom framework in `tests/test_utils.h`
- Run: `./build/tests`
### 2. Integration Tests (existing — `test.py`)
### 2. Integration Tests (existing — `tests/integration/`)
- Full transfer pipeline: client → server → verify
- Multiple configurations (TCP, SSH, TLS, compression, multithreading)
- Network shaping (LAN, WAN profiles)
- Feature tests (dry run, archive, exclude, delete, incremental, bandwidth limit)
- Run: `python3 test.py`
- Run: `python3 -m pytest tests/ -v --tb=short`
### 3. New: Focused Integration Tests
When adding new features or fixing bugs, write targeted integration tests.
@@ -106,35 +106,36 @@ test ! -f /tmp/dst/.../extra.txt
### Gitea Workflow Structure (`.gitea/workflows/ci.yaml`)
The project uses Gitea Actions. Key jobs:
1. **Build** — compile on push/PR
2. **Unit tests**run `./build/tests`
3. **Integration tests** — run `python3 test.py` (light mode)
4. **Sanitizer builds** — ASan, TSan variants
1. **build-and-test** — compile, unit tests, integration tests on push/PR
2. **sanitizer**ASan + UBSan build and test (separate job)
3. **clang-tidy** — static analysis on C source files
### Adding a New CI Job
```yaml
jobs:
sanitizer:
runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v7
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libzstd-dev libssl-dev
- name: Build with ASan
run: |
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build -j$(nproc)
- name: Run tests
run: ./build/tests
- name: Configure
run: cmake -B build-${{ matrix.sanitizer }} -S . -DSANITIZER=${{ matrix.sanitizer }}
- name: Build
run: cmake --build build-${{ matrix.sanitizer }} -j$(nproc)
- name: Symlink for integration tests
run: ln -sf build-${{ matrix.sanitizer }} build
- name: Unit Tests
run: ./build-${{ matrix.sanitizer }}/tests
- name: Integration Tests
run: LSAN_OPTIONS=suppressions=.lsan-suppressions.txt python3 -m pytest tests/ -v --tb=short
```
The symlink step is required because `tests/conftest.py` expects `./build` to exist.
## Verification Checklist
After any code change:
- [ ] Unit tests pass: `./build/tests`
- [ ] Integration tests pass: `python3 test.py` (light mode at minimum)
- [ ] Integration tests pass: `python3 -m pytest tests/ -v --tb=short`
- [ ] Build clean: no warnings with `-Wall`
- [ ] No memory errors: ASan clean
- [ ] No thread errors: TSan clean (if threading involved)
+134
View File
@@ -0,0 +1,134 @@
---
description: Reviews pull requests comprehensively — code correctness, CI/CD validity, configuration, documentation, and overall PR quality. Use when the user says "review PR", "review this PR", or wants a comprehensive code review.
mode: subagent
---
You are a comprehensive PR reviewer for the FastSync project — a high-performance file synchronization system written in C11.
## Your Role
Review pull requests holistically. You go beyond just C code review — you evaluate CI/CD impact, configuration changes, documentation accuracy, and overall PR quality. You are the final gatekeeper before merge.
## Review Dimensions
### 1. C Code Review
Review all changed `.c` and `.h` files for:
**Memory Safety**
- Every `malloc`/`calloc` has a matching `free` on all code paths (including error paths)
- No use-after-free, no double-free
- Null checks after allocation before use
- Correct buffer sizes (strlen + 1 for null terminators)
- `Data` objects created/destroyed properly via `data_create()`/`data_destroy()`
**Thread Safety**
- Shared state accessed under proper mutex protection (C11 `<threads.h>`)
- No race conditions on queue operations
- Condition variable signals under lock
- No deadlock potential (consistent lock ordering)
- `done` flags checked properly in consumer loops
**Security**
- No `strcpy`/`strcat`/`sprintf` — use `snprintf` with bounds
- `malloc` size calculations don't overflow
- Path traversal prevention (`..` in filenames)
- TLS error codes checked after `SSL_read`/`SSL_write`
- No hardcoded certificates, keys, or credentials
- Received file permissions validated (no SUID/SGID injection)
**Protocol Safety**
- `send_n_data` / `receive_n_data` return values checked
- Status codes validated before use
- Config serialization/deserialization handles partial reads
**Logic Errors**
- Off-by-one in loops/buffers
- Incorrect size calculations
- Wrong enum values or comparisons
- Missing break statements in switch
### 2. Build System Review
If `CMakeLists.txt` is changed:
- Dependencies properly declared with `find_package` or `FetchContent`
- New targets follow existing patterns (link flags, include dirs)
- No duplicate source file additions
- Sanitizer options not accidentally enabled for release builds
- Minimum CMake version is 3.22
### 3. CI/CD Review
If `.gitea/workflows/ci.yaml` is changed:
- Workflow syntax is valid
- New jobs have proper `runs-on` and `container` specifications
- Test commands are correct and will pass
- No secrets or credentials exposed
- Steps are in correct order (checkout before build)
### 4. Configuration & Documentation Review
If agents (`.opencode/agents/`), skills (`.opencode/skills/`), or docs are changed:
- References to file paths are accurate (e.g., `test.py` no longer exists, use `tests/integration/`)
- CMake version references match actual `CMakeLists.txt` (3.22, not 4.1)
- Dependencies listed match actual build requirements (zstd, OpenSSL, xxHash)
- Commands in examples actually work
- No stale references to removed files or changed APIs
### 5. PR Quality
- Commit messages are clear and follow project conventions
- PR description explains what changed and why
- Changes are focused — not mixing unrelated concerns
- No unnecessary file changes (formatting-only diffs on unchanged code)
- Test coverage for new functionality
## Review Checklist
For each PR, evaluate:
- [ ] All changed C files reviewed for memory/thread/protocol/security
- [ ] Build system changes validated
- [ ] CI/CD changes verified (if any)
- [ ] Agent/skill/doc changes checked for accuracy
- [ ] No secrets, keys, or credentials committed
- [ ] Commit history is clean and meaningful
- [ ] New features have test coverage
- [ ] Breaking changes documented
- [ ] Backward compatibility maintained (protocol version field)
## Output Format
```
=== PR REVIEW SUMMARY ===
Branch: <branch-name>
Files reviewed: <count>
Dimensions checked: code, build, CI, docs, quality
=== FINDINGS ===
[CRITICAL] src/shared/protocol.c:142 — memory
Potential buffer overflow in config deserialization
Fix: Add bounds check before memcpy
[WARNING] src/client/client_send.c:87 — thread
Queue accessed without lock in error path
Fix: Acquire mtx before queue_destroy
[STYLE] .opencode/agents/cmake-expert.md:5 — docs
References CMake 4.1 but project uses 3.22
Fix: Update version reference
=== VERDICT ===
[PASS] No critical issues found — safe to merge
— or —
[FAIL] <N> critical issues must be fixed before merge
```
## Rules
- Report ALL issues — don't filter or minimize
- Be specific about line numbers and fix suggestions
- Separate critical from warnings from style
- Check that the PR actually compiles (review CMake changes carefully)
- If agents/docs are changed, verify every reference is current
- Be constructive — suggest fixes, not just problems
+17 -23
View File
@@ -165,34 +165,28 @@ int main() {
## Integration Test Patterns
When writing integration tests (Python-based), follow the pattern in `test.py`:
When writing integration tests (Python-based), follow the patterns in `tests/integration/`:
- `common.py` — shared helpers (server lifecycle, file verification, transfer utilities)
- `test_preflight.py` — preflight checks and configuration validation
- `test_tcp.py` — TCP transport tests
- `test_ssh.py` — SSH transport tests
- `test_tls.py` — TLS transport tests
- `test_features.py` — feature-specific tests (delete, exclude, incremental, etc.)
Use `tests/conftest.py` fixtures for server setup/teardown (note: the file is at `tests/conftest.py`, not `tests/integration/conftest.py`).
### Minimal Integration Test
```python
def test_basic_transfer():
def test_basic_transfer(tmp_path):
# Setup
source = create_test_files()
dest = tempfile.mkdtemp()
source = tmp_path / "src"
dest = tmp_path / "dst"
source.mkdir()
dest.mkdir()
(source / "file.txt").write_text("test content")
# Start server
server = subprocess.Popen(["./build/server"], ...)
time.sleep(0.5)
# Run client
result = subprocess.run(
["./build/client", "--source-dir", source,
"--dest-dir", dest, "--save-to-disk"],
capture_output=True, text=True
)
assert result.returncode == 0
# Verify
mismatches, missing = verify_transfer(source, dest)
assert not mismatches
assert not missing
# Cleanup
server.terminate()
# Start server and run client (use fixtures from conftest.py)
# Verify with helper from common.py
```
### Edge Case Tests to Write
+57
View File
@@ -0,0 +1,57 @@
# AGENTS.md
FastSync is a high-performance file synchronization system written in C11. It supports TCP and SSH transports, TLS encryption (OpenSSL), streaming zstd compression, multithreaded transfers, and incremental sync. The build uses CMake; CI runs on Gitea Actions (`.gitea/workflows/ci.yaml`).
## Dependency installation
**Rule: always install dependencies using the project's custom Docker image — never via ad-hoc system package installs on the host** (no `apt-get install` / `pip install` on the host machine).
The image is built from the repo-root `Dockerfile` and is the same image CI uses: `gitea.tap-tap.win/taptap/fastsync-ci:v7`. It contains the full toolchain: gcc/g++, CMake, libzstd-dev, libssl-dev, make, git, cppcheck, clang-format, python3 + pytest, openssh-client, and Node.js.
```bash
# Use the prebuilt CI image directly (faster, guaranteed CI parity)
docker pull gitea.tap-tap.win/taptap/fastsync-ci:v7
docker tag gitea.tap-tap.win/taptap/fastsync-ci:v7 fastsync-ci:local
# Or build the image from the repo-root Dockerfile
# (Note: the prebuilt :v7 image reflects the previous Dockerfile state;
# rebuild from source to pick up any newly added packages like lcov/valgrind.)
docker build -t fastsync-ci:local .
# Build, run unit tests, and run integration tests inside the container
docker run --rm -v "$PWD:/workspace" -w /workspace fastsync-ci:local \
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/'
# Avoid root-owned build/ artifacts by matching your host UID/GID
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/workspace" \
-w /workspace fastsync-ci:local \
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/'
```
> **Note:** The first `cmake configure` (`cmake -B build -S .`) fetches xxHash from GitHub via `FetchContent` — network access is required. Subsequent reconfigures reuse the cached source.
If a dependency is missing from the image, add it to the `Dockerfile` (and rebuild) rather than installing it on the host.
## CI Conventions
When configuring for CI parity, use:
```bash
cmake -B build -S . -DSTRICT_WARNINGS=ON # -Wextra -Wpedantic -Werror
cmake -B build -S . -DSANITIZER=address # AddressSanitizer (ASan)
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (TSan)
```
The CI workflow (`.gitea/workflows/ci.yaml`) runs lint (clang-format, cppcheck), build + test (unit + integration), and sanitizer (currently only `address`) jobs sequentially.
## Build
```bash
cmake -B build -S . && cmake --build build -j$(nproc)
```
## Test
```bash
./build/tests # unit tests
python3 -m pytest tests/ # integration tests
```
+43 -7
View File
@@ -9,8 +9,8 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
add_compile_options(-Wall -g -O3)
# --- Sanitizer option ---
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)")
set_property(CACHE SANITIZER PROPERTY STRINGS address thread none)
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, undefined, none)")
set_property(CACHE SANITIZER PROPERTY STRINGS address thread undefined none)
if(SANITIZER STREQUAL "address")
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g)
@@ -18,8 +18,11 @@ if(SANITIZER STREQUAL "address")
elseif(SANITIZER STREQUAL "thread")
add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g)
add_link_options(-fsanitize=thread)
elseif(SANITIZER STREQUAL "undefined")
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer -g)
add_link_options(-fsanitize=undefined)
elseif(NOT SANITIZER STREQUAL "none")
message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, none")
message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, undefined, none")
endif()
# --- Strict warnings option ---
@@ -28,6 +31,13 @@ if(STRICT_WARNINGS)
add_compile_options(-Wextra -Wpedantic -Werror)
endif()
# --- Coverage option ---
option(ENABLE_COVERAGE "Enable gcov coverage" OFF)
if(ENABLE_COVERAGE)
add_compile_options(--coverage -fprofile-arcs -ftest-coverage -O0 -g)
add_link_options(--coverage)
endif()
include(FetchContent)
FetchContent_Declare(
xxhash
@@ -50,8 +60,8 @@ find_package(OpenSSL REQUIRED)
file(GLOB SHARED_SRCS "src/shared/*.c")
file(GLOB SERVER_SRCS "src/server/*.c")
file(GLOB CLIENT_SRCS "src/client/*.c")
file(GLOB TEST_SRCS "tests/*.c")
# --- Main executables ---
add_executable(server ${SERVER_SRCS} ${SHARED_SRCS})
target_include_directories(server PRIVATE src/shared src/server src/client)
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
@@ -60,7 +70,33 @@ add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS})
target_include_directories(client PRIVATE src/shared src/server src/client)
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c)
target_include_directories(tests PRIVATE tests src/shared src/server src/client)
target_link_libraries(tests PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
# --- Testing ---
enable_testing()
# Common test libraries
set(TEST_LIBS Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
set(TEST_INCLUDES tests src/shared src/server src/client)
# Monolithic test binary (backward compatible)
file(GLOB TEST_SRCS "tests/test_*.c" "tests/runner.c")
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c)
target_include_directories(tests PRIVATE ${TEST_INCLUDES})
target_link_libraries(tests PRIVATE ${TEST_LIBS})
add_test(NAME unit_all COMMAND tests)
# --- Fuzz targets (requires clang) ---
option(ENABLE_FUZZ "Build fuzz targets (requires clang)" OFF)
if(ENABLE_FUZZ)
if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "ENABLE_FUZZ requires Clang (compiler is ${CMAKE_C_COMPILER_ID})")
endif()
file(GLOB FUZZ_SRCS "tests/fuzz/*.c")
foreach(FUZZ_SRC ${FUZZ_SRCS})
get_filename_component(FUZZ_NAME ${FUZZ_SRC} NAME_WE)
add_executable(${FUZZ_NAME} ${FUZZ_SRC} ${SHARED_SRCS})
target_include_directories(${FUZZ_NAME} PRIVATE ${TEST_INCLUDES})
target_compile_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
target_link_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined)
target_link_libraries(${FUZZ_NAME} PRIVATE ${TEST_LIBS})
endforeach()
endif()
+2 -1
View File
@@ -1,7 +1,8 @@
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc g++ make libc6-dev cmake libzstd-dev libssl-dev git ca-certificates curl cppcheck clang-format \
python3 python3-pip python3-venv openssl openssh-client && \
python3 python3-pip python3-venv openssl openssh-client \
lcov valgrind clang libclang-rt-18-dev && \
pip3 install --break-system-packages pytest && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
+40 -17
View File
@@ -73,6 +73,8 @@ int main(int argc, char* argv[]) {
Config* config = config_create(str_dup(PROTOCOL_VERSION), NULL, NULL, save_to_disk, false, false,
false, false, 5, false, 0);
int exit_code = 0;
bool config_owned_by_pipeline = false;
int positional_args[2];
int positional_count = 0;
@@ -80,7 +82,7 @@ int main(int argc, char* argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0) {
print_usage();
return 0;
goto cleanup;
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) {
config->use_compression = true;
config->use_multithreading = true;
@@ -165,11 +167,13 @@ int main(int argc, char* argv[]) {
unsigned long long kbps = strtoull(argv[++i], &end, 10);
if (errno != 0 || *end != '\0' || kbps == 0) {
fprintf(stderr, "Error: --bwlimit must be a positive integer\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (kbps > ULLONG_MAX / 1024) {
fprintf(stderr, "Error: --bwlimit value too large\n");
return 1;
exit_code = 1;
goto cleanup;
}
io_set_bwlimit(kbps * 1024);
log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps);
@@ -195,14 +199,16 @@ int main(int argc, char* argv[]) {
} else if (argv[i][0] == '-') {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
print_usage();
return 1;
exit_code = 1;
goto cleanup;
} else {
if (positional_count < 2)
positional_args[positional_count++] = i;
else {
fprintf(stderr, "Unexpected argument: %s\n", argv[i]);
print_usage();
return 1;
exit_code = 1;
goto cleanup;
}
}
}
@@ -218,7 +224,8 @@ int main(int argc, char* argv[]) {
} else if (positional_count == 1) {
fprintf(stderr, "Error: missing destination argument\n");
print_usage();
return 1;
exit_code = 1;
goto cleanup;
} else {
if (!config->send_directory && env_source)
config->send_directory = str_dup((char*)env_source);
@@ -229,22 +236,26 @@ int main(int argc, char* argv[]) {
if (!config->send_directory || !config->receive_root_directory) {
fprintf(stderr, "Error: source and destination directories are required\n");
print_usage();
return 1;
exit_code = 1;
goto cleanup;
}
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk "
"serialization)\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (config->transport == TRANSPORT_SSH && config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (config->use_incremental && config->use_chunk_serialization) {
fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (config->use_incremental && !config->use_metadata) {
@@ -254,15 +265,18 @@ int main(int argc, char* argv[]) {
if (config->use_delta && !config->use_incremental) {
fprintf(stderr, "Error: --delta requires --incremental\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (config->use_delta && config->use_chunk_serialization) {
fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (config->use_delta && config->use_sendfile) {
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
return 1;
exit_code = 1;
goto cleanup;
}
if (config->use_delta && !config->use_metadata) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta");
@@ -272,12 +286,21 @@ int main(int argc, char* argv[]) {
if (config->use_tls) {
if (!config->tls_cert || !config->tls_key) {
fprintf(stderr, "Error: --tls requires --cert and --key\n");
return 1;
exit_code = 1;
goto cleanup;
}
tls_global_init();
}
if (config->use_multithreading)
return send_files_multithreaded(config);
return send_files(config);
if (config->use_multithreading) {
config_owned_by_pipeline = true;
exit_code = send_files_multithreaded(config);
} else {
exit_code = send_files(config);
}
cleanup:
if (!config_owned_by_pipeline)
config_delete(config);
return exit_code;
}
+17 -2
View File
@@ -95,7 +95,8 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
return NULL;
}
size_t path_len = *(size_t*)data_pointer;
size_t path_len;
memcpy(&path_len, data_pointer, sizeof(size_t));
data_pointer += sizeof(size_t);
remaining_size -= sizeof(size_t);
@@ -120,6 +121,19 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
free(path);
if (use_metadata) {
if (remaining_size < sizeof(int)) {
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
array_list_delete(files);
return NULL;
}
// Peek at present flag to determine total size needed before reading
int present_flag;
memcpy(&present_flag, data_pointer, sizeof(int));
if (present_flag && remaining_size < sizeof(int) + FILE_METADATA_WIRE_SIZE) {
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata body");
array_list_delete(files);
return NULL;
}
file->metadata = metadata_from_buf(&data_pointer);
remaining_size -= sizeof(int);
if (file->metadata)
@@ -132,7 +146,8 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
return NULL;
}
size_t file_data_size = *(size_t*)data_pointer;
size_t file_data_size;
memcpy(&file_data_size, data_pointer, sizeof(size_t));
data_pointer += sizeof(size_t);
remaining_size -= sizeof(size_t);
+26
View File
@@ -0,0 +1,26 @@
#include "chunk.h"
#include "data.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size == 0)
return 0;
void* buf = malloc(size);
if (!buf)
return 0;
memcpy(buf, data, size);
Data* d = data_create(buf, size);
if (!d)
return 0;
Chunk* chunk = chunk_deserialize(d, false);
if (chunk)
chunk_destroy(chunk);
data_destroy(d);
return 0;
}
+31
View File
@@ -0,0 +1,31 @@
#include "compression.h"
#include "data.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size == 0)
return 0;
void* buf = malloc(size);
if (!buf)
return 0;
memcpy(buf, data, size);
Data* d = data_create(buf, size);
if (!d)
return 0;
Data* compressed = data_compress(d, 3);
if (compressed) {
Data* decompressed = data_decompress(compressed);
if (decompressed) {
data_destroy(decompressed);
}
data_destroy(compressed);
}
data_destroy(d);
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
#include "delta.h"
#include "data.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size == 0)
return 0;
void* buf = malloc(size);
if (!buf)
return 0;
memcpy(buf, data, size);
Data* d = data_create(buf, size);
if (!d)
return 0;
Delta* delta = delta_deserialize(d);
if (delta)
delta_destroy(delta);
data_destroy(d);
return 0;
}
@@ -0,0 +1,26 @@
#include "delta.h"
#include "data.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size == 0)
return 0;
void* buf = malloc(size);
if (!buf)
return 0;
memcpy(buf, data, size);
Data* d = data_create(buf, size);
if (!d)
return 0;
DeltaSignature* sig = delta_signature_deserialize(d);
if (sig)
delta_signature_destroy(sig);
data_destroy(d);
return 0;
}
+32
View File
@@ -0,0 +1,32 @@
#include "utils.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 2)
return 0;
// Split input into pattern and string at the midpoint
size_t mid = size / 2;
char* pattern = malloc(mid + 1);
char* str = malloc(size - mid + 1);
if (!pattern || !str) {
free(pattern);
free(str);
return 0;
}
memcpy(pattern, data, mid);
pattern[mid] = '\0';
memcpy(str, data + mid, size - mid);
str[size - mid] = '\0';
glob_match(pattern, str);
free(pattern);
free(str);
return 0;
}
+23
View File
@@ -0,0 +1,23 @@
#include "metadata.h"
#include "file.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < sizeof(int) + FILE_METADATA_WIRE_SIZE)
return 0;
char* buf = malloc(size);
if (!buf)
return 0;
memcpy(buf, data, size);
char* original_buf = buf;
FileMetadata* m = metadata_from_buf(&buf);
if (m)
free(m);
free(original_buf);
return 0;
}
+16
View File
@@ -2,10 +2,18 @@
#include "test_chunk.h"
#include "test_compression.h"
#include "test_config.h"
#include "test_data.h"
#include "test_delta.h"
#include "test_file.h"
#include "test_glob.h"
#include "test_metadata.h"
#include "test_property.h"
#include "test_protocol.h"
#include "test_queue.h"
#include "test_robustness.h"
#include "test_scanner.h"
#include "test_shared_utils.h"
#include "test_stress.h"
#include "test_utils.h"
#include <stdio.h>
@@ -25,6 +33,14 @@ int main() {
RUN_TEST(test_compression);
RUN_TEST(test_scanner);
RUN_TEST(test_delta);
RUN_TEST(test_data);
RUN_TEST(test_protocol);
RUN_TEST(test_metadata);
RUN_TEST(test_glob);
RUN_TEST(test_file);
RUN_TEST(test_robustness);
RUN_TEST(test_stress);
RUN_TEST(test_property);
printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n");
printf("Total Tests Run: %d\n", tests_run);
+50
View File
@@ -0,0 +1,50 @@
#include "data.h"
#include "test_utils.h"
#include <stdlib.h>
#include <string.h>
static void test_data_create() {
char* buf = malloc(6);
EXPECT_NOT_NULL(buf);
memcpy(buf, "hello", 6);
Data* d = data_create(buf, 6);
EXPECT_NOT_NULL(d);
EXPECT_NOT_NULL(d->data);
EXPECT_TRUE(d->data == buf);
EXPECT_EQ_INT((int)d->size, 6);
data_destroy(d);
}
static void test_data_create_empty() {
Data* d = data_create_empty(256);
EXPECT_NOT_NULL(d);
EXPECT_NOT_NULL(d->data);
EXPECT_EQ_INT((int)d->size, 256);
data_destroy(d);
}
static void test_data_create_reserve() {
Data* d = data_create_reserve(1024);
EXPECT_NOT_NULL(d);
EXPECT_NULL(d->data);
EXPECT_EQ_INT((int)d->size, 1024);
data_destroy(d);
}
static void test_data_destroy_null() {
data_destroy(NULL);
}
static void test_data_destroy_normal() {
Data* d = data_create_empty(128);
EXPECT_NOT_NULL(d);
data_destroy(d);
}
void test_data() {
test_data_create();
test_data_create_empty();
test_data_create_reserve();
test_data_destroy_null();
test_data_destroy_normal();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_DATA_H
#define TEST_DATA_H
void test_data();
#endif
+284
View File
@@ -0,0 +1,284 @@
#include "test_file.h"
#include "file.h"
#include "data.h"
#include "utils.h"
#include "protocol.h"
#include "test_utils.h"
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
static void test_file_create() {
File* f = file_create("test_file_create.txt");
EXPECT_NOT_NULL(f);
EXPECT_NOT_NULL(f->path);
EXPECT_EQ_STR(f->path, "test_file_create.txt");
EXPECT_NOT_NULL(f->data);
EXPECT_NULL(f->data->data);
EXPECT_EQ_INT((int)f->data->size, 0);
EXPECT_NULL(f->metadata);
file_destroy(f);
}
static void test_file_destroy_null() {
file_destroy(NULL);
}
static void test_file_destroy_normal() {
File* f = file_create("test_destroy.txt");
EXPECT_NOT_NULL(f);
file_destroy(f);
}
static void test_file_load_data() {
const char* content = "Hello Load Test";
EXPECT_TRUE(to_disk("test_file_load_data.txt", content, strlen(content)));
struct stat st;
EXPECT_EQ_INT(stat("test_file_load_data.txt", &st), 0);
File* f = file_create("test_file_load_data.txt");
EXPECT_NOT_NULL(f);
f->data->size = st.st_size;
EXPECT_TRUE(file_load_data(f));
EXPECT_NOT_NULL(f->data->data);
EXPECT_EQ_INT((int)f->data->size, (int)st.st_size);
EXPECT_EQ_INT(memcmp(f->data->data, content, strlen(content)), 0);
file_destroy(f);
unlink("test_file_load_data.txt");
}
static void test_file_load_data_missing_file() {
File* f = file_create("nonexistent_test_file_xyz.txt");
EXPECT_NOT_NULL(f);
f->data->size = 10;
EXPECT_FALSE(file_load_data(f));
file_destroy(f);
}
static void test_file_save_to_disk() {
File* f = file_create("saved_file.txt");
EXPECT_NOT_NULL(f);
const char* content = "Save to disk content";
f->data->data = malloc(strlen(content));
EXPECT_NOT_NULL(f->data->data);
memcpy(f->data->data, content, strlen(content));
f->data->size = strlen(content);
EXPECT_TRUE(file_save_to_disk("test_save_tmp", f));
struct stat st;
EXPECT_EQ_INT(stat("test_save_tmp/saved_file.txt", &st), 0);
FILE* fp = fopen("test_save_tmp/saved_file.txt", "rb");
EXPECT_NOT_NULL(fp);
char buf[100];
size_t nread = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
EXPECT_EQ_INT((int)nread, (int)strlen(content));
EXPECT_EQ_INT(memcmp(buf, content, strlen(content)), 0);
file_destroy(f);
unlink("test_save_tmp/saved_file.txt");
rmdir("test_save_tmp");
}
static void test_to_disk_basic() {
const char* content = "Basic to_disk test";
EXPECT_TRUE(to_disk("test_to_disk_basic.txt", content, strlen(content)));
struct stat st;
EXPECT_EQ_INT(stat("test_to_disk_basic.txt", &st), 0);
EXPECT_EQ_INT((int)st.st_size, (int)strlen(content));
FILE* fp = fopen("test_to_disk_basic.txt", "rb");
EXPECT_NOT_NULL(fp);
char buf[100];
size_t nread = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
EXPECT_EQ_INT((int)nread, (int)strlen(content));
EXPECT_EQ_INT(memcmp(buf, content, strlen(content)), 0);
unlink("test_to_disk_basic.txt");
}
static void test_to_disk_creates_dirs() {
const char* content = "Nested dir test";
EXPECT_TRUE(to_disk("test_nested_tmp/nested/file.txt", content, strlen(content)));
struct stat st;
EXPECT_EQ_INT(stat("test_nested_tmp/nested/file.txt", &st), 0);
FILE* fp = fopen("test_nested_tmp/nested/file.txt", "rb");
EXPECT_NOT_NULL(fp);
char buf[100];
size_t nread = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
EXPECT_EQ_INT((int)nread, (int)strlen(content));
EXPECT_EQ_INT(memcmp(buf, content, strlen(content)), 0);
unlink("test_nested_tmp/nested/file.txt");
rmdir("test_nested_tmp/nested");
rmdir("test_nested_tmp");
}
static void test_file_content_to_buffer() {
const char* content = "Buffer content test";
EXPECT_TRUE(to_disk("test_buffer_file.txt", content, strlen(content)));
File* f = file_create("test_buffer_file.txt");
EXPECT_NOT_NULL(f);
f->data->size = strlen(content);
f->data->data = malloc(f->data->size);
EXPECT_NOT_NULL(f->data->data);
size_t bytes_read = file_content_to_buffer(f);
EXPECT_EQ_INT((int)bytes_read, (int)strlen(content));
EXPECT_EQ_INT(memcmp(f->data->data, content, strlen(content)), 0);
file_destroy(f);
unlink("test_buffer_file.txt");
}
static void test_file_send_receive() {
File* file = file_create("test_send_recv.txt");
EXPECT_NOT_NULL(file);
const char* content = "Hello, File Send!";
size_t len = strlen(content);
file->data->data = malloc(len);
EXPECT_NOT_NULL(file->data->data);
memcpy(file->data->data, content, len);
file->data->size = len;
Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false,
false, false, false, false, 0, false, 0);
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
File* received = file_receive(cfg, p[0]);
close(p[0]);
bool ok = true;
if (!received)
ok = false;
else {
if (!received->path || strcmp(received->path, "test_send_recv.txt") != 0)
ok = false;
if (!received->data || received->data->size != len)
ok = false;
else if (memcmp(received->data->data, content, len) != 0)
ok = false;
}
file_destroy(received);
config_delete(cfg);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
bool sent = file_send_single_calls(file, p[1], false, 0, true);
close(p[1]);
int status;
waitpid(pid, &status, 0);
file_destroy(file);
config_delete(cfg);
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
static void test_file_send_no_path() {
File* file = file_create("test_no_path.txt");
EXPECT_NOT_NULL(file);
const char* content = "No Path Data";
size_t len = strlen(content);
file->data->data = malloc(len);
EXPECT_NOT_NULL(file->data->data);
memcpy(file->data->data, content, len);
file->data->size = len;
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
Data* received = receive_data(p[0]);
close(p[0]);
bool ok = true;
if (!received)
ok = false;
else if (received->size != len)
ok = false;
else if (memcmp(received->data, content, len) != 0)
ok = false;
data_destroy(received);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
bool sent = file_send_single_calls(file, p[1], false, 0, false);
close(p[1]);
int status;
waitpid(pid, &status, 0);
file_destroy(file);
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
static void test_file_metadata_create() {
EXPECT_TRUE(to_disk("test_meta_file.txt", "metadata test", 13));
struct stat st;
EXPECT_EQ_INT(stat("test_meta_file.txt", &st), 0);
FileMetadata* m = file_metadata_create(&st);
EXPECT_NOT_NULL(m);
EXPECT_EQ_INT(m->mode, st.st_mode);
EXPECT_EQ_INT(m->uid, st.st_uid);
EXPECT_EQ_INT(m->gid, st.st_gid);
EXPECT_EQ_INT((int)m->mtime_sec, (int)st.st_mtime);
file_metadata_destroy(m);
unlink("test_meta_file.txt");
}
void test_file() {
test_file_create();
test_file_destroy_null();
test_file_destroy_normal();
test_file_load_data();
test_file_load_data_missing_file();
test_file_save_to_disk();
test_to_disk_basic();
test_to_disk_creates_dirs();
test_file_content_to_buffer();
if (!getenv("FASTSYNC_UNDER_VALGRIND")) {
// Fork tests are skipped under valgrind because the parent process runs
// orders of magnitude slower than the child (parent is instrumented, child
// is not), which causes pipe-based protocol handshake timeouts. The parent
// process itself has zero valgrind errors -- the failures are all in the
// forked children where inherited allocations are reported as leaks.
test_file_send_receive();
test_file_send_no_path();
}
test_file_metadata_create();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_FILE_H
#define TEST_FILE_H
void test_file();
#endif
+63
View File
@@ -0,0 +1,63 @@
#include "test_glob.h"
#include "utils.h"
#include "test_utils.h"
#include <string.h>
static void test_glob_exact_match() {
EXPECT_TRUE(glob_match("foo", "foo"));
}
static void test_glob_question_mark() {
EXPECT_TRUE(glob_match("f?o", "foo"));
EXPECT_FALSE(glob_match("f?o", "fo"));
}
static void test_glob_star() {
EXPECT_TRUE(glob_match("*.txt", "foo.txt"));
EXPECT_TRUE(glob_match("*.txt", "a.txt"));
}
static void test_glob_star_mid() {
EXPECT_TRUE(glob_match("f*o", "foo"));
EXPECT_TRUE(glob_match("f*o", "fxxo"));
EXPECT_FALSE(glob_match("f*o", "bar"));
}
static void test_glob_no_match() {
EXPECT_FALSE(glob_match("foo", "bar"));
}
static void test_glob_empty_pattern() {
EXPECT_TRUE(glob_match("", ""));
EXPECT_FALSE(glob_match("", "foo"));
}
static void test_glob_star_all() {
EXPECT_TRUE(glob_match("*", "anything"));
}
static void test_glob_slash_not_matched() {
EXPECT_FALSE(glob_match("f*o", "f/o"));
}
static void test_glob_complex() {
EXPECT_TRUE(glob_match("*.c", "main.c"));
EXPECT_FALSE(glob_match("*.c", "main.h"));
}
static void test_glob_question_star() {
EXPECT_TRUE(glob_match("?*.txt", "a.txt"));
}
void test_glob() {
test_glob_exact_match();
test_glob_question_mark();
test_glob_star();
test_glob_star_mid();
test_glob_no_match();
test_glob_empty_pattern();
test_glob_star_all();
test_glob_slash_not_matched();
test_glob_complex();
test_glob_question_star();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_GLOB_H
#define TEST_GLOB_H
void test_glob();
#endif
+141
View File
@@ -0,0 +1,141 @@
#include "test_metadata.h"
#include "metadata.h"
#include "protocol.h"
#include "test_utils.h"
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static void test_metadata_to_from_buf_roundtrip() {
FileMetadata original;
original.mode = 0755;
original.uid = 1000;
original.gid = 1000;
original.mtime_sec = 1234567890;
original.mtime_nsec = 500000000;
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
EXPECT_NOT_NULL(buf);
char* write_ptr = buf;
metadata_to_buf(&write_ptr, &original);
char* read_ptr = buf;
FileMetadata* result = metadata_from_buf(&read_ptr);
EXPECT_NOT_NULL(result);
EXPECT_EQ_INT(result->mode, 0755);
EXPECT_EQ_INT(result->uid, 1000);
EXPECT_EQ_INT(result->gid, 1000);
EXPECT_EQ_INT(result->mtime_sec, 1234567890);
EXPECT_EQ_INT(result->mtime_nsec, 500000000);
free(result);
free(buf);
}
static void test_metadata_to_buf_null() {
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
EXPECT_NOT_NULL(buf);
char* write_ptr = buf;
metadata_to_buf(&write_ptr, NULL);
char* read_ptr = buf;
int present;
memcpy(&present, read_ptr, sizeof(int));
EXPECT_EQ_INT(present, 0);
free(buf);
}
static void test_metadata_from_buf_null() {
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
EXPECT_NOT_NULL(buf);
int present = 0;
memcpy(buf, &present, sizeof(int));
char* read_ptr = buf;
const FileMetadata* result = metadata_from_buf(&read_ptr);
EXPECT_NULL(result);
free(buf);
}
static void test_metadata_send_receive_roundtrip() {
io_set_bwlimit(0);
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
FileMetadata original;
original.mode = 0755;
original.uid = 1000;
original.gid = 1000;
original.mtime_sec = 1234567890;
original.mtime_nsec = 500000000;
EXPECT_TRUE(metadata_send(p[1], &original));
int ok = 0;
FileMetadata* received = metadata_receive(p[0], &ok);
EXPECT_NOT_NULL(received);
EXPECT_EQ_INT(ok, 1);
EXPECT_EQ_INT(received->mode, 0755);
EXPECT_EQ_INT(received->uid, 1000);
EXPECT_EQ_INT(received->gid, 1000);
EXPECT_EQ_INT(received->mtime_sec, 1234567890);
EXPECT_EQ_INT(received->mtime_nsec, 500000000);
free(received);
close(p[0]);
close(p[1]);
}
static void test_metadata_send_null() {
io_set_bwlimit(0);
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
EXPECT_TRUE(metadata_send(p[1], NULL));
int ok = 0;
const FileMetadata* received = metadata_receive(p[0], &ok);
EXPECT_NULL(received);
EXPECT_EQ_INT(ok, 1);
close(p[0]);
close(p[1]);
}
static void test_file_restore_metadata() {
const char* path = "temp_meta_restore_test.txt";
const char* content = "test content";
EXPECT_TRUE(to_disk(path, content, strlen(content)));
FileMetadata m;
m.mode = 0644;
m.uid = getuid();
m.gid = getgid();
m.mtime_sec = 1234567890;
m.mtime_nsec = 0;
file_restore_metadata(path, &m);
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT(st.st_mode & 07777, 0644);
EXPECT_EQ_INT((int)st.st_mtime, 1234567890);
unlink(path);
}
void test_metadata() {
test_metadata_to_from_buf_roundtrip();
test_metadata_to_buf_null();
test_metadata_from_buf_null();
test_metadata_send_receive_roundtrip();
test_metadata_send_null();
test_file_restore_metadata();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_METADATA_H
#define TEST_METADATA_H
void test_metadata();
#endif
+120
View File
@@ -0,0 +1,120 @@
#include "test_property.h"
#include "test_utils.h"
#include "chunk.h"
#include "delta.h"
#include "data.h"
#include "compression.h"
#include "file.h"
#include "utils.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
static Data* random_data(int min_size, int max_size) {
int size = min_size + rand() % (max_size - min_size + 1);
char* buf = malloc(size);
for (int i = 0; i < size; i++)
buf[i] = (char)(rand() % 256);
return data_create(buf, size);
}
static void test_property_compress_roundtrip() {
for (int iter = 0; iter < 10; iter++) {
Data* original = random_data(1, 10000);
EXPECT_NOT_NULL(original);
size_t orig_size = original->size;
void* orig_copy = malloc(orig_size);
EXPECT_NOT_NULL(orig_copy);
memcpy(orig_copy, original->data, orig_size);
Data* compressed = data_compress(original, 3);
EXPECT_NOT_NULL(compressed);
Data* decompressed = data_decompress(compressed);
EXPECT_NOT_NULL(decompressed);
EXPECT_EQ_INT((int)decompressed->size, (int)orig_size);
EXPECT_EQ_INT(memcmp(decompressed->data, orig_copy, orig_size), 0);
free(orig_copy);
data_destroy(original);
data_destroy(compressed);
data_destroy(decompressed);
}
}
static void test_property_delta_roundtrip() {
for (int iter = 0; iter < 5; iter++) {
char old_data[4096], new_data[4096];
for (int i = 0; i < 4096; i++) {
old_data[i] = (char)(rand() % 256);
new_data[i] = old_data[i];
}
int num_changes = 1 + rand() % 10;
for (int c = 0; c < num_changes; c++) {
int offset = rand() % 4096;
new_data[offset] = (char)(rand() % 256);
}
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
EXPECT_NOT_NULL(sig);
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
EXPECT_NOT_NULL(delta);
void* result = delta_apply(old_data, 4096, delta, 1024);
EXPECT_NOT_NULL(result);
EXPECT_EQ_INT(memcmp(result, new_data, 4096), 0);
free(result);
delta_signature_destroy(sig);
delta_destroy(delta);
}
}
static void test_property_chunk_roundtrip() {
for (int iter = 0; iter < 5; iter++) {
char path[64];
snprintf(path, sizeof(path), "test_prop_chunk_%d.txt", iter);
int content_len = 1 + rand() % 4096;
char* content = malloc(content_len);
for (int i = 0; i < content_len; i++)
content[i] = (char)(rand() % 256);
to_disk(path, content, content_len);
struct stat st;
stat(path, &st);
File* f = file_create(path);
f->data->size = st.st_size;
file_load_data(f);
File* files[1] = {f};
Chunk* chunk = chunk_create(files, 1);
Data* serialized = chunk_serialize(chunk, false);
EXPECT_NOT_NULL(serialized);
Chunk* deserialized = chunk_deserialize(serialized, false);
EXPECT_NOT_NULL(deserialized);
EXPECT_EQ_INT(deserialized->element_count, 1);
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, content_len);
EXPECT_EQ_INT(memcmp(deserialized->items[0]->data->data, content, content_len), 0);
free(content);
data_destroy(serialized);
chunk_destroy(deserialized);
chunk_destroy(chunk);
unlink(path);
}
}
void test_property() {
srand(42);
test_property_compress_roundtrip();
test_property_delta_roundtrip();
test_property_chunk_roundtrip();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_PROPERTY_H
#define TEST_PROPERTY_H
void test_property();
#endif
+182
View File
@@ -0,0 +1,182 @@
#include "protocol.h"
#include "test_utils.h"
#include <limits.h>
#include <string.h>
#include <unistd.h>
static void test_send_receive_n_data() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
const char payload[] = "binary\x00test";
size_t len = sizeof(payload);
EXPECT_TRUE(send_n_data(0, payload, len));
char buf[64];
memset(buf, 0, sizeof(buf));
EXPECT_TRUE(receive_n_data(0, buf, len));
EXPECT_EQ_INT(memcmp(buf, payload, len), 0);
close(p[0]);
close(p[1]);
}
static void test_send_receive_n_data_zero() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
EXPECT_TRUE(send_n_data(0, "", 0));
char buf[4];
EXPECT_TRUE(receive_n_data(0, buf, 0));
close(p[0]);
close(p[1]);
}
static void test_send_receive_str() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
EXPECT_TRUE(send_str(0, ""));
char* received = receive_str(0);
EXPECT_NOT_NULL(received);
EXPECT_EQ_STR(received, "");
free(received);
close(p[0]);
close(p[1]);
}
static void test_send_receive_str_normal() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
EXPECT_TRUE(send_str(0, "Hello, Protocol!"));
char* received = receive_str(0);
EXPECT_NOT_NULL(received);
EXPECT_EQ_STR(received, "Hello, Protocol!");
free(received);
close(p[0]);
close(p[1]);
}
static void test_send_receive_data() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
unsigned char bin[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF};
void* buf = malloc(sizeof(bin));
EXPECT_NOT_NULL(buf);
memcpy(buf, bin, sizeof(bin));
Data* original = data_create(buf, sizeof(bin));
EXPECT_TRUE(send_data(0, original));
Data* received = receive_data(0);
EXPECT_NOT_NULL(received);
EXPECT_EQ_INT((int)received->size, (int)sizeof(bin));
EXPECT_EQ_INT(memcmp(received->data, bin, sizeof(bin)), 0);
data_destroy(original);
data_destroy(received);
close(p[0]);
close(p[1]);
}
static void test_send_receive_int() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
int val = 42;
EXPECT_TRUE(send_int(0, val));
int received = 0;
EXPECT_TRUE(receive_int(0, &received));
EXPECT_EQ_INT(received, 42);
val = 0;
EXPECT_TRUE(send_int(0, val));
EXPECT_TRUE(receive_int(0, &received));
EXPECT_EQ_INT(received, 0);
val = INT_MAX;
EXPECT_TRUE(send_int(0, val));
EXPECT_TRUE(receive_int(0, &received));
EXPECT_EQ_INT(received, INT_MAX);
close(p[0]);
close(p[1]);
}
static void test_send_receive_status() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
Status statuses[] = {STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT,
STATUS_CHUNK, STATUS_CHECK, STATUS_DELTA_SIGNATURE, STATUS_DELTA_DATA};
int count = sizeof(statuses) / sizeof(statuses[0]);
for (int i = 0; i < count; i++) {
EXPECT_TRUE(send_status(0, statuses[i]));
Status received = -1;
EXPECT_TRUE(receive_status(0, &received));
EXPECT_EQ_INT((int)received, (int)statuses[i]);
}
close(p[0]);
close(p[1]);
}
static void test_receive_n_data_truncated() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
close(p[1]);
char buf[32];
EXPECT_FALSE(receive_n_data(0, buf, 32));
close(p[0]);
}
static void test_receive_str_truncated() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
close(p[1]);
const char* received = receive_str(0);
EXPECT_NULL(received);
close(p[0]);
}
void test_protocol() {
test_send_receive_n_data();
test_send_receive_n_data_zero();
test_send_receive_str();
test_send_receive_str_normal();
test_send_receive_data();
test_send_receive_int();
test_send_receive_status();
test_receive_n_data_truncated();
test_receive_str_truncated();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_PROTOCOL_H
#define TEST_PROTOCOL_H
void test_protocol();
#endif
+215
View File
@@ -0,0 +1,215 @@
#include "test_robustness.h"
#include "test_utils.h"
#include "chunk.h"
#include "delta.h"
#include "data.h"
#include "file.h"
#include "protocol.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
static void test_chunk_deserialize_truncated() {
char* path = "test_rob_trunc.txt";
char* content = "hello";
to_disk(path, content, strlen(content));
struct stat st;
stat(path, &st);
File* f = file_create(path);
f->data->size = st.st_size;
file_load_data(f);
File* files[1] = {f};
Chunk* chunk = chunk_create(files, 1);
Data* serialized = chunk_serialize(chunk, false);
EXPECT_NOT_NULL(serialized);
size_t orig_size = serialized->size;
serialized->size = orig_size / 2;
const Chunk* result = chunk_deserialize(serialized, false);
EXPECT_NULL(result);
serialized->size = orig_size;
data_destroy(serialized);
chunk_destroy(chunk);
unlink(path);
}
static void test_chunk_deserialize_empty() {
unsigned char garbage[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB};
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
EXPECT_NOT_NULL(d);
memcpy(d->data, garbage, sizeof(garbage));
const Chunk* result = chunk_deserialize(d, false);
EXPECT_NULL(result);
data_destroy(d);
}
static void test_chunk_deserialize_garbage() {
unsigned char garbage[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA};
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
EXPECT_NOT_NULL(d);
memcpy(d->data, garbage, sizeof(garbage));
const Chunk* result = chunk_deserialize(d, false);
EXPECT_NULL(result);
data_destroy(d);
}
static void test_delta_deserialize_truncated() {
char old_data[4096], new_data[4096];
for (int i = 0; i < 4096; i++) {
old_data[i] = (char)(i % 256);
new_data[i] = old_data[i];
}
new_data[100] = 'X';
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
Data* serialized = delta_serialize(delta);
EXPECT_NOT_NULL(serialized);
serialized->size = 4;
const Delta* result = delta_deserialize(serialized);
EXPECT_NULL(result);
data_destroy(serialized);
delta_destroy(delta);
delta_signature_destroy(sig);
}
static void test_delta_deserialize_empty() {
char garbage[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
EXPECT_NOT_NULL(d);
memcpy(d->data, garbage, sizeof(garbage));
const Delta* result = delta_deserialize(d);
EXPECT_NULL(result);
data_destroy(d);
}
static void test_delta_deserialize_garbage() {
unsigned char garbage[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA};
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
EXPECT_NOT_NULL(d);
memcpy(d->data, garbage, sizeof(garbage));
const Delta* result = delta_deserialize(d);
EXPECT_NULL(result);
data_destroy(d);
}
static void test_delta_signature_deserialize_truncated() {
char old_data[4096];
for (int i = 0; i < 4096; i++)
old_data[i] = (char)(i % 256);
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
Data* serialized = delta_signature_serialize(sig);
EXPECT_NOT_NULL(serialized);
serialized->size = 4;
const DeltaSignature* result = delta_signature_deserialize(serialized);
EXPECT_NULL(result);
data_destroy(serialized);
delta_signature_destroy(sig);
}
static void test_delta_deserialize_truncated_instructions() {
// Create a real delta with 2 LITERAL instructions, serialize, then
// truncate after the header so the instruction-loop error paths are
// exercised (earlier tests with tiny buffers die at the 12-byte
// header guard and never reach the instruction decoder).
char old_data[4096], new_data[4096];
for (int i = 0; i < 4096; i++) {
old_data[i] = (char)(i % 256);
new_data[i] = old_data[i];
}
// Two small changes to produce 2 LITERAL instructions
new_data[100] = 'X';
new_data[200] = 'Y';
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
Data* serialized = delta_serialize(delta);
EXPECT_NOT_NULL(serialized);
// Truncate to include the header (12 bytes) + partial first instruction
serialized->size = 14;
const Delta* result = delta_deserialize(serialized);
EXPECT_NULL(result);
data_destroy(serialized);
delta_destroy(delta);
delta_signature_destroy(sig);
}
static void test_delta_apply_null() {
const void* result = delta_apply(NULL, 0, NULL, 0);
EXPECT_NULL(result);
}
static void test_protocol_receive_n_data_closed_pipe() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
close(p[1]);
char buf[32];
EXPECT_FALSE(receive_n_data(0, buf, 32));
close(p[0]);
}
static void test_receive_data_closed_pipe() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
close(p[1]);
const Data* result = receive_data(0);
EXPECT_NULL(result);
close(p[0]);
}
static void test_receive_str_closed_pipe() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
close(p[1]);
const char* result = receive_str(0);
EXPECT_NULL(result);
close(p[0]);
}
void test_robustness() {
test_chunk_deserialize_truncated();
test_chunk_deserialize_empty();
test_chunk_deserialize_garbage();
test_delta_deserialize_truncated();
test_delta_deserialize_empty();
test_delta_deserialize_garbage();
test_delta_deserialize_truncated_instructions();
test_delta_signature_deserialize_truncated();
test_delta_apply_null();
test_protocol_receive_n_data_closed_pipe();
test_receive_data_closed_pipe();
test_receive_str_closed_pipe();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_ROBUSTNESS_H
#define TEST_ROBUSTNESS_H
void test_robustness();
#endif
+214
View File
@@ -0,0 +1,214 @@
#include "test_stress.h"
#include "test_utils.h"
#include "queue.h"
#include <threads.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#define ITEMS_PER_PRODUCER 2500
#define NUM_PRODUCERS 4
#define NUM_CONSUMERS 4
#define TOTAL_ITEMS (ITEMS_PER_PRODUCER * NUM_PRODUCERS)
typedef struct {
Queue* q;
mtx_t* mutex;
cnd_t* cnd_empty;
cnd_t* cnd_full;
int producer_id;
} ProducerCtx;
typedef struct {
Queue* q;
mtx_t* mutex;
cnd_t* cnd_empty;
cnd_t* cnd_full;
volatile int* producers_remaining;
volatile bool* producers_done;
} ConsumerMPMC;
static int mpmc_producer_func(void* arg) {
ProducerCtx* ctx = (ProducerCtx*)arg;
for (int i = 1; i <= ITEMS_PER_PRODUCER; i++) {
int* val = malloc(sizeof(int));
*val = ctx->producer_id * ITEMS_PER_PRODUCER + i;
queue_enqueue_multithreaded(ctx->q, val, ctx->mutex, ctx->cnd_empty, ctx->cnd_full);
}
return 0;
}
static int mpmc_consumer_func(void* arg) {
ConsumerMPMC* ctx = (ConsumerMPMC*)arg;
while (true) {
int* val = (int*)queue_dequeue_multithreaded(ctx->q, ctx->mutex, ctx->cnd_empty, ctx->cnd_full,
(const bool*)ctx->producers_done);
if (val == NULL)
break;
free(val);
}
return 0;
}
static void test_queue_mpmc_stress() {
Queue* q = queue_create(16, NULL);
mtx_t mutex;
cnd_t cnd_empty;
cnd_t cnd_full;
mtx_init(&mutex, mtx_plain);
cnd_init(&cnd_empty);
cnd_init(&cnd_full);
volatile int producers_remaining = NUM_PRODUCERS;
volatile bool producers_done = false;
ConsumerMPMC cctx = {.q = q,
.mutex = &mutex,
.cnd_empty = &cnd_empty,
.cnd_full = &cnd_full,
.producers_remaining = &producers_remaining,
.producers_done = &producers_done};
thrd_t consumers[NUM_CONSUMERS];
for (int i = 0; i < NUM_CONSUMERS; i++) {
int res = thrd_create(&consumers[i], mpmc_consumer_func, &cctx);
EXPECT_EQ_INT(res, thrd_success);
}
ProducerCtx pctxs[NUM_PRODUCERS];
thrd_t producers[NUM_PRODUCERS];
for (int i = 0; i < NUM_PRODUCERS; i++) {
pctxs[i] = (ProducerCtx){
.q = q, .mutex = &mutex, .cnd_empty = &cnd_empty, .cnd_full = &cnd_full, .producer_id = i};
int res = thrd_create(&producers[i], mpmc_producer_func, &pctxs[i]);
EXPECT_EQ_INT(res, thrd_success);
}
for (int i = 0; i < NUM_PRODUCERS; i++) {
thrd_join(producers[i], NULL);
mtx_lock(&mutex);
producers_remaining--;
if (producers_remaining == 0)
producers_done = true;
cnd_broadcast(&cnd_empty);
mtx_unlock(&mutex);
}
for (int i = 0; i < NUM_CONSUMERS; i++) {
thrd_join(consumers[i], NULL);
}
EXPECT_TRUE(queue_is_empty(q));
queue_destroy(q);
mtx_destroy(&mutex);
cnd_destroy(&cnd_empty);
cnd_destroy(&cnd_full);
}
typedef struct {
Queue* q;
mtx_t* mutex;
cnd_t* cnd_empty;
cnd_t* cnd_full;
bool done;
int items_sent;
int items_received;
} BackpressureCtx;
static int bp_producer_func(void* arg) {
BackpressureCtx* ctx = (BackpressureCtx*)arg;
for (int i = 0; i < 5; i++) {
int* val = malloc(sizeof(int));
*val = i + 1;
queue_enqueue_multithreaded(ctx->q, val, ctx->mutex, ctx->cnd_empty, ctx->cnd_full);
ctx->items_sent++;
}
return 0;
}
static int bp_consumer_func(void* arg) {
BackpressureCtx* ctx = (BackpressureCtx*)arg;
while (ctx->items_received < 5) {
int* val = (int*)queue_dequeue_multithreaded(ctx->q, ctx->mutex, ctx->cnd_empty, ctx->cnd_full,
&ctx->done);
if (val == NULL)
break;
ctx->items_received++;
free(val);
}
return 0;
}
static void test_queue_backpressure() {
Queue* q = queue_create(1, NULL);
mtx_t mutex;
cnd_t cnd_empty;
cnd_t cnd_full;
mtx_init(&mutex, mtx_plain);
cnd_init(&cnd_empty);
cnd_init(&cnd_full);
BackpressureCtx ctx = {.q = q,
.mutex = &mutex,
.cnd_empty = &cnd_empty,
.cnd_full = &cnd_full,
.items_sent = 0,
.items_received = 0,
.done = false};
thrd_t producer, consumer;
int res;
res = thrd_create(&consumer, bp_consumer_func, &ctx);
EXPECT_EQ_INT(res, thrd_success);
res = thrd_create(&producer, bp_producer_func, &ctx);
EXPECT_EQ_INT(res, thrd_success);
thrd_join(producer, NULL);
mtx_lock(&mutex);
ctx.done = true;
cnd_signal(&cnd_empty);
mtx_unlock(&mutex);
thrd_join(consumer, NULL);
EXPECT_EQ_INT(ctx.items_sent, 5);
EXPECT_EQ_INT(ctx.items_received, 5);
EXPECT_TRUE(queue_is_empty(q));
queue_destroy(q);
mtx_destroy(&mutex);
cnd_destroy(&cnd_empty);
cnd_destroy(&cnd_full);
}
static void test_queue_rapid_create_destroy() {
for (int i = 0; i < 100; i++) {
Queue* q = queue_create(4, free);
EXPECT_NOT_NULL(q);
for (int j = 0; j < 3; j++) {
int* val = malloc(sizeof(int));
*val = j;
queue_enqueue(q, val);
}
while (!queue_is_empty(q)) {
void* v = queue_dequeue(q);
free(v);
}
queue_destroy(q);
}
}
void test_stress() {
test_queue_mpmc_stress();
test_queue_backpressure();
test_queue_rapid_create_destroy();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_STRESS_H
#define TEST_STRESS_H
void test_stress();
#endif