summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2012-07-21 16:32:07 +0100
committerDaniel Silverstone <dsilvers@digital-scurf.org>2012-07-21 16:32:07 +0100
commitb5460017f1e88baf283ebfaad341cd094f5041ff (patch)
tree445386ab7dbce5dbd3e97078225bb9be3e939960
parentb4d2d633cd88308487aa6e2b689a4328ce6023c1 (diff)
downloadsupple-b5460017f1e88baf283ebfaad341cd094f5041ff.tar.gz
Lots of stuff
-rw-r--r--Makefile15
-rw-r--r--README10
-rw-r--r--lib/supple.lua4
-rw-r--r--lib/supple/capi.c143
-rw-r--r--lib/supple/objects.lua111
-rw-r--r--lib/supple/request.lua37
-rw-r--r--notes/design149
-rw-r--r--test/test-supple.capi.lua134
-rw-r--r--test/test-supple.lua14
-rw-r--r--test/test-supple.objects.lua69
-rw-r--r--test/test-supple.request.lua54
11 files changed, 729 insertions, 11 deletions
diff --git a/Makefile b/Makefile
index a54466a..a90727b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,12 @@
all: test
-LMODULES := supple
+LMODULES := supple supple.request supple.objects
CMODULES := supple.capi
MODULES := $(LMODULES) $(CMODULES)
LUA_VER := 5.1
+TEST_MODULES := $(MODULES)
+
INST_BASE := /usr/local
LINST_ROOT := $(DESTDIR)$(INST_BASE)/share/lua/$(LUA_VER)
CINST_ROOT := $(DESTDIR)$(INST_BASE)/lib/lua/$(LUA_VER)
@@ -19,7 +21,8 @@ CMOD_OBJECTS := $(patsubst %,lib/%.o,$(subst .,/,$(CMODULES)))
INCS := -I/usr/include/lua$(LUA_VER)
OPT := -O0 -g
-CFLAGS := $(INCS) $(OPT) $(CFLAGS)
+WARN := -Wall -Werror
+CFLAGS := $(INCS) $(OPT) $(WARN) $(CFLAGS)
LFLAGS := -O1 -g -shared $(LFLAGS)
%.so: %.o
@@ -40,7 +43,11 @@ install: build
cp lib/$${MOD} $(CINST_ROOT)/$${MOD}; \
done
-LUA := LUA_PATH="$(shell pwd)/lib/?.lua;$(shell pwd)/extras/luacov/src/?.lua;;" LUA_CPATH="$(shell pwd)/lib/?.so;;" lua$(LUA_VER)
+ifeq ($(DEBUG),gdb)
+GDB := gdb --args
+endif
+
+LUA := LUA_PATH="$(shell pwd)/lib/?.lua;$(shell pwd)/extras/luacov/src/?.lua;;" LUA_CPATH="$(shell pwd)/lib/?.so;;" $(GDB) lua$(LUA_VER)
clean:
$(RM) luacov.report.out luacov.stats.out
@@ -57,7 +64,7 @@ example:
test: build
@$(RM) luacov.stats.out
@ERR=0; \
- for MOD in $(sort $(MODULES)); do \
+ for MOD in $(sort $(TEST_MODULES)); do \
echo -n "$${MOD}: "; \
$(LUA) test/test-$${MOD}.lua; \
test "x$$?" = "x0" || ERR=1; \
diff --git a/README b/README
index 9e1e51b..75acc11 100644
--- a/README
+++ b/README
@@ -9,11 +9,11 @@ developer implementing Supple in their project is responsible for ensuring that
any modules loaded into the subprocess cannot break the sandbox.
In order to reduce the chance of anything breaking the sandbox, Supple always
-presents remote objects as userdata and forces the use of coroutines in order
-to allow calls back and forth between the two ends of the sandbox connection.
-
-This means that, for example, methods can be called and passed callback
-functions which can thread back and forth unrestricted.
+presents remote objects as userdata and forces the use of a file descriptor in
+order to allow calls back and forth between the two ends of the sandbox
+connection. This means that, for example, methods can be called and passed
+callback functions which can thread back and forth with only a strict nesting
+requirement.
For examples of using Supple, please see the examples/ directory.
diff --git a/lib/supple.lua b/lib/supple.lua
index 5fc405b..0d87d20 100644
--- a/lib/supple.lua
+++ b/lib/supple.lua
@@ -8,6 +8,8 @@
--
local capi = require 'supple.capi'
+local request = require 'supple.request'
+local objects = require 'supple.objects'
local _VERSION = 1
local _ABI = 1
@@ -16,6 +18,8 @@ local VERSION = "Supple Version " .. tostring(_VERSION)
return {
capi = capi,
+ request = request,
+ objects = objects,
_VERSION = _VERSION,
VERSION = VERSION,
_ABI = _ABI,
diff --git a/lib/supple/capi.c b/lib/supple/capi.c
index 139cfe6..c413c20 100644
--- a/lib/supple/capi.c
+++ b/lib/supple/capi.c
@@ -13,13 +13,156 @@
#include <lua.h>
#include <lauxlib.h>
+#include <string.h>
+
/* The API exposed, increment on backward-compatible changes */
#define CAPI_API 1
/* The ABI exposed, increment on backward-incompatible changes */
#define CAPI_ABI 1
+static int
+supple_capi_explain(lua_State *L)
+{
+ int one_type;
+ if (lua_gettop(L) != 2) {
+ return luaL_error(L, "Expected 2 args got %d", lua_gettop(L));
+ }
+ one_type = lua_type(L, 1);
+ if ((one_type != LUA_TTABLE) &&
+ (one_type != LUA_TFUNCTION) &&
+ (one_type != LUA_TUSERDATA)) {
+ return luaL_error(L,
+ "Expected one of table, function or userdata. " \
+ "Got %s instead",
+ lua_typename(L, one_type));
+ }
+ if (lua_type(L, 2) != LUA_TSTRING) {
+ return luaL_error(L, "Expected string, got %s instead",
+ lua_typename(L, lua_type(L, 2)));
+ }
+ lua_newtable(L);
+ lua_pushvalue(L, 2);
+ lua_setfield(L, -2, "tag");
+ lua_pushstring(L, lua_typename(L, one_type));
+ lua_setfield(L, -2, "type");
+ if (one_type != LUA_TFUNCTION) {
+ /* Get the metatable */
+ if (lua_getmetatable(L, 1) != 0) {
+ int idx = 1;
+ /* Prepare a methods table */
+ lua_newtable(L);
+ lua_pushnil(L); /* For iterating */
+ while (lua_next(L, -3) != 0) { /* Iterate the metatable */
+ lua_pop(L, 1); /* Don't need the value */
+ if (lua_type(L, -1) != LUA_TSTRING) {
+ /* Not a string key */
+ } else if (strcmp(lua_tostring(L, -1),
+ "__mode") == 0) {
+ /* mode key is ignored */
+ } else {
+ /* dup */
+ lua_pushvalue(L, -1);
+ /* And copy into methods */
+ lua_rawseti(L, -3, idx++);
+ }
+ /* Key is left ready for lua_next() */
+ }
+ /* Stuff the methods table into the return */
+ lua_setfield(L, -3, "methods");
+ /* And pop the metatable */
+ lua_pop(L, 1);
+ }
+ }
+ /* And return the new table */
+ return 1;
+}
+
+static int
+supple_capi_proxy_tostring(lua_State *L)
+{
+ /* arg 1 is the object, upvalue 1 is the original type */
+ const char *orig_type = lua_tostring(L, lua_upvalueindex(1));
+ lua_pushfstring(L, "%s: %p", orig_type, lua_touserdata(L, 1));
+ return 1;
+}
+
+static int
+supple_capi_proxy_type(lua_State *L)
+{
+ /* arg 1 is the object, upvalue 1 is the original type */
+ lua_pushvalue(L, lua_upvalueindex(1));
+ return 1;
+}
+
+static int
+supple_capi_new_proxy(lua_State *L)
+{
+ void *proxy_ptr;
+ /* Input is a type string */
+ if (lua_type(L, 1) != LUA_TSTRING) {
+ return luaL_error(L, "Expected type string, got %s",
+ lua_typename(L, lua_type(L, 1)));
+ }
+ proxy_ptr = lua_newuserdata(L, 1);
+ if (proxy_ptr == NULL) {
+ return 0;
+ }
+ lua_newtable(L);
+ /* typestr, proxyobj, metatable */
+ /* First job is the tostring metamethod, we always add that */
+ lua_pushvalue(L, 1);
+ lua_pushcclosure(L, supple_capi_proxy_tostring, 1);
+ lua_setfield(L, -2, "__tostring");
+ /* next the __type metamethod for the lua level type call */
+ lua_pushvalue(L, 1);
+ lua_pushcclosure(L, supple_capi_proxy_type, 1);
+ lua_setfield(L, -2, "__type");
+ /* And now prevent anything poking in us later */
+ lua_pushboolean(L, 1);
+ lua_setfield(L, -2, "__metatable");
+
+ /* Finally set the metatable and return proxy, metatable */
+ lua_pushvalue(L, -1);
+ lua_setmetatable(L, -3);
+
+ return 2;
+}
+
+/* Replacement for lbaselib.c's type method, honouring __type */
+static int
+supple_capi_type(lua_State *L)
+{
+ lua_settop(L, 1);
+ if (lua_getmetatable(L, 1) != 0) {
+ /* There is a metatable, try and find __type */
+ lua_getfield(L, 2, "__type");
+ if (lua_isfunction(L, 3)) {
+ lua_pushvalue(L, 1);
+ lua_call(L, 1, 1);
+ return 1;
+ }
+ }
+
+ lua_pushstring(L, lua_typename(L, lua_type(L, 1)));
+
+ return 1;
+}
+
+/* Reimplementation of base type in case it's not available */
+static int
+supple_capi_rawtype(lua_State *L)
+{
+ lua_settop(L, 1);
+ lua_pushstring(L, lua_typename(L, lua_type(L, 1)));
+ return 1;
+}
+
static const struct luaL_Reg
supple_capi_functions[] = {
+ { "explain", supple_capi_explain },
+ { "new_proxy", supple_capi_new_proxy },
+ { "type", supple_capi_type },
+ { "rawtype", supple_capi_rawtype },
{ NULL, NULL }
};
diff --git a/lib/supple/objects.lua b/lib/supple/objects.lua
new file mode 100644
index 0000000..67cb71b
--- /dev/null
+++ b/lib/supple/objects.lua
@@ -0,0 +1,111 @@
+-- lib/supple/request.lua
+--
+-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
+--
+-- Contextual object storage and identification. Wrapping and unwrapping.
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+-- For licence terms, see COPYING
+--
+
+local capi = require 'supple.capi'
+
+local my_objects_by_obj = {}
+local my_objects_by_tag = {}
+local my_name = "UNSET"
+local my_counter = 0
+local their_objects_by_tag = setmetatable({}, { __mode="v" })
+local their_objects_by_obj = setmetatable({}, { __mode="k" })
+local proc_call = nil
+
+local type = capi.rawtype
+
+local function set_name(newname)
+ my_name = newname
+end
+
+local function set_proc_call(pc)
+ proc_call = pc
+end
+
+local integral = {
+ ["nil"] = true,
+ boolean = true,
+ string = true,
+ number = true,
+}
+
+local function give(obj)
+ -- If the object is integral, return it directly
+ if integral[type(obj)] then
+ return obj
+ end
+ -- If the passed object is one of their objects then "unwrap" it by
+ -- returning their tag.
+ local tag = their_objects_by_obj[obj];
+ if tag then
+ return { tag = tag }
+ end
+ -- If it's one of our objects which has already been wrapped, return our
+ -- tag
+ local tag = my_objects_by_obj[obj]
+ if tag then
+ return { tag = tag }
+ end
+ -- otherwise wrap it freshly for us and return that.
+ local tag = ("%s:%d"):format(my_name, my_counter)
+ local expn = capi.explain(obj, tag)
+ my_objects_by_obj[obj] = tag
+ my_objects_by_tag[tag] = obj
+ return expn
+end
+
+local function receive(obj)
+ -- If the object is integral, return it directly
+ if integral[type(obj)] then
+ return obj
+ end
+ -- It's not integral, so it must be a tagged object
+ assert(type(obj) == "table")
+ assert(type(obj.tag) == "string")
+ local tag = obj.tag
+ -- First up, is it one of our objects?
+ local ret = my_objects_by_tag[tag]
+ if ret then
+ return ret
+ end
+ -- Now is it a known one of their objects?
+ ret = their_objects_by_tag[tag]
+ if ret then
+ return ret
+ end
+ -- Okay, prepare a proxy?
+ assert(type(obj.type) == "string")
+ local proxy, mt = capi.new_proxy(obj.type)
+ their_objects_by_tag[tag] = proxy
+ their_objects_by_obj[proxy] = tag
+ -- Fill out the metatable
+ obj.methods = obj.methods or {}
+ obj.methods[#obj.methods+1] = "__gc"
+ if obj.type == "function" then
+ obj.methods[#obj.methods+1] = "__call"
+ end
+ if obj.type == "table" then
+ obj.methods[#obj.methods+1] = "__index"
+ obj.methods[#obj.methods+1] = "__newindex"
+ end
+ for _, name in ipairs(obj.methods or {}) do
+ mt[name] = function(...)
+ proc_call(tag, name, ...)
+ end
+ end
+ -- And return the proxy object
+ return proxy
+end
+
+return {
+ set_name = set_name,
+ set_proc_call = set_proc_call,
+ give = give,
+}
diff --git a/lib/supple/request.lua b/lib/supple/request.lua
new file mode 100644
index 0000000..7f01e99
--- /dev/null
+++ b/lib/supple/request.lua
@@ -0,0 +1,37 @@
+-- lib/supple/request.lua
+--
+-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
+--
+-- Request/response serialisation/deserialisation including contextual object
+-- management and organisation.
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+-- For licence terms, see COPYING
+--
+
+local tconcat = table.concat
+
+local function serialise_error(errstr, traceback)
+ return tconcat {
+ "error=true,",
+ ("message=%q,"):format(errstr),
+ ("traceback=%q"):format(traceback)
+ }
+end
+
+local function serialise_request(obj, method, ...)
+end
+
+local function serialise_response(...)
+end
+
+local function deserialise_entity(entity)
+end
+
+return {
+ error = serialise_error,
+ request = serialise_request,
+ response = serialise_response,
+ deserialise = deserialise_entity,
+}
diff --git a/notes/design b/notes/design
new file mode 100644
index 0000000..3cd1c90
--- /dev/null
+++ b/notes/design
@@ -0,0 +1,149 @@
+Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine - Supple
+=======================================================================
+
+# Requirements (mostly via Gitano)
+
+* Be able to run complex hook scripts in a totally restricted sandbox.
+* Be able to access appropriately filtered objects/functions/data from the
+ caller in some manner
+* Have the caller able to call back into the sandbox
+* Have the sandbox be in a subprocess which can be ulimit()ed etc.
+* Perhaps have some way to ensure the subprocess cannot access any of the
+ filesystem. (chrooted?)
+* Allow the caller to inject functions, values, etc into the remote sandbox
+ before execution. Perhaps even modules etc.
+* Be a minimal overhead system but still be 100% policy controllable at the
+ caller side of the sandbox
+* May as well use Luxio for the low level operations.
+* Provide the code to execute over the sandbox connection after the sandbox
+ has locked itself down.
+
+# Possible implementation details
+
+* Serialise requests and responses as simple packets which can be punted
+ across a single FD pair
+* Perhaps use userdata to ensure that there's no chance of rawset() etc
+ happening on either end for 'remote' objects.
+* Cannot use coroutines since pure lua 5.1 cannot yield across metamethod
+ boundaries :-( As such, pcall()s everydamnedwhere :-)
+* Types of packets passing across the link
+ * Procedure call
+ * Procedure reply
+
+## Context objects
+
+Contextual objects are anything non-integral (i.e. a table or userdata) which
+is passed across the connections. They get transformed into contextual objects
+which are always represented on the remote end as a userdata which has
+appropriate metamethods to cope with the object on the far end. Only
+metamethods which are defined by the sending end (and always __index and
+__newindex if it's a table) are defined on the receiving end in order to
+increase realism. The *sandboxed* end of the connection has pairs() and
+ipairs() augmented to cope with contextual objects, the non-sandboxed end has
+no such help.
+
+Contextual objects also represent remote functions (although they always
+tostring() to "function" rather than "table" or "userdata") and only support
+__call.
+
+## Notification of contextual object
+
+Contextual objects are always transferred as tables with the following keys:
+
+* type: "table" "userdata" "function"
+* tag: A tag for the caller to use when referring to this object in future
+* methods: Optional table containing the set of metamethods defined on this
+ object.
+
+Note that the remote end will always augment the methods list with __gc so that
+the notification of GC can be made. Also if the type is "table" then __index
+and __newindex metamethods will be added for those normal table operations. If
+the type is "function" then __call will also be forcibly added.
+
+At either end, the set of contextual objects is held in tables.
+
+* my_objects -- objects sent from this side to the other which have not been
+ garbage collected on the remote end. Strong key (object) strong value
+ (tag)
+* their_objects -- objects from the remote side strong key (tag) weak value
+ (object)
+
+Also, the objects created as part of a function call are held strongly by the
+receiver of the procedure call so that they *cannot* go out of scope until
+the procedure call has sent its return value to the caller.
+
+## Procedure call
+
+Procedure calls are quite simple, they consist of a contextual object and a
+procedure name and a set of arguments.
+
+The procedure names are always of the form of metamethod names.
+
+The __gc metamethod tells the remote end that the local end is 'forgetting' the
+contextual object and the remote end can drop it from its cache.
+
+Thusly a procedure call (unserialised) looks approximately like this:
+
+ {
+ object = "some object tag",
+ method = "__something",
+ args = {
+ n = 4, -- The number of arguments. Trailing nils might be lost.
+ "string arg",
+ 1234, -- number argument
+ false, -- boolean argument
+ { -- Object argument of some kind
+ type = "table",
+ tag = "tag name",
+ methods = { "__call" }
+ }
+ },
+ }
+
+If the call is passing a contextual object from the remote end back again then
+it omits the type and methods values, only passing the tag. Thusly whenever a
+contextual object is passed as an argument, it is automatically unwrapped at
+the remote end into the local object. This means that we do not end up with
+multiple layers of contextual objects going back and forth.
+
+## Procedure returns
+
+Procedure returns are always of the following form:
+
+ {
+ error = true,
+ message = "some message",
+ traceback = "some traceback"
+ }
+
+or
+
+ {
+ error = false,
+ results = {
+ n = N, -- The number of results detected
+ -- Any returns as a numerically indexed table.
+ -- Note that trailing nils may be collapsed.
+ }
+ }
+
+If the procedure return is an error form then the caller then stashes the
+traceback in the right place and raises a Lua error on the caller's side. This
+process might bounce back and forth until something catches the error in full.
+
+Otherwise, the results are returned exactly as arguments were provided,
+i.e. strings, booleans and numbers are passed directly, and anything else is an
+object. If a brand new object is returned as part of the call then the
+lifetime of that object will very much be limited to the caller remembering to
+anchor it somewhere. Otherwise it's quite possible that during the return from
+the procedure call, a __gc call will be automatically invoked to clean up.
+
+# Serialising requests and replies
+
+All requests and replies are structured as Lua tables once deserialised. The
+serialised form of the messages is simply a string which can be loaded and run
+to return the values. The limited number of formats mean that the serialisers
+can be written explicitly and the deserialiser can simply be a generic loader
+followed by a series of asserts and measures to ensure nothing malicious gets
+injected.
+
diff --git a/test/test-supple.capi.lua b/test/test-supple.capi.lua
index 30dbf81..7438874 100644
--- a/test/test-supple.capi.lua
+++ b/test/test-supple.capi.lua
@@ -1,4 +1,4 @@
--- test/test-supple.lua
+-- test/test-supple.capi.lua
--
-- Supple - Tests for the capi module
--
@@ -38,6 +38,138 @@ function suite.capi_has_abi()
assert(capi._ABI, "No _ABI found in the CAPI")
end
+function suite.capi_explain_non_table_etc()
+ local ok = pcall(capi.explain, nil, "");
+ assert(ok == false, "Explained nil unexpectedly")
+ ok = pcall(capi.explain, true, "");
+ assert(ok == false, "Explained true unexpectedly")
+ ok = pcall(capi.explain, false, "");
+ assert(ok == false, "Explained false unexpectedly")
+ ok = pcall(capi.explain, 123, "");
+ assert(ok == false, "Explained number unexpectedly")
+ ok = pcall(capi.explain, "", "");
+ assert(ok == false, "Explained string unexpectedly")
+ ok = pcall(capi.explain, coroutine.create(function()end), "");
+ assert(ok == false, "Explained coroutine unexpectedly")
+end
+
+function suite.capi_explain_bad_tag()
+ local ok = pcall(capi.explain, {}, nil)
+ assert(ok == false, "Explained with nil tag unexpectedly")
+ ok = pcall(capi.explain, {}, 1233)
+ assert(ok == false, "Explained with number tag unexpectedly")
+ ok = pcall(capi.explain, {}, true)
+ assert(ok == false, "Explained with true tag unexpectedly")
+ ok = pcall(capi.explain, {}, false)
+ assert(ok == false, "Explained with false tag unexpectedly")
+ ok = pcall(capi.explain, {}, {})
+ assert(ok == false, "Explained with table tag unexpectedly")
+ ok = pcall(capi.explain, {}, function()end)
+ assert(ok == false, "Explained with function tag unexpectedly")
+ ok = pcall(capi.explain, {}, coroutine.create(function()end))
+ assert(ok == false, "Explained with coroutine tag unexpectedly")
+end
+
+function suite.capi_explain_simple_table()
+ local expn = capi.explain({}, "fish")
+ assert(type(expn) == "table", "Explanation wasn't a table")
+ assert(type(expn.tag) == "string", "Tag not a string")
+ assert(expn.tag == "fish", "Tag wasn't as passed in")
+ assert(type(expn.type) == "string", "Type wasn't a string")
+ assert(expn.type == "table", "Type didn't declare it is a table")
+ assert(expn.methods == nil, "Methods table was present in some form")
+end
+
+function suite.capi_explain_table_with_mode()
+ local tab = setmetatable({}, { __mode="k"})
+ local expn = capi.explain(tab, "fish")
+ assert(type(expn) == "table", "Explanation wasn't a table")
+ assert(type(expn.tag) == "string", "Tag not a string")
+ assert(expn.tag == "fish", "Tag wasn't as passed in")
+ assert(type(expn.type) == "string", "Type wasn't a string")
+ assert(expn.type == "table", "Type didn't declare it is a table")
+ assert(type(expn.methods) == "table", "Methods were not present")
+ assert(next(expn.methods) == nil, "Methods had something in")
+end
+
+function suite.capi_explain_table_with_index()
+ local tab = setmetatable({}, { __index = {} })
+ local expn = capi.explain(tab, "fish")
+ assert(type(expn) == "table", "Explanation wasn't a table")
+ assert(type(expn.tag) == "string", "Tag not a string")
+ assert(expn.tag == "fish", "Tag wasn't as passed in")
+ assert(type(expn.type) == "string", "Type wasn't a string")
+ assert(expn.type == "table", "Type didn't declare it is a table")
+ assert(type(expn.methods) == "table", "Methods were not present")
+ assert(expn.methods[1] == "__index", "Missing __index")
+ assert(expn.methods[2] == nil, "Something other than __index")
+end
+
+function suite.capi_explain_function()
+ local expn = capi.explain(function()end, "fish")
+ assert(type(expn) == "table", "Explanation wasn't a table")
+ assert(type(expn.tag) == "string", "Tag not a string")
+ assert(expn.tag == "fish", "Tag wasn't as passed in")
+ assert(type(expn.type) == "string", "Type wasn't a string")
+ assert(expn.type == "function", "Type didn't declare it is a table")
+ assert(expn.methods == nil, "Methods table was present in some form")
+end
+
+function suite.capi_type_chains()
+ assert(capi.type({}) == "table", "Plain table wasn't a table")
+ local t = setmetatable({}, { __type = function() return "jeff" end })
+ assert(capi.type(t) == "jeff", "Chained table wasn't jeff")
+end
+
+function suite.capi_rawtype()
+ assert(capi.rawtype() == "nil", "nil was't nil")
+ assert(capi.rawtype({}) == "table", "table wasn't")
+ assert(capi.rawtype("") == "string", "string wasn't")
+ assert(capi.rawtype(false) == "boolean", "boolean wasn't")
+ assert(capi.rawtype(function()end) == "function", "function wasn't")
+ assert(capi.rawtype(coroutine.create(function()end)) == "thread",
+ "coroutine wasn't")
+ assert(capi.rawtype(capi.new_proxy("table")) == "userdata",
+ "userdata wasn't")
+end
+
+function suite.capi_new_proxy_fails()
+ local ok = pcall(capi.new_proxy)
+ assert(ok == false, "Made a new proxy with a nil type string")
+ ok = pcall(capi.new_proxy, {})
+ assert(ok == false, "Made a new proxy with a table type string")
+end
+
+function suite.capi_new_proxy_function()
+ local proxy, mt = capi.new_proxy("function")
+ local sparetype = mt.__type
+ mt.__type = nil
+ assert(capi.type(proxy) == "userdata", "Proxy wasn't userdata")
+ mt.__type = sparetype
+ assert(capi.type(proxy) == "function", "Proxy did not override type")
+ assert(tostring(proxy):match("^function: "), "Proxy didn't tostring nicely")
+end
+
+function suite.capi_new_proxy_table()
+ local proxy, mt = capi.new_proxy("table")
+ local sparetype = mt.__type
+ mt.__type = nil
+ assert(type(proxy) == "userdata", "Proxy wasn't userdata")
+ mt.__type = sparetype
+ assert(capi.type(proxy) == "table", "Proxy did not override type")
+ assert(tostring(proxy):match("^table: "), "Proxy didn't tostring nicely")
+end
+
+function suite.capi_new_proxy_table()
+ local proxy, mt = capi.new_proxy("userdata")
+ local sparetype = mt.__type
+ mt.__type = nil
+ assert(type(proxy) == "userdata", "Proxy wasn't userdata")
+ mt.__type = sparetype
+ assert(capi.type(proxy) == "userdata", "Proxy did not override type")
+ assert(tostring(proxy):match("^userdata: "), "Proxy didn't tostring nicely")
+end
+
local count_ok = 0
for _, testname in ipairs(testnames) do
-- print("Run: " .. testname)
diff --git a/test/test-supple.lua b/test/test-supple.lua
index 1053b87..fdd08e4 100644
--- a/test/test-supple.lua
+++ b/test/test-supple.lua
@@ -11,8 +11,10 @@
local luacov = require 'luacov'
-local supple = require 'supple'
local capi = require 'supple.capi'
+local request = require 'supple.request'
+local objects = require 'supple.objects'
+local supple = require 'supple'
local testnames = {}
@@ -35,6 +37,16 @@ function suite.capi_passed()
assert(supple.capi == capi, "Supple's capi entry is not supple.capi")
end
+function suite.request_passed()
+ assert(supple.request == request,
+ "Supple's request entry is not supple.request")
+end
+
+function suite.objects_passed()
+ assert(supple.objects == objects,
+ "Supple's objects entry is not supple.objects")
+end
+
local count_ok = 0
for _, testname in ipairs(testnames) do
-- print("Run: " .. testname)
diff --git a/test/test-supple.objects.lua b/test/test-supple.objects.lua
new file mode 100644
index 0000000..cb81420
--- /dev/null
+++ b/test/test-supple.objects.lua
@@ -0,0 +1,69 @@
+-- test/test-supple.lua
+--
+-- Supple - Tests for the core module
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+-- For Licence terms, see COPYING
+--
+
+-- Step one, start coverage
+
+local luacov = require 'luacov'
+
+local objects = require 'supple.objects'
+
+local testnames = {}
+
+local real_assert = assert
+local total_asserts = 0
+local function assert(...)
+ local retval = real_assert(...)
+ total_asserts = total_asserts + 1
+ return retval
+end
+
+local function add_test(suite, name, value)
+ rawset(suite, name, value)
+ testnames[#testnames+1] = name
+end
+
+local suite = setmetatable({}, {__newindex = add_test})
+
+function suite.give_integral_types()
+ assert(objects.give(nil) == nil, "Nil didn't pass through")
+ assert(objects.give(true) == true, "True didn't pass through")
+ assert(objects.give(false) == false, "False didn't pass through")
+ assert(objects.give(123) == 123, "Number didn't pass through")
+ assert(objects.give("") == "", "String didn't pass through")
+end
+
+function suite.set_name_works()
+ local unique_name = tostring({})
+ objects.set_name(unique_name)
+ local expn = objects.give({})
+ assert(expn.tag:find(unique_name), "set_name not passing through")
+end
+
+function suite.give_table()
+ local tab = {}
+ local expn = objects.give(tab)
+ assert(tab ~= expn, "Table passed through")
+
+end
+
+local count_ok = 0
+for _, testname in ipairs(testnames) do
+-- print("Run: " .. testname)
+ local ok, err = xpcall(suite[testname], debug.traceback)
+ if not ok then
+ print(err)
+ print()
+ else
+ count_ok = count_ok + 1
+ end
+end
+
+print(tostring(count_ok) .. "/" .. tostring(#testnames) .. " [" .. tostring(total_asserts) .. "] OK")
+
+os.exit(count_ok == #testnames and 0 or 1)
diff --git a/test/test-supple.request.lua b/test/test-supple.request.lua
new file mode 100644
index 0000000..5413cd8
--- /dev/null
+++ b/test/test-supple.request.lua
@@ -0,0 +1,54 @@
+-- test/test-supple.request.lua
+--
+-- Supple - Tests for the capi module
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+-- For Licence terms, see COPYING
+--
+
+-- Step one, start coverage
+
+local luacov = require 'luacov'
+
+local request = require 'supple.request'
+
+local testnames = {}
+
+local real_assert = assert
+local total_asserts = 0
+local function assert(...)
+ local retval = real_assert(...)
+ total_asserts = total_asserts + 1
+ return retval
+end
+
+local function add_test(suite, name, value)
+ rawset(suite, name, value)
+ testnames[#testnames+1] = name
+end
+
+local suite = setmetatable({}, {__newindex = add_test})
+
+function suite.serialise_error()
+ local err = request.error("m","tb")
+ assert(err == [[error=true,message="m",traceback="tb"]],
+ "Error did not serialise properly")
+
+end
+
+local count_ok = 0
+for _, testname in ipairs(testnames) do
+-- print("Run: " .. testname)
+ local ok, err = xpcall(suite[testname], debug.traceback)
+ if not ok then
+ print(err)
+ print()
+ else
+ count_ok = count_ok + 1
+ end
+end
+
+print(tostring(count_ok) .. "/" .. tostring(#testnames) .. " [" .. tostring(total_asserts) .. "] OK")
+
+os.exit(count_ok == #testnames and 0 or 1)