diff options
author | Noah Watkins <noahwatkins@gmail.com> | 2013-02-15 15:38:53 -0800 |
---|---|---|
committer | Noah Watkins <noahwatkins@gmail.com> | 2013-08-25 08:58:26 -0700 |
commit | 038688f3610eb2faba72c8246ded865ec46cdced (patch) | |
tree | 451a396ca56ef68e51a625d4f2fe4f348b83657d | |
parent | eec2f0f51f88ca571d1675ba9e249f2a0774ea4c (diff) | |
download | ceph-038688f3610eb2faba72c8246ded865ec46cdced.tar.gz |
cls_lua: add Lua object class
Adds cls_lua and associated tests. Supports many of the cls_cxx_*
functions. See test/cls_lua/ for examples.
Signed-off-by: Noah Watkins <noahwatkins@gmail.com>
-rw-r--r-- | src/.gitignore | 3 | ||||
-rw-r--r-- | src/Makefile.am | 29 | ||||
-rw-r--r-- | src/cls/lua/cls_lua.cc | 628 | ||||
-rw-r--r-- | src/cls/lua/cls_lua.h | 49 | ||||
-rw-r--r-- | src/test/cls_lua/test_cls_lua.cc | 361 | ||||
-rw-r--r-- | src/test/cls_lua/test_script.lua | 217 | ||||
-rw-r--r-- | src/test/cls_lua/txt2str.cc | 41 |
7 files changed, 1328 insertions, 0 deletions
diff --git a/src/.gitignore b/src/.gitignore index 4c98529bd87..08d32cc765f 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -71,3 +71,6 @@ Makefile # old dir, may in use by older branches /leveldb + +/test/cls_lua/test_script.h +/txt2str diff --git a/src/Makefile.am b/src/Makefile.am index 421179eda72..f811ca22045 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -25,6 +25,7 @@ EXTRA_DIST = \ CLEANFILES = bin_PROGRAMS = +noinst_PROGRAMS = # like bin_PROGRAMS, but these targets are only built for debug builds bin_DEBUGPROGRAMS = sbin_PROGRAMS = @@ -629,6 +630,12 @@ libcls_rgw_la_LDFLAGS = ${AM_LDFLAGS} -version-info 1:0:0 -export-symbols-regex radoslib_LTLIBRARIES += libcls_rgw.la +libcls_lua_la_SOURCES = cls/lua/cls_lua.cc cls/lua/cls_lua.h +libcls_lua_la_CXXFLAGS = ${AM_CXXFLAGS} +libcls_lua_la_LIBADD = liblua/liblua.la +libcls_lua_la_LDFLAGS = ${AM_LDFLAGS} -version-info 1:0:0 -export-symbols-regex '.*__cls_.*' +radoslib_LTLIBRARIES += libcls_lua.la + libcls_lock_client_a_SOURCES = \ cls/lock/cls_lock_client.cc \ cls/lock/cls_lock_types.cc \ @@ -1057,6 +1064,28 @@ ceph_test_cls_rbd_LDADD = librados.la ${UNITTEST_STATIC_LDADD} ceph_test_cls_rbd_CXXFLAGS = ${AM_CXXFLAGS} ${UNITTEST_CXXFLAGS} bin_DEBUGPROGRAMS += ceph_test_cls_rbd + +LUA_TEST_H=test/cls_lua/test_script.h +LUA_TEST_L=test/cls_lua/test_script.lua + +EXTRA_DIST += $(LUA_TEST_L) +BUILT_SOURCES += $(LUA_TEST_H) +CLEANFILES += $(LUA_TEST_H) + +txt2str_SOURCES = test/cls_lua/txt2str.cc +noinst_PROGRAMS += txt2str + +$(LUA_TEST_H): $(LUA_TEST_L) txt2str + $(top_builddir)/src/txt2str $(LUA_TEST_L) cls_lua_test_script > $@ + +ceph_test_cls_lua_SOURCES = \ + test/cls_lua/test_cls_lua.cc \ + test/librados/test.cc \ + $(LUA_TEST_H) +ceph_test_cls_lua_LDADD = librados.la liblua/liblua.la ${UNITTEST_STATIC_LDADD} +ceph_test_cls_lua_CXXFLAGS = ${AM_CXXFLAGS} ${UNITTEST_CXXFLAGS} +bin_DEBUGPROGRAMS += ceph_test_cls_lua + ceph_test_cls_refcount_SOURCES = test/cls_refcount/test_cls_refcount.cc \ test/librados/test.cc ceph_test_cls_refcount_LDADD = librados.la libcls_refcount_client.a ${UNITTEST_STATIC_LDADD} diff --git a/src/cls/lua/cls_lua.cc b/src/cls/lua/cls_lua.cc new file mode 100644 index 00000000000..bf4dc6a6e16 --- /dev/null +++ b/src/cls/lua/cls_lua.cc @@ -0,0 +1,628 @@ +/* + * Lua Bindings for RADOS Object Class + */ +#include <errno.h> +#include <setjmp.h> +#include <string> +#include <sstream> +#include "liblua/lua.hpp" +#include "include/types.h" +#include "objclass/objclass.h" +#include "cls/lua/cls_lua.h" + +CLS_VER(1,0) +CLS_NAME(lua) + +cls_handle_t h_class; +cls_method_handle_t h_eval; + +/* + * Jump point for recovering from Lua panic. + */ +static jmp_buf cls_lua_panic_jump; + +/* + * Handle Lua panic + */ +static int cls_lua_atpanic(lua_State *lua) +{ + CLS_ERR("error: Lua panic: %s", lua_tostring(lua, -1)); + longjmp(cls_lua_panic_jump, 1); + return 0; +} + +struct clslua_err { + bool error; + int ret; +}; + +struct clslua_hctx { + struct clslua_err error; + struct clslua_cmd cmd; + struct clslua_reply reply; + cls_method_context_t *hctx; + int ret; +}; + +/* Lua registry key for method context */ +static char clslua_hctx_reg_key; + +/* + * Grabs the full method handler context + */ +static clslua_hctx *__clslua_get_hctx(lua_State *L) +{ + /* lookup registry value */ + lua_pushlightuserdata(L, &clslua_hctx_reg_key); + lua_gettable(L, LUA_REGISTRYINDEX); + + /* check cls_lua assumptions */ + assert(!lua_isnil(L, -1)); + assert(lua_type(L, -1) == LUA_TLIGHTUSERDATA); + + /* cast and cleanup stack */ + clslua_hctx *hctx = (struct clslua_hctx *)lua_touserdata(L, -1); + lua_pop(L, 1); + + return hctx; +} + +/* + * Get the method context out of the registry. This is called at the beginning + * of each clx_cxx_* wrapper, and must be set before there is any chance a Lua + * script calling a 'cls' module function that requires it. + */ +static cls_method_context_t clslua_get_hctx(lua_State *L) +{ + struct clslua_hctx *hctx = __clslua_get_hctx(L); + return *hctx->hctx; +} + +/* + * Returns a reference to cls_lua error state from registry. + */ +struct clslua_err *clslua_checkerr(lua_State *L) +{ + struct clslua_hctx *hctx = __clslua_get_hctx(L); + struct clslua_err *err = &hctx->error; + return err; +} + + +/* Registry key for real `pcall` function */ +static char clslua_pcall_reg_key; + +/* + * Wrap Lua pcall to check for errors thrown by cls_lua (e.g. I/O errors or + * bufferlist decoding errors). The global error is cleared before returning + * to the caller. + */ +static int clslua_pcall(lua_State *L) +{ + int nargs = lua_gettop(L); + lua_pushlightuserdata(L, &clslua_pcall_reg_key); + lua_gettable(L, LUA_REGISTRYINDEX); + lua_insert(L, 1); + lua_call(L, nargs, LUA_MULTRET); + struct clslua_err *err = clslua_checkerr(L); + assert(err); + if (err->error) { + err->error = false; + lua_pushinteger(L, err->ret); + lua_insert(L, -2); + } + return lua_gettop(L); +} + + +/* + * cls_log + */ +static int clslua_log(lua_State *L) +{ + struct clslua_hctx *hctx = __clslua_get_hctx(L); + int nargs = lua_gettop(L); + + if (!nargs) + return 0; + + int loglevel = LOG_LEVEL_DEFAULT; + bool custom_ll = false; + + /* check if first arg can be a log level */ + if (nargs > 1 && lua_isnumber(L, 1)) { + int ll = (int)lua_tonumber(L, 1); + if (ll >= 0) { + loglevel = ll; + custom_ll = true; + } + } + + /* check space for args and seperators (" ") */ + int nelems = ((nargs - (custom_ll ? 1 : 0)) * 2) - 1; + luaL_checkstack(L, nelems, "rados.log(..)"); + + for (int i = custom_ll ? 2 : 1; i <= nargs; i++) { + const char *part = lua_tostring(L, i); + if (!part) { + if (lua_type(L, i) == LUA_TBOOLEAN) + part = lua_toboolean(L, i) ? "true" : "false"; + else + part = luaL_typename(L, i); + } + lua_pushstring(L, part); + if ((i+1) <= nargs) + lua_pushstring(L, " "); + } + + /* join string parts and send to Ceph/reply log */ + lua_concat(L, nelems); + CLS_LOG(loglevel, "%s", lua_tostring(L, -1)); + hctx->reply.log.push_back(lua_tostring(L, -1)); + + /* concat leaves result at top of stack */ + return 1; +} + +static char clslua_registered_handle_reg_key; + +/* + * Register a function to be used as a handler target + */ +static int clslua_register(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TFUNCTION); + + /* get table of registered handlers */ + lua_pushlightuserdata(L, &clslua_registered_handle_reg_key); + lua_gettable(L, LUA_REGISTRYINDEX); + assert(lua_type(L, -1) == LUA_TTABLE); + + /* lookup function argument */ + lua_pushvalue(L, 1); + lua_gettable(L, -2); + + if (lua_isnil(L, -1)) { + lua_pushvalue(L, 1); + lua_pushvalue(L, 1); + lua_settable(L, -4); + } else { + lua_pushstring(L, "Cannot register handler more than once"); + return lua_error(L); + } + + return 0; +} + +/* + * Check if a function is registered as a handler + */ +static void clslua_check_registered_handler(lua_State *L) +{ + luaL_checktype(L, -1, LUA_TFUNCTION); + + /* get table of registered handlers */ + lua_pushlightuserdata(L, &clslua_registered_handle_reg_key); + lua_gettable(L, LUA_REGISTRYINDEX); + assert(lua_type(L, -1) == LUA_TTABLE); + + /* lookup function argument */ + lua_pushvalue(L, -2); + lua_gettable(L, -2); + + if (!lua_rawequal(L, -1, -3)) { + lua_pushstring(L, "Handler is not registered"); + lua_error(L); + } + + lua_pop(L, 2); +} + +/* + * Handle result of a cls_cxx_* call. If @ok is non-zero then we return with + * the number of Lua return arguments on the stack. Otherwise we save error + * information in the registry and throw a Lua error. + */ +static int clslua_opresult(lua_State *L, int ok, int ret, int nargs) +{ + struct clslua_err *err = clslua_checkerr(L); + + assert(err); + if (err->error) { + CLS_ERR("error: cls_lua state machine: unexpected error"); + assert(0); + } + + /* everything is cherry */ + if (ok) + return nargs; + + /* set error in registry */ + err->error = true; + err->ret = ret; + + /* push error message */ + lua_pushfstring(L, "%s", strerror(ret)); + + return lua_error(L); +} + +/* + * cls_cxx_create + */ +static int clslua_create(lua_State *lua) +{ + cls_method_context_t hctx = clslua_get_hctx(lua); + int exclusive = lua_toboolean(lua, 1); + + int ret = cls_cxx_create(hctx, exclusive); + return clslua_opresult(lua, (ret == 0), ret, 0); +} + +/* + * cls_cxx_remove + */ +static int clslua_remove(lua_State *lua) +{ + cls_method_context_t hctx = clslua_get_hctx(lua); + + int ret = cls_cxx_remove(hctx); + return clslua_opresult(lua, (ret == 0), ret, 0); +} + +/* + * cls_cxx_stat + */ +static int clslua_stat(lua_State *L) +{ + cls_method_context_t hctx = clslua_get_hctx(L); + + uint64_t size; + time_t mtime; + int ret = cls_cxx_stat(hctx, &size, &mtime); + if (!ret) { + lua_pushinteger(L, size); + lua_pushinteger(L, mtime); + } + return clslua_opresult(L, (ret == 0), ret, 2); +} + +/* + * cls_cxx_read + */ +static int clslua_read(lua_State *L) +{ + cls_method_context_t hctx = clslua_get_hctx(L); + int offset = luaL_checkint(L, 1); + int length = luaL_checkint(L, 2); + bufferlist *bl = clslua_pushbufferlist(L, NULL); + int ret = cls_cxx_read(hctx, offset, length, bl); + return clslua_opresult(L, (ret >= 0), ret, 1); +} + +/* + * cls_cxx_map_get_val + */ +static int clslua_map_get_val(lua_State *L) +{ + cls_method_context_t hctx = clslua_get_hctx(L); + const char *key = luaL_checkstring(L, 1); + bufferlist *bl = clslua_pushbufferlist(L, NULL); + int ret = cls_cxx_map_get_val(hctx, key, bl); + return clslua_opresult(L, (ret == 0), ret, 1); +} + +/* + * cls_cxx_map_set_val + */ +static int clslua_map_set_val(lua_State *L) +{ + cls_method_context_t hctx = clslua_get_hctx(L); + const char *key = luaL_checkstring(L, 1); + bufferlist *val = clslua_checkbufferlist(L, 2); + int ret = cls_cxx_map_set_val(hctx, key, val); + return clslua_opresult(L, (ret == 0), ret, 0); +} + +/* + * Functions registered in the 'cls' module. + */ +static const luaL_Reg clslua_lib[] = { + {"log", clslua_log}, + {"register", clslua_register}, + {"create", clslua_create}, + {"remove", clslua_remove}, + {"stat", clslua_stat}, + {"read", clslua_read}, + {"map_get_val", clslua_map_get_val}, + {"map_set_val", clslua_map_set_val}, + {NULL, NULL} +}; + +/* + * Set int const in table at top of stack + */ +#define SET_INT_CONST(var) do { \ + lua_pushinteger(L, var); \ + lua_setfield(L, -2, #var); \ +} while (0) + +/* + * Setup the execution environment. Our sandbox currently is not + * sophisticated. With a new Lua state per-request we don't need to work about + * users stepping on each other, but we do rip out access to the local file + * system. All this will change when/if we decide to use some shared Lua + * states, most likely for performance reasons. + */ +static void clslua_setup_env(lua_State *L) +{ + /* load base Lua library */ + lua_pushcfunction(L, luaopen_base); + lua_pushstring(L, ""); + lua_call(L, 1, 0); + + /* mask unsafe */ + lua_pushnil(L); + lua_setglobal(L, "loadfile"); + + /* mask unsafe */ + lua_pushnil(L); + lua_setglobal(L, "dofile"); + + /* not integrated into our error handling */ + lua_pushnil(L); + lua_setglobal(L, "xpcall"); + + /* load table library */ + lua_pushcfunction(L, luaopen_table); + lua_pushstring(L, LUA_TABLIBNAME); + lua_call(L, 1, 0); + + /* load string library */ + lua_pushcfunction(L, luaopen_string); + lua_pushstring(L, LUA_STRLIBNAME); + lua_call(L, 1, 0); + + /* load math library */ + lua_pushcfunction(L, luaopen_math); + lua_pushstring(L, LUA_MATHLIBNAME); + lua_call(L, 1, 0); + + /* load debug library */ + lua_pushcfunction(L, luaopen_debug); + lua_pushstring(L, LUA_DBLIBNAME); + lua_call(L, 1, 0); + + /* save normal pcall method */ + lua_pushlightuserdata(L, &clslua_pcall_reg_key); + lua_getglobal(L, "pcall"); + lua_settable(L, LUA_REGISTRYINDEX); + + /* replace with our pcall method */ + lua_pushcfunction(L, clslua_pcall); + lua_setglobal(L, "pcall"); + + /* + * Register cls functions (cls.log, etc...) + */ + luaL_register(L, "cls", clslua_lib); + + /* + * Register generic errno values under 'cls' + */ + SET_INT_CONST(EPERM); + SET_INT_CONST(ENOENT); + SET_INT_CONST(ESRCH); + SET_INT_CONST(EINTR); + SET_INT_CONST(EIO); + SET_INT_CONST(ENXIO); + SET_INT_CONST(E2BIG); + SET_INT_CONST(ENOEXEC); + SET_INT_CONST(EBADF); + SET_INT_CONST(ECHILD); + SET_INT_CONST(EAGAIN); + SET_INT_CONST(ENOMEM); + SET_INT_CONST(EACCES); + SET_INT_CONST(EFAULT); + SET_INT_CONST(ENOTBLK); + SET_INT_CONST(EBUSY); + SET_INT_CONST(EEXIST); + SET_INT_CONST(EXDEV); + SET_INT_CONST(ENODEV); + SET_INT_CONST(ENOTDIR); + SET_INT_CONST(EISDIR); + SET_INT_CONST(EINVAL); + SET_INT_CONST(ENFILE); + SET_INT_CONST(EMFILE); + SET_INT_CONST(ENOTTY); + SET_INT_CONST(ETXTBSY); + SET_INT_CONST(EFBIG); + SET_INT_CONST(ENOSPC); + SET_INT_CONST(ESPIPE); + SET_INT_CONST(EROFS); + SET_INT_CONST(EMLINK); + SET_INT_CONST(EPIPE); + SET_INT_CONST(EDOM); + SET_INT_CONST(ERANGE); + lua_pop(L, 1); + + /* load bufferlist lib */ + lua_pushcfunction(L, luaopen_bufferlist); + lua_pushstring(L, "bufferlist"); + lua_call(L, 1, 0); + + /* load msgpack lib */ + lua_pushcfunction(L, luaopen_cmsgpack); + lua_pushstring(L, "cmsgpack"); + lua_call(L, 1, 0); +} + +/* + * Runs the script, and calls handler. + */ +static int clslua_eval(lua_State *L) +{ + struct clslua_hctx *ctx = __clslua_get_hctx(L); + const char *funcname = ctx->cmd.funcname.c_str(); + ctx->ret = -EIO; /* assume failure */ + + /* + * Load modules, errno value constants, and other environment goodies. Must + * be done before loading/compiling the chunk. + */ + clslua_setup_env(L); + + /* + * Create table to hold registered (valid) handlers. + * + * Must be done before running the script for the first time because the + * script will immediately try to register one or more handlers using + * cls.register(function), which depends on this table. + */ + lua_pushlightuserdata(L, &clslua_registered_handle_reg_key); + lua_newtable(L); + lua_settable(L, LUA_REGISTRYINDEX); + + /* load and compile chunk */ + if (luaL_loadstring(L, ctx->cmd.script.c_str())) + return lua_error(L); + + /* execute chunk */ + lua_call(L, 0, 0); + + /* no error, but nothing left to do */ + if (!strlen(funcname)) { + CLS_LOG(10, "no handler name provided"); + ctx->ret = 0; /* success */ + return 0; + } + + lua_getglobal(L, funcname); + if (lua_type(L, -1) != LUA_TFUNCTION) { + CLS_ERR("error: unknown handler or not function: %s", funcname); + ctx->ret = -EOPNOTSUPP; + return 0; + } + + /* throw error if function is not registered */ + clslua_check_registered_handler(L); + + /* create input/output bufferlist objects */ + clslua_pushbufferlist(L, &ctx->cmd.input); + clslua_pushbufferlist(L, &ctx->reply.output); + + /* + * Call the target Lua object class handler. If the call is successful then + * we will examine the return value here and store it in the context. Errors + * that occur are handled in the top-level eval() function. + */ + int top = lua_gettop(L); + lua_call(L, 2, LUA_MULTRET); + + /* store return value in context */ + if (!(lua_gettop(L) + 3 - top)) + lua_pushinteger(L, 0); + ctx->ret = luaL_checkint(L, -1); + + return 0; +} + +/* + * Main handler. Proxies the Lua VM and the Lua-defined handler. + */ +static int eval(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + struct clslua_hctx ctx; + lua_State *L = NULL; + int ret = -EIO; + + try { + bufferlist::iterator iter = in->begin(); + ::decode(ctx.cmd, iter); + } catch (const buffer::error &err) { + CLS_ERR("error decoding input command structure"); + ret = -EINVAL; + goto out; + } + + /* save context */ + ctx.hctx = &hctx; + ctx.error.error = false; + + /* build lua vm state */ + L = luaL_newstate(); + if (!L) { + CLS_ERR("error creating new Lua state"); + goto out; + } + + /* panic handler for unhandled errors */ + lua_atpanic(L, &cls_lua_atpanic); + + if (setjmp(cls_lua_panic_jump) == 0) { + + /* + * Stash the handler context in the register. It contains the objclass + * method context, global error state, and the command and reply structs. + */ + lua_pushlightuserdata(L, &clslua_hctx_reg_key); + lua_pushlightuserdata(L, &ctx); + lua_settable(L, LUA_REGISTRYINDEX); + + /* Process and run the script */ + lua_pushcfunction(L, clslua_eval); + ret = lua_pcall(L, 0, 0, 0); + + /* Encountered an error? */ + if (ret) { + struct clslua_err *err = clslua_checkerr(L); + if (!err) { + CLS_ERR("error: cls_lua state machine: unexpected error"); + assert(0); + } + + /* Error origin a cls_cxx_* method? */ + if (err->error) { + ret = err->ret; /* cls_cxx_* return value */ + + /* Errors always abort. Fix up ret and log error */ + if (ret >= 0) { + CLS_ERR("error: unexpected handler return value"); + ret = -EFAULT; + } + + } else + ret = -EIO; /* Generic error code */ + + CLS_ERR("error: %s", lua_tostring(L, -1)); + ctx.reply.log.push_back(lua_tostring(L, -1)); + + } else { + /* + * No Lua error encountered while running the script, but the handler + * may still have returned an error code (e.g. an errno value). + */ + ret = ctx.ret; + } + + } else { + CLS_ERR("error: recovering from Lua panic"); + ret = -EFAULT; + } + +out: + if (L) + lua_close(L); + ::encode(ctx.reply, *out); + return ret; +} + +void __cls_init() +{ + CLS_LOG(20, "Loaded lua class!"); + + cls_register("lua", &h_class); + + cls_register_cxx_method(h_class, "eval", + CLS_METHOD_RD | CLS_METHOD_WR, eval, &h_eval); +} diff --git a/src/cls/lua/cls_lua.h b/src/cls/lua/cls_lua.h new file mode 100644 index 00000000000..3444386e914 --- /dev/null +++ b/src/cls/lua/cls_lua.h @@ -0,0 +1,49 @@ +#ifndef CEPH_CLS_LUA_H +#define CEPH_CLS_LUA_H + +#define LOG_LEVEL_DEFAULT 10 + +struct clslua_cmd { + string script; + string funcname; + bufferlist input; + + void encode(bufferlist& bl) const { + ENCODE_START(1, 1, bl); + ::encode(script, bl); + ::encode(funcname, bl); + ::encode(input, bl); + ENCODE_FINISH(bl); + } + + void decode(bufferlist::iterator& bl) { + DECODE_START(1, bl); + ::decode(script, bl); + ::decode(funcname, bl); + ::decode(input, bl); + DECODE_FINISH(bl); + } +}; +WRITE_CLASS_ENCODER(clslua_cmd); + +struct clslua_reply { + vector<string> log; + bufferlist output; + + void encode(bufferlist& bl) const { + ENCODE_START(1, 1, bl); + ::encode(log, bl); + ::encode(output, bl); + ENCODE_FINISH(bl); + } + + void decode(bufferlist::iterator& bl) { + DECODE_START(1, bl); + ::decode(log, bl); + ::decode(output, bl); + DECODE_FINISH(bl); + } +}; +WRITE_CLASS_ENCODER(clslua_reply); + +#endif diff --git a/src/test/cls_lua/test_cls_lua.cc b/src/test/cls_lua/test_cls_lua.cc new file mode 100644 index 00000000000..e73577f6a8a --- /dev/null +++ b/src/test/cls_lua/test_cls_lua.cc @@ -0,0 +1,361 @@ +#include <errno.h> +#include "include/encoding.h" +#include "include/rados.h" +#include "include/rados/librados.h" +#include "include/types.h" +#include "gtest/gtest.h" +#include "test/librados/test.h" +#include "cls/lua/cls_lua.h" +#include "liblua/lua.hpp" + +/* + * Auto-generated during build process. It includes the Lua unit test script + * in the current directory, 'test_script.lua', serialized as a C string. See + * src/Makefile.am for details on how that is generated. + * + * Makes available a static char* variable named 'cls_lua_test_script'. + */ +#include "test_script.h" +static string test_script; + +/* + * Test harness uses single pool for the entire test case, and generates + * unique object names for each test. + */ +class ClsLua : public ::testing::Test { + protected: + static void SetUpTestCase() { + pool_name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(pool_name, rados)); + ASSERT_EQ(0, rados.ioctx_create(pool_name.c_str(), ioctx)); + + /* auto-generated from test_script.lua */ + test_script.assign(cls_lua_test_script); + } + + static void TearDownTestCase() { + ioctx.close(); + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, rados)); + } + + virtual void SetUp() { + /* Grab test names to build unique objects */ + const ::testing::TestInfo* const test_info = + ::testing::UnitTest::GetInstance()->current_test_info(); + + /* Create unique string using test/testname/pid */ + std::stringstream ss_oid; + ss_oid << test_info->test_case_name() << "_" << + test_info->name() << "_" << getpid(); + + /* Unique object for test to use */ + oid = ss_oid.str(); + + /* + * Setup Lua env. + * + * We don't want to introduce a full dependency on the C++ MessagePack + * library. However, it is a common case that a user will use MsgPack to + * interface C++ and Lua. Rather than use the C++ library, we are going to + * just re-use the Lua MsgPack library. However, it means we need to run + * some Lua to actually pack and unpack the objects. + * + * It isn't the most robust test, but assuming the Lua MsgPack library + * conforms to the standard (and this is the version used in Redis + * production Lua support), it should be just fine. + */ + L = luaL_newstate(); + ASSERT_TRUE(L != NULL); + + /* load msgpack lib */ + lua_pushcfunction(L, luaopen_cmsgpack); + lua_pushstring(L, "cmsgpack"); + lua_call(L, 1, 0); + } + + virtual void TearDown() { + lua_close(L); + } + + /* + * Helper function. This functionality should eventually make its way into + * a clslua client library of some sort. + */ + int __clslua_exec(const string& oid, const string& script, + librados::bufferlist *input = NULL, const string& funcname = "") + { + struct clslua_cmd cmd; + cmd.script = script; + cmd.funcname = funcname; + if (input) + cmd.input = *input; + + bufferlist inbl; + ::encode(cmd, inbl); + + bufferlist outbl; + int ret = ioctx.exec(oid, "lua", "eval", inbl, outbl); + + ::decode(reply, outbl); + + return ret; + } + + int clslua_exec(const string& script, librados::bufferlist *input = NULL, + const string& funcname = "") + { + return __clslua_exec(oid, script, input, funcname); + } + + static librados::Rados rados; + static librados::IoCtx ioctx; + static string pool_name; + static string test_script; + + string oid; + struct clslua_reply reply; + + lua_State *L; +}; + +librados::Rados ClsLua::rados; +librados::IoCtx ClsLua::ioctx; +string ClsLua::pool_name; +string ClsLua::test_script; + +TEST_F(ClsLua, SyntaxError) { + ASSERT_EQ(-EIO, clslua_exec("-")); +} + +TEST_F(ClsLua, EmptyScript) { + ASSERT_EQ(0, clslua_exec("")); +} + +TEST_F(ClsLua, RetVal) { + /* handlers can return numeric values */ + ASSERT_EQ(1, clslua_exec(test_script, NULL, "rv_h1")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "rv_h0")); + ASSERT_EQ(-1, clslua_exec(test_script, NULL, "rv_hn1")); + ASSERT_EQ(1, clslua_exec(test_script, NULL, "rv_hs1")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "rv_hs0")); + ASSERT_EQ(-1, clslua_exec(test_script, NULL, "rv_hsn1")); + + /* no return value is success */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "rv_h")); + + /* non-numeric return values are errors */ + ASSERT_EQ(-EIO, clslua_exec(test_script, NULL, "rv_hnil")); + ASSERT_EQ(-EIO, clslua_exec(test_script, NULL, "rv_ht")); + ASSERT_EQ(-EIO, clslua_exec(test_script, NULL, "rv_hstr")); +} + +TEST_F(ClsLua, Create) { + /* create works */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "create_c")); + + /* exclusive works */ + ASSERT_EQ(-EEXIST, clslua_exec(test_script, NULL, "create_c")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "create_cne")); +} + +TEST_F(ClsLua, Pcall) { + /* create and error works */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "pcall_c")); + ASSERT_EQ(-EEXIST, clslua_exec(test_script, NULL, "pcall_c")); + + /* pcall masks the error */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "pcall_pc")); + + /* pcall lets us get the failed return value */ + ASSERT_EQ(-EEXIST, clslua_exec(test_script, NULL, "pcall_pcr")); + + /* + * the first call in pcr2 will fail (check ret != 0), and the second pcall + * should also fail (we check with a bogus return value to mask real + * errors). This is also an important check for our error handling because + * we need a case where two functions in the same handler fail to exercise + * our internal error book keeping. + */ + ASSERT_EQ(-9999, clslua_exec(test_script, NULL, "pcall_pcr2")); +} + +TEST_F(ClsLua, Remove) { + /* object doesn't exist */ + ASSERT_EQ(-ENOENT, clslua_exec(test_script, NULL, "remove_r")); + + /* can remove */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "remove_c")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "remove_r")); + ASSERT_EQ(-ENOENT, clslua_exec(test_script, NULL, "remove_r")); +} + +TEST_F(ClsLua, Stat) { + /* build object and stat */ + char buf[1024]; + bufferlist bl; + bl.append(buf, sizeof(buf)); + ASSERT_EQ(0, ioctx.write_full(oid, bl)); + uint64_t size; + time_t mtime; + ASSERT_EQ(0, ioctx.stat(oid, &size, &mtime)); + + /* test stat success */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "stat_ret")); + + /* unpack msgpack msg with Lua msgpack library */ + lua_getglobal(L, "cmsgpack"); + lua_getfield(L, -1, "unpack"); + lua_pushlstring(L, reply.output.c_str(), reply.output.length()); + ASSERT_EQ(0, lua_pcall(L, 1, 1, 0)); + /* array on top of stack now */ + + /* check size -- first array element */ + lua_pushinteger(L, 1); + lua_gettable(L, -2); + ASSERT_EQ(lua_tointeger(L, -1), (int)size); + lua_pop(L, 1); + + /* check mtime -- second array element */ + lua_pushinteger(L, 2); + lua_gettable(L, -2); + ASSERT_EQ(lua_tointeger(L, -1), (int)mtime); + lua_pop(L, 1); + + /* test object dne */ + ASSERT_EQ(-ENOENT, __clslua_exec("dne", test_script, NULL, "stat_sdne")); + + /* can capture error with pcall */ + ASSERT_EQ(-ENOENT, __clslua_exec("dne", test_script, NULL, "stat_sdne_pcall")); +} + +TEST_F(ClsLua, MapSetVal) { + /* build some input value */ + bufferlist orig_val; + ::encode("this is the original value yay", orig_val); + + /* have the lua script stuff the data into a map value */ + ASSERT_EQ(0, clslua_exec(test_script, &orig_val, "map_set_val")); + + /* grap the key now and compare to orig */ + map<string, bufferlist> out_map; + set<string> out_keys; + out_keys.insert("foo"); + ASSERT_EQ(0, ioctx.omap_get_vals_by_keys(oid, out_keys, &out_map)); + bufferlist out_bl = out_map["foo"]; + string out_val; + ::decode(out_val, out_bl); + ASSERT_EQ(out_val, "this is the original value yay"); +} + +TEST_F(ClsLua, MapGetVal) { + /* write some data into a key */ + string msg = "This is a test message"; + bufferlist orig_val; + orig_val.append(msg.c_str(), msg.size()); + map<string, bufferlist> orig_map; + orig_map["foo"] = orig_val; + ASSERT_EQ(0, ioctx.omap_set(oid, orig_map)); + + /* now compare to what we put it */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "map_get_val_foo")); + + /* check return */ + string ret_val; + ret_val.assign(reply.output.c_str(), reply.output.length()); + ASSERT_EQ(ret_val, msg); + + /* error case */ + ASSERT_EQ(-ENOENT, clslua_exec(test_script, NULL, "map_get_val_dne")); +} + +TEST_F(ClsLua, Read) { + /* put data into object */ + string msg = "This is a test message"; + bufferlist bl; + bl.append(msg.c_str(), msg.size()); + ASSERT_EQ(0, ioctx.write_full(oid, bl)); + + /* get lua to read it and send it back */ + ASSERT_EQ(0, clslua_exec(test_script, NULL, "read")); + + /* check return */ + string ret_val; + ret_val.assign(reply.output.c_str(), reply.output.length()); + ASSERT_EQ(ret_val, msg); +} + +TEST_F(ClsLua, Log) { + ASSERT_EQ(0, clslua_exec("cls.log()")); + ASSERT_EQ(0, clslua_exec("s = cls.log(); cls.log(s);")); + ASSERT_EQ(0, clslua_exec("cls.log(1)")); + ASSERT_EQ(0, clslua_exec("cls.log(-1)")); + ASSERT_EQ(0, clslua_exec("cls.log('x')")); + ASSERT_EQ(0, clslua_exec("cls.log(0, 0)")); + ASSERT_EQ(0, clslua_exec("cls.log(1, 1)")); + ASSERT_EQ(0, clslua_exec("cls.log(-10, -10)")); + ASSERT_EQ(0, clslua_exec("cls.log('x', 'y')")); + ASSERT_EQ(0, clslua_exec("cls.log(1, 'one')")); + ASSERT_EQ(0, clslua_exec("cls.log(1, 'one', 'two')")); + ASSERT_EQ(0, clslua_exec("cls.log('one', 'two', 'three')")); + ASSERT_EQ(0, clslua_exec("s = cls.log('one', 'two', 'three'); cls.log(s);")); +} + +TEST_F(ClsLua, BufferlistEquality) { + ASSERT_EQ(0, clslua_exec(test_script, NULL, "bl_eq_empty_equal")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "bl_eq_empty_selfequal")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "bl_eq_selfequal")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "bl_eq_equal")); + ASSERT_EQ(0, clslua_exec(test_script, NULL, "bl_eq_notequal")); +} + +TEST_F(ClsLua, RunError) { + ASSERT_EQ(-EIO, clslua_exec(test_script, NULL, "runerr_c")); + ASSERT_GT((int)reply.log.size(), 0); +} + +TEST_F(ClsLua, HandleNotFunc) { + string script = "x = 1;"; + ASSERT_EQ(-EOPNOTSUPP, clslua_exec(script, NULL, "x")); +} + +TEST_F(ClsLua, Register) { + /* normal cases: register and maybe call the handler */ + string script = "function h() end; cls.register(h);"; + ASSERT_EQ(0, clslua_exec(script, NULL, "")); + ASSERT_EQ(0, clslua_exec(script, NULL, "h")); + + /* can register and call multiple handlers */ + script = "function h1() end; function h2() end;" + "cls.register(h1); cls.register(h2);"; + ASSERT_EQ(0, clslua_exec(script, NULL, "")); + ASSERT_EQ(0, clslua_exec(script, NULL, "h1")); + ASSERT_EQ(0, clslua_exec(script, NULL, "h2")); + + /* normal cases: register before function is defined */ + script = "cls.register(h); function h() end;"; + ASSERT_EQ(-EIO, clslua_exec(script, NULL, "")); + ASSERT_EQ(-EIO, clslua_exec(script, NULL, "h")); + + /* cannot call handler that isn't registered */ + script = "function h() end;"; + ASSERT_EQ(-EIO, clslua_exec(script, NULL, "h")); + + /* handler doesn't exist */ + script = "cls.register(lalala);"; + ASSERT_EQ(-EIO, clslua_exec(script, NULL, "")); + + /* handler isn't a function */ + script = "cls.register('some string');"; + ASSERT_EQ(-EIO, clslua_exec(script, NULL, "")); + + /* cannot register handler multiple times */ + script = "function h() end; cls.register(h); cls.register(h);"; + ASSERT_EQ(-EIO, clslua_exec(script, NULL, "")); +} + +TEST_F(ClsLua, ClsLog) { + string script = "cls.log('la tee da'); cls.log('coffee');"; + ASSERT_EQ(0, clslua_exec(script)); + ASSERT_EQ(reply.log[0], "la tee da"); + ASSERT_EQ(reply.log[1], "coffee"); +} diff --git a/src/test/cls_lua/test_script.lua b/src/test/cls_lua/test_script.lua new file mode 100644 index 00000000000..73c602a9087 --- /dev/null +++ b/src/test/cls_lua/test_script.lua @@ -0,0 +1,217 @@ +-- +-- This Lua script file contains all of the handlers used in the cls_lua unit +-- tests (src/test/cls_lua/test_cls_lua.cc). Each section header corresponds +-- to the ClsLua.XYZ test. +-- + +-- +-- Read +-- +function read(input, output) + size = cls.stat() + bl = cls.read(0, size) + output:append(bl:str()) +end + +cls.register(read) + +-- +-- MsgPack +-- +function msgpack(input, output) + data = input:str() + obj = cmsgpack.unpack(data) + cls.log(obj) + ret = cmsgpack.pack(obj) + cls.log(ret) + output:append(ret) +end + +cls.register(msgpack) + +-- +-- MapGetVal +-- +function map_get_val_foo(input, output) + bl = cls.map_get_val('foo') + output:append(bl:str()) +end + +function map_get_val_dne() + bl = cls.map_get_val('dne') +end + +cls.register(map_get_val_foo) +cls.register(map_get_val_dne) + +-- +-- Stat +-- +function stat_ret(input, output) + size, mtime = cls.stat() + data = {} + data[1] = size + data[2] = mtime + ret = cmsgpack.pack(data) + output:append(ret) +end + +function stat_sdne() + size, mtime = cls.stat() +end + +function stat_sdne_pcall() + ok, ret, size, mtime = pcall(cls.stat, o) + assert(ok == false) + assert(ret == -cls.ENOENT) + return ret +end + +cls.register(stat_ret) +cls.register(stat_sdne) +cls.register(stat_sdne_pcall) + +-- +-- RetVal +-- +function rv_h() end +function rv_h1() return 1; end +function rv_h0() return 0; end +function rv_hn1() return -1; end +function rv_hs1() return '1'; end +function rv_hs0() return '0'; end +function rv_hsn1() return '-1'; end +function rv_hnil() return nil; end +function rv_ht() return {}; end +function rv_hstr() return 'asdf'; end + +cls.register(rv_h) +cls.register(rv_h1) +cls.register(rv_h0) +cls.register(rv_hn1) +cls.register(rv_hs1) +cls.register(rv_hs0) +cls.register(rv_hsn1) +cls.register(rv_hnil) +cls.register(rv_ht) +cls.register(rv_hstr) + +-- +-- Create +-- +function create_c() cls.create(true); end +function create_cne() cls.create(false); end + +cls.register(create_c) +cls.register(create_cne) + +-- +-- Pcall +-- +function pcall_c() cls.create(true); end + +function pcall_pc() + ok, ret = pcall(cls.create, true) + assert(ok == false) + assert(ret == -cls.EEXIST) +end + +function pcall_pcr() + ok, ret = pcall(cls.create, true) + assert(ok == false) + assert(ret == -cls.EEXIST) + return ret +end + +function pcall_pcr2() + ok, ret = pcall(cls.create, true) + assert(ok == false) + assert(ret == -cls.EEXIST) + ok, ret = pcall(cls.create, true) + assert(ok == false) + assert(ret == -cls.EEXIST) + return -9999 +end + +cls.register(pcall_c) +cls.register(pcall_pc) +cls.register(pcall_pcr) +cls.register(pcall_pcr2) + +-- +-- Remove +-- +function remove_c() cls.create(true); end +function remove_r() cls.remove(); end + +cls.register(remove_c) +cls.register(remove_r) + +-- +-- MapSetVal +-- +function map_set_val(input, output) + cls.map_set_val('foo', input) +end + +cls.register(map_set_val) + +-- +-- BufferlistEquality +-- +function bl_eq_empty_equal(input, output) + bl1 = bufferlist.new() + bl2 = bufferlist.new() + assert(bl1 == bl2) +end + +function bl_eq_empty_selfequal() + bl1 = bufferlist.new() + assert(bl1 == bl1) +end + +function bl_eq_selfequal() + bl1 = bufferlist.new() + bl1:append('asdf') + assert(bl1 == bl1) +end + +function bl_eq_equal() + bl1 = bufferlist.new() + bl2 = bufferlist.new() + bl1:append('abc') + bl2:append('abc') + assert(bl1 == bl2) +end + +function bl_eq_notequal() + bl1 = bufferlist.new() + bl2 = bufferlist.new() + bl1:append('abc') + bl2:append('abcd') + assert(bl1 ~= bl2) +end + +cls.register(bl_eq_empty_equal) +cls.register(bl_eq_empty_selfequal) +cls.register(bl_eq_selfequal) +cls.register(bl_eq_equal) +cls.register(bl_eq_notequal) + +-- +-- RunError +-- +function runerr_a() + error('WTF') +end + +function runerr_b() + runerr_a() +end + +function runerr_c() + runerr_b() +end + +-- only runerr_c is called +cls.register(runerr_c) diff --git a/src/test/cls_lua/txt2str.cc b/src/test/cls_lua/txt2str.cc new file mode 100644 index 00000000000..2041a36a00f --- /dev/null +++ b/src/test/cls_lua/txt2str.cc @@ -0,0 +1,41 @@ +/* + * Adapted from linux-kernel/scripts/bin2c.c + * Jan 1999 Matt Mackall <mpm@selenic.com> + * + * And StackOverflow question + * http://stackoverflow.com/questions/2602013/read-whole-ascii-file-into-c-stdstring + */ +#include <string> +#include <fstream> +#include <streambuf> +#include <stdio.h> + +int main(int argc, char **argv) +{ + const char *filename = argv[1]; + + std::ifstream t(filename); + std::string str((std::istreambuf_iterator<char>(t)), + std::istreambuf_iterator<char>()); + + int cur = 0; + int len = str.length(); + const char *buf = str.c_str(); + + printf("static const char %s[] =\n", argv[2]); + + do { + printf(" \""); + while (cur < len) { + printf("\\x%02x", buf[cur]); + if (++cur % 16 == 0) + break; + } + printf("\""); + if (cur >= len) + printf(";"); + printf("\n"); + } while (cur < len); + + return 0; +} |