This commit is contained in:
Harry Lin 2025-12-16 20:42:47 -08:00 committed by GitHub
commit d29d5d43cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 941 additions and 23 deletions

3
.gitignore vendored
View File

@ -14,6 +14,7 @@ dump*.rdb
*-sentinel
*-server
*-unit-tests
*-unit-gtests
doc-tools
release
misc/*
@ -54,3 +55,5 @@ build-debug/
build-release/
cmake-build-debug/
cmake-build-release/
__pycache__
src/gtest/.flags

View File

@ -14,6 +14,7 @@ endif ()
# Options
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)
@ -39,6 +40,7 @@ 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)

View File

@ -66,14 +66,14 @@ After building Valkey, it is a good idea to test it using:
The above runs the main integration tests. Additional tests are started using:
% make test-unit # Unit tests
% make test-unit # Unit tests (both C unit tests and gtest unit tests)
% make test-modules # Tests of the module API
% make test-sentinel # Valkey Sentinel integration tests
% make test-cluster # Valkey Cluster integration tests
More about running the integration tests can be found in
[tests/README.md](tests/README.md) and for unit tests, see
[src/unit/README.md](src/unit/README.md).
[tests/README.md](tests/README.md), for unit tests, see
[src/unit/README.md](src/unit/README.md) and [src/gtest/README.md](src/gtest/README.md).
## Fixing build problems with dependencies or cached build options
@ -315,6 +315,7 @@ Other options supported by Valkey's `CMake` build system:
- `-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 gtest 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`

18
deps/Makefile vendored
View File

@ -43,6 +43,7 @@ distclean:
-(cd hdr_histogram && $(MAKE) clean) > /dev/null || true
-(cd fpconv && $(MAKE) clean) > /dev/null || true
-(cd fast_float_c_interface && $(MAKE) clean) > /dev/null || true
-(cd googletest && rm -rf build) > /dev/null || true
-(rm -f .make-*)
.PHONY: distclean
@ -128,3 +129,20 @@ fast_float_c_interface: .make-prerequisites
cd fast_float_c_interface && $(MAKE)
.PHONY: fast_float_c_interface
gtest: .make-prerequisites
@printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
@if [ ! -f googletest/CMakeLists.txt ]; then \
echo "Downloading googletest..."; \
rm -rf googletest; \
git clone --depth 1 --branch v1.8.x https://github.com/google/googletest.git googletest; \
fi
@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
cd googletest && cmake3 -B build -DCMAKE_BUILD_TYPE=Release
cd googletest && cmake3 --build build
.PHONY: gtest

23
deps/README.md vendored
View File

@ -6,7 +6,9 @@ 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.
* **googletest** is Google's testing framework used for gtest unit tests.
* **gtest-parallel** is a script for running googletest tests in parallel.
How to upgrade the above dependencies
===
@ -121,3 +123,22 @@ 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/".
googletest and gtest-parallel
---
To upgrade googletest and gtest-parallel:
```sh
# googletest (v1.8.x)
rm -rf googletest
git clone --branch v1.8.x --depth 1 https://github.com/google/googletest.git googletest
rm -rf googletest/.git
# gtest-parallel (master)
rm -rf gtest-parallel
git clone --depth 1 https://github.com/google/gtest-parallel.git gtest-parallel
rm -rf gtest-parallel/.git
```
Commit the changes.

1
deps/googletest vendored Submodule

@ -0,0 +1 @@
Subproject commit dea0216d0c6bc5e63cf5f6c8651cd268668032ec

1
deps/gtest-parallel vendored Submodule

@ -0,0 +1 @@
Subproject commit cd488bdedc1d2cffb98201a17afc1b298b0b90f1

1
src/.gitignore vendored
View File

@ -3,3 +3,4 @@
*.gcov
valkey.info
lcov-html
generated_*

View File

@ -93,3 +93,7 @@ endif ()
if (BUILD_UNIT_TESTS)
add_subdirectory(unit)
endif ()
if (BUILD_UNIT_GTESTS)
add_subdirectory(gtest)
endif ()

View File

@ -31,7 +31,7 @@ endif
ifneq ($(OPTIMIZATION),-O0)
OPTIMIZATION+=-fno-omit-frame-pointer
endif
DEPENDENCY_TARGETS=libvalkey linenoise lua hdr_histogram fpconv
DEPENDENCY_TARGETS=libvalkey linenoise lua hdr_histogram fpconv gtest
NODEPS:=clean distclean
# Default settings
@ -435,6 +435,7 @@ 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
@ -462,7 +463,7 @@ endif
.PHONY: all
all-with-unit-tests: all $(ENGINE_UNIT_TESTS)
all-with-unit-tests: all $(ENGINE_UNIT_TESTS) $(ENGINE_UNIT_GTESTS)
.PHONY: all
persist-settings: distclean
@ -574,7 +575,7 @@ 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 lua/*.o lua/*.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_TESTS) $(ENGINE_UNIT_GTESTS) $(ENGINE_LIB_NAME) unit/*.o unit/*.d lua/*.o lua/*.d trace/*.o trace/*.d *.o *.gcda *.gcno *.gcov valkey.info lcov-html Makefile.dep *.so
rm -f $(DEP)
.PHONY: clean
@ -583,15 +584,27 @@ distclean: clean
-(cd ../deps && $(MAKE) distclean)
-(cd modules && $(MAKE) clean)
-(cd ../tests/modules && $(MAKE) clean)
-(cd gtest && $(MAKE) clean)
-(rm -f .make-*)
-(find gtest -type d -name __pycache__ -exec rm -rf {} +)
.PHONY: distclean
test: $(SERVER_NAME) $(ENGINE_CHECK_AOF_NAME) $(ENGINE_CHECK_RDB_NAME) $(ENGINE_CLI_NAME) $(ENGINE_BENCHMARK_NAME)
@(cd ..; ./runtest)
test-unit: $(ENGINE_UNIT_TESTS)
./$(ENGINE_UNIT_TESTS)
test-unit: $(ENGINE_UNIT_TESTS) $(ENGINE_UNIT_GTESTS)
@echo "Running C unit tests..."
./$(ENGINE_UNIT_TESTS) || echo "C unit tests failed"
@echo "Running gtest unit tests..."
./gtest/$(ENGINE_UNIT_GTESTS) || echo "gtest unit tests failed"
test-gtest:
@(cd gtest && $(MAKE) test-gtest)
# valkey-unit-gtests
$(ENGINE_UNIT_GTESTS): $(ENGINE_LIB_NAME)
@(cd gtest && $(MAKE) $(ENGINE_UNIT_GTESTS))
test-modules: $(SERVER_NAME)
@(cd ..; ./runtest-moduleapi)

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__

155
src/gtest/CMakeLists.txt Normal file
View File

@ -0,0 +1,155 @@
cmake_minimum_required(VERSION 3.14)
project(valkey_unit_gtests)
# 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 GoogleTest-based tests
message(STATUS "Building gtest unit tests")
if (USE_TLS)
if (BUILD_TLS_MODULE)
# TLS as a module
list(APPEND COMPILE_DEFINITIONS "USE_OPENSSL=2")
else (BUILD_TLS_MODULE)
# Built-in TLS support
list(APPEND COMPILE_DEFINITIONS "USE_OPENSSL=1")
list(APPEND COMPILE_DEFINITIONS "BUILD_TLS_MODULE=0")
endif ()
endif ()
# Fetch GoogleTest using FetchContent (modern approach)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
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 "${COMPILE_FLAGS}")
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 "${COMPILE_FLAGS}")
# 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-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 - 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-gtests execinfo)
endif ()
# Link with GoogleTest using target names
target_link_libraries(
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-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-gtest
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"
)

147
src/gtest/Makefile Normal file
View File

@ -0,0 +1,147 @@
CXX ?= g++
OPT=-O3 -DNDEBUG
ifeq ($(DEBUG_BUILD),1)
CXX+=-DDEBUG_BUILD=1
endif
.DEFAULT_GOAL := all
SOURCES := $(wildcard *.cpp)
ALL_OBJECTS := $(patsubst %.cpp,%.o,$(SOURCES))
DEPENDS := $(patsubst %.cpp,%.d,$(SOURCES))
# Hold new gtest test to a high standard
MORE_WARNINGS=-Wold-style-cast \
-Wno-variadic-macros \
-Werror \
-Wpedantic \
-Wextra \
-Wall \
-Wcast-align \
-Wcast-qual \
-Wframe-larger-than=32768 \
-Wno-strict-overflow \
-Wsync-nand \
-Wtrampolines \
-Wsign-compare \
-Werror=float-equal \
-Werror=missing-braces \
-Werror=init-self \
-Werror=logical-op \
-Werror=write-strings \
-Werror=address \
-Werror=array-bounds \
-Werror=char-subscripts \
-Werror=enum-compare \
-Werror=empty-body \
-Werror=main \
-Werror=aggressive-loop-optimizations \
-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
-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
.PHONY: .flags
.flags:
@echo '$(TEST_CFLAGS)' | cmp -s - $@ || echo '$(TEST_CFLAGS)' > $@
# OSS Valkey library dependencies
VALKEY_LIB := ../libvalkey.a
HIREDIS_LIB := ../../deps/libvalkey/lib/libvalkey.a
LUA_LIB := ../../deps/lua/src/liblua.a
JEMALLOC_LIB := ../../deps/jemalloc/lib/libjemalloc.a
HDR_HISTOGRAM_LIB := ../../deps/hdr_histogram/libhdrhistogram.a
FPCONV_LIB := ../../deps/fpconv/libfpconv.a
GTEST_LIB := ../../deps/googletest/build/googlemock/gtest/libgtest.a
GMOCK_LIB := ../../deps/googletest/build/googlemock/libgmock.a
# Ensure GoogleTest is built
../../deps/googletest/build/googlemock/gtest/libgtest.a ../../deps/googletest/build/googlemock/libgmock.a:
cd ../../deps && $(MAKE) gtest
# Ensure Valkey library is built
$(VALKEY_LIB):
cd .. && $(MAKE) libvalkey.a
# Ensure parent prerequisites are built
.PHONY: make-prerequisites
make-prerequisites:
cd .. && $(MAKE) .make-prerequisites
# Default target to compile against Valkey, LUA, jemalloc (as Valkey does) as well as gmock
%.o: %.cpp make-prerequisites generated_wrappers.cpp .flags
$(CXX) -MD -MP -std=c++14 -faligned-new -Wno-write-strings -fpermissive -fno-var-tracking-assignments $(OPT) $(DEBUG) $(TEST_CFLAGS) -Wall -Wno-deprecated-declarations -c -isystem .. -I ../../deps/lua/src/ -I ../../deps/hdr_histogram/ -I ../../deps/fpconv/ -DUSE_JEMALLOC -isystem../../deps/jemalloc/include -I ../../deps/googletest/googletest/include -I ../../deps/googletest/googlemock/include $<
$(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: wrappers.h generate-wrappers.py
./generate-wrappers.py
generated_wrappers.o: generated_wrappers.cpp
LD_LIBS := $(HIREDIS_LIB) \
$(LUA_LIB) \
$(JEMALLOC_LIB) \
$(GMOCK_LIB) \
$(GTEST_LIB) \
$(HDR_HISTOGRAM_LIB) \
$(FPCONV_LIB)
# Generate the single test binary in parent directory.
valkey-unit-gtests: $(ALL_OBJECTS) generated_wrappers.o $(VALKEY_LIB)
$(CXX) -g \
-Wl,--allow-multiple-definition \
$(OVERRIDES) \
$(SYSCALL_WRAP_LINKER_OPTIONS) \
$(COVERAGE_LDFLAGS) \
-Wall -Wno-deprecated-declarations \
-o $@ $^ $(LD_LIBS) \
-lrt -lm -pthread -ldl -lgcov
.PHONY: all
all: valkey-unit-gtests
.PHONY: test-gtest
test-gtest: valkey-unit-gtests
python3 ../../deps/gtest-parallel/gtest_parallel.py ./valkey-unit-gtests
.PHONY: valgrind
valgrind: valkey-unit-gtests
valgrind $(VALGRIND_ARGS) ./valkey-unit-gtests
clean:
rm -f *.o valkey-unit-gtests ../valkey-unit-gtests generated_* $(DEPENDS) .flags
distclean: clean
$(MAKE) -C .. distclean
.PHONY: clean distclean

85
src/gtest/README.md Normal file
View File

@ -0,0 +1,85 @@
## Valkey GoogleTest Unit Test Framework
This directory contains the GoogleTest (gtest) framework integration for Valkey
unit testing. GoogleTest provides advanced unit testing capabilities that are
not available in Valkey's legacy unit testing framework. These capabilities
include:
- Integrated mocking via GoogleMock, providing expressive, behavior-based mocks
- Rich argument matchers and call sequencing / ordering
- Advanced test fixtures and lifecycle control
- Richful assertions and detailed diagnostics
- Parameterized and typed tests
These features enable more expressive, maintainable, and scalable unit tests,
particularly for complex components and edge-case validation.
For more information on GoogleTest, see: https://google.github.io/googletest/
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 C++ linker, which implements 'wrap' functionality to rename function calls
to foo() to a method __wrap_foo() and renames the real foo() method to
__real_foo().
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.
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.
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.
To extend the Valkey classes for mocking further methods, simply add your method
to 'wrappers.h' and re-run 'make test-gtest' to regenerate the Valkey glue code
and run the tests.
## Tricks in running 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:
1. Running all unit tests (C unit tests and gtest unit tests)
```bash
make test-unit
```
2. Running all gtest unit tests
```bash
make test-gtest
```
3. Running all gtest unit tests in the test class, replace TEST_CLASS_NAME with
expected test class name
```bash
make valkey-unit-gtests
./src/gtest/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/gtest/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
```

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*>(arg->ptr), str) == 0;
}
#endif // _CUSTOM_MATCHERS_HPP_

View File

@ -0,0 +1,66 @@
#include "generated_wrappers.hpp"
extern "C" {
#include "dict.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(
{
*(static_cast<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 via zfree
TEST_F(ExampleTest, TestMocking) {
// zfree should be called in dictRelease
EXPECT_CALL(mock, zfree(_)).Times(AtLeast(1)); // Verifies that zfree() is called at least once
dict *d = dictCreate(&keylistDictType);
dictRelease(d);
}

161
src/gtest/generate-wrappers.py Executable file
View File

@ -0,0 +1,161 @@
#!/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 re
import sys
import os
from wrapper_util import find_wrapper_functions_in_header, Method, Arg
def generate_header(header_file, methods):
# Write the generated_wrappers hpp file
f = open(header_file, 'w')
f.write('''#ifndef __GENERATED_WRAPPERS_HPP
#define __GENERATED_WRAPPERS_HPP
// AUTOGENERATED - DO NOT MODIFY
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "wrappers.h"
#include "custom_matchers.hpp"
using namespace ::testing;
using ::testing::StrictMock;
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
''')
f.close()
def generate_cpp(cpp_file, methods):
f = open(cpp_file, 'w')
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;
}
''')
f.close()
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()

8
src/gtest/main.cpp Normal file
View File

@ -0,0 +1,8 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
int main(int argc, char** argv) {
// The following line must be executed to initialize GoogleTest before running the tests.
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}

120
src/gtest/wrapper_util.py Normal file
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\*])([a-zA-Z0-9_]+)$", arg)
if m_normal:
arg_type = m_normal.group(1).strip()
arg_name = m_normal.group(2).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

72
src/gtest/wrappers.h Normal file
View File

@ -0,0 +1,72 @@
/**
* 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
#define _Atomic /* nothing */
#define _Bool bool
#define typename _typename
#define protected protected_
#include "ae.h"
#include "dict.h"
#include "server.h"
#include "adlist.h"
#include "zmalloc.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.
*
* 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);
void* __wrap_valkey_malloc(size_t size);
void* __wrap_valkey_free(void* ptr);
void *__wrap_valkey_calloc(size_t size);
void * __wrap_valkey_realloc(void* ptr, size_t size);
list* __wrap_listCreate();
dict *__wrap_dictCreate(dictType *type);
void __wrap_listRelease(struct list *list);
#undef protected
#undef _Bool
#undef typename
#endif
#ifdef __cplusplus
}
#endif

