mirror of
https://github.com/valkey-io/valkey.git
synced 2026-05-06 05:26:42 -04:00
Introduce GoogleTest for Valkey unit testing (#3241)
This PR adds GoogleTest (gtest) support to Valkey to enable writing modern unit tests,as mentioned in https://github.com/valkey-io/valkey/issues/2878 **Motivation**: GoogleTest provides richer assertions, test fixtures, mocking support, and improved diagnostics, helping improve test coverage and maintainability over time. For more details, see `src/gtest/README.md`. **Changes** This PR integrates the GoogleTest framework and migrates all existing C unit tests to GoogleTest. --------- Signed-off-by: Harry Lin <harrylhl@amazon.com> Signed-off-by: Madelyn Olson <madelyneolson@gmail.com> Signed-off-by: Alina Liu <liusalisa6363@gmail.com> Signed-off-by: Jacob Murphy <jkmurphy@google.com> Signed-off-by: Jim Brunner <brunnerj@amazon.com> Co-authored-by: Harry Lin <harrylhl@amazon.com> Co-authored-by: Jim Brunner <brunnerj@amazon.com> Co-authored-by: Madelyn Olson <madelyneolson@gmail.com> Co-authored-by: Jacob Murphy <jkmurphy@google.com> Co-authored-by: Alina Liu <liusalisa6363@gmail.com>
This commit is contained in:
@@ -71,3 +71,6 @@ ake = "ake"
|
||||
[type.tcl.extend-words]
|
||||
fo = "fo"
|
||||
tre = "tre"
|
||||
|
||||
[type.cpp.extend-words]
|
||||
fo = "fo"
|
||||
|
||||
@@ -36,7 +36,7 @@ Apply these standards to core engine C code. Do NOT apply to `deps/` (vendored d
|
||||
- **PR Scope:** Separate refactoring from functional changes for easier backporting.
|
||||
|
||||
## 5. Testing & Documentation
|
||||
- **Unit Tests:** Required for data structures in `src/unit/`. Test files should follow `test_*.c` naming.
|
||||
- **Unit Tests:** Required for data structures in `src/unit/`. Test files should follow `test_*.cpp` naming.
|
||||
- **Integration Tests:** Required for commands in `tests/`.
|
||||
- **Command Changes:** New/modified commands need corresponding updates in `src/commands/*.json`.
|
||||
- **New C Files:** Remind to update `CMakeLists.txt` when adding new `.c` source files.
|
||||
|
||||
+40
-10
@@ -35,7 +35,9 @@ jobs:
|
||||
- name: make
|
||||
# Fail build if there are warnings
|
||||
# build with TLS just for compilation coverage
|
||||
run: make -j4 all-with-unit-tests SERVER_CFLAGS='-Werror' BUILD_TLS=yes USE_FAST_FLOAT=yes USE_LIBBACKTRACE=yes
|
||||
run: |
|
||||
sudo apt-get install pkg-config libgtest-dev libgmock-dev
|
||||
make -j4 all-with-unit-tests SERVER_CFLAGS='-Werror' BUILD_TLS=yes USE_FAST_FLOAT=yes USE_LIBBACKTRACE=yes
|
||||
- name: test
|
||||
run: |
|
||||
sudo apt-get install tcl8.6 tclx
|
||||
@@ -50,7 +52,7 @@ jobs:
|
||||
if [[ ! -z "$dirty" ]]; then echo "$dirty"; exit 1; fi
|
||||
- name: unit tests
|
||||
run: |
|
||||
./src/valkey-unit-tests
|
||||
./src/unit/valkey-unit-gtests
|
||||
|
||||
test-ubuntu-latest-compatibility:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -62,6 +64,9 @@ jobs:
|
||||
- {version: "8.0.6", file: "valkey-8.0.6-noble-x86_64.tar.gz"}
|
||||
- {version: "8.1.4", file: "valkey-8.1.4-noble-x86_64.tar.gz"}
|
||||
steps:
|
||||
- name: Install modern CMake and gtests
|
||||
run: |
|
||||
sudo apt-get install pkg-config libgtest-dev libgmock-dev
|
||||
- name: Install libbacktrace
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -103,7 +108,7 @@ jobs:
|
||||
sudo apt-get install -y cmake libssl-dev
|
||||
mkdir -p build-release
|
||||
cd build-release
|
||||
cmake -DCMAKE_BUILD_TYPE=Release .. -DBUILD_TLS=yes -DBUILD_UNIT_TESTS=yes
|
||||
cmake -DCMAKE_BUILD_TYPE=Release .. -DBUILD_TLS=yes -DBUILD_UNIT_GTESTS=yes
|
||||
make -j$(nproc)
|
||||
- name: test
|
||||
run: |
|
||||
@@ -112,7 +117,7 @@ jobs:
|
||||
./build-release/runtest --verbose --tags -slow --dump-logs --tls
|
||||
- name: unit tests
|
||||
run: |
|
||||
./build-release/bin/valkey-unit-tests
|
||||
./build-release/bin/valkey-unit-gtests
|
||||
|
||||
test-sanitizer-address:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -128,7 +133,9 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: make
|
||||
# build with TLS module just for compilation coverage
|
||||
run: make -j4 all-with-unit-tests SANITIZER=address SERVER_CFLAGS='-Werror' BUILD_TLS=module USE_LIBBACKTRACE=yes
|
||||
run: |
|
||||
sudo apt-get install pkg-config libgtest-dev libgmock-dev
|
||||
make -j4 all-with-unit-tests SANITIZER=address SERVER_CFLAGS='-Werror' BUILD_TLS=module USE_LIBBACKTRACE=yes
|
||||
- name: testprep
|
||||
run: sudo apt-get install tcl8.6 tclx -y
|
||||
- name: test
|
||||
@@ -136,7 +143,8 @@ jobs:
|
||||
- name: module api test
|
||||
run: CFLAGS='-Werror' ./runtest-moduleapi --verbose --dump-logs
|
||||
- name: unit tests
|
||||
run: ./src/valkey-unit-tests
|
||||
run: |
|
||||
./src/unit/valkey-unit-gtests
|
||||
|
||||
test-rdma:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -220,9 +228,15 @@ jobs:
|
||||
- run: cd libbacktrace && ./configure && make && sudo make install
|
||||
- name: Checkout Valkey
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Install build dependencies
|
||||
run: brew install llvm googletest
|
||||
- name: make
|
||||
# Build with additional upcoming features
|
||||
run: make -j3 all-with-unit-tests SERVER_CFLAGS='-Werror' USE_FAST_FLOAT=yes USE_LIBBACKTRACE=yes
|
||||
run: |
|
||||
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
|
||||
export CC=/opt/homebrew/opt/llvm/bin/clang
|
||||
export CXX=/opt/homebrew/opt/llvm/bin/clang++
|
||||
make -j3 all-with-unit-tests SERVER_CFLAGS='-Werror' USE_FAST_FLOAT=yes USE_LIBBACKTRACE=yes LIBBACKTRACE_PREFIX=/usr/local
|
||||
|
||||
build-32bit:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -246,10 +260,26 @@ jobs:
|
||||
# also requires multilib support for g++ compiler i.e. "-multilib"
|
||||
# suffixed version of g++. g++-multilib generally includes libstdc++.
|
||||
# *cross version as well, but it is also added explicitly just in case.
|
||||
run: make -j4 SERVER_CFLAGS='-Werror' 32bit USE_FAST_FLOAT=yes USE_LIBBACKTRACE=yes LIBBACKTRACE_PREFIX=/usr/local/libbacktrace32
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libc6-dev-i386 libstdc++-11-dev-i386-cross gcc-multilib g++-multilib libgtest-dev
|
||||
mkdir -p /tmp/gtest32
|
||||
cd /tmp/gtest32
|
||||
cmake -B build32 \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_FLAGS="-m32" \
|
||||
-DCMAKE_CXX_FLAGS="-m32" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-m32" \
|
||||
/usr/src/googletest
|
||||
cmake --build build32 --parallel
|
||||
sudo cp build32/lib/*.a /usr/lib32/
|
||||
cd $GITHUB_WORKSPACE
|
||||
make -j4 SERVER_CFLAGS='-Werror' 32bit USE_FAST_FLOAT=yes USE_LIBBACKTRACE=yes LIBBACKTRACE_PREFIX=/usr/local/libbacktrace32 \
|
||||
GTEST_CFLAGS="-I/usr/src/googletest/googletest/include -I/usr/src/googletest/googlemock/include" \
|
||||
GTEST_LIBS="/usr/lib32/libgtest.a /usr/lib32/libgmock.a"
|
||||
- name: unit tests
|
||||
run: |
|
||||
./src/valkey-unit-tests
|
||||
./src/unit/valkey-unit-gtests
|
||||
|
||||
build-libc-malloc:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -278,7 +308,7 @@ jobs:
|
||||
path: libbacktrace
|
||||
- name: Build libbacktrace
|
||||
run: |
|
||||
dnf -y install epel-release gcc gcc-c++ make procps-ng which
|
||||
dnf -y install epel-release gcc gcc-c++ make procps-ng which git cmake
|
||||
cd libbacktrace && ./configure && make && make install
|
||||
- name: Checkout Valkey
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
# Run clang-format and capture the diff
|
||||
cd src
|
||||
shopt -s globstar
|
||||
clang-format-18 -i **/*.c **/*.h
|
||||
clang-format-18 -i **/*.c **/*.h **/*.cpp **/*.hpp
|
||||
# Capture the diff output
|
||||
DIFF=$(git diff)
|
||||
if [ ! -z "$DIFF" ]; then
|
||||
|
||||
+13
-13
@@ -128,7 +128,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --accurate
|
||||
run: ./src/unit/valkey-unit-gtests --accurate
|
||||
- name: install Redis OSS 6.2 server for compatibility testing
|
||||
run: |
|
||||
cd tests/tmp
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --accurate
|
||||
run: ./src/unit/valkey-unit-gtests --accurate
|
||||
test-ubuntu-jemalloc-fortify:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
path: libbacktrace
|
||||
- name: Build libbacktrace
|
||||
run: |
|
||||
apt-get update && apt-get install -y make gcc-13
|
||||
apt-get update && apt-get install -y make gcc-13 git cmake g++ python3
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100
|
||||
cd libbacktrace && ./configure && make && make install
|
||||
- name: make
|
||||
@@ -265,7 +265,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --accurate
|
||||
run: ./src/unit/valkey-unit-gtests --accurate
|
||||
test-ubuntu-libc-malloc:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
path: libbacktrace
|
||||
- name: Build libbacktrace
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install libc6-dev-i386
|
||||
sudo apt-get update && sudo apt-get install libc6-dev-i386 g++-multilib
|
||||
cd libbacktrace && ./configure && make && sudo make install
|
||||
- name: make
|
||||
run: make 32bit SERVER_CFLAGS='-Werror' USE_LIBBACKTRACE=yes
|
||||
@@ -423,7 +423,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --accurate
|
||||
run: ./src/unit/valkey-unit-gtests --accurate
|
||||
test-ubuntu-tls:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
@@ -796,7 +796,7 @@ jobs:
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: |
|
||||
valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/valkey-unit-tests --valgrind
|
||||
valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/unit/valkey-unit-gtests --valgrind
|
||||
if grep -q 0x err.txt; then cat err.txt; exit 1; fi
|
||||
test-valgrind-no-malloc-usable-size-test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -877,7 +877,7 @@ jobs:
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: |
|
||||
valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/valkey-unit-tests --valgrind
|
||||
valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/unit/valkey-unit-gtests --valgrind
|
||||
if grep -q 0x err.txt; then cat err.txt; exit 1; fi
|
||||
test-sanitizer-address:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -934,7 +934,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests
|
||||
run: ./src/unit/valkey-unit-gtests
|
||||
test-sanitizer-address-large-memory:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
@@ -972,7 +972,7 @@ jobs:
|
||||
sudo apt-get install tcl8.6 tclx -y
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --large-memory
|
||||
run: ./src/unit/valkey-unit-gtests --large-memory
|
||||
- name: large memory tests
|
||||
if: true && !contains(github.event.inputs.skiptests, 'valkey')
|
||||
run: ./runtest --accurate --verbose --dump-logs --clients 5 --large-memory --tags large-memory ${{github.event.inputs.test_args}}
|
||||
@@ -1034,7 +1034,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --accurate
|
||||
run: ./src/unit/valkey-unit-gtests --accurate
|
||||
test-sanitizer-undefined-large-memory:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
@@ -1072,7 +1072,7 @@ jobs:
|
||||
sudo apt-get install tcl8.6 tclx -y
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests --accurate --large-memory
|
||||
run: ./src/unit/valkey-unit-gtests --accurate --large-memory
|
||||
- name: large memory tests
|
||||
if: true && !contains(github.event.inputs.skiptests, 'valkey')
|
||||
run: ./runtest --accurate --verbose --dump-logs --clients 5 --large-memory --tags large-memory ${{github.event.inputs.test_args}}
|
||||
@@ -1130,7 +1130,7 @@ jobs:
|
||||
run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}}
|
||||
- name: unittest
|
||||
if: true && !contains(github.event.inputs.skiptests, 'unittest')
|
||||
run: ./src/valkey-unit-tests
|
||||
run: ./src/unit/valkey-unit-gtests
|
||||
test-ubuntu-lttng:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
|
||||
+3
-1
@@ -15,7 +15,7 @@ dump*.rdb
|
||||
*-cli
|
||||
*-sentinel
|
||||
*-server
|
||||
*-unit-tests
|
||||
*-unit-gtests
|
||||
doc-tools
|
||||
release
|
||||
misc/*
|
||||
@@ -56,3 +56,5 @@ build-debug/
|
||||
build-release/
|
||||
cmake-build-debug/
|
||||
cmake-build-release/
|
||||
__pycache__
|
||||
src/unit/.flags
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ endif ()
|
||||
|
||||
# Options
|
||||
option(BUILD_LUA "Build Valkey Lua scripting engine" ON)
|
||||
option(BUILD_UNIT_TESTS "Build valkey-unit-tests" OFF)
|
||||
option(BUILD_UNIT_GTESTS "Build valkey-unit-gtests" OFF)
|
||||
option(BUILD_TEST_MODULES "Build all test modules" OFF)
|
||||
option(BUILD_EXAMPLE_MODULES "Build example modules" OFF)
|
||||
|
||||
@@ -40,7 +40,7 @@ unset(CLANGPP CACHE)
|
||||
unset(CLANG CACHE)
|
||||
unset(BUILD_RDMA_MODULE CACHE)
|
||||
unset(BUILD_TLS_MODULE CACHE)
|
||||
unset(BUILD_UNIT_TESTS CACHE)
|
||||
unset(BUILD_UNIT_GTESTS CACHE)
|
||||
unset(BUILD_TEST_MODULES CACHE)
|
||||
unset(BUILD_EXAMPLE_MODULES CACHE)
|
||||
unset(USE_TLS CACHE)
|
||||
|
||||
@@ -342,7 +342,7 @@ Other options supported by Valkey's `CMake` build system:
|
||||
- `-DBUILD_RDMA=<no|module>` enable RDMA module build (only module mode supported). Default: `no`
|
||||
- `-DBUILD_MALLOC=<libc|jemalloc|tcmalloc|tcmalloc_minimal>` choose the allocator to use. Default on Linux: `jemalloc`, for other OS: `libc`
|
||||
- `-DBUILD_SANITIZER=<address|thread|undefined>` build with address sanitizer enabled. Default: disabled (no sanitizer)
|
||||
- `-DBUILD_UNIT_TESTS=[yes|no]` when set, the build will produce the executable `valkey-unit-tests`. Default: `no`
|
||||
- `-DBUILD_UNIT_GTESTS=[yes|no]` when set, the build will produce unit tests executable `valkey-unit-gtests`. Default: `no`
|
||||
- `-DBUILD_TEST_MODULES=[yes|no]` when set, the build will include the modules located under the `tests/modules` folder. Default: `no`
|
||||
- `-DBUILD_EXAMPLE_MODULES=[yes|no]` when set, the build will include the example modules located under the `src/modules` folder. Default: `no`
|
||||
|
||||
|
||||
@@ -330,22 +330,10 @@ if (PYTHON_EXE)
|
||||
COMMAND touch ${CMAKE_BINARY_DIR}/fmtargs_generated
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src")
|
||||
add_custom_target(generate_fmtargs_h DEPENDS ${CMAKE_BINARY_DIR}/fmtargs_generated)
|
||||
|
||||
# Rule for generating test_files.h
|
||||
message(STATUS "Adding target generate_test_files_h")
|
||||
file(GLOB UNIT_TEST_SRCS "${CMAKE_SOURCE_DIR}/src/unit/*.c")
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/test_files_generated
|
||||
DEPENDS "${UNIT_TEST_SRCS};${CMAKE_SOURCE_DIR}/utils/generate-unit-test-header.py"
|
||||
COMMAND ${PYTHON_EXE} ${CMAKE_SOURCE_DIR}/utils/generate-unit-test-header.py
|
||||
COMMAND touch ${CMAKE_BINARY_DIR}/test_files_generated
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src")
|
||||
add_custom_target(generate_test_files_h DEPENDS ${CMAKE_BINARY_DIR}/test_files_generated)
|
||||
else ()
|
||||
# Fake targets
|
||||
add_custom_target(generate_commands_def)
|
||||
add_custom_target(generate_fmtargs_h)
|
||||
add_custom_target(generate_test_files_h)
|
||||
endif ()
|
||||
|
||||
# Generate release.h file (always)
|
||||
|
||||
Vendored
+8
@@ -131,3 +131,11 @@ fast_float_c_interface: .make-prerequisites
|
||||
cd fast_float_c_interface && $(MAKE)
|
||||
|
||||
.PHONY: fast_float_c_interface
|
||||
|
||||
gtest-parallel: .make-prerequisites
|
||||
@printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
|
||||
@if [ ! -f gtest-parallel/gtest_parallel.py ]; then \
|
||||
echo "Downloading gtest-parallel..."; \
|
||||
rm -rf gtest-parallel; \
|
||||
git clone --depth 1 https://github.com/google/gtest-parallel.git gtest-parallel; \
|
||||
fi
|
||||
|
||||
Vendored
+38
-1
@@ -6,7 +6,8 @@ should be provided by the operating system.
|
||||
* **linenoise** is a readline replacement. It is developed by the same authors of Valkey but is managed as a separated project and updated as needed.
|
||||
* **lua** is Lua 5.1 with minor changes for security and additional libraries.
|
||||
* **hdr_histogram** Used for per-command latency tracking histograms.
|
||||
* **fast_float** is a replacement for strtod to convert strings to floats efficiently.
|
||||
* **fast_float** is a replacement for strtod to convert strings to floats efficiently.
|
||||
* **gtest-parallel** is a script for running googletest tests in parallel.
|
||||
|
||||
How to upgrade the above dependencies
|
||||
===
|
||||
@@ -121,3 +122,39 @@ To upgrade the library,
|
||||
2. cd fast_float
|
||||
3. Invoke "python3 ./script/amalgamate.py --output fast_float.h"
|
||||
4. Copy fast_float.h file to "deps/fast_float/".
|
||||
|
||||
gtest-parallel
|
||||
---
|
||||
|
||||
The `deps/gtest-parallel` directory is imported from the upstream
|
||||
https://github.com/google/gtest-parallel repository as a subtree snapshot (not a real Git subtree).
|
||||
|
||||
Current upstream version:
|
||||
- Upstream commit: `cd488bd` (from google/gtest-parallel)
|
||||
|
||||
Updating gtest-parallel
|
||||
|
||||
Run the following from the repository root.
|
||||
|
||||
1. Add the remote and fetch upstream:
|
||||
```sh
|
||||
git remote add gtest-parallel https://github.com/google/gtest-parallel.git
|
||||
git fetch gtest-parallel master
|
||||
```
|
||||
|
||||
2. Remove any previous import and commit (commit A):
|
||||
```sh
|
||||
rm -rf deps/gtest-parallel
|
||||
```
|
||||
|
||||
3. Update the subtree from upstream:
|
||||
```sh
|
||||
git subtree add --prefix=deps/gtest-parallel gtest-parallel master --squash
|
||||
```
|
||||
|
||||
4. Reset back to commit A with proper sign-off:
|
||||
```sh
|
||||
git reset --soft <commit-A-hash>
|
||||
```
|
||||
|
||||
5. Commit the changes.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.*.swp
|
||||
*.py[co]
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
[style]
|
||||
based_on_style = pep8
|
||||
indent_width = 2
|
||||
column_limit = 80
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
# How to Contribute
|
||||
|
||||
We'd love to accept your patches and contributions to this project. There are
|
||||
just a few small guidelines you need to follow.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution;
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
|
||||
## Community Guidelines
|
||||
|
||||
This project follows [Google's Open Source Community
|
||||
Guidelines](https://opensource.google.com/conduct/).
|
||||
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
# gtest-parallel
|
||||
|
||||
_This is not an official Google product._
|
||||
|
||||
`gtest-parallel` is a script that executes [Google
|
||||
Test](https://github.com/google/googletest) binaries in parallel, providing good
|
||||
speedup for single-threaded tests (on multi-core machines) and tests that do not
|
||||
run at 100% CPU (on single- or multi-core machines).
|
||||
|
||||
The script works by listing the tests of each binary, and then executing them on
|
||||
workers in separate processes. This works fine so long as the tests are self
|
||||
contained and do not share resources (reading data is fine, writing to the same
|
||||
log file is probably not).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
_For a full list of options, see `--help`._
|
||||
|
||||
$ ./gtest-parallel path/to/binary...
|
||||
|
||||
This shards all enabled tests across a number of workers, defaulting to the
|
||||
number of cores in the system. If your system uses Python 2, but you have no
|
||||
python2 binary, run `python gtest-parallel` instead of `./gtest-parallel`.
|
||||
|
||||
To run only a select set of tests, run:
|
||||
|
||||
$ ./gtest-parallel path/to/binary... --gtest_filter=Foo.*:Bar.*
|
||||
|
||||
This filter takes the same parameters as Google Test, so -Foo.\* can be used for
|
||||
test exclusion as well. This is especially useful for slow tests (that you're
|
||||
not working on), or tests that may not be able to run in parallel.
|
||||
|
||||
## Flakiness
|
||||
|
||||
Flaky tests (tests that do not deterministically pass or fail) often cause a lot
|
||||
of developer headache. A test that fails only 1% of the time can be very hard to
|
||||
detect as flaky, and even harder to convince yourself of having fixed.
|
||||
|
||||
`gtest-parallel` supports repeating individual tests (`--repeat=`), which can be
|
||||
very useful for flakiness testing. Some tests are also more flaky under high
|
||||
loads (especially tests that use realtime clocks), so raising the number of
|
||||
`--workers=` well above the number of available core can often cause contention
|
||||
and be fruitful for detecting flaky tests as well.
|
||||
|
||||
$ ./gtest-parallel out/{binary1,binary2,binary3} --repeat=1000 --workers=128
|
||||
|
||||
The above command repeats all tests inside `binary1`, `binary2` and `binary3`
|
||||
located in `out/`. The tests are run `1000` times each on `128` workers (this is
|
||||
more than I have cores on my machine anyways). This can often be done and then
|
||||
left overnight if you've no initial guess to which tests are flaky and which
|
||||
ones aren't. When you've figured out which tests are flaky (and want to fix
|
||||
them), repeat the above command with `--gtest_filter=` to only retry the flaky
|
||||
tests that you are fixing.
|
||||
|
||||
Note that repeated tests do run concurrently with themselves for efficiency, and
|
||||
as such they have problem writing to hard-coded files, even if they are only
|
||||
used by that single test. `tmpfile()` and similar library functions are often
|
||||
your friends here.
|
||||
|
||||
### Flakiness Summaries
|
||||
|
||||
Especially for disabled tests, you might wonder how stable a test seems before
|
||||
trying to enable it. `gtest-parallel` prints summaries (number of passed/failed
|
||||
tests) when `--repeat=` is used and at least one test fails. This can be used to
|
||||
generate passed/failed statistics per test. If no statistics are generated then
|
||||
all invocations tests are passing, congratulations!
|
||||
|
||||
For example, to try all disabled tests and see how stable they are:
|
||||
|
||||
$ ./gtest-parallel path/to/binary... -r1000 --gtest_filter=*.DISABLED_* --gtest_also_run_disabled_tests
|
||||
|
||||
Which will generate something like this at the end of the run:
|
||||
|
||||
SUMMARY:
|
||||
path/to/binary... Foo.DISABLED_Bar passed 0 / 1000 times.
|
||||
path/to/binary... FooBar.DISABLED_Baz passed 30 / 1000 times.
|
||||
path/to/binary... Foo.DISABLED_Baz passed 1000 / 1000 times.
|
||||
|
||||
## Running Tests Within Test Cases Sequentially
|
||||
|
||||
Sometimes tests within a single test case use globally-shared resources
|
||||
(hard-coded file paths, sockets, etc.) and cannot be run in parallel. Running
|
||||
such tests in parallel will either fail or be flaky (if they happen to not
|
||||
overlap during execution, they pass). So long as these resources are only shared
|
||||
within the same test case `gtest-parallel` can still provide some parallelism.
|
||||
|
||||
For such binaries where test cases are independent, `gtest-parallel` provides
|
||||
`--serialize_test_cases` that runs tests within the same test case sequentially.
|
||||
While generally not providing as much speedup as fully parallel test execution,
|
||||
this permits such binaries to partially benefit from parallel execution.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2017 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import gtest_parallel
|
||||
import sys
|
||||
|
||||
sys.exit(gtest_parallel.main())
|
||||
+955
@@ -0,0 +1,955 @@
|
||||
# Copyright 2013 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import errno
|
||||
from functools import total_ordering
|
||||
import gzip
|
||||
import io
|
||||
import json
|
||||
import multiprocessing
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
if sys.version_info.major >= 3:
|
||||
long = int
|
||||
import _pickle as cPickle
|
||||
import _thread as thread
|
||||
else:
|
||||
import cPickle
|
||||
import thread
|
||||
|
||||
from pickle import HIGHEST_PROTOCOL as PICKLE_HIGHEST_PROTOCOL
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import msvcrt
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
|
||||
# An object that catches SIGINT sent to the Python process and notices
|
||||
# if processes passed to wait() die by SIGINT (we need to look for
|
||||
# both of those cases, because pressing Ctrl+C can result in either
|
||||
# the main process or one of the subprocesses getting the signal).
|
||||
#
|
||||
# Before a SIGINT is seen, wait(p) will simply call p.wait() and
|
||||
# return the result. Once a SIGINT has been seen (in the main process
|
||||
# or a subprocess, including the one the current call is waiting for),
|
||||
# wait(p) will call p.terminate() and raise ProcessWasInterrupted.
|
||||
class SigintHandler(object):
|
||||
class ProcessWasInterrupted(Exception):
|
||||
pass
|
||||
|
||||
sigint_returncodes = {
|
||||
-signal.SIGINT, # Unix
|
||||
-1073741510, # Windows
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.__lock = threading.Lock()
|
||||
self.__processes = set()
|
||||
self.__got_sigint = False
|
||||
signal.signal(signal.SIGINT, lambda signal_num, frame: self.interrupt())
|
||||
|
||||
def __on_sigint(self):
|
||||
self.__got_sigint = True
|
||||
while self.__processes:
|
||||
try:
|
||||
self.__processes.pop().terminate()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def interrupt(self):
|
||||
with self.__lock:
|
||||
self.__on_sigint()
|
||||
|
||||
def got_sigint(self):
|
||||
with self.__lock:
|
||||
return self.__got_sigint
|
||||
|
||||
def wait(self, p, timeout_per_test):
|
||||
with self.__lock:
|
||||
if self.__got_sigint:
|
||||
p.terminate()
|
||||
self.__processes.add(p)
|
||||
try:
|
||||
code = p.wait(timeout_per_test)
|
||||
except subprocess.TimeoutExpired :
|
||||
p.terminate()
|
||||
self.__processes.remove(p)
|
||||
code = -errno.ETIME
|
||||
with self.__lock:
|
||||
self.__processes.discard(p)
|
||||
if code in self.sigint_returncodes:
|
||||
self.__on_sigint()
|
||||
if self.__got_sigint:
|
||||
raise self.ProcessWasInterrupted
|
||||
return code
|
||||
|
||||
|
||||
sigint_handler = SigintHandler()
|
||||
|
||||
|
||||
# Return the width of the terminal, or None if it couldn't be
|
||||
# determined (e.g. because we're not being run interactively).
|
||||
def term_width(out):
|
||||
if not out.isatty():
|
||||
return None
|
||||
try:
|
||||
p = subprocess.Popen(["stty", "size"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
if p.returncode != 0 or err:
|
||||
return None
|
||||
return int(out.split()[1])
|
||||
except (IndexError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
# Output transient and permanent lines of text. If several transient
|
||||
# lines are written in sequence, the new will overwrite the old. We
|
||||
# use this to ensure that lots of unimportant info (tests passing)
|
||||
# won't drown out important info (tests failing).
|
||||
class Outputter(object):
|
||||
def __init__(self, out_file):
|
||||
self.__out_file = out_file
|
||||
self.__previous_line_was_transient = False
|
||||
self.__width = term_width(out_file) # Line width, or None if not a tty.
|
||||
|
||||
def transient_line(self, msg):
|
||||
if self.__width is None:
|
||||
self.__out_file.write(msg + "\n")
|
||||
self.__out_file.flush()
|
||||
else:
|
||||
self.__out_file.write("\r" + msg[:self.__width].ljust(self.__width))
|
||||
self.__previous_line_was_transient = True
|
||||
|
||||
def flush_transient_output(self):
|
||||
if self.__previous_line_was_transient:
|
||||
self.__out_file.write("\n")
|
||||
self.__previous_line_was_transient = False
|
||||
|
||||
def permanent_line(self, msg):
|
||||
self.flush_transient_output()
|
||||
self.__out_file.write(msg + "\n")
|
||||
if self.__width is None:
|
||||
self.__out_file.flush()
|
||||
|
||||
|
||||
def get_save_file_path():
|
||||
"""Return path to file for saving transient data."""
|
||||
if sys.platform == 'win32':
|
||||
default_cache_path = os.path.join(os.path.expanduser('~'), 'AppData',
|
||||
'Local')
|
||||
cache_path = os.environ.get('LOCALAPPDATA', default_cache_path)
|
||||
else:
|
||||
# We don't use xdg module since it's not a standard.
|
||||
default_cache_path = os.path.join(os.path.expanduser('~'), '.cache')
|
||||
cache_path = os.environ.get('XDG_CACHE_HOME', default_cache_path)
|
||||
|
||||
if os.path.isdir(cache_path):
|
||||
return os.path.join(cache_path, 'gtest-parallel')
|
||||
else:
|
||||
sys.stderr.write('Directory {} does not exist'.format(cache_path))
|
||||
return os.path.join(os.path.expanduser('~'), '.gtest-parallel-times')
|
||||
|
||||
|
||||
@total_ordering
|
||||
class Task(object):
|
||||
"""Stores information about a task (single execution of a test).
|
||||
|
||||
This class stores information about the test to be executed (gtest binary and
|
||||
test name), and its result (log file, exit code and runtime).
|
||||
Each task is uniquely identified by the gtest binary, the test name and an
|
||||
execution number that increases each time the test is executed.
|
||||
Additionaly we store the last execution time, so that next time the test is
|
||||
executed, the slowest tests are run first.
|
||||
"""
|
||||
|
||||
def __init__(self, test_binary, test_name, test_command, execution_number,
|
||||
last_execution_time, output_dir):
|
||||
self.test_name = test_name
|
||||
self.output_dir = output_dir
|
||||
self.test_binary = test_binary
|
||||
self.test_command = test_command
|
||||
self.execution_number = execution_number
|
||||
self.last_execution_time = last_execution_time
|
||||
|
||||
self.exit_code = None
|
||||
self.runtime_ms = None
|
||||
|
||||
self.test_id = (test_binary, test_name)
|
||||
self.task_id = (test_binary, test_name, self.execution_number)
|
||||
|
||||
self.log_file = Task._logname(self.output_dir, self.test_binary, test_name,
|
||||
self.execution_number)
|
||||
|
||||
def __sorting_key(self):
|
||||
# Unseen or failing tests (both missing execution time) take precedence over
|
||||
# execution time. Tests are greater (seen as slower) when missing times so
|
||||
# that they are executed first.
|
||||
return (1 if self.last_execution_time is None else 0,
|
||||
self.last_execution_time)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__sorting_key() == other.__sorting_key()
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.__sorting_key() < other.__sorting_key()
|
||||
|
||||
@staticmethod
|
||||
def _normalize(string):
|
||||
return re.sub('[^A-Za-z0-9]', '_', string)
|
||||
|
||||
@staticmethod
|
||||
def _logname(output_dir, test_binary, test_name, execution_number):
|
||||
# Store logs to temporary files if there is no output_dir.
|
||||
if output_dir is None:
|
||||
(log_handle, log_name) = tempfile.mkstemp(prefix='gtest_parallel_',
|
||||
suffix=".log")
|
||||
os.close(log_handle)
|
||||
return log_name
|
||||
|
||||
log_name = '%s-%s-%d.log' % (Task._normalize(os.path.basename(test_binary)),
|
||||
Task._normalize(test_name), execution_number)
|
||||
|
||||
return os.path.join(output_dir, log_name)
|
||||
|
||||
def run(self, timeout_per_test):
|
||||
begin = time.time()
|
||||
with open(self.log_file, 'w') as log:
|
||||
task = subprocess.Popen(self.test_command, stdout=log, stderr=log)
|
||||
try:
|
||||
self.exit_code = sigint_handler.wait(task, timeout_per_test)
|
||||
except sigint_handler.ProcessWasInterrupted:
|
||||
thread.exit()
|
||||
self.runtime_ms = int(1000 * (time.time() - begin))
|
||||
self.last_execution_time = None if self.exit_code else self.runtime_ms
|
||||
|
||||
|
||||
class TaskManager(object):
|
||||
"""Executes the tasks and stores the passed, failed and interrupted tasks.
|
||||
|
||||
When a task is run, this class keeps track if it passed, failed or was
|
||||
interrupted. After a task finishes it calls the relevant functions of the
|
||||
Logger, TestResults and TestTimes classes, and in case of failure, retries the
|
||||
test as specified by the --retry_failed flag.
|
||||
"""
|
||||
|
||||
def __init__(self, times, logger, test_results, task_factory, times_to_retry,
|
||||
initial_execution_number):
|
||||
self.times = times
|
||||
self.logger = logger
|
||||
self.test_results = test_results
|
||||
self.task_factory = task_factory
|
||||
self.times_to_retry = times_to_retry
|
||||
self.initial_execution_number = initial_execution_number
|
||||
|
||||
self.global_exit_code = 0
|
||||
|
||||
self.passed = []
|
||||
self.failed = []
|
||||
self.started = {}
|
||||
self.timed_out = []
|
||||
self.execution_number = {}
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def __get_next_execution_number(self, test_id):
|
||||
with self.lock:
|
||||
next_execution_number = self.execution_number.setdefault(
|
||||
test_id, self.initial_execution_number)
|
||||
self.execution_number[test_id] += 1
|
||||
return next_execution_number
|
||||
|
||||
def __register_start(self, task):
|
||||
with self.lock:
|
||||
self.started[task.task_id] = task
|
||||
|
||||
def register_exit(self, task):
|
||||
self.logger.log_exit(task)
|
||||
self.times.record_test_time(task.test_binary, task.test_name,
|
||||
task.last_execution_time)
|
||||
if self.test_results:
|
||||
self.test_results.log(task.test_name, task.runtime_ms / 1000.0,
|
||||
task.exit_code)
|
||||
|
||||
with self.lock:
|
||||
self.started.pop(task.task_id)
|
||||
if task.exit_code == 0:
|
||||
self.passed.append(task)
|
||||
elif task.exit_code == -errno.ETIME:
|
||||
self.timed_out.append(task)
|
||||
else:
|
||||
self.failed.append(task)
|
||||
|
||||
def run_task(self, task, timeout_per_test):
|
||||
for try_number in range(self.times_to_retry + 1):
|
||||
self.__register_start(task)
|
||||
task.run(timeout_per_test)
|
||||
self.register_exit(task)
|
||||
|
||||
if task.exit_code == 0:
|
||||
break
|
||||
|
||||
if try_number < self.times_to_retry:
|
||||
execution_number = self.__get_next_execution_number(task.test_id)
|
||||
# We need create a new Task instance. Each task represents a single test
|
||||
# execution, with its own runtime, exit code and log file.
|
||||
task = self.task_factory(task.test_binary, task.test_name,
|
||||
task.test_command, execution_number,
|
||||
task.last_execution_time, task.output_dir)
|
||||
|
||||
with self.lock:
|
||||
if task.exit_code != 0:
|
||||
self.global_exit_code = task.exit_code
|
||||
|
||||
|
||||
class FilterFormat(object):
|
||||
def __init__(self, output_dir):
|
||||
if sys.stdout.isatty():
|
||||
# stdout needs to be unbuffered since the output is interactive.
|
||||
if isinstance(sys.stdout, io.TextIOWrapper):
|
||||
# workaround for https://bugs.python.org/issue17404
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.detach(),
|
||||
line_buffering=True,
|
||||
write_through=True,
|
||||
newline='\n')
|
||||
else:
|
||||
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
|
||||
|
||||
self.output_dir = output_dir
|
||||
|
||||
self.total_tasks = 0
|
||||
self.finished_tasks = 0
|
||||
self.out = Outputter(sys.stdout)
|
||||
self.stdout_lock = threading.Lock()
|
||||
|
||||
def move_to(self, destination_dir, tasks):
|
||||
if self.output_dir is None:
|
||||
return
|
||||
|
||||
destination_dir = os.path.join(self.output_dir, destination_dir)
|
||||
os.makedirs(destination_dir)
|
||||
for task in tasks:
|
||||
shutil.move(task.log_file, destination_dir)
|
||||
|
||||
def print_tests(self, message, tasks, print_try_number, print_test_command):
|
||||
self.out.permanent_line("%s (%s/%s):" %
|
||||
(message, len(tasks), self.total_tasks))
|
||||
for task in sorted(tasks):
|
||||
runtime_ms = 'Interrupted'
|
||||
if task.runtime_ms is not None:
|
||||
runtime_ms = '%d ms' % task.runtime_ms
|
||||
if print_test_command:
|
||||
try:
|
||||
cmd_str = " ".join(task.test_command)
|
||||
except TypeError:
|
||||
cmd_str = task.test_command
|
||||
self.out.permanent_line(
|
||||
"%11s: %s%s" %
|
||||
(runtime_ms, cmd_str,
|
||||
(" (try #%d)" % task.execution_number) if print_try_number else ""))
|
||||
else:
|
||||
self.out.permanent_line(
|
||||
"%11s: %s %s%s" %
|
||||
(runtime_ms, task.test_binary, task.test_name,
|
||||
(" (try #%d)" % task.execution_number) if print_try_number else ""))
|
||||
|
||||
def log_exit(self, task):
|
||||
with self.stdout_lock:
|
||||
self.finished_tasks += 1
|
||||
self.out.transient_line("[%d/%d] %s (%d ms)" %
|
||||
(self.finished_tasks, self.total_tasks,
|
||||
task.test_name, task.runtime_ms))
|
||||
if task.exit_code != 0:
|
||||
signal_name = None
|
||||
if task.exit_code < 0:
|
||||
try:
|
||||
signal_name = signal.Signals(-task.exit_code).name
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
with open(task.log_file) as f:
|
||||
for line in f.readlines():
|
||||
self.out.permanent_line(line.rstrip())
|
||||
if task.exit_code is None:
|
||||
self.out.permanent_line("[%d/%d] %s aborted after %d ms" %
|
||||
(self.finished_tasks, self.total_tasks,
|
||||
task.test_name, task.runtime_ms))
|
||||
elif task.exit_code == -errno.ETIME:
|
||||
self.out.permanent_line(
|
||||
"\033[31m[ TIMEOUT ]\033[0m %s timed out after %d s"
|
||||
% (task.test_name, task.runtime_ms/1000))
|
||||
elif signal_name is not None:
|
||||
self.out.permanent_line(
|
||||
"[%d/%d] %s killed by signal %s (%d ms)" %
|
||||
(self.finished_tasks, self.total_tasks, task.test_name,
|
||||
signal_name, task.runtime_ms))
|
||||
else:
|
||||
self.out.permanent_line(
|
||||
"[%d/%d] %s returned with exit code %d (%d ms)" %
|
||||
(self.finished_tasks, self.total_tasks, task.test_name,
|
||||
task.exit_code, task.runtime_ms))
|
||||
|
||||
if self.output_dir is None:
|
||||
# Try to remove the file 100 times (sleeping for 0.1 second in between).
|
||||
# This is a workaround for a process handle seemingly holding on to the
|
||||
# file for too long inside os.subprocess. This workaround is in place
|
||||
# until we figure out a minimal repro to report upstream (or a better
|
||||
# suspect) to prevent os.remove exceptions.
|
||||
num_tries = 100
|
||||
for i in range(num_tries):
|
||||
try:
|
||||
os.remove(task.log_file)
|
||||
except OSError as e:
|
||||
if e.errno is not errno.ENOENT:
|
||||
if i is num_tries - 1:
|
||||
self.out.permanent_line('Could not remove temporary log file: ' +
|
||||
str(e))
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
break
|
||||
|
||||
def log_tasks(self, total_tasks):
|
||||
self.total_tasks += total_tasks
|
||||
self.out.transient_line("[0/%d] Running tests..." % self.total_tasks)
|
||||
|
||||
def summarize(self, passed_tasks, failed_tasks, interrupted_tasks):
|
||||
stats = {}
|
||||
|
||||
def add_stats(stats, task, idx):
|
||||
task_key = (task.test_binary, task.test_name)
|
||||
if not task_key in stats:
|
||||
# (passed, failed, interrupted) task_key is added as tie breaker to get
|
||||
# alphabetic sorting on equally-stable tests
|
||||
stats[task_key] = [0, 0, 0, task_key]
|
||||
stats[task_key][idx] += 1
|
||||
|
||||
for task in passed_tasks:
|
||||
add_stats(stats, task, 0)
|
||||
for task in failed_tasks:
|
||||
add_stats(stats, task, 1)
|
||||
for task in interrupted_tasks:
|
||||
add_stats(stats, task, 2)
|
||||
|
||||
self.out.permanent_line("SUMMARY:")
|
||||
for task_key in sorted(stats, key=stats.__getitem__):
|
||||
(num_passed, num_failed, num_interrupted, _) = stats[task_key]
|
||||
(test_binary, task_name) = task_key
|
||||
total_runs = num_passed + num_failed + num_interrupted
|
||||
if num_passed == total_runs:
|
||||
continue
|
||||
self.out.permanent_line(" %s %s passed %d / %d times%s." %
|
||||
(test_binary, task_name, num_passed, total_runs,
|
||||
"" if num_interrupted == 0 else
|
||||
(" (%d interrupted)" % num_interrupted)))
|
||||
|
||||
def flush(self):
|
||||
self.out.flush_transient_output()
|
||||
|
||||
|
||||
class CollectTestResults(object):
|
||||
def __init__(self, json_dump_filepath):
|
||||
self.test_results_lock = threading.Lock()
|
||||
self.json_dump_file = open(json_dump_filepath, 'w')
|
||||
self.test_results = {
|
||||
"interrupted": False,
|
||||
"path_delimiter": ".",
|
||||
# Third version of the file format. See the link in the flag description
|
||||
# for details.
|
||||
"version": 3,
|
||||
"seconds_since_epoch": int(time.time()),
|
||||
"num_failures_by_type": {
|
||||
"PASS": 0,
|
||||
"FAIL": 0,
|
||||
"TIMEOUT": 0,
|
||||
},
|
||||
"tests": {},
|
||||
}
|
||||
|
||||
def log(self, test, runtime_seconds, exit_code):
|
||||
if exit_code is None:
|
||||
actual_result = "TIMEOUT"
|
||||
elif exit_code == 0:
|
||||
actual_result = "PASS"
|
||||
else:
|
||||
actual_result = "FAIL"
|
||||
with self.test_results_lock:
|
||||
self.test_results['num_failures_by_type'][actual_result] += 1
|
||||
results = self.test_results['tests']
|
||||
for name in test.split('.'):
|
||||
results = results.setdefault(name, {})
|
||||
|
||||
if results:
|
||||
results['actual'] += ' ' + actual_result
|
||||
results['times'].append(runtime_seconds)
|
||||
else: # This is the first invocation of the test
|
||||
results['actual'] = actual_result
|
||||
results['times'] = [runtime_seconds]
|
||||
results['time'] = runtime_seconds
|
||||
results['expected'] = 'PASS'
|
||||
|
||||
def dump_to_file_and_close(self):
|
||||
json.dump(self.test_results, self.json_dump_file)
|
||||
self.json_dump_file.close()
|
||||
|
||||
|
||||
# Record of test runtimes. Has built-in locking.
|
||||
class TestTimes(object):
|
||||
class LockedFile(object):
|
||||
def __init__(self, filename, mode):
|
||||
self._filename = filename
|
||||
self._mode = mode
|
||||
self._fo = None
|
||||
|
||||
def __enter__(self):
|
||||
self._fo = open(self._filename, self._mode)
|
||||
|
||||
# Regardless of opening mode we always seek to the beginning of file.
|
||||
# This simplifies code working with LockedFile and also ensures that
|
||||
# we lock (and unlock below) always the same region in file on win32.
|
||||
self._fo.seek(0)
|
||||
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
# We are locking here fixed location in file to use it as
|
||||
# an exclusive lock on entire file.
|
||||
msvcrt.locking(self._fo.fileno(), msvcrt.LK_LOCK, 1)
|
||||
else:
|
||||
fcntl.flock(self._fo.fileno(), fcntl.LOCK_EX)
|
||||
except IOError:
|
||||
self._fo.close()
|
||||
raise
|
||||
|
||||
return self._fo
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
# Flush any buffered data to disk. This is needed to prevent race
|
||||
# condition which happens from the moment of releasing file lock
|
||||
# till closing the file.
|
||||
self._fo.flush()
|
||||
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
self._fo.seek(0)
|
||||
msvcrt.locking(self._fo.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
else:
|
||||
fcntl.flock(self._fo.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
self._fo.close()
|
||||
|
||||
return exc_value is None
|
||||
|
||||
def __init__(self, save_file):
|
||||
"Create new object seeded with saved test times from the given file."
|
||||
self.__times = {} # (test binary, test name) -> runtime in ms
|
||||
|
||||
# Protects calls to record_test_time(); other calls are not
|
||||
# expected to be made concurrently.
|
||||
self.__lock = threading.Lock()
|
||||
|
||||
try:
|
||||
with TestTimes.LockedFile(save_file, 'rb') as fd:
|
||||
times = TestTimes.__read_test_times_file(fd)
|
||||
except IOError:
|
||||
# We couldn't obtain the lock.
|
||||
return
|
||||
|
||||
# Discard saved times if the format isn't right.
|
||||
if type(times) is not dict:
|
||||
return
|
||||
for ((test_binary, test_name), runtime) in times.items():
|
||||
if (type(test_binary) is not str or type(test_name) is not str
|
||||
or type(runtime) not in {int, long, type(None)}):
|
||||
return
|
||||
|
||||
self.__times = times
|
||||
|
||||
def get_test_time(self, binary, testname):
|
||||
"""Return the last duration for the given test as an integer number of
|
||||
milliseconds, or None if the test failed or if there's no record for it."""
|
||||
return self.__times.get((binary, testname), None)
|
||||
|
||||
def record_test_time(self, binary, testname, runtime_ms):
|
||||
"""Record that the given test ran in the specified number of
|
||||
milliseconds. If the test failed, runtime_ms should be None."""
|
||||
with self.__lock:
|
||||
self.__times[(binary, testname)] = runtime_ms
|
||||
|
||||
def write_to_file(self, save_file):
|
||||
"Write all the times to file."
|
||||
try:
|
||||
with TestTimes.LockedFile(save_file, 'a+b') as fd:
|
||||
times = TestTimes.__read_test_times_file(fd)
|
||||
|
||||
if times is None:
|
||||
times = self.__times
|
||||
else:
|
||||
times.update(self.__times)
|
||||
|
||||
# We erase data from file while still holding a lock to it. This
|
||||
# way reading old test times and appending new ones are atomic
|
||||
# for external viewer.
|
||||
fd.seek(0)
|
||||
fd.truncate()
|
||||
with gzip.GzipFile(fileobj=fd, mode='wb') as gzf:
|
||||
cPickle.dump(times, gzf, PICKLE_HIGHEST_PROTOCOL)
|
||||
except IOError:
|
||||
pass # ignore errors---saving the times isn't that important
|
||||
|
||||
@staticmethod
|
||||
def __read_test_times_file(fd):
|
||||
try:
|
||||
with gzip.GzipFile(fileobj=fd, mode='rb') as gzf:
|
||||
times = cPickle.load(gzf)
|
||||
except Exception:
|
||||
# File doesn't exist, isn't readable, is malformed---whatever.
|
||||
# Just ignore it.
|
||||
return None
|
||||
else:
|
||||
return times
|
||||
|
||||
|
||||
def find_tests(binaries, additional_args, options, times):
|
||||
test_count = 0
|
||||
tasks = []
|
||||
for test_binary in binaries:
|
||||
command = [test_binary] + additional_args
|
||||
if options.gtest_also_run_disabled_tests:
|
||||
command += ['--gtest_also_run_disabled_tests']
|
||||
|
||||
list_command = command + ['--gtest_list_tests']
|
||||
if options.gtest_filter != '':
|
||||
list_command += ['--gtest_filter=' + options.gtest_filter]
|
||||
|
||||
try:
|
||||
test_list = subprocess.check_output(list_command,
|
||||
stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
sys.exit("%s: %s\n%s" % (test_binary, str(e), e.output))
|
||||
|
||||
try:
|
||||
test_list = test_list.split('\n')
|
||||
except TypeError:
|
||||
# subprocess.check_output() returns bytes in python3
|
||||
test_list = test_list.decode(sys.stdout.encoding).split('\n')
|
||||
|
||||
command += ['--gtest_color=' + options.gtest_color]
|
||||
|
||||
test_group = ''
|
||||
for line in test_list:
|
||||
if not line.strip():
|
||||
continue
|
||||
if line[0] != " ":
|
||||
# Remove comments for typed tests and strip whitespace.
|
||||
test_group = line.split('#')[0].strip()
|
||||
continue
|
||||
# Remove comments for parameterized tests and strip whitespace.
|
||||
line = line.split('#')[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
test_name = test_group + line
|
||||
if not options.gtest_also_run_disabled_tests and 'DISABLED_' in test_name:
|
||||
continue
|
||||
|
||||
# Skip PRE_ tests which are used by Chromium.
|
||||
if '.PRE_' in test_name:
|
||||
continue
|
||||
|
||||
last_execution_time = times.get_test_time(test_binary, test_name)
|
||||
if options.failed and last_execution_time is not None:
|
||||
continue
|
||||
|
||||
test_command = command + ['--gtest_filter=' + test_name]
|
||||
if (test_count - options.shard_index) % options.shard_count == 0:
|
||||
for execution_number in range(options.repeat):
|
||||
tasks.append(
|
||||
Task(test_binary, test_name, test_command, execution_number + 1,
|
||||
last_execution_time, options.output_dir))
|
||||
|
||||
test_count += 1
|
||||
|
||||
# Sort the tasks to run the slowest tests first, so that faster ones can be
|
||||
# finished in parallel.
|
||||
return sorted(tasks, reverse=True)
|
||||
|
||||
|
||||
def execute_tasks(tasks, pool_size, task_manager, timeout_seconds,
|
||||
timeout_per_test, serialize_test_cases):
|
||||
class WorkerFn(object):
|
||||
def __init__(self, tasks, running_groups, timeout_per_test):
|
||||
self.tasks = tasks
|
||||
self.running_groups = running_groups
|
||||
self.timeout_per_test = timeout_per_test
|
||||
self.task_lock = threading.Lock()
|
||||
|
||||
def __call__(self):
|
||||
while True:
|
||||
with self.task_lock:
|
||||
for task_id in range(len(self.tasks)):
|
||||
task = self.tasks[task_id]
|
||||
|
||||
if self.running_groups is not None:
|
||||
test_group = task.test_name.split('.')[0]
|
||||
if test_group in self.running_groups:
|
||||
# Try to find other non-running test group.
|
||||
continue
|
||||
else:
|
||||
self.running_groups.add(test_group)
|
||||
|
||||
del self.tasks[task_id]
|
||||
break
|
||||
else:
|
||||
# Either there is no tasks left or number or remaining test
|
||||
# cases (groups) is less than number or running threads.
|
||||
return
|
||||
|
||||
task_manager.run_task(task, self.timeout_per_test)
|
||||
|
||||
if self.running_groups is not None:
|
||||
with self.task_lock:
|
||||
self.running_groups.remove(test_group)
|
||||
|
||||
def start_daemon(func):
|
||||
t = threading.Thread(target=func)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
return t
|
||||
|
||||
timeout = None
|
||||
try:
|
||||
if timeout_seconds:
|
||||
timeout = threading.Timer(timeout_seconds, sigint_handler.interrupt)
|
||||
timeout.start()
|
||||
running_groups = set() if serialize_test_cases else None
|
||||
worker_fn = WorkerFn(tasks, running_groups, timeout_per_test)
|
||||
workers = [start_daemon(worker_fn) for _ in range(pool_size)]
|
||||
for worker in workers:
|
||||
worker.join()
|
||||
finally:
|
||||
if timeout:
|
||||
timeout.cancel()
|
||||
for task in list(task_manager.started.values()):
|
||||
task.runtime_ms = timeout_seconds * 1000
|
||||
task_manager.register_exit(task)
|
||||
|
||||
|
||||
def default_options_parser():
|
||||
parser = optparse.OptionParser(
|
||||
usage='usage: %prog [options] binary [binary ...] -- [additional args]')
|
||||
|
||||
parser.add_option('-d',
|
||||
'--output_dir',
|
||||
type='string',
|
||||
default=None,
|
||||
help='Output directory for test logs. Logs will be '
|
||||
'available under gtest-parallel-logs/, so '
|
||||
'--output_dir=/tmp will results in all logs being '
|
||||
'available under /tmp/gtest-parallel-logs/.')
|
||||
parser.add_option('-r',
|
||||
'--repeat',
|
||||
type='int',
|
||||
default=1,
|
||||
help='Number of times to execute all the tests.')
|
||||
parser.add_option('--retry_failed',
|
||||
type='int',
|
||||
default=0,
|
||||
help='Number of times to repeat failed tests.')
|
||||
parser.add_option('--failed',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='run only failed and new tests')
|
||||
parser.add_option('-w',
|
||||
'--workers',
|
||||
type='int',
|
||||
default=multiprocessing.cpu_count(),
|
||||
help='number of workers to spawn')
|
||||
parser.add_option('--gtest_color',
|
||||
type='string',
|
||||
default='yes',
|
||||
help='color output')
|
||||
parser.add_option('--gtest_filter',
|
||||
type='string',
|
||||
default='',
|
||||
help='test filter')
|
||||
parser.add_option('--gtest_also_run_disabled_tests',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='run disabled tests too')
|
||||
parser.add_option(
|
||||
'--print_test_times',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='list the run time of each test at the end of execution')
|
||||
parser.add_option(
|
||||
'--print_test_command',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Print full test command instead of name')
|
||||
parser.add_option('--shard_count',
|
||||
type='int',
|
||||
default=1,
|
||||
help='total number of shards (for sharding test execution '
|
||||
'between multiple machines)')
|
||||
parser.add_option('--shard_index',
|
||||
type='int',
|
||||
default=0,
|
||||
help='zero-indexed number identifying this shard (for '
|
||||
'sharding test execution between multiple machines)')
|
||||
parser.add_option(
|
||||
'--dump_json_test_results',
|
||||
type='string',
|
||||
default=None,
|
||||
help='Saves the results of the tests as a JSON machine-'
|
||||
'readable file. The format of the file is specified at '
|
||||
'https://www.chromium.org/developers/the-json-test-results-format')
|
||||
parser.add_option('--timeout',
|
||||
type='int',
|
||||
default=None,
|
||||
help='Interrupt all remaining processes after the given '
|
||||
'time (in seconds).')
|
||||
parser.add_option('--timeout_per_test',
|
||||
type='int',
|
||||
default=None,
|
||||
help='Interrupt single processes after the given '
|
||||
'time (in seconds).')
|
||||
parser.add_option('--serialize_test_cases',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run tests from the same test '
|
||||
'case in parallel.')
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
# Remove additional arguments (anything after --).
|
||||
additional_args = []
|
||||
|
||||
for i in range(len(sys.argv)):
|
||||
if sys.argv[i] == '--':
|
||||
additional_args = sys.argv[i + 1:]
|
||||
sys.argv = sys.argv[:i]
|
||||
break
|
||||
|
||||
parser = default_options_parser()
|
||||
(options, binaries) = parser.parse_args()
|
||||
|
||||
if (options.output_dir is not None and not os.path.isdir(options.output_dir)):
|
||||
parser.error('--output_dir value must be an existing directory, '
|
||||
'current value is "%s"' % options.output_dir)
|
||||
|
||||
# Append gtest-parallel-logs to log output, this is to avoid deleting user
|
||||
# data if an user passes a directory where files are already present. If a
|
||||
# user specifies --output_dir=Docs/, we'll create Docs/gtest-parallel-logs
|
||||
# and clean that directory out on startup, instead of nuking Docs/.
|
||||
if options.output_dir:
|
||||
options.output_dir = os.path.join(options.output_dir, 'gtest-parallel-logs')
|
||||
|
||||
if binaries == []:
|
||||
parser.print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
if options.shard_count < 1:
|
||||
parser.error("Invalid number of shards: %d. Must be at least 1." %
|
||||
options.shard_count)
|
||||
if not (0 <= options.shard_index < options.shard_count):
|
||||
parser.error("Invalid shard index: %d. Must be between 0 and %d "
|
||||
"(less than the number of shards)." %
|
||||
(options.shard_index, options.shard_count - 1))
|
||||
|
||||
# Check that all test binaries have an unique basename. That way we can ensure
|
||||
# the logs are saved to unique files even when two different binaries have
|
||||
# common tests.
|
||||
unique_binaries = set(os.path.basename(binary) for binary in binaries)
|
||||
assert len(unique_binaries) == len(binaries), (
|
||||
"All test binaries must have an unique basename.")
|
||||
|
||||
if options.output_dir:
|
||||
# Remove files from old test runs.
|
||||
if os.path.isdir(options.output_dir):
|
||||
shutil.rmtree(options.output_dir)
|
||||
# Create directory for test log output.
|
||||
try:
|
||||
os.makedirs(options.output_dir)
|
||||
except OSError as e:
|
||||
# Ignore errors if this directory already exists.
|
||||
if e.errno != errno.EEXIST or not os.path.isdir(options.output_dir):
|
||||
raise e
|
||||
|
||||
test_results = None
|
||||
if options.dump_json_test_results is not None:
|
||||
test_results = CollectTestResults(options.dump_json_test_results)
|
||||
|
||||
save_file = get_save_file_path()
|
||||
|
||||
times = TestTimes(save_file)
|
||||
logger = FilterFormat(options.output_dir)
|
||||
|
||||
task_manager = TaskManager(times, logger, test_results, Task,
|
||||
options.retry_failed, options.repeat + 1)
|
||||
|
||||
tasks = find_tests(binaries, additional_args, options, times)
|
||||
logger.log_tasks(len(tasks))
|
||||
execute_tasks(tasks, options.workers, task_manager, options.timeout,
|
||||
options.timeout_per_test, options.serialize_test_cases)
|
||||
|
||||
print_try_number = options.retry_failed > 0 or options.repeat > 1
|
||||
if task_manager.passed:
|
||||
logger.move_to('passed', task_manager.passed)
|
||||
if options.print_test_times:
|
||||
logger.print_tests('PASSED TESTS', task_manager.passed, print_try_number, options.print_test_command)
|
||||
|
||||
if task_manager.failed:
|
||||
logger.print_tests('FAILED TESTS', task_manager.failed, print_try_number, options.print_test_command)
|
||||
logger.move_to('failed', task_manager.failed)
|
||||
|
||||
if task_manager.timed_out:
|
||||
logger.print_tests('TIMED OUT TESTS', task_manager.timed_out, print_try_number, options.print_test_command)
|
||||
logger.move_to('timed_out', task_manager.timed_out)
|
||||
|
||||
if task_manager.started:
|
||||
logger.print_tests('INTERRUPTED TESTS', task_manager.started.values(),
|
||||
print_try_number, options.print_test_command)
|
||||
logger.move_to('interrupted', task_manager.started.values())
|
||||
|
||||
if options.repeat > 1 and (task_manager.failed or task_manager.started):
|
||||
logger.summarize(task_manager.passed, task_manager.failed,
|
||||
task_manager.started.values())
|
||||
|
||||
logger.flush()
|
||||
times.write_to_file(save_file)
|
||||
if test_results:
|
||||
test_results.dump_to_file_and_close()
|
||||
|
||||
if sigint_handler.got_sigint():
|
||||
return -signal.SIGINT
|
||||
|
||||
return task_manager.global_exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
# Copyright 2017 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import collections
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class LoggerMock(object):
|
||||
def __init__(self, test_lib):
|
||||
self.test_lib = test_lib
|
||||
self.runtimes = collections.defaultdict(list)
|
||||
self.exit_codes = collections.defaultdict(list)
|
||||
self.last_execution_times = collections.defaultdict(list)
|
||||
self.execution_numbers = collections.defaultdict(list)
|
||||
|
||||
def log_exit(self, task):
|
||||
self.runtimes[task.test_id].append(task.runtime_ms)
|
||||
self.exit_codes[task.test_id].append(task.exit_code)
|
||||
self.last_execution_times[task.test_id].append(task.last_execution_time)
|
||||
self.execution_numbers[task.test_id].append(task.execution_number)
|
||||
|
||||
def assertRecorded(self, test_id, expected, retries):
|
||||
self.test_lib.assertIn(test_id, self.runtimes)
|
||||
self.test_lib.assertListEqual(expected['runtime_ms'][:retries],
|
||||
self.runtimes[test_id])
|
||||
self.test_lib.assertListEqual(expected['exit_code'][:retries],
|
||||
self.exit_codes[test_id])
|
||||
self.test_lib.assertListEqual(expected['last_execution_time'][:retries],
|
||||
self.last_execution_times[test_id])
|
||||
self.test_lib.assertListEqual(expected['execution_number'][:retries],
|
||||
self.execution_numbers[test_id])
|
||||
|
||||
|
||||
class TestTimesMock(object):
|
||||
def __init__(self, test_lib, test_data=None):
|
||||
self.test_lib = test_lib
|
||||
self.test_data = test_data or {}
|
||||
self.last_execution_times = collections.defaultdict(list)
|
||||
|
||||
def record_test_time(self, test_binary, test_name, last_execution_time):
|
||||
test_id = (test_binary, test_name)
|
||||
self.last_execution_times[test_id].append(last_execution_time)
|
||||
|
||||
def get_test_time(self, test_binary, test_name):
|
||||
test_group, test = test_name.split('.')
|
||||
return self.test_data.get(test_binary, {}).get(test_group,
|
||||
{}).get(test, None)
|
||||
|
||||
def assertRecorded(self, test_id, expected, retries):
|
||||
self.test_lib.assertIn(test_id, self.last_execution_times)
|
||||
self.test_lib.assertListEqual(expected['last_execution_time'][:retries],
|
||||
self.last_execution_times[test_id])
|
||||
|
||||
|
||||
class TestResultsMock(object):
|
||||
def __init__(self, test_lib):
|
||||
self.results = []
|
||||
self.test_lib = test_lib
|
||||
|
||||
def log(self, test_name, runtime_ms, actual_result):
|
||||
self.results.append((test_name, runtime_ms, actual_result))
|
||||
|
||||
def assertRecorded(self, test_id, expected, retries):
|
||||
test_results = [
|
||||
(test_id[1], runtime_ms / 1000.0, exit_code)
|
||||
for runtime_ms, exit_code in zip(expected['runtime_ms'][:retries],
|
||||
expected['exit_code'][:retries])
|
||||
]
|
||||
for test_result in test_results:
|
||||
self.test_lib.assertIn(test_result, self.results)
|
||||
|
||||
|
||||
class TaskManagerMock(object):
|
||||
def __init__(self):
|
||||
self.running_groups = []
|
||||
self.check_lock = threading.Lock()
|
||||
|
||||
self.had_running_parallel_groups = False
|
||||
self.total_tasks_run = 0
|
||||
self.started = {}
|
||||
|
||||
def __register_start(self, task):
|
||||
self.started[task.task_id] = task
|
||||
|
||||
def register_exit(self, task):
|
||||
self.started.pop(task.task_id)
|
||||
|
||||
def run_task(self, task, timeout_per_test):
|
||||
self.__register_start(task)
|
||||
test_group = task.test_name.split('.')[0]
|
||||
|
||||
with self.check_lock:
|
||||
self.total_tasks_run += 1
|
||||
if test_group in self.running_groups:
|
||||
self.had_running_parallel_groups = True
|
||||
self.running_groups.append(test_group)
|
||||
|
||||
# Delay as if real test were run.
|
||||
time.sleep(0.001)
|
||||
|
||||
with self.check_lock:
|
||||
self.running_groups.remove(test_group)
|
||||
|
||||
|
||||
class TaskMockFactory(object):
|
||||
def __init__(self, test_data):
|
||||
self.data = test_data
|
||||
self.passed = []
|
||||
self.failed = []
|
||||
|
||||
def get_task(self, test_id, execution_number=0):
|
||||
task = TaskMock(test_id, execution_number, self.data[test_id])
|
||||
if task.exit_code == 0:
|
||||
self.passed.append(task)
|
||||
else:
|
||||
self.failed.append(task)
|
||||
return task
|
||||
|
||||
def __call__(self, test_binary, test_name, test_command, execution_number,
|
||||
last_execution_time, output_dir):
|
||||
return self.get_task((test_binary, test_name), execution_number)
|
||||
|
||||
|
||||
class TaskMock(object):
|
||||
def __init__(self, test_id, execution_number, test_data):
|
||||
self.test_id = test_id
|
||||
self.execution_number = execution_number
|
||||
|
||||
self.runtime_ms = test_data['runtime_ms'][execution_number]
|
||||
self.exit_code = test_data['exit_code'][execution_number]
|
||||
self.last_execution_time = (
|
||||
test_data['last_execution_time'][execution_number])
|
||||
if 'log_file' in test_data:
|
||||
self.log_file = test_data['log_file'][execution_number]
|
||||
else:
|
||||
self.log_file = None
|
||||
self.test_command = None
|
||||
self.output_dir = None
|
||||
|
||||
self.test_binary = test_id[0]
|
||||
self.test_name = test_id[1]
|
||||
self.task_id = (test_id[0], test_id[1], execution_number)
|
||||
|
||||
def run(self, timeout_per_test):
|
||||
pass
|
||||
|
||||
|
||||
class SubprocessMock(object):
|
||||
def __init__(self, test_data=None):
|
||||
self._test_data = test_data
|
||||
self.last_invocation = None
|
||||
|
||||
def __call__(self, command, **kwargs):
|
||||
self.last_invocation = command
|
||||
binary = command[0]
|
||||
test_list = []
|
||||
tests_for_binary = sorted(self._test_data.get(binary, {}).items())
|
||||
for test_group, tests in tests_for_binary:
|
||||
test_list.append(test_group + ".")
|
||||
for test in sorted(tests):
|
||||
test_list.append(" " + test)
|
||||
return '\n'.join(test_list)
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2017 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import contextlib
|
||||
import os.path
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
import gtest_parallel
|
||||
|
||||
from gtest_parallel_mocks import LoggerMock
|
||||
from gtest_parallel_mocks import SubprocessMock
|
||||
from gtest_parallel_mocks import TestTimesMock
|
||||
from gtest_parallel_mocks import TestResultsMock
|
||||
from gtest_parallel_mocks import TaskManagerMock
|
||||
from gtest_parallel_mocks import TaskMockFactory
|
||||
from gtest_parallel_mocks import TaskMock
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def guard_temp_dir():
|
||||
try:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
yield temp_dir
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def guard_temp_subdir(temp_dir, *path):
|
||||
assert path, 'Path should not be empty'
|
||||
|
||||
try:
|
||||
temp_subdir = os.path.join(temp_dir, *path)
|
||||
os.makedirs(temp_subdir)
|
||||
yield temp_subdir
|
||||
finally:
|
||||
shutil.rmtree(os.path.join(temp_dir, path[0]))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def guard_patch_module(import_name, new_val):
|
||||
def patch(module, names, val):
|
||||
if len(names) == 1:
|
||||
old = getattr(module, names[0])
|
||||
setattr(module, names[0], val)
|
||||
return old
|
||||
else:
|
||||
return patch(getattr(module, names[0]), names[1:], val)
|
||||
|
||||
try:
|
||||
old_val = patch(gtest_parallel, import_name.split('.'), new_val)
|
||||
yield old_val
|
||||
finally:
|
||||
patch(gtest_parallel, import_name.split('.'), old_val)
|
||||
|
||||
|
||||
class TestTaskManager(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.passing_task = (('fake_binary', 'Fake.PassingTest'), {
|
||||
'runtime_ms': [10],
|
||||
'exit_code': [0],
|
||||
'last_execution_time': [10],
|
||||
})
|
||||
self.failing_task = (('fake_binary', 'Fake.FailingTest'), {
|
||||
'runtime_ms': [20, 30, 40],
|
||||
'exit_code': [1, 1, 1],
|
||||
'last_execution_time': [None, None, None],
|
||||
})
|
||||
self.fails_once_then_succeeds = (('another_binary', 'Fake.Test.FailOnce'), {
|
||||
'runtime_ms': [21, 22],
|
||||
'exit_code': [1, 0],
|
||||
'last_execution_time': [None, 22],
|
||||
})
|
||||
self.fails_twice_then_succeeds = (('yet_another_binary',
|
||||
'Fake.Test.FailTwice'), {
|
||||
'runtime_ms': [23, 25, 24],
|
||||
'exit_code': [1, 1, 0],
|
||||
'last_execution_time':
|
||||
[None, None, 24],
|
||||
})
|
||||
|
||||
def execute_tasks(self, tasks, retries, expected_exit_code):
|
||||
repeat = 1
|
||||
|
||||
times = TestTimesMock(self)
|
||||
logger = LoggerMock(self)
|
||||
test_results = TestResultsMock(self)
|
||||
|
||||
task_mock_factory = TaskMockFactory(dict(tasks))
|
||||
task_manager = gtest_parallel.TaskManager(times, logger, test_results,
|
||||
task_mock_factory, retries,
|
||||
repeat)
|
||||
|
||||
for test_id, expected in tasks:
|
||||
task = task_mock_factory.get_task(test_id)
|
||||
task_manager.run_task(task, timeout_per_test=30)
|
||||
expected['execution_number'] = list(range(len(expected['exit_code'])))
|
||||
|
||||
logger.assertRecorded(test_id, expected, retries + 1)
|
||||
times.assertRecorded(test_id, expected, retries + 1)
|
||||
test_results.assertRecorded(test_id, expected, retries + 1)
|
||||
|
||||
self.assertEqual(len(task_manager.started), 0)
|
||||
self.assertListEqual(
|
||||
sorted(task.task_id for task in task_manager.passed),
|
||||
sorted(task.task_id for task in task_mock_factory.passed))
|
||||
self.assertListEqual(
|
||||
sorted(task.task_id for task in task_manager.failed),
|
||||
sorted(task.task_id for task in task_mock_factory.failed))
|
||||
|
||||
self.assertEqual(task_manager.global_exit_code, expected_exit_code)
|
||||
|
||||
def test_passing_task_succeeds(self):
|
||||
self.execute_tasks(tasks=[self.passing_task],
|
||||
retries=0,
|
||||
expected_exit_code=0)
|
||||
|
||||
def test_failing_task_fails(self):
|
||||
self.execute_tasks(tasks=[self.failing_task],
|
||||
retries=0,
|
||||
expected_exit_code=1)
|
||||
|
||||
def test_failing_task_fails_even_with_retries(self):
|
||||
self.execute_tasks(tasks=[self.failing_task],
|
||||
retries=2,
|
||||
expected_exit_code=1)
|
||||
|
||||
def test_executing_passing_and_failing_fails(self):
|
||||
# Executing both a faling test and a passing one should make gtest-parallel
|
||||
# fail, no matter if the failing task is run first or last.
|
||||
self.execute_tasks(tasks=[self.failing_task, self.passing_task],
|
||||
retries=2,
|
||||
expected_exit_code=1)
|
||||
|
||||
self.execute_tasks(tasks=[self.passing_task, self.failing_task],
|
||||
retries=2,
|
||||
expected_exit_code=1)
|
||||
|
||||
def test_task_succeeds_with_one_retry(self):
|
||||
# Executes test and retries once. The first run should fail and the second
|
||||
# succeed, so gtest-parallel should succeed.
|
||||
self.execute_tasks(tasks=[self.fails_once_then_succeeds],
|
||||
retries=1,
|
||||
expected_exit_code=0)
|
||||
|
||||
def test_task_fails_with_one_retry(self):
|
||||
# Executes test and retries once, not enough for the test to start passing,
|
||||
# so gtest-parallel should return an error.
|
||||
self.execute_tasks(tasks=[self.fails_twice_then_succeeds],
|
||||
retries=1,
|
||||
expected_exit_code=1)
|
||||
|
||||
def test_runner_succeeds_when_all_tasks_eventually_succeeds(self):
|
||||
# Executes the test and retries twice. One test should pass in the first
|
||||
# attempt, another should take two runs, and the last one should take three
|
||||
# runs. All tests should succeed, so gtest-parallel should succeed too.
|
||||
self.execute_tasks(tasks=[
|
||||
self.passing_task, self.fails_once_then_succeeds,
|
||||
self.fails_twice_then_succeeds
|
||||
],
|
||||
retries=2,
|
||||
expected_exit_code=0)
|
||||
|
||||
|
||||
class TestSaveFilePath(unittest.TestCase):
|
||||
class StreamMock(object):
|
||||
def write(*args):
|
||||
# Suppress any output.
|
||||
pass
|
||||
|
||||
def test_get_save_file_path_unix(self):
|
||||
with guard_temp_dir() as temp_dir, \
|
||||
guard_patch_module('os.path.expanduser', lambda p: temp_dir), \
|
||||
guard_patch_module('sys.stderr', TestSaveFilePath.StreamMock()), \
|
||||
guard_patch_module('sys.platform', 'darwin'):
|
||||
with guard_patch_module('os.environ', {}), \
|
||||
guard_temp_subdir(temp_dir, '.cache'):
|
||||
self.assertEqual(os.path.join(temp_dir, '.cache', 'gtest-parallel'),
|
||||
gtest_parallel.get_save_file_path())
|
||||
|
||||
with guard_patch_module('os.environ', {'XDG_CACHE_HOME': temp_dir}):
|
||||
self.assertEqual(os.path.join(temp_dir, 'gtest-parallel'),
|
||||
gtest_parallel.get_save_file_path())
|
||||
|
||||
with guard_patch_module('os.environ',
|
||||
{'XDG_CACHE_HOME': os.path.realpath(__file__)}):
|
||||
self.assertEqual(os.path.join(temp_dir, '.gtest-parallel-times'),
|
||||
gtest_parallel.get_save_file_path())
|
||||
|
||||
def test_get_save_file_path_win32(self):
|
||||
with guard_temp_dir() as temp_dir, \
|
||||
guard_patch_module('os.path.expanduser', lambda p: temp_dir), \
|
||||
guard_patch_module('sys.stderr', TestSaveFilePath.StreamMock()), \
|
||||
guard_patch_module('sys.platform', 'win32'):
|
||||
with guard_patch_module('os.environ', {}), \
|
||||
guard_temp_subdir(temp_dir, 'AppData', 'Local'):
|
||||
self.assertEqual(
|
||||
os.path.join(temp_dir, 'AppData', 'Local', 'gtest-parallel'),
|
||||
gtest_parallel.get_save_file_path())
|
||||
|
||||
with guard_patch_module('os.environ', {'LOCALAPPDATA': temp_dir}):
|
||||
self.assertEqual(os.path.join(temp_dir, 'gtest-parallel'),
|
||||
gtest_parallel.get_save_file_path())
|
||||
|
||||
with guard_patch_module('os.environ',
|
||||
{'LOCALAPPDATA': os.path.realpath(__file__)}):
|
||||
self.assertEqual(os.path.join(temp_dir, '.gtest-parallel-times'),
|
||||
gtest_parallel.get_save_file_path())
|
||||
|
||||
|
||||
class TestSerializeTestCases(unittest.TestCase):
|
||||
def _execute_tasks(self, max_number_of_test_cases,
|
||||
max_number_of_tests_per_test_case, max_number_of_repeats,
|
||||
max_number_of_workers, timeout_per_test, serialize_test_cases):
|
||||
tasks = []
|
||||
for test_case in range(max_number_of_test_cases):
|
||||
for test_name in range(max_number_of_tests_per_test_case):
|
||||
# All arguments for gtest_parallel.Task except for test_name are fake.
|
||||
test_name = 'TestCase{}.test{}'.format(test_case, test_name)
|
||||
|
||||
for execution_number in range(random.randint(1, max_number_of_repeats)):
|
||||
tasks.append(
|
||||
gtest_parallel.Task('path/to/binary', test_name,
|
||||
['path/to/binary', '--gtest_filter=*'],
|
||||
execution_number + 1, None, 'path/to/output'))
|
||||
|
||||
expected_tasks_number = len(tasks)
|
||||
|
||||
task_manager = TaskManagerMock()
|
||||
|
||||
gtest_parallel.execute_tasks(tasks, max_number_of_workers, task_manager,
|
||||
None, timeout_per_test, serialize_test_cases)
|
||||
|
||||
self.assertEqual(serialize_test_cases,
|
||||
not task_manager.had_running_parallel_groups)
|
||||
self.assertEqual(expected_tasks_number, task_manager.total_tasks_run)
|
||||
|
||||
def test_running_parallel_test_cases_without_repeats(self):
|
||||
self._execute_tasks(max_number_of_test_cases=4,
|
||||
max_number_of_tests_per_test_case=32,
|
||||
max_number_of_repeats=1,
|
||||
max_number_of_workers=16,
|
||||
timeout_per_test=30,
|
||||
serialize_test_cases=True)
|
||||
|
||||
def test_running_parallel_test_cases_with_repeats(self):
|
||||
self._execute_tasks(max_number_of_test_cases=4,
|
||||
max_number_of_tests_per_test_case=32,
|
||||
max_number_of_repeats=4,
|
||||
max_number_of_workers=16,
|
||||
timeout_per_test=30,
|
||||
serialize_test_cases=True)
|
||||
|
||||
def test_running_parallel_tests(self):
|
||||
self._execute_tasks(max_number_of_test_cases=4,
|
||||
max_number_of_tests_per_test_case=128,
|
||||
max_number_of_repeats=1,
|
||||
max_number_of_workers=16,
|
||||
timeout_per_test=30,
|
||||
serialize_test_cases=False)
|
||||
|
||||
|
||||
class TestTestTimes(unittest.TestCase):
|
||||
def test_race_in_test_times_load_save(self):
|
||||
max_number_of_workers = 8
|
||||
max_number_of_read_write_cycles = 64
|
||||
test_times_file_name = 'test_times.pickle'
|
||||
|
||||
def start_worker(save_file):
|
||||
def test_times_worker():
|
||||
thread_id = threading.current_thread().ident
|
||||
path_to_binary = 'path/to/binary' + hex(thread_id)
|
||||
|
||||
for cnt in range(max_number_of_read_write_cycles):
|
||||
times = gtest_parallel.TestTimes(save_file)
|
||||
|
||||
threads_test_times = [
|
||||
binary for (binary, _) in times._TestTimes__times.keys()
|
||||
if binary.startswith(path_to_binary)
|
||||
]
|
||||
|
||||
self.assertEqual(cnt, len(threads_test_times))
|
||||
|
||||
times.record_test_time('{}-{}'.format(path_to_binary, cnt),
|
||||
'TestFoo.testBar', 1000)
|
||||
|
||||
times.write_to_file(save_file)
|
||||
|
||||
self.assertEqual(
|
||||
1000,
|
||||
times.get_test_time('{}-{}'.format(path_to_binary, cnt),
|
||||
'TestFoo.testBar'))
|
||||
self.assertIsNone(
|
||||
times.get_test_time('{}-{}'.format(path_to_binary, cnt), 'baz'))
|
||||
|
||||
t = threading.Thread(target=test_times_worker)
|
||||
t.start()
|
||||
return t
|
||||
|
||||
with guard_temp_dir() as temp_dir:
|
||||
try:
|
||||
workers = [
|
||||
start_worker(os.path.join(temp_dir, test_times_file_name))
|
||||
for _ in range(max_number_of_workers)
|
||||
]
|
||||
finally:
|
||||
for worker in workers:
|
||||
worker.join()
|
||||
|
||||
|
||||
class TestTimeoutTestCases(unittest.TestCase):
|
||||
def test_task_timeout(self):
|
||||
timeout = 1
|
||||
pool_size = 1
|
||||
timeout_per_test = 30
|
||||
task = gtest_parallel.Task('test_binary', 'test_name', ['test_command'], 1,
|
||||
None, 'output_dir')
|
||||
tasks = [task]
|
||||
|
||||
task_manager = TaskManagerMock()
|
||||
gtest_parallel.execute_tasks(tasks, pool_size, task_manager, timeout, timeout_per_test, True)
|
||||
|
||||
self.assertEqual(1, task_manager.total_tasks_run)
|
||||
self.assertEqual(None, task.exit_code)
|
||||
self.assertEqual(1000, task.runtime_ms)
|
||||
|
||||
|
||||
class TestTask(unittest.TestCase):
|
||||
def test_log_file_names(self):
|
||||
def root():
|
||||
return 'C:\\' if sys.platform == 'win32' else '/'
|
||||
|
||||
self.assertEqual(os.path.join('.', 'bin-Test_case-100.log'),
|
||||
gtest_parallel.Task._logname('.', 'bin', 'Test.case', 100))
|
||||
|
||||
self.assertEqual(
|
||||
os.path.join('..', 'a', 'b', 'bin-Test_case_2-1.log'),
|
||||
gtest_parallel.Task._logname(os.path.join('..', 'a', 'b'),
|
||||
os.path.join('..', 'bin'), 'Test.case/2',
|
||||
1))
|
||||
|
||||
self.assertEqual(
|
||||
os.path.join('..', 'a', 'b', 'bin-Test_case_2-5.log'),
|
||||
gtest_parallel.Task._logname(os.path.join('..', 'a', 'b'),
|
||||
os.path.join(root(), 'c', 'd', 'bin'),
|
||||
'Test.case/2', 5))
|
||||
|
||||
self.assertEqual(
|
||||
os.path.join(root(), 'a', 'b', 'bin-Instantiation_Test_case_2-3.log'),
|
||||
gtest_parallel.Task._logname(os.path.join(root(), 'a', 'b'),
|
||||
os.path.join('..', 'c', 'bin'),
|
||||
'Instantiation/Test.case/2', 3))
|
||||
|
||||
self.assertEqual(
|
||||
os.path.join(root(), 'a', 'b', 'bin-Test_case-1.log'),
|
||||
gtest_parallel.Task._logname(os.path.join(root(), 'a', 'b'),
|
||||
os.path.join(root(), 'c', 'd', 'bin'),
|
||||
'Test.case', 1))
|
||||
|
||||
def test_logs_to_temporary_files_without_output_dir(self):
|
||||
log_file = gtest_parallel.Task._logname(None, None, None, None)
|
||||
self.assertEqual(tempfile.gettempdir(), os.path.dirname(log_file))
|
||||
os.remove(log_file)
|
||||
|
||||
def _execute_run_test(self, run_test_body, interrupt_test):
|
||||
def popen_mock(*_args, **_kwargs):
|
||||
return None
|
||||
|
||||
class SigHandlerMock(object):
|
||||
class ProcessWasInterrupted(Exception):
|
||||
pass
|
||||
|
||||
def wait(*_args):
|
||||
if interrupt_test:
|
||||
raise SigHandlerMock.ProcessWasInterrupted()
|
||||
|
||||
return 42
|
||||
|
||||
with guard_temp_dir() as temp_dir, \
|
||||
guard_patch_module('subprocess.Popen', popen_mock), \
|
||||
guard_patch_module('sigint_handler', SigHandlerMock()), \
|
||||
guard_patch_module('thread.exit', lambda: None):
|
||||
run_test_body(temp_dir)
|
||||
|
||||
def test_run_normal_task(self):
|
||||
def run_test(temp_dir):
|
||||
task = gtest_parallel.Task('fake/binary', 'test', ['fake/binary'], 1,
|
||||
None, temp_dir)
|
||||
|
||||
self.assertFalse(os.path.isfile(task.log_file))
|
||||
|
||||
task.run(timeout_per_test=30)
|
||||
|
||||
self.assertTrue(os.path.isfile(task.log_file))
|
||||
self.assertEqual(42, task.exit_code)
|
||||
|
||||
self._execute_run_test(run_test, False)
|
||||
|
||||
def test_run_interrupted_task_with_transient_log(self):
|
||||
def run_test(_):
|
||||
task = gtest_parallel.Task('fake/binary', 'test', ['fake/binary'], 1,
|
||||
None, None)
|
||||
|
||||
self.assertTrue(os.path.isfile(task.log_file))
|
||||
|
||||
task.run(timeout_per_test=30)
|
||||
|
||||
self.assertTrue(os.path.isfile(task.log_file))
|
||||
self.assertIsNone(task.exit_code)
|
||||
|
||||
self._execute_run_test(run_test, True)
|
||||
|
||||
|
||||
class TestFilterFormat(unittest.TestCase):
|
||||
def _execute_test(self, test_body, drop_output):
|
||||
class StdoutMock(object):
|
||||
def isatty(*_args):
|
||||
return False
|
||||
|
||||
def write(*args):
|
||||
pass
|
||||
|
||||
def flush(*args):
|
||||
pass
|
||||
|
||||
with guard_temp_dir() as temp_dir, \
|
||||
guard_patch_module('sys.stdout', StdoutMock()):
|
||||
logger = gtest_parallel.FilterFormat(None if drop_output else temp_dir)
|
||||
logger.log_tasks(42)
|
||||
|
||||
test_body(logger)
|
||||
|
||||
logger.flush()
|
||||
|
||||
def test_no_output_dir(self):
|
||||
def run_test(logger):
|
||||
passed = [
|
||||
TaskMock(
|
||||
('fake/binary', 'FakeTest'), 0, {
|
||||
'runtime_ms': [10],
|
||||
'exit_code': [0],
|
||||
'last_execution_time': [10],
|
||||
'log_file': [os.path.join(tempfile.gettempdir(), 'fake.log')]
|
||||
})
|
||||
]
|
||||
|
||||
open(passed[0].log_file, 'w').close()
|
||||
self.assertTrue(os.path.isfile(passed[0].log_file))
|
||||
|
||||
logger.log_exit(passed[0])
|
||||
|
||||
self.assertFalse(os.path.isfile(passed[0].log_file))
|
||||
|
||||
logger.print_tests('', passed, print_try_number=True, print_test_command=True)
|
||||
logger.move_to(None, passed)
|
||||
|
||||
logger.summarize(passed, [], [])
|
||||
|
||||
self._execute_test(run_test, True)
|
||||
|
||||
def test_with_output_dir(self):
|
||||
def run_test(logger):
|
||||
failed = [
|
||||
TaskMock(
|
||||
('fake/binary', 'FakeTest'), 0, {
|
||||
'runtime_ms': [10],
|
||||
'exit_code': [1],
|
||||
'last_execution_time': [10],
|
||||
'log_file': [os.path.join(logger.output_dir, 'fake.log')]
|
||||
})
|
||||
]
|
||||
|
||||
open(failed[0].log_file, 'w').close()
|
||||
self.assertTrue(os.path.isfile(failed[0].log_file))
|
||||
|
||||
logger.log_exit(failed[0])
|
||||
|
||||
self.assertTrue(os.path.isfile(failed[0].log_file))
|
||||
|
||||
logger.print_tests('', failed, print_try_number=True, print_test_command=True)
|
||||
logger.move_to('failed', failed)
|
||||
|
||||
self.assertFalse(os.path.isfile(failed[0].log_file))
|
||||
self.assertTrue(
|
||||
os.path.isfile(os.path.join(logger.output_dir, 'failed', 'fake.log')))
|
||||
|
||||
logger.summarize([], failed, [])
|
||||
|
||||
self._execute_test(run_test, False)
|
||||
|
||||
|
||||
class TestFindTests(unittest.TestCase):
|
||||
ONE_DISABLED_ONE_ENABLED_TEST = {
|
||||
"fake_unittests": {
|
||||
"FakeTest": {
|
||||
"Test1": None,
|
||||
"DISABLED_Test2": None,
|
||||
}
|
||||
}
|
||||
}
|
||||
ONE_FAILED_ONE_PASSED_TEST = {
|
||||
"fake_unittests": {
|
||||
"FakeTest": {
|
||||
# Failed (and new) tests have no recorded runtime.
|
||||
"FailedTest": None,
|
||||
"Test": 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
ONE_TEST = {
|
||||
"fake_unittests": {
|
||||
"FakeTest": {
|
||||
"TestSomething": None,
|
||||
}
|
||||
}
|
||||
}
|
||||
MULTIPLE_BINARIES_MULTIPLE_TESTS_ONE_FAILURE = {
|
||||
"fake_unittests": {
|
||||
"FakeTest": {
|
||||
"TestSomething": None,
|
||||
"TestSomethingElse": 2,
|
||||
},
|
||||
"SomeOtherTest": {
|
||||
"YetAnotherTest": 3,
|
||||
},
|
||||
},
|
||||
"fake_tests": {
|
||||
"Foo": {
|
||||
"Bar": 4,
|
||||
"Baz": 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def _process_options(self, options):
|
||||
parser = gtest_parallel.default_options_parser()
|
||||
options, binaries = parser.parse_args(options)
|
||||
self.assertEqual(len(binaries), 0)
|
||||
return options
|
||||
|
||||
def _call_find_tests(self, test_data, options=None):
|
||||
subprocess_mock = SubprocessMock(test_data)
|
||||
options = self._process_options(options or [])
|
||||
with guard_patch_module('subprocess.check_output', subprocess_mock):
|
||||
tasks = gtest_parallel.find_tests(test_data.keys(), [], options,
|
||||
TestTimesMock(self, test_data))
|
||||
# Clean transient tasks' log files created because
|
||||
# by default now output_dir is None.
|
||||
for task in tasks:
|
||||
if os.path.isfile(task.log_file):
|
||||
os.remove(task.log_file)
|
||||
return tasks, subprocess_mock
|
||||
|
||||
def test_tasks_are_sorted(self):
|
||||
tasks, _ = self._call_find_tests(
|
||||
self.MULTIPLE_BINARIES_MULTIPLE_TESTS_ONE_FAILURE)
|
||||
self.assertEqual([task.last_execution_time for task in tasks],
|
||||
[None, 4, 4, 3, 2])
|
||||
|
||||
def test_does_not_run_disabled_tests_by_default(self):
|
||||
tasks, subprocess_mock = self._call_find_tests(
|
||||
self.ONE_DISABLED_ONE_ENABLED_TEST)
|
||||
self.assertEqual(len(tasks), 1)
|
||||
self.assertFalse("DISABLED_" in tasks[0].test_name)
|
||||
self.assertNotIn("--gtest_also_run_disabled_tests",
|
||||
subprocess_mock.last_invocation)
|
||||
|
||||
def test_runs_disabled_tests_when_asked(self):
|
||||
tasks, subprocess_mock = self._call_find_tests(
|
||||
self.ONE_DISABLED_ONE_ENABLED_TEST, ['--gtest_also_run_disabled_tests'])
|
||||
self.assertEqual(len(tasks), 2)
|
||||
self.assertEqual(sorted([task.test_name for task in tasks]),
|
||||
["FakeTest.DISABLED_Test2", "FakeTest.Test1"])
|
||||
self.assertIn("--gtest_also_run_disabled_tests",
|
||||
subprocess_mock.last_invocation)
|
||||
|
||||
def test_runs_failed_tests_by_default(self):
|
||||
tasks, _ = self._call_find_tests(self.ONE_FAILED_ONE_PASSED_TEST)
|
||||
self.assertEqual(len(tasks), 2)
|
||||
self.assertEqual(sorted([task.test_name for task in tasks]),
|
||||
["FakeTest.FailedTest", "FakeTest.Test"])
|
||||
self.assertEqual({task.last_execution_time for task in tasks}, {None, 1})
|
||||
|
||||
def test_runs_only_failed_tests_when_asked(self):
|
||||
tasks, _ = self._call_find_tests(self.ONE_FAILED_ONE_PASSED_TEST,
|
||||
['--failed'])
|
||||
self.assertEqual(len(tasks), 1)
|
||||
self.assertEqual(tasks[0].test_binary, "fake_unittests")
|
||||
self.assertEqual(tasks[0].test_name, "FakeTest.FailedTest")
|
||||
self.assertIsNone(tasks[0].last_execution_time)
|
||||
|
||||
def test_does_not_apply_gtest_filter_by_default(self):
|
||||
_, subprocess_mock = self._call_find_tests(self.ONE_TEST)
|
||||
self.assertFalse(
|
||||
any(
|
||||
arg.startswith('--gtest_filter=SomeFilter')
|
||||
for arg in subprocess_mock.last_invocation))
|
||||
|
||||
def test_applies_gtest_filter(self):
|
||||
_, subprocess_mock = self._call_find_tests(self.ONE_TEST,
|
||||
['--gtest_filter=SomeFilter'])
|
||||
self.assertIn('--gtest_filter=SomeFilter', subprocess_mock.last_invocation)
|
||||
|
||||
def test_applies_gtest_color_by_default(self):
|
||||
tasks, _ = self._call_find_tests(self.ONE_TEST)
|
||||
self.assertEqual(len(tasks), 1)
|
||||
self.assertIn('--gtest_color=yes', tasks[0].test_command)
|
||||
|
||||
def test_applies_gtest_color(self):
|
||||
tasks, _ = self._call_find_tests(self.ONE_TEST, ['--gtest_color=Lemur'])
|
||||
self.assertEqual(len(tasks), 1)
|
||||
self.assertIn('--gtest_color=Lemur', tasks[0].test_command)
|
||||
|
||||
def test_repeats_tasks_once_by_default(self):
|
||||
tasks, _ = self._call_find_tests(self.ONE_TEST)
|
||||
self.assertEqual(len(tasks), 1)
|
||||
|
||||
def test_repeats_tasks_multiple_times(self):
|
||||
tasks, _ = self._call_find_tests(self.ONE_TEST, ['--repeat=3'])
|
||||
self.assertEqual(len(tasks), 3)
|
||||
# Test all tasks have the same test_name, test_binary and test_command
|
||||
all_tasks_set = set(
|
||||
(task.test_name, task.test_binary, tuple(task.test_command))
|
||||
for task in tasks)
|
||||
self.assertEqual(len(all_tasks_set), 1)
|
||||
# Test tasks have consecutive execution_numbers starting from 1
|
||||
self.assertEqual(sorted(task.execution_number for task in tasks), [1, 2, 3])
|
||||
|
||||
def test_gtest_list_tests_fails(self):
|
||||
def exit_mock(*args):
|
||||
raise AssertionError('Foo')
|
||||
|
||||
options = self._process_options([])
|
||||
with guard_patch_module('sys.exit', exit_mock):
|
||||
self.assertRaises(AssertionError, gtest_parallel.find_tests,
|
||||
[sys.executable], [], options, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -3,3 +3,5 @@
|
||||
*.gcov
|
||||
valkey.info
|
||||
lcov-html
|
||||
generated_*
|
||||
unit/wrapped/
|
||||
|
||||
+2
-2
@@ -109,7 +109,7 @@ if (BUILD_EXAMPLE_MODULES)
|
||||
add_subdirectory(modules)
|
||||
endif ()
|
||||
|
||||
if (BUILD_UNIT_TESTS)
|
||||
if (BUILD_UNIT_GTESTS)
|
||||
add_subdirectory(unit)
|
||||
endif ()
|
||||
|
||||
@@ -121,4 +121,4 @@ add_custom_target(hint ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Hint: It is a good idea to run tests with your CMake-built binaries \\;\\)"
|
||||
COMMAND ${CMAKE_COMMAND} -E echo " ./${_CMAKE_DIR_RELATIVE_PATH}/runtest"
|
||||
COMMAND ${CMAKE_COMMAND} -E echo ""
|
||||
)
|
||||
)
|
||||
|
||||
+15
-19
@@ -611,9 +611,7 @@ ENGINE_BENCHMARK_OBJ = \
|
||||
ENGINE_CHECK_RDB_NAME=$(ENGINE_NAME)-check-rdb$(PROG_SUFFIX)
|
||||
ENGINE_CHECK_AOF_NAME=$(ENGINE_NAME)-check-aof$(PROG_SUFFIX)
|
||||
ENGINE_LIB_NAME=lib$(ENGINE_NAME).a
|
||||
ENGINE_TEST_FILES:=$(wildcard unit/*.c)
|
||||
ENGINE_TEST_OBJ:=$(sort $(patsubst unit/%.c,unit/%.o,$(ENGINE_TEST_FILES)))
|
||||
ENGINE_UNIT_TESTS:=$(ENGINE_NAME)-unit-tests$(PROG_SUFFIX)
|
||||
ENGINE_UNIT_GTESTS:=$(ENGINE_NAME)-unit-gtests$(PROG_SUFFIX)
|
||||
ALL_SOURCES=$(sort $(patsubst %.o,%.c,$(ENGINE_SERVER_OBJ) $(ENGINE_CLI_OBJ) $(ENGINE_BENCHMARK_OBJ)))
|
||||
|
||||
USE_FAST_FLOAT?=no
|
||||
@@ -637,6 +635,8 @@ ifeq ($(USE_LIBBACKTRACE),yes)
|
||||
FINAL_LIBS += -lbacktrace
|
||||
endif
|
||||
|
||||
DEPENDENCY_TARGETS+= gtest-parallel
|
||||
|
||||
ALL_BUILD_PREREQUISITES=$(SERVER_NAME) $(ENGINE_SENTINEL_NAME) $(ENGINE_CLI_NAME) $(ENGINE_BENCHMARK_NAME) $(ENGINE_CHECK_RDB_NAME) $(ENGINE_CHECK_AOF_NAME) $(TLS_MODULE) $(RDMA_MODULE) $(LUA_MODULE)
|
||||
|
||||
all: $(ALL_BUILD_PREREQUISITES)
|
||||
@@ -653,7 +653,7 @@ endif
|
||||
|
||||
.PHONY: all
|
||||
|
||||
all-with-unit-tests: all $(ENGINE_UNIT_TESTS)
|
||||
all-with-unit-tests: all $(ENGINE_UNIT_GTESTS)
|
||||
.PHONY: all
|
||||
|
||||
persist-settings: distclean
|
||||
@@ -668,12 +668,14 @@ persist-settings: distclean
|
||||
echo BUILD_LUA=$(BUILD_LUA) >> .make-settings
|
||||
echo USE_FAST_FLOAT=$(USE_FAST_FLOAT) >> .make-settings
|
||||
echo USE_LIBBACKTRACE=$(USE_LIBBACKTRACE) >> .make-settings
|
||||
echo LIBBACKTRACE_PREFIX=$(LIBBACKTRACE_PREFIX) >> .make-settings
|
||||
echo CFLAGS=$(CFLAGS) >> .make-settings
|
||||
echo LDFLAGS=$(LDFLAGS) >> .make-settings
|
||||
echo SERVER_CFLAGS=$(SERVER_CFLAGS) >> .make-settings
|
||||
echo SERVER_LDFLAGS=$(SERVER_LDFLAGS) >> .make-settings
|
||||
echo PREV_FINAL_CFLAGS=$(FINAL_CFLAGS) >> .make-settings
|
||||
echo PREV_FINAL_LDFLAGS=$(FINAL_LDFLAGS) >> .make-settings
|
||||
echo ENGINE_SERVER_OBJ=$(ENGINE_SERVER_OBJ) >> .make-settings
|
||||
-(cd ../deps && $(MAKE) $(DEPENDENCY_TARGETS))
|
||||
|
||||
.PHONY: persist-settings
|
||||
@@ -699,10 +701,6 @@ $(SERVER_NAME): $(ENGINE_SERVER_OBJ)
|
||||
$(ENGINE_LIB_NAME): $(ENGINE_SERVER_OBJ)
|
||||
$(SERVER_AR) rcs $@ $^
|
||||
|
||||
# valkey-unit-tests
|
||||
$(ENGINE_UNIT_TESTS): $(ENGINE_TEST_OBJ) $(ENGINE_LIB_NAME)
|
||||
$(SERVER_LD) -o $@ $^ ../deps/libvalkey/lib/libvalkey.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a $(FINAL_LIBS)
|
||||
|
||||
# valkey-sentinel
|
||||
$(ENGINE_SENTINEL_NAME): $(SERVER_NAME)
|
||||
$(ENGINE_INSTALL) $(SERVER_NAME) $(ENGINE_SENTINEL_NAME)
|
||||
@@ -747,9 +745,6 @@ DEP = $(ENGINE_SERVER_OBJ:%.o=%.d) $(ENGINE_CLI_OBJ:%.o=%.d) $(ENGINE_BENCHMARK_
|
||||
trace/%.o: trace/%.c .make-prerequisites
|
||||
$(SERVER_CC) -Itrace -MMD -o $@ -c $<
|
||||
|
||||
unit/%.o: unit/%.c .make-prerequisites
|
||||
$(SERVER_CC) -MMD -o $@ -c $<
|
||||
|
||||
# The following files are checked in and don't normally need to be rebuilt. They
|
||||
# are built only if python is available and their prereqs are modified.
|
||||
ifneq (,$(PYTHON))
|
||||
@@ -760,17 +755,12 @@ fmtargs.h: ../utils/generate-fmtargs.py
|
||||
$(QUITE_GEN)sed '/Everything below this line/,$$d' $@ > $@.tmp
|
||||
$(QUITE_GEN)$(PYTHON) ../utils/generate-fmtargs.py >> $@.tmp
|
||||
$(QUITE_GEN)mv $@.tmp $@
|
||||
|
||||
unit/test_files.h: unit/*.c ../utils/generate-unit-test-header.py
|
||||
$(QUIET_GEN)$(PYTHON) ../utils/generate-unit-test-header.py
|
||||
|
||||
unit/test_main.o: unit/test_files.h
|
||||
endif
|
||||
|
||||
commands.c: $(COMMANDS_DEF_FILENAME).def
|
||||
|
||||
clean:
|
||||
rm -rf $(SERVER_NAME) $(ENGINE_SENTINEL_NAME) $(ENGINE_CLI_NAME) $(ENGINE_BENCHMARK_NAME) $(ENGINE_CHECK_RDB_NAME) $(ENGINE_CHECK_AOF_NAME) $(ENGINE_UNIT_TESTS) $(ENGINE_LIB_NAME) unit/*.o unit/*.d trace/*.o trace/*.d *.o *.gcda *.gcno *.gcov valkey.info lcov-html Makefile.dep *.so
|
||||
rm -rf $(SERVER_NAME) $(ENGINE_SENTINEL_NAME) $(ENGINE_CLI_NAME) $(ENGINE_BENCHMARK_NAME) $(ENGINE_CHECK_RDB_NAME) $(ENGINE_CHECK_AOF_NAME) $(ENGINE_UNIT_GTESTS) $(ENGINE_LIB_NAME) trace/*.o trace/*.d *.o *.gcda *.gcno *.gcov valkey.info lcov-html Makefile.dep *.so
|
||||
rm -f $(DEP)
|
||||
-(cd modules/lua && $(MAKE) clean)
|
||||
|
||||
@@ -780,15 +770,21 @@ distclean: clean
|
||||
-(cd ../deps && $(MAKE) distclean)
|
||||
-(cd modules && $(MAKE) clean)
|
||||
-(cd ../tests/modules && $(MAKE) clean)
|
||||
-(cd unit && $(MAKE) clean)
|
||||
-(rm -f .make-*)
|
||||
-(find unit -type d -name __pycache__ -exec rm -rf {} +)
|
||||
|
||||
.PHONY: distclean
|
||||
|
||||
test: $(ALL_BUILD_PREREQUISITES)
|
||||
@(cd ..; VALKEY_PROG_SUFFIX="$(PROG_SUFFIX)" ./runtest)
|
||||
|
||||
test-unit: $(ENGINE_UNIT_TESTS)
|
||||
./$(ENGINE_UNIT_TESTS)
|
||||
test-unit:
|
||||
@(cd unit && $(MAKE) test-unit)
|
||||
|
||||
# valkey-unit-gtests
|
||||
$(ENGINE_UNIT_GTESTS): $(ENGINE_LIB_NAME)
|
||||
@(cd unit && $(MAKE) $(ENGINE_UNIT_GTESTS))
|
||||
|
||||
test-modules: $(SERVER_NAME)
|
||||
@(cd ..; ./runtest-moduleapi)
|
||||
|
||||
+13
@@ -1414,3 +1414,16 @@ void dictRehashingInfo(dict *d, unsigned long long *from_size, unsigned long lon
|
||||
*from_size = DICTHT_SIZE(d->ht_size_exp[0]);
|
||||
*to_size = DICTHT_SIZE(d->ht_size_exp[1]);
|
||||
}
|
||||
|
||||
/* Wrapper functions for gtest to access static internals. */
|
||||
unsigned int testOnlyDictGetForceResizeRatio(void) {
|
||||
return dict_force_resize_ratio;
|
||||
}
|
||||
|
||||
signed char testOnlyDictNextExp(unsigned long size) {
|
||||
return dictNextExp(size);
|
||||
}
|
||||
|
||||
long long testOnlyTimeInMilliseconds(void) {
|
||||
return timeInMilliseconds();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
#define _BSD_SOURCE
|
||||
|
||||
#if defined(__linux__)
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
/*
|
||||
* This macro might have already been defined by including <features.h>, which
|
||||
* is transitively included by <sys/types.h>. Therefore, to avoid redefinition
|
||||
@@ -62,7 +64,9 @@
|
||||
#define _POSIX_C_SOURCE 199506L
|
||||
#endif
|
||||
|
||||
#ifndef _LARGEFILE_SOURCE
|
||||
#define _LARGEFILE_SOURCE
|
||||
#endif
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
|
||||
/* deprecate unsafe functions
|
||||
@@ -70,12 +74,20 @@
|
||||
* NOTE: We do not use the poison pragma since it
|
||||
* will error on stdlib definitions in files as well*/
|
||||
#if (__GNUC__ && __GNUC__ >= 4) && !defined __APPLE__
|
||||
|
||||
/* These deprecation attributes rely on C-only constructs (e.g. `restrict`)
|
||||
* and redeclare libc symbols. They are disabled for building gtest
|
||||
* to avoid conflicts with C++ standard library declarations.
|
||||
*/
|
||||
#ifndef __cplusplus
|
||||
int sprintf(char *str, const char *format, ...)
|
||||
__attribute__((deprecated("please avoid use of unsafe C functions. prefer use of snprintf instead")));
|
||||
char *strcpy(char *restrict dest, const char *src)
|
||||
__attribute__((deprecated("please avoid use of unsafe C functions. prefer use of valkey_strlcpy instead")));
|
||||
char *strcat(char *restrict dest, const char *restrict src)
|
||||
__attribute__((deprecated("please avoid use of unsafe C functions. prefer use of valkey_strlcat instead")));
|
||||
#endif /* !__cplusplus */
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
@@ -350,3 +350,20 @@ intset *intsetDup(intset *is) {
|
||||
memcpy(copy, is, size);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* Wrapper functions for gtest to access static internals. */
|
||||
uint8_t testOnlyIntsetValueEncoding(int64_t v) {
|
||||
return _intsetValueEncoding(v);
|
||||
}
|
||||
|
||||
int64_t testOnlyIntsetGetEncoded(intset *is, int pos, uint8_t enc) {
|
||||
return _intsetGetEncoded(is, pos, enc);
|
||||
}
|
||||
|
||||
int64_t testOnlyIntsetGet(intset *is, int pos) {
|
||||
return _intsetGet(is, pos);
|
||||
}
|
||||
|
||||
uint8_t testOnlyIntsetSearch(intset *is, int64_t value, uint32_t *pos) {
|
||||
return intsetSearch(is, value, pos);
|
||||
}
|
||||
|
||||
+70
-2
@@ -6023,8 +6023,8 @@ int getClientTypeByName(char *name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *getClientTypeName(int class) {
|
||||
switch (class) {
|
||||
char *getClientTypeName(int client_class) {
|
||||
switch (client_class) {
|
||||
case CLIENT_TYPE_NORMAL: return "normal";
|
||||
case CLIENT_TYPE_REPLICA: return "slave";
|
||||
case CLIENT_TYPE_PUBSUB: return "pubsub";
|
||||
@@ -6569,3 +6569,71 @@ void ioThreadWriteToClient(void *data) {
|
||||
atomic_thread_fence(memory_order_release);
|
||||
c->io_write_state = CLIENT_COMPLETED_IO;
|
||||
}
|
||||
|
||||
/* ========================== Wrapper Functions for Testing ========================== */
|
||||
/* These wrapper functions expose static functions for use in GoogleTest unit tests.
|
||||
* They are non-static wrappers that simply call the corresponding static functions. */
|
||||
|
||||
void testOnlyPostWriteToReplica(client *c) {
|
||||
postWriteToReplica(c);
|
||||
}
|
||||
|
||||
void testOnlyWriteToReplica(client *c) {
|
||||
writeToReplica(c);
|
||||
}
|
||||
|
||||
void testOnlyBackupAndUpdateClientArgv(client *c, int new_argc, robj **new_argv) {
|
||||
backupAndUpdateClientArgv(c, new_argc, new_argv);
|
||||
}
|
||||
|
||||
size_t testOnlyUpsertPayloadHeader(char *buf, size_t *bufpos, payloadHeader **last_header, uint8_t type, size_t len, int slot, size_t available) {
|
||||
return upsertPayloadHeader(buf, bufpos, last_header, type, len, slot, 0, available);
|
||||
}
|
||||
|
||||
int testOnlyIsCopyAvoidPreferred(client *c, robj *obj) {
|
||||
return isCopyAvoidPreferred(c, obj);
|
||||
}
|
||||
|
||||
size_t testOnlyAddReplyPayloadToBuffer(client *c, const void *payload, size_t len, uint8_t payload_type) {
|
||||
return _addReplyPayloadToBuffer(c, payload, len, payload_type);
|
||||
}
|
||||
|
||||
size_t testOnlyAddBulkStrRefToBuffer(client *c, const void *payload, size_t len) {
|
||||
return _addBulkStrRefToBuffer(c, payload, len);
|
||||
}
|
||||
|
||||
void testOnlyAddReplyPayloadToList(client *c, list *reply_list, const char *payload, size_t len, uint8_t payload_type) {
|
||||
_addReplyPayloadToList(c, reply_list, payload, len, payload_type);
|
||||
}
|
||||
|
||||
void testOnlyAddBulkStrRefToToList(client *c, const void *payload, size_t len) {
|
||||
_addBulkStrRefToToList(c, payload, len);
|
||||
}
|
||||
|
||||
void testOnlyAddBulkStrRefToBufferOrList(client *c, robj *obj) {
|
||||
_addBulkStrRefToBufferOrList(c, obj);
|
||||
}
|
||||
|
||||
void testOnlyInitReplyIOV(client *c, int iovsize, struct iovec *iov_arr, char (*prefixes)[BULK_STR_LEN_PREFIX_MAX_SIZE], char *crlf, replyIOV *reply) {
|
||||
initReplyIOV(c, iovsize, iov_arr, prefixes, crlf, reply);
|
||||
}
|
||||
|
||||
void testOnlyAddPlainBufferToReplyIOV(char *buf, size_t buf_len, replyIOV *reply, bufWriteMetadata *metadata) {
|
||||
addPlainBufferToReplyIOV(buf, buf_len, reply, metadata);
|
||||
}
|
||||
|
||||
void testOnlyAddBulkStringToReplyIOV(char *buf, size_t buf_len, replyIOV *reply, bufWriteMetadata *metadata) {
|
||||
addBulkStringToReplyIOV(buf, buf_len, reply, metadata);
|
||||
}
|
||||
|
||||
void testOnlyAddEncodedBufferToReplyIOV(char *buf, size_t bufpos, replyIOV *reply, bufWriteMetadata *metadata) {
|
||||
addEncodedBufferToReplyIOV(buf, bufpos, reply, metadata);
|
||||
}
|
||||
|
||||
void testOnlyAddBufferToReplyIOV(int encoded, char *buf, size_t bufpos, replyIOV *reply, bufWriteMetadata *metadata) {
|
||||
addBufferToReplyIOV(encoded, buf, bufpos, reply, metadata);
|
||||
}
|
||||
|
||||
void testOnlySaveLastWrittenBuf(client *c, bufWriteMetadata *metadata, int bufcnt, size_t totlen, size_t totwritten) {
|
||||
saveLastWrittenBuf(c, metadata, bufcnt, totlen, totwritten);
|
||||
}
|
||||
|
||||
@@ -1688,3 +1688,25 @@ void quicklistBookmarksClear(quicklist *ql) {
|
||||
/* NOTE: We do not shrink (realloc) the quick list. main use case for this
|
||||
* function is just before releasing the allocation. */
|
||||
}
|
||||
|
||||
/* Wrapper functions for gtest to access static internals. */
|
||||
|
||||
size_t testOnlyQuicklistNodeNegFillLimit(int fill) {
|
||||
return quicklistNodeNegFillLimit(fill);
|
||||
}
|
||||
|
||||
quicklistNode *testOnlyQuicklistCreateNode(void) {
|
||||
return quicklistCreateNode();
|
||||
}
|
||||
|
||||
quicklistNode *testOnlyQuicklistCreateNodeWithValue(int container, void *value, size_t sz) {
|
||||
return __quicklistCreateNode(container, value, sz);
|
||||
}
|
||||
|
||||
int testOnlyQuicklistCompressNode(quicklistNode *node) {
|
||||
return __quicklistCompressNode(node);
|
||||
}
|
||||
|
||||
int testOnlyQuicklistDecompressNode(quicklistNode *node) {
|
||||
return __quicklistDecompressNode(node);
|
||||
}
|
||||
|
||||
@@ -1262,9 +1262,9 @@ void sds_free(void *ptr) {
|
||||
* Template variables are specified using curly brackets, e.g. {variable}.
|
||||
* An opening bracket can be quoted by repeating it twice.
|
||||
*/
|
||||
sds sdstemplate(const char *template, sdstemplate_callback_t cb_func, void *cb_arg) {
|
||||
sds sdstemplate(const char *sds_template, sdstemplate_callback_t cb_func, void *cb_arg) {
|
||||
sds res = sdsempty();
|
||||
const char *p = template;
|
||||
const char *p = sds_template;
|
||||
|
||||
while (*p) {
|
||||
/* Find next variable, copy everything until there */
|
||||
|
||||
@@ -87,7 +87,7 @@ struct __attribute__((__packed__)) sdshdr64 {
|
||||
#define SDS_TYPE_64 4
|
||||
#define SDS_TYPE_MASK 7
|
||||
#define SDS_TYPE_BITS 3
|
||||
#define SDS_HDR_VAR(T, s) struct sdshdr##T *sh = (void *)((s) - (sizeof(struct sdshdr##T)));
|
||||
#define SDS_HDR_VAR(T, s) struct sdshdr##T *sh = (struct sdshdr##T *)((s) - (sizeof(struct sdshdr##T)))
|
||||
#define SDS_HDR(T, s) ((struct sdshdr##T *)((s) - (sizeof(struct sdshdr##T))))
|
||||
#define SDS_TYPE_5_LEN(f) ((unsigned char)(f) >> SDS_TYPE_BITS)
|
||||
|
||||
@@ -269,7 +269,7 @@ int sdsneedsrepr(const_sds s);
|
||||
* substitution value. Returning a NULL indicates an error.
|
||||
*/
|
||||
typedef sds (*sdstemplate_callback_t)(const_sds variable, void *arg);
|
||||
sds sdstemplate(const char *template, sdstemplate_callback_t cb_func, void *cb_arg);
|
||||
sds sdstemplate(const char *sds_template, sdstemplate_callback_t cb_func, void *cb_arg);
|
||||
|
||||
/* Low level functions exposed to the user API */
|
||||
int sdsHdrSize(char type);
|
||||
|
||||
+2
-2
@@ -7254,9 +7254,9 @@ static sds expandProcTitleTemplate(const char *template, const char *title) {
|
||||
return sdstrim(res, " ");
|
||||
}
|
||||
/* Validate the specified template, returns 1 if valid or 0 otherwise. */
|
||||
int validateProcTitleTemplate(const char *template) {
|
||||
int validateProcTitleTemplate(const char *templ) {
|
||||
int ok = 1;
|
||||
sds res = expandProcTitleTemplate(template, "");
|
||||
sds res = expandProcTitleTemplate(templ, "");
|
||||
if (!res) return 0;
|
||||
if (sdslen(res) == 0) ok = 0;
|
||||
sdsfree(res);
|
||||
|
||||
+5
-3
@@ -40,7 +40,9 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#ifndef __cplusplus
|
||||
#include <stdatomic.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <limits.h>
|
||||
@@ -57,7 +59,7 @@
|
||||
#include <systemd/sd-daemon.h>
|
||||
#endif
|
||||
|
||||
#ifndef static_assert
|
||||
#if !defined(static_assert) && !defined(__cplusplus)
|
||||
#define static_assert _Static_assert
|
||||
#endif
|
||||
|
||||
@@ -2826,7 +2828,7 @@ uint64_t crc64(uint64_t crc, const unsigned char *s, uint64_t l);
|
||||
void exitFromChild(int retcode);
|
||||
long long serverPopcount(void *s, long count);
|
||||
int serverSetProcTitle(char *title);
|
||||
int validateProcTitleTemplate(const char *template);
|
||||
int validateProcTitleTemplate(const char *templ);
|
||||
int serverCommunicateSystemd(const char *sd_notify_msg);
|
||||
void serverSetCpuAffinity(const char *cpulist);
|
||||
void dictVanillaFree(void *val);
|
||||
@@ -2971,7 +2973,7 @@ int freeClientsInAsyncFreeQueue(void);
|
||||
int closeClientOnOutputBufferLimitReached(client *c, int async);
|
||||
int getClientType(client *c);
|
||||
int getClientTypeByName(char *name);
|
||||
char *getClientTypeName(int class);
|
||||
char *getClientTypeName(int client_class);
|
||||
void flushReplicasOutputBuffers(void);
|
||||
void disconnectReplicas(void);
|
||||
void evictClients(void);
|
||||
|
||||
+1
-1
@@ -14,13 +14,13 @@ typedef struct streamID {
|
||||
} streamID;
|
||||
|
||||
typedef struct stream {
|
||||
rax *cgroups; /* Consumer groups dictionary: name -> streamCG */
|
||||
rax *rax; /* The radix tree holding the stream. */
|
||||
uint64_t length; /* Current number of elements inside this stream. */
|
||||
streamID last_id; /* Zero if there are yet no items. */
|
||||
streamID first_id; /* The first non-tombstone entry, zero if empty. */
|
||||
streamID max_deleted_entry_id; /* The maximal ID that was deleted. */
|
||||
uint64_t entries_added; /* All time count of elements added. */
|
||||
rax *cgroups; /* Consumer groups dictionary: name -> streamCG */
|
||||
} stream;
|
||||
|
||||
/* We define an iterator to iterate stream items in an abstract way, without
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
ColumnLimit: 0
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 100
|
||||
PenaltyExcessCharacter: 100
|
||||
MaxEmptyLinesToKeep: 2
|
||||
BreakBeforeBraces: Attach
|
||||
AllowShortCaseLabelsOnASingleLine: true
|
||||
AllowShortIfStatementsOnASingleLine: WithoutElse
|
||||
AllowShortLoopsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: false
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignTrailingComments: true
|
||||
PointerAlignment: Right
|
||||
DerivePointerAlignment: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
BreakConstructorInitializers: AfterColon
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
AllowShortLambdasOnASingleLine: Inline
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpacesInSquareBrackets: false
|
||||
ReflowComments: true
|
||||
CommentPragmas: '^\\s*\\*'
|
||||
SortIncludes: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
BinPackParameters: false
|
||||
AlignAfterOpenBracket: Align
|
||||
Standard: Cpp11
|
||||
+126
-20
@@ -1,12 +1,14 @@
|
||||
project(valkey-unit-tests)
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(valkey_unit_gtests)
|
||||
|
||||
file(GLOB UNIT_TEST_SRCS "${CMAKE_CURRENT_LIST_DIR}/*.c")
|
||||
set(UNIT_TEST_SRCS "${UNIT_TEST_SRCS}")
|
||||
# GoogleTest requires at least C++17
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
get_valkey_server_linker_option(VALKEY_SERVER_LDFLAGS)
|
||||
|
||||
# Build unit tests only
|
||||
message(STATUS "Building unit tests")
|
||||
# Build GoogleTest-based tests
|
||||
message(STATUS "Building gtest unit tests")
|
||||
if (USE_TLS)
|
||||
if (BUILD_TLS_MODULE)
|
||||
# TLS as a module
|
||||
@@ -18,40 +20,144 @@ if (USE_TLS)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Build Valkey sources as a static library for the test
|
||||
add_library(valkeylib STATIC ${VALKEY_SERVER_SRCS})
|
||||
target_compile_options(valkeylib PRIVATE "${COMPILE_FLAGS}")
|
||||
target_compile_definitions(valkeylib PRIVATE "${COMPILE_DEFINITIONS}")
|
||||
# Propagate allocator defines so that C++ test code uses the same
|
||||
# malloc_usable_size variant (e.g. je_malloc_usable_size) as the C library.
|
||||
if (USE_JEMALLOC)
|
||||
list(APPEND COMPILE_DEFINITIONS "USE_JEMALLOC")
|
||||
elseif (USE_TCMALLOC OR USE_TCMALLOC_MINIMAL)
|
||||
list(APPEND COMPILE_DEFINITIONS "USE_TCMALLOC")
|
||||
endif ()
|
||||
|
||||
add_executable(valkey-unit-tests ${UNIT_TEST_SRCS})
|
||||
target_compile_options(valkey-unit-tests PRIVATE "${COMPILE_FLAGS}")
|
||||
target_compile_definitions(valkey-unit-tests PRIVATE "${COMPILE_DEFINITIONS}")
|
||||
add_dependencies(valkey-unit-tests generate_test_files_h)
|
||||
# Fetch GoogleTest using FetchContent (modern approach)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG v1.14.0 # Pinned GoogleTest version
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
# Build Valkey sources as a static library for the test (C code compiled with C compiler)
|
||||
add_library(valkeylib-gtest STATIC ${VALKEY_SERVER_SRCS})
|
||||
target_compile_options(valkeylib-gtest PRIVATE -Og -g -fno-lto)
|
||||
target_compile_definitions(valkeylib-gtest PRIVATE "${COMPILE_DEFINITIONS}")
|
||||
target_include_directories(valkeylib-gtest PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/deps)
|
||||
|
||||
# Generate wrapper files from wrappers.h
|
||||
set(GENERATED_WRAPPERS_DIR ${CMAKE_BINARY_DIR}/gtest_generated)
|
||||
file(MAKE_DIRECTORY ${GENERATED_WRAPPERS_DIR})
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_WRAPPERS_DIR}/generated_wrappers.cpp ${GENERATED_WRAPPERS_DIR}/generated_wrappers.hpp
|
||||
COMMAND python3 ${CMAKE_CURRENT_LIST_DIR}/generate-wrappers.py ${GENERATED_WRAPPERS_DIR}
|
||||
DEPENDS ${CMAKE_CURRENT_LIST_DIR}/wrappers.h ${CMAKE_CURRENT_LIST_DIR}/generate-wrappers.py
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
|
||||
COMMENT "Generating wrapper files from wrappers.h"
|
||||
)
|
||||
|
||||
# Create a target for generating wrapper files
|
||||
add_custom_target(generate_wrappers DEPENDS ${GENERATED_WRAPPERS_DIR}/generated_wrappers.cpp ${GENERATED_WRAPPERS_DIR}/generated_wrappers.hpp)
|
||||
|
||||
# Collect C++ test source files (excluding generated ones for now)
|
||||
file(GLOB GTEST_SRCS "${CMAKE_CURRENT_LIST_DIR}/*.cpp")
|
||||
list(REMOVE_ITEM GTEST_SRCS "${GENERATED_WRAPPERS_DIR}/generated_wrappers.cpp")
|
||||
|
||||
# Create GoogleTest executable (C++ code compiled with C++ compiler)
|
||||
add_executable(valkey-unit-gtests ${GTEST_SRCS} ${GENERATED_WRAPPERS_DIR}/generated_wrappers.cpp)
|
||||
|
||||
# Make sure generated files are created before building
|
||||
add_dependencies(valkey-unit-gtests generate_wrappers)
|
||||
|
||||
# Set C++ properties
|
||||
set_target_properties(valkey-unit-gtests PROPERTIES
|
||||
CXX_STANDARD 17
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
)
|
||||
|
||||
# Add C++ compile definitions and flags for C header compatibility
|
||||
target_compile_definitions(valkey-unit-gtests PRIVATE "${COMPILE_DEFINITIONS}")
|
||||
|
||||
# Allow C-style void pointer conversions in C++
|
||||
target_compile_options(valkey-unit-gtests PRIVATE -Og -g)
|
||||
|
||||
# Disable warnings
|
||||
target_compile_options(valkey-unit-gtests PRIVATE
|
||||
-Wno-deprecated-declarations
|
||||
-Wno-write-strings
|
||||
-fno-var-tracking-assignments
|
||||
)
|
||||
|
||||
# Include directories for C++ compilation
|
||||
target_include_directories(valkey-unit-gtests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/deps
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${GENERATED_WRAPPERS_DIR}
|
||||
)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
# Avoid duplicate symbols on non macOS
|
||||
target_link_options(valkey-unit-tests PRIVATE "-Wl,--allow-multiple-definition")
|
||||
target_link_options(valkey-unit-gtests PRIVATE "-Wl,--allow-multiple-definition")
|
||||
|
||||
# Extract all wrapped function names from wrappers.h and add --wrap linker flags
|
||||
execute_process(
|
||||
COMMAND python3 -c "
|
||||
import re
|
||||
import sys
|
||||
with open('${CMAKE_CURRENT_LIST_DIR}/wrappers.h', 'r') as f:
|
||||
for line in f:
|
||||
m = re.match(r'.*__wrap_(\\w+)\\(.*\\);', line)
|
||||
if m:
|
||||
print(m.group(1))
|
||||
"
|
||||
OUTPUT_VARIABLE WRAPPED_FUNCTIONS
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
# Convert the list of functions to linker flags
|
||||
string(REPLACE "\n" ";" WRAPPED_FUNCTIONS_LIST "${WRAPPED_FUNCTIONS}")
|
||||
foreach(FUNC ${WRAPPED_FUNCTIONS_LIST})
|
||||
target_link_options(valkey-unit-gtests PRIVATE "-Wl,--wrap=${FUNC}")
|
||||
endforeach()
|
||||
endif ()
|
||||
|
||||
if (USE_JEMALLOC)
|
||||
# Using jemalloc
|
||||
target_link_libraries(valkey-unit-tests jemalloc)
|
||||
# Using jemalloc - link to both the static library and the executable
|
||||
target_link_libraries(valkeylib-gtest jemalloc)
|
||||
target_link_libraries(valkey-unit-gtests jemalloc)
|
||||
endif ()
|
||||
|
||||
if (IS_FREEBSD)
|
||||
target_link_libraries(valkey-unit-tests execinfo)
|
||||
target_link_libraries(valkey-unit-gtests execinfo)
|
||||
endif ()
|
||||
|
||||
# Link with GoogleTest using target names
|
||||
target_link_libraries(
|
||||
valkey-unit-tests
|
||||
valkeylib
|
||||
valkey-unit-gtests
|
||||
valkeylib-gtest
|
||||
fpconv
|
||||
lualib
|
||||
hdr_histogram
|
||||
valkey::valkey
|
||||
GTest::gtest_main
|
||||
GTest::gmock
|
||||
pthread
|
||||
${VALKEY_SERVER_LDFLAGS})
|
||||
|
||||
if (USE_TLS)
|
||||
# Add required libraries needed for TLS
|
||||
target_link_libraries(valkey-unit-tests OpenSSL::SSL valkey::valkey_tls)
|
||||
target_link_libraries(valkey-unit-gtests OpenSSL::SSL valkey::valkey_tls)
|
||||
endif ()
|
||||
|
||||
# Enable testing and discover tests
|
||||
enable_testing()
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(valkey-unit-gtests)
|
||||
|
||||
# Add custom test target using gtest-parallel
|
||||
add_custom_target(test-unit
|
||||
COMMAND python3 ${CMAKE_SOURCE_DIR}/deps/gtest-parallel/gtest_parallel.py $<TARGET_FILE:valkey-unit-gtests>
|
||||
DEPENDS valkey-unit-gtests
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMENT "Running tests with gtest-parallel"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
CXX ?= g++
|
||||
AR ?= ar
|
||||
SERVER_AR ?= $(AR)
|
||||
-include ../.make-settings
|
||||
OPT=-Og -g
|
||||
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
|
||||
|
||||
# Add -m32 for 32bit builds
|
||||
ifeq ($(findstring -m32,$(CFLAGS)),-m32)
|
||||
CXX+= -m32
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
SOURCES := $(filter-out generated_wrappers.cpp, $(wildcard *.cpp))
|
||||
ALL_OBJECTS := $(patsubst %.cpp,%.o,$(SOURCES))
|
||||
DEPENDS := $(patsubst %.cpp,%.d,$(SOURCES))
|
||||
|
||||
# Inherit CFLAGS from parent Makefile
|
||||
TEST_CFLAGS := $(CFLAGS)
|
||||
|
||||
# Hold new gtest test to a high standard
|
||||
MORE_WARNINGS=-Wno-variadic-macros \
|
||||
-Werror \
|
||||
-Wpedantic \
|
||||
-Wextra \
|
||||
-Wall \
|
||||
-Wcast-align \
|
||||
-Wframe-larger-than=32768 \
|
||||
-Wno-strict-overflow \
|
||||
-Wsign-compare \
|
||||
-Werror=missing-braces \
|
||||
-Werror=init-self \
|
||||
-Werror=write-strings \
|
||||
-Werror=address \
|
||||
-Werror=array-bounds \
|
||||
-Werror=char-subscripts \
|
||||
-Werror=enum-compare \
|
||||
-Werror=empty-body \
|
||||
-Werror=main \
|
||||
-Werror=nonnull \
|
||||
-Werror=parentheses \
|
||||
-Werror=return-type \
|
||||
-Werror=sequence-point \
|
||||
-Werror=uninitialized \
|
||||
-Werror=volatile-register-var \
|
||||
-Werror=ignored-qualifiers \
|
||||
-Wno-unused-function \
|
||||
-Wno-missing-field-initializers \
|
||||
-Wdouble-promotion \
|
||||
-Wformat=2
|
||||
|
||||
# Detect compiler
|
||||
IS_GCC := $(if $(findstring clang,$(CXX)),,$(findstring g++,$(CXX)))
|
||||
|
||||
GCC_FLAGS :=
|
||||
|
||||
# Only add GCC-only warnings and flags
|
||||
ifeq ($(IS_GCC),g++)
|
||||
# GCC supports these extra warnings and -fno-var-tracking-assignments
|
||||
MORE_WARNINGS += -Wsync-nand -Wtrampolines -Werror=logical-op -Werror=aggressive-loop-optimizations
|
||||
GCC_FLAGS += -fno-var-tracking-assignments
|
||||
else
|
||||
# Clang: disable warnings that are errors in gtest/gmock
|
||||
MORE_WARNINGS += -Wno-deprecated-copy
|
||||
MORE_WARNINGS += -Wno-keyword-macro
|
||||
endif
|
||||
|
||||
# Enable -Werror=float-equal only for non-32bit builds.
|
||||
# On 32bit (-m32), GoogleTest headers trigger -Wfloat-equal
|
||||
# (e.g., in gtest-printers.h), which would fail the build.
|
||||
ifneq ($(findstring -m32,$(CFLAGS)),-m32)
|
||||
MORE_WARNINGS += -Werror=float-equal
|
||||
endif
|
||||
|
||||
-include $(DEPENDS)
|
||||
|
||||
ifdef COVERAGE
|
||||
TEST_CFLAGS += -DCOVERAGE_TEST $(COVERAGE_CFLAGS)
|
||||
endif
|
||||
|
||||
ifdef FORCE_RUN_SKIPPED_TESTS
|
||||
TEST_CFLAGS+=-DFORCE_RUN_SKIPPED_TESTS
|
||||
endif
|
||||
|
||||
# Track compilation flags to force rebuild when they change
|
||||
.flags: FORCE
|
||||
@echo '$(TEST_CFLAGS)' | cmp -s - $@ || echo '$(TEST_CFLAGS)' > $@
|
||||
|
||||
.PHONY: FORCE
|
||||
|
||||
# Valkey server library dependencies
|
||||
VALKEY_SERVER_LIB := ../libvalkey.a
|
||||
HIREDIS_LIB := ../../deps/libvalkey/lib/libvalkey.a
|
||||
JEMALLOC_LIB := ../../deps/jemalloc/lib/libjemalloc.a
|
||||
HDR_HISTOGRAM_LIB := ../../deps/hdr_histogram/libhdrhistogram.a
|
||||
FPCONV_LIB := ../../deps/fpconv/libfpconv.a
|
||||
TLS_LIBS := ../../deps/libvalkey/lib/libvalkey_tls.a -lssl -lcrypto
|
||||
GTEST_CFLAGS ?=
|
||||
GTEST_LIBS ?=
|
||||
ifeq ($(strip $(GTEST_LIBS)),)
|
||||
GTEST_CFLAGS := $(shell pkg-config --cflags gtest gmock 2>/dev/null)
|
||||
GTEST_LIBS := $(shell pkg-config --libs gtest gmock 2>/dev/null)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(GTEST_LIBS)),)
|
||||
$(error googletest not found. Install gtest or set GTEST_CFLAGS/GTEST_LIBS explicitly)
|
||||
endif
|
||||
FAST_FLOAT_LIB := ../../deps/fast_float_c_interface/fast_float_strtod.o
|
||||
|
||||
# On Darwin (macOS), the linker does not support -Wl,--wrap,
|
||||
# so we manually define __wrap_* and __real_* for each function in wrappers.h.
|
||||
# by copying all object files into the wrapped/ folder and applying symbol redefinitions.
|
||||
ifeq ($(uname_S), Darwin)
|
||||
WRAP_DIR := wrapped
|
||||
VALKEY_SERVER_WRAP_LIB := $(WRAP_DIR)/wrappedlibvalkey.a
|
||||
ENGINE_SERVER_WRAP_OBJ := $(addprefix $(WRAP_DIR)/, $(ENGINE_SERVER_OBJ))
|
||||
|
||||
$(WRAP_DIR):
|
||||
@mkdir -p $(WRAP_DIR)
|
||||
|
||||
$(WRAP_DIR)/%.o: ../%.o $(WRAP_DIR) generated_wrappers.o
|
||||
@mkdir -p $(dir $@)
|
||||
@if file $< | grep -q "LLVM bitcode"; then \
|
||||
llc $< -filetype=obj -o $(WRAP_DIR)/$*_temp.o; \
|
||||
python3 generate-redefine-syms.py $(WRAP_DIR)/$*_temp.o > $(WRAP_DIR)/$*-wrap-syms; \
|
||||
llvm-objcopy --redefine-syms=$(WRAP_DIR)/$*-wrap-syms $(WRAP_DIR)/$*_temp.o $@; \
|
||||
rm -f $(WRAP_DIR)/$*_temp.o; \
|
||||
else \
|
||||
python3 generate-redefine-syms.py $< > $(WRAP_DIR)/$*-wrap-syms; \
|
||||
llvm-objcopy --redefine-syms=$(WRAP_DIR)/$*-wrap-syms $< $@; \
|
||||
fi
|
||||
@rm -f $(WRAP_DIR)/$*-wrap-syms
|
||||
|
||||
$(VALKEY_SERVER_WRAP_LIB): $(VALKEY_SERVER_LIB) $(ENGINE_SERVER_WRAP_OBJ) generated_wrappers.o
|
||||
$(SERVER_AR) rcs $@ $(ENGINE_SERVER_WRAP_OBJ)
|
||||
endif
|
||||
|
||||
# Ensure Valkey server library is built
|
||||
$(VALKEY_SERVER_LIB): make-prerequisites
|
||||
cd .. && $(MAKE) libvalkey.a
|
||||
|
||||
# Ensure parent prerequisites are built
|
||||
.PHONY: make-prerequisites
|
||||
make-prerequisites:
|
||||
cd .. && $(MAKE) .make-prerequisites
|
||||
|
||||
# Read MALLOC / USE_JEMALLOC setting from parent Makefile
|
||||
JEMALLOC_CFLAGS := -DUSE_JEMALLOC -isystem../../deps/jemalloc/include
|
||||
ifeq ($(MALLOC),libc)
|
||||
JEMALLOC_CFLAGS :=
|
||||
JEMALLOC_LIB :=
|
||||
endif
|
||||
|
||||
LD_LIBS := $(HIREDIS_LIB) \
|
||||
$(JEMALLOC_LIB) \
|
||||
$(GTEST_LIBS) \
|
||||
$(HDR_HISTOGRAM_LIB) \
|
||||
$(FPCONV_LIB)
|
||||
|
||||
# Add libbacktrace support if enabled
|
||||
ifeq ($(USE_LIBBACKTRACE),yes)
|
||||
TEST_CFLAGS += -DUSE_LIBBACKTRACE
|
||||
ifdef LIBBACKTRACE_PREFIX
|
||||
TEST_CFLAGS += -I$(LIBBACKTRACE_PREFIX)/include
|
||||
LDFLAGS += -L$(LIBBACKTRACE_PREFIX)/lib
|
||||
endif
|
||||
LD_LIBS += -lbacktrace
|
||||
endif
|
||||
|
||||
# Add fast_float support if enabled
|
||||
ifeq ($(USE_FAST_FLOAT),yes)
|
||||
LD_LIBS += $(FAST_FLOAT_LIB)
|
||||
endif
|
||||
|
||||
# Add TLS libraries if enabled
|
||||
ifeq ($(BUILD_TLS),yes)
|
||||
LD_LIBS += $(TLS_LIBS)
|
||||
endif
|
||||
|
||||
# Compile C++ test files, recompile if generated_wrappers or compiler flags change
|
||||
%.o: %.cpp generated_wrappers.cpp .flags
|
||||
$(CXX) -MD -MP -std=c++17 -faligned-new -Wno-write-strings -fpermissive $(GCC_FLAGS) $(OPT) $(DEBUG) $(TEST_CFLAGS) -Wall -Wno-deprecated-declarations -c \
|
||||
-isystem .. -I ../../deps/lua/src/ -I ../../deps/hdr_histogram/ -I ../../deps/fpconv/ $(JEMALLOC_CFLAGS) \
|
||||
$(GTEST_CFLAGS) $<
|
||||
|
||||
$(ALL_OBJECTS): generated_wrappers.o
|
||||
$(ALL_OBJECTS): OPT := $(OPT) $(MORE_WARNINGS)
|
||||
|
||||
# Obtain the list of function calls for which we are generating mocks using the gmock library. We assume they are defined in
|
||||
# wrappers.h and begin with the prefix '__wrap_' (which is required by the linker). These command line arguments are then
|
||||
# passed in to g++ during linking.
|
||||
ifneq ($(wildcard wrappers.h),)
|
||||
CMD:=egrep '__wrap_.*\(.*;' wrappers.h | sed 's/__wrap_/@/' | sed 's/[^@]*@\([^(]*\)(.*/\1/' | sort | uniq | xargs -I{} echo "-Wl,--wrap={}" | tr "\n" " "
|
||||
OVERRIDES:=$(shell $(CMD))
|
||||
else
|
||||
OVERRIDES:=
|
||||
endif
|
||||
|
||||
# Generate a new set of wrappers every time our header changes.
|
||||
generated_wrappers.cpp: make-prerequisites wrappers.h generate-wrappers.py
|
||||
./generate-wrappers.py
|
||||
|
||||
generated_wrappers.o: generated_wrappers.cpp
|
||||
|
||||
# Generate the single test binary in parent directory.
|
||||
ifeq ($(uname_S), Darwin)
|
||||
valkey-unit-gtests: $(ALL_OBJECTS) generated_wrappers.o $(VALKEY_SERVER_WRAP_LIB) $(FINAL_LIBS)
|
||||
$(CXX) -g $(LDFLAGS) -fno-lto \
|
||||
-Wl,-flat_namespace -Wl,-undefined,dynamic_lookup \
|
||||
-Wall -Wno-deprecated-declarations \
|
||||
-o $@ $^ $(LD_LIBS) \
|
||||
-lm -pthread -ldl
|
||||
else
|
||||
valkey-unit-gtests: $(ALL_OBJECTS) generated_wrappers.o $(VALKEY_SERVER_LIB) $(FINAL_LIBS)
|
||||
$(CXX) -g $(LDFLAGS) -fno-lto \
|
||||
$(OVERRIDES) \
|
||||
-Wall -Wno-deprecated-declarations \
|
||||
-o $@ $^ $(LD_LIBS) \
|
||||
-lrt -lm -pthread -ldl $(if $(COVERAGE),-lgcov)
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: valkey-unit-gtests
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: valkey-unit-gtests
|
||||
python3 ../../deps/gtest-parallel/gtest_parallel.py ./valkey-unit-gtests
|
||||
@printf '\033[32mAll UNIT TESTS PASSED!\033[0m\n'
|
||||
|
||||
.PHONY: valgrind
|
||||
valgrind: valkey-unit-gtests
|
||||
valgrind $(VALGRIND_ARGS) ./valkey-unit-gtests
|
||||
|
||||
clean:
|
||||
rm -rf *.o valkey-unit-gtests ../valkey-unit-gtests generated_* $(DEPENDS) .flags wrapped
|
||||
|
||||
distclean: clean
|
||||
$(MAKE) -C .. distclean
|
||||
|
||||
.PHONY: clean distclean
|
||||
+97
-45
@@ -1,59 +1,111 @@
|
||||
## Introduction
|
||||
Valkey uses a very simple C testing framework, built up over time but now based loosely off of [Unity](https://www.throwtheswitch.org/unity).
|
||||
## Valkey GoogleTest Unit Test Framework
|
||||
|
||||
All test files are located at `src/unit/test_*`.
|
||||
A single test file can have multiple individual tests, and they must be of the form `int test_<test_name>(int argc, char *argv[], int flags) {`, where test_name is the name of the test.
|
||||
The test name must be globally unique.
|
||||
A test will be marked as successful if returns 0, and will be marked failed in all other cases.
|
||||
To use this framework to write unit tests, we have modified Valkey to build as
|
||||
a library that can link against other test executables. This framework uses the
|
||||
GNU linker (ld), which implements 'wrap' functionality to rename function calls
|
||||
to foo() to a method __wrap_foo() and renames the real foo() method to
|
||||
__real_foo().
|
||||
|
||||
The test framework also parses several flags passed in, and sets them based on the arguments to the tests.
|
||||
Using this trick, we define the Valkey wrappers we wish to mock in 'wrappers.h'.
|
||||
Note that these functions can only be mocked if they include calls between
|
||||
source files.
|
||||
|
||||
Tests flags:
|
||||
* `UNIT_TEST_ACCURATE`: Corresponds to the `--accurate` flag. This flag indicates the test should use extra computation to more accurately validate the tests.
|
||||
* `UNIT_TEST_LARGE_MEMORY`: Corresponds to the `--large-memory` flag. This flag indicates whether or not tests should use more than 100mb of memory.
|
||||
* `UNIT_TEST_SINGLE`: Corresponds to the `--single` flag. This flag indicates that a single test is being executed.
|
||||
* `UNIT_TEST_VALGRIND`: Corresponds to the `--valgrind` flag. This flag is just a hint passed to the test to indicate that we are running it under valgrind.
|
||||
Using this set of functions, we run 'generate-wrappers.py' to generate the glue
|
||||
code needed to mock functions. Specifically, this generates an interface named
|
||||
Valkey containing all the desired methods and two implementations, MockValkey
|
||||
and RealValkey.
|
||||
|
||||
Tests are allowed to be passed in additional arbitrary `argv`/`argc`, which they can access from the `argc` and `argv` arguments of the test.
|
||||
MockValkey uses gtest definitions to define a mock class. RealValkey uses the
|
||||
__real_foo() methods to call the renamed methods. The script also implements
|
||||
every __wrap_foo() command that delegates to the last MockValkey instance
|
||||
initialized.
|
||||
|
||||
## Assertions
|
||||
To extend the Valkey classes for mocking further methods, simply add your method
|
||||
to 'wrappers.h' and re-run 'make test-unit' to regenerate the Valkey glue code
|
||||
and run the tests.
|
||||
|
||||
There are a few built in assertions that can be used, that will return from the current function with the correct error code.
|
||||
`ASSERT` assertions are for fatal errors and automatically return upon error.
|
||||
`EXPECT` assertions are for non-fatal errors and continue execution after an error.
|
||||
When an assertion fails, they will print out the line number that they failed on.
|
||||
Important: All mocking should occur at software boundaries where interfaces are
|
||||
clearly defined. Your use of mocking will be denied if it is not at a well
|
||||
defined boundary. Overuse of mocking turns the unit tests into a "change
|
||||
detector" which will fail whenever the code is modified. Please also consider
|
||||
whether other testing strategies like injecting fakes/stubs or integration
|
||||
testing would yield similar test coverage.
|
||||
|
||||
* `TEST_ASSERT(condition)`: Will evaluate the condition, and if it fails it will return 1 and print out the condition that failed.
|
||||
* `TEST_EXPECT(condition)`: Will evaluate the condition, and if it fails it will print the condition that failed, record the failure, and continue.
|
||||
* `TEST_ASSERT_MESSAGE(message, condition)`: Is like `TEST_ASSERT`, but prints the given message instead on failure.
|
||||
* `TEST_EXPECT_MESSAGE(message, condition)`: Is like `TEST_EXPECT`, but prints the given message instead on failure.
|
||||
This framework depends on GoogleTest and GoogleMock. You need to install them manually
|
||||
before building the gtests (e.g., `libgtest-dev` / `libgmock-dev` on Debian/Ubuntu,
|
||||
`gtest-devel` / `gmock-devel` on CentOS/Fedora, or `brew install googletest` on macOS).
|
||||
|
||||
## Other utilities
|
||||
Alternatively, you can build and install GoogleTest from source:
|
||||
|
||||
If you would like to print out additional data, use the `TEST_PRINT_INFO(info, ...)` option, which has arguments similar to `printf`.
|
||||
This macro will also print out the function the code was executed from in addition to the line it was printed from.
|
||||
|
||||
## Example test
|
||||
|
||||
```
|
||||
int test_example(int argc, char *argv[], int flags) {
|
||||
TEST_ASSERT(5 == 5);
|
||||
TEST_ASSERT_MESSAGE("This should pass", 6 == 6);
|
||||
return 0;
|
||||
}
|
||||
```bash
|
||||
git clone https://github.com/google/googletest.git
|
||||
cd googletest
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## Running tests
|
||||
Tests can be run by executing:
|
||||
## Tricks in running unit tests
|
||||
|
||||
```
|
||||
make valkey-unit-tests
|
||||
./valkey-unit-tests
|
||||
```
|
||||
Sometimes the developer might want to run only one gtest unit test, or only a
|
||||
subset of all unit tests for debugging. We have a few different flavors of
|
||||
gtest unit tests that you can filter/play with:
|
||||
|
||||
Running a single unit test file
|
||||
```
|
||||
./valkey-unit-tests --single test_crc64.c
|
||||
```
|
||||
1. Running all unit tests
|
||||
|
||||
Will just run the `test_crc64.c` file.
|
||||
```bash
|
||||
make test-unit
|
||||
```
|
||||
|
||||
3. Running all unit tests in the test class, replace TEST_CLASS_NAME with
|
||||
expected test class name
|
||||
|
||||
```bash
|
||||
make valkey-unit-gtests
|
||||
./src/unit/valkey-unit-gtests --gtest_filter='TEST_CLASS_NAME.*'
|
||||
```
|
||||
|
||||
4. Running a subset of gtest unit tests in the test class, replace
|
||||
TEST_CLASS_NAME with expected test class name, and replace TEST_NAME_PREFIX
|
||||
with test name
|
||||
|
||||
```bash
|
||||
make valkey-unit-gtests
|
||||
./src/unit/valkey-unit-gtests --gtest_filter='TEST_CLASS_NAME.TEST_NAME_PREFIX*'
|
||||
```
|
||||
|
||||
5. Building and running with CMake
|
||||
|
||||
```bash
|
||||
mkdir build-release && cd $_
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/valkey -DBUILD_UNIT_GTESTS=yes
|
||||
make valkey-unit-gtests
|
||||
./bin/valkey-unit-gtests
|
||||
```
|
||||
|
||||
6. Running disabled tests
|
||||
|
||||
GoogleTest allows tests to be disabled by prefixing the test name with `DISABLED_`. These tests are skipped during normal test runs.
|
||||
Some tests are disabled by default because they take a long time to run (e.g., 1M iterations for performance benchmarks).
|
||||
To run a specific disabled test explicitly:
|
||||
|
||||
```bash
|
||||
make valkey-unit-gtests
|
||||
./src/unit/valkey-unit-gtests --gtest_filter=TEST_CLASS_NAME.DISABLED_TEST_NAME --gtest_also_run_disabled_tests
|
||||
```
|
||||
|
||||
## Test flags
|
||||
|
||||
The gtest framework supports several command-line flags to control test behavior:
|
||||
|
||||
* `--accurate`: Indicates the test should use extra computation to more accurately validate the tests.
|
||||
* `--large-memory`: Indicates whether tests should use more than 100mb of memory.
|
||||
* `--valgrind`: A hint passed to tests to indicate that we are running under valgrind.
|
||||
* `--seed <number>`: Sets a specific random seed for reproducible test runs. All `rand()` calls will produce the same sequence with the same seed.
|
||||
|
||||
Example usage:
|
||||
|
||||
```bash
|
||||
./src/unit/valkey-unit-gtests --accurate --large-memory --seed 12345
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef _CUSTOM_MATCHERS_HPP_
|
||||
#define _CUSTOM_MATCHERS_HPP_
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <string>
|
||||
|
||||
/* Matchers can be used for complex comparisons inside of EXPECT_THAT(val, matcher)
|
||||
* For example, to check if an robj contains a string "abc", a matcher can be used like:
|
||||
* EXPECT_THAT(o, robjEqualsStr("abc"));
|
||||
*/
|
||||
|
||||
// Matches an robj (which MUST contain an sds encoded string) to a char* string.
|
||||
MATCHER_P(robjEqualsStr, str, "robj string matcher") {
|
||||
assert(arg->type == OBJ_STRING);
|
||||
assert(sdsEncodedObject(arg));
|
||||
return strcmp(static_cast<const char *>(objectGetVal(arg)), str) == 0;
|
||||
}
|
||||
|
||||
#endif // _CUSTOM_MATCHERS_HPP_
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "server.h"
|
||||
}
|
||||
|
||||
// Use a class name descriptive of your test unit
|
||||
class ExampleTest : public ::testing::Test {
|
||||
// standard boilerplate supporting mocked functions
|
||||
protected:
|
||||
MockValkey mock;
|
||||
RealValkey real;
|
||||
|
||||
// The SetUp() function is called before each test.
|
||||
void SetUp() override {
|
||||
memset(&server, 0, sizeof(valkeyServer));
|
||||
server.hz = CONFIG_DEFAULT_HZ;
|
||||
}
|
||||
|
||||
// The TearDown() function is called after each test.
|
||||
void TearDown() override {
|
||||
}
|
||||
};
|
||||
|
||||
// Include this (should end in "DeathTest") if testing that code asserts/dies.
|
||||
using ExampleDeathTest = ExampleTest;
|
||||
|
||||
// Example of a DeathTest, which passes only if the code crashes.
|
||||
TEST_F(ExampleDeathTest, TestSimpleDeath) {
|
||||
EXPECT_DEATH(
|
||||
{
|
||||
*((volatile char *)0) = 'x'; // SEGV
|
||||
},
|
||||
"");
|
||||
}
|
||||
|
||||
// Simple assertions test
|
||||
TEST_F(ExampleTest, TestAssertions) {
|
||||
int a = 5, b = 3;
|
||||
const char *str = "hello";
|
||||
// Use EXPECT_ macros to test a condition. If the value is not as expected, the test will fail.
|
||||
// Use ASSERT_ macros to test a condition AND immediately end the test.
|
||||
// Prefer to use EXPECT_ macros unless the test can't reasonably continue. This allows multiple
|
||||
// conditions to be tested and reported rather than ending at the first unexpected value.
|
||||
EXPECT_EQ(8, a + b);
|
||||
EXPECT_LE(b, a);
|
||||
EXPECT_GT(a, b);
|
||||
EXPECT_STREQ(str, "hello");
|
||||
ASSERT_EQ(2, a - b);
|
||||
}
|
||||
|
||||
// Test matcher works in custom_matchers.hpp
|
||||
TEST_F(ExampleTest, TestMatchers) {
|
||||
robj *robj_str = createStringObject("test", 4);
|
||||
ASSERT_NE(robj_str, nullptr); // "ASSERT" is correct here, because the test can't reasonably continue
|
||||
EXPECT_THAT(robj_str, robjEqualsStr("test"));
|
||||
decrRefCount(robj_str);
|
||||
}
|
||||
|
||||
// Verify mocking works
|
||||
TEST_F(ExampleTest, TestMocking) {
|
||||
// verifies that aeCreateTimeEvent() is called at least once in startEvictionTimeProc.
|
||||
EXPECT_CALL(mock, aeCreateTimeEvent(_, _, _, _, _)).Times(AtLeast(1));
|
||||
startEvictionTimeProc();
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Generate a symbol redefinition file for llvm-objcopy's --redefine-syms option.
|
||||
|
||||
On Darwin (macOS), the linker does not support the --wrap directive.
|
||||
We emulate this behavior by renaming symbols using llvm-objcopy:
|
||||
* Symbols defined in the object file are renamed to __real_<symbol>
|
||||
* Symbols referenced elsewhere are renamed to __wrap_<symbol>
|
||||
|
||||
The script uses 'wrappers.h' to determine which functions to wrap and
|
||||
analyzes the supplied .o file to determine which symbols are defined in it.
|
||||
|
||||
Usage:
|
||||
generate-redefine-syms.py <input .o file>
|
||||
|
||||
Notes:
|
||||
* Must be run in the same directory as 'wrappers.h'
|
||||
* Input object file must be a Mach-O object file
|
||||
* Output is written to stdout in a format compatible with llvm-objcopy
|
||||
"""
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from wrapper_util import find_wrapper_functions_in_header
|
||||
|
||||
|
||||
def wrap_symbols(methods, object_file):
|
||||
"""
|
||||
For each function in `methods`, determine if it is defined in `object_file`.
|
||||
Print symbol redefinition lines for llvm-objcopy:
|
||||
* defined symbols -> ___real_<name>
|
||||
* undefined symbols -> ___wrap_<name>
|
||||
"""
|
||||
safe_arg = shlex.quote(object_file)
|
||||
|
||||
# List all defined global symbols (text section) in the object file
|
||||
cmd = "nm --defined-only --extern-only {} | grep ' T ' | awk '{{print $3}}'".format(safe_arg)
|
||||
try:
|
||||
output = subprocess.check_output(cmd, shell=True, text=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error running nm on '{object_file}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
defined_syms = set(output.split()) # faster lookup
|
||||
|
||||
for wrapped_method in methods:
|
||||
# On macOS LLVM, symbols may have a leading underscore
|
||||
if wrapped_method.name in defined_syms or '_' + wrapped_method.name in defined_syms:
|
||||
print(f"_{wrapped_method.name} ___real_{wrapped_method.name}")
|
||||
else:
|
||||
print(f"_{wrapped_method.name} ___wrap_{wrapped_method.name}")
|
||||
|
||||
def main():
|
||||
# Check for single argument
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: generate-redefine-syms.py <input .o file>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
|
||||
# Check file exists and is an object file
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"Error: File '{file_path}' does not exist.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse the source file containing the wrappers
|
||||
wrapped_methods = find_wrapper_functions_in_header('wrappers.h')
|
||||
|
||||
wrap_symbols(wrapped_methods, file_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Autogenerated wrapper generator for OSS Valkey.
|
||||
|
||||
Parses '__wrap_' C function signatures from 'wrappers.h' and generates
|
||||
'generated_wrappers.hpp' and 'generated_wrappers.cpp', providing
|
||||
classes (RealValkey and MockValkey) for gtest-based testing.
|
||||
The generated files wrap the real functions and provide mockable
|
||||
interfaces using GoogleTest.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
from wrapper_util import find_wrapper_functions_in_header
|
||||
|
||||
|
||||
def generate_header(header_file, methods):
|
||||
# Write the generated_wrappers hpp file
|
||||
with open(header_file, 'w') as f:
|
||||
f.write('''#ifndef __GENERATED_WRAPPERS_HPP
|
||||
#define __GENERATED_WRAPPERS_HPP
|
||||
|
||||
// AUTOGENERATED - DO NOT MODIFY
|
||||
|
||||
#include "../fmacros.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "wrappers.h"
|
||||
#include "custom_matchers.hpp"
|
||||
using namespace ::testing;
|
||||
using ::testing::StrictMock;
|
||||
|
||||
#ifndef GTEST_REMOVE_LEGACY_TEST_CASEAPI_
|
||||
// Older versions and transition versions have 'TestCase' naming available.
|
||||
#define SetUpTestSuite SetUpTestCase
|
||||
#define TearDownTestSuite TearDownTestCase
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
''')
|
||||
|
||||
for m in methods:
|
||||
f.write(" extern %s __real_%s(%s);\n" % (m.ret_type, m.name, m.full_args))
|
||||
|
||||
f.write('''
|
||||
}
|
||||
|
||||
class Valkey {
|
||||
public:
|
||||
virtual ~Valkey() {};
|
||||
''')
|
||||
|
||||
for m in methods:
|
||||
f.write(" virtual %s %s(%s) = 0;\n" % (m.ret_type, m.name, m.arg_string))
|
||||
|
||||
f.write('''
|
||||
};
|
||||
|
||||
class RealValkey : public Valkey {
|
||||
public:
|
||||
virtual ~RealValkey() {};
|
||||
''')
|
||||
|
||||
for m in methods:
|
||||
f.write('''
|
||||
virtual %s %s(%s) {
|
||||
%s__real_%s(%s);
|
||||
}''' % (m.ret_type, m.name, m.arg_string, 'return ' if m.ret_type != 'void' else '', m.name, ','.join(x.name for x in m.args)))
|
||||
|
||||
f.write('''
|
||||
};
|
||||
|
||||
class MockValkey : public Valkey {
|
||||
protected:
|
||||
RealValkey _real;
|
||||
|
||||
public:
|
||||
MockValkey();
|
||||
virtual ~MockValkey();
|
||||
''')
|
||||
|
||||
for m in methods:
|
||||
f.write(" MOCK_METHOD%d(%s, %s(%s));\n" % (len(m.args), m.name, m.ret_type, m.arg_string))
|
||||
|
||||
f.write('''};
|
||||
#endif
|
||||
''')
|
||||
|
||||
|
||||
def generate_cpp(cpp_file, methods):
|
||||
with open(cpp_file, 'w') as f:
|
||||
f.write('''// AUTOGENERATED - DO NOT MODIFY
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
// A global pointer to our current test's MockValkey
|
||||
// so that the C valkey wrappers can delegate to the
|
||||
// correct mock
|
||||
MockValkey* globalValkey = NULL;
|
||||
|
||||
extern "C" {
|
||||
''')
|
||||
|
||||
for m in methods:
|
||||
prefix = 'return ' if m.ret_type != 'void' else ''
|
||||
names = ','.join(x.name for x in m.args)
|
||||
f.write('''
|
||||
%s __wrap_%s(%s) {
|
||||
// Delegate to the global valkey if it is initialized
|
||||
if (globalValkey != NULL) {
|
||||
%sglobalValkey->%s(%s);
|
||||
} else {
|
||||
%s__real_%s(%s);
|
||||
}
|
||||
}''' % (m.ret_type, m.name, m.full_args, prefix, m.name, names, prefix, m.name, names))
|
||||
|
||||
f.write('''
|
||||
}
|
||||
|
||||
MockValkey::MockValkey() {
|
||||
// Set the global valkey to the current
|
||||
// instance
|
||||
globalValkey = this;
|
||||
''')
|
||||
for m in methods:
|
||||
args = ','.join('_' for x in m.args)
|
||||
f.write(" EXPECT_CALL(*globalValkey, %s(%s)).WillRepeatedly(Invoke(&_real, &RealValkey::%s));\n" % (m.name, args, m.name))
|
||||
|
||||
f.write('''
|
||||
}
|
||||
|
||||
MockValkey::~MockValkey() {
|
||||
// Unset the global valkey
|
||||
globalValkey = NULL;
|
||||
}
|
||||
''')
|
||||
|
||||
def main():
|
||||
# Determine the output directory
|
||||
output_dir = "" if len(sys.argv) == 1 else f"{sys.argv[1]}/"
|
||||
|
||||
# Parse the source file containing the wrappers
|
||||
methods = find_wrapper_functions_in_header('wrappers.h')
|
||||
|
||||
# Do not generate the file if not needed
|
||||
generated_header = f"{output_dir}generated_wrappers.hpp"
|
||||
generated_cpp = f"{output_dir}generated_wrappers.cpp"
|
||||
|
||||
generated_output_last_modified = 0 if not os.path.exists(generated_header) else os.path.getmtime(generated_header)
|
||||
input_last_modified = os.path.getmtime('wrappers.h')
|
||||
|
||||
if generated_output_last_modified > input_last_modified:
|
||||
# Output files are up-to-date
|
||||
print(f"{generated_header} is up-to-date")
|
||||
print(f"{generated_cpp} is up-to-date")
|
||||
os.sys.exit(0)
|
||||
|
||||
generate_header(generated_header, methods)
|
||||
generate_cpp(generated_cpp, methods)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
extern "C" {
|
||||
#include "sha256.h"
|
||||
#include "util.h"
|
||||
}
|
||||
|
||||
|
||||
bool accurate = false;
|
||||
bool large_memory = false;
|
||||
bool valgrind = false;
|
||||
char *seed = nullptr;
|
||||
int test_argc = 0;
|
||||
char **test_argv = nullptr;
|
||||
|
||||
bool hasFlag(int argc, char **argv, const char *flag) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], flag) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
char *getFlagValue(int argc, char **argv, const char *flag) {
|
||||
for (int i = 1; i < argc - 1; i++) {
|
||||
if (strcmp(argv[i], flag) == 0) return argv[i + 1];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
accurate = hasFlag(argc, argv, "--accurate");
|
||||
large_memory = hasFlag(argc, argv, "--large-memory");
|
||||
valgrind = hasFlag(argc, argv, "--valgrind");
|
||||
seed = getFlagValue(argc, argv, "--seed");
|
||||
if (seed) {
|
||||
unsigned int seed_value = (unsigned int)atoi(seed);
|
||||
srandom(seed_value);
|
||||
srand(seed_value);
|
||||
|
||||
// Convert the seed to a 128-character hexadecimal string
|
||||
// by hashing it with SHA256 twice (to get 64 bytes = 128 hex chars)
|
||||
char seed_hex[129];
|
||||
SHA256_CTX ctx;
|
||||
unsigned char hash[SHA256_BLOCK_SIZE];
|
||||
|
||||
// First hash
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, (const unsigned char *)seed, strlen(seed));
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
// Convert first hash to hex (32 bytes = 64 hex chars)
|
||||
for (int i = 0; i < SHA256_BLOCK_SIZE; i++) {
|
||||
snprintf(seed_hex + (i * 2), 3, "%02X", hash[i]);
|
||||
}
|
||||
|
||||
// Second hash to get another 32 bytes
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, hash, SHA256_BLOCK_SIZE);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
// Convert second hash to hex (32 bytes = 64 hex chars)
|
||||
for (int i = 0; i < SHA256_BLOCK_SIZE; i++) {
|
||||
snprintf(seed_hex + 64 + (i * 2), 3, "%02X", hash[i]);
|
||||
}
|
||||
seed_hex[128] = '\0';
|
||||
|
||||
// Now we have a 128-character hex string
|
||||
setRandomSeedCString(seed_hex, strlen(seed_hex));
|
||||
}
|
||||
|
||||
// The following line must be executed to initialize GoogleTest before running the tests.
|
||||
::testing::InitGoogleMock(&argc, argv);
|
||||
|
||||
test_argc = argc;
|
||||
test_argv = argv;
|
||||
int result = RUN_ALL_TESTS();
|
||||
if (result == 0) {
|
||||
printf("\033[32mAll UNIT TESTS PASSED!\033[0m\n");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "test_help.h"
|
||||
|
||||
#include "../fmacros.h"
|
||||
#include "../config.h"
|
||||
#include "../zmalloc.h"
|
||||
extern "C" {
|
||||
#include "config.h"
|
||||
#include "fmacros.h"
|
||||
#include "zmalloc.h"
|
||||
|
||||
extern long long popcountScalar(void *s, long count);
|
||||
#if HAVE_X86_SIMD
|
||||
@@ -14,6 +22,7 @@ extern long long popcountAVX2(void *s, long count);
|
||||
#if HAVE_ARM_NEON
|
||||
extern long long popcountNEON(void *s, long count);
|
||||
#endif
|
||||
}
|
||||
|
||||
static long long bitcount(void *s, long count) {
|
||||
long long bits = 0;
|
||||
@@ -28,9 +37,11 @@ static long long bitcount(void *s, long count) {
|
||||
return bits;
|
||||
}
|
||||
|
||||
static int test_case(const char *msg, int size) {
|
||||
static void test_case(const char *msg, int size) {
|
||||
size_t bufsize = size > 0 ? size : 1;
|
||||
uint8_t buf[bufsize];
|
||||
uint8_t *buf = (uint8_t *)malloc(bufsize);
|
||||
ASSERT_NE(buf, nullptr) << msg;
|
||||
|
||||
int fuzzing = 1000;
|
||||
for (int y = 0; y < fuzzing; y += 1) {
|
||||
for (int z = 0; z < size; z += 1) {
|
||||
@@ -39,45 +50,37 @@ static int test_case(const char *msg, int size) {
|
||||
|
||||
long long expect = bitcount(buf, size);
|
||||
long long ret_scalar = popcountScalar(buf, size);
|
||||
TEST_ASSERT_MESSAGE(msg, expect == ret_scalar);
|
||||
ASSERT_EQ(expect, ret_scalar) << msg;
|
||||
#if HAVE_X86_SIMD
|
||||
long long ret_avx2 = popcountAVX2(buf, size);
|
||||
TEST_ASSERT_MESSAGE(msg, expect == ret_avx2);
|
||||
ASSERT_EQ(expect, ret_avx2) << msg;
|
||||
#endif
|
||||
#if HAVE_ARM_NEON
|
||||
long long ret_neon = popcountNEON(buf, size);
|
||||
TEST_ASSERT_MESSAGE(msg, expect == ret_neon);
|
||||
ASSERT_EQ(expect, ret_neon) << msg;
|
||||
#endif
|
||||
}
|
||||
|
||||
return 0;
|
||||
free(buf);
|
||||
}
|
||||
|
||||
int test_popcount(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
/* Minimal test fixture */
|
||||
class BitopsTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
#define TEST_CASE(MSG, SIZE) \
|
||||
if (test_case("Test failed: " MSG, SIZE)) { \
|
||||
return 1; \
|
||||
}
|
||||
|
||||
/* The AVX2 version divides the array into the following 3 parts."
|
||||
TEST_F(BitopsTest, TestPopcount) {
|
||||
/* The AVX2 version divides the array into the following 3 parts.
|
||||
* Part A Part B Part C
|
||||
* +-----------------+--------------+---------+
|
||||
* | 8 * 32bytes * X | 32bytes * Y | Z bytes |
|
||||
* +-----------------+--------------+---------+
|
||||
*/
|
||||
/* So we test the following cases */
|
||||
TEST_CASE("Popcount(Part A)", 8 * 32 * 2);
|
||||
TEST_CASE("Popcount(Part B)", 32 * 2);
|
||||
TEST_CASE("Popcount(Part C)", 2);
|
||||
TEST_CASE("Popcount(Part A + Part B)", 8 * 32 * 7 + 32 * 2);
|
||||
TEST_CASE("Popcount(Part A + Part C)", 8 * 32 * 11 + 7);
|
||||
TEST_CASE("Popcount(Part A + Part B + Part C)", 8 * 32 * 3 + 3 * 32 + 5);
|
||||
TEST_CASE("Popcount(Corner case)", 0);
|
||||
#undef TEST_CASE
|
||||
|
||||
return 0;
|
||||
test_case("Popcount(Part A)", 8 * 32 * 2);
|
||||
test_case("Popcount(Part B)", 32 * 2);
|
||||
test_case("Popcount(Part C)", 2);
|
||||
test_case("Popcount(Part A + Part B)", 8 * 32 * 7 + 32 * 2);
|
||||
test_case("Popcount(Part A + Part C)", 8 * 32 * 11 + 7);
|
||||
test_case("Popcount(Part A + Part B + Part C)", 8 * 32 * 3 + 3 * 32 + 5);
|
||||
test_case("Popcount(Corner case)", 0);
|
||||
}
|
||||
@@ -1,20 +1,32 @@
|
||||
#include <stdio.h>
|
||||
#include "../fmacros.h"
|
||||
#include "../crc64.h"
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "test_help.h"
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
extern "C" {
|
||||
#include "crc64.h"
|
||||
#include "fmacros.h"
|
||||
|
||||
extern uint64_t _crc64(uint_fast64_t crc, const void *in_data, const uint64_t len);
|
||||
}
|
||||
|
||||
int test_crc64(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
crc64_init();
|
||||
class Crc64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
crc64_init();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(Crc64Test, TestCrc64) {
|
||||
unsigned char numbers[] = "123456789";
|
||||
TEST_ASSERT_MESSAGE("[calcula]: CRC64 '123456789'", (uint64_t)_crc64(0, numbers, 9) == 16845390139448941002ull);
|
||||
TEST_ASSERT_MESSAGE("[calcula]: CRC64 '123456789'", (uint64_t)crc64(0, numbers, 9) == 16845390139448941002ull);
|
||||
ASSERT_EQ((uint64_t)_crc64(0, numbers, 9), 16845390139448941002ull) << "[calcula]: CRC64 '123456789'";
|
||||
ASSERT_EQ((uint64_t)crc64(0, numbers, 9), 16845390139448941002ull) << "[calcula]: CRC64 '123456789'";
|
||||
|
||||
unsigned char li[] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed "
|
||||
"do eiusmod tempor incididunt ut labore et dolore magna "
|
||||
@@ -24,8 +36,6 @@ int test_crc64(int argc, char **argv, int flags) {
|
||||
"cillum dolore eu fugiat nulla pariatur. Excepteur sint "
|
||||
"occaecat cupidatat non proident, sunt in culpa qui officia "
|
||||
"deserunt mollit anim id est laborum.";
|
||||
|
||||
TEST_ASSERT_MESSAGE("[calcula]: CRC64 TEXT'", (uint64_t)_crc64(0, li, sizeof(li)) == 14373597793578550195ull);
|
||||
TEST_ASSERT_MESSAGE("[calcula]: CRC64 TEXT", (uint64_t)crc64(0, li, sizeof(li)) == 14373597793578550195ull);
|
||||
return 0;
|
||||
ASSERT_EQ((uint64_t)_crc64(0, li, sizeof(li)), 14373597793578550195ull) << "[calcula]: CRC64 TEXT'";
|
||||
ASSERT_EQ((uint64_t)crc64(0, li, sizeof(li)), 14373597793578550195ull) << "[calcula]: CRC64 TEXT";
|
||||
}
|
||||
@@ -1,20 +1,29 @@
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "test_help.h"
|
||||
#include "../fmacros.h"
|
||||
#include "../zmalloc.h"
|
||||
#include "../crc64.h"
|
||||
#include "../crcspeed.h"
|
||||
#include "../crccombine.h"
|
||||
extern "C" {
|
||||
#include "crc64.h"
|
||||
#include "crccombine.h"
|
||||
#include "crcspeed.h"
|
||||
#include "fmacros.h"
|
||||
#include "zmalloc.h"
|
||||
}
|
||||
|
||||
static void genBenchmarkRandomData(char *data, int count);
|
||||
static int bench_crc64(unsigned char *data, uint64_t size, long long passes, uint64_t check, char *name, int csv);
|
||||
static void bench_combine(char *label, uint64_t size, uint64_t expect, int csv);
|
||||
extern int test_argc;
|
||||
extern char **test_argv;
|
||||
|
||||
long long _ustime(void) {
|
||||
struct timeval tv;
|
||||
@@ -26,9 +35,10 @@ long long _ustime(void) {
|
||||
return ust;
|
||||
}
|
||||
|
||||
static int bench_crc64(unsigned char *data, uint64_t size, long long passes, uint64_t check, char *name, int csv) {
|
||||
static int bench_crc64(unsigned char *data, uint64_t size, long long passes, uint64_t check, const char *name, int csv) {
|
||||
uint64_t min = size, hash = 0;
|
||||
long long original_start = _ustime(), original_end;
|
||||
|
||||
for (long long i = passes; i > 0; i--) {
|
||||
hash = crc64(0, data, size);
|
||||
}
|
||||
@@ -38,17 +48,18 @@ static int bench_crc64(unsigned char *data, uint64_t size, long long passes, uin
|
||||
if (csv) {
|
||||
printf("%s,%" PRIu64 ",%" PRIu64 ",%d\n", name, size, (1000 * size) / min, hash == check);
|
||||
} else {
|
||||
TEST_PRINT_INFO("test size=%" PRIu64 " algorithm=%s %" PRIu64 " M/sec matches=%d", size, name,
|
||||
(1000 * size) / min, hash == check);
|
||||
printf("test size=%" PRIu64 " algorithm=%s %" PRIu64 " M/sec matches=%d\n", size, name,
|
||||
(1000 * size) / min, hash == check);
|
||||
}
|
||||
return hash != check;
|
||||
}
|
||||
|
||||
const uint64_t BENCH_RPOLY = UINT64_C(0x95ac9329ac4bc9b5);
|
||||
|
||||
static void bench_combine(char *label, uint64_t size, uint64_t expect, int csv) {
|
||||
static void bench_combine(const char *label, uint64_t size, uint64_t expect, int csv) {
|
||||
uint64_t min = size, start = expect, thash = expect ^ (expect >> 17);
|
||||
long long original_start = _ustime(), original_end;
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
crc64_combine(thash, start, size, BENCH_RPOLY, 64);
|
||||
}
|
||||
@@ -72,19 +83,18 @@ static void genBenchmarkRandomData(char *data, int count) {
|
||||
}
|
||||
}
|
||||
|
||||
/* This is a special unit test useful for benchmarking crc64combine performance. The
|
||||
* benchmarking is only done when the tests are invoked with a single test target,
|
||||
* like 'valkey-unit-tests --single test_crc64combine.c --crc 16384'. */
|
||||
int test_crc64combine(int argc, char **argv, int flags) {
|
||||
if (!(flags & UNIT_TEST_SINGLE)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* This is a special unit test useful for benchmarking crc64combine performance. The
|
||||
* benchmarking is only done when the tests are invoked with specific arguments,
|
||||
* like './src/unit/valkey-unit-gtests --gtest_filter='*Crc64Combine*' --gtest_also_run_disabled_tests -- --crc 16384'. */
|
||||
int test_crc64combine_impl(int argc, char **argv) {
|
||||
uint64_t crc64_test_size = 0;
|
||||
int i, lastarg, csv = 0, loop = 0, combine = 0;
|
||||
|
||||
again:
|
||||
for (i = 3; i < argc; i++) {
|
||||
for (i = 1; i < argc; i++) {
|
||||
lastarg = (i == (argc - 1));
|
||||
|
||||
if (!strcmp(argv[i], "--help")) {
|
||||
goto usage;
|
||||
} else if (!strcmp(argv[i], "--csv")) {
|
||||
@@ -96,11 +106,17 @@ again:
|
||||
crc64_test_size = atoll(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--combine")) {
|
||||
combine = 1;
|
||||
} else if (argv[i][0] == '-' && argv[i][1] == '-' && argv[i][2] == '\0') {
|
||||
/* Skip "--" separator */
|
||||
continue;
|
||||
} else if (strncmp(argv[i], "--gtest", 7) == 0) {
|
||||
/* Skip gtest arguments */
|
||||
continue;
|
||||
} else {
|
||||
invalid:
|
||||
printf("Invalid option \"%s\" or option argument missing\n\n", argv[i]);
|
||||
usage:
|
||||
printf("Usage: --single test_crc64combine.c [OPTIONS]\n\n"
|
||||
printf("Usage: ./src/unit/valkey-unit-gtests --gtest_filter='*Crc64Combine*' --gtest_also_run_disabled_tests -- --[OPTIONS]\n\n"
|
||||
" --csv Output in CSV format\n"
|
||||
" -l Loop. Run the tests forever\n"
|
||||
" --crc <bytes> Benchmark crc64 faster options, using a buffer this big, and quit when done.\n"
|
||||
@@ -109,18 +125,23 @@ again:
|
||||
}
|
||||
}
|
||||
|
||||
/* If no --crc size specified, use a default */
|
||||
if (crc64_test_size == 0) {
|
||||
crc64_test_size = 16384;
|
||||
}
|
||||
|
||||
int init_this_loop = 1;
|
||||
long long init_start, init_end;
|
||||
|
||||
do {
|
||||
unsigned char *data = NULL;
|
||||
uint64_t passes = 0;
|
||||
|
||||
if (crc64_test_size) {
|
||||
data = zmalloc(crc64_test_size);
|
||||
data = (unsigned char *)zmalloc(crc64_test_size);
|
||||
genBenchmarkRandomData((char *)data, crc64_test_size);
|
||||
/* We want to hash about 1 gig of data in total, looped, to get a good
|
||||
* idea of our performance.
|
||||
*/
|
||||
* idea of our performance. */
|
||||
passes = (UINT64_C(0x100000000) / crc64_test_size);
|
||||
passes = passes >= 2 ? passes : 2;
|
||||
passes = passes <= 1000 ? passes : 1000;
|
||||
@@ -172,32 +193,49 @@ again:
|
||||
}
|
||||
|
||||
uint64_t INIT_SIZE = UINT64_C(0xffffffffffffffff);
|
||||
|
||||
if (combine) {
|
||||
if (init_this_loop) {
|
||||
init_start = _ustime();
|
||||
crc64_combine(UINT64_C(0xdeadbeefdeadbeef), UINT64_C(0xfeebdaedfeebdaed), INIT_SIZE, BENCH_RPOLY, 64);
|
||||
init_end = _ustime();
|
||||
|
||||
init_end -= init_start;
|
||||
init_end *= 1000;
|
||||
|
||||
if (csv) {
|
||||
printf("operation,size,nanoseconds\n");
|
||||
printf("init_64,%" PRIu64 ",%" PRIu64 "\n", INIT_SIZE, (uint64_t)init_end);
|
||||
} else {
|
||||
TEST_PRINT_INFO("init_64 size=%" PRIu64 " in %" PRIu64 " nsec", INIT_SIZE, (uint64_t)init_end);
|
||||
printf("init_64 size=%" PRIu64 " in %" PRIu64 " nsec\n", INIT_SIZE, (uint64_t)init_end);
|
||||
}
|
||||
|
||||
/* use the hash itself as the size (unpredictable) */
|
||||
bench_combine("hash_as_size_combine", crc64_test_size, expect, csv);
|
||||
|
||||
/* let's do something big (predictable, so fast) */
|
||||
bench_combine("largest_combine", INIT_SIZE, expect, csv);
|
||||
}
|
||||
bench_combine("combine", crc64_test_size, expect, csv);
|
||||
}
|
||||
|
||||
init_this_loop = 0;
|
||||
/* step down by ~1.641 for a range of test sizes */
|
||||
crc64_test_size -= (crc64_test_size >> 2) + (crc64_test_size >> 3) + (crc64_test_size >> 6);
|
||||
} while (crc64_test_size > 3);
|
||||
|
||||
if (loop) goto again;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
class Crc64CombineBenchmark : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp(void) override {
|
||||
crc64_init();
|
||||
}
|
||||
};
|
||||
|
||||
/* Benchmark test for CRC64 - disabled by default.
|
||||
* Run with: ./src/unit/valkey-unit-gtests --gtest_filter='*Crc64Combine*' --gtest_also_run_disabled_tests -- --crc 16384 */
|
||||
TEST_F(Crc64CombineBenchmark, DISABLED_Benchmark) {
|
||||
ASSERT_EQ(test_crc64combine_impl(test_argc, test_argv), 0);
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
#include "../dict.c"
|
||||
#include "test_help.h"
|
||||
|
||||
uint64_t hashCallback(const void *key) {
|
||||
return dictGenHashFunction((unsigned char *)key, strlen((char *)key));
|
||||
}
|
||||
|
||||
int compareCallback(const void *key1, const void *key2) {
|
||||
int l1, l2;
|
||||
l1 = strlen((char *)key1);
|
||||
l2 = strlen((char *)key2);
|
||||
if (l1 != l2) return 0;
|
||||
return memcmp(key1, key2, l1) == 0;
|
||||
}
|
||||
|
||||
void freeCallback(void *val) {
|
||||
zfree(val);
|
||||
}
|
||||
|
||||
char *stringFromLongLong(long long value) {
|
||||
char buf[32];
|
||||
int len;
|
||||
char *s;
|
||||
|
||||
len = snprintf(buf, sizeof(buf), "%lld", value);
|
||||
s = zmalloc(len + 1);
|
||||
memcpy(s, buf, len);
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
dictType BenchmarkDictType = {hashCallback, NULL, compareCallback, freeCallback, NULL, NULL};
|
||||
|
||||
#define start_benchmark() start = timeInMilliseconds()
|
||||
#define end_benchmark(msg) \
|
||||
do { \
|
||||
elapsed = timeInMilliseconds() - start; \
|
||||
printf(msg ": %ld items in %lld ms\n", count, elapsed); \
|
||||
} while (0)
|
||||
|
||||
static dict *_dict = NULL;
|
||||
static long j;
|
||||
static int retval;
|
||||
static unsigned long new_dict_size, current_dict_used, remain_keys;
|
||||
|
||||
int test_dictCreate(int argc, char **argv, int flags) {
|
||||
_dict = dictCreate(&BenchmarkDictType);
|
||||
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
monotonicInit(); /* Required for dict tests, that are relying on monotime during dict rehashing. */
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictAdd16Keys(int argc, char **argv, int flags) {
|
||||
/* Add 16 keys and verify dict resize is ok */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
for (j = 0; j < 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
TEST_ASSERT(dictSize(_dict) == 16);
|
||||
TEST_ASSERT(dictBuckets(_dict) == 16);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictDisableResize(int argc, char **argv, int flags) {
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict resize and pad to (dict_force_resize_ratio * 16) */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict resize, and pad
|
||||
* the number of keys to (dict_force_resize_ratio * 16), so we can satisfy
|
||||
* dict_force_resize_ratio in next test. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
for (j = 16; j < (long)dict_force_resize_ratio * 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
current_dict_used = dict_force_resize_ratio * 16;
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(dictBuckets(_dict) == 16);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictAddOneKeyTriggerResize(int argc, char **argv, int flags) {
|
||||
/* Add one more key, trigger the dict resize */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
retval = dictAdd(_dict, stringFromLongLong(current_dict_used), (void *)(current_dict_used));
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
current_dict_used++;
|
||||
new_dict_size = 1UL << dictNextExp(current_dict_used);
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == 16);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == new_dict_size);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == new_dict_size);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictDeleteKeys(int argc, char **argv, int flags) {
|
||||
/* Delete keys until we can trigger shrink in next test */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* Delete keys until we can satisfy (1 / HASHTABLE_MIN_FILL) in the next test. */
|
||||
for (j = new_dict_size / HASHTABLE_MIN_FILL + 1; j < (long)current_dict_used; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
current_dict_used = new_dict_size / HASHTABLE_MIN_FILL + 1;
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == new_dict_size);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictDeleteOneKeyTriggerResize(int argc, char **argv, int flags) {
|
||||
/* Delete one more key, trigger the dict resize */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
current_dict_used--;
|
||||
char *key = stringFromLongLong(current_dict_used);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
unsigned long oldDictSize = new_dict_size;
|
||||
new_dict_size = 1UL << dictNextExp(current_dict_used);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == oldDictSize);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == new_dict_size);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == new_dict_size);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictEmptyDirAdd128Keys(int argc, char **argv, int flags) {
|
||||
/* Empty the dictionary and add 128 keys */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
dictEmpty(_dict, NULL);
|
||||
for (j = 0; j < 128; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
TEST_ASSERT(dictSize(_dict) == 128);
|
||||
TEST_ASSERT(dictBuckets(_dict) == 128);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictDisableResizeReduceTo3(int argc, char **argv, int flags) {
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict resize and reduce to 3 */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict reset, and reduce
|
||||
* the number of keys until we can trigger shrinking in next test. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
remain_keys = DICTHT_SIZE(_dict->ht_size_exp[0]) / (HASHTABLE_MIN_FILL * dict_force_resize_ratio) + 1;
|
||||
for (j = remain_keys; j < 128; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
current_dict_used = remain_keys;
|
||||
TEST_ASSERT(dictSize(_dict) == remain_keys);
|
||||
TEST_ASSERT(dictBuckets(_dict) == 128);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictDeleteOneKeyTriggerResizeAgain(int argc, char **argv, int flags) {
|
||||
/* Delete one more key, trigger the dict resize */
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
current_dict_used--;
|
||||
char *key = stringFromLongLong(current_dict_used);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
new_dict_size = 1UL << dictNextExp(current_dict_used);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == 128);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == new_dict_size);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
TEST_ASSERT(dictSize(_dict) == current_dict_used);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[0]) == new_dict_size);
|
||||
TEST_ASSERT(DICTHT_SIZE(_dict->ht_size_exp[1]) == 0);
|
||||
|
||||
/* This is the last one, restore to original state */
|
||||
dictRelease(_dict);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_dictBenchmark(int argc, char **argv, int flags) {
|
||||
long j;
|
||||
long long start, elapsed;
|
||||
int retval;
|
||||
dict *dict = dictCreate(&BenchmarkDictType);
|
||||
long count = 0;
|
||||
int accurate = (flags & UNIT_TEST_ACCURATE);
|
||||
|
||||
if (argc == 4) {
|
||||
if (accurate) {
|
||||
count = 5000000;
|
||||
} else {
|
||||
count = strtol(argv[3], NULL, 10);
|
||||
}
|
||||
} else {
|
||||
count = 5000;
|
||||
}
|
||||
|
||||
monotonicInit(); /* Required for dict tests, that are relying on monotime during dict rehashing. */
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
retval = dictAdd(dict, stringFromLongLong(j), (void *)j);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
end_benchmark("Inserting");
|
||||
TEST_ASSERT((long)dictSize(dict) == count);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
while (dictIsRehashing(dict)) {
|
||||
dictRehashMicroseconds(dict, 100 * 1000);
|
||||
}
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
TEST_ASSERT(de != NULL);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Linear access of existing elements");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
TEST_ASSERT(de != NULL);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Linear access of existing elements (2nd round)");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(rand() % count);
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
TEST_ASSERT(de != NULL);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Random access of existing elements");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
dictEntry *de = dictGetRandomKey(dict);
|
||||
TEST_ASSERT(de != NULL);
|
||||
}
|
||||
end_benchmark("Accessing random keys");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(rand() % count);
|
||||
key[0] = 'X';
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
TEST_ASSERT(de == NULL);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Accessing missing");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(dict, key);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
key[0] += 17; /* Change first number to letter. */
|
||||
retval = dictAdd(dict, key, (void *)j);
|
||||
TEST_ASSERT(retval == DICT_OK);
|
||||
}
|
||||
end_benchmark("Removing and adding");
|
||||
dictRelease(dict);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <sys/time.h>
|
||||
|
||||
extern "C" {
|
||||
#include "dict.h"
|
||||
|
||||
extern bool accurate;
|
||||
/* Wrapper function declarations for accessing static dict.c internals */
|
||||
unsigned int testOnlyDictGetForceResizeRatio(void);
|
||||
signed char testOnlyDictNextExp(unsigned long size);
|
||||
long long testOnlyTimeInMilliseconds(void);
|
||||
}
|
||||
|
||||
uint64_t hashCallback(const void *key) {
|
||||
return dictGenHashFunction((const unsigned char *)key, strlen((const char *)key));
|
||||
}
|
||||
|
||||
int compareCallback(const void *key1, const void *key2) {
|
||||
int l1, l2;
|
||||
l1 = strlen((const char *)key1);
|
||||
l2 = strlen((const char *)key2);
|
||||
if (l1 != l2) return 0;
|
||||
return memcmp(key1, key2, l1) == 0;
|
||||
}
|
||||
|
||||
void freeCallback(void *val) {
|
||||
zfree(val);
|
||||
}
|
||||
|
||||
char *stringFromLongLong(long long value) {
|
||||
char buf[32];
|
||||
int len;
|
||||
char *s;
|
||||
|
||||
len = snprintf(buf, sizeof(buf), "%lld", value);
|
||||
s = (char *)zmalloc(len + 1);
|
||||
memcpy(s, buf, len);
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
dictType BenchmarkDictType = {hashCallback, nullptr, compareCallback, freeCallback, nullptr, nullptr};
|
||||
|
||||
class DictTest : public ::testing::Test {
|
||||
protected:
|
||||
dict *_dict = nullptr;
|
||||
long j;
|
||||
int retval;
|
||||
unsigned long new_dict_size, current_dict_used, remain_keys;
|
||||
|
||||
static void SetUpTestSuite() {
|
||||
monotonicInit(); /* Required for dict tests, that are relying on monotime during dict rehashing. */
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
_dict = dictCreate(&BenchmarkDictType);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (_dict) {
|
||||
dictRelease(_dict);
|
||||
_dict = nullptr;
|
||||
}
|
||||
/* Restore global resize mode to avoid leaking state across tests. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DictTest, dictAdd16Keys) {
|
||||
/* Add 16 keys and verify dict resize is ok */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
for (j = 0; j < 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
ASSERT_EQ(dictSize(_dict), 16u);
|
||||
ASSERT_EQ(dictBuckets(_dict), 16u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictDisableResize) {
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict resize and pad to (dict_force_resize_ratio * 16) */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
for (j = 0; j < 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict resize, and pad
|
||||
* the number of keys to (dict_force_resize_ratio * 16), so we can satisfy
|
||||
* dict_force_resize_ratio in next test. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
for (j = 16; j < (long)testOnlyDictGetForceResizeRatio() * 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = testOnlyDictGetForceResizeRatio() * 16;
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(dictBuckets(_dict), 16u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictAddOneKeyTriggerResize) {
|
||||
/* Add one more key, trigger the dict resize */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
for (j = 0; j < 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
for (j = 16; j < (long)testOnlyDictGetForceResizeRatio() * 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = testOnlyDictGetForceResizeRatio() * 16;
|
||||
|
||||
retval = dictAdd(_dict, stringFromLongLong(current_dict_used), (void *)current_dict_used);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
current_dict_used++;
|
||||
new_dict_size = 1UL << testOnlyDictNextExp(current_dict_used);
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), 16u);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), new_dict_size);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), new_dict_size);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), 0u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictDeleteKeys) {
|
||||
/* Delete keys until we can trigger shrink in next test */
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
for (j = 0; j < 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
for (j = 16; j < (long)testOnlyDictGetForceResizeRatio() * 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = testOnlyDictGetForceResizeRatio() * 16;
|
||||
|
||||
retval = dictAdd(_dict, stringFromLongLong(current_dict_used), (void *)current_dict_used);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
current_dict_used++;
|
||||
new_dict_size = 1UL << testOnlyDictNextExp(current_dict_used);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
/* Delete keys until we can satisfy (1 / HASHTABLE_MIN_FILL) in the next test. */
|
||||
for (j = new_dict_size / HASHTABLE_MIN_FILL + 1; j < (long)current_dict_used; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = new_dict_size / HASHTABLE_MIN_FILL + 1;
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), new_dict_size);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), 0u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictDeleteOneKeyTriggerResize) {
|
||||
/* Delete one more key, trigger the dict resize */
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
for (j = 0; j < 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
for (j = 16; j < (long)testOnlyDictGetForceResizeRatio() * 16; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = testOnlyDictGetForceResizeRatio() * 16;
|
||||
|
||||
retval = dictAdd(_dict, stringFromLongLong(current_dict_used), (void *)current_dict_used);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
current_dict_used++;
|
||||
new_dict_size = 1UL << testOnlyDictNextExp(current_dict_used);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
for (j = new_dict_size / HASHTABLE_MIN_FILL + 1; j < (long)current_dict_used; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = new_dict_size / HASHTABLE_MIN_FILL + 1;
|
||||
|
||||
current_dict_used--;
|
||||
char *key = stringFromLongLong(current_dict_used);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
unsigned long oldDictSize = new_dict_size;
|
||||
new_dict_size = 1UL << testOnlyDictNextExp(current_dict_used);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), oldDictSize);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), new_dict_size);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), new_dict_size);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), 0u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictEmptyDirAdd128Keys) {
|
||||
/* Empty the dictionary and add 128 keys */
|
||||
dictEmpty(_dict, nullptr);
|
||||
for (j = 0; j < 128; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
ASSERT_EQ(dictSize(_dict), 128u);
|
||||
ASSERT_EQ(dictBuckets(_dict), 128u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictDisableResizeReduceTo3) {
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict resize and reduce to 3 */
|
||||
|
||||
/* Setup: Add 128 keys */
|
||||
dictEmpty(_dict, nullptr);
|
||||
for (j = 0; j < 128; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
/* Use DICT_RESIZE_AVOID to disable the dict reset, and reduce
|
||||
* the number of keys until we can trigger shrinking in next test. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
remain_keys = DICTHT_SIZE(_dict->ht_size_exp[0]) / (HASHTABLE_MIN_FILL * testOnlyDictGetForceResizeRatio()) + 1;
|
||||
for (j = remain_keys; j < 128; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = remain_keys;
|
||||
ASSERT_EQ(dictSize(_dict), remain_keys);
|
||||
ASSERT_EQ(dictBuckets(_dict), 128u);
|
||||
}
|
||||
|
||||
TEST_F(DictTest, dictDeleteOneKeyTriggerResizeAgain) {
|
||||
/* Delete one more key, trigger the dict resize */
|
||||
|
||||
/* Setup: Add 128 keys and reduce */
|
||||
dictEmpty(_dict, nullptr);
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE); /* Reset resize mode before adding keys */
|
||||
for (j = 0; j < 128; j++) {
|
||||
retval = dictAdd(_dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
|
||||
dictSetResizeEnabled(DICT_RESIZE_AVOID);
|
||||
remain_keys = DICTHT_SIZE(_dict->ht_size_exp[0]) / (HASHTABLE_MIN_FILL * testOnlyDictGetForceResizeRatio()) + 1;
|
||||
for (j = remain_keys; j < 128; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
current_dict_used = remain_keys;
|
||||
|
||||
current_dict_used--;
|
||||
char *key = stringFromLongLong(current_dict_used);
|
||||
retval = dictDelete(_dict, key);
|
||||
zfree(key);
|
||||
new_dict_size = 1UL << testOnlyDictNextExp(current_dict_used);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), 128u);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), new_dict_size);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
dictSetResizeEnabled(DICT_RESIZE_ENABLE);
|
||||
while (dictIsRehashing(_dict)) dictRehashMicroseconds(_dict, 1000);
|
||||
ASSERT_EQ(dictSize(_dict), current_dict_used);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[0]), new_dict_size);
|
||||
ASSERT_EQ(DICTHT_SIZE(_dict->ht_size_exp[1]), 0u);
|
||||
}
|
||||
|
||||
/* This is a benchmark test for dict performance.
|
||||
* To run this test explicitly, use:
|
||||
* ./src/unit/valkey-unit-gtests --gtest_filter=DictTest.DISABLED_dictBenchmark --gtest_also_run_disabled_tests
|
||||
*/
|
||||
TEST_F(DictTest, DISABLED_dictBenchmark) {
|
||||
long j;
|
||||
long long start, elapsed;
|
||||
int retval;
|
||||
dict *dict = dictCreate(&BenchmarkDictType);
|
||||
long count = accurate ? 5000000 : 5000;
|
||||
|
||||
#define start_benchmark() start = testOnlyTimeInMilliseconds()
|
||||
#define end_benchmark(msg) \
|
||||
do { \
|
||||
elapsed = testOnlyTimeInMilliseconds() - start; \
|
||||
printf(msg ": %ld items in %lld ms\n", count, elapsed); \
|
||||
} while (0)
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
retval = dictAdd(dict, stringFromLongLong(j), (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
end_benchmark("Inserting");
|
||||
ASSERT_EQ((long)dictSize(dict), count);
|
||||
|
||||
/* Wait for rehashing. */
|
||||
while (dictIsRehashing(dict)) {
|
||||
dictRehashMicroseconds(dict, 100 * 1000);
|
||||
}
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
ASSERT_NE(de, nullptr);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Linear access of existing elements");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
ASSERT_NE(de, nullptr);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Linear access of existing elements (2nd round)");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(rand() % count);
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
ASSERT_NE(de, nullptr);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Random access of existing elements");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
dictEntry *de = dictGetRandomKey(dict);
|
||||
ASSERT_NE(de, nullptr);
|
||||
}
|
||||
end_benchmark("Accessing random keys");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(rand() % count);
|
||||
key[0] = 'X';
|
||||
dictEntry *de = dictFind(dict, key);
|
||||
ASSERT_EQ(de, nullptr);
|
||||
zfree(key);
|
||||
}
|
||||
end_benchmark("Accessing missing");
|
||||
|
||||
start_benchmark();
|
||||
for (j = 0; j < count; j++) {
|
||||
char *key = stringFromLongLong(j);
|
||||
retval = dictDelete(dict, key);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
key[0] += 17; /* Change first number to letter. */
|
||||
retval = dictAdd(dict, key, (void *)j);
|
||||
ASSERT_EQ(retval, DICT_OK);
|
||||
}
|
||||
end_benchmark("Removing and adding");
|
||||
dictRelease(dict);
|
||||
|
||||
#undef start_benchmark
|
||||
#undef end_benchmark
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "../endianconv.h"
|
||||
#include "test_help.h"
|
||||
|
||||
int test_endianconv(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
char buf[32];
|
||||
|
||||
snprintf(buf, sizeof(buf), "ciaoroma");
|
||||
memrev16(buf);
|
||||
TEST_ASSERT(!strcmp(buf, "icaoroma"));
|
||||
|
||||
snprintf(buf, sizeof(buf), "ciaoroma");
|
||||
memrev32(buf);
|
||||
TEST_ASSERT(!strcmp(buf, "oaicroma"));
|
||||
|
||||
snprintf(buf, sizeof(buf), "ciaoroma");
|
||||
memrev64(buf);
|
||||
TEST_ASSERT(!strcmp(buf, "amoroaic"));
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "endianconv.h"
|
||||
}
|
||||
|
||||
class EndianconvTest : public ::testing::Test {};
|
||||
|
||||
TEST_F(EndianconvTest, TestEndianconv) {
|
||||
char buf[32];
|
||||
|
||||
snprintf(buf, sizeof(buf), "ciaoroma");
|
||||
memrev16(buf);
|
||||
ASSERT_STREQ(buf, "icaoroma");
|
||||
|
||||
snprintf(buf, sizeof(buf), "ciaoroma");
|
||||
memrev32(buf);
|
||||
ASSERT_STREQ(buf, "oaicroma");
|
||||
|
||||
snprintf(buf, sizeof(buf), "ciaoroma");
|
||||
memrev64(buf);
|
||||
ASSERT_STREQ(buf, "amoroaic");
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
#include "../fmacros.h"
|
||||
#include "../entry.h"
|
||||
#include "test_help.h"
|
||||
#include "../expire.h"
|
||||
#include "../monotonic.h"
|
||||
#include "../server.h"
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "entry.h"
|
||||
#include "expire.h"
|
||||
#include "fmacros.h"
|
||||
#include "monotonic.h"
|
||||
#include "server.h"
|
||||
}
|
||||
|
||||
/* Constants for test values */
|
||||
#define SHORT_FIELD "foo"
|
||||
@@ -15,30 +25,28 @@
|
||||
#define LONG_FIELD "k:123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
#define LONG_VALUE "v:12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
|
||||
/* Verify entry properties */
|
||||
static int verify_entry_properties(entry *e, sds field, sds value_copy, long long expiry, bool has_expiry, bool has_valueptr) {
|
||||
TEST_ASSERT(sdscmp(entryGetField(e), field) == 0);
|
||||
size_t len;
|
||||
TEST_ASSERT(sdscmp(entryGetValue(e, &len), value_copy) == 0);
|
||||
TEST_ASSERT(len == sdslen(value_copy));
|
||||
TEST_ASSERT(entryGetExpiry(e) == expiry);
|
||||
TEST_ASSERT(entryHasExpiry(e) == has_expiry);
|
||||
TEST_ASSERT(entryHasEmbeddedValue(e) != has_valueptr);
|
||||
return 0;
|
||||
}
|
||||
class EntryTest : public ::testing::Test {
|
||||
protected:
|
||||
/* Verify entry properties */
|
||||
void verify_entry_properties(entry *e, sds field, sds value_copy, long long expiry, bool has_expiry, bool has_valueptr) {
|
||||
ASSERT_EQ(sdscmp(entryGetField(e), field), 0);
|
||||
size_t len;
|
||||
ASSERT_EQ(sdscmp(entryGetValue(e, &len), value_copy), 0);
|
||||
ASSERT_EQ(len, sdslen(value_copy));
|
||||
ASSERT_EQ(entryGetExpiry(e), expiry);
|
||||
ASSERT_EQ(entryHasExpiry(e), has_expiry);
|
||||
ASSERT_EQ(entryHasEmbeddedValue(e), !has_valueptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test entryCreate functunallity:
|
||||
* Test entryCreate functionality:
|
||||
* 1. embedded with expiry
|
||||
* 2. embedded without expiry
|
||||
* 3. non-embedded with expiry
|
||||
* 4. non-embedded without expiry
|
||||
*/
|
||||
int test_entryCreate(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(EntryTest, entryCreate) {
|
||||
// Test with embedded value with expiry
|
||||
sds field1 = sdsnew(SHORT_FIELD);
|
||||
sds value1 = sdsnew(SHORT_VALUE);
|
||||
@@ -86,8 +94,6 @@ int test_entryCreate(int argc, char **argv, int flags) {
|
||||
sdsfree(value_copy2);
|
||||
sdsfree(value_copy3);
|
||||
sdsfree(value_copy4);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,11 +109,7 @@ int test_entryCreate(int argc, char **argv, int flags) {
|
||||
* 9. Update entry to more than 3/4 allocation size
|
||||
* 8. Update entry to exactly 3/4 allocation size
|
||||
*/
|
||||
int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(EntryTest, entryUpdate) {
|
||||
// Create embedded entry
|
||||
sds value1 = sdsnew(SHORT_VALUE);
|
||||
sds field = sdsnew(SHORT_FIELD);
|
||||
@@ -125,7 +127,7 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
|
||||
// Update only expiry (keeping embedded)
|
||||
long long expiry3 = 200;
|
||||
entry *e3 = entryUpdate(e2, NULL, expiry3);
|
||||
entry *e3 = entryUpdate(e2, nullptr, expiry3);
|
||||
verify_entry_properties(e3, field, value_copy2, expiry3, true, false);
|
||||
|
||||
// Update both value and expiry (keeping embedded)
|
||||
@@ -136,9 +138,9 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
verify_entry_properties(e4, field, value_copy4, expiry4, true, false);
|
||||
|
||||
// Update with no changes (should return same entry)
|
||||
entry *e5 = entryUpdate(e4, NULL, expiry4);
|
||||
entry *e5 = entryUpdate(e4, nullptr, expiry4);
|
||||
verify_entry_properties(e5, field, value_copy4, expiry4, true, false);
|
||||
TEST_ASSERT(e5 == e4);
|
||||
ASSERT_EQ(e5, e4);
|
||||
|
||||
// Update to a value that's too large to be embedded
|
||||
sds value6 = sdsnew(LONG_VALUE);
|
||||
@@ -149,7 +151,7 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
|
||||
// Update expiry of a non-embedded entry
|
||||
long long expiry7 = 400;
|
||||
entry *e7 = entryUpdate(e6, NULL, expiry7);
|
||||
entry *e7 = entryUpdate(e6, nullptr, expiry7);
|
||||
verify_entry_properties(e7, field, value_copy6, expiry7, true, true);
|
||||
|
||||
// Update from non-embedded back to embedded value
|
||||
@@ -174,8 +176,8 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
long long expiry10 = expiry9;
|
||||
entry *e10 = entryUpdate(e9, value10, expiry10);
|
||||
verify_entry_properties(e10, field, value_copy10, expiry10, true, false);
|
||||
TEST_ASSERT(entryMemUsage(e10) < current_embedded_allocation_size * 3 / 4);
|
||||
TEST_ASSERT(e10 != e9);
|
||||
ASSERT_LT(entryMemUsage(e10), current_embedded_allocation_size * 3 / 4);
|
||||
ASSERT_NE(e10, e9);
|
||||
|
||||
// Update the value so that memory usage is at least 3/4 of the current memory usage
|
||||
// Ensuring required_embedded_size > current_embedded_allocation_size * 3 / 4 without creating a new entry
|
||||
@@ -185,11 +187,10 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
long long expiry11 = expiry10;
|
||||
entry *e11 = entryUpdate(e10, value11, expiry11);
|
||||
verify_entry_properties(e11, field, value_copy11, expiry11, true, false);
|
||||
TEST_ASSERT(entryMemUsage(e11) >= current_embedded_allocation_size * 3 / 4);
|
||||
TEST_ASSERT(entryMemUsage(e11) <= current_embedded_allocation_size);
|
||||
TEST_ASSERT(entryMemUsage(e11) <=
|
||||
EMBED_VALUE_MAX_ALLOC_SIZE);
|
||||
TEST_ASSERT(e10 == e11);
|
||||
ASSERT_GE(entryMemUsage(e11), current_embedded_allocation_size * 3 / 4);
|
||||
ASSERT_LE(entryMemUsage(e11), current_embedded_allocation_size);
|
||||
ASSERT_LE(entryMemUsage(e11), (size_t)EMBED_VALUE_MAX_ALLOC_SIZE);
|
||||
ASSERT_EQ(e10, e11);
|
||||
|
||||
// Update the value so that memory usage is exactly equal to the current allocation size
|
||||
// Ensuring required_embedded_size == current_embedded_allocation_size without creating a new entry
|
||||
@@ -199,9 +200,9 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
long long expiry12 = expiry11;
|
||||
entry *e12 = entryUpdate(e11, value12, expiry12);
|
||||
verify_entry_properties(e11, field, value_copy12, expiry12, true, false);
|
||||
TEST_ASSERT(entryMemUsage(e12) == current_embedded_allocation_size);
|
||||
TEST_ASSERT(entryMemUsage(e12) <= EMBED_VALUE_MAX_ALLOC_SIZE);
|
||||
TEST_ASSERT(e12 == e11);
|
||||
ASSERT_EQ(entryMemUsage(e12), current_embedded_allocation_size);
|
||||
ASSERT_LE(entryMemUsage(e12), (size_t)EMBED_VALUE_MAX_ALLOC_SIZE);
|
||||
ASSERT_EQ(e12, e11);
|
||||
|
||||
entryFree(e12);
|
||||
sdsfree(field);
|
||||
@@ -214,8 +215,6 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
sdsfree(value_copy10);
|
||||
sdsfree(value_copy11);
|
||||
sdsfree(value_copy12);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,57 +225,51 @@ int test_entryUpdate(int argc, char **argv, int flags) {
|
||||
* 4. Test with non-embedded entry
|
||||
* 5. Set expiry on non-embedded entry
|
||||
*/
|
||||
int test_entryHasexpiry_entrySetExpiry(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(EntryTest, entryHasexpiry_entrySetExpiry) {
|
||||
// No expiry
|
||||
sds field1 = sdsnew(SHORT_FIELD);
|
||||
sds value1 = sdsnew(SHORT_VALUE);
|
||||
entry *e1 = entryCreate(field1, value1, EXPIRY_NONE);
|
||||
TEST_ASSERT(entryHasExpiry(e1) == false);
|
||||
TEST_ASSERT(entryGetExpiry(e1) == EXPIRY_NONE);
|
||||
ASSERT_FALSE(entryHasExpiry(e1));
|
||||
ASSERT_EQ(entryGetExpiry(e1), EXPIRY_NONE);
|
||||
|
||||
// Set expiry on entry without expiry
|
||||
long long expiry2 = 100;
|
||||
entry *e2 = entrySetExpiry(e1, expiry2);
|
||||
TEST_ASSERT(entryHasExpiry(e2) == true);
|
||||
TEST_ASSERT(entryGetExpiry(e2) == expiry2);
|
||||
ASSERT_TRUE(entryHasExpiry(e2));
|
||||
ASSERT_EQ(entryGetExpiry(e2), expiry2);
|
||||
|
||||
// Update expiry on entry with expiry
|
||||
long long expiry3 = 200;
|
||||
entry *e3 = entrySetExpiry(e2, expiry3);
|
||||
TEST_ASSERT(entryHasExpiry(e3) == true);
|
||||
TEST_ASSERT(entryGetExpiry(e3) == expiry3);
|
||||
TEST_ASSERT(e2 == e3); // Should be the same pointer when just updating expiry
|
||||
ASSERT_TRUE(entryHasExpiry(e3));
|
||||
ASSERT_EQ(entryGetExpiry(e3), expiry3);
|
||||
ASSERT_EQ(e2, e3); // Should be the same pointer when just updating expiry
|
||||
|
||||
// Test with non-embedded entry
|
||||
sds field4 = sdsnew(LONG_FIELD);
|
||||
sds value4 = sdsnew(LONG_VALUE);
|
||||
entry *e4 = entryCreate(field4, value4, EXPIRY_NONE);
|
||||
TEST_ASSERT(entryHasExpiry(e4) == false);
|
||||
TEST_ASSERT(entryHasEmbeddedValue(e4) == false);
|
||||
ASSERT_FALSE(entryHasExpiry(e4));
|
||||
ASSERT_FALSE(entryHasEmbeddedValue(e4));
|
||||
|
||||
// Set expiry on entry without expiry
|
||||
long long expiry5 = 100;
|
||||
entry *e5 = entrySetExpiry(e4, expiry5);
|
||||
TEST_ASSERT(entryHasExpiry(e5) == true);
|
||||
TEST_ASSERT(entryGetExpiry(e5) == expiry5);
|
||||
ASSERT_TRUE(entryHasExpiry(e5));
|
||||
ASSERT_EQ(entryGetExpiry(e5), expiry5);
|
||||
|
||||
// Update expiry on entry with expiry
|
||||
long long expiry6 = 200;
|
||||
entry *e6 = entrySetExpiry(e5, expiry6);
|
||||
TEST_ASSERT(entryHasExpiry(e6) == true);
|
||||
TEST_ASSERT(entryGetExpiry(e6) == expiry6);
|
||||
TEST_ASSERT(e5 == e6); // Should be the same pointer when just updating expiry
|
||||
ASSERT_TRUE(entryHasExpiry(e6));
|
||||
ASSERT_EQ(entryGetExpiry(e6), expiry6);
|
||||
ASSERT_EQ(e5, e6); // Should be the same pointer when just updating expiry
|
||||
|
||||
entryFree(e3);
|
||||
entryFree(e6);
|
||||
sdsfree(field1);
|
||||
sdsfree(field4);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,11 +283,7 @@ int test_entryHasexpiry_entrySetExpiry(int argc, char **argv, int flags) {
|
||||
* 7. Test with import mode and import source client and import expiry
|
||||
* 8. Test with import mode and import source client and import expiry and import expiry is in the past
|
||||
*/
|
||||
int test_entryIsExpired(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(EntryTest, entryIsExpired) {
|
||||
// Setup server state
|
||||
enterExecutionUnit(1, ustime());
|
||||
long long current_time = commandTimeSnapshot();
|
||||
@@ -303,31 +292,31 @@ int test_entryIsExpired(int argc, char **argv, int flags) {
|
||||
sds field1 = sdsnew(SHORT_FIELD);
|
||||
sds value1 = sdsnew(SHORT_VALUE);
|
||||
entry *e1 = entryCreate(field1, value1, EXPIRY_NONE);
|
||||
TEST_ASSERT(entryGetExpiry(e1) == EXPIRY_NONE);
|
||||
TEST_ASSERT(entryIsExpired(e1) == false);
|
||||
ASSERT_EQ(entryGetExpiry(e1), EXPIRY_NONE);
|
||||
ASSERT_FALSE(entryIsExpired(e1));
|
||||
|
||||
// Future expiry
|
||||
sds field2 = sdsnew(SHORT_FIELD);
|
||||
sds value2 = sdsnew(SHORT_VALUE);
|
||||
long long future_time = current_time + 10000; // 10 seconds in future
|
||||
entry *e2 = entryCreate(field2, value2, future_time);
|
||||
TEST_ASSERT(entryGetExpiry(e2) == future_time);
|
||||
TEST_ASSERT(entryIsExpired(e2) == false);
|
||||
ASSERT_EQ(entryGetExpiry(e2), future_time);
|
||||
ASSERT_FALSE(entryIsExpired(e2));
|
||||
|
||||
// Current time expiry
|
||||
sds field3 = sdsnew(SHORT_FIELD);
|
||||
sds value3 = sdsnew(SHORT_VALUE);
|
||||
entry *e3 = entryCreate(field3, value3, current_time);
|
||||
TEST_ASSERT(entryGetExpiry(e3) == current_time);
|
||||
TEST_ASSERT(entryIsExpired(e3) == false);
|
||||
ASSERT_EQ(entryGetExpiry(e3), current_time);
|
||||
ASSERT_FALSE(entryIsExpired(e3));
|
||||
|
||||
// Test with past expiry
|
||||
sds field4 = sdsnew(SHORT_FIELD);
|
||||
sds value4 = sdsnew(SHORT_VALUE);
|
||||
long long past_time = current_time - 10000; // 10 seconds ago
|
||||
entry *e4 = entryCreate(field4, value4, past_time);
|
||||
TEST_ASSERT(entryGetExpiry(e4) == past_time);
|
||||
TEST_ASSERT(entryIsExpired(e4) == true);
|
||||
ASSERT_EQ(entryGetExpiry(e4), past_time);
|
||||
ASSERT_TRUE(entryIsExpired(e4));
|
||||
|
||||
entryFree(e1);
|
||||
entryFree(e2);
|
||||
@@ -338,7 +327,6 @@ int test_entryIsExpired(int argc, char **argv, int flags) {
|
||||
sdsfree(field3);
|
||||
sdsfree(field4);
|
||||
exitExecutionUnit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,11 +347,7 @@ int test_entryIsExpired(int argc, char **argv, int flags) {
|
||||
* * To smaller value (should decrease memory usage)
|
||||
* * To bigger value (should increase memory usage)
|
||||
*/
|
||||
int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(EntryTest, entryMemUsage_entrySetExpiry_entryUpdate) {
|
||||
// Tests with embedded entry
|
||||
// Embedded entry without expiry
|
||||
sds field1 = sdsnew(SHORT_FIELD);
|
||||
@@ -373,7 +357,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e1 = entryCreate(field1, value1, expiry1);
|
||||
size_t e1_entryMemUsage = entryMemUsage(e1);
|
||||
verify_entry_properties(e1, field1, value_copy1, expiry1, false, false);
|
||||
TEST_ASSERT(e1_entryMemUsage > 0);
|
||||
ASSERT_GT(e1_entryMemUsage, 0u);
|
||||
|
||||
// Add expiry to embedded entry without expiry
|
||||
// This should increase memory usage by sizeof(long long) + 2 bytes
|
||||
@@ -382,7 +366,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e2 = entrySetExpiry(e1, expiry2);
|
||||
size_t e2_entryMemUsage = entryMemUsage(e2);
|
||||
verify_entry_properties(e2, field1, value_copy1, expiry2, true, false);
|
||||
TEST_ASSERT(zmalloc_usable_size((char *)e2 - sizeof(long long) - 3) == e2_entryMemUsage);
|
||||
ASSERT_EQ(zmalloc_usable_size((char *)e2 - sizeof(long long) - 3), e2_entryMemUsage);
|
||||
|
||||
// Update expiry on an entry that already has one
|
||||
// This should NOT change memory usage as we're just updating the expiry value (long long)
|
||||
@@ -390,7 +374,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e3 = entrySetExpiry(e2, expiry3);
|
||||
size_t e3_entryMemUsage = entryMemUsage(e3);
|
||||
verify_entry_properties(e3, field1, value_copy1, expiry3, true, false);
|
||||
TEST_ASSERT(e3_entryMemUsage == e2_entryMemUsage);
|
||||
ASSERT_EQ(e3_entryMemUsage, e2_entryMemUsage);
|
||||
|
||||
// Update to smaller value (keeping embedded)
|
||||
// Memory usage should decrease by the difference in value size (2 bytes)
|
||||
@@ -399,7 +383,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e4 = entryUpdate(e3, value4, entryGetExpiry(e3));
|
||||
size_t e4_entryMemUsage = entryMemUsage(e4);
|
||||
verify_entry_properties(e4, field1, value_copy4, expiry3, true, false);
|
||||
TEST_ASSERT(zmalloc_usable_size((char *)e4 - sizeof(long long) - 3) == e4_entryMemUsage);
|
||||
ASSERT_EQ(zmalloc_usable_size((char *)e4 - sizeof(long long) - 3), e4_entryMemUsage);
|
||||
|
||||
// Update to bigger value (keeping embedded)
|
||||
// Memory usage should increase by the difference in value size (1 byte)
|
||||
@@ -408,7 +392,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e5 = entryUpdate(e4, value5, entryGetExpiry(e4));
|
||||
size_t e5_entryMemUsage = entryMemUsage(e5);
|
||||
verify_entry_properties(e5, field1, value_copy5, expiry3, true, false);
|
||||
TEST_ASSERT(zmalloc_usable_size((char *)e5 - sizeof(long long) - 3) == e5_entryMemUsage);
|
||||
ASSERT_EQ(zmalloc_usable_size((char *)e5 - sizeof(long long) - 3), e5_entryMemUsage);
|
||||
|
||||
// Tests with non-embedded entry
|
||||
// Non-embedded entry without expiry
|
||||
@@ -420,7 +404,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e6 = entryCreate(field6, value6, EXPIRY_NONE);
|
||||
size_t e6_entryMemUsage = entryMemUsage(e6);
|
||||
verify_entry_properties(e6, field6, value_copy6, expiry6, false, true);
|
||||
TEST_ASSERT(e6_entryMemUsage > 0);
|
||||
ASSERT_GT(e6_entryMemUsage, 0u);
|
||||
|
||||
// Add expiry to non-embedded entry without expiry
|
||||
// For non-embedded entries this increases memory by exactly sizeof(long long)
|
||||
@@ -429,7 +413,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
size_t e7_entryMemUsage = entryMemUsage(e7);
|
||||
verify_entry_properties(e7, field6, value_copy6, expiry7, true, true);
|
||||
size_t expected_e7_entry_mem = zmalloc_usable_size((char *)e7 - sizeof(long long) - sizeof(sds) - 3) + sdsAllocSize(value6);
|
||||
TEST_ASSERT(expected_e7_entry_mem == e7_entryMemUsage);
|
||||
ASSERT_EQ(expected_e7_entry_mem, e7_entryMemUsage);
|
||||
|
||||
// Update expiry on a non-embedded entry that already has one
|
||||
// This should not change memory usage as we're just updating the expiry value
|
||||
@@ -437,7 +421,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e8 = entrySetExpiry(e7, expiry8);
|
||||
size_t e8_entryMemUsage = entryMemUsage(e8);
|
||||
verify_entry_properties(e8, field6, value_copy6, expiry8, true, true);
|
||||
TEST_ASSERT(e8_entryMemUsage == e7_entryMemUsage);
|
||||
ASSERT_EQ(e8_entryMemUsage, e7_entryMemUsage);
|
||||
|
||||
// Update to smaller value (keeping non-embedded)
|
||||
// Memory usage should increase by at least the difference between LONG_VALUE and "x" (143)
|
||||
@@ -447,7 +431,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
size_t e9_entryMemUsage = entryMemUsage(e9);
|
||||
verify_entry_properties(e9, field6, value_copy9, expiry8, true, true);
|
||||
size_t expected_e9_entry_mem = zmalloc_usable_size((char *)e9 - sizeof(long long) - sizeof(sds) - 3) + sdsAllocSize(value9);
|
||||
TEST_ASSERT(expected_e9_entry_mem == e9_entryMemUsage);
|
||||
ASSERT_EQ(expected_e9_entry_mem, e9_entryMemUsage);
|
||||
|
||||
// Update to bigger value (keeping non-embedded)
|
||||
// Memory usage increases by the difference in value size (1 byte)
|
||||
@@ -456,7 +440,7 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
entry *e10 = entryUpdate(e9, value10, entryGetExpiry(e9));
|
||||
size_t e10_entryMemUsage = entryMemUsage(e10);
|
||||
size_t expected_10_entry_mem = zmalloc_usable_size((char *)e10 - sizeof(long long) - sizeof(sds) - 3) + sdsAllocSize(value10);
|
||||
TEST_ASSERT(expected_10_entry_mem == e10_entryMemUsage);
|
||||
ASSERT_EQ(expected_10_entry_mem, e10_entryMemUsage);
|
||||
|
||||
entryFree(e5);
|
||||
entryFree(e10);
|
||||
@@ -468,15 +452,9 @@ int test_entryMemUsage_entrySetExpiry_entryUpdate(int argc, char **argv, int fla
|
||||
sdsfree(value_copy6);
|
||||
sdsfree(value_copy9);
|
||||
sdsfree(value_copy10);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_entryStringRef(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(EntryTest, entryStringRef) {
|
||||
sds field1 = sdsnew(SHORT_FIELD);
|
||||
sds value1 = sdsnew(SHORT_VALUE);
|
||||
sds value_copy1 = sdsdup(value1);
|
||||
@@ -484,41 +462,40 @@ int test_entryStringRef(int argc, char **argv, int flags) {
|
||||
entry *e1 = entryCreate(field1, value1, expiry1);
|
||||
entry *e2 = entryUpdateAsStringRef(e1, value_copy1, sdslen(value_copy1), entryGetExpiry(e1));
|
||||
verify_entry_properties(e2, field1, value_copy1, expiry1, false, true);
|
||||
TEST_ASSERT(entryHasStringRef(e2) == true);
|
||||
ASSERT_TRUE(entryHasStringRef(e2));
|
||||
|
||||
long long expiry2 = 100;
|
||||
entry *e3 = entryUpdateAsStringRef(e2, value_copy1, sdslen(value_copy1), expiry2);
|
||||
TEST_ASSERT(e2 != e3);
|
||||
ASSERT_NE(e2, e3);
|
||||
verify_entry_properties(e3, field1, value_copy1, expiry2, true, true);
|
||||
TEST_ASSERT(entryHasStringRef(e3) == true);
|
||||
ASSERT_TRUE(entryHasStringRef(e3));
|
||||
|
||||
long long expiry3 = 200;
|
||||
entry *e4 = entryUpdateAsStringRef(e3, value_copy1, sdslen(value_copy1), expiry3);
|
||||
TEST_ASSERT(e3 == e4);
|
||||
ASSERT_EQ(e3, e4);
|
||||
verify_entry_properties(e4, field1, value_copy1, expiry3, true, true);
|
||||
TEST_ASSERT(entryHasStringRef(e4) == true);
|
||||
ASSERT_TRUE(entryHasStringRef(e4));
|
||||
|
||||
sds value2 = sdsnew(SHORT_VALUE);
|
||||
sds value_copy2 = sdsdup(value2);
|
||||
entry *e5 = entryUpdate(e4, value2, expiry3);
|
||||
verify_entry_properties(e5, field1, value_copy2, expiry3, true, false);
|
||||
TEST_ASSERT(entryHasStringRef(e5) == false);
|
||||
ASSERT_FALSE(entryHasStringRef(e5));
|
||||
|
||||
entry *e6 = entryUpdateAsStringRef(e5, value_copy1, sdslen(value_copy1), expiry2);
|
||||
TEST_ASSERT(e5 != e6);
|
||||
ASSERT_NE(e5, e6);
|
||||
verify_entry_properties(e6, field1, value_copy1, expiry2, true, true);
|
||||
TEST_ASSERT(entryHasStringRef(e6) == true);
|
||||
ASSERT_TRUE(entryHasStringRef(e6));
|
||||
|
||||
sds value3 = sdsnew(LONG_VALUE);
|
||||
sds value_copy3 = sdsdup(value3);
|
||||
entry *e7 = entryUpdate(e6, value3, expiry1);
|
||||
verify_entry_properties(e7, field1, value_copy3, expiry1, false, true);
|
||||
TEST_ASSERT(entryHasStringRef(e7) == false);
|
||||
ASSERT_FALSE(entryHasStringRef(e7));
|
||||
|
||||
entryFree(e7);
|
||||
sdsfree(value_copy1);
|
||||
sdsfree(value_copy2);
|
||||
sdsfree(value_copy3);
|
||||
sdsfree(field1);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "../fifo.h"
|
||||
#include "test_help.h"
|
||||
#include <strings.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static inline void *intToPointer(intptr_t i) {
|
||||
return (void *)i;
|
||||
}
|
||||
|
||||
static inline intptr_t pointerToInt(void *p) {
|
||||
return (intptr_t)p;
|
||||
}
|
||||
|
||||
/* Helper functions */
|
||||
static void push(fifo *q, intptr_t value) {
|
||||
int len = fifoLength(q);
|
||||
fifoPush(q, intToPointer(value));
|
||||
TEST_EXPECT(fifoLength(q) == len + 1);
|
||||
}
|
||||
|
||||
static intptr_t popTest(fifo *q, intptr_t expected) {
|
||||
void *peekPtr;
|
||||
TEST_EXPECT(fifoPeek(q, &peekPtr));
|
||||
intptr_t peekValue = pointerToInt(peekPtr);
|
||||
TEST_EXPECT(peekValue == expected);
|
||||
int len = fifoLength(q);
|
||||
void *popPtr;
|
||||
TEST_EXPECT(fifoPop(q, &popPtr));
|
||||
intptr_t value = pointerToInt(popPtr);
|
||||
TEST_EXPECT(fifoLength(q) == len - 1);
|
||||
TEST_EXPECT(value == expected);
|
||||
return value;
|
||||
}
|
||||
|
||||
/* Test: emptyPop - verify that popping from empty fifo returns false */
|
||||
int test_fifoEmptyPop(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
fifo *q = fifoCreate();
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
void *result;
|
||||
TEST_EXPECT(fifoPop(q, &result) == false);
|
||||
fifoRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: emptyPeek - verify that peeking at empty fifo returns false */
|
||||
int test_fifoEmptyPeek(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
fifo *q = fifoCreate();
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
void *result;
|
||||
TEST_EXPECT(fifoPeek(q, &result) == false);
|
||||
fifoRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: simplePushPop */
|
||||
int test_fifoSimplePushPop(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
fifo *q = fifoCreate();
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
push(q, 1);
|
||||
popTest(q, 1);
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
fifoRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: tryVariousSizes */
|
||||
int test_fifoTryVariousSizes(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
fifo *q = fifoCreate();
|
||||
for (int items = 1; items < 50; items++) {
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
for (int value = 1; value <= items; value++) push(q, value);
|
||||
for (int value = 1; value <= items; value++) popTest(q, value);
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
}
|
||||
fifoRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: pushPopTest */
|
||||
int test_fifoPushPopTest(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
fifo *q = fifoCreate();
|
||||
/* In this test, we repeatedly push 2 and pop 1. This hits the list differently than
|
||||
* other tests which push a bunch and then pop them all off. */
|
||||
int pushVal = 1;
|
||||
int popVal = 1;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (i % 3 == 0 || i % 3 == 1) {
|
||||
push(q, pushVal++);
|
||||
} else {
|
||||
popTest(q, popVal++);
|
||||
}
|
||||
}
|
||||
fifoRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: joinTest */
|
||||
int test_fifoJoinTest(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
fifo *q = fifoCreate();
|
||||
fifo *q2 = fifoCreate();
|
||||
|
||||
/* In this test, there are 2 fifos Q and Q2.
|
||||
* Various sizes are tested. For each size, various amounts are popped off the front first.
|
||||
* Q2 is appended to Q. */
|
||||
for (int qLen = 0; qLen <= 21; qLen++) {
|
||||
for (int qPop = 0; qPop < 6 && qPop <= qLen; qPop++) {
|
||||
for (int q2Len = 0; q2Len <= 21; q2Len++) {
|
||||
for (int q2Pop = 0; q2Pop < 6 && q2Pop <= q2Len; q2Pop++) {
|
||||
intptr_t pushValue = 1;
|
||||
intptr_t popQValue = 1;
|
||||
|
||||
for (int i = 0; i < qLen; i++) fifoPush(q, intToPointer(pushValue++));
|
||||
for (int i = 0; i < qPop; i++) {
|
||||
void *ptr;
|
||||
TEST_EXPECT(fifoPop(q, &ptr) && pointerToInt(ptr) == popQValue++);
|
||||
}
|
||||
|
||||
intptr_t popQ2Value = pushValue;
|
||||
for (int i = 0; i < q2Len; i++) fifoPush(q2, intToPointer(pushValue++));
|
||||
for (int i = 0; i < q2Pop; i++) {
|
||||
void *ptr;
|
||||
TEST_EXPECT(fifoPop(q2, &ptr) && pointerToInt(ptr) == popQ2Value++);
|
||||
}
|
||||
|
||||
fifoJoin(q, q2);
|
||||
TEST_EXPECT(fifoLength(q) == (qLen - qPop) + (q2Len - q2Pop));
|
||||
TEST_EXPECT(fifoLength(q2) == 0);
|
||||
|
||||
fifo *temp = fifoPopAll(q); /* Exercise fifoPopAll also */
|
||||
TEST_EXPECT(fifoLength(temp) == (qLen - qPop) + (q2Len - q2Pop));
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
|
||||
for (int i = 0; i < (qLen - qPop); i++) {
|
||||
void *ptr;
|
||||
TEST_EXPECT(fifoPop(temp, &ptr) && pointerToInt(ptr) == popQValue++);
|
||||
}
|
||||
for (int i = 0; i < (q2Len - q2Pop); i++) {
|
||||
void *ptr;
|
||||
TEST_EXPECT(fifoPop(temp, &ptr) && pointerToInt(ptr) == popQ2Value++);
|
||||
}
|
||||
TEST_EXPECT(fifoLength(temp) == 0);
|
||||
|
||||
fifoRelease(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fifoRelease(q);
|
||||
fifoRelease(q2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include "../adlist.h"
|
||||
#include "../monotonic.h"
|
||||
|
||||
const int LIST_ITEMS = 10000;
|
||||
|
||||
static void exerciseList(void) {
|
||||
list *q = listCreate();
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
listAddNodeTail(q, intToPointer(i));
|
||||
}
|
||||
TEST_EXPECT(listLength(q) == (unsigned)LIST_ITEMS);
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
listNode *node = listFirst(q);
|
||||
listDelNode(q, node);
|
||||
TEST_EXPECT(listNodeValue(node) == intToPointer(i));
|
||||
}
|
||||
TEST_EXPECT(listLength(q) == 0u);
|
||||
listRelease(q);
|
||||
}
|
||||
|
||||
static void exerciseFifo(void) {
|
||||
fifo *q = fifoCreate();
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
fifoPush(q, intToPointer(i));
|
||||
}
|
||||
TEST_EXPECT(fifoLength(q) == LIST_ITEMS);
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
void *ptr;
|
||||
TEST_EXPECT(fifoPop(q, &ptr) && ptr == intToPointer(i));
|
||||
}
|
||||
TEST_EXPECT(fifoLength(q) == 0);
|
||||
fifoRelease(q);
|
||||
}
|
||||
|
||||
int test_fifoComparePerformance(int argc, char *argv[], int flags) {
|
||||
UNUSED(flags);
|
||||
|
||||
/* To run the performance comparison test, use:
|
||||
* ./valkey-unit-tests --single test_fifo.c --compare-performance-to-adlist
|
||||
* This test will exercise both FIFO and ADLIST to compare performance.
|
||||
* The test will (intentionally) fail, printing the results as failed assertions. */
|
||||
if (argc > 3 && !strcasecmp(argv[3], "--compare-performance-to-adlist")) {
|
||||
monotonicInit();
|
||||
monotime timer;
|
||||
const int iterations = 500;
|
||||
|
||||
exerciseList(); /* Warm up the list before timing */
|
||||
elapsedStart(&timer);
|
||||
for (int i = 0; i < iterations; i++) exerciseList();
|
||||
long listMs = elapsedMs(timer);
|
||||
|
||||
exerciseFifo(); /* Warm up the fifo before timing */
|
||||
elapsedStart(&timer);
|
||||
for (int i = 0; i < iterations; i++) exerciseFifo();
|
||||
long fifoMs = elapsedMs(timer);
|
||||
|
||||
double percentImprovement = (double)(listMs - fifoMs) * 100.0 / listMs;
|
||||
TEST_PRINT_INFO("List: %ld ms, FIFO: %ld ms, Improvement: %.2f%%", listMs, fifoMs, percentImprovement);
|
||||
TEST_EXPECT(percentImprovement == 0.0); /* This will fail, printing result */
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <strings.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" {
|
||||
#include "adlist.h"
|
||||
#include "fifo.h"
|
||||
#include "monotonic.h"
|
||||
}
|
||||
|
||||
static inline void *intToPointer(intptr_t i) {
|
||||
return (void *)i;
|
||||
}
|
||||
|
||||
static inline intptr_t pointerToInt(void *p) {
|
||||
return (intptr_t)p;
|
||||
}
|
||||
|
||||
class FifoTest : public ::testing::Test {
|
||||
protected:
|
||||
fifo *q;
|
||||
|
||||
void SetUp() override {
|
||||
q = fifoCreate();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
fifoRelease(q);
|
||||
}
|
||||
|
||||
/* Helper function to push a value and verify the length increases */
|
||||
void push(intptr_t value) {
|
||||
int len = fifoLength(q);
|
||||
fifoPush(q, intToPointer(value));
|
||||
EXPECT_EQ(fifoLength(q), len + 1);
|
||||
}
|
||||
|
||||
/* Helper function to pop and verify the expected value */
|
||||
intptr_t popTest(intptr_t expected) {
|
||||
void *peekPtr;
|
||||
EXPECT_TRUE(fifoPeek(q, &peekPtr));
|
||||
intptr_t peekValue = pointerToInt(peekPtr);
|
||||
EXPECT_EQ(peekValue, expected);
|
||||
int len = fifoLength(q);
|
||||
void *popPtr;
|
||||
EXPECT_TRUE(fifoPop(q, &popPtr));
|
||||
intptr_t value = pointerToInt(popPtr);
|
||||
EXPECT_EQ(fifoLength(q), len - 1);
|
||||
EXPECT_EQ(value, expected);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
/* Test: emptyPop - verify that popping from empty fifo returns false */
|
||||
TEST_F(FifoTest, TestFifoEmptyPop) {
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
void *result;
|
||||
EXPECT_FALSE(fifoPop(q, &result));
|
||||
}
|
||||
|
||||
/* Test: emptyPeek - verify that peeking at empty fifo returns false */
|
||||
TEST_F(FifoTest, TestFifoEmptyPeek) {
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
void *result;
|
||||
EXPECT_FALSE(fifoPeek(q, &result));
|
||||
}
|
||||
|
||||
/* Test: simplePushPop */
|
||||
TEST_F(FifoTest, TestFifoSimplePushPop) {
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
push(1);
|
||||
popTest(1);
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
}
|
||||
|
||||
/* Test: tryVariousSizes */
|
||||
TEST_F(FifoTest, TestFifoTryVariousSizes) {
|
||||
for (int items = 1; items < 50; items++) {
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
for (int value = 1; value <= items; value++) push(value);
|
||||
for (int value = 1; value <= items; value++) popTest(value);
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Test: pushPopTest */
|
||||
TEST_F(FifoTest, TestFifoPushPopTest) {
|
||||
/* In this test, we repeatedly push 2 and pop 1. This hits the list differently than
|
||||
* other tests which push a bunch and then pop them all off. */
|
||||
int pushVal = 1;
|
||||
int popVal = 1;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (i % 3 == 0 || i % 3 == 1) {
|
||||
push(pushVal++);
|
||||
} else {
|
||||
popTest(popVal++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Test: joinTest */
|
||||
TEST_F(FifoTest, TestFifoJoinTest) {
|
||||
fifo *q2 = fifoCreate();
|
||||
|
||||
/* In this test, there are 2 fifos Q and Q2.
|
||||
* Various sizes are tested. For each size, various amounts are popped off the front first.
|
||||
* Q2 is appended to Q. */
|
||||
for (int qLen = 0; qLen <= 21; qLen++) {
|
||||
for (int qPop = 0; qPop < 6 && qPop <= qLen; qPop++) {
|
||||
for (int q2Len = 0; q2Len <= 21; q2Len++) {
|
||||
for (int q2Pop = 0; q2Pop < 6 && q2Pop <= q2Len; q2Pop++) {
|
||||
intptr_t pushValue = 1;
|
||||
intptr_t popQValue = 1;
|
||||
|
||||
for (int i = 0; i < qLen; i++) fifoPush(q, intToPointer(pushValue++));
|
||||
for (int i = 0; i < qPop; i++) {
|
||||
void *ptr;
|
||||
EXPECT_TRUE(fifoPop(q, &ptr) && pointerToInt(ptr) == popQValue++);
|
||||
}
|
||||
|
||||
intptr_t popQ2Value = pushValue;
|
||||
for (int i = 0; i < q2Len; i++) fifoPush(q2, intToPointer(pushValue++));
|
||||
for (int i = 0; i < q2Pop; i++) {
|
||||
void *ptr;
|
||||
EXPECT_TRUE(fifoPop(q2, &ptr) && pointerToInt(ptr) == popQ2Value++);
|
||||
}
|
||||
|
||||
fifoJoin(q, q2);
|
||||
EXPECT_EQ(fifoLength(q), (qLen - qPop) + (q2Len - q2Pop));
|
||||
EXPECT_EQ(fifoLength(q2), 0);
|
||||
|
||||
fifo *temp = fifoPopAll(q); /* Exercise fifoPopAll also */
|
||||
EXPECT_EQ(fifoLength(temp), (qLen - qPop) + (q2Len - q2Pop));
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
|
||||
for (int i = 0; i < (qLen - qPop); i++) {
|
||||
void *ptr;
|
||||
EXPECT_TRUE(fifoPop(temp, &ptr) && pointerToInt(ptr) == popQValue++);
|
||||
}
|
||||
for (int i = 0; i < (q2Len - q2Pop); i++) {
|
||||
void *ptr;
|
||||
EXPECT_TRUE(fifoPop(temp, &ptr) && pointerToInt(ptr) == popQ2Value++);
|
||||
}
|
||||
EXPECT_EQ(fifoLength(temp), 0);
|
||||
|
||||
fifoRelease(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fifoRelease(q2);
|
||||
}
|
||||
|
||||
const int LIST_ITEMS = 10000;
|
||||
|
||||
static void exerciseList(void) {
|
||||
list *q = listCreate();
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
listAddNodeTail(q, intToPointer(i));
|
||||
}
|
||||
EXPECT_EQ(listLength(q), (unsigned)LIST_ITEMS);
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
listNode *node = listFirst(q);
|
||||
listDelNode(q, node);
|
||||
EXPECT_EQ(listNodeValue(node), intToPointer(i));
|
||||
}
|
||||
EXPECT_EQ(listLength(q), 0u);
|
||||
listRelease(q);
|
||||
}
|
||||
|
||||
static void exerciseFifo(void) {
|
||||
fifo *q = fifoCreate();
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
fifoPush(q, intToPointer(i));
|
||||
}
|
||||
EXPECT_EQ(fifoLength(q), LIST_ITEMS);
|
||||
for (intptr_t i = 0; i < LIST_ITEMS; i++) {
|
||||
void *ptr;
|
||||
EXPECT_TRUE(fifoPop(q, &ptr) && ptr == intToPointer(i));
|
||||
}
|
||||
EXPECT_EQ(fifoLength(q), 0);
|
||||
fifoRelease(q);
|
||||
}
|
||||
|
||||
/* This test is disabled by default because it's a performance comparison test.
|
||||
* To run this test explicitly, use:
|
||||
* ./src/unit/valkey-unit-gtests --gtest_filter=FifoTest.DISABLED_TestFifoComparePerformance --gtest_also_run_disabled_tests
|
||||
* This test will exercise both FIFO and ADLIST to compare performance.
|
||||
* The test will (intentionally) fail, printing the results as failed assertions. */
|
||||
TEST_F(FifoTest, DISABLED_TestFifoComparePerformance) {
|
||||
monotonicInit();
|
||||
monotime timer;
|
||||
const int iterations = 500;
|
||||
|
||||
exerciseList(); /* Warm up the list before timing */
|
||||
elapsedStart(&timer);
|
||||
for (int i = 0; i < iterations; i++) exerciseList();
|
||||
long listMs = elapsedMs(timer);
|
||||
|
||||
exerciseFifo(); /* Warm up the fifo before timing */
|
||||
elapsedStart(&timer);
|
||||
for (int i = 0; i < iterations; i++) exerciseFifo();
|
||||
long fifoMs = elapsedMs(timer);
|
||||
|
||||
double percentImprovement = (double)(listMs - fifoMs) * 100.0 / listMs;
|
||||
printf("List: %ld ms, FIFO: %ld ms, Improvement: %.2f%%\n", listMs, fifoMs, percentImprovement);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
/* A very simple test framework for valkey. See unit/README.md for more information on usage.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* int test_example(int argc, char *argv[], int flags) {
|
||||
* TEST_ASSERT_MESSAGE("Check if 1 == 1", 1==1);
|
||||
* TEST_ASSERT(5 == 5);
|
||||
* return 0;
|
||||
* }
|
||||
*/
|
||||
|
||||
#ifndef __TESTHELP_H
|
||||
#define __TESTHELP_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* The flags are the following:
|
||||
* --accurate: Runs tests with more iterations.
|
||||
* --large-memory: Enables tests that consume more than 100mb.
|
||||
* --single: A flag to indicate a specific test file was executed.
|
||||
* --valgrind: Runs tests with valgrind. */
|
||||
#define UNIT_TEST_ACCURATE (1 << 0)
|
||||
#define UNIT_TEST_LARGE_MEMORY (1 << 1)
|
||||
#define UNIT_TEST_SINGLE (1 << 2)
|
||||
#define UNIT_TEST_VALGRIND (1 << 3)
|
||||
|
||||
#define KRED "\33[31m"
|
||||
#define KGRN "\33[32m"
|
||||
#define KBLUE "\33[34m"
|
||||
#define KRESET "\33[0m"
|
||||
|
||||
#define TEST_PRINT_ERROR(descr) printf("[" KRED "%s - %s:%d" KRESET "] %s\n", __func__, __FILE__, __LINE__, descr)
|
||||
|
||||
#define TEST_PRINT_INFO(descr, ...) \
|
||||
printf("[" KBLUE "%s - %s:%d" KRESET "] " descr "\n", __func__, __FILE__, __LINE__, __VA_ARGS__)
|
||||
|
||||
#define TEST_ASSERT_MESSAGE(descr, _c) \
|
||||
do { \
|
||||
if (!(_c)) { \
|
||||
TEST_PRINT_ERROR(descr); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define TEST_ASSERT(_c) TEST_ASSERT_MESSAGE("Failed assertion: " #_c, _c)
|
||||
|
||||
#define TEST_EXPECT_MESSAGE(descr, _c) \
|
||||
do { \
|
||||
if (!(_c)) { \
|
||||
TEST_PRINT_ERROR(descr); \
|
||||
failed_expects++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define TEST_EXPECT(_c) TEST_EXPECT_MESSAGE("Failed expect: " #_c, _c)
|
||||
|
||||
#ifndef UNUSED
|
||||
#define UNUSED(x) (void)(x)
|
||||
#endif
|
||||
|
||||
extern int failed_expects;
|
||||
|
||||
#endif
|
||||
@@ -1,241 +0,0 @@
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "../intset.c"
|
||||
#include "test_help.h"
|
||||
#if defined(__GNUC__) && __GNUC__ >= 7
|
||||
/* Several functions in this file get inlined in such a way that fortify warns there might
|
||||
* be an out of bounds memory access depending on the intset encoding, but they aren't actually
|
||||
* reachable because we check the encoding. There are other strategies to fix this, but they
|
||||
* all require other hacks to prevent the inlining. So for now, just omit the check. */
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Warray-bounds"
|
||||
#endif
|
||||
|
||||
static long long usec(void) {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (((long long)tv.tv_sec) * 1000000) + tv.tv_usec;
|
||||
}
|
||||
|
||||
static intset *createSet(int bits, int size) {
|
||||
uint64_t mask = (1 << bits) - 1;
|
||||
uint64_t value;
|
||||
intset *is = intsetNew();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (bits > 32) {
|
||||
value = (rand() * rand()) & mask;
|
||||
} else {
|
||||
value = rand() & mask;
|
||||
}
|
||||
is = intsetAdd(is, value, NULL);
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
static int checkConsistency(intset *is) {
|
||||
for (uint32_t i = 0; i < (intrev32ifbe(is->length) - 1); i++) {
|
||||
uint32_t encoding = intrev32ifbe(is->encoding);
|
||||
|
||||
if (encoding == INTSET_ENC_INT16) {
|
||||
int16_t *i16 = (int16_t *)is->contents;
|
||||
TEST_ASSERT(i16[i] < i16[i + 1]);
|
||||
} else if (encoding == INTSET_ENC_INT32) {
|
||||
int32_t *i32 = (int32_t *)is->contents;
|
||||
TEST_ASSERT(i32[i] < i32[i + 1]);
|
||||
} else {
|
||||
int64_t *i64 = (int64_t *)is->contents;
|
||||
TEST_ASSERT(i64[i] < i64[i + 1]);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int test_intsetValueEncodings(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_ASSERT(_intsetValueEncoding(-32768) == INTSET_ENC_INT16);
|
||||
TEST_ASSERT(_intsetValueEncoding(+32767) == INTSET_ENC_INT16);
|
||||
TEST_ASSERT(_intsetValueEncoding(-32769) == INTSET_ENC_INT32);
|
||||
TEST_ASSERT(_intsetValueEncoding(+32768) == INTSET_ENC_INT32);
|
||||
TEST_ASSERT(_intsetValueEncoding(-2147483648) == INTSET_ENC_INT32);
|
||||
TEST_ASSERT(_intsetValueEncoding(+2147483647) == INTSET_ENC_INT32);
|
||||
TEST_ASSERT(_intsetValueEncoding(-2147483649) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(_intsetValueEncoding(+2147483648) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(_intsetValueEncoding(-9223372036854775808ull) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(_intsetValueEncoding(+9223372036854775807ull) == INTSET_ENC_INT64);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetBasicAdding(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
intset *is = intsetNew();
|
||||
uint8_t success;
|
||||
is = intsetAdd(is, 5, &success);
|
||||
TEST_ASSERT(success);
|
||||
is = intsetAdd(is, 6, &success);
|
||||
TEST_ASSERT(success);
|
||||
is = intsetAdd(is, 4, &success);
|
||||
TEST_ASSERT(success);
|
||||
is = intsetAdd(is, 4, &success);
|
||||
TEST_ASSERT(!success);
|
||||
TEST_ASSERT(6 == intsetMax(is));
|
||||
TEST_ASSERT(4 == intsetMin(is));
|
||||
zfree(is);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetLargeNumberRandomAdd(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
uint32_t inserts = 0;
|
||||
uint8_t success;
|
||||
intset *is = intsetNew();
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
is = intsetAdd(is, rand() % 0x800, &success);
|
||||
if (success) inserts++;
|
||||
}
|
||||
TEST_ASSERT(intrev32ifbe(is->length) == inserts);
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetUpgradeFromint16Toint32(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
intset *is = intsetNew();
|
||||
is = intsetAdd(is, 32, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, 65535, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT32);
|
||||
TEST_ASSERT(intsetFind(is, 32));
|
||||
TEST_ASSERT(intsetFind(is, 65535));
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is, 32, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, -65535, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT32);
|
||||
TEST_ASSERT(intsetFind(is, 32));
|
||||
TEST_ASSERT(intsetFind(is, -65535));
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetUpgradeFromint16Toint64(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
intset *is = intsetNew();
|
||||
is = intsetAdd(is, 32, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, 4294967295, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(intsetFind(is, 32));
|
||||
TEST_ASSERT(intsetFind(is, 4294967295));
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is, 32, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, -4294967295, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(intsetFind(is, 32));
|
||||
TEST_ASSERT(intsetFind(is, -4294967295));
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetUpgradeFromint32Toint64(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
intset *is = intsetNew();
|
||||
is = intsetAdd(is, 65535, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT32);
|
||||
is = intsetAdd(is, 4294967295, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(intsetFind(is, 65535));
|
||||
TEST_ASSERT(intsetFind(is, 4294967295));
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is, 65535, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT32);
|
||||
is = intsetAdd(is, -4294967295, NULL);
|
||||
TEST_ASSERT(intrev32ifbe(is->encoding) == INTSET_ENC_INT64);
|
||||
TEST_ASSERT(intsetFind(is, 65535));
|
||||
TEST_ASSERT(intsetFind(is, -4294967295));
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetStressLookups(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
long num = 100000, size = 10000;
|
||||
int i, bits = 20;
|
||||
long long start;
|
||||
intset *is = createSet(bits, size);
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
|
||||
start = usec();
|
||||
for (i = 0; i < num; i++) intsetSearch(is, rand() % ((1 << bits) - 1), NULL);
|
||||
TEST_PRINT_INFO("%ld lookups, %ld element set, %lldusec\n", num, size, usec() - start);
|
||||
zfree(is);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_intsetStressAddDelete(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int i, v1, v2;
|
||||
intset *is = intsetNew();
|
||||
for (i = 0; i < 0xffff; i++) {
|
||||
v1 = rand() % 0xfff;
|
||||
is = intsetAdd(is, v1, NULL);
|
||||
TEST_ASSERT(intsetFind(is, v1));
|
||||
|
||||
v2 = rand() % 0xfff;
|
||||
is = intsetRemove(is, v2, NULL);
|
||||
TEST_ASSERT(!intsetFind(is, v2));
|
||||
}
|
||||
TEST_ASSERT(checkConsistency(is) == 1);
|
||||
zfree(is);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) && __GNUC__ >= 12
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <sys/time.h>
|
||||
|
||||
extern "C" {
|
||||
#include "intset.h"
|
||||
|
||||
/* Wrapper function declarations for accessing static intset.c internals */
|
||||
uint8_t testOnlyIntsetValueEncoding(int64_t v);
|
||||
int64_t testOnlyIntsetGetEncoded(intset *is, int pos, uint8_t enc);
|
||||
int64_t testOnlyIntsetGet(intset *is, int pos);
|
||||
uint8_t testOnlyIntsetSearch(intset *is, int64_t value, uint32_t *pos);
|
||||
}
|
||||
|
||||
/* Macros from intset.c needed for testing */
|
||||
#define INTSET_ENC_INT16 (sizeof(int16_t))
|
||||
#define INTSET_ENC_INT32 (sizeof(int32_t))
|
||||
#define INTSET_ENC_INT64 (sizeof(int64_t))
|
||||
|
||||
static long long usec(void) {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, nullptr);
|
||||
return ((long long)tv.tv_sec * 1000000) + tv.tv_usec;
|
||||
}
|
||||
|
||||
static intset *createSet(int bits, int size) {
|
||||
uint64_t mask = (1 << bits) - 1;
|
||||
uint64_t value;
|
||||
intset *is = intsetNew();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (bits > 32) {
|
||||
value = (rand() * rand()) & mask;
|
||||
} else {
|
||||
value = rand() & mask;
|
||||
}
|
||||
is = intsetAdd(is, value, nullptr);
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
/* Use memcpy to avoid strict aliasing violations and potential alignment issues. */
|
||||
static int checkConsistency(intset *is) {
|
||||
for (uint32_t i = 0; i < (intrev32ifbe(is->length) - 1); i++) {
|
||||
uint32_t encoding = intrev32ifbe(is->encoding);
|
||||
|
||||
if (encoding == INTSET_ENC_INT16) {
|
||||
int16_t v1, v2;
|
||||
memcpy(&v1, is->contents + i * sizeof(int16_t), sizeof(int16_t));
|
||||
memcpy(&v2, is->contents + (i + 1) * sizeof(int16_t), sizeof(int16_t));
|
||||
if (v1 >= v2) return 0;
|
||||
} else if (encoding == INTSET_ENC_INT32) {
|
||||
int32_t v1, v2;
|
||||
memcpy(&v1, is->contents + i * sizeof(int32_t), sizeof(int32_t));
|
||||
memcpy(&v2, is->contents + (i + 1) * sizeof(int32_t), sizeof(int32_t));
|
||||
if (v1 >= v2) return 0;
|
||||
} else {
|
||||
int64_t v1, v2;
|
||||
memcpy(&v1, is->contents + i * sizeof(int64_t), sizeof(int64_t));
|
||||
memcpy(&v2, is->contents + (i + 1) * sizeof(int64_t), sizeof(int64_t));
|
||||
if (v1 >= v2) return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
class IntsetTest : public ::testing::Test {};
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetValueEncodings) {
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(-32768), INTSET_ENC_INT16);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(+32767), INTSET_ENC_INT16);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(-32769), INTSET_ENC_INT32);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(+32768), INTSET_ENC_INT32);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(-2147483648), INTSET_ENC_INT32);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(+2147483647), INTSET_ENC_INT32);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(-2147483649), INTSET_ENC_INT64);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(+2147483648), INTSET_ENC_INT64);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(-9223372036854775808ull), INTSET_ENC_INT64);
|
||||
ASSERT_EQ(testOnlyIntsetValueEncoding(+9223372036854775807ull), INTSET_ENC_INT64);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetBasicAdding) {
|
||||
intset *is = intsetNew();
|
||||
uint8_t success;
|
||||
is = intsetAdd(is, 5, &success);
|
||||
ASSERT_TRUE(success);
|
||||
is = intsetAdd(is, 6, &success);
|
||||
ASSERT_TRUE(success);
|
||||
is = intsetAdd(is, 4, &success);
|
||||
ASSERT_TRUE(success);
|
||||
is = intsetAdd(is, 4, &success);
|
||||
ASSERT_FALSE(success);
|
||||
ASSERT_EQ(6, intsetMax(is));
|
||||
ASSERT_EQ(4, intsetMin(is));
|
||||
zfree(is);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetLargeNumberRandomAdd) {
|
||||
uint32_t inserts = 0;
|
||||
uint8_t success;
|
||||
intset *is = intsetNew();
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
is = intsetAdd(is, rand() % 0x800, &success);
|
||||
if (success) inserts++;
|
||||
}
|
||||
ASSERT_EQ(intrev32ifbe(is->length), inserts);
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetUpgradeFromint16Toint32) {
|
||||
intset *is = intsetNew();
|
||||
is = intsetAdd(is, 32, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, 65535, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT32);
|
||||
ASSERT_TRUE(intsetFind(is, 32));
|
||||
ASSERT_TRUE(intsetFind(is, 65535));
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is, 32, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, -65535, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT32);
|
||||
ASSERT_TRUE(intsetFind(is, 32));
|
||||
ASSERT_TRUE(intsetFind(is, -65535));
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetUpgradeFromint16Toint64) {
|
||||
intset *is = intsetNew();
|
||||
is = intsetAdd(is, 32, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, 4294967295, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT64);
|
||||
ASSERT_TRUE(intsetFind(is, 32));
|
||||
ASSERT_TRUE(intsetFind(is, 4294967295));
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is, 32, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT16);
|
||||
is = intsetAdd(is, -4294967295, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT64);
|
||||
ASSERT_TRUE(intsetFind(is, 32));
|
||||
ASSERT_TRUE(intsetFind(is, -4294967295));
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetUpgradeFromint32Toint64) {
|
||||
intset *is = intsetNew();
|
||||
is = intsetAdd(is, 65535, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT32);
|
||||
is = intsetAdd(is, 4294967295, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT64);
|
||||
ASSERT_TRUE(intsetFind(is, 65535));
|
||||
ASSERT_TRUE(intsetFind(is, 4294967295));
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is, 65535, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT32);
|
||||
is = intsetAdd(is, -4294967295, nullptr);
|
||||
ASSERT_EQ(intrev32ifbe(is->encoding), INTSET_ENC_INT64);
|
||||
ASSERT_TRUE(intsetFind(is, 65535));
|
||||
ASSERT_TRUE(intsetFind(is, -4294967295));
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetStressLookups) {
|
||||
long num = 100000, size = 10000;
|
||||
int i, bits = 20;
|
||||
long long start;
|
||||
intset *is = createSet(bits, size);
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
|
||||
start = usec();
|
||||
for (i = 0; i < num; i++) testOnlyIntsetSearch(is, rand() % ((1 << bits) - 1), nullptr);
|
||||
printf("%ld lookups, %ld element set, %lldusec\n", num, size, usec() - start);
|
||||
zfree(is);
|
||||
}
|
||||
|
||||
TEST_F(IntsetTest, TestIntsetStressAddDelete) {
|
||||
int i, v1, v2;
|
||||
intset *is = intsetNew();
|
||||
for (i = 0; i < 0xffff; i++) {
|
||||
v1 = rand() % 0xfff;
|
||||
is = intsetAdd(is, v1, nullptr);
|
||||
ASSERT_TRUE(intsetFind(is, v1));
|
||||
|
||||
v2 = rand() % 0xfff;
|
||||
is = intsetRemove(is, v2, nullptr);
|
||||
ASSERT_FALSE(intsetFind(is, v2));
|
||||
}
|
||||
ASSERT_EQ(checkConsistency(is), 1);
|
||||
zfree(is);
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
#include "../kvstore.c"
|
||||
#include "test_help.h"
|
||||
|
||||
uint64_t hashTestCallback(const void *key) {
|
||||
return hashtableGenHashFunction((char *)key, strlen((char *)key));
|
||||
}
|
||||
|
||||
uint64_t hashConflictTestCallback(const void *key) {
|
||||
UNUSED(key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cmpTestCallback(const void *k1, const void *k2) {
|
||||
return strcmp(k1, k2);
|
||||
}
|
||||
|
||||
void freeTestCallback(void *val) {
|
||||
zfree(val);
|
||||
}
|
||||
|
||||
hashtableType KvstoreHashtableTestType = {
|
||||
.hashFunction = hashTestCallback,
|
||||
.keyCompare = cmpTestCallback,
|
||||
.entryDestructor = freeTestCallback,
|
||||
.rehashingStarted = kvstoreHashtableRehashingStarted,
|
||||
.rehashingCompleted = kvstoreHashtableRehashingCompleted,
|
||||
.trackMemUsage = kvstoreHashtableTrackMemUsage,
|
||||
.getMetadataSize = kvstoreHashtableMetadataSize,
|
||||
};
|
||||
|
||||
hashtableType KvstoreConflictHashtableTestType = {
|
||||
.hashFunction = hashConflictTestCallback,
|
||||
.keyCompare = cmpTestCallback,
|
||||
.entryDestructor = freeTestCallback,
|
||||
.rehashingStarted = kvstoreHashtableRehashingStarted,
|
||||
.rehashingCompleted = kvstoreHashtableRehashingCompleted,
|
||||
.trackMemUsage = kvstoreHashtableTrackMemUsage,
|
||||
.getMetadataSize = kvstoreHashtableMetadataSize,
|
||||
};
|
||||
|
||||
char *stringFromInt(int value) {
|
||||
char buf[32];
|
||||
int len;
|
||||
char *s;
|
||||
|
||||
len = snprintf(buf, sizeof(buf), "%d", value);
|
||||
s = zmalloc(len + 1);
|
||||
memcpy(s, buf, len);
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
int test_kvstoreAdd16Keys(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int i;
|
||||
|
||||
int didx = 0;
|
||||
kvstore *kvs0 = kvstoreCreate(&KvstoreHashtableTestType, 0, 0);
|
||||
kvstore *kvs1 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND);
|
||||
kvstore *kvs2 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs0, didx, stringFromInt(i)));
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs1, didx, stringFromInt(i)));
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs2, didx, stringFromInt(i)));
|
||||
}
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs0, didx) == 16);
|
||||
TEST_ASSERT(kvstoreSize(kvs0) == 16);
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs1, didx) == 16);
|
||||
TEST_ASSERT(kvstoreSize(kvs1) == 16);
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs2, didx) == 16);
|
||||
TEST_ASSERT(kvstoreSize(kvs2) == 16);
|
||||
|
||||
kvstoreRelease(kvs0);
|
||||
kvstoreRelease(kvs1);
|
||||
kvstoreRelease(kvs2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_kvstoreIteratorRemoveAllKeysNoDeleteEmptyHashtable(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
hashtableType *type[] = {
|
||||
&KvstoreHashtableTestType,
|
||||
&KvstoreConflictHashtableTestType,
|
||||
NULL,
|
||||
};
|
||||
|
||||
for (int t = 0; type[t] != NULL; t++) {
|
||||
hashtableType *testType = type[t];
|
||||
TEST_PRINT_INFO("Testing %d hashtableType\n", t);
|
||||
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreIterator *kvs_it;
|
||||
|
||||
int didx = 0;
|
||||
int curr_slot = 0;
|
||||
kvstore *kvs1 = kvstoreCreate(testType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs1, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_it = kvstoreIteratorInit(kvs1, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreIteratorNext(kvs_it, &key)) {
|
||||
curr_slot = kvstoreIteratorGetCurrentHashtableIndex(kvs_it);
|
||||
TEST_ASSERT(kvstoreHashtableDelete(kvs1, curr_slot, key));
|
||||
}
|
||||
kvstoreIteratorRelease(kvs_it);
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs1, didx);
|
||||
TEST_ASSERT(ht != NULL);
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs1, didx) == 0);
|
||||
TEST_ASSERT(kvstoreSize(kvs1) == 0);
|
||||
|
||||
kvstoreRelease(kvs1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_kvstoreIteratorRemoveAllKeysDeleteEmptyHashtable(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreIterator *kvs_it;
|
||||
|
||||
int didx = 0;
|
||||
int curr_slot = 0;
|
||||
kvstore *kvs2 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs2, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_it = kvstoreIteratorInit(kvs2, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreIteratorNext(kvs_it, &key)) {
|
||||
curr_slot = kvstoreIteratorGetCurrentHashtableIndex(kvs_it);
|
||||
TEST_ASSERT(kvstoreHashtableDelete(kvs2, curr_slot, key));
|
||||
}
|
||||
kvstoreIteratorRelease(kvs_it);
|
||||
|
||||
/* Make sure the hashtable was removed from the rehashing list. */
|
||||
while (kvstoreIncrementallyRehash(kvs2, 1000)) {
|
||||
}
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs2, didx);
|
||||
TEST_ASSERT(ht == NULL);
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs2, didx) == 0);
|
||||
TEST_ASSERT(kvstoreSize(kvs2) == 0);
|
||||
|
||||
kvstoreRelease(kvs2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_kvstoreHashtableIteratorRemoveAllKeysNoDeleteEmptyHashtable(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreHashtableIterator *kvs_di;
|
||||
|
||||
int didx = 0;
|
||||
kvstore *kvs1 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs1, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_di = kvstoreGetHashtableIterator(kvs1, didx, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreHashtableIteratorNext(kvs_di, &key)) {
|
||||
TEST_ASSERT(kvstoreHashtableDelete(kvs1, didx, key));
|
||||
}
|
||||
kvstoreReleaseHashtableIterator(kvs_di);
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs1, didx);
|
||||
TEST_ASSERT(ht != NULL);
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs1, didx) == 0);
|
||||
TEST_ASSERT(kvstoreSize(kvs1) == 0);
|
||||
|
||||
kvstoreRelease(kvs1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_kvstoreHashtableIteratorRemoveAllKeysDeleteEmptyHashtable(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreHashtableIterator *kvs_di;
|
||||
|
||||
int didx = 0;
|
||||
kvstore *kvs2 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
TEST_ASSERT(kvstoreHashtableAdd(kvs2, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_di = kvstoreGetHashtableIterator(kvs2, didx, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreHashtableIteratorNext(kvs_di, &key)) {
|
||||
TEST_ASSERT(kvstoreHashtableDelete(kvs2, didx, key));
|
||||
}
|
||||
kvstoreReleaseHashtableIterator(kvs_di);
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs2, didx);
|
||||
TEST_ASSERT(ht == NULL);
|
||||
TEST_ASSERT(kvstoreHashtableSize(kvs2, didx) == 0);
|
||||
TEST_ASSERT(kvstoreSize(kvs2) == 0);
|
||||
|
||||
kvstoreRelease(kvs2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_kvstoreHashtableExpand(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
kvstore *kvs = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
TEST_ASSERT(kvstoreGetHashtable(kvs, 0) == NULL);
|
||||
TEST_ASSERT(kvstoreHashtableExpand(kvs, 0, 10000));
|
||||
TEST_ASSERT(kvstoreGetHashtable(kvs, 0) != NULL);
|
||||
TEST_ASSERT(kvstoreBuckets(kvs) > 0);
|
||||
TEST_ASSERT(kvstoreBuckets(kvs) == kvstoreHashtableBuckets(kvs, 0));
|
||||
|
||||
kvstoreRelease(kvs);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "kvstore.h"
|
||||
}
|
||||
|
||||
uint64_t hashTestCallback(const void *key) {
|
||||
return hashtableGenHashFunction((const char *)key, strlen((const char *)key));
|
||||
}
|
||||
|
||||
uint64_t hashConflictTestCallback(const void *key) {
|
||||
UNUSED(key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cmpTestCallback(const void *k1, const void *k2) {
|
||||
return strcmp((const char *)k1, (const char *)k2);
|
||||
}
|
||||
|
||||
void freeTestCallback(void *val) {
|
||||
zfree(val);
|
||||
}
|
||||
|
||||
/* Hashtable types used for tests - initialized in SetUpTestSuite */
|
||||
static hashtableType KvstoreHashtableTestType;
|
||||
static hashtableType KvstoreConflictHashtableTestType;
|
||||
|
||||
char *stringFromInt(int value) {
|
||||
char buf[32];
|
||||
int len;
|
||||
char *s;
|
||||
|
||||
len = snprintf(buf, sizeof(buf), "%d", value);
|
||||
s = (char *)zmalloc(len + 1);
|
||||
memcpy(s, buf, len);
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
class KvstoreTest : public ::testing::Test {
|
||||
protected:
|
||||
static void SetUpTestSuite() {
|
||||
/* Initialize KvstoreHashtableTestType explicitly by field name to avoid
|
||||
* dependency on field order (designated initializers require C++20). */
|
||||
memset(&KvstoreHashtableTestType, 0, sizeof(KvstoreHashtableTestType));
|
||||
KvstoreHashtableTestType.hashFunction = hashTestCallback;
|
||||
KvstoreHashtableTestType.keyCompare = cmpTestCallback;
|
||||
KvstoreHashtableTestType.entryDestructor = freeTestCallback;
|
||||
KvstoreHashtableTestType.rehashingStarted = kvstoreHashtableRehashingStarted;
|
||||
KvstoreHashtableTestType.rehashingCompleted = kvstoreHashtableRehashingCompleted;
|
||||
KvstoreHashtableTestType.trackMemUsage = kvstoreHashtableTrackMemUsage;
|
||||
KvstoreHashtableTestType.getMetadataSize = kvstoreHashtableMetadataSize;
|
||||
|
||||
/* Initialize KvstoreConflictHashtableTestType */
|
||||
memset(&KvstoreConflictHashtableTestType, 0, sizeof(KvstoreConflictHashtableTestType));
|
||||
KvstoreConflictHashtableTestType.hashFunction = hashConflictTestCallback;
|
||||
KvstoreConflictHashtableTestType.keyCompare = cmpTestCallback;
|
||||
KvstoreConflictHashtableTestType.entryDestructor = freeTestCallback;
|
||||
KvstoreConflictHashtableTestType.rehashingStarted = kvstoreHashtableRehashingStarted;
|
||||
KvstoreConflictHashtableTestType.rehashingCompleted = kvstoreHashtableRehashingCompleted;
|
||||
KvstoreConflictHashtableTestType.trackMemUsage = kvstoreHashtableTrackMemUsage;
|
||||
KvstoreConflictHashtableTestType.getMetadataSize = kvstoreHashtableMetadataSize;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(KvstoreTest, kvstoreAdd16Keys) {
|
||||
int i;
|
||||
|
||||
int didx = 0;
|
||||
kvstore *kvs0 = kvstoreCreate(&KvstoreHashtableTestType, 0, 0);
|
||||
kvstore *kvs1 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND);
|
||||
kvstore *kvs2 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs0, didx, stringFromInt(i)));
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs1, didx, stringFromInt(i)));
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs2, didx, stringFromInt(i)));
|
||||
}
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs0, didx), 16u);
|
||||
ASSERT_EQ(kvstoreSize(kvs0), 16u);
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs1, didx), 16u);
|
||||
ASSERT_EQ(kvstoreSize(kvs1), 16u);
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs2, didx), 16u);
|
||||
ASSERT_EQ(kvstoreSize(kvs2), 16u);
|
||||
|
||||
kvstoreRelease(kvs0);
|
||||
kvstoreRelease(kvs1);
|
||||
kvstoreRelease(kvs2);
|
||||
}
|
||||
|
||||
TEST_F(KvstoreTest, kvstoreIteratorRemoveAllKeysNoDeleteEmptyHashtable) {
|
||||
hashtableType *type[] = {
|
||||
&KvstoreHashtableTestType,
|
||||
&KvstoreConflictHashtableTestType,
|
||||
nullptr,
|
||||
};
|
||||
|
||||
for (int t = 0; type[t] != nullptr; t++) {
|
||||
hashtableType *testType = type[t];
|
||||
printf("Testing %d hashtableType\n", t);
|
||||
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreIterator *kvs_it;
|
||||
|
||||
int didx = 0;
|
||||
int curr_slot = 0;
|
||||
kvstore *kvs1 = kvstoreCreate(testType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs1, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_it = kvstoreIteratorInit(kvs1, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreIteratorNext(kvs_it, &key)) {
|
||||
curr_slot = kvstoreIteratorGetCurrentHashtableIndex(kvs_it);
|
||||
ASSERT_TRUE(kvstoreHashtableDelete(kvs1, curr_slot, key));
|
||||
}
|
||||
kvstoreIteratorRelease(kvs_it);
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs1, didx);
|
||||
ASSERT_NE(ht, nullptr);
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs1, didx), 0u);
|
||||
ASSERT_EQ(kvstoreSize(kvs1), 0u);
|
||||
|
||||
kvstoreRelease(kvs1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(KvstoreTest, kvstoreIteratorRemoveAllKeysDeleteEmptyHashtable) {
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreIterator *kvs_it;
|
||||
|
||||
int didx = 0;
|
||||
int curr_slot = 0;
|
||||
kvstore *kvs2 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs2, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_it = kvstoreIteratorInit(kvs2, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreIteratorNext(kvs_it, &key)) {
|
||||
curr_slot = kvstoreIteratorGetCurrentHashtableIndex(kvs_it);
|
||||
ASSERT_TRUE(kvstoreHashtableDelete(kvs2, curr_slot, key));
|
||||
}
|
||||
kvstoreIteratorRelease(kvs_it);
|
||||
|
||||
/* Make sure the hashtable was removed from the rehashing list. */
|
||||
while (kvstoreIncrementallyRehash(kvs2, 1000)) {
|
||||
}
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs2, didx);
|
||||
ASSERT_EQ(ht, nullptr);
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs2, didx), 0u);
|
||||
ASSERT_EQ(kvstoreSize(kvs2), 0u);
|
||||
|
||||
kvstoreRelease(kvs2);
|
||||
}
|
||||
|
||||
TEST_F(KvstoreTest, kvstoreHashtableIteratorRemoveAllKeysNoDeleteEmptyHashtable) {
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreHashtableIterator *kvs_di;
|
||||
|
||||
int didx = 0;
|
||||
kvstore *kvs1 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs1, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_di = kvstoreGetHashtableIterator(kvs1, didx, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreHashtableIteratorNext(kvs_di, &key)) {
|
||||
ASSERT_TRUE(kvstoreHashtableDelete(kvs1, didx, key));
|
||||
}
|
||||
kvstoreReleaseHashtableIterator(kvs_di);
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs1, didx);
|
||||
ASSERT_NE(ht, nullptr);
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs1, didx), 0u);
|
||||
ASSERT_EQ(kvstoreSize(kvs1), 0u);
|
||||
|
||||
kvstoreRelease(kvs1);
|
||||
}
|
||||
|
||||
TEST_F(KvstoreTest, kvstoreHashtableIteratorRemoveAllKeysDeleteEmptyHashtable) {
|
||||
int i;
|
||||
void *key;
|
||||
kvstoreHashtableIterator *kvs_di;
|
||||
|
||||
int didx = 0;
|
||||
kvstore *kvs2 = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
ASSERT_TRUE(kvstoreHashtableAdd(kvs2, didx, stringFromInt(i)));
|
||||
}
|
||||
|
||||
kvs_di = kvstoreGetHashtableIterator(kvs2, didx, HASHTABLE_ITER_SAFE);
|
||||
while (kvstoreHashtableIteratorNext(kvs_di, &key)) {
|
||||
ASSERT_TRUE(kvstoreHashtableDelete(kvs2, didx, key));
|
||||
}
|
||||
kvstoreReleaseHashtableIterator(kvs_di);
|
||||
|
||||
hashtable *ht = kvstoreGetHashtable(kvs2, didx);
|
||||
ASSERT_EQ(ht, nullptr);
|
||||
ASSERT_EQ(kvstoreHashtableSize(kvs2, didx), 0u);
|
||||
ASSERT_EQ(kvstoreSize(kvs2), 0u);
|
||||
|
||||
kvstoreRelease(kvs2);
|
||||
}
|
||||
|
||||
TEST_F(KvstoreTest, kvstoreHashtableExpand) {
|
||||
kvstore *kvs = kvstoreCreate(&KvstoreHashtableTestType, 0, KVSTORE_ALLOCATE_HASHTABLES_ON_DEMAND | KVSTORE_FREE_EMPTY_HASHTABLES);
|
||||
|
||||
ASSERT_EQ(kvstoreGetHashtable(kvs, 0), nullptr);
|
||||
ASSERT_TRUE(kvstoreHashtableExpand(kvs, 0, 10000));
|
||||
ASSERT_NE(kvstoreGetHashtable(kvs, 0), nullptr);
|
||||
ASSERT_GT(kvstoreBuckets(kvs), 0u);
|
||||
ASSERT_EQ(kvstoreBuckets(kvs), kvstoreHashtableBuckets(kvs, 0));
|
||||
|
||||
kvstoreRelease(kvs);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include <strings.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "test_files.h"
|
||||
#include "test_help.h"
|
||||
#include "../fmacros.h"
|
||||
#include "../util.h"
|
||||
#include "../mt19937-64.h"
|
||||
#include "../hashtable.h"
|
||||
#include "../zmalloc.h"
|
||||
|
||||
int failed_expects;
|
||||
|
||||
/* We override the default assertion mechanism, so that it prints out info and then dies. */
|
||||
void _serverAssert(const char *estr, const char *file, int line) {
|
||||
printf("[" KRED "serverAssert - %s:%d" KRESET "] - %s\n", file, line, estr);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* Run the tests defined by the test suite. */
|
||||
int runTestSuite(struct unitTestSuite *test, int argc, char **argv, int flags) {
|
||||
int test_num = 0;
|
||||
int failed_tests = 0;
|
||||
size_t used_mem_before = zmalloc_used_memory();
|
||||
printf("[" KBLUE "START" KRESET "] - %s\n", test->filename);
|
||||
|
||||
for (int id = 0; test->tests[id].proc != NULL; id++) {
|
||||
test_num++;
|
||||
failed_expects = 0;
|
||||
int test_result = test->tests[id].proc(argc, argv, flags);
|
||||
if (!test_result && !failed_expects) {
|
||||
printf("[" KGRN "ok" KRESET "] - %s:%s\n", test->filename, test->tests[id].name);
|
||||
} else {
|
||||
printf("[" KRED "fail" KRESET "] - %s:%s\n", test->filename, test->tests[id].name);
|
||||
failed_tests++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if the test suite has cleaned up all the memory used. */
|
||||
test_num++;
|
||||
if (zmalloc_used_memory() != used_mem_before) {
|
||||
printf("[" KRED "%s" KRESET "] Memory leak detected of %lld bytes\n",
|
||||
test->filename,
|
||||
(long long)zmalloc_used_memory() - (long long)used_mem_before);
|
||||
failed_tests++;
|
||||
}
|
||||
|
||||
printf("[" KBLUE "END" KRESET "] - %s: ", test->filename);
|
||||
printf("%d tests, %d passed, %d failed\n", test_num, test_num - failed_tests, failed_tests);
|
||||
return !failed_tests;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int flags = 0;
|
||||
char *file = NULL;
|
||||
char *seed = NULL;
|
||||
for (int j = 1; j < argc; j++) {
|
||||
char *arg = argv[j];
|
||||
if (!strcasecmp(arg, "--accurate"))
|
||||
flags |= UNIT_TEST_ACCURATE;
|
||||
else if (!strcasecmp(arg, "--large-memory"))
|
||||
flags |= UNIT_TEST_LARGE_MEMORY;
|
||||
else if (!strcasecmp(arg, "--single") && (j + 1 < argc)) {
|
||||
flags |= UNIT_TEST_SINGLE;
|
||||
file = argv[j + 1];
|
||||
} else if (!strcasecmp(arg, "--valgrind")) {
|
||||
flags |= UNIT_TEST_VALGRIND;
|
||||
} else if (!strcasecmp(arg, "--seed")) {
|
||||
seed = argv[j + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (seed) {
|
||||
setRandomSeedCString(seed, strlen(seed));
|
||||
}
|
||||
|
||||
char seed_cstr[129];
|
||||
getRandomSeedCString(seed_cstr, 129);
|
||||
|
||||
printf("Tests will run with seed=%s\n", seed_cstr);
|
||||
|
||||
unsigned long long genrandseed;
|
||||
getRandomBytes((void *)&genrandseed, sizeof(genrandseed));
|
||||
|
||||
uint8_t hashseed[16];
|
||||
getRandomBytes(hashseed, sizeof(hashseed));
|
||||
|
||||
|
||||
int numtests = sizeof(unitTestSuite) / sizeof(struct unitTestSuite);
|
||||
int failed_num = 0, suites_executed = 0;
|
||||
for (int j = 0; j < numtests; j++) {
|
||||
if (file && strcasecmp(file, unitTestSuite[j].filename)) continue;
|
||||
|
||||
/* We need to explicitly set the seed in the several random numbers
|
||||
* generator that valkey server uses so that the unit tests reproduce
|
||||
* the random values in a deterministic way. */
|
||||
setRandomSeedCString(seed_cstr, strlen(seed_cstr));
|
||||
init_genrand64(genrandseed);
|
||||
srandom((unsigned)genrandseed);
|
||||
hashtableSetHashFunctionSeed(hashseed);
|
||||
|
||||
if (!runTestSuite(&unitTestSuite[j], argc, argv, flags)) {
|
||||
failed_num++;
|
||||
}
|
||||
suites_executed++;
|
||||
}
|
||||
printf("%d test suites executed, %d passed, %d failed\n", suites_executed, suites_executed - failed_num,
|
||||
failed_num);
|
||||
|
||||
return failed_num == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "../mutexqueue.h"
|
||||
#include "test_help.h"
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
|
||||
/* Helper functions */
|
||||
static void add(mutexQueue *q, long value) {
|
||||
unsigned long len = mutexQueueLength(q);
|
||||
mutexQueueAdd(q, (void *)value);
|
||||
TEST_EXPECT(mutexQueueLength(q) == len + 1);
|
||||
}
|
||||
|
||||
static void priorityAdd(mutexQueue *q, long value) {
|
||||
unsigned long len = mutexQueueLength(q);
|
||||
mutexQueuePushPriority(q, (void *)value);
|
||||
TEST_EXPECT(mutexQueueLength(q) == len + 1);
|
||||
}
|
||||
|
||||
static void popTest(mutexQueue *q, long expected) {
|
||||
unsigned long len = mutexQueueLength(q);
|
||||
long value = (long)mutexQueuePop(q, false);
|
||||
TEST_EXPECT(mutexQueueLength(q) == len - 1);
|
||||
TEST_EXPECT(value == expected);
|
||||
}
|
||||
|
||||
/* Test: simplePushPop */
|
||||
int test_mutexQueueSimplePushPop(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
add(q, 1);
|
||||
popTest(q, 1);
|
||||
TEST_EXPECT(mutexQueuePop(q, false) == NULL);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: doublePushPop */
|
||||
int test_mutexQueueDoublePushPop(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
add(q, 1);
|
||||
add(q, 2);
|
||||
popTest(q, 1);
|
||||
popTest(q, 2);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: priorityOrdering */
|
||||
int test_mutexQueuePriorityOrdering(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
add(q, 10);
|
||||
priorityAdd(q, 1);
|
||||
add(q, 11);
|
||||
priorityAdd(q, 2);
|
||||
popTest(q, 1);
|
||||
popTest(q, 2);
|
||||
popTest(q, 10);
|
||||
popTest(q, 11);
|
||||
TEST_EXPECT(mutexQueuePop(q, false) == NULL);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: fifoPopAll */
|
||||
int test_mutexQueueFifoPopAll(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
add(q, 10);
|
||||
priorityAdd(q, 1);
|
||||
add(q, 11);
|
||||
priorityAdd(q, 2);
|
||||
|
||||
fifo *f = mutexQueuePopAll(q, false);
|
||||
TEST_ASSERT(f != NULL); /* Fatal - can't continue if NULL */
|
||||
TEST_EXPECT(mutexQueuePop(q, false) == NULL);
|
||||
TEST_EXPECT(mutexQueuePopAll(q, false) == NULL);
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
|
||||
void *ptr;
|
||||
TEST_EXPECT(fifoPop(f, &ptr) && (unsigned long)ptr == 1ul);
|
||||
TEST_EXPECT(fifoPop(f, &ptr) && (unsigned long)ptr == 2ul);
|
||||
TEST_EXPECT(fifoPop(f, &ptr) && (unsigned long)ptr == 10ul);
|
||||
TEST_EXPECT(fifoPop(f, &ptr) && (unsigned long)ptr == 11ul);
|
||||
TEST_EXPECT(fifoLength(f) == 0);
|
||||
|
||||
fifoRelease(f);
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: fifoAddMultiple */
|
||||
int test_mutexQueueFifoAddMultiple(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
add(q, 1);
|
||||
|
||||
fifo *f = fifoCreate();
|
||||
fifoPush(f, (void *)2);
|
||||
fifoPush(f, (void *)3);
|
||||
mutexQueueAddMultiple(q, f);
|
||||
TEST_EXPECT(fifoLength(f) == 0u);
|
||||
fifoRelease(f);
|
||||
|
||||
add(q, 4);
|
||||
priorityAdd(q, 0);
|
||||
popTest(q, 0);
|
||||
popTest(q, 1);
|
||||
popTest(q, 2);
|
||||
popTest(q, 3);
|
||||
popTest(q, 4);
|
||||
TEST_EXPECT(mutexQueuePop(q, false) == NULL);
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Thread functions for concurrent tests */
|
||||
static void *queue_writer(void *arg) {
|
||||
mutexQueue *queue = (mutexQueue *)arg;
|
||||
for (int i = 1; i <= 1000; i++) {
|
||||
mutexQueueAdd(queue, (void *)(long)i);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *queue_reader(void *arg) {
|
||||
mutexQueue *queue = (mutexQueue *)arg;
|
||||
int count = 0;
|
||||
while (count < 1000) {
|
||||
long value = (long)mutexQueuePop(queue, true);
|
||||
TEST_EXPECT(value != 0); /* Should never be null if blocking */
|
||||
count++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Test: simpleThread */
|
||||
int test_mutexQueueSimpleThread(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
int rc;
|
||||
pthread_t writer, reader;
|
||||
|
||||
rc = pthread_create(&writer, NULL, &queue_writer, q);
|
||||
TEST_ASSERT(rc == 0); /* Fatal - can't continue if thread creation fails */
|
||||
rc = pthread_create(&reader, NULL, &queue_reader, q);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_join(writer, NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_join(reader, NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
|
||||
rc = pthread_create(&reader, NULL, &queue_reader, q);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_create(&writer, NULL, &queue_writer, q);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_join(writer, NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_join(reader, NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: parallelWriters */
|
||||
int test_mutexQueueParallelWriters(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
const int num_threads = 20;
|
||||
int rc;
|
||||
pthread_t writer[num_threads];
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_create(&writer[i], NULL, &queue_writer, q);
|
||||
TEST_ASSERT(rc == 0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_join(writer[i], NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
}
|
||||
|
||||
TEST_EXPECT(mutexQueueLength(q) == (unsigned long)(num_threads * 1000));
|
||||
|
||||
fifo *f = mutexQueuePopAll(q, false);
|
||||
fifoRelease(f);
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: parallelReaders */
|
||||
int test_mutexQueueParallelReaders(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
const int num_threads = 20;
|
||||
int rc;
|
||||
pthread_t reader[num_threads];
|
||||
|
||||
/* Start readers in advance - we want them fighting... */
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_create(&reader[i], NULL, &queue_reader, q);
|
||||
TEST_ASSERT(rc == 0); /* Fatal - can't continue if thread creation fails */
|
||||
}
|
||||
|
||||
/* Now perform writes serially... */
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
queue_writer(q);
|
||||
/* make sure other threads get to fight with a short sleep (don't write all at once!) */
|
||||
nanosleep((const struct timespec[]){{0, 10000000L}}, NULL); /* 10ms */
|
||||
}
|
||||
|
||||
/* Readers should finish */
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_join(reader[i], NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
}
|
||||
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Test: parallelReadWrite */
|
||||
int test_mutexQueueParallelReadWrite(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
mutexQueue *q = mutexQueueCreate();
|
||||
const int num_threads = 20;
|
||||
int rc;
|
||||
pthread_t reader[num_threads];
|
||||
pthread_t writer[num_threads];
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_create(&writer[i], NULL, &queue_writer, q);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_create(&reader[i], NULL, &queue_reader, q);
|
||||
TEST_ASSERT(rc == 0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_join(writer[i], NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
rc = pthread_join(reader[i], NULL);
|
||||
TEST_ASSERT(rc == 0);
|
||||
}
|
||||
|
||||
TEST_EXPECT(mutexQueueLength(q) == 0ul);
|
||||
|
||||
mutexQueueRelease(q);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
|
||||
extern "C" {
|
||||
#include "fifo.h"
|
||||
#include "mutexqueue.h"
|
||||
}
|
||||
|
||||
class MutexQueueTest : public ::testing::Test {
|
||||
protected:
|
||||
mutexQueue *q;
|
||||
|
||||
void SetUp() override {
|
||||
q = mutexQueueCreate();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
fifo *fifo = mutexQueuePopAll(q, false);
|
||||
if (fifo) fifoRelease(fifo);
|
||||
mutexQueueRelease(q);
|
||||
}
|
||||
|
||||
/* Helper function to add a value and verify the length increases */
|
||||
void add(long value) {
|
||||
unsigned long len = mutexQueueLength(q);
|
||||
mutexQueueAdd(q, (void *)value);
|
||||
EXPECT_EQ(mutexQueueLength(q), len + 1);
|
||||
}
|
||||
|
||||
/* Helper function to add a priority value and verify the length increases */
|
||||
void priorityAdd(long value) {
|
||||
unsigned long len = mutexQueueLength(q);
|
||||
mutexQueuePushPriority(q, (void *)value);
|
||||
EXPECT_EQ(mutexQueueLength(q), len + 1);
|
||||
}
|
||||
|
||||
/* Helper function to pop and verify the expected value */
|
||||
void popTest(long expected) {
|
||||
unsigned long len = mutexQueueLength(q);
|
||||
long value = (long)mutexQueuePop(q, false);
|
||||
EXPECT_EQ(mutexQueueLength(q), len - 1);
|
||||
EXPECT_EQ(value, expected);
|
||||
}
|
||||
};
|
||||
|
||||
/* Test: simplePushPop */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueSimplePushPop) {
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
add(1);
|
||||
popTest(1);
|
||||
EXPECT_EQ(mutexQueuePop(q, false), nullptr);
|
||||
}
|
||||
|
||||
/* Test: doublePushPop */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueDoublePushPop) {
|
||||
add(1);
|
||||
add(2);
|
||||
popTest(1);
|
||||
popTest(2);
|
||||
}
|
||||
|
||||
/* Test: priorityOrdering */
|
||||
TEST_F(MutexQueueTest, TestMutexQueuePriorityOrdering) {
|
||||
add(10);
|
||||
priorityAdd(1);
|
||||
add(11);
|
||||
priorityAdd(2);
|
||||
popTest(1);
|
||||
popTest(2);
|
||||
popTest(10);
|
||||
popTest(11);
|
||||
EXPECT_EQ(mutexQueuePop(q, false), nullptr);
|
||||
}
|
||||
|
||||
/* Test: fifoPopAll */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueFifoPopAll) {
|
||||
add(10);
|
||||
priorityAdd(1);
|
||||
add(11);
|
||||
priorityAdd(2);
|
||||
|
||||
fifo *f = mutexQueuePopAll(q, false);
|
||||
ASSERT_NE(f, nullptr); /* Fatal - can't continue if NULL */
|
||||
EXPECT_EQ(mutexQueuePop(q, false), nullptr);
|
||||
EXPECT_EQ(mutexQueuePopAll(q, false), nullptr);
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
|
||||
void *ptr;
|
||||
EXPECT_TRUE(fifoPop(f, &ptr) && (unsigned long)ptr == 1ul);
|
||||
EXPECT_TRUE(fifoPop(f, &ptr) && (unsigned long)ptr == 2ul);
|
||||
EXPECT_TRUE(fifoPop(f, &ptr) && (unsigned long)ptr == 10ul);
|
||||
EXPECT_TRUE(fifoPop(f, &ptr) && (unsigned long)ptr == 11ul);
|
||||
EXPECT_EQ(fifoLength(f), 0);
|
||||
|
||||
fifoRelease(f);
|
||||
}
|
||||
|
||||
/* Test: fifoAddMultiple */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueFifoAddMultiple) {
|
||||
add(1);
|
||||
|
||||
fifo *f = fifoCreate();
|
||||
fifoPush(f, (void *)2);
|
||||
fifoPush(f, (void *)3);
|
||||
mutexQueueAddMultiple(q, f);
|
||||
EXPECT_EQ(fifoLength(f), 0L);
|
||||
fifoRelease(f);
|
||||
|
||||
add(4);
|
||||
priorityAdd(0);
|
||||
popTest(0);
|
||||
popTest(1);
|
||||
popTest(2);
|
||||
popTest(3);
|
||||
popTest(4);
|
||||
EXPECT_EQ(mutexQueuePop(q, false), nullptr);
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
}
|
||||
|
||||
/* Thread functions for concurrent tests */
|
||||
static void *queue_writer(void *arg) {
|
||||
mutexQueue *queue = (mutexQueue *)arg;
|
||||
for (int i = 1; i <= 1000; i++) {
|
||||
mutexQueueAdd(queue, (void *)(long)i);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void *queue_reader(void *arg) {
|
||||
mutexQueue *queue = (mutexQueue *)arg;
|
||||
int count = 0;
|
||||
while (count < 1000) {
|
||||
long value = (long)mutexQueuePop(queue, true);
|
||||
EXPECT_NE(value, 0); /* Should never be null if blocking */
|
||||
count++;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Test: simpleThread */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueSimpleThread) {
|
||||
int rc;
|
||||
pthread_t writer, reader;
|
||||
|
||||
rc = pthread_create(&writer, nullptr, &queue_writer, q);
|
||||
ASSERT_EQ(rc, 0); /* Fatal - can't continue if thread creation fails */
|
||||
rc = pthread_create(&reader, nullptr, &queue_reader, q);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_join(writer, nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_join(reader, nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
|
||||
rc = pthread_create(&reader, nullptr, &queue_reader, q);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_create(&writer, nullptr, &queue_writer, q);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_join(writer, nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_join(reader, nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
}
|
||||
|
||||
/* Test: parallelWriters */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueParallelWriters) {
|
||||
const int num_threads = 20;
|
||||
int rc;
|
||||
pthread_t writer[num_threads];
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_create(&writer[i], nullptr, &queue_writer, q);
|
||||
ASSERT_EQ(rc, 0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_join(writer[i], nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
}
|
||||
|
||||
EXPECT_EQ(mutexQueueLength(q), (unsigned long)(num_threads * 1000));
|
||||
|
||||
fifo *f = mutexQueuePopAll(q, false);
|
||||
fifoRelease(f);
|
||||
}
|
||||
|
||||
/* Test: parallelReaders */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueParallelReaders) {
|
||||
const int num_threads = 20;
|
||||
int rc;
|
||||
pthread_t reader[num_threads];
|
||||
|
||||
/* Start readers in advance - we want them fighting... */
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_create(&reader[i], nullptr, &queue_reader, q);
|
||||
ASSERT_EQ(rc, 0); /* Fatal - can't continue if thread creation fails */
|
||||
}
|
||||
|
||||
/* Now perform writes serially... */
|
||||
struct timespec sleep_time = {0, 10000000L}; /* 10ms */
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
queue_writer(q);
|
||||
/* make sure other threads get to fight with a short sleep (don't write all at once!) */
|
||||
nanosleep(&sleep_time, nullptr);
|
||||
}
|
||||
|
||||
/* Readers should finish */
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_join(reader[i], nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
}
|
||||
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
}
|
||||
|
||||
/* Test: parallelReadWrite */
|
||||
TEST_F(MutexQueueTest, TestMutexQueueParallelReadWrite) {
|
||||
const int num_threads = 20;
|
||||
int rc;
|
||||
pthread_t reader[num_threads];
|
||||
pthread_t writer[num_threads];
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_create(&writer[i], nullptr, &queue_writer, q);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_create(&reader[i], nullptr, &queue_reader, q);
|
||||
ASSERT_EQ(rc, 0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_threads; i++) {
|
||||
rc = pthread_join(writer[i], nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
rc = pthread_join(reader[i], nullptr);
|
||||
ASSERT_EQ(rc, 0);
|
||||
}
|
||||
|
||||
EXPECT_EQ(mutexQueueLength(q), 0ul);
|
||||
}
|
||||
@@ -1,703 +0,0 @@
|
||||
#include "../networking.c"
|
||||
#include "../server.c"
|
||||
#include "test_help.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
/* Fake structures and functions */
|
||||
typedef struct fakeConnection {
|
||||
connection conn;
|
||||
int error;
|
||||
char *buffer;
|
||||
size_t buf_size;
|
||||
size_t written;
|
||||
} fakeConnection;
|
||||
|
||||
/* Fake connWrite function */
|
||||
static int fake_connWrite(connection *conn, const void *data, size_t size) {
|
||||
fakeConnection *fake_conn = (fakeConnection *)conn;
|
||||
if (fake_conn->error) return -1;
|
||||
|
||||
size_t to_write = size;
|
||||
if (fake_conn->written + to_write > fake_conn->buf_size) {
|
||||
to_write = fake_conn->buf_size - fake_conn->written;
|
||||
}
|
||||
|
||||
memcpy(fake_conn->buffer + fake_conn->written, data, to_write);
|
||||
fake_conn->written += to_write;
|
||||
return to_write;
|
||||
}
|
||||
|
||||
/* Fake connWritev function */
|
||||
static int fake_connWritev(connection *conn, const struct iovec *iov, int iovcnt) {
|
||||
fakeConnection *fake_conn = (fakeConnection *)conn;
|
||||
if (fake_conn->error) return -1;
|
||||
|
||||
size_t total = 0;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
size_t to_write = iov[i].iov_len;
|
||||
if (fake_conn->written + to_write > fake_conn->buf_size) {
|
||||
to_write = fake_conn->buf_size - fake_conn->written;
|
||||
}
|
||||
if (to_write == 0) break;
|
||||
|
||||
memcpy(fake_conn->buffer + fake_conn->written, iov[i].iov_base, to_write);
|
||||
fake_conn->written += to_write;
|
||||
total += to_write;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/* Fake connection type */
|
||||
static ConnectionType CT_Fake = {
|
||||
.write = fake_connWrite,
|
||||
.writev = fake_connWritev,
|
||||
};
|
||||
|
||||
static fakeConnection *connCreateFake(void) {
|
||||
fakeConnection *conn = zcalloc(sizeof(fakeConnection));
|
||||
conn->conn.type = &CT_Fake;
|
||||
conn->conn.fd = -1;
|
||||
conn->conn.iovcnt = IOV_MAX;
|
||||
return conn;
|
||||
}
|
||||
|
||||
int test_writeToReplica(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
client *c = zcalloc(sizeof(client));
|
||||
initClientReplicationData(c);
|
||||
server.repl_buffer_blocks = listCreate();
|
||||
createReplicationBacklog();
|
||||
c->reply = listCreate();
|
||||
|
||||
/* Test 1: Single block write */
|
||||
{
|
||||
fakeConnection *fake_conn = connCreateFake();
|
||||
fake_conn->buffer = zmalloc(1024);
|
||||
fake_conn->buf_size = 1024;
|
||||
c->conn = (connection *)fake_conn;
|
||||
|
||||
/* Create replication buffer block */
|
||||
replBufBlock *block = zmalloc(sizeof(replBufBlock) + 128);
|
||||
block->size = 128;
|
||||
block->used = 64;
|
||||
memset(block->buf, 'A', 64);
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 0;
|
||||
c->bufpos = 0;
|
||||
|
||||
writeToReplica(c);
|
||||
|
||||
TEST_ASSERT(c->nwritten == 64);
|
||||
TEST_ASSERT(fake_conn->written == 64);
|
||||
TEST_ASSERT(memcmp(fake_conn->buffer, block->buf, 64) == 0);
|
||||
TEST_ASSERT((c->write_flags & WRITE_FLAGS_WRITE_ERROR) == 0);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(fake_conn->buffer);
|
||||
zfree(fake_conn);
|
||||
zfree(block);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 2: Multiple blocks write */
|
||||
{
|
||||
fakeConnection *fake_conn = connCreateFake();
|
||||
fake_conn->error = 0;
|
||||
fake_conn->written = 0;
|
||||
fake_conn->buffer = zmalloc(1024);
|
||||
fake_conn->buf_size = 1024;
|
||||
c->conn = (connection *)fake_conn;
|
||||
|
||||
/* Create multiple replication buffer blocks */
|
||||
replBufBlock *block1 = zmalloc(sizeof(replBufBlock) + 128);
|
||||
replBufBlock *block2 = zmalloc(sizeof(replBufBlock) + 128);
|
||||
block1->size = 128;
|
||||
block1->used = 64;
|
||||
block2->size = 128;
|
||||
block2->used = 32;
|
||||
memset(block1->buf, 'A', 64);
|
||||
memset(block2->buf, 'B', 32);
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block1);
|
||||
listAddNodeTail(server.repl_buffer_blocks, block2);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 0;
|
||||
c->bufpos = 0;
|
||||
|
||||
writeToReplica(c);
|
||||
|
||||
TEST_ASSERT(c->nwritten == 96); /* 64 + 32 */
|
||||
TEST_ASSERT(fake_conn->written == 96);
|
||||
TEST_ASSERT(memcmp(fake_conn->buffer, block1->buf, 64) == 0);
|
||||
TEST_ASSERT(memcmp(fake_conn->buffer + 64, block2->buf, 32) == 0);
|
||||
TEST_ASSERT((c->write_flags & WRITE_FLAGS_WRITE_ERROR) == 0);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(fake_conn->buffer);
|
||||
zfree(fake_conn);
|
||||
zfree(block1);
|
||||
zfree(block2);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 3: Write error */
|
||||
{
|
||||
fakeConnection *fake_conn = connCreateFake();
|
||||
fake_conn->error = 1; /* Simulate write error */
|
||||
fake_conn->buffer = zmalloc(1024);
|
||||
fake_conn->buf_size = 1024;
|
||||
fake_conn->written = 0;
|
||||
c->conn = (connection *)fake_conn;
|
||||
|
||||
/* Create replication buffer block */
|
||||
replBufBlock *block = zmalloc(sizeof(replBufBlock) + 128);
|
||||
block->size = 128;
|
||||
block->used = 64;
|
||||
memset(block->buf, 'A', 64);
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
block->refcount = 1;
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 0;
|
||||
c->bufpos = 0;
|
||||
|
||||
writeToReplica(c);
|
||||
|
||||
TEST_ASSERT(c->nwritten <= 0);
|
||||
TEST_ASSERT((c->write_flags & WRITE_FLAGS_WRITE_ERROR) != 0);
|
||||
|
||||
/* Cleanup */
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
zfree(fake_conn->buffer);
|
||||
zfree(fake_conn);
|
||||
zfree(block);
|
||||
c->repl_data->ref_repl_buf_node = NULL;
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
listRelease(server.repl_buffer_blocks);
|
||||
listRelease(c->reply);
|
||||
freeClientReplicationData(c);
|
||||
zfree(c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_postWriteToReplica(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
client *c = zcalloc(sizeof(client));
|
||||
initClientReplicationData(c);
|
||||
server.repl_buffer_blocks = listCreate();
|
||||
c->reply = listCreate();
|
||||
|
||||
/* Test 1: No write case */
|
||||
{
|
||||
c->nwritten = 0;
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
postWriteToReplica(c);
|
||||
|
||||
TEST_ASSERT(server.stat_net_repl_output_bytes == 0);
|
||||
}
|
||||
|
||||
/* Test 2: Single block partial write */
|
||||
{
|
||||
replBufBlock *block = zmalloc(sizeof(replBufBlock) + 128);
|
||||
block->size = 128;
|
||||
block->used = 100;
|
||||
block->refcount = 1;
|
||||
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 20;
|
||||
c->nwritten = 30;
|
||||
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
postWriteToReplica(c);
|
||||
|
||||
TEST_ASSERT(server.stat_net_repl_output_bytes == 30);
|
||||
TEST_ASSERT(c->repl_data->ref_block_pos == 50); /* 20 + 30 */
|
||||
TEST_ASSERT(c->repl_data->ref_repl_buf_node == listFirst(server.repl_buffer_blocks));
|
||||
TEST_ASSERT(block->refcount == 1);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(block);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 3: Multiple blocks write */
|
||||
{
|
||||
replBufBlock *block1 = zmalloc(sizeof(replBufBlock) + 128);
|
||||
replBufBlock *block2 = zmalloc(sizeof(replBufBlock) + 128);
|
||||
block1->size = 128;
|
||||
block1->used = 64;
|
||||
block1->refcount = 1;
|
||||
block2->size = 128;
|
||||
block2->used = 100;
|
||||
block2->refcount = 0;
|
||||
|
||||
listAddNodeTail(server.repl_buffer_blocks, block1);
|
||||
listAddNodeTail(server.repl_buffer_blocks, block2);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 30;
|
||||
c->nwritten = 50;
|
||||
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
postWriteToReplica(c);
|
||||
|
||||
TEST_ASSERT(server.stat_net_repl_output_bytes == 50);
|
||||
TEST_ASSERT(c->repl_data->ref_block_pos == 16); /* (30 + 50) - 64 */
|
||||
TEST_ASSERT(c->repl_data->ref_repl_buf_node == listLast(server.repl_buffer_blocks));
|
||||
TEST_ASSERT(block1->refcount == 0);
|
||||
TEST_ASSERT(block2->refcount == 1);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(block1);
|
||||
zfree(block2);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 4: Write exactly to block boundary */
|
||||
{
|
||||
replBufBlock *block = zmalloc(sizeof(replBufBlock) + 128);
|
||||
block->size = 128;
|
||||
block->used = 64;
|
||||
block->refcount = 1;
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 30;
|
||||
c->nwritten = 34; /* Should reach exactly the end of block */
|
||||
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
postWriteToReplica(c);
|
||||
|
||||
TEST_ASSERT(server.stat_net_repl_output_bytes == 34);
|
||||
TEST_ASSERT(c->repl_data->ref_block_pos == 64);
|
||||
TEST_ASSERT(c->repl_data->ref_repl_buf_node == listFirst(server.repl_buffer_blocks));
|
||||
TEST_ASSERT(block->refcount == 1); /* we don't free the last block even if it's fully written */
|
||||
|
||||
/* Cleanup */
|
||||
zfree(block);
|
||||
c->repl_data->ref_repl_buf_node = NULL;
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
freeClientReplicationData(c);
|
||||
raxFree(server.repl_backlog->blocks_index);
|
||||
zfree(server.repl_backlog);
|
||||
listRelease(server.repl_buffer_blocks);
|
||||
listRelease(c->reply);
|
||||
zfree(c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_backupAndUpdateClientArgv(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
client *c = zmalloc(sizeof(client));
|
||||
|
||||
/* Test 1: Initial backup of arguments */
|
||||
c->argc = 2;
|
||||
robj **initial_argv = zmalloc(sizeof(robj *) * 2);
|
||||
c->argv = initial_argv;
|
||||
c->argv[0] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
c->argv[1] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test2"));
|
||||
c->original_argv = NULL;
|
||||
|
||||
backupAndUpdateClientArgv(c, 3, NULL);
|
||||
|
||||
TEST_ASSERT(c->argv != initial_argv);
|
||||
TEST_ASSERT(c->original_argv == initial_argv);
|
||||
TEST_ASSERT(c->original_argc == 2);
|
||||
TEST_ASSERT(c->argc == 3);
|
||||
TEST_ASSERT(c->argv_len == 3);
|
||||
TEST_ASSERT(c->argv[0]->refcount == 2);
|
||||
TEST_ASSERT(c->argv[1]->refcount == 2);
|
||||
TEST_ASSERT(c->argv[2] == NULL);
|
||||
|
||||
/* Test 2: Direct argv replacement */
|
||||
robj **new_argv = zmalloc(sizeof(robj *) * 2);
|
||||
new_argv[0] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
new_argv[1] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test2"));
|
||||
|
||||
backupAndUpdateClientArgv(c, 2, new_argv);
|
||||
|
||||
TEST_ASSERT(c->argv == new_argv);
|
||||
TEST_ASSERT(c->argc == 2);
|
||||
TEST_ASSERT(c->argv_len == 2);
|
||||
TEST_ASSERT(c->original_argv != c->argv);
|
||||
TEST_ASSERT(c->original_argv == initial_argv);
|
||||
TEST_ASSERT(c->original_argc == 2);
|
||||
TEST_ASSERT(c->original_argv[0]->refcount == 1);
|
||||
TEST_ASSERT(c->original_argv[1]->refcount == 1);
|
||||
|
||||
/* Test 3: Expanding argc */
|
||||
backupAndUpdateClientArgv(c, 4, NULL);
|
||||
|
||||
TEST_ASSERT(c->argc == 4);
|
||||
TEST_ASSERT(c->argv_len == 4);
|
||||
TEST_ASSERT(c->argv[0] != NULL);
|
||||
TEST_ASSERT(c->argv[1] != NULL);
|
||||
TEST_ASSERT(c->argv[2] == NULL);
|
||||
TEST_ASSERT(c->argv[3] == NULL);
|
||||
TEST_ASSERT(c->original_argv == initial_argv);
|
||||
|
||||
/* Cleanup */
|
||||
for (int i = 0; i < c->original_argc; i++) {
|
||||
decrRefCount(c->original_argv[i]);
|
||||
}
|
||||
zfree(c->original_argv);
|
||||
|
||||
for (int i = 0; i < c->argc; i++) {
|
||||
if (c->argv[i]) decrRefCount(c->argv[i]);
|
||||
}
|
||||
zfree(c->argv);
|
||||
zfree(c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_rewriteClientCommandArgument(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
client *c = zmalloc(sizeof(client));
|
||||
c->argc = 3;
|
||||
robj **initial_argv = zmalloc(sizeof(robj *) * 3);
|
||||
c->argv = initial_argv;
|
||||
c->original_argv = NULL;
|
||||
c->argv_len_sum = 0;
|
||||
|
||||
/* Initialize client with command "SET key value" */
|
||||
c->argv[0] = createStringObject("SET", 3);
|
||||
robj *original_key = createStringObject("key", 3);
|
||||
c->argv[1] = original_key;
|
||||
c->argv[2] = createStringObject("value", 5);
|
||||
c->argv_len_sum = 11; // 3 + 3 + 5
|
||||
|
||||
/* Test 1: Rewrite existing argument */
|
||||
robj *newval = createStringObject("newkey", 6);
|
||||
rewriteClientCommandArgument(c, 1, newval);
|
||||
|
||||
TEST_ASSERT(c->argv[1] == newval);
|
||||
TEST_ASSERT(c->argv[1]->refcount == 2);
|
||||
TEST_ASSERT(c->argv_len_sum == 14); // 3 + 6 + 5
|
||||
TEST_ASSERT(c->original_argv == initial_argv);
|
||||
TEST_ASSERT(c->original_argv[1] == original_key);
|
||||
TEST_ASSERT(c->original_argv[1]->refcount == 1);
|
||||
|
||||
/* Test 3: Extend argument vector */
|
||||
robj *extraval = createStringObject("extra", 5);
|
||||
rewriteClientCommandArgument(c, 3, extraval);
|
||||
|
||||
TEST_ASSERT(c->argc == 4);
|
||||
TEST_ASSERT(c->argv[3] == extraval);
|
||||
TEST_ASSERT(c->argv_len_sum == 19); // 3 + 6 + 5 + 5
|
||||
TEST_ASSERT(c->original_argv == initial_argv);
|
||||
|
||||
/* Cleanup */
|
||||
for (int i = 0; i < c->argc; i++) {
|
||||
if (c->argv[i]) decrRefCount(c->argv[i]);
|
||||
}
|
||||
zfree(c->argv);
|
||||
|
||||
for (int i = 0; i < c->original_argc; i++) {
|
||||
if (c->original_argv[i]) decrRefCount(c->original_argv[i]);
|
||||
}
|
||||
zfree(c->original_argv);
|
||||
|
||||
decrRefCount(newval);
|
||||
decrRefCount(extraval);
|
||||
|
||||
zfree(c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static client *createTestClient(void) {
|
||||
client *c = zcalloc(sizeof(client));
|
||||
|
||||
c->buf = zmalloc_usable(PROTO_REPLY_CHUNK_BYTES, &c->buf_usable_size);
|
||||
c->reply = listCreate();
|
||||
listSetFreeMethod(c->reply, freeClientReplyValue);
|
||||
listSetDupMethod(c->reply, dupClientReplyValue);
|
||||
/* dummy connection to bypass assert in closeClientOnOutputBufferLimitReached */
|
||||
c->conn = (connection *)c;
|
||||
c->deferred_reply_bytes = ULLONG_MAX;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
static void freeReplyOffloadClient(client *c) {
|
||||
listRelease(c->reply);
|
||||
zfree(c->buf);
|
||||
zfree(c);
|
||||
}
|
||||
|
||||
/* Each bulk offload puts 2 pointers to a reply buffer */
|
||||
#define PTRS_LEN (sizeof(void *) * 2)
|
||||
|
||||
int test_addRepliesWithOffloadsToBuffer(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
client *c = createTestClient();
|
||||
|
||||
/* Test 1: Add bulk offloads to the buffer */
|
||||
robj *obj = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
_addBulkStrRefToBufferOrList(c, obj);
|
||||
|
||||
TEST_ASSERT(obj->refcount == 2);
|
||||
TEST_ASSERT(c->bufpos == sizeof(payloadHeader) + PTRS_LEN);
|
||||
|
||||
payloadHeader *header1 = c->last_header;
|
||||
TEST_ASSERT(header1->payload_type == BULK_STR_REF);
|
||||
TEST_ASSERT(header1->payload_len == PTRS_LEN);
|
||||
|
||||
|
||||
robj *ptr;
|
||||
memcpy(&ptr, c->buf + sizeof(payloadHeader), sizeof(ptr));
|
||||
TEST_ASSERT(obj == ptr);
|
||||
|
||||
robj *obj2 = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test2"));
|
||||
_addBulkStrRefToBufferOrList(c, obj2);
|
||||
|
||||
/* 2 offloads expected in c->buf */
|
||||
TEST_ASSERT(c->bufpos == sizeof(payloadHeader) + 2 * PTRS_LEN);
|
||||
TEST_ASSERT(header1->payload_type == BULK_STR_REF);
|
||||
TEST_ASSERT(header1->payload_len == 2 * PTRS_LEN);
|
||||
|
||||
memcpy(&ptr, c->buf + sizeof(payloadHeader) + PTRS_LEN, sizeof(ptr));
|
||||
TEST_ASSERT(obj2 == ptr);
|
||||
|
||||
/* Test 2: Add plain reply to the buffer */
|
||||
const char *plain = "+OK\r\n";
|
||||
size_t plain_len = strlen(plain);
|
||||
_addReplyToBufferOrList(c, plain, plain_len);
|
||||
|
||||
/* 2 offloads and plain reply expected in c->buf. So 2 headers expected as well */
|
||||
TEST_ASSERT(c->bufpos == 2 * sizeof(payloadHeader) + 2 * PTRS_LEN + plain_len);
|
||||
TEST_ASSERT(header1->payload_type == BULK_STR_REF);
|
||||
TEST_ASSERT(header1->payload_len == 2 * PTRS_LEN);
|
||||
payloadHeader *header2 = c->last_header;
|
||||
TEST_ASSERT(header2->payload_type == PLAIN_REPLY);
|
||||
TEST_ASSERT(header2->payload_len == plain_len);
|
||||
|
||||
/* Add more plain replies. Check same plain reply header updated properly */
|
||||
for (int i = 0; i < 9; ++i) _addReplyToBufferOrList(c, plain, plain_len);
|
||||
TEST_ASSERT(c->bufpos == 2 * sizeof(payloadHeader) + 2 * PTRS_LEN + 10 * plain_len);
|
||||
TEST_ASSERT(header2->payload_type == PLAIN_REPLY);
|
||||
TEST_ASSERT(header2->payload_len == plain_len * 10);
|
||||
|
||||
/* Test 3: Add one more bulk offload to the buffer */
|
||||
_addBulkStrRefToBufferOrList(c, obj);
|
||||
TEST_ASSERT(obj->refcount == 3);
|
||||
TEST_ASSERT(c->bufpos == 3 * sizeof(payloadHeader) + 3 * PTRS_LEN + 10 * plain_len);
|
||||
payloadHeader *header3 = c->last_header;
|
||||
TEST_ASSERT(header3->payload_type == BULK_STR_REF);
|
||||
memcpy(&ptr, (char *)c->last_header + sizeof(payloadHeader), sizeof(ptr));
|
||||
TEST_ASSERT(obj == ptr);
|
||||
|
||||
releaseReplyReferences(c);
|
||||
decrRefCount(obj);
|
||||
decrRefCount(obj2);
|
||||
|
||||
freeReplyOffloadClient(c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_addRepliesWithOffloadsToList(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* Required for isCopyAvoidPreferred / isCopyAvoidIndicatedByIOThreads */
|
||||
int io_threads_num = server.io_threads_num;
|
||||
int min_io_threads_for_copy_avoid = server.min_io_threads_copy_avoid;
|
||||
server.io_threads_num = 1;
|
||||
server.min_io_threads_copy_avoid = 1;
|
||||
|
||||
client *c = createTestClient();
|
||||
|
||||
// Mock ACL
|
||||
user u;
|
||||
DefaultUser = &u;
|
||||
DefaultUser->flags = USER_FLAG_NOPASS;
|
||||
|
||||
/* Test 1: Add bulk offloads to the reply list */
|
||||
|
||||
/* Select reply length so that there is place for 2 headers and 4 bytes only
|
||||
* 4 bytes is not enough for object pointer(s)
|
||||
* This will force bulk offload to be added to reply list
|
||||
*/
|
||||
size_t reply_len = c->buf_usable_size - 2 * sizeof(payloadHeader) - 4;
|
||||
char *reply = zmalloc(reply_len);
|
||||
memset(reply, 'a', reply_len);
|
||||
_addReplyToBufferOrList(c, reply, reply_len);
|
||||
TEST_ASSERT(c->flag.buf_encoded);
|
||||
TEST_ASSERT(c->bufpos == sizeof(payloadHeader) + reply_len);
|
||||
TEST_ASSERT(listLength(c->reply) == 0);
|
||||
|
||||
/* As bulk offload header+pointer can't be accommodated in c->buf
|
||||
* then one block is expected in c->reply */
|
||||
robj *obj = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
_addBulkStrRefToBufferOrList(c, obj);
|
||||
TEST_ASSERT(obj->refcount == 2);
|
||||
TEST_ASSERT(c->bufpos == sizeof(payloadHeader) + reply_len);
|
||||
TEST_ASSERT(listLength(c->reply) == 1);
|
||||
|
||||
/* Check bulk offload header+pointer inside c->reply */
|
||||
listIter iter;
|
||||
listRewind(c->reply, &iter);
|
||||
listNode *next = listNext(&iter);
|
||||
clientReplyBlock *blk = listNodeValue(next);
|
||||
|
||||
TEST_ASSERT(blk->used == sizeof(payloadHeader) + PTRS_LEN);
|
||||
payloadHeader *header1 = blk->last_header;
|
||||
TEST_ASSERT(header1->payload_type == BULK_STR_REF);
|
||||
TEST_ASSERT(header1->payload_len == PTRS_LEN);
|
||||
|
||||
robj *ptr;
|
||||
memcpy(&ptr, blk->buf + sizeof(payloadHeader), sizeof(ptr));
|
||||
TEST_ASSERT(obj == ptr);
|
||||
|
||||
/* Test 2: Add one more bulk offload to the reply list */
|
||||
_addBulkStrRefToBufferOrList(c, obj);
|
||||
TEST_ASSERT(obj->refcount == 3);
|
||||
TEST_ASSERT(listLength(c->reply) == 1);
|
||||
TEST_ASSERT(blk->used == sizeof(payloadHeader) + 2 * PTRS_LEN);
|
||||
TEST_ASSERT(header1->payload_type == BULK_STR_REF);
|
||||
TEST_ASSERT(header1->payload_len == 2 * PTRS_LEN);
|
||||
|
||||
/* Test 3: Add plain replies to cause reply list grow */
|
||||
while (reply_len < blk->size - blk->used) _addReplyToBufferOrList(c, reply, reply_len);
|
||||
_addReplyToBufferOrList(c, reply, reply_len);
|
||||
|
||||
TEST_ASSERT(listLength(c->reply) == 2);
|
||||
/* last header in 1st block */
|
||||
payloadHeader *header2 = blk->last_header;
|
||||
listRewind(c->reply, &iter);
|
||||
listNext(&iter);
|
||||
next = listNext(&iter);
|
||||
clientReplyBlock *blk2 = listNodeValue(next);
|
||||
/* last header in 2nd block */
|
||||
payloadHeader *header3 = blk2->last_header;
|
||||
TEST_ASSERT(header2->payload_type == PLAIN_REPLY && header3->payload_type == PLAIN_REPLY);
|
||||
TEST_ASSERT((header2->payload_len + header3->payload_len) % reply_len == 0);
|
||||
|
||||
releaseReplyReferences(c);
|
||||
decrRefCount(obj);
|
||||
|
||||
zfree(reply);
|
||||
|
||||
freeReplyOffloadClient(c);
|
||||
|
||||
/* Restore modified values */
|
||||
server.io_threads_num = io_threads_num;
|
||||
server.min_io_threads_copy_avoid = min_io_threads_for_copy_avoid;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_addBufferToReplyIOV(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
const char *expected_reply = "$5\r\nhello\r\n";
|
||||
ssize_t total_len = strlen(expected_reply);
|
||||
const int iovmax = 16;
|
||||
char crlf[2] = {'\r', '\n'};
|
||||
|
||||
/* Test 1: 1st writevToclient invocation */
|
||||
client *c = createTestClient();
|
||||
robj *obj = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "hello"));
|
||||
_addBulkStrRefToBufferOrList(c, obj);
|
||||
|
||||
struct iovec iov_arr[iovmax];
|
||||
char prefixes[iovmax / 3 + 1][LONG_STR_SIZE + 3];
|
||||
bufWriteMetadata metadata[1];
|
||||
|
||||
replyIOV reply;
|
||||
initReplyIOV(c, iovmax, iov_arr, prefixes, crlf, &reply);
|
||||
addBufferToReplyIOV(c->flag.buf_encoded, c->buf, c->bufpos, &reply, &metadata[0]);
|
||||
|
||||
TEST_ASSERT(reply.iov_len_total == total_len);
|
||||
TEST_ASSERT(reply.iovcnt == 3);
|
||||
const char *ptr = expected_reply;
|
||||
for (int i = 0; i < reply.iovcnt; ++i) {
|
||||
TEST_ASSERT(memcmp(ptr, reply.iov[i].iov_base, reply.iov[i].iov_len) == 0);
|
||||
ptr += reply.iov[i].iov_len;
|
||||
}
|
||||
|
||||
/* Test 2: Last written buf/pos/data_len after 1st invocation */
|
||||
saveLastWrittenBuf(c, metadata, 1, reply.iov_len_total, 1); /* only 1 byte has been written */
|
||||
TEST_ASSERT(c->io_last_written.buf == c->buf);
|
||||
TEST_ASSERT(c->io_last_written.bufpos == 0); /* incomplete write */
|
||||
TEST_ASSERT(c->io_last_written.data_len == 1);
|
||||
|
||||
/* Test 3: 2nd writevToclient invocation */
|
||||
struct iovec iov_arr2[iovmax];
|
||||
char prefixes2[iovmax / 3 + 1][LONG_STR_SIZE + 3];
|
||||
bufWriteMetadata metadata2[1];
|
||||
|
||||
replyIOV reply2;
|
||||
initReplyIOV(c, iovmax, iov_arr2, prefixes2, crlf, &reply2);
|
||||
addBufferToReplyIOV(c->flag.buf_encoded, c->buf, c->bufpos, &reply2, &metadata2[0]);
|
||||
TEST_ASSERT(reply2.iov_len_total == total_len - 1);
|
||||
TEST_ASSERT((*(char *)reply2.iov[0].iov_base) == '5');
|
||||
|
||||
/* Test 4: Last written buf/pos/data_len after 2nd invocation */
|
||||
saveLastWrittenBuf(c, metadata2, 1, reply2.iov_len_total, 4); /* 4 more bytes has been written */
|
||||
TEST_ASSERT(c->io_last_written.buf == c->buf);
|
||||
TEST_ASSERT(c->io_last_written.bufpos == 0); /* incomplete write */
|
||||
TEST_ASSERT(c->io_last_written.data_len == 5); /* 1 + 4 */
|
||||
|
||||
/* Test 5: 3rd writevToclient invocation */
|
||||
struct iovec iov_arr3[iovmax];
|
||||
char prefixes3[iovmax / 3 + 1][LONG_STR_SIZE + 3];
|
||||
bufWriteMetadata metadata3[1];
|
||||
|
||||
replyIOV reply3;
|
||||
initReplyIOV(c, iovmax, iov_arr3, prefixes3, crlf, &reply3);
|
||||
addBufferToReplyIOV(c->flag.buf_encoded, c->buf, c->bufpos, &reply3, &metadata3[0]);
|
||||
TEST_ASSERT(reply3.iov_len_total == total_len - 5);
|
||||
TEST_ASSERT((*(char *)reply3.iov[0].iov_base) == 'e');
|
||||
|
||||
/* Test 6: Last written buf/pos/data_len after 3rd invocation */
|
||||
saveLastWrittenBuf(c, metadata3, 1, reply3.iov_len_total, reply3.iov_len_total); /* everything has been written */
|
||||
TEST_ASSERT(c->io_last_written.buf == c->buf);
|
||||
TEST_ASSERT(c->io_last_written.bufpos == c->bufpos);
|
||||
TEST_ASSERT(c->io_last_written.data_len == (size_t)total_len);
|
||||
|
||||
decrRefCount(obj);
|
||||
decrRefCount(obj);
|
||||
|
||||
freeReplyOffloadClient(c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,769 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "server.h"
|
||||
|
||||
/* Internal types from networking.c */
|
||||
typedef enum {
|
||||
PLAIN_REPLY,
|
||||
BULK_STR_REF,
|
||||
} payloadType;
|
||||
|
||||
typedef struct bulkStrRef {
|
||||
robj *obj;
|
||||
sds str;
|
||||
} bulkStrRef;
|
||||
|
||||
typedef struct __attribute__((__packed__)) payloadHeader {
|
||||
size_t payload_len;
|
||||
size_t reply_len;
|
||||
int16_t slot;
|
||||
uint8_t payload_type : 1;
|
||||
uint8_t track_bytes : 1;
|
||||
uint8_t reserved : 6;
|
||||
} payloadHeader;
|
||||
|
||||
typedef struct bufWriteMetadata {
|
||||
char *buf;
|
||||
size_t bufpos;
|
||||
uint64_t data_len;
|
||||
int complete;
|
||||
} bufWriteMetadata;
|
||||
|
||||
#define BULK_STR_LEN_PREFIX_MAX_SIZE 24
|
||||
|
||||
typedef struct replyIOV {
|
||||
int iovcnt;
|
||||
int iovsize;
|
||||
struct iovec *iov;
|
||||
ssize_t iov_len_total;
|
||||
size_t last_written_len;
|
||||
int limit_reached;
|
||||
int prfxcnt;
|
||||
char (*prefixes)[BULK_STR_LEN_PREFIX_MAX_SIZE];
|
||||
char *crlf;
|
||||
} replyIOV;
|
||||
|
||||
/* Forward declarations for helper functions from networking.c */
|
||||
void _addReplyToBufferOrList(client *c, const char *s, size_t len);
|
||||
int inMainThread(void);
|
||||
int clusterSlotStatsEnabled(int slot);
|
||||
|
||||
/* Wrapper functions for static functions in networking.c */
|
||||
void testOnlyPostWriteToReplica(client *c);
|
||||
void testOnlyWriteToReplica(client *c);
|
||||
void testOnlyBackupAndUpdateClientArgv(client *c, int new_argc, robj **new_argv);
|
||||
size_t testOnlyUpsertPayloadHeader(char *buf, size_t *bufpos, payloadHeader **last_header, uint8_t type, size_t len, int slot, size_t available);
|
||||
int testOnlyIsCopyAvoidPreferred(client *c, robj *obj);
|
||||
size_t testOnlyAddReplyPayloadToBuffer(client *c, const void *payload, size_t len, uint8_t payload_type);
|
||||
size_t testOnlyAddBulkStrRefToBuffer(client *c, const void *payload, size_t len);
|
||||
void testOnlyAddReplyPayloadToList(client *c, list *reply_list, const char *payload, size_t len, uint8_t payload_type);
|
||||
void testOnlyAddBulkStrRefToToList(client *c, const void *payload, size_t len);
|
||||
void testOnlyAddBulkStrRefToBufferOrList(client *c, robj *obj);
|
||||
void testOnlyInitReplyIOV(client *c, int iovsize, struct iovec *iov_arr, char (*prefixes)[BULK_STR_LEN_PREFIX_MAX_SIZE], char *crlf, replyIOV *reply);
|
||||
void testOnlyAddPlainBufferToReplyIOV(char *buf, size_t buf_len, replyIOV *reply, bufWriteMetadata *metadata);
|
||||
void testOnlyAddBulkStringToReplyIOV(char *buf, size_t buf_len, replyIOV *reply, bufWriteMetadata *metadata);
|
||||
void testOnlyAddEncodedBufferToReplyIOV(char *buf, size_t bufpos, replyIOV *reply, bufWriteMetadata *metadata);
|
||||
void testOnlyAddBufferToReplyIOV(int encoded, char *buf, size_t bufpos, replyIOV *reply, bufWriteMetadata *metadata);
|
||||
void testOnlySaveLastWrittenBuf(client *c, bufWriteMetadata *metadata, int bufcnt, size_t totlen, size_t totwritten);
|
||||
}
|
||||
|
||||
/* Fake structures and functions */
|
||||
typedef struct fakeConnection {
|
||||
connection conn;
|
||||
int error;
|
||||
char *buffer;
|
||||
size_t buf_size;
|
||||
size_t written;
|
||||
} fakeConnection;
|
||||
|
||||
/* Fake connWrite function */
|
||||
static int fake_connWrite(connection *conn, const void *data, size_t size) {
|
||||
fakeConnection *fake_conn = (fakeConnection *)conn;
|
||||
if (fake_conn->error) return -1;
|
||||
|
||||
size_t to_write = size;
|
||||
if (fake_conn->written + to_write > fake_conn->buf_size) {
|
||||
to_write = fake_conn->buf_size - fake_conn->written;
|
||||
}
|
||||
|
||||
memcpy(fake_conn->buffer + fake_conn->written, data, to_write);
|
||||
fake_conn->written += to_write;
|
||||
return (int)to_write;
|
||||
}
|
||||
|
||||
/* Fake connWritev function */
|
||||
static int fake_connWritev(connection *conn, const struct iovec *iov, int iovcnt) {
|
||||
fakeConnection *fake_conn = (fakeConnection *)conn;
|
||||
if (fake_conn->error) return -1;
|
||||
|
||||
size_t total = 0;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
size_t to_write = iov[i].iov_len;
|
||||
if (fake_conn->written + to_write > fake_conn->buf_size) {
|
||||
to_write = fake_conn->buf_size - fake_conn->written;
|
||||
}
|
||||
if (to_write == 0) break;
|
||||
|
||||
memcpy(fake_conn->buffer + fake_conn->written, iov[i].iov_base, to_write);
|
||||
fake_conn->written += to_write;
|
||||
total += to_write;
|
||||
}
|
||||
return (int)total;
|
||||
}
|
||||
|
||||
/* Fake connection type - initialized in SetUpTestSuite */
|
||||
static ConnectionType CT_Fake;
|
||||
|
||||
static fakeConnection *connCreateFake(void) {
|
||||
fakeConnection *conn = (fakeConnection *)(zcalloc(sizeof(fakeConnection)));
|
||||
conn->conn.type = &CT_Fake;
|
||||
conn->conn.fd = -1;
|
||||
conn->conn.iovcnt = IOV_MAX;
|
||||
return conn;
|
||||
}
|
||||
|
||||
/* Test fixture for networking tests - minimal fixture with no setup/teardown */
|
||||
class NetworkingTest : public ::testing::Test {
|
||||
protected:
|
||||
static void SetUpTestSuite() {
|
||||
/* Initialize CT_Fake explicitly by field name to avoid dependency
|
||||
* on field order (designated initializers require C++20). */
|
||||
memset(&CT_Fake, 0, sizeof(CT_Fake));
|
||||
CT_Fake.write = fake_connWrite;
|
||||
CT_Fake.writev = fake_connWritev;
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
/* Initialize server fields that are accessed by networking functions */
|
||||
server.commandlog[COMMANDLOG_TYPE_LARGE_REPLY].threshold = -1; /* Disable tracking */
|
||||
server.debug_client_enforce_reply_list = 0;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(NetworkingTest, TestWriteToReplica) {
|
||||
client *c = (client *)(zcalloc(sizeof(client)));
|
||||
initClientReplicationData(c);
|
||||
server.repl_buffer_blocks = listCreate();
|
||||
/* Ensure replicas list exists before creating backlog */
|
||||
if (!server.replicas) {
|
||||
server.replicas = listCreate();
|
||||
}
|
||||
createReplicationBacklog();
|
||||
c->reply = listCreate();
|
||||
/* Test 1: Single block write */
|
||||
{
|
||||
fakeConnection *fake_conn = connCreateFake();
|
||||
fake_conn->buffer = (char *)zmalloc(1024);
|
||||
fake_conn->buf_size = 1024;
|
||||
c->conn = (connection *)fake_conn;
|
||||
|
||||
/* Create replication buffer block */
|
||||
replBufBlock *block = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
block->size = 128;
|
||||
block->used = 64;
|
||||
block->refcount = 1; /* Initialize refcount - client will reference it */
|
||||
memset(block->buf, 'A', 64);
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 0;
|
||||
c->bufpos = 0;
|
||||
|
||||
testOnlyWriteToReplica(c);
|
||||
|
||||
ASSERT_EQ(c->nwritten, 64);
|
||||
ASSERT_EQ(fake_conn->written, 64u);
|
||||
ASSERT_EQ(memcmp(fake_conn->buffer, block->buf, 64), 0);
|
||||
ASSERT_EQ((c->write_flags & WRITE_FLAGS_WRITE_ERROR), 0);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(fake_conn->buffer);
|
||||
zfree(fake_conn);
|
||||
zfree(block);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 2: Multiple blocks write */
|
||||
{
|
||||
fakeConnection *fake_conn = connCreateFake();
|
||||
fake_conn->error = 0;
|
||||
fake_conn->written = 0;
|
||||
fake_conn->buffer = (char *)zmalloc(1024);
|
||||
fake_conn->buf_size = 1024;
|
||||
c->conn = (connection *)fake_conn;
|
||||
|
||||
/* Create multiple replication buffer blocks */
|
||||
replBufBlock *block1 = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
replBufBlock *block2 = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
block1->size = 128;
|
||||
block1->used = 64;
|
||||
block1->refcount = 1; /* Initialize refcount */
|
||||
block2->size = 128;
|
||||
block2->used = 32;
|
||||
block2->refcount = 0; /* Not referenced by client initially */
|
||||
memset(block1->buf, 'A', 64);
|
||||
memset(block2->buf, 'B', 32);
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block1);
|
||||
listAddNodeTail(server.repl_buffer_blocks, block2);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 0;
|
||||
c->bufpos = 0;
|
||||
|
||||
testOnlyWriteToReplica(c);
|
||||
|
||||
ASSERT_EQ(c->nwritten, 96); /* 64 + 32 */
|
||||
ASSERT_EQ(fake_conn->written, 96u);
|
||||
ASSERT_EQ(memcmp(fake_conn->buffer, block1->buf, 64), 0);
|
||||
ASSERT_EQ(memcmp(fake_conn->buffer + 64, block2->buf, 32), 0);
|
||||
ASSERT_EQ((c->write_flags & WRITE_FLAGS_WRITE_ERROR), 0);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(fake_conn->buffer);
|
||||
zfree(fake_conn);
|
||||
zfree(block1);
|
||||
zfree(block2);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 3: Write error */
|
||||
{
|
||||
fakeConnection *fake_conn = connCreateFake();
|
||||
fake_conn->error = 1; /* Simulate write error */
|
||||
fake_conn->buffer = (char *)zmalloc(1024);
|
||||
fake_conn->buf_size = 1024;
|
||||
fake_conn->written = 0;
|
||||
c->conn = (connection *)fake_conn;
|
||||
|
||||
/* Create replication buffer block */
|
||||
replBufBlock *block = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
block->size = 128;
|
||||
block->used = 64;
|
||||
memset(block->buf, 'A', 64);
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
block->refcount = 1;
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 0;
|
||||
c->bufpos = 0;
|
||||
|
||||
testOnlyWriteToReplica(c);
|
||||
|
||||
ASSERT_LE(c->nwritten, 0);
|
||||
ASSERT_NE((c->write_flags & WRITE_FLAGS_WRITE_ERROR), 0);
|
||||
|
||||
/* Cleanup */
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
zfree(fake_conn->buffer);
|
||||
zfree(fake_conn);
|
||||
zfree(block);
|
||||
c->repl_data->ref_repl_buf_node = nullptr;
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
listRelease(server.repl_buffer_blocks);
|
||||
listRelease(c->reply);
|
||||
freeClientReplicationData(c);
|
||||
zfree(c);
|
||||
|
||||
/* Clean up replication backlog */
|
||||
if (server.repl_backlog) {
|
||||
if (server.repl_backlog->ref_repl_buf_node) {
|
||||
server.repl_backlog->ref_repl_buf_node = NULL;
|
||||
}
|
||||
raxFree(server.repl_backlog->blocks_index);
|
||||
zfree(server.repl_backlog);
|
||||
server.repl_backlog = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(NetworkingTest, TestPostWriteToReplica) {
|
||||
client *c = (client *)(zcalloc(sizeof(client)));
|
||||
initClientReplicationData(c);
|
||||
server.repl_buffer_blocks = listCreate();
|
||||
/* Ensure replicas list exists before creating backlog */
|
||||
if (!server.replicas) {
|
||||
server.replicas = listCreate();
|
||||
}
|
||||
createReplicationBacklog();
|
||||
c->reply = listCreate();
|
||||
/* Test 1: No write case */
|
||||
{
|
||||
c->nwritten = 0;
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
testOnlyPostWriteToReplica(c);
|
||||
|
||||
ASSERT_EQ(server.stat_net_repl_output_bytes, 0);
|
||||
}
|
||||
|
||||
/* Test 2: Single block partial write */
|
||||
{
|
||||
replBufBlock *block = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
block->size = 128;
|
||||
block->used = 100;
|
||||
block->refcount = 1;
|
||||
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 20;
|
||||
c->nwritten = 30;
|
||||
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
testOnlyPostWriteToReplica(c);
|
||||
|
||||
ASSERT_EQ(server.stat_net_repl_output_bytes, 30);
|
||||
ASSERT_EQ(c->repl_data->ref_block_pos, 50u); /* 20 + 30 */
|
||||
ASSERT_EQ(c->repl_data->ref_repl_buf_node, listFirst(server.repl_buffer_blocks));
|
||||
ASSERT_EQ(block->refcount, 1);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(block);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 3: Multiple blocks write */
|
||||
{
|
||||
replBufBlock *block1 = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
replBufBlock *block2 = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
block1->size = 128;
|
||||
block1->used = 64;
|
||||
block1->refcount = 1;
|
||||
block2->size = 128;
|
||||
block2->used = 100;
|
||||
block2->refcount = 0;
|
||||
|
||||
listAddNodeTail(server.repl_buffer_blocks, block1);
|
||||
listAddNodeTail(server.repl_buffer_blocks, block2);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 30;
|
||||
c->nwritten = 50;
|
||||
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
testOnlyPostWriteToReplica(c);
|
||||
|
||||
ASSERT_EQ(server.stat_net_repl_output_bytes, 50);
|
||||
ASSERT_EQ(c->repl_data->ref_block_pos, 16u); /* (30 + 50) - 64 */
|
||||
ASSERT_EQ(c->repl_data->ref_repl_buf_node, listLast(server.repl_buffer_blocks));
|
||||
ASSERT_EQ(block1->refcount, 0);
|
||||
ASSERT_EQ(block2->refcount, 1);
|
||||
|
||||
/* Cleanup */
|
||||
zfree(block1);
|
||||
zfree(block2);
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Test 4: Write exactly to block boundary */
|
||||
{
|
||||
replBufBlock *block = (replBufBlock *)(zmalloc(sizeof(replBufBlock) + 128));
|
||||
block->size = 128;
|
||||
block->used = 64;
|
||||
block->refcount = 1;
|
||||
|
||||
/* Setup client state */
|
||||
listAddNodeTail(server.repl_buffer_blocks, block);
|
||||
c->repl_data->ref_repl_buf_node = listFirst(server.repl_buffer_blocks);
|
||||
c->repl_data->ref_block_pos = 30;
|
||||
c->nwritten = 34; /* Should reach exactly the end of block */
|
||||
|
||||
server.stat_net_repl_output_bytes = 0;
|
||||
|
||||
testOnlyPostWriteToReplica(c);
|
||||
|
||||
ASSERT_EQ(server.stat_net_repl_output_bytes, 34);
|
||||
ASSERT_EQ(c->repl_data->ref_block_pos, 64u);
|
||||
ASSERT_EQ(c->repl_data->ref_repl_buf_node, listFirst(server.repl_buffer_blocks));
|
||||
ASSERT_EQ(block->refcount, 1); /* we don't free the last block even if it's fully written */
|
||||
|
||||
/* Cleanup */
|
||||
zfree(block);
|
||||
c->repl_data->ref_repl_buf_node = nullptr;
|
||||
listEmpty(server.repl_buffer_blocks);
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
freeClientReplicationData(c);
|
||||
raxFree(server.repl_backlog->blocks_index);
|
||||
zfree(server.repl_backlog);
|
||||
listRelease(server.repl_buffer_blocks);
|
||||
listRelease(c->reply);
|
||||
zfree(c);
|
||||
}
|
||||
|
||||
TEST_F(NetworkingTest, TestBackupAndUpdateClientArgv) {
|
||||
client *c = (client *)(zmalloc(sizeof(client)));
|
||||
/* Test 1: Initial backup of arguments */
|
||||
c->argc = 2;
|
||||
robj **initial_argv = (robj **)(zmalloc(sizeof(robj *) * 2));
|
||||
c->argv = initial_argv;
|
||||
c->argv[0] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
c->argv[1] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test2"));
|
||||
c->original_argv = nullptr;
|
||||
|
||||
testOnlyBackupAndUpdateClientArgv(c, 3, nullptr);
|
||||
|
||||
ASSERT_NE(c->argv, initial_argv);
|
||||
ASSERT_EQ(c->original_argv, initial_argv);
|
||||
ASSERT_EQ(c->original_argc, 2);
|
||||
ASSERT_EQ(c->argc, 3);
|
||||
ASSERT_EQ(c->argv_len, 3);
|
||||
ASSERT_EQ(c->argv[0]->refcount, 2u);
|
||||
ASSERT_EQ(c->argv[1]->refcount, 2u);
|
||||
ASSERT_EQ(c->argv[2], nullptr);
|
||||
|
||||
/* Test 2: Direct argv replacement */
|
||||
robj **new_argv = (robj **)(zmalloc(sizeof(robj *) * 2));
|
||||
new_argv[0] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
new_argv[1] = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test2"));
|
||||
|
||||
testOnlyBackupAndUpdateClientArgv(c, 2, new_argv);
|
||||
|
||||
ASSERT_EQ(c->argv, new_argv);
|
||||
ASSERT_EQ(c->argc, 2);
|
||||
ASSERT_EQ(c->argv_len, 2);
|
||||
ASSERT_NE(c->original_argv, c->argv);
|
||||
ASSERT_EQ(c->original_argv, initial_argv);
|
||||
ASSERT_EQ(c->original_argc, 2);
|
||||
ASSERT_EQ(c->original_argv[0]->refcount, 1u);
|
||||
ASSERT_EQ(c->original_argv[1]->refcount, 1u);
|
||||
|
||||
/* Test 3: Expanding argc */
|
||||
testOnlyBackupAndUpdateClientArgv(c, 4, nullptr);
|
||||
|
||||
ASSERT_EQ(c->argc, 4);
|
||||
ASSERT_EQ(c->argv_len, 4);
|
||||
ASSERT_NE(c->argv[0], nullptr);
|
||||
ASSERT_NE(c->argv[1], nullptr);
|
||||
ASSERT_EQ(c->argv[2], nullptr);
|
||||
ASSERT_EQ(c->argv[3], nullptr);
|
||||
ASSERT_EQ(c->original_argv, initial_argv);
|
||||
|
||||
/* Cleanup */
|
||||
for (int i = 0; i < c->original_argc; i++) {
|
||||
decrRefCount(c->original_argv[i]);
|
||||
}
|
||||
zfree(c->original_argv);
|
||||
|
||||
for (int i = 0; i < c->argc; i++) {
|
||||
if (c->argv[i]) decrRefCount(c->argv[i]);
|
||||
}
|
||||
zfree(c->argv);
|
||||
zfree(c);
|
||||
}
|
||||
|
||||
TEST_F(NetworkingTest, TestRewriteClientCommandArgument) {
|
||||
client *c = (client *)(zmalloc(sizeof(client)));
|
||||
c->argc = 3;
|
||||
robj **initial_argv = (robj **)(zmalloc(sizeof(robj *) * 3));
|
||||
c->argv = initial_argv;
|
||||
c->original_argv = nullptr;
|
||||
c->argv_len_sum = 0;
|
||||
|
||||
/* Initialize client with command "SET key value" */
|
||||
c->argv[0] = createStringObject("SET", 3);
|
||||
robj *original_key = createStringObject("key", 3);
|
||||
c->argv[1] = original_key;
|
||||
c->argv[2] = createStringObject("value", 5);
|
||||
c->argv_len_sum = 11; // 3 + 3 + 5
|
||||
|
||||
/* Test 1: Rewrite existing argument */
|
||||
robj *newval = createStringObject("newkey", 6);
|
||||
rewriteClientCommandArgument(c, 1, newval);
|
||||
|
||||
ASSERT_EQ(c->argv[1], newval);
|
||||
ASSERT_EQ(c->argv[1]->refcount, 2u);
|
||||
ASSERT_EQ(c->argv_len_sum, 14u); // 3 + 6 + 5
|
||||
ASSERT_EQ(c->original_argv, initial_argv);
|
||||
ASSERT_EQ(c->original_argv[1], original_key);
|
||||
ASSERT_EQ(c->original_argv[1]->refcount, 1u);
|
||||
|
||||
/* Test 2: Extend argument vector */
|
||||
robj *extraval = createStringObject("extra", 5);
|
||||
rewriteClientCommandArgument(c, 3, extraval);
|
||||
|
||||
ASSERT_EQ(c->argc, 4);
|
||||
ASSERT_EQ(c->argv[3], extraval);
|
||||
ASSERT_EQ(c->argv_len_sum, 19u); // 3 + 6 + 5 + 5
|
||||
ASSERT_EQ(c->original_argv, initial_argv);
|
||||
|
||||
/* Cleanup */
|
||||
for (int i = 0; i < c->argc; i++) {
|
||||
if (c->argv[i]) decrRefCount(c->argv[i]);
|
||||
}
|
||||
zfree(c->argv);
|
||||
|
||||
for (int i = 0; i < c->original_argc; i++) {
|
||||
if (c->original_argv[i]) decrRefCount(c->original_argv[i]);
|
||||
}
|
||||
zfree(c->original_argv);
|
||||
|
||||
decrRefCount(newval);
|
||||
decrRefCount(extraval);
|
||||
|
||||
zfree(c);
|
||||
}
|
||||
|
||||
/* Helper function to create test client */
|
||||
static client *createTestClient(void) {
|
||||
client *c = (client *)(zcalloc(sizeof(client)));
|
||||
|
||||
c->buf = (char *)zmalloc_usable(PROTO_REPLY_CHUNK_BYTES, &c->buf_usable_size);
|
||||
c->reply = listCreate();
|
||||
listSetFreeMethod(c->reply, freeClientReplyValue);
|
||||
listSetDupMethod(c->reply, dupClientReplyValue);
|
||||
/* dummy connection to bypass assert in closeClientOnOutputBufferLimitReached */
|
||||
c->conn = (connection *)c;
|
||||
c->deferred_reply_bytes = ULLONG_MAX;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
static void freeReplyOffloadClient(client *c) {
|
||||
listRelease(c->reply);
|
||||
zfree(c->buf);
|
||||
zfree(c);
|
||||
}
|
||||
|
||||
/* Each bulk offload puts 2 pointers to a reply buffer */
|
||||
#define PTRS_LEN (sizeof(void *) * 2)
|
||||
|
||||
TEST_F(NetworkingTest, TestAddRepliesWithOffloadsToBuffer) {
|
||||
client *c = createTestClient();
|
||||
/* Test 1: Add bulk offloads to the buffer */
|
||||
robj *obj = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
testOnlyAddBulkStrRefToBufferOrList(c, obj);
|
||||
|
||||
ASSERT_EQ(obj->refcount, 2u);
|
||||
ASSERT_EQ(c->bufpos, sizeof(payloadHeader) + PTRS_LEN);
|
||||
|
||||
payloadHeader *header1 = c->last_header;
|
||||
ASSERT_EQ(header1->payload_type, BULK_STR_REF);
|
||||
ASSERT_EQ(header1->payload_len, PTRS_LEN);
|
||||
|
||||
robj *ptr;
|
||||
memcpy(&ptr, c->buf + sizeof(payloadHeader), sizeof(ptr));
|
||||
ASSERT_EQ(obj, ptr);
|
||||
|
||||
robj *obj2 = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test2"));
|
||||
testOnlyAddBulkStrRefToBufferOrList(c, obj2);
|
||||
|
||||
/* 2 offloads expected in c->buf */
|
||||
ASSERT_EQ(c->bufpos, sizeof(payloadHeader) + 2 * PTRS_LEN);
|
||||
ASSERT_EQ(header1->payload_type, BULK_STR_REF);
|
||||
ASSERT_EQ(header1->payload_len, 2 * PTRS_LEN);
|
||||
|
||||
memcpy(&ptr, c->buf + sizeof(payloadHeader) + PTRS_LEN, sizeof(ptr));
|
||||
ASSERT_EQ(obj2, ptr);
|
||||
|
||||
/* Test 2: Add plain reply to the buffer */
|
||||
const char *plain = "+OK\r\n";
|
||||
size_t plain_len = strlen(plain);
|
||||
_addReplyToBufferOrList(c, plain, plain_len);
|
||||
|
||||
/* 2 offloads and plain reply expected in c->buf. So 2 headers expected as well */
|
||||
ASSERT_EQ(c->bufpos, 2 * sizeof(payloadHeader) + 2 * PTRS_LEN + plain_len);
|
||||
ASSERT_EQ(header1->payload_type, BULK_STR_REF);
|
||||
ASSERT_EQ(header1->payload_len, 2 * PTRS_LEN);
|
||||
payloadHeader *header2 = c->last_header;
|
||||
ASSERT_EQ(header2->payload_type, PLAIN_REPLY);
|
||||
ASSERT_EQ(header2->payload_len, plain_len);
|
||||
|
||||
/* Add more plain replies. Check same plain reply header updated properly */
|
||||
for (int i = 0; i < 9; ++i) _addReplyToBufferOrList(c, plain, plain_len);
|
||||
ASSERT_EQ(c->bufpos, 2 * sizeof(payloadHeader) + 2 * PTRS_LEN + 10 * plain_len);
|
||||
ASSERT_EQ(header2->payload_type, PLAIN_REPLY);
|
||||
ASSERT_EQ(header2->payload_len, plain_len * 10);
|
||||
|
||||
/* Test 3: Add one more bulk offload to the buffer */
|
||||
testOnlyAddBulkStrRefToBufferOrList(c, obj);
|
||||
ASSERT_EQ(obj->refcount, 3u);
|
||||
ASSERT_EQ(c->bufpos, 3 * sizeof(payloadHeader) + 3 * PTRS_LEN + 10 * plain_len);
|
||||
payloadHeader *header3 = c->last_header;
|
||||
ASSERT_EQ(header3->payload_type, BULK_STR_REF);
|
||||
|
||||
memcpy(&ptr, (char *)c->last_header + sizeof(payloadHeader), sizeof(ptr));
|
||||
ASSERT_EQ(obj, ptr);
|
||||
|
||||
releaseReplyReferences(c);
|
||||
decrRefCount(obj);
|
||||
decrRefCount(obj2);
|
||||
|
||||
freeReplyOffloadClient(c);
|
||||
}
|
||||
|
||||
TEST_F(NetworkingTest, TestAddRepliesWithOffloadsToList) {
|
||||
/* Required for isCopyAvoidPreferred / isCopyAvoidIndicatedByIOThreads */
|
||||
int io_threads_num = server.io_threads_num;
|
||||
int min_io_threads_for_copy_avoid = server.min_io_threads_copy_avoid;
|
||||
server.io_threads_num = 1;
|
||||
server.min_io_threads_copy_avoid = 1;
|
||||
|
||||
client *c = createTestClient();
|
||||
|
||||
// Mock ACL
|
||||
user u;
|
||||
DefaultUser = &u;
|
||||
DefaultUser->flags = USER_FLAG_NOPASS;
|
||||
/* Test 1: Add bulk offloads to the reply list */
|
||||
/* Select reply length so that there is place for 2 headers and 4 bytes only
|
||||
* 4 bytes is not enough for object pointer(s)
|
||||
* This will force bulk offload to be added to reply list
|
||||
*/
|
||||
size_t reply_len = c->buf_usable_size - 2 * sizeof(payloadHeader) - 4;
|
||||
char *reply = (char *)zmalloc(reply_len);
|
||||
memset(reply, 'a', reply_len);
|
||||
_addReplyToBufferOrList(c, reply, reply_len);
|
||||
ASSERT_TRUE(c->flag.buf_encoded);
|
||||
ASSERT_EQ(c->bufpos, sizeof(payloadHeader) + reply_len);
|
||||
ASSERT_EQ(listLength(c->reply), 0u);
|
||||
|
||||
/* As bulk offload header+pointer can't be accommodated in c->buf
|
||||
* then one block is expected in c->reply */
|
||||
robj *obj = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "test"));
|
||||
testOnlyAddBulkStrRefToBufferOrList(c, obj);
|
||||
ASSERT_EQ(obj->refcount, 2u);
|
||||
ASSERT_EQ(c->bufpos, sizeof(payloadHeader) + reply_len);
|
||||
ASSERT_EQ(listLength(c->reply), 1u);
|
||||
|
||||
/* Check bulk offload header+pointer inside c->reply */
|
||||
listIter iter;
|
||||
listRewind(c->reply, &iter);
|
||||
listNode *next = listNext(&iter);
|
||||
clientReplyBlock *blk = (clientReplyBlock *)(listNodeValue(next));
|
||||
|
||||
ASSERT_EQ(blk->used, sizeof(payloadHeader) + PTRS_LEN);
|
||||
payloadHeader *header1 = blk->last_header;
|
||||
ASSERT_EQ(header1->payload_type, BULK_STR_REF);
|
||||
ASSERT_EQ(header1->payload_len, PTRS_LEN);
|
||||
|
||||
robj *ptr;
|
||||
memcpy(&ptr, blk->buf + sizeof(payloadHeader), sizeof(ptr));
|
||||
ASSERT_EQ(obj, ptr);
|
||||
|
||||
/* Test 2: Add one more bulk offload to the reply list */
|
||||
testOnlyAddBulkStrRefToBufferOrList(c, obj);
|
||||
ASSERT_EQ(obj->refcount, 3u);
|
||||
ASSERT_EQ(listLength(c->reply), 1u);
|
||||
ASSERT_EQ(blk->used, sizeof(payloadHeader) + 2 * PTRS_LEN);
|
||||
ASSERT_EQ(header1->payload_type, BULK_STR_REF);
|
||||
ASSERT_EQ(header1->payload_len, 2 * PTRS_LEN);
|
||||
|
||||
/* Test 3: Add plain replies to cause reply list grow */
|
||||
while (reply_len < blk->size - blk->used) _addReplyToBufferOrList(c, reply, reply_len);
|
||||
_addReplyToBufferOrList(c, reply, reply_len);
|
||||
|
||||
ASSERT_EQ(listLength(c->reply), 2u);
|
||||
/* last header in 1st block */
|
||||
payloadHeader *header2 = blk->last_header;
|
||||
listRewind(c->reply, &iter);
|
||||
listNext(&iter);
|
||||
next = listNext(&iter);
|
||||
clientReplyBlock *blk2 = (clientReplyBlock *)(listNodeValue(next));
|
||||
/* last header in 2nd block */
|
||||
payloadHeader *header3 = blk2->last_header;
|
||||
ASSERT_EQ(header2->payload_type, PLAIN_REPLY);
|
||||
ASSERT_EQ(header3->payload_type, PLAIN_REPLY);
|
||||
ASSERT_EQ((header2->payload_len + header3->payload_len) % reply_len, 0u);
|
||||
|
||||
zfree(reply);
|
||||
decrRefCount(obj);
|
||||
|
||||
releaseReplyReferences(c);
|
||||
freeReplyOffloadClient(c);
|
||||
|
||||
/* Restore modified values */
|
||||
server.io_threads_num = io_threads_num;
|
||||
server.min_io_threads_copy_avoid = min_io_threads_for_copy_avoid;
|
||||
}
|
||||
|
||||
TEST_F(NetworkingTest, TestAddBufferToReplyIOV) {
|
||||
client *c = createTestClient();
|
||||
const char *expected_reply = "$5\r\nhello\r\n";
|
||||
ssize_t total_len = (ssize_t)(strlen(expected_reply));
|
||||
const int iovmax = 16;
|
||||
char crlf[2];
|
||||
crlf[0] = '\r';
|
||||
crlf[1] = '\n';
|
||||
|
||||
robj *obj = createObject(OBJ_STRING, sdscatfmt(sdsempty(), "hello"));
|
||||
testOnlyAddBulkStrRefToBufferOrList(c, obj);
|
||||
|
||||
/* Test 1: 1st writevToclient invocation */
|
||||
struct iovec iov_arr[iovmax];
|
||||
char prefixes[iovmax / 3 + 1][LONG_STR_SIZE + 3];
|
||||
bufWriteMetadata metadata[1];
|
||||
|
||||
replyIOV reply;
|
||||
testOnlyInitReplyIOV(c, iovmax, iov_arr, prefixes, crlf, &reply);
|
||||
testOnlyAddBufferToReplyIOV(c->flag.buf_encoded, c->buf, c->bufpos, &reply, &metadata[0]);
|
||||
|
||||
ASSERT_EQ(reply.iov_len_total, (ssize_t)total_len);
|
||||
ASSERT_EQ(reply.iovcnt, 3);
|
||||
const char *ptr = expected_reply;
|
||||
for (int i = 0; i < reply.iovcnt; ++i) {
|
||||
ASSERT_EQ(memcmp(ptr, reply.iov[i].iov_base, reply.iov[i].iov_len), 0);
|
||||
ptr += reply.iov[i].iov_len;
|
||||
}
|
||||
|
||||
/* Test 2: Last written buf/pos/data_len after 1st invocation */
|
||||
testOnlySaveLastWrittenBuf(c, metadata, 1, reply.iov_len_total, 1); /* only 1 byte has been written */
|
||||
ASSERT_EQ(c->io_last_written.buf, c->buf);
|
||||
ASSERT_EQ(c->io_last_written.bufpos, 0u); /* incomplete write */
|
||||
ASSERT_EQ(c->io_last_written.data_len, 1u);
|
||||
|
||||
/* Test 3: 2nd writevToclient invocation */
|
||||
struct iovec iov_arr2[iovmax];
|
||||
char prefixes2[iovmax / 3 + 1][LONG_STR_SIZE + 3];
|
||||
bufWriteMetadata metadata2[1];
|
||||
|
||||
replyIOV reply2;
|
||||
testOnlyInitReplyIOV(c, iovmax, iov_arr2, prefixes2, crlf, &reply2);
|
||||
testOnlyAddBufferToReplyIOV(c->flag.buf_encoded, c->buf, c->bufpos, &reply2, &metadata2[0]);
|
||||
ASSERT_EQ(reply2.iov_len_total, (ssize_t)(total_len - 1));
|
||||
ASSERT_EQ(*(char *)reply2.iov[0].iov_base, '5');
|
||||
|
||||
/* Test 4: Last written buf/pos/data_len after 2nd invocation */
|
||||
testOnlySaveLastWrittenBuf(c, metadata2, 1, reply2.iov_len_total, 4); /* 4 more bytes has been written */
|
||||
ASSERT_EQ(c->io_last_written.buf, c->buf);
|
||||
ASSERT_EQ(c->io_last_written.bufpos, 0u); /* incomplete write */
|
||||
ASSERT_EQ(c->io_last_written.data_len, 5u); /* 1 + 4 */
|
||||
|
||||
/* Test 5: 3rd writevToclient invocation */
|
||||
struct iovec iov_arr3[iovmax];
|
||||
char prefixes3[iovmax / 3 + 1][LONG_STR_SIZE + 3];
|
||||
bufWriteMetadata metadata3[1];
|
||||
|
||||
replyIOV reply3;
|
||||
testOnlyInitReplyIOV(c, iovmax, iov_arr3, prefixes3, crlf, &reply3);
|
||||
testOnlyAddBufferToReplyIOV(c->flag.buf_encoded, c->buf, c->bufpos, &reply3, &metadata3[0]);
|
||||
ASSERT_EQ(reply3.iov_len_total, (ssize_t)(total_len - 5));
|
||||
ASSERT_EQ(*(char *)reply3.iov[0].iov_base, 'e');
|
||||
|
||||
/* Test 6: Last written buf/pos/data_len after 3rd invocation */
|
||||
testOnlySaveLastWrittenBuf(c, metadata3, 1, reply3.iov_len_total, reply3.iov_len_total); /* everything has been written */
|
||||
ASSERT_EQ(c->io_last_written.buf, c->buf);
|
||||
ASSERT_EQ(c->io_last_written.bufpos, c->bufpos);
|
||||
ASSERT_EQ(c->io_last_written.data_len, (size_t)total_len);
|
||||
|
||||
decrRefCount(obj);
|
||||
|
||||
releaseReplyReferences(c);
|
||||
freeReplyOffloadClient(c);
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
#include "../object.c"
|
||||
#include "test_help.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
|
||||
int test_object_with_key(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
sds key = sdsnew("foo");
|
||||
robj *val = createStringObject("bar", strlen("bar"));
|
||||
TEST_ASSERT(val->encoding == OBJ_ENCODING_EMBSTR);
|
||||
TEST_ASSERT(sdslen(objectGetVal(val)) == 3);
|
||||
|
||||
/* Prevent objectSetKeyAndExpire from freeing the old val when reallocating it. */
|
||||
incrRefCount(val);
|
||||
|
||||
robj *o = objectSetKeyAndExpire(val, key, -1);
|
||||
TEST_ASSERT(o->encoding == OBJ_ENCODING_EMBSTR);
|
||||
TEST_ASSERT(objectGetKey(o) != NULL);
|
||||
|
||||
/* Check embedded key "foo" */
|
||||
TEST_ASSERT(sdslen(objectGetKey(o)) == 3);
|
||||
TEST_ASSERT(sdslen(key) == 3);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(o), key) == 0);
|
||||
TEST_ASSERT(strcmp(objectGetKey(o), "foo") == 0);
|
||||
|
||||
/* Check embedded value "bar" (EMBSTR content) */
|
||||
TEST_ASSERT(sdscmp(objectGetVal(o), objectGetVal(val)) == 0);
|
||||
TEST_ASSERT(strcmp(objectGetVal(o), "bar") == 0);
|
||||
TEST_ASSERT(sdslen(objectGetVal(o)) == 3);
|
||||
|
||||
/* Either they're two separate objects, or one object with refcount == 2. */
|
||||
if (o == val) {
|
||||
TEST_ASSERT(o->refcount == 2);
|
||||
} else {
|
||||
TEST_ASSERT(o->refcount == 1);
|
||||
TEST_ASSERT(val->refcount == 1);
|
||||
}
|
||||
|
||||
/* Free them. */
|
||||
sdsfree(key);
|
||||
decrRefCount(val);
|
||||
decrRefCount(o);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_embedded_string_with_key(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* key of length 32 - type 8 */
|
||||
sds key = sdsnew("k:123456789012345678901234567890");
|
||||
TEST_ASSERT(sdslen(key) == 32);
|
||||
|
||||
/* 32B key and 15B value should be embedded within 64B. Contents:
|
||||
* - 8B robj (no ptr) + 1B key header size
|
||||
* - 3B key header + 32B key + 1B null terminator
|
||||
* - 3B val header + 15B val + 1B null terminator
|
||||
* because no pointers are stored, there is no difference for 32 bit builds*/
|
||||
const char *short_value = "123456789012345";
|
||||
TEST_ASSERT(strlen(short_value) == 15);
|
||||
robj *short_val_obj = createStringObject(short_value, strlen(short_value));
|
||||
robj *embstr_obj = objectSetKeyAndExpire(short_val_obj, key, -1);
|
||||
TEST_ASSERT(embstr_obj->encoding == OBJ_ENCODING_EMBSTR);
|
||||
TEST_ASSERT(sdslen(objectGetKey(embstr_obj)) == 32);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(embstr_obj), key) == 0);
|
||||
TEST_ASSERT(sdslen(objectGetVal(embstr_obj)) == 15);
|
||||
TEST_ASSERT(strcmp(objectGetVal(embstr_obj), short_value) == 0);
|
||||
|
||||
/* value of length 16 cannot be embedded with other contents within 64B */
|
||||
const char *longer_value = "1234567890123456";
|
||||
TEST_ASSERT(strlen(longer_value) == 16);
|
||||
robj *longer_val_obj = createStringObject(longer_value, strlen(longer_value));
|
||||
robj *raw_obj = objectSetKeyAndExpire(longer_val_obj, key, -1);
|
||||
TEST_ASSERT(raw_obj->encoding == OBJ_ENCODING_RAW);
|
||||
TEST_ASSERT(sdslen(objectGetKey(raw_obj)) == 32);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(raw_obj), key) == 0);
|
||||
TEST_ASSERT(sdslen(objectGetVal(raw_obj)) == 16);
|
||||
TEST_ASSERT(strcmp(objectGetVal(raw_obj), longer_value) == 0);
|
||||
|
||||
sdsfree(key);
|
||||
decrRefCount(embstr_obj);
|
||||
decrRefCount(raw_obj);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_embedded_string_with_key_and_expire(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* key of length 32 - type 8 */
|
||||
sds key = sdsnew("k:123456789012345678901234567890");
|
||||
TEST_ASSERT(sdslen(key) == 32);
|
||||
|
||||
/* 32B key and 7B value should be embedded within 64B. Contents:
|
||||
* - 8B robj (no ptr) + 8B expire + 1B key header size
|
||||
* - 3B key header + 32B key + 1B null terminator
|
||||
* - 3B val header + 7B val + 1B null terminator
|
||||
* because no pointers are stored, there is no difference for 32 bit builds*/
|
||||
const char *short_value = "1234567";
|
||||
TEST_ASSERT(strlen(short_value) == 7);
|
||||
robj *short_val_obj = createStringObject(short_value, strlen(short_value));
|
||||
robj *embstr_obj = objectSetKeyAndExpire(short_val_obj, key, 128);
|
||||
TEST_ASSERT(embstr_obj->encoding == OBJ_ENCODING_EMBSTR);
|
||||
TEST_ASSERT(sdslen(objectGetKey(embstr_obj)) == 32);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(embstr_obj), key) == 0);
|
||||
TEST_ASSERT(sdslen(objectGetVal(embstr_obj)) == 7);
|
||||
TEST_ASSERT(strcmp(objectGetVal(embstr_obj), short_value) == 0);
|
||||
|
||||
/* value of length 8 cannot be embedded with other contents within 64B */
|
||||
const char *longer_value = "12345678";
|
||||
TEST_ASSERT(strlen(longer_value) == 8);
|
||||
robj *longer_val_obj = createStringObject(longer_value, strlen(longer_value));
|
||||
robj *raw_obj = objectSetKeyAndExpire(longer_val_obj, key, 128);
|
||||
TEST_ASSERT(raw_obj->encoding == OBJ_ENCODING_RAW);
|
||||
TEST_ASSERT(sdslen(objectGetKey(raw_obj)) == 32);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(raw_obj), key) == 0);
|
||||
TEST_ASSERT(sdslen(objectGetVal(raw_obj)) == 8);
|
||||
TEST_ASSERT(strcmp(objectGetVal(raw_obj), longer_value) == 0);
|
||||
|
||||
sdsfree(key);
|
||||
decrRefCount(embstr_obj);
|
||||
decrRefCount(raw_obj);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_embedded_value(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* with only value there is only 12B overhead, so we can embed up to 52B.
|
||||
* 8B robj (no ptr) + 3B val header + 52B val + 1B null terminator */
|
||||
const char *val = "v:12345678901234567890123456789012345678901234567890";
|
||||
TEST_ASSERT(strlen(val) == 52);
|
||||
robj *embstr_obj = createStringObject(val, strlen(val));
|
||||
TEST_ASSERT(embstr_obj->encoding == OBJ_ENCODING_EMBSTR);
|
||||
TEST_ASSERT(sdslen(objectGetVal(embstr_obj)) == 52);
|
||||
TEST_ASSERT(strcmp(objectGetVal(embstr_obj), val) == 0);
|
||||
|
||||
decrRefCount(embstr_obj);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_unembed_value(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
const char *short_value = "embedded value";
|
||||
robj *short_val_obj = createStringObject(short_value, strlen(short_value));
|
||||
sds key = sdsnew("embedded key");
|
||||
long long expire = 155;
|
||||
|
||||
robj *obj = objectSetKeyAndExpire(short_val_obj, key, expire);
|
||||
TEST_ASSERT(obj->encoding == OBJ_ENCODING_EMBSTR);
|
||||
TEST_ASSERT(strcmp(objectGetVal(obj), short_value) == 0);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(obj), key) == 0);
|
||||
TEST_ASSERT(objectGetExpire(obj) == expire);
|
||||
TEST_ASSERT(objectGetVal(obj) != short_value);
|
||||
|
||||
/* Unembed the value - it uses a separate allocation now.
|
||||
* the other embedded data gets shifted, so check them too */
|
||||
objectUnembedVal(obj);
|
||||
TEST_ASSERT(obj->encoding == OBJ_ENCODING_RAW);
|
||||
TEST_ASSERT(strcmp(objectGetVal(obj), short_value) == 0);
|
||||
TEST_ASSERT(sdscmp(objectGetKey(obj), key) == 0);
|
||||
TEST_ASSERT(objectGetExpire(obj) == expire);
|
||||
TEST_ASSERT(objectGetVal(obj) != short_value); /* different allocation, different copy */
|
||||
|
||||
sdsfree(key);
|
||||
decrRefCount(obj);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "server.h"
|
||||
}
|
||||
|
||||
class ObjectTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
TEST_F(ObjectTest, object_with_key) {
|
||||
sds key = sdsnew("foo");
|
||||
robj *val = createStringObject("bar", strlen("bar"));
|
||||
ASSERT_EQ(val->encoding, (unsigned)OBJ_ENCODING_EMBSTR);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(val)), 3u);
|
||||
|
||||
/* Prevent objectSetKeyAndExpire from freeing the old val when reallocating it. */
|
||||
incrRefCount(val);
|
||||
|
||||
robj *o = objectSetKeyAndExpire(val, key, -1);
|
||||
ASSERT_EQ(o->encoding, (unsigned)OBJ_ENCODING_EMBSTR);
|
||||
ASSERT_NE(objectGetKey(o), nullptr);
|
||||
|
||||
/* Check embedded key "foo" */
|
||||
ASSERT_EQ(sdslen(objectGetKey(o)), 3u);
|
||||
ASSERT_EQ(sdslen(key), 3u);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(o), key), 0);
|
||||
ASSERT_EQ(strcmp(objectGetKey(o), "foo"), 0);
|
||||
|
||||
/* Check embedded value "bar" (EMBSTR content) */
|
||||
ASSERT_EQ(sdscmp((sds)objectGetVal(o), (sds)objectGetVal(val)), 0);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(o), "bar"), 0);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(o)), 3u);
|
||||
|
||||
/* Either they're two separate objects, or one object with refcount == 2. */
|
||||
if (o == val) {
|
||||
ASSERT_EQ((unsigned)o->refcount, 2u);
|
||||
} else {
|
||||
ASSERT_EQ((unsigned)o->refcount, 1u);
|
||||
ASSERT_EQ((unsigned)val->refcount, 1u);
|
||||
}
|
||||
|
||||
/* Free them. */
|
||||
sdsfree(key);
|
||||
decrRefCount(val);
|
||||
decrRefCount(o);
|
||||
}
|
||||
|
||||
TEST_F(ObjectTest, embedded_string_with_key) {
|
||||
/* key of length 32 - type 8 */
|
||||
sds key = sdsnew("k:123456789012345678901234567890");
|
||||
ASSERT_EQ(sdslen(key), 32u);
|
||||
|
||||
/* 32B key and 15B value should be embedded within 64B. Contents:
|
||||
* - 8B robj (no ptr) + 1B key header size
|
||||
* - 3B key header + 32B key + 1B null terminator
|
||||
* - 3B val header + 15B val + 1B null terminator
|
||||
* because no pointers are stored, there is no difference for 32 bit builds*/
|
||||
const char *short_value = "123456789012345";
|
||||
ASSERT_EQ(strlen(short_value), 15u);
|
||||
robj *short_val_obj = createStringObject(short_value, strlen(short_value));
|
||||
robj *embstr_obj = objectSetKeyAndExpire(short_val_obj, key, -1);
|
||||
ASSERT_EQ(embstr_obj->encoding, (unsigned)OBJ_ENCODING_EMBSTR);
|
||||
ASSERT_EQ(sdslen(objectGetKey(embstr_obj)), 32u);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(embstr_obj), key), 0);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(embstr_obj)), 15u);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(embstr_obj), short_value), 0);
|
||||
|
||||
/* value of length 16 cannot be embedded with other contents within 64B */
|
||||
const char *longer_value = "1234567890123456";
|
||||
ASSERT_EQ(strlen(longer_value), 16u);
|
||||
robj *longer_val_obj = createStringObject(longer_value, strlen(longer_value));
|
||||
robj *raw_obj = objectSetKeyAndExpire(longer_val_obj, key, -1);
|
||||
ASSERT_EQ(raw_obj->encoding, (unsigned)OBJ_ENCODING_RAW);
|
||||
ASSERT_EQ(sdslen(objectGetKey(raw_obj)), 32u);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(raw_obj), key), 0);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(raw_obj)), 16u);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(raw_obj), longer_value), 0);
|
||||
|
||||
sdsfree(key);
|
||||
decrRefCount(embstr_obj);
|
||||
decrRefCount(raw_obj);
|
||||
}
|
||||
|
||||
TEST_F(ObjectTest, embedded_string_with_key_and_expire) {
|
||||
/* key of length 32 - type 8 */
|
||||
sds key = sdsnew("k:123456789012345678901234567890");
|
||||
ASSERT_EQ(sdslen(key), 32u);
|
||||
|
||||
/* 32B key and 7B value should be embedded within 64B. Contents:
|
||||
* - 8B robj (no ptr) + 8B expire + 1B key header size
|
||||
* - 3B key header + 32B key + 1B null terminator
|
||||
* - 3B val header + 7B val + 1B null terminator
|
||||
* because no pointers are stored, there is no difference for 32 bit builds*/
|
||||
const char *short_value = "1234567";
|
||||
ASSERT_EQ(strlen(short_value), 7u);
|
||||
robj *short_val_obj = createStringObject(short_value, strlen(short_value));
|
||||
robj *embstr_obj = objectSetKeyAndExpire(short_val_obj, key, 128);
|
||||
ASSERT_EQ(embstr_obj->encoding, (unsigned)OBJ_ENCODING_EMBSTR);
|
||||
ASSERT_EQ(sdslen(objectGetKey(embstr_obj)), 32u);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(embstr_obj), key), 0);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(embstr_obj)), 7u);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(embstr_obj), short_value), 0);
|
||||
|
||||
/* value of length 8 cannot be embedded with other contents within 64B */
|
||||
const char *longer_value = "12345678";
|
||||
ASSERT_EQ(strlen(longer_value), 8u);
|
||||
robj *longer_val_obj = createStringObject(longer_value, strlen(longer_value));
|
||||
robj *raw_obj = objectSetKeyAndExpire(longer_val_obj, key, 128);
|
||||
ASSERT_EQ(raw_obj->encoding, (unsigned)OBJ_ENCODING_RAW);
|
||||
ASSERT_EQ(sdslen(objectGetKey(raw_obj)), 32u);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(raw_obj), key), 0);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(raw_obj)), 8u);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(raw_obj), longer_value), 0);
|
||||
|
||||
sdsfree(key);
|
||||
decrRefCount(embstr_obj);
|
||||
decrRefCount(raw_obj);
|
||||
}
|
||||
|
||||
TEST_F(ObjectTest, embedded_value) {
|
||||
/* with only value there is only 12B overhead, so we can embed up to 52B.
|
||||
* 8B robj (no ptr) + 3B val header + 52B val + 1B null terminator */
|
||||
const char *val = "v:12345678901234567890123456789012345678901234567890";
|
||||
ASSERT_EQ(strlen(val), 52u);
|
||||
robj *embstr_obj = createStringObject(val, strlen(val));
|
||||
ASSERT_EQ(embstr_obj->encoding, (unsigned)OBJ_ENCODING_EMBSTR);
|
||||
ASSERT_EQ(sdslen((sds)objectGetVal(embstr_obj)), 52u);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(embstr_obj), val), 0);
|
||||
|
||||
decrRefCount(embstr_obj);
|
||||
}
|
||||
|
||||
TEST_F(ObjectTest, unembed_value) {
|
||||
const char *short_value = "embedded value";
|
||||
robj *short_val_obj = createStringObject(short_value, strlen(short_value));
|
||||
sds key = sdsnew("embedded key");
|
||||
long long expire = 155;
|
||||
|
||||
robj *obj = objectSetKeyAndExpire(short_val_obj, key, expire);
|
||||
ASSERT_EQ(obj->encoding, (unsigned)OBJ_ENCODING_EMBSTR);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(obj), short_value), 0);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(obj), key), 0);
|
||||
ASSERT_EQ(objectGetExpire(obj), expire);
|
||||
ASSERT_NE(objectGetVal(obj), short_value);
|
||||
|
||||
/* Unembed the value - it uses a separate allocation now.
|
||||
* the other embedded data gets shifted, so check them too */
|
||||
objectUnembedVal(obj);
|
||||
ASSERT_EQ(obj->encoding, (unsigned)OBJ_ENCODING_RAW);
|
||||
ASSERT_EQ(strcmp((const char *)objectGetVal(obj), short_value), 0);
|
||||
ASSERT_EQ(sdscmp(objectGetKey(obj), key), 0);
|
||||
ASSERT_EQ(objectGetExpire(obj), expire);
|
||||
ASSERT_NE(objectGetVal(obj), short_value); /* different allocation, different copy */
|
||||
|
||||
sdsfree(key);
|
||||
decrRefCount(obj);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,651 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include "test_help.h"
|
||||
|
||||
#include "../fmacros.h"
|
||||
#include "../sds.h"
|
||||
#include "../sdsalloc.h"
|
||||
|
||||
long long _ustime(void); /* From test_crc64combine.c */
|
||||
|
||||
static sds sdsTestTemplateCallback(const_sds varname, void *arg) {
|
||||
UNUSED(arg);
|
||||
static const char *_var1 = "variable1";
|
||||
static const char *_var2 = "variable2";
|
||||
|
||||
if (!strcmp(varname, _var1))
|
||||
return sdsnew("value1");
|
||||
else if (!strcmp(varname, _var2))
|
||||
return sdsnew("value2");
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int test_sds(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
sds x = sdsnew("foo"), y;
|
||||
|
||||
TEST_ASSERT_MESSAGE("Create a string and obtain the length", sdslen(x) == 3 && memcmp(x, "foo\0", 4) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnewlen("foo", 2);
|
||||
TEST_ASSERT_MESSAGE("Create a string with specified length", sdslen(x) == 2 && memcmp(x, "fo\0", 3) == 0);
|
||||
|
||||
x = sdscat(x, "bar");
|
||||
TEST_ASSERT_MESSAGE("Strings concatenation", sdslen(x) == 5 && memcmp(x, "fobar\0", 6) == 0);
|
||||
|
||||
x = sdscpy(x, "a");
|
||||
TEST_ASSERT_MESSAGE("sdscpy() against an originally longer string", sdslen(x) == 1 && memcmp(x, "a\0", 2) == 0);
|
||||
|
||||
x = sdscpy(x, "xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk");
|
||||
TEST_ASSERT_MESSAGE("sdscpy() against an originally shorter string",
|
||||
sdslen(x) == 33 && memcmp(x, "xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk\0", 33) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdscatprintf(sdsempty(), "%d", 123);
|
||||
TEST_ASSERT_MESSAGE("sdscatprintf() seems working in the base case", sdslen(x) == 3 && memcmp(x, "123\0", 4) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdscatprintf(sdsempty(), "a%cb", 0);
|
||||
TEST_ASSERT_MESSAGE("sdscatprintf() seems working with \\0 inside of result", sdslen(x) == 3 && memcmp(x,
|
||||
"a\0"
|
||||
"b\0",
|
||||
4) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
char etalon[1024 * 1024];
|
||||
for (size_t i = 0; i < sizeof(etalon); i++) {
|
||||
etalon[i] = '0';
|
||||
}
|
||||
x = sdscatprintf(sdsempty(), "%0*d", (int)sizeof(etalon), 0);
|
||||
TEST_ASSERT_MESSAGE("sdscatprintf() can print 1MB",
|
||||
sdslen(x) == sizeof(etalon) && memcmp(x, etalon, sizeof(etalon)) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew("--");
|
||||
x = sdscatfmt(x, "Hello %s World %I,%I--", "Hi!", LLONG_MIN, LLONG_MAX);
|
||||
TEST_ASSERT_MESSAGE("sdscatfmt() seems working in the base case",
|
||||
sdslen(x) == 60 && memcmp(x,
|
||||
"--Hello Hi! World -9223372036854775808,"
|
||||
"9223372036854775807--",
|
||||
60) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew("--");
|
||||
x = sdscatfmt(x, "%u,%U--", UINT_MAX, ULLONG_MAX);
|
||||
TEST_ASSERT_MESSAGE("sdscatfmt() seems working with unsigned numbers",
|
||||
sdslen(x) == 35 && memcmp(x, "--4294967295,18446744073709551615--", 35) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew(" x ");
|
||||
sdstrim(x, " x");
|
||||
TEST_ASSERT_MESSAGE("sdstrim() works when all chars match", sdslen(x) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew(" x ");
|
||||
sdstrim(x, " ");
|
||||
TEST_ASSERT_MESSAGE("sdstrim() works when a single char remains", sdslen(x) == 1 && x[0] == 'x');
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew("xxciaoyyy");
|
||||
sdstrim(x, "xy");
|
||||
TEST_ASSERT_MESSAGE("sdstrim() correctly trims characters", sdslen(x) == 4 && memcmp(x, "ciao\0", 5) == 0);
|
||||
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 1, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,1,1)", sdslen(y) == 1 && memcmp(y, "i\0", 2) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 1, -1);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,1,-1)", sdslen(y) == 3 && memcmp(y, "iao\0", 4) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, -2, -1);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,-2,-1)", sdslen(y) == 2 && memcmp(y, "ao\0", 3) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 2, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,2,1)", sdslen(y) == 0 && memcmp(y, "\0", 1) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 1, 100);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,1,100)", sdslen(y) == 3 && memcmp(y, "iao\0", 4) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 100, 100);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,100,100)", sdslen(y) == 0 && memcmp(y, "\0", 1) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 4, 6);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,4,6)", sdslen(y) == 0 && memcmp(y, "\0", 1) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 3, 6);
|
||||
TEST_ASSERT_MESSAGE("sdsrange(...,3,6)", sdslen(y) == 1 && memcmp(y, "o\0", 2) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnew("foo");
|
||||
y = sdsnew("foa");
|
||||
TEST_ASSERT_MESSAGE("sdscmp(foo,foa)", sdscmp(x, y) > 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnew("bar");
|
||||
y = sdsnew("bar");
|
||||
TEST_ASSERT_MESSAGE("sdscmp(bar,bar)", sdscmp(x, y) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnew("aar");
|
||||
y = sdsnew("bar");
|
||||
TEST_ASSERT_MESSAGE("sdscmp(bar,bar)", sdscmp(x, y) < 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnewlen("\a\n\0foo\r", 7);
|
||||
y = sdscatrepr(sdsempty(), x, sdslen(x));
|
||||
TEST_ASSERT_MESSAGE("sdscatrepr(...data...)", memcmp(y, "\"\\a\\n\\x00foo\\r\"", 15) == 0);
|
||||
|
||||
unsigned int oldfree;
|
||||
char *p;
|
||||
int i;
|
||||
size_t step = 10, j;
|
||||
|
||||
sdsfree(x);
|
||||
sdsfree(y);
|
||||
x = sdsnew("0");
|
||||
TEST_ASSERT_MESSAGE("sdsnew() free/len buffers", sdslen(x) == 1 && sdsavail(x) == 0);
|
||||
|
||||
/* Run the test a few times in order to hit the first two
|
||||
* SDS header types. */
|
||||
for (i = 0; i < 10; i++) {
|
||||
size_t oldlen = sdslen(x);
|
||||
x = sdsMakeRoomFor(x, step);
|
||||
int type = x[-1] & SDS_TYPE_MASK;
|
||||
|
||||
TEST_ASSERT_MESSAGE("sdsMakeRoomFor() len", sdslen(x) == oldlen);
|
||||
if (type != SDS_TYPE_5) {
|
||||
TEST_ASSERT_MESSAGE("sdsMakeRoomFor() free", sdsavail(x) >= step);
|
||||
oldfree = sdsavail(x);
|
||||
UNUSED(oldfree);
|
||||
}
|
||||
p = x + oldlen;
|
||||
for (j = 0; j < step; j++) {
|
||||
p[j] = 'A' + j;
|
||||
}
|
||||
sdsIncrLen(x, step);
|
||||
}
|
||||
TEST_ASSERT_MESSAGE("sdsMakeRoomFor() content", memcmp("0ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGH"
|
||||
"IJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ",
|
||||
x, 101) == 0);
|
||||
TEST_ASSERT_MESSAGE("sdsMakeRoomFor() final length", sdslen(x) == 101);
|
||||
|
||||
sdsfree(x);
|
||||
|
||||
/* Simple template */
|
||||
x = sdstemplate("v1={variable1} v2={variable2}", sdsTestTemplateCallback, NULL);
|
||||
TEST_ASSERT_MESSAGE("sdstemplate() normal flow", memcmp(x, "v1=value1 v2=value2", 19) == 0);
|
||||
sdsfree(x);
|
||||
|
||||
/* Template with callback error */
|
||||
x = sdstemplate("v1={variable1} v3={doesnotexist}", sdsTestTemplateCallback, NULL);
|
||||
TEST_ASSERT_MESSAGE("sdstemplate() with callback error", x == NULL);
|
||||
|
||||
/* Template with empty var name */
|
||||
x = sdstemplate("v1={", sdsTestTemplateCallback, NULL);
|
||||
TEST_ASSERT_MESSAGE("sdstemplate() with empty var name", x == NULL);
|
||||
|
||||
/* Template with truncated var name */
|
||||
x = sdstemplate("v1={start", sdsTestTemplateCallback, NULL);
|
||||
TEST_ASSERT_MESSAGE("sdstemplate() with truncated var name", x == NULL);
|
||||
|
||||
/* Template with quoting */
|
||||
x = sdstemplate("v1={{{variable1}} {{} v2={variable2}", sdsTestTemplateCallback, NULL);
|
||||
TEST_ASSERT_MESSAGE("sdstemplate() with quoting", memcmp(x, "v1={value1} {} v2=value2", 24) == 0);
|
||||
sdsfree(x);
|
||||
|
||||
/* Test sdsResize - extend */
|
||||
x = sdsnew("1234567890123456789012345678901234567890");
|
||||
x = sdsResize(x, 200, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand type", x[-1] == SDS_TYPE_8);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand len", sdslen(x) == 40);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand strlen", strlen(x) == 40);
|
||||
/* Different allocator allocates at least as large as requested size,
|
||||
* to confirm the allocator won't waste too much,
|
||||
* we add a largest size checker here. */
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand alloc", sdsalloc(x) >= 200 && sdsalloc(x) < 400);
|
||||
/* Test sdsResize - trim free space */
|
||||
x = sdsResize(x, 80, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() shrink type", x[-1] == SDS_TYPE_8);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() shrink len", sdslen(x) == 40);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() shrink strlen", strlen(x) == 40);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() shrink alloc", sdsalloc(x) >= 80);
|
||||
/* Test sdsResize - crop used space */
|
||||
x = sdsResize(x, 30, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop type", x[-1] == SDS_TYPE_8);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop len", sdslen(x) == 30);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop strlen", strlen(x) == 30);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop alloc", sdsalloc(x) >= 30);
|
||||
/* Test sdsResize - extend to different class */
|
||||
x = sdsResize(x, 400, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand type", x[-1] == SDS_TYPE_16);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand len", sdslen(x) == 30);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand strlen", strlen(x) == 30);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() expand alloc", sdsalloc(x) >= 400);
|
||||
/* Test sdsResize - shrink to different class */
|
||||
x = sdsResize(x, 4, 1);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop type", x[-1] == SDS_TYPE_8);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop len", sdslen(x) == 4);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop strlen", strlen(x) == 4);
|
||||
TEST_ASSERT_MESSAGE("sdsResize() crop alloc", sdsalloc(x) >= 4);
|
||||
sdsfree(x);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_typesAndAllocSize(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
sds x = sdsnewlen(NULL, 31);
|
||||
TEST_ASSERT_MESSAGE("len 31 type", (x[-1] & SDS_TYPE_MASK) == SDS_TYPE_5);
|
||||
TEST_ASSERT_MESSAGE("len 31 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 32);
|
||||
TEST_ASSERT_MESSAGE("len 32 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_8);
|
||||
TEST_ASSERT_MESSAGE("len 32 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 252);
|
||||
TEST_ASSERT_MESSAGE("len 252 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_8);
|
||||
TEST_ASSERT_MESSAGE("len 252 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 253);
|
||||
TEST_ASSERT_MESSAGE("len 253 type", (x[-1] & SDS_TYPE_MASK) == SDS_TYPE_16);
|
||||
TEST_ASSERT_MESSAGE("len 253 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 65530);
|
||||
TEST_ASSERT_MESSAGE("len 65530 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_16);
|
||||
TEST_ASSERT_MESSAGE("len 65530 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 65531);
|
||||
TEST_ASSERT_MESSAGE("len 65531 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_32);
|
||||
TEST_ASSERT_MESSAGE("len 65531 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
#if (LONG_MAX == LLONG_MAX)
|
||||
if (flags & UNIT_TEST_LARGE_MEMORY) {
|
||||
x = sdsnewlen(NULL, 4294967286);
|
||||
TEST_ASSERT_MESSAGE("len 4294967286 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_32);
|
||||
TEST_ASSERT_MESSAGE("len 4294967286 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 4294967287);
|
||||
TEST_ASSERT_MESSAGE("len 4294967287 type", (x[-1] & SDS_TYPE_MASK) == SDS_TYPE_64);
|
||||
TEST_ASSERT_MESSAGE("len 4294967287 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The test verifies that we can adjust SDS types if an allocator returned
|
||||
* larger buffer. The maximum length for type SDS_TYPE_X is
|
||||
* 2^X - header_size(SDS_TYPE_X) - 1. The maximum value to be stored in alloc
|
||||
* field is 2^X - 1. When allocated buffer is larger than
|
||||
* 2^X + header_size(SDS_TYPE_X), we "move" to a larger type SDS_TYPE_Y. To be
|
||||
* sure SDS_TYPE_Y header fits into 2^X + header_size(SDS_TYPE_X) + 1 bytes, the
|
||||
* difference between header sizes must be smaller than
|
||||
* header_size(SDS_TYPE_X) + 1.
|
||||
* We ignore SDS_TYPE_5 as it doesn't have alloc field. */
|
||||
int test_sdsHeaderSizes(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_ASSERT_MESSAGE("can't always adjust SDS_TYPE_8 with SDS_TYPE_16",
|
||||
sizeof(struct sdshdr16) <= 2 * sizeof(struct sdshdr8) + 1);
|
||||
TEST_ASSERT_MESSAGE("can't always adjust SDS_TYPE_16 with SDS_TYPE_32",
|
||||
sizeof(struct sdshdr32) <= 2 * sizeof(struct sdshdr16) + 1);
|
||||
#if (LONG_MAX == LLONG_MAX)
|
||||
TEST_ASSERT_MESSAGE("can't always adjust SDS_TYPE_32 with SDS_TYPE_64",
|
||||
sizeof(struct sdshdr64) <= 2 * sizeof(struct sdshdr32) + 1);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_sdssplitargs(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int len;
|
||||
sds *sargv;
|
||||
|
||||
sargv = sdssplitargs("Testing one two three", &len);
|
||||
TEST_ASSERT(4 == len);
|
||||
TEST_ASSERT(!strcmp("Testing", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("one", sargv[1]));
|
||||
TEST_ASSERT(!strcmp("two", sargv[2]));
|
||||
TEST_ASSERT(!strcmp("three", sargv[3]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("", &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv != NULL);
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"Testing split strings\" 'Another split string'", &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("Testing split strings", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("Another split string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"Hello\" ", &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("Hello", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
char *binary_string = "\"\\x73\\x75\\x70\\x65\\x72\\x20\\x00\\x73\\x65\\x63\\x72\\x65\\x74\\x20\\x70\\x61\\x73\\x73\\x77\\x6f\\x72\\x64\"";
|
||||
sargv = sdssplitargs(binary_string, &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("super \x00secret password", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("unquoted", &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("unquoted", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("empty string \"\"", &len);
|
||||
TEST_ASSERT(3 == len);
|
||||
TEST_ASSERT(!strcmp("empty", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("string", sargv[1]));
|
||||
TEST_ASSERT(!strcmp("", sargv[2]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"deeply\\\"quoted\" 's\\'t\\\"r'ing", &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("deeply\"quoted", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("s't\\\"ring", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("unquoted\" \"with' 'quotes string", &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("unquoted with quotes", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"quoted\"' another 'quoted string", &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("quoted another quoted", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"shell-like \"'\"'\"'\"' quote-escaping '", &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("shell-like \"' quote-escaping ", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"unterminated \"'single quotes", &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv == NULL);
|
||||
|
||||
sargv = sdssplitargs("'unterminated '\"double quotes", &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv == NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_sdsnsplitargs(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
int len;
|
||||
sds *sargv;
|
||||
const char *test_str;
|
||||
|
||||
// Test basic parameter splitting
|
||||
test_str = "Testing one two three";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(4 == len);
|
||||
TEST_ASSERT(!strcmp("Testing", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("one", sargv[1]));
|
||||
TEST_ASSERT(!strcmp("two", sargv[2]));
|
||||
TEST_ASSERT(!strcmp("three", sargv[3]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test empty string
|
||||
sargv = sdsnsplitargs("", 0, &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv != NULL);
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test quoted strings
|
||||
test_str = "\"Testing split strings\" 'Another split string'";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("Testing split strings", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("Another split string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test trailing space after quoted string
|
||||
test_str = "\"Hello\" ";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("Hello", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test binary string with null character using \x escape
|
||||
test_str = "\"\\x73\\x75\\x70\\x65\\x72\\x20\\x00\\x73\\x65\\x63\\x72\\x65\\x74\\x20\\x70\\x61\\x73\\x73\\x77\\x6f\\x72\\x64\"";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("super \x00secret password", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
char str_with_null[] = "test\0null";
|
||||
sargv = sdsnsplitargs(str_with_null, sizeof(str_with_null) - 1, &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!memcmp("test", sargv[0], 4));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test single unquoted string
|
||||
test_str = "unquoted";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("unquoted", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test empty quoted string
|
||||
test_str = "empty string \"\"";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(3 == len);
|
||||
TEST_ASSERT(!strcmp("empty", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("string", sargv[1]));
|
||||
TEST_ASSERT(!strcmp("", sargv[2]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test escaped quotes
|
||||
test_str = "\"deeply\\\"quoted\" 's\\'t\\\"r'ing";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("deeply\"quoted", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("s't\\\"ring", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test mixed quoted and unquoted parts
|
||||
test_str = "unquoted\" \"with' 'quotes string";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("unquoted with quotes", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test concatenated quoted strings
|
||||
test_str = "\"quoted\"' another 'quoted string";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("quoted another quoted", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test complex quote escaping
|
||||
test_str = "\"shell-like \"'\"'\"'\"' quote-escaping '";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("shell-like \"' quote-escaping ", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test unterminated double quote
|
||||
test_str = "\"unterminated \"'single quotes";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv == NULL);
|
||||
|
||||
// Test unterminated single quote
|
||||
test_str = "'unterminated '\"double quotes";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv == NULL);
|
||||
|
||||
// Test partial string length (truncated input)
|
||||
test_str = "Testing one two three";
|
||||
sargv = sdsnsplitargs(test_str, 8, &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!strcmp("Testing", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with exact length (no truncation)
|
||||
test_str = "Exact length";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("Exact", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("length", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with leading spaces
|
||||
test_str = " leading spaces";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("leading", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("spaces", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with trailing spaces
|
||||
test_str = "trailing spaces ";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("trailing", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("spaces", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with consecutive spaces
|
||||
test_str = "multiple spaces";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("multiple", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("spaces", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with only spaces
|
||||
test_str = " ";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
TEST_ASSERT(sargv != NULL);
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string containing null character in the middle of parsing
|
||||
char str_with_null_in_middle[] = "arg1\0arg2 arg3";
|
||||
sargv = sdsnsplitargs(str_with_null_in_middle, sizeof(str_with_null_in_middle) - 1, &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(!memcmp("arg1", sargv[0], 4));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test very long single argument
|
||||
char long_arg[1024];
|
||||
memset(long_arg, 'a', sizeof(long_arg) - 1);
|
||||
long_arg[sizeof(long_arg) - 1] = '\0';
|
||||
sargv = sdsnsplitargs(long_arg, sizeof(long_arg) - 1, &len);
|
||||
TEST_ASSERT(1 == len);
|
||||
TEST_ASSERT(strlen(sargv[0]) == sizeof(long_arg) - 1);
|
||||
TEST_ASSERT(!memcmp(long_arg, sargv[0], sizeof(long_arg) - 1));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test mixed quote types in one argument
|
||||
test_str = "\"double'quotes\" 'single\"quotes'";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("double'quotes", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("single\"quotes", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test mixed quote types with different lengths
|
||||
sds complex_str = sdsnew("\"double'quotes\" 'single\"quotes'");
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str) - 1, &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
|
||||
complex_str = sdscatlen(complex_str, "\0", 1);
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str) - 1, &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("double'quotes", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("single\"quotes", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str), &len);
|
||||
TEST_ASSERT(2 == len);
|
||||
TEST_ASSERT(!strcmp("double'quotes", sargv[0]));
|
||||
TEST_ASSERT(!strcmp("single\"quotes", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str) - 2, &len);
|
||||
TEST_ASSERT(0 == len);
|
||||
sdsfree(complex_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_sdsnsplitargsBenchmark(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
|
||||
if (!(flags & UNIT_TEST_SINGLE)) return 0;
|
||||
|
||||
char str_with_null_in_middle[] = "arg1\0arg2 arg3";
|
||||
size_t str_len = sizeof(str_with_null_in_middle) - 1;
|
||||
int len = 0;
|
||||
long long start = _ustime();
|
||||
for (int i = 0; i < 1000000; i++) {
|
||||
sds *sargv = sdsnsplitargs(str_with_null_in_middle, str_len, &len);
|
||||
sdsfreesplitres(sargv, len);
|
||||
}
|
||||
printf("sdsnsplitargs 1000000 times: %f\n", (double)(_ustime() - start) / 1000000);
|
||||
|
||||
start = _ustime();
|
||||
for (int i = 0; i < 1000000; i++) {
|
||||
sds str = sdsnewlen(str_with_null_in_middle, str_len);
|
||||
sds *sargv = sdssplitargs(str, &len);
|
||||
sdsfreesplitres(sargv, len);
|
||||
sdsfree(str);
|
||||
}
|
||||
printf("sdssplitargs 1000000 times: %f\n", (double)(_ustime() - start) / 1000000);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "sds.h"
|
||||
#include "sdsalloc.h"
|
||||
#include "util.h"
|
||||
|
||||
extern bool large_memory;
|
||||
}
|
||||
|
||||
static sds sdsTestTemplateCallback(const_sds varname, void *arg) {
|
||||
UNUSED(arg);
|
||||
static const char *_var1 = "variable1";
|
||||
static const char *_var2 = "variable2";
|
||||
|
||||
if (!strcmp(varname, _var1))
|
||||
return sdsnew("value1");
|
||||
else if (!strcmp(varname, _var2))
|
||||
return sdsnew("value2");
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
class SdsTest : public ::testing::Test {}; // Empty fixture for test organization and filtering
|
||||
|
||||
TEST_F(SdsTest, TestSds) {
|
||||
sds x = sdsnew("foo"), y;
|
||||
|
||||
/* Create a string and obtain the length */
|
||||
ASSERT_TRUE(sdslen(x) == 3 && memcmp(x, "foo\0", 4) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnewlen("foo", 2);
|
||||
/* Create a string with specified length */
|
||||
ASSERT_TRUE(sdslen(x) == 2 && memcmp(x, "fo\0", 3) == 0);
|
||||
|
||||
x = sdscat(x, "bar");
|
||||
/* Strings concatenation */
|
||||
ASSERT_TRUE(sdslen(x) == 5 && memcmp(x, "fobar\0", 6) == 0);
|
||||
|
||||
x = sdscpy(x, "a");
|
||||
/* sdscpy() against an originally longer string */
|
||||
ASSERT_TRUE(sdslen(x) == 1 && memcmp(x, "a\0", 2) == 0);
|
||||
|
||||
x = sdscpy(x, "xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk");
|
||||
/* sdscpy() against an originally shorter string */
|
||||
ASSERT_TRUE(sdslen(x) == 33 && memcmp(x, "xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk\0", 33) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdscatprintf(sdsempty(), "%d", 123);
|
||||
/* sdscatprintf() seems working in the base case */
|
||||
ASSERT_TRUE(sdslen(x) == 3 && memcmp(x, "123\0", 4) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdscatprintf(sdsempty(), "a%cb", 0);
|
||||
/* sdscatprintf() seems working with \0 inside of result */
|
||||
ASSERT_TRUE(sdslen(x) == 3 && memcmp(x, "a\0"
|
||||
"b\0",
|
||||
4) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
size_t etalon_size = 1024 * 1024;
|
||||
char *etalon = (char *)malloc(etalon_size);
|
||||
for (size_t i = 0; i < etalon_size; i++) {
|
||||
etalon[i] = '0';
|
||||
}
|
||||
x = sdscatprintf(sdsempty(), "%0*d", (int)etalon_size, 0);
|
||||
/* sdscatprintf() can print 1MB */
|
||||
ASSERT_TRUE(sdslen(x) == etalon_size && memcmp(x, etalon, etalon_size) == 0);
|
||||
free(etalon);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew("--");
|
||||
x = sdscatfmt(x, "Hello %s World %I,%I--", "Hi!", LLONG_MIN, LLONG_MAX);
|
||||
/* sdscatfmt() seems working in the base case */
|
||||
ASSERT_TRUE(sdslen(x) == 60 && memcmp(x, "--Hello Hi! World -9223372036854775808,"
|
||||
"9223372036854775807--",
|
||||
60) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew("--");
|
||||
x = sdscatfmt(x, "%u,%U--", UINT_MAX, ULLONG_MAX);
|
||||
/* sdscatfmt() seems working with unsigned numbers */
|
||||
ASSERT_TRUE(sdslen(x) == 35 && memcmp(x, "--4294967295,18446744073709551615--", 35) == 0);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew(" x ");
|
||||
sdstrim(x, " x");
|
||||
/* sdstrim() works when all chars match */
|
||||
ASSERT_EQ(sdslen(x), 0u);
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew(" x ");
|
||||
sdstrim(x, " ");
|
||||
/* sdstrim() works when a single char remains */
|
||||
ASSERT_TRUE(sdslen(x) == 1 && x[0] == 'x');
|
||||
|
||||
sdsfree(x);
|
||||
x = sdsnew("xxciaoyyy");
|
||||
sdstrim(x, "xy");
|
||||
/* sdstrim() correctly trims characters */
|
||||
ASSERT_TRUE(sdslen(x) == 4 && memcmp(x, "ciao\0", 5) == 0);
|
||||
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 1, 1);
|
||||
/* sdsrange(...,1,1) */
|
||||
ASSERT_TRUE(sdslen(y) == 1 && memcmp(y, "i\0", 2) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 1, -1);
|
||||
/* sdsrange(...,1,-1) */
|
||||
ASSERT_TRUE(sdslen(y) == 3 && memcmp(y, "iao\0", 4) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, -2, -1);
|
||||
/* sdsrange(...,-2,-1) */
|
||||
ASSERT_TRUE(sdslen(y) == 2 && memcmp(y, "ao\0", 3) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 2, 1);
|
||||
/* sdsrange(...,2,1) */
|
||||
ASSERT_TRUE(sdslen(y) == 0 && memcmp(y, "\0", 1) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 1, 100);
|
||||
/* sdsrange(...,1,100) */
|
||||
ASSERT_TRUE(sdslen(y) == 3 && memcmp(y, "iao\0", 4) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 100, 100);
|
||||
/* sdsrange(...,100,100) */
|
||||
ASSERT_TRUE(sdslen(y) == 0 && memcmp(y, "\0", 1) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 4, 6);
|
||||
/* sdsrange(...,4,6) */
|
||||
ASSERT_TRUE(sdslen(y) == 0 && memcmp(y, "\0", 1) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
y = sdsdup(x);
|
||||
sdsrange(y, 3, 6);
|
||||
/* sdsrange(...,3,6) */
|
||||
ASSERT_TRUE(sdslen(y) == 1 && memcmp(y, "o\0", 2) == 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnew("foo");
|
||||
y = sdsnew("foa");
|
||||
/* sdscmp(foo,foa) */
|
||||
ASSERT_GT(sdscmp(x, y), 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnew("bar");
|
||||
y = sdsnew("bar");
|
||||
/* sdscmp(bar,bar) */
|
||||
ASSERT_EQ(sdscmp(x, y), 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnew("aar");
|
||||
y = sdsnew("bar");
|
||||
/* sdscmp(bar,bar) */
|
||||
ASSERT_LT(sdscmp(x, y), 0);
|
||||
|
||||
sdsfree(y);
|
||||
sdsfree(x);
|
||||
x = sdsnewlen("\a\n\0foo\r", 7);
|
||||
y = sdscatrepr(sdsempty(), x, sdslen(x));
|
||||
/* sdscatrepr(...data...) */
|
||||
ASSERT_EQ(memcmp(y, "\"\\a\\n\\x00foo\\r\"", 15), 0);
|
||||
|
||||
unsigned int oldfree;
|
||||
char *p;
|
||||
int i;
|
||||
size_t step = 10, j;
|
||||
|
||||
sdsfree(x);
|
||||
sdsfree(y);
|
||||
x = sdsnew("0");
|
||||
/* sdsnew() free/len buffers */
|
||||
ASSERT_TRUE(sdslen(x) == 1 && sdsavail(x) == 0);
|
||||
|
||||
/* Run the test a few times in order to hit the first two
|
||||
* SDS header types. */
|
||||
for (i = 0; i < 10; i++) {
|
||||
size_t oldlen = sdslen(x);
|
||||
x = sdsMakeRoomFor(x, step);
|
||||
int type = x[-1] & SDS_TYPE_MASK;
|
||||
|
||||
/* sdsMakeRoomFor() len */
|
||||
ASSERT_EQ(sdslen(x), oldlen);
|
||||
if (type != SDS_TYPE_5) {
|
||||
/* sdsMakeRoomFor() free */
|
||||
ASSERT_GE(sdsavail(x), step);
|
||||
oldfree = sdsavail(x);
|
||||
UNUSED(oldfree);
|
||||
}
|
||||
p = x + oldlen;
|
||||
for (j = 0; j < step; j++) {
|
||||
p[j] = 'A' + j;
|
||||
}
|
||||
sdsIncrLen(x, step);
|
||||
}
|
||||
/* sdsMakeRoomFor() content */
|
||||
ASSERT_EQ(memcmp("0ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGH"
|
||||
"IJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ",
|
||||
x, 101),
|
||||
0);
|
||||
/* sdsMakeRoomFor() final length */
|
||||
ASSERT_EQ(sdslen(x), 101u);
|
||||
|
||||
sdsfree(x);
|
||||
|
||||
/* Simple template */
|
||||
x = sdstemplate("v1={variable1} v2={variable2}", sdsTestTemplateCallback, NULL);
|
||||
/* sdstemplate() normal flow */
|
||||
ASSERT_EQ(memcmp(x, "v1=value1 v2=value2", 19), 0);
|
||||
sdsfree(x);
|
||||
|
||||
/* Template with callback error */
|
||||
x = sdstemplate("v1={variable1} v3={doesnotexist}", sdsTestTemplateCallback, NULL);
|
||||
/* sdstemplate() with callback error */
|
||||
ASSERT_EQ(x, nullptr);
|
||||
|
||||
/* Template with empty var name */
|
||||
x = sdstemplate("v1={", sdsTestTemplateCallback, NULL);
|
||||
/* sdstemplate() with empty var name */
|
||||
ASSERT_EQ(x, nullptr);
|
||||
|
||||
/* Template with truncated var name */
|
||||
x = sdstemplate("v1={start", sdsTestTemplateCallback, NULL);
|
||||
/* sdstemplate() with truncated var name */
|
||||
ASSERT_EQ(x, nullptr);
|
||||
|
||||
/* Template with quoting */
|
||||
x = sdstemplate("v1={{{variable1}} {{} v2={variable2}", sdsTestTemplateCallback, NULL);
|
||||
/* sdstemplate() with quoting */
|
||||
ASSERT_EQ(memcmp(x, "v1={value1} {} v2=value2", 24), 0);
|
||||
sdsfree(x);
|
||||
|
||||
/* Test sdsResize - extend */
|
||||
x = sdsnew("1234567890123456789012345678901234567890");
|
||||
x = sdsResize(x, 200, 1);
|
||||
/* sdsResize() expand type */
|
||||
ASSERT_EQ(x[-1], SDS_TYPE_8);
|
||||
/* sdsResize() expand len */
|
||||
ASSERT_EQ(sdslen(x), 40u);
|
||||
/* sdsResize() expand strlen */
|
||||
ASSERT_EQ(strlen(x), 40u);
|
||||
/* Different allocator allocates at least as large as requested size,
|
||||
* to confirm the allocator won't waste too much,
|
||||
* we add a largest size checker here. */
|
||||
/* sdsResize() expand alloc */
|
||||
ASSERT_TRUE(sdsalloc(x) >= 200 && sdsalloc(x) < 400);
|
||||
|
||||
/* Test sdsResize - trim free space */
|
||||
x = sdsResize(x, 80, 1);
|
||||
/* sdsResize() shrink type */
|
||||
ASSERT_EQ(x[-1], SDS_TYPE_8);
|
||||
/* sdsResize() shrink len */
|
||||
ASSERT_EQ(sdslen(x), 40u);
|
||||
/* sdsResize() shrink strlen */
|
||||
ASSERT_EQ(strlen(x), 40u);
|
||||
/* sdsResize() shrink alloc */
|
||||
ASSERT_GE(sdsalloc(x), 80u);
|
||||
|
||||
/* Test sdsResize - crop used space */
|
||||
x = sdsResize(x, 30, 1);
|
||||
/* sdsResize() crop type */
|
||||
ASSERT_EQ(x[-1], SDS_TYPE_8);
|
||||
/* sdsResize() crop len */
|
||||
ASSERT_EQ(sdslen(x), 30u);
|
||||
/* sdsResize() crop strlen */
|
||||
ASSERT_EQ(strlen(x), 30u);
|
||||
/* sdsResize() crop alloc */
|
||||
ASSERT_GE(sdsalloc(x), 30u);
|
||||
|
||||
/* Test sdsResize - extend to different class */
|
||||
x = sdsResize(x, 400, 1);
|
||||
/* sdsResize() expand type */
|
||||
ASSERT_EQ(x[-1], SDS_TYPE_16);
|
||||
/* sdsResize() expand len */
|
||||
ASSERT_EQ(sdslen(x), 30u);
|
||||
/* sdsResize() expand strlen */
|
||||
ASSERT_EQ(strlen(x), 30u);
|
||||
/* sdsResize() expand alloc */
|
||||
ASSERT_GE(sdsalloc(x), 400u);
|
||||
|
||||
/* Test sdsResize - shrink to different class */
|
||||
x = sdsResize(x, 4, 1);
|
||||
/* sdsResize() crop type */
|
||||
ASSERT_EQ(x[-1], SDS_TYPE_8);
|
||||
/* sdsResize() crop len */
|
||||
ASSERT_EQ(sdslen(x), 4u);
|
||||
/* sdsResize() crop strlen */
|
||||
ASSERT_EQ(strlen(x), 4u);
|
||||
/* sdsResize() crop alloc */
|
||||
ASSERT_GE(sdsalloc(x), 4u);
|
||||
sdsfree(x);
|
||||
}
|
||||
|
||||
TEST_F(SdsTest, TestTypesAndAllocSize) {
|
||||
sds x = sdsnewlen(NULL, 31);
|
||||
/* len 31 type */
|
||||
ASSERT_EQ((x[-1] & SDS_TYPE_MASK), SDS_TYPE_5);
|
||||
/* len 31 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 32);
|
||||
/* len 32 type */
|
||||
ASSERT_GE((x[-1] & SDS_TYPE_MASK), SDS_TYPE_8);
|
||||
/* len 32 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 252);
|
||||
/* len 252 type */
|
||||
ASSERT_GE((x[-1] & SDS_TYPE_MASK), SDS_TYPE_8);
|
||||
/* len 252 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 253);
|
||||
/* len 253 type */
|
||||
ASSERT_EQ((x[-1] & SDS_TYPE_MASK), SDS_TYPE_16);
|
||||
/* len 253 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 65530);
|
||||
/* len 65530 type */
|
||||
ASSERT_GE((x[-1] & SDS_TYPE_MASK), SDS_TYPE_16);
|
||||
/* len 65530 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 65531);
|
||||
/* len 65531 type */
|
||||
ASSERT_GE((x[-1] & SDS_TYPE_MASK), SDS_TYPE_32);
|
||||
/* len 65531 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
#if (LONG_MAX == LLONG_MAX)
|
||||
if (large_memory) {
|
||||
x = sdsnewlen(NULL, 4294967286);
|
||||
/* len 4294967286 type */
|
||||
ASSERT_GE((x[-1] & SDS_TYPE_MASK), SDS_TYPE_32);
|
||||
/* len 4294967286 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
|
||||
x = sdsnewlen(NULL, 4294967287);
|
||||
/* len 4294967287 type */
|
||||
ASSERT_EQ((x[-1] & SDS_TYPE_MASK), SDS_TYPE_64);
|
||||
/* len 4294967287 sdsAllocSize */
|
||||
ASSERT_EQ(sdsAllocSize(x), s_malloc_usable_size(sdsAllocPtr(x)));
|
||||
sdsfree(x);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The test verifies that we can adjust SDS types if an allocator returned
|
||||
* larger buffer. The maximum length for type SDS_TYPE_X is
|
||||
* 2^X - header_size(SDS_TYPE_X) - 1. The maximum value to be stored in alloc
|
||||
* field is 2^X - 1. When allocated buffer is larger than
|
||||
* 2^X + header_size(SDS_TYPE_X), we "move" to a larger type SDS_TYPE_Y. To be
|
||||
* sure SDS_TYPE_Y header fits into 2^X + header_size(SDS_TYPE_X) + 1 bytes, the
|
||||
* difference between header sizes must be smaller than
|
||||
* header_size(SDS_TYPE_X) + 1.
|
||||
* We ignore SDS_TYPE_5 as it doesn't have alloc field. */
|
||||
TEST_F(SdsTest, TestSdsHeaderSizes) {
|
||||
/* can't always adjust SDS_TYPE_8 with SDS_TYPE_16 */
|
||||
ASSERT_LE(sizeof(struct sdshdr16), 2 * sizeof(struct sdshdr8) + 1);
|
||||
/* can't always adjust SDS_TYPE_16 with SDS_TYPE_32 */
|
||||
ASSERT_LE(sizeof(struct sdshdr32), 2 * sizeof(struct sdshdr16) + 1);
|
||||
#if (LONG_MAX == LLONG_MAX)
|
||||
/* can't always adjust SDS_TYPE_32 with SDS_TYPE_64 */
|
||||
ASSERT_LE(sizeof(struct sdshdr64), 2 * sizeof(struct sdshdr32) + 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(SdsTest, TestSdssplitargs) {
|
||||
int len;
|
||||
sds *sargv;
|
||||
|
||||
sargv = sdssplitargs("Testing one two three", &len);
|
||||
ASSERT_EQ(4, len);
|
||||
ASSERT_TRUE(!strcmp("Testing", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("one", sargv[1]));
|
||||
ASSERT_TRUE(!strcmp("two", sargv[2]));
|
||||
ASSERT_TRUE(!strcmp("three", sargv[3]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("", &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_NE(sargv, nullptr);
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"Testing split strings\" 'Another split string'", &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("Testing split strings", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("Another split string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"Hello\" ", &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("Hello", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
const char *binary_string = "\"\\x73\\x75\\x70\\x65\\x72\\x20\\x00\\x73\\x65\\x63\\x72\\x65\\x74\\x20\\x70\\x61\\x73\\x73\\x77\\x6f\\x72\\x64\"";
|
||||
sargv = sdssplitargs(binary_string, &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("super \x00secret password", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("unquoted", &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("unquoted", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("empty string \"\"", &len);
|
||||
ASSERT_EQ(3, len);
|
||||
ASSERT_TRUE(!strcmp("empty", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("string", sargv[1]));
|
||||
ASSERT_TRUE(!strcmp("", sargv[2]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"deeply\\\"quoted\" 's\\'t\\\"r'ing", &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("deeply\"quoted", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("s't\\\"ring", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("unquoted\" \"with' 'quotes string", &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("unquoted with quotes", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"quoted\"' another 'quoted string", &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("quoted another quoted", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"shell-like \"'\"'\"'\"' quote-escaping '", &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("shell-like \"' quote-escaping ", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdssplitargs("\"unterminated \"'single quotes", &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_EQ(sargv, nullptr);
|
||||
|
||||
sargv = sdssplitargs("'unterminated '\"double quotes", &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_EQ(sargv, nullptr);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(SdsTest, TestSdsnsplitargs) {
|
||||
int len;
|
||||
sds *sargv;
|
||||
const char *test_str;
|
||||
|
||||
// Test basic parameter splitting
|
||||
test_str = "Testing one two three";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(4, len);
|
||||
ASSERT_TRUE(!strcmp("Testing", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("one", sargv[1]));
|
||||
ASSERT_TRUE(!strcmp("two", sargv[2]));
|
||||
ASSERT_TRUE(!strcmp("three", sargv[3]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test empty string
|
||||
sargv = sdsnsplitargs("", 0, &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_NE(sargv, nullptr);
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test quoted strings
|
||||
test_str = "\"Testing split strings\" 'Another split string'";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("Testing split strings", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("Another split string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test trailing space after quoted string
|
||||
test_str = "\"Hello\" ";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("Hello", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test binary string with null character using \x escape
|
||||
test_str = "\"\\x73\\x75\\x70\\x65\\x72\\x20\\x00\\x73\\x65\\x63\\x72\\x65\\x74\\x20\\x70\\x61\\x73\\x73\\x77\\x6f\\x72\\x64\"";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("super \x00secret password", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
char str_with_null[] = "test\0null";
|
||||
sargv = sdsnsplitargs(str_with_null, sizeof(str_with_null) - 1, &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!memcmp("test", sargv[0], 4));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test single unquoted string
|
||||
test_str = "unquoted";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("unquoted", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test empty quoted string
|
||||
test_str = "empty string \"\"";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(3, len);
|
||||
ASSERT_TRUE(!strcmp("empty", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("string", sargv[1]));
|
||||
ASSERT_TRUE(!strcmp("", sargv[2]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test escaped quotes
|
||||
test_str = "\"deeply\\\"quoted\" 's\\'t\\\"r'ing";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("deeply\"quoted", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("s't\\\"ring", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test mixed quoted and unquoted parts
|
||||
test_str = "unquoted\" \"with' 'quotes string";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("unquoted with quotes", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test concatenated quoted strings
|
||||
test_str = "\"quoted\"' another 'quoted string";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("quoted another quoted", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("string", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test complex quote escaping
|
||||
test_str = "\"shell-like \"'\"'\"'\"' quote-escaping '";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("shell-like \"' quote-escaping ", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test unterminated double quote
|
||||
test_str = "\"unterminated \"'single quotes";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_EQ(sargv, nullptr);
|
||||
|
||||
// Test unterminated single quote
|
||||
test_str = "'unterminated '\"double quotes";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_EQ(sargv, nullptr);
|
||||
|
||||
// Test partial string length (truncated input)
|
||||
test_str = "Testing one two three";
|
||||
sargv = sdsnsplitargs(test_str, 8, &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!strcmp("Testing", sargv[0]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with exact length (no truncation)
|
||||
test_str = "Exact length";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("Exact", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("length", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with leading spaces
|
||||
test_str = " leading spaces";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("leading", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("spaces", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with trailing spaces
|
||||
test_str = "trailing spaces ";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("trailing", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("spaces", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with consecutive spaces
|
||||
test_str = "multiple spaces";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("multiple", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("spaces", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string with only spaces
|
||||
test_str = " ";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(0, len);
|
||||
ASSERT_NE(sargv, nullptr);
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test string containing null character in the middle of parsing
|
||||
char str_with_null_in_middle[] = "arg1\0arg2 arg3";
|
||||
sargv = sdsnsplitargs(str_with_null_in_middle, sizeof(str_with_null_in_middle) - 1, &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_TRUE(!memcmp("arg1", sargv[0], 4));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test very long single argument
|
||||
char long_arg[1024];
|
||||
memset(long_arg, 'a', sizeof(long_arg) - 1);
|
||||
long_arg[sizeof(long_arg) - 1] = '\0';
|
||||
sargv = sdsnsplitargs(long_arg, sizeof(long_arg) - 1, &len);
|
||||
ASSERT_EQ(1, len);
|
||||
ASSERT_EQ(strlen(sargv[0]), sizeof(long_arg) - 1);
|
||||
ASSERT_TRUE(!memcmp(long_arg, sargv[0], sizeof(long_arg) - 1));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test mixed quote types in one argument
|
||||
test_str = "\"double'quotes\" 'single\"quotes'";
|
||||
sargv = sdsnsplitargs(test_str, strlen(test_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("double'quotes", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("single\"quotes", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
// Test mixed quote types with different lengths
|
||||
sds complex_str = sdsnew("\"double'quotes\" 'single\"quotes'");
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str) - 1, &len);
|
||||
ASSERT_EQ(0, len);
|
||||
|
||||
complex_str = sdscatlen(complex_str, "\0", 1);
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str) - 1, &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("double'quotes", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("single\"quotes", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str), &len);
|
||||
ASSERT_EQ(2, len);
|
||||
ASSERT_TRUE(!strcmp("double'quotes", sargv[0]));
|
||||
ASSERT_TRUE(!strcmp("single\"quotes", sargv[1]));
|
||||
sdsfreesplitres(sargv, len);
|
||||
|
||||
sargv = sdsnsplitargs(complex_str, sdslen(complex_str) - 2, &len);
|
||||
ASSERT_EQ(0, len);
|
||||
sdsfree(complex_str);
|
||||
}
|
||||
|
||||
/* This test is disabled by default because it takes a long time to run (1M iterations).
|
||||
* It's used for performance comparison between sdsnsplitargs and sdssplitargs.
|
||||
* To run this test explicitly, use:
|
||||
* ./src/unit/valkey-unit-gtests --gtest_filter=SdsTest.DISABLED_TestSdsnsplitargsBenchmark --gtest_also_run_disabled_tests */
|
||||
TEST_F(SdsTest, DISABLED_TestSdsnsplitargsBenchmark) {
|
||||
char str_with_null_in_middle[] = "arg1\0arg2 arg3";
|
||||
size_t str_len = sizeof(str_with_null_in_middle) - 1;
|
||||
int len = 0;
|
||||
|
||||
long long start = ustime();
|
||||
for (int i = 0; i < 1000000; i++) {
|
||||
sds *sargv = sdsnsplitargs(str_with_null_in_middle, str_len, &len);
|
||||
sdsfreesplitres(sargv, len);
|
||||
}
|
||||
printf("sdsnsplitargs 1000000 times: %f\n", (double)(ustime() - start) / 1000000);
|
||||
|
||||
start = ustime();
|
||||
for (int i = 0; i < 1000000; i++) {
|
||||
sds str = sdsnewlen(str_with_null_in_middle, str_len);
|
||||
sds *sargv = sdssplitargs(str, &len);
|
||||
sdsfreesplitres(sargv, len);
|
||||
sdsfree(str);
|
||||
}
|
||||
printf("sdssplitargs 1000000 times: %f\n", (double)(ustime() - start) / 1000000);
|
||||
}
|
||||
@@ -1,25 +1,33 @@
|
||||
#include "../sha1.c"
|
||||
#include "test_help.h"
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "sha1.h"
|
||||
}
|
||||
|
||||
#define BUFSIZE 4096
|
||||
|
||||
int test_sha1(int argc, char **argv, int flags) {
|
||||
class Sha1Test : public ::testing::Test {};
|
||||
|
||||
TEST_F(Sha1Test, TestSha1) {
|
||||
SHA1_CTX ctx;
|
||||
unsigned char hash[20], buf[BUFSIZE];
|
||||
unsigned char expected[20] = {0x15, 0xdd, 0x99, 0xa1, 0x99, 0x1e, 0x0b, 0x38, 0x26, 0xfe,
|
||||
0xde, 0x3d, 0xef, 0xfc, 0x1f, 0xeb, 0xa4, 0x22, 0x78, 0xe6};
|
||||
int i;
|
||||
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
for (i = 0; i < BUFSIZE; i++) buf[i] = i;
|
||||
|
||||
SHA1Init(&ctx);
|
||||
for (i = 0; i < 1000; i++) SHA1Update(&ctx, buf, BUFSIZE);
|
||||
SHA1Final(hash, &ctx);
|
||||
|
||||
TEST_ASSERT(memcmp(hash, expected, 20) == 0);
|
||||
return 0;
|
||||
ASSERT_EQ(memcmp(hash, expected, 20), 0);
|
||||
}
|
||||
@@ -1,89 +1,86 @@
|
||||
/*
|
||||
* Comprehensive unit tests for SHA-256 implementation.
|
||||
*
|
||||
* These tests verify:
|
||||
* 1. Basic functionality with known test vectors (e.g., "abc")
|
||||
* 2. Handling of large input data (4KB repeated 1000 times)
|
||||
* 3. Edge case with repeated single-byte input (1 million 'a' characters)
|
||||
*
|
||||
* The tests ensure compatibility with standard SHA-256 implementations
|
||||
* and will help detect regressions during future code changes.
|
||||
*/
|
||||
|
||||
#include "../sha256.h"
|
||||
#include "test_help.h"
|
||||
#include <string.h>
|
||||
|
||||
#define BUFSIZE 4096
|
||||
|
||||
int test_sha256_abc(int argc, char **argv, int flags) {
|
||||
SHA256_CTX ctx;
|
||||
BYTE hash[32];
|
||||
BYTE expected[32] = {
|
||||
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
|
||||
0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
|
||||
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
|
||||
0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad};
|
||||
const char *test_str = "abc";
|
||||
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, (BYTE *)test_str, 3);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
TEST_ASSERT(memcmp(hash, expected, 32) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_sha256_large(int argc, char **argv, int flags) {
|
||||
SHA256_CTX ctx;
|
||||
BYTE hash[32], buf[BUFSIZE];
|
||||
BYTE expected[32] = {
|
||||
0x8e, 0x44, 0xff, 0x94, 0xb6, 0x8f, 0xcb, 0x09,
|
||||
0x6a, 0x8d, 0x5c, 0xdb, 0x8f, 0x1c, 0xc7, 0x8a,
|
||||
0x9c, 0x47, 0x58, 0x45, 0xf1, 0x1a, 0x8d, 0x67,
|
||||
0x6f, 0x39, 0xc9, 0x53, 0x7e, 0xd2, 0x31, 0xe0};
|
||||
int i;
|
||||
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
for (i = 0; i < BUFSIZE; i++)
|
||||
buf[i] = i % 256;
|
||||
|
||||
sha256_init(&ctx);
|
||||
for (i = 0; i < 1000; i++)
|
||||
sha256_update(&ctx, buf, BUFSIZE);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
TEST_ASSERT(memcmp(hash, expected, 32) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_sha256_million_a(int argc, char **argv, int flags) {
|
||||
SHA256_CTX ctx;
|
||||
BYTE hash[32];
|
||||
BYTE expected[32] = {
|
||||
0xcd, 0xc7, 0x6e, 0x5c, 0x99, 0x14, 0xfb, 0x92,
|
||||
0x81, 0xa1, 0xc7, 0xe2, 0x84, 0xd7, 0x3e, 0x67,
|
||||
0xf1, 0x80, 0x9a, 0x48, 0xa4, 0x97, 0x20, 0x0e,
|
||||
0x04, 0x6d, 0x39, 0xcc, 0xc7, 0x11, 0x2c, 0xd0};
|
||||
int i;
|
||||
BYTE a = 'a';
|
||||
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
sha256_init(&ctx);
|
||||
for (i = 0; i < 1000000; i++)
|
||||
sha256_update(&ctx, &a, 1);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
TEST_ASSERT(memcmp(hash, expected, 32) == 0);
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
/*
|
||||
* Comprehensive unit tests for SHA-256 implementation.
|
||||
*
|
||||
* These tests verify:
|
||||
* 1. Basic functionality with known test vectors (e.g., "abc")
|
||||
* 2. Handling of large input data (4KB repeated 1000 times)
|
||||
* 3. Edge case with repeated single-byte input (1 million 'a' characters)
|
||||
*
|
||||
* The tests ensure compatibility with standard SHA-256 implementations
|
||||
* and will help detect regressions during future code changes.
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "sha256.h"
|
||||
}
|
||||
|
||||
#define BUFSIZE 4096
|
||||
|
||||
class Sha256Test : public ::testing::Test {};
|
||||
|
||||
TEST_F(Sha256Test, TestSha256Abc) {
|
||||
SHA256_CTX ctx;
|
||||
BYTE hash[32];
|
||||
BYTE expected[32] = {
|
||||
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
|
||||
0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
|
||||
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
|
||||
0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad};
|
||||
const char *test_str = "abc";
|
||||
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, (BYTE *)test_str, 3);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
ASSERT_EQ(memcmp(hash, expected, 32), 0);
|
||||
}
|
||||
|
||||
TEST_F(Sha256Test, TestSha256Large) {
|
||||
SHA256_CTX ctx;
|
||||
BYTE hash[32], buf[BUFSIZE];
|
||||
BYTE expected[32] = {
|
||||
0x8e, 0x44, 0xff, 0x94, 0xb6, 0x8f, 0xcb, 0x09,
|
||||
0x6a, 0x8d, 0x5c, 0xdb, 0x8f, 0x1c, 0xc7, 0x8a,
|
||||
0x9c, 0x47, 0x58, 0x45, 0xf1, 0x1a, 0x8d, 0x67,
|
||||
0x6f, 0x39, 0xc9, 0x53, 0x7e, 0xd2, 0x31, 0xe0};
|
||||
int i;
|
||||
|
||||
for (i = 0; i < BUFSIZE; i++)
|
||||
buf[i] = i % 256;
|
||||
|
||||
sha256_init(&ctx);
|
||||
for (i = 0; i < 1000; i++)
|
||||
sha256_update(&ctx, buf, BUFSIZE);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
ASSERT_EQ(memcmp(hash, expected, 32), 0);
|
||||
}
|
||||
|
||||
TEST_F(Sha256Test, TestSha256MillionA) {
|
||||
SHA256_CTX ctx;
|
||||
BYTE hash[32];
|
||||
BYTE expected[32] = {
|
||||
0xcd, 0xc7, 0x6e, 0x5c, 0x99, 0x14, 0xfb, 0x92,
|
||||
0x81, 0xa1, 0xc7, 0xe2, 0x84, 0xd7, 0x3e, 0x67,
|
||||
0xf1, 0x80, 0x9a, 0x48, 0xa4, 0x97, 0x20, 0x0e,
|
||||
0x04, 0x6d, 0x39, 0xcc, 0xc7, 0x11, 0x2c, 0xd0};
|
||||
int i;
|
||||
BYTE a = 'a';
|
||||
|
||||
sha256_init(&ctx);
|
||||
for (i = 0; i < 1000000; i++)
|
||||
sha256_update(&ctx, &a, 1);
|
||||
sha256_final(&ctx, hash);
|
||||
|
||||
ASSERT_EQ(memcmp(hash, expected, 32), 0);
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
#include "../fmacros.h"
|
||||
#include <sys/mman.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "../config.h"
|
||||
#include "../util.h"
|
||||
#include "test_help.h"
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <sys/statfs.h>
|
||||
#include <linux/magic.h>
|
||||
#endif
|
||||
|
||||
int test_string2ll(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
char buf[32];
|
||||
long long v;
|
||||
|
||||
/* May not start with +. */
|
||||
valkey_strlcpy(buf, "+1", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
/* Leading space. */
|
||||
valkey_strlcpy(buf, " 1", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
/* Trailing space. */
|
||||
valkey_strlcpy(buf, "1 ", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
/* May not start with 0. */
|
||||
valkey_strlcpy(buf, "01", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
valkey_strlcpy(buf, "-1", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == -1);
|
||||
|
||||
valkey_strlcpy(buf, "0", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == 0);
|
||||
|
||||
valkey_strlcpy(buf, "1", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == 1);
|
||||
|
||||
valkey_strlcpy(buf, "99", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == 99);
|
||||
|
||||
valkey_strlcpy(buf, "-99", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == -99);
|
||||
|
||||
valkey_strlcpy(buf, "-9223372036854775808", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == LLONG_MIN);
|
||||
|
||||
valkey_strlcpy(buf, "-9223372036854775809", sizeof(buf)); /* overflow */
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
valkey_strlcpy(buf, "9223372036854775807", sizeof(buf));
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == LLONG_MAX);
|
||||
|
||||
valkey_strlcpy(buf, "9223372036854775808", sizeof(buf)); /* overflow */
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
valkey_strlcpy(buf, "18446744073709551615", sizeof(buf)); /* overflow */
|
||||
TEST_ASSERT(string2ll(buf, strlen(buf), &v) == 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_string2l(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
char buf[32];
|
||||
long v;
|
||||
|
||||
/* May not start with +. */
|
||||
valkey_strlcpy(buf, "+1", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 0);
|
||||
|
||||
/* May not start with 0. */
|
||||
valkey_strlcpy(buf, "01", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 0);
|
||||
|
||||
valkey_strlcpy(buf, "-1", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == -1);
|
||||
|
||||
valkey_strlcpy(buf, "0", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == 0);
|
||||
|
||||
valkey_strlcpy(buf, "1", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == 1);
|
||||
|
||||
valkey_strlcpy(buf, "99", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == 99);
|
||||
|
||||
valkey_strlcpy(buf, "-99", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == -99);
|
||||
|
||||
#if LONG_MAX != LLONG_MAX
|
||||
valkey_strlcpy(buf, "-2147483648", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == LONG_MIN);
|
||||
|
||||
valkey_strlcpy(buf, "-2147483649", sizeof(buf)); /* overflow */
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 0);
|
||||
|
||||
valkey_strlcpy(buf, "2147483647", sizeof(buf));
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 1);
|
||||
TEST_ASSERT(v == LONG_MAX);
|
||||
|
||||
valkey_strlcpy(buf, "2147483648", sizeof(buf)); /* overflow */
|
||||
TEST_ASSERT(string2l(buf, strlen(buf), &v) == 0);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_ll2string(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
char buf[32];
|
||||
long long v;
|
||||
int sz;
|
||||
|
||||
v = 0;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 1);
|
||||
TEST_ASSERT(!strcmp(buf, "0"));
|
||||
|
||||
v = -1;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 2);
|
||||
TEST_ASSERT(!strcmp(buf, "-1"));
|
||||
|
||||
v = 99;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 2);
|
||||
TEST_ASSERT(!strcmp(buf, "99"));
|
||||
|
||||
v = -99;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 3);
|
||||
TEST_ASSERT(!strcmp(buf, "-99"));
|
||||
|
||||
v = -2147483648;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 11);
|
||||
TEST_ASSERT(!strcmp(buf, "-2147483648"));
|
||||
|
||||
v = LLONG_MIN;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 20);
|
||||
TEST_ASSERT(!strcmp(buf, "-9223372036854775808"));
|
||||
|
||||
v = LLONG_MAX;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
TEST_ASSERT(sz == 19);
|
||||
TEST_ASSERT(!strcmp(buf, "9223372036854775807"));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_ld2string(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
char buf[32];
|
||||
long double v;
|
||||
int sz;
|
||||
|
||||
v = 0.0 / 0.0;
|
||||
sz = ld2string(buf, sizeof(buf), v, LD_STR_AUTO);
|
||||
TEST_ASSERT(sz == 3);
|
||||
TEST_ASSERT(!strcmp(buf, "nan"));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_fixedpoint_d2string(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
char buf[32];
|
||||
double v;
|
||||
int sz;
|
||||
v = 0.0;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
TEST_ASSERT(sz == 6);
|
||||
TEST_ASSERT(!strcmp(buf, "0.0000"));
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
TEST_ASSERT(sz == 3);
|
||||
TEST_ASSERT(!strcmp(buf, "0.0"));
|
||||
/* set junk in buffer */
|
||||
memset(buf, 'A', 32);
|
||||
v = 0.0001;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
TEST_ASSERT(sz == 6);
|
||||
TEST_ASSERT(buf[sz] == '\0');
|
||||
TEST_ASSERT(!strcmp(buf, "0.0001"));
|
||||
/* set junk in buffer */
|
||||
memset(buf, 'A', 32);
|
||||
v = 6.0642951598391699e-05;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
TEST_ASSERT(sz == 6);
|
||||
TEST_ASSERT(buf[sz] == '\0');
|
||||
TEST_ASSERT(!strcmp(buf, "0.0001"));
|
||||
v = 0.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
TEST_ASSERT(sz == 6);
|
||||
TEST_ASSERT(!strcmp(buf, "0.0100"));
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
TEST_ASSERT(sz == 3);
|
||||
TEST_ASSERT(!strcmp(buf, "0.0"));
|
||||
v = -0.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
TEST_ASSERT(sz == 7);
|
||||
TEST_ASSERT(!strcmp(buf, "-0.0100"));
|
||||
v = -0.1;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
TEST_ASSERT(sz == 4);
|
||||
TEST_ASSERT(!strcmp(buf, "-0.1"));
|
||||
v = 0.1;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
TEST_ASSERT(sz == 3);
|
||||
TEST_ASSERT(!strcmp(buf, "0.1"));
|
||||
v = 0.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 17);
|
||||
TEST_ASSERT(sz == 19);
|
||||
TEST_ASSERT(!strcmp(buf, "0.01000000000000000"));
|
||||
v = 10.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
TEST_ASSERT(sz == 7);
|
||||
TEST_ASSERT(!strcmp(buf, "10.0100"));
|
||||
/* negative tests */
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 18);
|
||||
TEST_ASSERT(sz == 0);
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 0);
|
||||
TEST_ASSERT(sz == 0);
|
||||
sz = fixedpoint_d2string(buf, 1, v, 1);
|
||||
TEST_ASSERT(sz == 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_version2num(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
TEST_ASSERT(version2num("7.2.5") == 0x070205);
|
||||
TEST_ASSERT(version2num("255.255.255") == 0xffffff);
|
||||
TEST_ASSERT(version2num("7.2.256") == -1);
|
||||
TEST_ASSERT(version2num("7.2") == -1);
|
||||
TEST_ASSERT(version2num("7.2.1.0") == -1);
|
||||
TEST_ASSERT(version2num("1.-2.-3") == -1);
|
||||
TEST_ASSERT(version2num("1.2.3-rc4") == -1);
|
||||
TEST_ASSERT(version2num("") == -1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
/* Since fadvise and mincore is only supported in specific platforms like
|
||||
* Linux, we only verify the fadvise mechanism works in Linux */
|
||||
static int cache_exist(int fd) {
|
||||
unsigned char flag;
|
||||
void *m = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
|
||||
TEST_ASSERT(m);
|
||||
TEST_ASSERT(mincore(m, 4096, &flag) == 0);
|
||||
munmap(m, 4096);
|
||||
/* the least significant bit of the byte will be set if the corresponding
|
||||
* page is currently resident in memory */
|
||||
return flag & 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int test_reclaimFilePageCache(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
|
||||
/* The test is incompatible with valgrind, skip it. */
|
||||
if (flags & UNIT_TEST_VALGRIND) return 0;
|
||||
|
||||
#if defined(__linux__)
|
||||
struct statfs stats;
|
||||
|
||||
/* Check if /tmp is memory-backed (e.g., tmpfs) */
|
||||
if (statfs("/tmp", &stats) == 0) {
|
||||
if (stats.f_type != TMPFS_MAGIC) { // Not tmpfs, use /tmp
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
char *tmpfile = "/tmp/redis-reclaim-cache-test";
|
||||
int fd = open(tmpfile, O_RDWR | O_CREAT, 0644);
|
||||
TEST_ASSERT(fd >= 0);
|
||||
|
||||
/* test write file */
|
||||
char buf[4] = "foo";
|
||||
TEST_ASSERT(write(fd, buf, sizeof(buf)) > 0);
|
||||
TEST_ASSERT(cache_exist(fd));
|
||||
TEST_ASSERT(valkey_fsync(fd) == 0);
|
||||
TEST_ASSERT(reclaimFilePageCache(fd, 0, 0) == 0);
|
||||
TEST_ASSERT(!cache_exist(fd));
|
||||
|
||||
/* test read file */
|
||||
TEST_ASSERT(pread(fd, buf, sizeof(buf), 0) > 0);
|
||||
TEST_ASSERT(cache_exist(fd));
|
||||
TEST_ASSERT(reclaimFilePageCache(fd, 0, 0) == 0);
|
||||
TEST_ASSERT(!cache_exist(fd));
|
||||
|
||||
unlink(tmpfile);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_writePointerWithPadding(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
unsigned char buf[8];
|
||||
static int dummy;
|
||||
void *ptr = &dummy;
|
||||
size_t ptr_size = sizeof(ptr);
|
||||
|
||||
/* Write the pointer and pad to 8 bytes */
|
||||
writePointerWithPadding(buf, ptr);
|
||||
|
||||
/* The first ptr_size bytes must match the raw pointer bytes */
|
||||
unsigned char expected[sizeof(ptr)];
|
||||
memcpy(expected, &ptr, ptr_size);
|
||||
TEST_ASSERT(memcmp(buf, expected, ptr_size) == 0);
|
||||
|
||||
|
||||
/* The remaining bytes (if any) must be zero */
|
||||
for (size_t i = ptr_size; i < sizeof(buf); i++) {
|
||||
TEST_ASSERT(buf[i] == 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" {
|
||||
#include "config.h"
|
||||
#include "fmacros.h"
|
||||
#include "util.h"
|
||||
|
||||
extern bool valgrind;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <linux/magic.h>
|
||||
#include <sys/statfs.h>
|
||||
#endif
|
||||
|
||||
class UtilTest : public ::testing::Test {};
|
||||
|
||||
TEST_F(UtilTest, TestString2ll) {
|
||||
char buf[32];
|
||||
long long v;
|
||||
|
||||
/* May not start with +. */
|
||||
valkey_strlcpy(buf, "+1", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
|
||||
/* Leading space. */
|
||||
valkey_strlcpy(buf, " 1", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
|
||||
/* Trailing space. */
|
||||
valkey_strlcpy(buf, "1 ", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
|
||||
/* May not start with 0. */
|
||||
valkey_strlcpy(buf, "01", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
|
||||
valkey_strlcpy(buf, "-1", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, -1);
|
||||
|
||||
valkey_strlcpy(buf, "0", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, 0);
|
||||
|
||||
valkey_strlcpy(buf, "1", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, 1);
|
||||
|
||||
valkey_strlcpy(buf, "99", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, 99);
|
||||
|
||||
valkey_strlcpy(buf, "-99", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, -99);
|
||||
|
||||
valkey_strlcpy(buf, "-9223372036854775808", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, LLONG_MIN);
|
||||
|
||||
valkey_strlcpy(buf, "-9223372036854775809", sizeof(buf)); /* overflow */
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
|
||||
valkey_strlcpy(buf, "9223372036854775807", sizeof(buf));
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, LLONG_MAX);
|
||||
|
||||
valkey_strlcpy(buf, "9223372036854775808", sizeof(buf)); /* overflow */
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
|
||||
valkey_strlcpy(buf, "18446744073709551615", sizeof(buf)); /* overflow */
|
||||
ASSERT_EQ(string2ll(buf, strlen(buf), &v), 0);
|
||||
}
|
||||
|
||||
TEST_F(UtilTest, TestString2l) {
|
||||
char buf[32];
|
||||
long v;
|
||||
|
||||
/* May not start with +. */
|
||||
valkey_strlcpy(buf, "+1", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 0);
|
||||
|
||||
/* May not start with 0. */
|
||||
valkey_strlcpy(buf, "01", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 0);
|
||||
|
||||
valkey_strlcpy(buf, "-1", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, -1);
|
||||
|
||||
valkey_strlcpy(buf, "0", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, 0);
|
||||
|
||||
valkey_strlcpy(buf, "1", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, 1);
|
||||
|
||||
valkey_strlcpy(buf, "99", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, 99);
|
||||
|
||||
valkey_strlcpy(buf, "-99", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, -99);
|
||||
|
||||
#if LONG_MAX != LLONG_MAX
|
||||
valkey_strlcpy(buf, "-2147483648", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, LONG_MIN);
|
||||
|
||||
valkey_strlcpy(buf, "-2147483649", sizeof(buf)); /* overflow */
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 0);
|
||||
|
||||
valkey_strlcpy(buf, "2147483647", sizeof(buf));
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 1);
|
||||
ASSERT_EQ(v, LONG_MAX);
|
||||
|
||||
valkey_strlcpy(buf, "2147483648", sizeof(buf)); /* overflow */
|
||||
ASSERT_EQ(string2l(buf, strlen(buf), &v), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(UtilTest, TestLl2string) {
|
||||
char buf[32];
|
||||
long long v;
|
||||
int sz;
|
||||
|
||||
v = 0;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 1);
|
||||
ASSERT_TRUE(!strcmp(buf, "0"));
|
||||
|
||||
v = -1;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 2);
|
||||
ASSERT_TRUE(!strcmp(buf, "-1"));
|
||||
|
||||
v = 99;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 2);
|
||||
ASSERT_TRUE(!strcmp(buf, "99"));
|
||||
|
||||
v = -99;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 3);
|
||||
ASSERT_TRUE(!strcmp(buf, "-99"));
|
||||
|
||||
v = -2147483648;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 11);
|
||||
ASSERT_TRUE(!strcmp(buf, "-2147483648"));
|
||||
|
||||
v = LLONG_MIN;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 20);
|
||||
ASSERT_TRUE(!strcmp(buf, "-9223372036854775808"));
|
||||
|
||||
v = LLONG_MAX;
|
||||
sz = ll2string(buf, sizeof buf, v);
|
||||
ASSERT_EQ(sz, 19);
|
||||
ASSERT_TRUE(!strcmp(buf, "9223372036854775807"));
|
||||
}
|
||||
|
||||
TEST_F(UtilTest, TestLd2string) {
|
||||
char buf[32];
|
||||
long double v;
|
||||
int sz;
|
||||
|
||||
v = 0.0 / 0.0;
|
||||
sz = ld2string(buf, sizeof(buf), v, LD_STR_AUTO);
|
||||
ASSERT_EQ(sz, 3);
|
||||
ASSERT_TRUE(!strcmp(buf, "nan"));
|
||||
}
|
||||
|
||||
TEST_F(UtilTest, TestFixedpointD2string) {
|
||||
char buf[32];
|
||||
double v;
|
||||
int sz;
|
||||
|
||||
v = 0.0;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
ASSERT_EQ(sz, 6);
|
||||
ASSERT_TRUE(!strcmp(buf, "0.0000"));
|
||||
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
ASSERT_EQ(sz, 3);
|
||||
ASSERT_TRUE(!strcmp(buf, "0.0"));
|
||||
|
||||
/* set junk in buffer */
|
||||
memset(buf, 'A', 32);
|
||||
v = 0.0001;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
ASSERT_EQ(sz, 6);
|
||||
ASSERT_EQ(buf[sz], '\0');
|
||||
ASSERT_TRUE(!strcmp(buf, "0.0001"));
|
||||
|
||||
/* set junk in buffer */
|
||||
memset(buf, 'A', 32);
|
||||
v = 6.0642951598391699e-05;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
ASSERT_EQ(sz, 6);
|
||||
ASSERT_EQ(buf[sz], '\0');
|
||||
ASSERT_TRUE(!strcmp(buf, "0.0001"));
|
||||
|
||||
v = 0.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
ASSERT_EQ(sz, 6);
|
||||
ASSERT_TRUE(!strcmp(buf, "0.0100"));
|
||||
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
ASSERT_EQ(sz, 3);
|
||||
ASSERT_TRUE(!strcmp(buf, "0.0"));
|
||||
|
||||
v = -0.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
ASSERT_EQ(sz, 7);
|
||||
ASSERT_TRUE(!strcmp(buf, "-0.0100"));
|
||||
|
||||
v = -0.1;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
ASSERT_EQ(sz, 4);
|
||||
ASSERT_TRUE(!strcmp(buf, "-0.1"));
|
||||
|
||||
v = 0.1;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 1);
|
||||
ASSERT_EQ(sz, 3);
|
||||
ASSERT_TRUE(!strcmp(buf, "0.1"));
|
||||
|
||||
v = 0.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 17);
|
||||
ASSERT_EQ(sz, 19);
|
||||
ASSERT_TRUE(!strcmp(buf, "0.01000000000000000"));
|
||||
|
||||
v = 10.01;
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 4);
|
||||
ASSERT_EQ(sz, 7);
|
||||
ASSERT_TRUE(!strcmp(buf, "10.0100"));
|
||||
|
||||
/* negative tests */
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 18);
|
||||
ASSERT_EQ(sz, 0);
|
||||
|
||||
sz = fixedpoint_d2string(buf, sizeof buf, v, 0);
|
||||
ASSERT_EQ(sz, 0);
|
||||
|
||||
sz = fixedpoint_d2string(buf, 1, v, 1);
|
||||
ASSERT_EQ(sz, 0);
|
||||
}
|
||||
|
||||
TEST_F(UtilTest, TestVersion2num) {
|
||||
ASSERT_EQ(version2num("7.2.5"), 0x070205);
|
||||
ASSERT_EQ(version2num("255.255.255"), 0xffffff);
|
||||
ASSERT_EQ(version2num("7.2.256"), -1);
|
||||
ASSERT_EQ(version2num("7.2"), -1);
|
||||
ASSERT_EQ(version2num("7.2.1.0"), -1);
|
||||
ASSERT_EQ(version2num("1.-2.-3"), -1);
|
||||
ASSERT_EQ(version2num("1.2.3-rc4"), -1);
|
||||
ASSERT_EQ(version2num(""), -1);
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
/* Since fadvise and mincore is only supported in specific platforms like
|
||||
* Linux, we only verify the fadvise mechanism works in Linux */
|
||||
static int cache_exist(int fd) {
|
||||
unsigned char flag;
|
||||
void *m = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
|
||||
if (m == MAP_FAILED) return -1;
|
||||
if (mincore(m, 4096, &flag) != 0) {
|
||||
munmap(m, 4096);
|
||||
return -1;
|
||||
}
|
||||
munmap(m, 4096);
|
||||
/* the least significant bit of the byte will be set if the corresponding
|
||||
* page is currently resident in memory */
|
||||
return flag & 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(UtilTest, TestReclaimFilePageCache) {
|
||||
/* The test is incompatible with valgrind, skip it. */
|
||||
if (valgrind) GTEST_SKIP() << "Skipping test due to incompatibility with valgrind";
|
||||
|
||||
#if defined(__linux__)
|
||||
struct statfs stats;
|
||||
|
||||
/* Check if /tmp is memory-backed (e.g., tmpfs) */
|
||||
if (statfs("/tmp", &stats) == 0) {
|
||||
if (stats.f_type != TMPFS_MAGIC) { // Not tmpfs, use /tmp
|
||||
GTEST_SKIP() << "Skipping test because /tmp is not tmpfs";
|
||||
}
|
||||
}
|
||||
|
||||
const char *tmpfile = "/tmp/redis-reclaim-cache-test";
|
||||
int fd = open(tmpfile, O_RDWR | O_CREAT, 0644);
|
||||
ASSERT_GE(fd, 0);
|
||||
|
||||
/* test write file */
|
||||
char buf[4] = "foo";
|
||||
ASSERT_GT(write(fd, buf, sizeof(buf)), 0);
|
||||
ASSERT_TRUE(cache_exist(fd));
|
||||
ASSERT_EQ(valkey_fsync(fd), 0);
|
||||
ASSERT_EQ(reclaimFilePageCache(fd, 0, 0), 0);
|
||||
ASSERT_TRUE(!cache_exist(fd));
|
||||
|
||||
/* test read file */
|
||||
ASSERT_GT(pread(fd, buf, sizeof(buf), 0), 0);
|
||||
ASSERT_TRUE(cache_exist(fd));
|
||||
ASSERT_EQ(reclaimFilePageCache(fd, 0, 0), 0);
|
||||
ASSERT_TRUE(!cache_exist(fd));
|
||||
|
||||
close(fd);
|
||||
unlink(tmpfile);
|
||||
#else
|
||||
GTEST_SKIP() << "Test only supported on Linux";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(UtilTest, TestWritePointerWithPadding) {
|
||||
unsigned char buf[8];
|
||||
static int dummy;
|
||||
void *ptr = &dummy;
|
||||
size_t ptr_size = sizeof(ptr);
|
||||
|
||||
/* Write the pointer and pad to 8 bytes */
|
||||
writePointerWithPadding(buf, ptr);
|
||||
|
||||
/* The first ptr_size bytes must match the raw pointer bytes */
|
||||
unsigned char expected[sizeof(ptr)];
|
||||
memcpy(expected, &ptr, ptr_size);
|
||||
ASSERT_EQ(memcmp(buf, expected, ptr_size), 0);
|
||||
|
||||
/* The remaining bytes (if any) must be zero */
|
||||
for (size_t i = ptr_size; i < sizeof(buf); i++) {
|
||||
ASSERT_EQ(buf[i], 0u);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "../valkey_strtod.h"
|
||||
#include "errno.h"
|
||||
#include "math.h"
|
||||
#include "test_help.h"
|
||||
|
||||
int test_valkey_strtod(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
errno = 0;
|
||||
double value = valkey_strtod("231.2341234", NULL);
|
||||
TEST_ASSERT(value == 231.2341234);
|
||||
TEST_ASSERT(errno == 0);
|
||||
|
||||
value = valkey_strtod("+inf", NULL);
|
||||
TEST_ASSERT(isinf(value));
|
||||
TEST_ASSERT(errno == 0);
|
||||
|
||||
value = valkey_strtod("-inf", NULL);
|
||||
TEST_ASSERT(isinf(value));
|
||||
TEST_ASSERT(errno == 0);
|
||||
|
||||
value = valkey_strtod("inf", NULL);
|
||||
TEST_ASSERT(isinf(value));
|
||||
TEST_ASSERT(errno == 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <math.h>
|
||||
|
||||
extern "C" {
|
||||
#include "valkey_strtod.h"
|
||||
}
|
||||
|
||||
class ValkeyStrtodTest : public ::testing::Test {};
|
||||
|
||||
TEST_F(ValkeyStrtodTest, TestValkeyStrtod) {
|
||||
errno = 0;
|
||||
double value = valkey_strtod("231.2341234", NULL);
|
||||
ASSERT_DOUBLE_EQ(value, 231.2341234);
|
||||
ASSERT_EQ(errno, 0);
|
||||
|
||||
value = valkey_strtod("+inf", NULL);
|
||||
ASSERT_TRUE(isinf(value));
|
||||
ASSERT_EQ(errno, 0);
|
||||
|
||||
value = valkey_strtod("-inf", NULL);
|
||||
ASSERT_TRUE(isinf(value));
|
||||
ASSERT_EQ(errno, 0);
|
||||
|
||||
value = valkey_strtod("inf", NULL);
|
||||
ASSERT_TRUE(isinf(value));
|
||||
ASSERT_EQ(errno, 0);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#include "../fmacros.h"
|
||||
#include "../vector.h"
|
||||
|
||||
#include "test_help.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t uint8;
|
||||
uint64_t uint64;
|
||||
} test_struct;
|
||||
|
||||
int test_vector(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* The test cases cover the following scenarios:
|
||||
* 1) Whether the array pre-allocates memory during initialization;
|
||||
* 2) Element sizes smaller than, equal to, and larger than sizeof(void*);
|
||||
* 3) Usage of each API.
|
||||
*/
|
||||
vector uint8_vector;
|
||||
vector uint64_vector;
|
||||
vector struct_vector;
|
||||
vectorInit(&uint8_vector, 0, sizeof(uint8_t));
|
||||
vectorInit(&uint64_vector, 10, sizeof(uint64_t));
|
||||
vectorInit(&struct_vector, 128, sizeof(test_struct));
|
||||
|
||||
for (uint64_t i = 0; i < 128; i++) {
|
||||
uint8_t *uint8_item = vectorPush(&uint8_vector);
|
||||
*uint8_item = i;
|
||||
|
||||
uint64_t *uint64_item = vectorPush(&uint64_vector);
|
||||
*uint64_item = i * 1000;
|
||||
|
||||
test_struct *struct_item = vectorPush(&struct_vector);
|
||||
struct_item->uint8 = i;
|
||||
struct_item->uint64 = i * 1000;
|
||||
}
|
||||
|
||||
TEST_ASSERT_MESSAGE("uint8_vector length", vectorLen(&uint8_vector) == 128);
|
||||
TEST_ASSERT_MESSAGE("uint64_vector length", vectorLen(&uint64_vector) == 128);
|
||||
TEST_ASSERT_MESSAGE("struct_vector length", vectorLen(&struct_vector) == 128);
|
||||
for (uint32_t i = 0; i < vectorLen(&uint8_vector); i++) {
|
||||
uint8_t *uint8_item = vectorGet(&uint8_vector, i);
|
||||
TEST_ASSERT_MESSAGE("uint8_item value", *uint8_item == i);
|
||||
|
||||
uint64_t *uint64_item = vectorGet(&uint64_vector, i);
|
||||
TEST_ASSERT_MESSAGE("uint64_item value", *uint64_item == i * 1000);
|
||||
|
||||
test_struct *struct_item = vectorGet(&struct_vector, i);
|
||||
TEST_ASSERT_MESSAGE("struct_item uint8 value", struct_item->uint8 == i);
|
||||
TEST_ASSERT_MESSAGE("struct_item uint64 value", struct_item->uint64 == i * 1000);
|
||||
}
|
||||
|
||||
vectorCleanup(&uint8_vector);
|
||||
vectorCleanup(&uint64_vector);
|
||||
vectorCleanup(&struct_vector);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" {
|
||||
#include "fmacros.h"
|
||||
#include "vector.h"
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint8_t uint8;
|
||||
uint64_t uint64;
|
||||
} test_struct;
|
||||
|
||||
class VectorTest : public ::testing::Test {};
|
||||
|
||||
TEST_F(VectorTest, TestVector) {
|
||||
/* The test cases cover the following scenarios:
|
||||
* 1) Whether the array pre-allocates memory during initialization;
|
||||
* 2) Element sizes smaller than, equal to, and larger than sizeof(void*);
|
||||
* 3) Usage of each API.
|
||||
*/
|
||||
vector uint8_vector;
|
||||
vector uint64_vector;
|
||||
vector struct_vector;
|
||||
vectorInit(&uint8_vector, 0, sizeof(uint8_t));
|
||||
vectorInit(&uint64_vector, 10, sizeof(uint64_t));
|
||||
vectorInit(&struct_vector, 128, sizeof(test_struct));
|
||||
|
||||
for (uint64_t i = 0; i < 128; i++) {
|
||||
uint8_t *uint8_item = (uint8_t *)vectorPush(&uint8_vector);
|
||||
*uint8_item = i;
|
||||
|
||||
uint64_t *uint64_item = (uint64_t *)vectorPush(&uint64_vector);
|
||||
*uint64_item = i * 1000;
|
||||
|
||||
test_struct *struct_item = (test_struct *)vectorPush(&struct_vector);
|
||||
struct_item->uint8 = i;
|
||||
struct_item->uint64 = i * 1000;
|
||||
}
|
||||
|
||||
/* uint8_vector length */
|
||||
ASSERT_EQ(vectorLen(&uint8_vector), 128u);
|
||||
/* uint64_vector length */
|
||||
ASSERT_EQ(vectorLen(&uint64_vector), 128u);
|
||||
/* struct_vector length */
|
||||
ASSERT_EQ(vectorLen(&struct_vector), 128u);
|
||||
|
||||
for (uint32_t i = 0; i < vectorLen(&uint8_vector); i++) {
|
||||
uint8_t *uint8_item = (uint8_t *)(vectorGet(&uint8_vector, i));
|
||||
/* uint8_item value */
|
||||
ASSERT_EQ(*uint8_item, i);
|
||||
|
||||
uint64_t *uint64_item = (uint64_t *)(vectorGet(&uint64_vector, i));
|
||||
/* uint64_item value */
|
||||
ASSERT_EQ(*uint64_item, i * 1000);
|
||||
|
||||
test_struct *struct_item = (test_struct *)(vectorGet(&struct_vector, i));
|
||||
/* struct_item uint8 value */
|
||||
ASSERT_EQ(struct_item->uint8, i);
|
||||
/* struct_item uint64 value */
|
||||
ASSERT_EQ(struct_item->uint64, i * 1000);
|
||||
}
|
||||
|
||||
vectorCleanup(&uint8_vector);
|
||||
vectorCleanup(&uint64_vector);
|
||||
vectorCleanup(&struct_vector);
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
#include "../fmacros.h"
|
||||
#include "../vset.h"
|
||||
#include "../entry.h"
|
||||
#include "test_help.h"
|
||||
#include "../zmalloc.h"
|
||||
#include "../allocator_defrag.h"
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
/* Ensure assert() is never compiled out, even in Release builds. */
|
||||
#undef NDEBUG
|
||||
#include <cassert>
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
extern "C" {
|
||||
#include "allocator_defrag.h"
|
||||
#include "entry.h"
|
||||
#include "fmacros.h"
|
||||
#include "vset.h"
|
||||
#include "zmalloc.h"
|
||||
}
|
||||
|
||||
typedef entry mock_entry;
|
||||
|
||||
@@ -23,8 +35,7 @@ static mock_entry *mockCreateEntry(const char *keystr, long long expiry) {
|
||||
}
|
||||
|
||||
static void mockFreeEntry(void *entry) {
|
||||
// printf("mockFreeEntry: %p\n", entry);
|
||||
entryFree(entry);
|
||||
entryFree((mock_entry *)entry);
|
||||
}
|
||||
|
||||
static mock_entry *mockEntryUpdate(mock_entry *entry, long long expiry) {
|
||||
@@ -36,24 +47,144 @@ static mock_entry *mockEntryUpdate(mock_entry *entry, long long expiry) {
|
||||
}
|
||||
|
||||
static long long mockGetExpiry(const void *entry) {
|
||||
return entryGetExpiry(entry);
|
||||
return entryGetExpiry((const mock_entry *)entry);
|
||||
}
|
||||
|
||||
int test_vset_add_and_iterate(int argc, char **argv, int flags) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)flags;
|
||||
/* Global array to simulate a test database */
|
||||
static mock_entry *mock_entries[10000];
|
||||
static int mock_entry_count = 0;
|
||||
|
||||
/* --------- volatileEntryType Callbacks --------- */
|
||||
static long long mock_entry_get_expiry(const void *entry) {
|
||||
return mockGetExpiry(entry);
|
||||
}
|
||||
|
||||
static int mock_entry_expire(void *entry, void *ctx) {
|
||||
mock_entry *e = (mock_entry *)entry;
|
||||
long long now = *(long long *)ctx;
|
||||
(void)now;
|
||||
serverAssert(mock_entry_get_expiry(entry) <= now);
|
||||
for (int i = 0; i < mock_entry_count; i++) {
|
||||
if (mock_entries[i] == e) {
|
||||
mockFreeEntry(e);
|
||||
mock_entries[i] = mock_entries[--mock_entry_count];
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------- Helper Functions --------- */
|
||||
static mock_entry *mock_entry_create(const char *keystr, long long expiry) {
|
||||
return mockCreateEntry(keystr, expiry);
|
||||
}
|
||||
|
||||
static int insert_mock_entry(vset *set) {
|
||||
if (mock_entry_count >= 10000) return 0;
|
||||
char keybuf[32];
|
||||
snprintf(keybuf, sizeof(keybuf), "key_%d", mock_entry_count);
|
||||
|
||||
long long expiry = rand() % 10000 + 100;
|
||||
mock_entry *e = mock_entry_create(keybuf, expiry);
|
||||
assert(vsetAddEntry(set, mockGetExpiry, e));
|
||||
mock_entries[mock_entry_count++] = e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int insert_mock_entry_with_expiry(vset *set, long long expiry) {
|
||||
if (mock_entry_count >= 10000) return 0;
|
||||
char keybuf[32];
|
||||
snprintf(keybuf, sizeof(keybuf), "key_%d", mock_entry_count);
|
||||
|
||||
mock_entry *e = mock_entry_create(keybuf, expiry);
|
||||
assert(vsetAddEntry(set, mockGetExpiry, e));
|
||||
mock_entries[mock_entry_count++] = e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int update_mock_entry(vset *set) {
|
||||
if (mock_entry_count == 0) return 0;
|
||||
int idx = rand() % mock_entry_count;
|
||||
mock_entry *old = mock_entries[idx];
|
||||
long long old_expiry = mockGetExpiry(old);
|
||||
long long new_expiry = old_expiry + (rand() % 500);
|
||||
mock_entry *updated = mockEntryUpdate(old, new_expiry);
|
||||
mock_entries[idx] = updated;
|
||||
assert(vsetUpdateEntry(set, mockGetExpiry, old, updated, old_expiry, new_expiry));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int remove_mock_entry(vset *set) {
|
||||
if (mock_entry_count == 0) return 0;
|
||||
int idx = rand() % mock_entry_count;
|
||||
mock_entry *e = mock_entries[idx];
|
||||
assert(vsetRemoveEntry(set, mockGetExpiry, e));
|
||||
mockFreeEntry(e);
|
||||
mock_entries[idx] = mock_entries[--mock_entry_count];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int expire_mock_entries(vset *set, mstime_t now) {
|
||||
vsetRemoveExpired(set, mockGetExpiry, mock_entry_expire, now, mock_entry_count, &now);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *mock_defragfn(void *ptr) {
|
||||
size_t size = zmalloc_usable_size(ptr);
|
||||
void *newptr = zmalloc(size);
|
||||
memcpy(newptr, ptr, size);
|
||||
zfree(ptr);
|
||||
/* Update mock_entries to track the new pointer so that expire/remove
|
||||
* callbacks can still find the entry after defrag. */
|
||||
for (int i = 0; i < mock_entry_count; i++) {
|
||||
if ((void *)mock_entries[i] == ptr) {
|
||||
mock_entries[i] = (mock_entry *)newptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return newptr;
|
||||
}
|
||||
|
||||
static size_t defrag_vset(vset *set, size_t cursor, size_t steps) {
|
||||
if (steps == 0) steps = ULONG_MAX;
|
||||
do {
|
||||
cursor = vsetScanDefrag(set, cursor, mock_defragfn);
|
||||
steps--;
|
||||
} while (cursor != 0 && steps > 0);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
static int free_mock_entries(void) {
|
||||
for (int i = 0; i < mock_entry_count; i++) {
|
||||
mock_entry *e = mock_entries[i];
|
||||
mockFreeEntry(e);
|
||||
}
|
||||
mock_entry_count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
class VsetTest : public ::testing::Test {
|
||||
protected:
|
||||
static void SetUpTestSuite() {
|
||||
allocatorDefragInit();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
free_mock_entries();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(VsetTest, TestVsetAddAndIterate) {
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
mock_entry *e1 = mockCreateEntry("item1", 123);
|
||||
mock_entry *e2 = mockCreateEntry("item2", 456);
|
||||
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, e1));
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, e2));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, e1));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, e2));
|
||||
|
||||
TEST_ASSERT(!vsetIsEmpty(&set));
|
||||
ASSERT_FALSE(vsetIsEmpty(&set));
|
||||
|
||||
vsetIterator it;
|
||||
vsetInitIterator(&set, &it);
|
||||
@@ -61,59 +192,48 @@ int test_vset_add_and_iterate(int argc, char **argv, int flags) {
|
||||
void *entry;
|
||||
int count = 0;
|
||||
while (vsetNext(&it, &entry)) {
|
||||
TEST_EXPECT(entry != NULL);
|
||||
ASSERT_NE(entry, nullptr);
|
||||
count++;
|
||||
}
|
||||
|
||||
TEST_ASSERT(count == 2);
|
||||
ASSERT_EQ(count, 2);
|
||||
|
||||
vsetResetIterator(&it);
|
||||
vsetRelease(&set);
|
||||
mockFreeEntry(e1);
|
||||
mockFreeEntry(e2);
|
||||
|
||||
TEST_PRINT_INFO("Test passed with %d expects", failed_expects);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_vset_large_batch_same_expiry(int argc, char **argv, int flags) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)flags;
|
||||
|
||||
TEST_F(VsetTest, TestVsetLargeBatchSameExpiry) {
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
const long long expiry_time = 1000LL;
|
||||
const int total_entries = 200;
|
||||
|
||||
// Allocate and add 200 entries with same expiry
|
||||
mock_entry **entries = zmalloc(sizeof(mock_entry *) * total_entries);
|
||||
TEST_ASSERT(entries != NULL);
|
||||
mock_entry **entries = (mock_entry **)zmalloc(sizeof(mock_entry *) * total_entries);
|
||||
ASSERT_NE(entries, nullptr);
|
||||
|
||||
for (int i = 0; i < total_entries; i++) {
|
||||
char key_buf[32];
|
||||
snprintf(key_buf, sizeof(key_buf), "entry_%d", i);
|
||||
entries[i] = mockCreateEntry(key_buf, expiry_time);
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
|
||||
// Verify set is not empty
|
||||
TEST_ASSERT(!vsetIsEmpty(&set));
|
||||
ASSERT_FALSE(vsetIsEmpty(&set));
|
||||
|
||||
// Iterate all entries and count them
|
||||
vsetIterator it;
|
||||
vsetInitIterator(&set, &it);
|
||||
|
||||
void *entry;
|
||||
int count = 0;
|
||||
while (vsetNext(&it, &entry)) {
|
||||
TEST_EXPECT(entry != NULL);
|
||||
ASSERT_NE(entry, nullptr);
|
||||
count++;
|
||||
}
|
||||
TEST_ASSERT(count == total_entries);
|
||||
ASSERT_EQ(count, total_entries);
|
||||
|
||||
// Cleanup
|
||||
vsetResetIterator(&it);
|
||||
vsetRelease(&set);
|
||||
|
||||
@@ -121,16 +241,9 @@ int test_vset_large_batch_same_expiry(int argc, char **argv, int flags) {
|
||||
mockFreeEntry(entries[i]);
|
||||
}
|
||||
zfree(entries);
|
||||
|
||||
TEST_PRINT_INFO("Inserted and iterated %d entries with same expiry", total_entries);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_vset_large_batch_update_entry_same_expiry(int argc, char **argv, int flags) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)flags;
|
||||
|
||||
TEST_F(VsetTest, TestVsetLargeBatchUpdateEntrySameExpiry) {
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
@@ -143,101 +256,77 @@ int test_vset_large_batch_update_entry_same_expiry(int argc, char **argv, int fl
|
||||
char key_buf[32];
|
||||
snprintf(key_buf, sizeof(key_buf), "entry_%d", i);
|
||||
entries[i] = mockCreateEntry(key_buf, expiry_time);
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
// Verify set is not empty
|
||||
TEST_ASSERT(!vsetIsEmpty(&set));
|
||||
ASSERT_FALSE(vsetIsEmpty(&set));
|
||||
|
||||
// Now iterate and replace all entries
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
mock_entry *old_entry = entries[i];
|
||||
entries[i] = mockEntryUpdate(entries[i], expiry_time);
|
||||
TEST_ASSERT(vsetUpdateEntry(&set, mockGetExpiry, old_entry, entries[i], expiry_time, expiry_time));
|
||||
ASSERT_TRUE(vsetUpdateEntry(&set, mockGetExpiry, old_entry, entries[i], expiry_time, expiry_time));
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
TEST_ASSERT(vsetRemoveEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetRemoveEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
|
||||
// Verify set is empty
|
||||
TEST_ASSERT(vsetIsEmpty(&set));
|
||||
ASSERT_TRUE(vsetIsEmpty(&set));
|
||||
|
||||
// Cleanup
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
mockFreeEntry(entries[i]);
|
||||
}
|
||||
|
||||
TEST_PRINT_INFO("Inserted, updated and deleted %d entries with same expiry", total_entries);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_vset_large_batch_update_entry_multiple_expiries(int argc, char **argv, int flags) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)flags;
|
||||
TEST_F(VsetTest, TestVsetLargeBatchUpdateEntryMultipleExpiries) {
|
||||
const unsigned int total_entries = 1000;
|
||||
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
// Prepare entries with mixed expiry times, some duplicates
|
||||
mock_entry *entries[total_entries];
|
||||
|
||||
// Initialize keys
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
char key_buf[32];
|
||||
snprintf(key_buf, sizeof(key_buf), "entry_%d", i);
|
||||
long long expiry_time = rand() % 10000;
|
||||
entries[i] = mockCreateEntry(key_buf, expiry_time);
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
// Verify set is not empty
|
||||
TEST_ASSERT(!vsetIsEmpty(&set));
|
||||
ASSERT_FALSE(vsetIsEmpty(&set));
|
||||
|
||||
// Now iterate and replace all entries
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
mock_entry *old_entry = entries[i];
|
||||
long long old_expiry = entryGetExpiry(entries[i]);
|
||||
long long new_expiry = old_expiry + rand() % 100000;
|
||||
entries[i] = mockEntryUpdate(entries[i], new_expiry);
|
||||
TEST_ASSERT(vsetUpdateEntry(&set, mockGetExpiry, old_entry, entries[i], old_expiry, new_expiry));
|
||||
ASSERT_TRUE(vsetUpdateEntry(&set, mockGetExpiry, old_entry, entries[i], old_expiry, new_expiry));
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
TEST_ASSERT(vsetRemoveEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetRemoveEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
|
||||
// Verify set is empty
|
||||
TEST_ASSERT(vsetIsEmpty(&set));
|
||||
ASSERT_TRUE(vsetIsEmpty(&set));
|
||||
|
||||
// Cleanup
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
mockFreeEntry(entries[i]);
|
||||
}
|
||||
|
||||
TEST_PRINT_INFO("Inserted, updated and deleted %d entries with different expiry", total_entries);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_vset_iterate_multiple_expiries(int argc, char **argv, int flags) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)flags;
|
||||
TEST_F(VsetTest, TestVsetIterateMultipleExpiries) {
|
||||
const unsigned int total_entries = 5;
|
||||
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
// Prepare entries with mixed expiry times, some duplicates
|
||||
mock_entry *entries[total_entries];
|
||||
|
||||
// Initialize keys
|
||||
for (unsigned int i = 0; i < total_entries; i++) {
|
||||
char key_buf[32];
|
||||
snprintf(key_buf, sizeof(key_buf), "entry_%d", i);
|
||||
long long expiry_time = rand() % 10000;
|
||||
entries[i] = mockCreateEntry(key_buf, expiry_time);
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
|
||||
vsetIterator it;
|
||||
@@ -248,10 +337,9 @@ int test_vset_iterate_multiple_expiries(int argc, char **argv, int flags) {
|
||||
|
||||
void *entry;
|
||||
while (vsetNext(&it, &entry)) {
|
||||
TEST_EXPECT(entry != NULL);
|
||||
ASSERT_NE(entry, nullptr);
|
||||
mock_entry *e = (mock_entry *)entry;
|
||||
|
||||
// Match the entries we inserted
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (strcmp(entryGetField(e), entryGetField(entries[i])) == 0) {
|
||||
found[i] = 1;
|
||||
@@ -261,25 +349,18 @@ int test_vset_iterate_multiple_expiries(int argc, char **argv, int flags) {
|
||||
total++;
|
||||
}
|
||||
|
||||
TEST_ASSERT(total == 5);
|
||||
ASSERT_EQ(total, 5);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
TEST_EXPECT(found[i]);
|
||||
ASSERT_TRUE(found[i]);
|
||||
}
|
||||
|
||||
vsetResetIterator(&it);
|
||||
vsetRelease(&set);
|
||||
for (int i = 0; i < 5; i++) mockFreeEntry(entries[i]);
|
||||
|
||||
TEST_PRINT_INFO("Iterated all %d mixed expiry entries successfully", total);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_vset_add_and_remove_all(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(VsetTest, TestVsetAddAndRemoveAll) {
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
@@ -291,149 +372,19 @@ int test_vset_add_and_remove_all(int argc, char **argv, int flags) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof(key), "key_%d", i);
|
||||
entries[i] = mockCreateEntry(key, expiry);
|
||||
TEST_ASSERT(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetAddEntry(&set, mockGetExpiry, entries[i]));
|
||||
}
|
||||
|
||||
for (int i = 0; i < total_entries; i++) {
|
||||
TEST_ASSERT(vsetRemoveEntry(&set, mockGetExpiry, entries[i]));
|
||||
ASSERT_TRUE(vsetRemoveEntry(&set, mockGetExpiry, entries[i]));
|
||||
mockFreeEntry(entries[i]);
|
||||
}
|
||||
|
||||
TEST_ASSERT(vsetIsEmpty(&set));
|
||||
ASSERT_TRUE(vsetIsEmpty(&set));
|
||||
vsetRelease(&set);
|
||||
|
||||
TEST_PRINT_INFO("Add/remove %d entries, set size now 0", total_entries);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/********************* Fuzzer tests ********************************/
|
||||
|
||||
#define NUM_ITERATIONS 100000
|
||||
#define MAX_ENTRIES 10000
|
||||
#define NUM_DEFRAG_STEPS 100
|
||||
|
||||
/* Global array to simulate a test database */
|
||||
mock_entry *mock_entries[MAX_ENTRIES];
|
||||
int mock_entry_count = 0;
|
||||
|
||||
/* --------- volatileEntryType Callbacks --------- */
|
||||
sds mock_entry_get_key(const void *entry) {
|
||||
return (sds)entry;
|
||||
}
|
||||
|
||||
long long mock_entry_get_expiry(const void *entry) {
|
||||
return mockGetExpiry(entry);
|
||||
}
|
||||
|
||||
int mock_entry_expire(void *entry, void *ctx) {
|
||||
mock_entry *e = (mock_entry *)entry;
|
||||
long long now = *(long long *)ctx;
|
||||
TEST_ASSERT(mock_entry_get_expiry(entry) <= now);
|
||||
for (int i = 0; i < mock_entry_count; i++) {
|
||||
if (mock_entries[i] == e) {
|
||||
// printf("expire entry %p with expiry %llu\n", e, mockGetExpiry(e));
|
||||
mockFreeEntry(e);
|
||||
mock_entries[i] = mock_entries[--mock_entry_count];
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------- Helper Functions --------- */
|
||||
mock_entry *mock_entry_create(const char *keystr, long long expiry) {
|
||||
return mockCreateEntry(keystr, expiry);
|
||||
}
|
||||
|
||||
int insert_mock_entry(vset *set) {
|
||||
if (mock_entry_count >= MAX_ENTRIES) return 0;
|
||||
char keybuf[32];
|
||||
snprintf(keybuf, sizeof(keybuf), "key_%d", mock_entry_count);
|
||||
|
||||
long long expiry = rand() % 10000 + 100;
|
||||
mock_entry *e = mock_entry_create(keybuf, expiry);
|
||||
// printf("adding entry %p with expiry %llu\n", e, expiry);
|
||||
TEST_ASSERT(vsetAddEntry(set, mockGetExpiry, e));
|
||||
mock_entries[mock_entry_count++] = e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int insert_mock_entry_with_expiry(vset *set, long long expiry) {
|
||||
if (mock_entry_count >= MAX_ENTRIES) return 0;
|
||||
char keybuf[32];
|
||||
snprintf(keybuf, sizeof(keybuf), "key_%d", mock_entry_count);
|
||||
|
||||
mock_entry *e = mock_entry_create(keybuf, expiry);
|
||||
// printf("adding entry %p with expiry %llu\n", e, expiry);
|
||||
TEST_ASSERT(vsetAddEntry(set, mockGetExpiry, e));
|
||||
mock_entries[mock_entry_count++] = e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int update_mock_entry(vset *set) {
|
||||
if (mock_entry_count == 0) return 0;
|
||||
int idx = rand() % mock_entry_count;
|
||||
mock_entry *old = mock_entries[idx];
|
||||
long long old_expiry = mockGetExpiry(old);
|
||||
long long new_expiry = old_expiry + (rand() % 500);
|
||||
mock_entry *updated = mockEntryUpdate(old, new_expiry);
|
||||
mock_entries[idx] = updated;
|
||||
// printf("Update entry %p with entry %p with old expiry %llu new expiry %llu\n", old, updated, old_expiry, new_expiry);
|
||||
TEST_ASSERT(vsetUpdateEntry(set, mockGetExpiry, old, updated, old_expiry, new_expiry));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int remove_mock_entry(vset *set) {
|
||||
if (mock_entry_count == 0) return 0;
|
||||
int idx = rand() % mock_entry_count;
|
||||
mock_entry *e = mock_entries[idx];
|
||||
// printf("removing entry %p with expiry %llu\n", e, mockGetExpiry(e));
|
||||
TEST_ASSERT(vsetRemoveEntry(set, mockGetExpiry, e));
|
||||
mockFreeEntry(e);
|
||||
mock_entries[idx] = mock_entries[--mock_entry_count];
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int expire_mock_entries(vset *set, mstime_t now) {
|
||||
// printf("Before expired entries entries: %d\n", mock_entry_count);
|
||||
vsetRemoveExpired(set, mockGetExpiry, mock_entry_expire, now, mock_entry_count, &now);
|
||||
// printf("After expired %zu entries left entries: %d and set is empty: %s\n", count, mock_entry_count, vsetIsEmpty(set) ? "true" : "false");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *mock_defragfn(void *ptr) {
|
||||
size_t size = zmalloc_usable_size(ptr);
|
||||
void *newptr = zmalloc(size);
|
||||
memcpy(newptr, ptr, size);
|
||||
zfree(ptr);
|
||||
return newptr;
|
||||
}
|
||||
|
||||
size_t defrag_vset(vset *set, size_t cursor, size_t steps) {
|
||||
if (steps == 0) steps = ULONG_MAX;
|
||||
do {
|
||||
cursor = vsetScanDefrag(set, cursor, mock_defragfn);
|
||||
steps--;
|
||||
} while (cursor != 0 && steps > 0);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
int free_mock_entries(void) {
|
||||
for (int i = 0; i < mock_entry_count; i++) {
|
||||
mock_entry *e = mock_entries[i];
|
||||
mockFreeEntry(e);
|
||||
}
|
||||
mock_entry_count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_vset_remove_expire_shrink(int argc, char **argv, int flags) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)flags;
|
||||
|
||||
TEST_F(VsetTest, TestVsetRemoveExpireShrink) {
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
@@ -444,81 +395,63 @@ int test_vset_remove_expire_shrink(int argc, char **argv, int flags) {
|
||||
insert_mock_entry_with_expiry(&set, expiry_time);
|
||||
}
|
||||
|
||||
// Verify set is not empty
|
||||
TEST_ASSERT(!vsetIsEmpty(&set));
|
||||
ASSERT_FALSE(vsetIsEmpty(&set));
|
||||
mstime_t now = expiry_time + 10000;
|
||||
size_t count = vsetRemoveExpired(&set, mockGetExpiry, mock_entry_expire, now, mock_entry_count - 1, &now);
|
||||
|
||||
TEST_ASSERT(count == total_entries - 1);
|
||||
ASSERT_EQ(count, total_entries - 1);
|
||||
|
||||
// Verify set is not empty
|
||||
TEST_ASSERT(!vsetIsEmpty(&set));
|
||||
ASSERT_FALSE(vsetIsEmpty(&set));
|
||||
|
||||
// Now complete the expiration
|
||||
TEST_ASSERT(vsetRemoveExpired(&set, mockGetExpiry, mock_entry_expire, now, mock_entry_count, &now) == 1);
|
||||
ASSERT_EQ(vsetRemoveExpired(&set, mockGetExpiry, mock_entry_expire, now, mock_entry_count, &now), 1u);
|
||||
|
||||
// Verify set is empty
|
||||
TEST_ASSERT(vsetIsEmpty(&set));
|
||||
ASSERT_TRUE(vsetIsEmpty(&set));
|
||||
|
||||
vsetRelease(&set);
|
||||
free_mock_entries();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------- Defrag Test --------- */
|
||||
int test_vset_defrag(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
allocatorDefragInit();
|
||||
srand(time(NULL));
|
||||
TEST_F(VsetTest, TestVsetDefrag) {
|
||||
srand(time(nullptr));
|
||||
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
/* defrag empty set */
|
||||
TEST_ASSERT(defrag_vset(&set, 0, 0) == 0);
|
||||
ASSERT_EQ(defrag_vset(&set, 0, 0), 0u);
|
||||
|
||||
/* defrag when single entry */
|
||||
insert_mock_entry(&set);
|
||||
TEST_ASSERT(defrag_vset(&set, 0, 0) == 0);
|
||||
ASSERT_EQ(defrag_vset(&set, 0, 0), 0u);
|
||||
|
||||
/* defrag when vector */
|
||||
for (int i = 0; i < 127 - 1; i++)
|
||||
insert_mock_entry(&set);
|
||||
TEST_ASSERT(defrag_vset(&set, 0, 0) == 0);
|
||||
ASSERT_EQ(defrag_vset(&set, 0, 0), 0u);
|
||||
|
||||
long long expiry = rand() % 10000 + 100;
|
||||
for (int i = 0; i < 127 * 2; i++) {
|
||||
insert_mock_entry_with_expiry(&set, expiry);
|
||||
}
|
||||
TEST_ASSERT(defrag_vset(&set, 0, 0) == 0);
|
||||
ASSERT_EQ(defrag_vset(&set, 0, 0), 0u);
|
||||
|
||||
size_t cursor = 0;
|
||||
for (int i = 0; i < NUM_ITERATIONS; i++) {
|
||||
if (i % NUM_DEFRAG_STEPS == 0)
|
||||
cursor = defrag_vset(&set, cursor, NUM_DEFRAG_STEPS);
|
||||
for (int i = 0; i < 100000; i++) {
|
||||
if (i % 100 == 0)
|
||||
cursor = defrag_vset(&set, cursor, 100);
|
||||
insert_mock_entry_with_expiry(&set, expiry);
|
||||
}
|
||||
TEST_ASSERT(defrag_vset(&set, 0, 0) == 0);
|
||||
ASSERT_EQ(defrag_vset(&set, 0, 0), 0u);
|
||||
|
||||
vsetRelease(&set);
|
||||
free_mock_entries();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------- Fuzzer Test --------- */
|
||||
int test_vset_fuzzer(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
srand(time(NULL));
|
||||
TEST_F(VsetTest, TestVsetFuzzer) {
|
||||
srand(time(nullptr));
|
||||
|
||||
vset set;
|
||||
vsetInit(&set);
|
||||
|
||||
for (int i = 0; i < NUM_ITERATIONS; i++) {
|
||||
for (int i = 0; i < 100000; i++) {
|
||||
int op = rand() % 5;
|
||||
switch (op) {
|
||||
case 0:
|
||||
@@ -532,7 +465,7 @@ int test_vset_fuzzer(int argc, char **argv, int flags) {
|
||||
remove_mock_entry(&set);
|
||||
break;
|
||||
case 4:
|
||||
TEST_ASSERT(defrag_vset(&set, 0, 0) == 0);
|
||||
ASSERT_EQ(defrag_vset(&set, 0, 0), 0u);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -542,9 +475,7 @@ int test_vset_fuzzer(int argc, char **argv, int flags) {
|
||||
}
|
||||
}
|
||||
/* now expire all the entries and check that we have no entries left */
|
||||
expire_mock_entries(&set, LONG_LONG_MAX);
|
||||
TEST_ASSERT(vsetIsEmpty(&set) && mock_entry_count == 0);
|
||||
expire_mock_entries(&set, LLONG_MAX);
|
||||
ASSERT_TRUE(vsetIsEmpty(&set) && mock_entry_count == 0);
|
||||
vsetRelease(&set);
|
||||
free_mock_entries(); /* Just in case */
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,19 @@
|
||||
#include "../zipmap.c"
|
||||
#include "test_help.h"
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
int test_zipmapIterateWithLargeKey(int argc, char *argv[], int flags) {
|
||||
return 0;
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "zipmap.h"
|
||||
}
|
||||
|
||||
class ZipmapTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
TEST_F(ZipmapTest, zipmapIterateWithLargeKey) {
|
||||
char zm[] = "\x04"
|
||||
"\x04"
|
||||
"name"
|
||||
@@ -16,6 +23,7 @@ int test_zipmapIterateWithLargeKey(int argc, char *argv[], int flags) {
|
||||
"surname"
|
||||
"\x03\x00"
|
||||
"foo"
|
||||
"\x05"
|
||||
"noval"
|
||||
"\x00\x00"
|
||||
"\xfe\x00\x02\x00\x00"
|
||||
@@ -30,36 +38,35 @@ int test_zipmapIterateWithLargeKey(int argc, char *argv[], int flags) {
|
||||
"\x04\x00"
|
||||
"long"
|
||||
"\xff";
|
||||
TEST_ASSERT(zipmapValidateIntegrity((unsigned char *)zm, sizeof zm - 1, 1));
|
||||
ASSERT_TRUE(zipmapValidateIntegrity((unsigned char *)zm, sizeof zm - 1, 1));
|
||||
|
||||
unsigned char *p = zipmapRewind((unsigned char *)zm);
|
||||
unsigned char *key, *value;
|
||||
unsigned int klen, vlen;
|
||||
char buf[512];
|
||||
memset(buf, 'a', 512);
|
||||
char *expected_key[] = {"name", "surname", "noval", buf};
|
||||
char *expected_value[] = {"foo", "foo", NULL, "long"};
|
||||
char *expected_key[] = {(char *)"name", (char *)"surname", (char *)"noval", buf};
|
||||
char *expected_value[] = {(char *)"foo", (char *)"foo", nullptr, (char *)"long"};
|
||||
unsigned int expected_klen[] = {4, 7, 5, 512};
|
||||
unsigned int expected_vlen[] = {3, 3, 0, 4};
|
||||
int iter = 0;
|
||||
|
||||
while ((p = zipmapNext(p, &key, &klen, &value, &vlen)) != NULL) {
|
||||
while ((p = zipmapNext(p, &key, &klen, &value, &vlen)) != nullptr) {
|
||||
char *tmp = expected_key[iter];
|
||||
TEST_ASSERT(klen == expected_klen[iter]);
|
||||
TEST_ASSERT(strncmp((const char *)tmp, (const char *)key, klen) == 0);
|
||||
ASSERT_EQ(klen, expected_klen[iter]);
|
||||
ASSERT_EQ(strncmp(tmp, (const char *)key, klen), 0);
|
||||
tmp = expected_value[iter];
|
||||
TEST_ASSERT(vlen == expected_vlen[iter]);
|
||||
TEST_ASSERT(strncmp((const char *)tmp, (const char *)value, vlen) == 0);
|
||||
ASSERT_EQ(vlen, expected_vlen[iter]);
|
||||
if (tmp == nullptr) {
|
||||
ASSERT_EQ(vlen, 0u);
|
||||
} else {
|
||||
ASSERT_EQ(strncmp(tmp, (const char *)value, vlen), 0);
|
||||
}
|
||||
iter++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_zipmapIterateThroughElements(int argc, char *argv[], int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
TEST_F(ZipmapTest, zipmapIterateThroughElements) {
|
||||
char zm[] = "\x06"
|
||||
"\x04"
|
||||
"name"
|
||||
@@ -85,25 +92,24 @@ int test_zipmapIterateThroughElements(int argc, char *argv[], int flags) {
|
||||
"noval"
|
||||
"\x00\x00"
|
||||
"\xff";
|
||||
TEST_ASSERT(zipmapValidateIntegrity((unsigned char *)zm, sizeof zm - 1, 1));
|
||||
ASSERT_TRUE(zipmapValidateIntegrity((unsigned char *)zm, sizeof zm - 1, 1));
|
||||
|
||||
unsigned char *i = zipmapRewind((unsigned char *)zm);
|
||||
unsigned char *key, *value;
|
||||
unsigned int klen, vlen;
|
||||
char *expected_key[] = {"name", "surname", "age", "hello", "foo", "noval"};
|
||||
char *expected_value[] = {"foo", "foo", "foo", "world!", "12345", ""};
|
||||
char *expected_key[] = {(char *)"name", (char *)"surname", (char *)"age", (char *)"hello", (char *)"foo", (char *)"noval"};
|
||||
char *expected_value[] = {(char *)"foo", (char *)"foo", (char *)"foo", (char *)"world!", (char *)"12345", (char *)""};
|
||||
unsigned int expected_klen[] = {4, 7, 3, 5, 3, 5};
|
||||
unsigned int expected_vlen[] = {3, 3, 3, 6, 5, 0};
|
||||
int iter = 0;
|
||||
|
||||
while ((i = zipmapNext(i, &key, &klen, &value, &vlen)) != NULL) {
|
||||
while ((i = zipmapNext(i, &key, &klen, &value, &vlen)) != nullptr) {
|
||||
char *tmp = expected_key[iter];
|
||||
TEST_ASSERT(klen == expected_klen[iter]);
|
||||
TEST_ASSERT(strncmp((const char *)tmp, (const char *)key, klen) == 0);
|
||||
ASSERT_EQ(klen, expected_klen[iter]);
|
||||
ASSERT_EQ(strncmp(tmp, (const char *)key, klen), 0);
|
||||
tmp = expected_value[iter];
|
||||
TEST_ASSERT(vlen == expected_vlen[iter]);
|
||||
TEST_ASSERT(strncmp((const char *)tmp, (const char *)value, vlen) == 0);
|
||||
ASSERT_EQ(vlen, expected_vlen[iter]);
|
||||
ASSERT_EQ(strncmp(tmp, (const char *)value, vlen), 0);
|
||||
iter++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#include "../zmalloc.h"
|
||||
#include "test_help.h"
|
||||
|
||||
int test_zmallocAllocReallocCallocAndFree(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
size_t used_memory_before = zmalloc_used_memory();
|
||||
void *ptr, *ptr2;
|
||||
|
||||
ptr = zmalloc(123);
|
||||
TEST_PRINT_INFO("Allocated 123 bytes; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
ptr = zrealloc(ptr, 456);
|
||||
TEST_PRINT_INFO("Reallocated to 456 bytes; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
ptr2 = zcalloc(123);
|
||||
TEST_PRINT_INFO("Callocated 123 bytes; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
zfree(ptr);
|
||||
zfree(ptr2);
|
||||
TEST_PRINT_INFO("Freed pointers; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
TEST_ASSERT(zmalloc_used_memory() == used_memory_before);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_zmallocAllocZeroByteAndFree(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
size_t used_memory_before = zmalloc_used_memory();
|
||||
void *ptr;
|
||||
|
||||
ptr = zmalloc(0);
|
||||
TEST_PRINT_INFO("Allocated 0 bytes; used: %zu\n", zmalloc_used_memory());
|
||||
zfree(ptr);
|
||||
|
||||
TEST_ASSERT(zmalloc_used_memory() == used_memory_before);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) Valkey Contributors
|
||||
* All rights reserved.
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "generated_wrappers.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
extern "C" {
|
||||
#include "zmalloc.h"
|
||||
}
|
||||
|
||||
class ZmallocTest : public ::testing::Test {};
|
||||
|
||||
TEST_F(ZmallocTest, TestZmallocAllocReallocCallocAndFree) {
|
||||
size_t used_memory_before = zmalloc_used_memory();
|
||||
void *ptr, *ptr2;
|
||||
|
||||
ptr = zmalloc(123);
|
||||
printf("Allocated 123 bytes; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
ptr = zrealloc(ptr, 456);
|
||||
printf("Reallocated to 456 bytes; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
ptr2 = zcalloc(123);
|
||||
printf("Callocated 123 bytes; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
zfree(ptr);
|
||||
zfree(ptr2);
|
||||
printf("Freed pointers; used: %lld\n",
|
||||
(long long)zmalloc_used_memory() - used_memory_before);
|
||||
|
||||
ASSERT_EQ(zmalloc_used_memory(), used_memory_before);
|
||||
}
|
||||
|
||||
TEST_F(ZmallocTest, TestZmallocAllocZeroByteAndFree) {
|
||||
size_t used_memory_before = zmalloc_used_memory();
|
||||
void *ptr;
|
||||
|
||||
ptr = zmalloc(0);
|
||||
printf("Allocated 0 bytes; used: %zu\n", zmalloc_used_memory());
|
||||
zfree(ptr);
|
||||
|
||||
ASSERT_EQ(zmalloc_used_memory(), used_memory_before);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Utility functions for parsing '__wrap_' C function signatures from header files (e.g. wrappers.h).
|
||||
|
||||
Extracts return types, parameters, and function pointers, producing Method and Arg namedtuples.
|
||||
This structured data is used by generate-wrappers.py to create MockValkey and RealValkey classes
|
||||
for gtest-based tests.
|
||||
"""
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
Method = namedtuple("Method", "ret_type name full_args arg_string args")
|
||||
Arg = namedtuple("Arg", "type name")
|
||||
|
||||
def split_args(arg_string):
|
||||
"""
|
||||
Split a C-style argument string by commas, but ignore commas inside parentheses.
|
||||
Example:
|
||||
"int x, void (*cb)(int, int), char* buf"
|
||||
becomes:
|
||||
["int x", "void (*cb)(int, int)", "char* buf"]
|
||||
"""
|
||||
args = []
|
||||
current = []
|
||||
depth = 0
|
||||
for char in arg_string:
|
||||
if char == '(':
|
||||
depth += 1
|
||||
current.append(char)
|
||||
elif char == ')':
|
||||
depth -= 1
|
||||
current.append(char)
|
||||
elif char == ',' and depth == 0:
|
||||
# Split only at top-level commas
|
||||
args.append(''.join(current).strip())
|
||||
current = []
|
||||
else:
|
||||
current.append(char)
|
||||
if current:
|
||||
args.append(''.join(current).strip())
|
||||
# Single "void" means no args
|
||||
if len(args) == 1 and args[0] == "void":
|
||||
return []
|
||||
# Remove variadic "..."
|
||||
if args and args[-1] == "...":
|
||||
args = args[:-1]
|
||||
return args
|
||||
|
||||
|
||||
def find_wrapper_functions_in_header(header_file_name):
|
||||
"""
|
||||
Parse a header file and extract all functions starting with '__wrap_'.
|
||||
|
||||
Each function is returned as a Method namedtuple containing:
|
||||
- ret_type: the return type of the function
|
||||
- name: the function name (without __wrap_)
|
||||
- full_args: raw argument string from the header
|
||||
- arg_string: comma-separated type + name strings for declarations
|
||||
- args: list of Arg namedtuples (type, name)
|
||||
|
||||
This parser recognizes two types of arguments:
|
||||
|
||||
1. Normal arguments (e.g., int x, char* buffer)
|
||||
2. Function pointer arguments (e.g., int *(fp)(int, void*))
|
||||
"""
|
||||
methods = []
|
||||
|
||||
with open(header_file_name, 'r') as f:
|
||||
for line in f:
|
||||
# Match a function signature starting with __wrap_
|
||||
m = re.match(r"(.*)__wrap_(\w+)\((.*)\);", line)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
method_ret_type = m.group(1).strip()
|
||||
method_name = m.group(2).strip()
|
||||
full_args = m.group(3)
|
||||
|
||||
args_declaration = []
|
||||
args_definition = []
|
||||
|
||||
for arg in split_args(full_args):
|
||||
|
||||
# Normal argument
|
||||
m_normal = re.match(r"(.+?)\s*((?:\*+\s*)*)([a-zA-Z_][a-zA-Z0-9_]*\s*(?:\[[^\]]*\])*)$", arg)
|
||||
if m_normal:
|
||||
arg_type = (m_normal.group(1) + m_normal.group(2)).strip()
|
||||
arg_name = m_normal.group(3).strip()
|
||||
args_definition.append(Arg(arg_type, arg_name))
|
||||
args_declaration.append(Arg(arg_type, arg_name))
|
||||
continue
|
||||
|
||||
# Function pointer argument
|
||||
m_func_ptr = re.match(r"(.+?)\s*\(\s*(?:\*\s*)?([a-zA-Z0-9_]+)\s*\)\s*\((.*)\)", arg)
|
||||
if m_func_ptr:
|
||||
arg_ret_type = m_func_ptr.group(1).strip()
|
||||
arg_name = m_func_ptr.group(2).strip()
|
||||
params = m_func_ptr.group(3).strip()
|
||||
|
||||
arg_type = f"{arg_ret_type} ({arg_name})({params})"
|
||||
args_definition.append(Arg(arg_type, arg_name))
|
||||
args_declaration.append(Arg(arg_type, ""))
|
||||
continue
|
||||
|
||||
# If argument cannot be parsed
|
||||
print(f"WARNING: Could not parse argument: '{arg}', {line}")
|
||||
|
||||
# Build comma-separated argument string for declaration
|
||||
arg_string = ', '.join(f"{x.type} {x.name}".strip() for x in args_declaration)
|
||||
|
||||
# Append the method to the results
|
||||
methods.append(Method(
|
||||
ret_type=method_ret_type,
|
||||
name=method_name,
|
||||
full_args=full_args,
|
||||
arg_string=arg_string,
|
||||
args=args_definition
|
||||
))
|
||||
|
||||
return methods
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* wrappers.h - Function Wrapper Declarations for GoogleTest Unit Tests
|
||||
*
|
||||
* PURPOSE:
|
||||
* This file declares C function wrappers that enable mocking of Valkey C functions
|
||||
* in GoogleTest unit tests. It bridges C code with gtest infrastructure.
|
||||
*
|
||||
* HOW IT WORKS:
|
||||
* 1. Declare wrapper functions with __wrap_ prefix (e.g., __wrap_mstime for mstime())
|
||||
* 2. generate-wrappers.py parses this file and auto-generates TWO files:
|
||||
* - generated_wrappers.hpp (MockValkey class with MOCK_METHOD macros)
|
||||
* - generated_wrappers.cpp (wrapper implementations that delegate to MockValkey)
|
||||
* 3. Build system uses --wrap linker flags to redirect calls: mstime() -> __wrap_mstime()
|
||||
* 4. GoogleTest can mock these wrappers to control behavior and verify calls
|
||||
*
|
||||
* RULES:
|
||||
* - All wrapper functions MUST be prefixed with __wrap_
|
||||
* - Function signatures MUST exactly match the original C function
|
||||
* - DO NOT wrap variadic functions (functions with ...) - GoogleTest doesn't support them
|
||||
* - Each wrapper becomes mockable in gtest via the auto-generated MockValkey class
|
||||
*
|
||||
* WORKFLOW:
|
||||
* wrappers.h -> generate-wrappers.py -> [generated_wrappers.hpp + generated_wrappers.cpp]
|
||||
* -> linked with gtest
|
||||
*
|
||||
* See: wrapper_util.py, generate-wrappers.py
|
||||
*/
|
||||
|
||||
#include <sched.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef __WRAPPERS_H
|
||||
#define __WRAPPERS_H
|
||||
// C/C++ cross-compatibility definitions:
|
||||
// Some C keywords or built-in types (e.g., _Atomic, _Bool) are not
|
||||
// recognized or have different meanings in C++. To allow C headers
|
||||
// to be included in C++ code without errors, we redefine them appropriately.
|
||||
#define _Atomic /* _Atomic is not a C++ keyword; define empty */
|
||||
#define _Bool bool /* Replace C _Bool with C++ bool */
|
||||
#define typename _typename /* Avoid conflict with C++ 'typename' keyword */
|
||||
#define protected protected_ /* Avoid conflict with C++ 'protected' keyword */
|
||||
|
||||
#include "ae.h"
|
||||
#include "server.h"
|
||||
|
||||
/**
|
||||
* The list of wrapper methods defined. Each wrapper method must
|
||||
* conform to the same naming conventions (i.e. prefix with a
|
||||
* '__wrap_') and have its method signature match the overridden
|
||||
* method exactly.
|
||||
*
|
||||
* Important: Please read the README.md file for guidelines about mocking. Your
|
||||
* usage of mocking will not be approved if it doesn't follow the guidelines.
|
||||
*
|
||||
* Note: You should NOT wrap variable argument functions (i.e have "...")
|
||||
* See: https://github.com/google/googletest/blob/master/googlemock/docs/gmock_faq.md#can-i-mock-a-variadic-function
|
||||
* Example: serverLog(int level, const char *fmt, ...) should NOT be mocked.
|
||||
*/
|
||||
long long __wrap_aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, aeTimeProc *proc, void *clientData, aeEventFinalizerProc *finalizerProc);
|
||||
#undef protected
|
||||
#undef _Bool
|
||||
#undef typename
|
||||
|
||||
#endif
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1644,3 +1644,8 @@ unsigned int ziplistRandomPairsUnique(unsigned char *zl, unsigned int count, zip
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
/* Wrapper function for gtest to access static zipEntry function. */
|
||||
void testOnlyZipEntry(unsigned char *p, zlentry *e) {
|
||||
zipEntry(p, e);
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
|
||||
UNIT_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + '/../src/unit')
|
||||
TEST_FILE = UNIT_DIR + '/test_files.h'
|
||||
TEST_PROTOTYPE = r'(int (test_[a-zA-Z0-9_]*)\(.*\)).*{'
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open(TEST_FILE, 'w') as output:
|
||||
# Find each test file and collect the test names.
|
||||
test_suites = []
|
||||
for root, dirs, files in os.walk(UNIT_DIR):
|
||||
for file in files:
|
||||
file_path = UNIT_DIR + '/' + file
|
||||
if not file.endswith('.c') or file == 'test_main.c':
|
||||
continue
|
||||
tests = []
|
||||
with open(file_path, 'r') as f:
|
||||
for line in f:
|
||||
match = re.match(TEST_PROTOTYPE, line)
|
||||
if match:
|
||||
function = match.group(1)
|
||||
test_name = match.group(2)
|
||||
tests.append((test_name, function))
|
||||
test_suites.append({'file': file, 'tests': tests})
|
||||
test_suites.sort(key=lambda test_suite: test_suite['file'])
|
||||
output.write("""/* Do not modify this file, it's automatically generated from utils/generate-unit-test-header.py */
|
||||
typedef int unitTestProc(int argc, char **argv, int flags);
|
||||
|
||||
typedef struct unitTest {
|
||||
char *name;
|
||||
unitTestProc *proc;
|
||||
} unitTest;
|
||||
|
||||
""")
|
||||
|
||||
# Write the headers for the functions
|
||||
for test_suite in test_suites:
|
||||
for test in test_suite['tests']:
|
||||
output.write('{};\n'.format(test[1]))
|
||||
output.write("\n")
|
||||
|
||||
# Create test suite lists
|
||||
for test_suite in test_suites:
|
||||
output.write('unitTest __{}[] = {{'.format(test_suite['file'].replace('.c', '_c')))
|
||||
for test in test_suite['tests']:
|
||||
output.write('{{"{}", {}}}, '.format(test[0], test[0]))
|
||||
output.write('{NULL, NULL}};\n')
|
||||
|
||||
output.write("""
|
||||
struct unitTestSuite {
|
||||
char *filename;
|
||||
unitTest *tests;
|
||||
} unitTestSuite[] = {
|
||||
""")
|
||||
for test_suite in test_suites:
|
||||
output.write(' {{"{0}", __{1}}},\n'.format(test_suite['file'], test_suite['file'].replace('.c', '_c')))
|
||||
output.write('};\n')
|
||||
Reference in New Issue
Block a user