basic gtest implementation
Note that this does require that gtest be installed in `contrib/gtest`. This should be done using the `get_gtest` script.
This commit is contained in:
parent
f12d52c1ed
commit
91717bd15c
@ -11,6 +11,11 @@ MACRO(getVersionBit name)
|
||||
STRING(REGEX REPLACE ${VERSION_REGEX} "\\1" ${name} "${VERSION_BIT}")
|
||||
ENDMACRO(getVersionBit)
|
||||
|
||||
IF (${CMAKE_C_COMPILER_ID} MATCHES "GNU" OR ${CMAKE_C_COMPILER_ID} MATCHES "Clang")
|
||||
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")
|
||||
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
||||
ENDIF()
|
||||
|
||||
getVersionBit(HIREDIS_MAJOR)
|
||||
getVersionBit(HIREDIS_MINOR)
|
||||
getVersionBit(HIREDIS_PATCH)
|
||||
@ -75,3 +80,8 @@ ENDIF()
|
||||
IF(ENABLE_EXAMPLES)
|
||||
ADD_SUBDIRECTORY(examples)
|
||||
ENDIF(ENABLE_EXAMPLES)
|
||||
|
||||
IF(ENABLE_TESTS)
|
||||
ADD_SUBDIRECTORY(contrib/gtest)
|
||||
ADD_SUBDIRECTORY(cpptests)
|
||||
ENDIF()
|
||||
10
cpptests/CMakeLists.txt
Normal file
10
cpptests/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
||||
INCLUDE(CTest)
|
||||
INCLUDE_DIRECTORIES(${gtest_SOURCE_DIR}/include)
|
||||
INCLUDE_DIRECTORIES(PROJECT_SOURCE_DIR)
|
||||
INCLUDE_DIRECTORIES(${LIBEVENT_INCLUDES})
|
||||
|
||||
ADD_EXECUTABLE(hiredis-gtest t_basic.cpp t_client.cpp t_async.cpp
|
||||
common.cpp)
|
||||
TARGET_LINK_LIBRARIES(hiredis-gtest gtest_main hiredis event)
|
||||
ADD_TEST(NAME hiredis-test COMMAND hiredis-test)
|
||||
SET_PROPERTY(TARGET hiredis-gtest PROPERTY CXX_STANDARD 11)
|
||||
43
cpptests/common.cpp
Normal file
43
cpptests/common.cpp
Normal file
@ -0,0 +1,43 @@
|
||||
#include "common.h"
|
||||
|
||||
hiredis::ClientSettings hiredis::settings_g;
|
||||
|
||||
using namespace hiredis;
|
||||
|
||||
|
||||
ClientSettings::ClientSettings() {
|
||||
std::string hostval;
|
||||
if (getenv("REDIS_SOCKET")) {
|
||||
m_hostname.assign(getenv("REDIS_SOCKET"));
|
||||
m_mode = REDIS_CONN_UNIX;
|
||||
return;
|
||||
}
|
||||
|
||||
if (getenv("REDIS_HOST")) {
|
||||
hostval.assign(getenv("REDIS_HOST"));
|
||||
}
|
||||
size_t idx = hostval.find(':');
|
||||
if (idx == std::string::npos) {
|
||||
// First part is hostname only
|
||||
m_hostname = hostval;
|
||||
} else {
|
||||
m_port = atoi(hostval.c_str() + idx);
|
||||
hostval.resize(idx);
|
||||
}
|
||||
if (!m_port) {
|
||||
m_port = 6379;
|
||||
}
|
||||
|
||||
// Handle SSL settings as well
|
||||
m_ssl_cert_path = getenv("REDIS_SSL_CLIENT_CERT");
|
||||
m_ssl_key_path = getenv("REDIS_SSL_CLIENT_KEY");
|
||||
m_ssl_ca_path = getenv("REDIS_SSL_CA");
|
||||
}
|
||||
|
||||
void ClientSettings::initOptions(redisOptions& options) const {
|
||||
if (m_mode == REDIS_CONN_TCP) {
|
||||
REDIS_OPTIONS_SET_TCP(&options, hostname(), port());
|
||||
} else if (options.type == REDIS_CONN_UNIX) {
|
||||
REDIS_OPTIONS_SET_UNIX(&options, hostname());
|
||||
}
|
||||
}
|
||||
48
cpptests/common.h
Normal file
48
cpptests/common.h
Normal file
@ -0,0 +1,48 @@
|
||||
#ifndef HIREDIS_CPP_COMMON_H
|
||||
#define HIREDIS_CPP_COMMON_H
|
||||
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include "hiredis.h"
|
||||
|
||||
namespace hiredis {
|
||||
class ClientSettings {
|
||||
public:
|
||||
ClientSettings();
|
||||
|
||||
const char *ssl_cert() const { return m_ssl_cert_path; }
|
||||
const char *ssl_key() const { return m_ssl_key_path; }
|
||||
const char *ssl_ca() const { return m_ssl_ca_path; }
|
||||
|
||||
bool is_ssl() const {
|
||||
return m_ssl_ca_path != NULL;
|
||||
}
|
||||
bool is_unix() const {
|
||||
return false;
|
||||
}
|
||||
const char *hostname() const {
|
||||
return m_hostname.c_str();
|
||||
}
|
||||
uint16_t port() const {
|
||||
return m_port;
|
||||
}
|
||||
int mode() const {
|
||||
return m_mode;
|
||||
}
|
||||
|
||||
void initOptions(redisOptions& options) const;
|
||||
|
||||
private:
|
||||
std::string m_hostname;
|
||||
uint16_t m_port;
|
||||
|
||||
int m_mode = REDIS_CONN_TCP;
|
||||
const char *m_ssl_cert_path = NULL;
|
||||
const char *m_ssl_ca_path = NULL;
|
||||
const char *m_ssl_key_path = NULL;
|
||||
};
|
||||
|
||||
extern ClientSettings settings_g;
|
||||
|
||||
}
|
||||
#endif
|
||||
139
cpptests/t_async.cpp
Normal file
139
cpptests/t_async.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <hiredis.h>
|
||||
#include "adapters/libevent.h"
|
||||
#include <functional>
|
||||
#include <cstdarg>
|
||||
#include "common.h"
|
||||
|
||||
using namespace hiredis;
|
||||
|
||||
struct AsyncClient;
|
||||
typedef std::function<void(AsyncClient*, bool)> ConnectionCallback;
|
||||
typedef std::function<void(AsyncClient*, bool)> DisconnectCallback;
|
||||
typedef std::function<void(AsyncClient*, redisReply *)> CommandCallback;
|
||||
|
||||
static void realConnectCb(const redisAsyncContext*, int);
|
||||
static void realDisconnectCb(const redisAsyncContext*, int);
|
||||
static void realCommandCb(redisAsyncContext*, void*, void*);
|
||||
|
||||
struct CmdData {
|
||||
AsyncClient *client;
|
||||
CommandCallback cb;
|
||||
};
|
||||
|
||||
struct AsyncClient {
|
||||
AsyncClient(const redisOptions& options, event_base* b) {
|
||||
ac = redisAsyncConnectWithOptions(&options);
|
||||
redisLibeventAttach(ac, b);
|
||||
redisAsyncSetConnectCallback(ac, realConnectCb);
|
||||
ac->data = this;
|
||||
}
|
||||
|
||||
AsyncClient(const ClientSettings& settings, event_base *b) {
|
||||
redisOptions options = { 0 };
|
||||
ac = redisAsyncConnectWithOptions(&options);
|
||||
redisLibeventAttach(ac, b);
|
||||
redisAsyncSetConnectCallback(ac, realConnectCb);
|
||||
if (settings.is_ssl()) {
|
||||
redisSecureConnection(&ac->c,
|
||||
settings.ssl_ca(), settings.ssl_cert(), settings.ssl_key(),
|
||||
NULL);
|
||||
}
|
||||
ac->data = this;
|
||||
}
|
||||
|
||||
AsyncClient(redisAsyncContext *ac) : ac(ac) {
|
||||
redisAsyncSetDisconnectCallback(ac, realDisconnectCb);
|
||||
}
|
||||
|
||||
void onConnect(ConnectionCallback cb) {
|
||||
conncb = cb;
|
||||
}
|
||||
|
||||
~AsyncClient() {
|
||||
if (ac != NULL) {
|
||||
auto tmpac = ac;
|
||||
ac = NULL;
|
||||
redisAsyncDisconnect(tmpac);
|
||||
}
|
||||
}
|
||||
|
||||
void cmd(CommandCallback cb, const char *fmt, ...) {
|
||||
auto data = new CmdData {this, cb };
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
redisvAsyncCommand(ac, realCommandCb, data, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void disconnect(DisconnectCallback cb) {
|
||||
disconncb = cb;
|
||||
redisAsyncDisconnect(ac);
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
redisAsyncDisconnect(ac);
|
||||
}
|
||||
|
||||
ConnectionCallback conncb;
|
||||
DisconnectCallback disconncb;
|
||||
redisAsyncContext *ac;
|
||||
};
|
||||
|
||||
|
||||
static void realConnectCb(const redisAsyncContext *ac, int status) {
|
||||
auto self = reinterpret_cast<AsyncClient*>(ac->data);
|
||||
if (self->conncb) {
|
||||
self->conncb(self, status == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void realDisconnectCb(const redisAsyncContext *ac, int status) {
|
||||
auto self = reinterpret_cast<AsyncClient*>(ac->data);
|
||||
if (self->disconncb) {
|
||||
self->disconncb(self, status == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void realCommandCb(redisAsyncContext *ac, void *r, void *ctx) {
|
||||
auto *d = reinterpret_cast<CmdData*>(ctx);
|
||||
auto *rep = reinterpret_cast<redisReply*>(r);
|
||||
auto *self = reinterpret_cast<AsyncClient*>(ac->data);
|
||||
d->cb(self, rep);
|
||||
delete d;
|
||||
}
|
||||
|
||||
class AsyncTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
libevent = event_base_new();
|
||||
}
|
||||
void TearDown() override {
|
||||
event_base_free(libevent);
|
||||
libevent = NULL;
|
||||
}
|
||||
void wait() {
|
||||
event_base_dispatch(libevent);
|
||||
}
|
||||
event_base *libevent;
|
||||
};
|
||||
|
||||
TEST_F(AsyncTest, testAsync) {
|
||||
redisOptions options = {0};
|
||||
struct timeval tv = {0};
|
||||
tv.tv_sec = 1;
|
||||
options.timeout = &tv;
|
||||
settings_g.initOptions(options);
|
||||
AsyncClient client(options, libevent);
|
||||
|
||||
client.onConnect([](AsyncClient*, bool status) {
|
||||
printf("Status: %d\n", status);
|
||||
});
|
||||
|
||||
client.cmd([](AsyncClient *c, redisReply*){
|
||||
printf("Got reply!\n");
|
||||
c->disconnect();
|
||||
}, "PING");
|
||||
|
||||
wait();
|
||||
}
|
||||
34
cpptests/t_basic.cpp
Normal file
34
cpptests/t_basic.cpp
Normal file
@ -0,0 +1,34 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdarg>
|
||||
#include "hiredis.h"
|
||||
|
||||
class FormatterTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
|
||||
static std::string formatCommand(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
char *s = NULL;
|
||||
size_t n = redisvFormatCommand(&s, fmt, ap);
|
||||
va_end(ap);
|
||||
std::string xs(s, n);
|
||||
free(s);
|
||||
return xs;
|
||||
}
|
||||
|
||||
TEST_F(FormatterTest, testFormatCommands) {
|
||||
auto expected = "*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
|
||||
ASSERT_EQ(expected, formatCommand("SET foo bar"))
|
||||
<< "No interpolation";
|
||||
|
||||
expected = "*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
|
||||
ASSERT_EQ(expected, formatCommand("SET %s %s", "foo", "bar"))
|
||||
<< "interpolation";
|
||||
|
||||
expected = "*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n";
|
||||
ASSERT_EQ(expected, formatCommand("SET %s %s", "foo", ""))
|
||||
<< "empty string";
|
||||
}
|
||||
169
cpptests/t_client.cpp
Normal file
169
cpptests/t_client.cpp
Normal file
@ -0,0 +1,169 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include "hiredis.h"
|
||||
#include "common.h"
|
||||
|
||||
|
||||
using namespace hiredis;
|
||||
|
||||
class ClientError : public std::runtime_error {
|
||||
public:
|
||||
ClientError() : std::runtime_error("hiredis error") {
|
||||
}
|
||||
ClientError(const char *s) : std::runtime_error(s) {
|
||||
}
|
||||
};
|
||||
|
||||
class ConnectError : public ClientError {
|
||||
public:
|
||||
ConnectError() : ClientError(){}
|
||||
ConnectError(const redisOptions& options) {
|
||||
if (options.type == REDIS_CONN_TCP) {
|
||||
endpoint = options.endpoint.tcp.ip;
|
||||
endpoint += ":";
|
||||
endpoint += options.endpoint.tcp.port;
|
||||
} else if (options.type == REDIS_CONN_UNIX) {
|
||||
endpoint = "unix://";
|
||||
endpoint += options.endpoint.unix_socket;
|
||||
}
|
||||
}
|
||||
virtual const char *what() const noexcept override{
|
||||
return endpoint.c_str();
|
||||
}
|
||||
private:
|
||||
std::string endpoint;
|
||||
};
|
||||
|
||||
class IOError : public ClientError {};
|
||||
class TimeoutError : public ClientError {};
|
||||
class SSLError : public ClientError {};
|
||||
|
||||
class CommandError : public ClientError {
|
||||
public:
|
||||
CommandError(const redisReply *r) {
|
||||
errstr = r->str;
|
||||
}
|
||||
virtual const char *what() const noexcept override {
|
||||
return errstr.c_str();
|
||||
}
|
||||
private:
|
||||
std::string errstr;
|
||||
};
|
||||
|
||||
void errorFromCode(int code) {
|
||||
switch (code) {
|
||||
case REDIS_ERR_IO:
|
||||
case REDIS_ERR_EOF:
|
||||
case REDIS_ERR_PROTOCOL:
|
||||
throw IOError();
|
||||
case REDIS_ERR_TIMEOUT:
|
||||
throw TimeoutError();
|
||||
default:
|
||||
throw ClientError();
|
||||
}
|
||||
}
|
||||
|
||||
class Client {
|
||||
public:
|
||||
operator redisContext*() {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
Client(redisContext *ctx) :ctx(ctx) {
|
||||
}
|
||||
|
||||
Client(const redisOptions& options) {
|
||||
connectOrThrow(options);
|
||||
}
|
||||
|
||||
Client(const ClientSettings& settings) {
|
||||
redisOptions options = {0};
|
||||
settings.initOptions(options);
|
||||
connectOrThrow(options);
|
||||
if (settings.is_ssl()) {
|
||||
secureConnection(settings);
|
||||
}
|
||||
}
|
||||
|
||||
void secureConnection(const ClientSettings& settings) {
|
||||
if (redisSecureConnection(
|
||||
ctx, settings.ssl_ca(), settings.ssl_cert(),
|
||||
settings.ssl_key(), NULL) != REDIS_OK) {
|
||||
redisFree(ctx);
|
||||
ctx = NULL;
|
||||
throw SSLError();
|
||||
}
|
||||
}
|
||||
|
||||
redisReply *cmd(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
void *p = redisvCommand(ctx, fmt, ap);
|
||||
va_end(ap);
|
||||
return reinterpret_cast<redisReply*>(p);
|
||||
}
|
||||
|
||||
void flushdb() {
|
||||
redisReply *p = cmd("FLUSHDB");
|
||||
if (p == NULL) {
|
||||
errorFromCode(ctx->err);
|
||||
}
|
||||
if (p->type == REDIS_REPLY_ERROR) {
|
||||
auto pp = CommandError(p);
|
||||
freeReplyObject(p);
|
||||
throw pp;
|
||||
}
|
||||
freeReplyObject(p);
|
||||
}
|
||||
|
||||
~Client() {
|
||||
if (ctx != NULL) {
|
||||
redisFree(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
Client(Client&& other) {
|
||||
this->ctx = other.ctx;
|
||||
other.ctx = NULL;
|
||||
}
|
||||
|
||||
void nothing() const {}
|
||||
|
||||
private:
|
||||
void destroyAndThrow() {
|
||||
assert(ctx->err);
|
||||
int err = ctx->err;
|
||||
redisFree(ctx);
|
||||
ctx = NULL;
|
||||
errorFromCode(err);
|
||||
}
|
||||
void connectOrThrow(const redisOptions& options) {
|
||||
ctx = redisConnectWithOptions(&options);
|
||||
if (!ctx) {
|
||||
throw ConnectError();
|
||||
}
|
||||
if (ctx->err) {
|
||||
destroyAndThrow();
|
||||
}
|
||||
}
|
||||
redisContext *ctx;
|
||||
};
|
||||
|
||||
class ClientTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
TEST_F(ClientTest, testTimeout) {
|
||||
redisOptions options = {0};
|
||||
timeval tv = {0};
|
||||
tv.tv_usec = 10000; // 10k micros, small enough
|
||||
options.timeout = &tv;
|
||||
// see https://tools.ietf.org/html/rfc5737
|
||||
// this block of addresses is reserved for "documentation", and it
|
||||
// would likely not connect, ever.
|
||||
ASSERT_THROW(Client(options).nothing(), ClientError);
|
||||
|
||||
// Test the normal timeout
|
||||
Client c(settings_g);
|
||||
c.flushdb();
|
||||
}
|
||||
23
get_gtest.sh
Executable file
23
get_gtest.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Run this from the toplevel directory of the source code tree
|
||||
GTEST_URL_BASE=https://s3-eu-central-1.amazonaws.com/redislabs-dev-public-deps
|
||||
GTEST_URL_BASE=https://github.com/google/googletest/archive/
|
||||
GTEST_FILENAME=release-1.8.0.tar.gz
|
||||
GTEST_TOPDIR=googletest-release-1.8.0
|
||||
DESTDIR=contrib
|
||||
|
||||
if [ -d $DESTDIR/gtest ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
curdir=$PWD
|
||||
tarball=/tmp/${GTEST_FILENAME}
|
||||
url=${GTEST_URL_BASE}/${GTEST_FILENAME}
|
||||
if [ ! -e $tarball ]; then
|
||||
wget -O $tarball $url
|
||||
fi
|
||||
|
||||
tar -C $DESTDIR -xf $tarball
|
||||
rm $DESTDIR/gtest
|
||||
cd $DESTDIR
|
||||
ln -s $GTEST_TOPDIR gtest
|
||||
Loading…
Reference in New Issue
Block a user