View File

@ -5923,8 +5923,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";

View File

@ -1240,9 +1240,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 */

View File

@ -86,7 +86,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)
@ -256,7 +256,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);

View File

@ -7014,9 +7014,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);

View File

@ -57,7 +57,7 @@
#include <systemd/sd-daemon.h>
#endif
#ifndef static_assert
#if !defined(static_assert) && !defined(__cplusplus)
#define static_assert _Static_assert
#endif
@ -2737,7 +2737,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);
@ -2879,7 +2879,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);
@ -4066,11 +4066,18 @@ void resetCommand(client *c);
void failoverCommand(client *c);
#if defined(__GNUC__)
#ifdef __cplusplus
void *calloc(size_t count, size_t size) throw() __attribute__((deprecated));
void free(void *ptr) throw() __attribute__((deprecated));
void *malloc(size_t size) throw() __attribute__((deprecated));
void *realloc(void *ptr, size_t size) throw() __attribute__((deprecated));
#else
void *calloc(size_t count, size_t size) __attribute__((deprecated));
void free(void *ptr) __attribute__((deprecated));
void *malloc(size_t size) __attribute__((deprecated));
void *realloc(void *ptr, size_t size) __attribute__((deprecated));
#endif
#endif
/* Debugging stuff */
void _serverAssertWithInfo(const client *c, const robj *o, const char *estr, const char *file, int line);

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

View File

@ -1,7 +1,7 @@
## 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 uses a very simple C testing framework, built up over time but now based loosely off of [Unity](https://www.throwtheswitch.org/unity). Valkey now also supports [gtest unit tests](https://google.github.io/googletest/), see the `src/gtest/README.md` for details.
All test files are located at `src/unit/test_*`.
All C unit 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.