Commit 91717bd1 authored by Mark Nunberg's avatar Mark Nunberg
Browse files

basic gtest implementation

Note that this does require that gtest be installed in
`contrib/gtest`. This should be done using the `get_gtest` script.
parent f12d52c1
......@@ -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()
\ No newline at end of file
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)
\ No newline at end of file
#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());
}
}
#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
#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();
}
#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";
}
\ No newline at end of file
#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();
}
#!/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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment