Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f73012b36 | |||
| ed6242cfc5 | |||
| a27f13d657 | |||
| eb8651e3d5 | |||
| e9ab84b5ea | |||
| 7c6e69ad81 | |||
| 66d994f01b | |||
| 44d0f99de9 | |||
| 8f77395b58 | |||
| afa5aeca37 | |||
| 5c686b0655 | |||
| c7f34eaf9d | |||
| bd17f50f9b | |||
| a4b35e136b | |||
| 02ceb6828b | |||
| 0f9e689e39 | |||
| a7bb6454a4 | |||
| 8a222bffa6 |
@@ -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"
|
||||
|
||||
@@ -3,3 +3,5 @@ data_copied
|
||||
test_data/
|
||||
__pycache__/
|
||||
build-asan
|
||||
coverage.info
|
||||
build-*/
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_DATA_H
|
||||
#define TEST_DATA_H
|
||||
|
||||
void test_data();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_FILE_H
|
||||
#define TEST_FILE_H
|
||||
|
||||
void test_file();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_GLOB_H
|
||||
#define TEST_GLOB_H
|
||||
|
||||
void test_glob();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_METADATA_H
|
||||
#define TEST_METADATA_H
|
||||
|
||||
void test_metadata();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_PROPERTY_H
|
||||
#define TEST_PROPERTY_H
|
||||
|
||||
void test_property();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_PROTOCOL_H
|
||||
#define TEST_PROTOCOL_H
|
||||
|
||||
void test_protocol();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_ROBUSTNESS_H
|
||||
#define TEST_ROBUSTNESS_H
|
||||
|
||||
void test_robustness();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_STRESS_H
|
||||
#define TEST_STRESS_H
|
||||
|
||||
void test_stress();
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user