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:
Harry Lin
2026-02-26 14:14:23 -08:00
committed by GitHub
parent 9c8130d414
commit f3e957cee8
90 changed files with 10889 additions and 8904 deletions
+3
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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`
-12
View File
@@ -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)
+8
View File
@@ -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
+38 -1
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
.*.swp
*.py[co]
+4
View File
@@ -0,0 +1,4 @@
[style]
based_on_style = pep8
indent_width = 2
column_limit = 80
+28
View File
@@ -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/).
+202
View File
@@ -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.
+90
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+2
View File
@@ -3,3 +3,5 @@
*.gcov
valkey.info
lcov-html
generated_*
unit/wrapped/
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+12
View File
@@ -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__
+17
View File
@@ -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
View File
@@ -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);
}
+22
View File
@@ -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);
}
+2 -2
View File
@@ -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 */
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+38
View File
@@ -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
View File
@@ -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"
)
+240
View File
@@ -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
View File
@@ -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
```
+20
View File
@@ -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_
+65
View File
@@ -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();
}
+75
View File
@@ -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()
+163
View File
@@ -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()
+90
View File
@@ -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);
}
-327
View File
@@ -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;
}
+398
View File
@@ -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
}
-26
View File
@@ -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;
}
+32
View File
@@ -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");
}
+94 -117
View File
@@ -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;
}
-247
View File
@@ -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;
}
+217
View File
@@ -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
-64
View File
@@ -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
-241
View File
@@ -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
+213
View File
@@ -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);
}
-242
View File
@@ -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;
}
+230
View File
@@ -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
-117
View File
@@ -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;
}
-294
View File
@@ -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;
}
+247
View File
@@ -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);
}
-703
View File
@@ -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;
}
+769
View File
@@ -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);
}
-181
View File
@@ -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;
}
+168
View File
@@ -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
-651
View File
@@ -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;
}
+706
View File
@@ -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);
}
-359
View File
@@ -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;
}
+349
View File
@@ -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);
}
}
-35
View File
@@ -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;
}
+35
View File
@@ -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);
}
-59
View File
@@ -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;
}
+74
View File
@@ -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);
}
+203 -272
View File
@@ -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;
}
-47
View File
@@ -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;
}
+50
View File
@@ -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);
}
+120
View File
@@ -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
+69
View File
@@ -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
+5
View File
@@ -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);
}
-59
View File
@@ -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')