Add unit tests and correct bugs detected by the tests

This commit is contained in:
DanyLE 2023-01-25 17:01:01 +01:00
parent d4bb12c6b5
commit 7711526a7c
29 changed files with 1814 additions and 1024 deletions

View File

@ -22,16 +22,14 @@ silk_DATA = silkmvc/router.lua.tpl \
silkmvc/Template.lua \ silkmvc/Template.lua \
silkmvc/Logger.lua \ silkmvc/Logger.lua \
silkmvc/BaseObject.lua \ silkmvc/BaseObject.lua \
silkmvc/DBHelper.lua \
silkmvc/api.lua silkmvc/api.lua
coredir = $(libdir)/lua/silk/core coredir = $(libdir)/lua/silk/core
core_DATA = silkmvc/core/OOP.lua \ core_DATA = silkmvc/core/OOP.lua \
silkmvc/core/std.lua \ silkmvc/core/std.lua \
silkmvc/core/extra_mime.lua \ silkmvc/core/mimes.lua \
silkmvc/core/utils.lua \ silkmvc/core/utils.lua \
silkmvc/core/api.lua \ silkmvc/core/hook.lua \
silkmvc/core/cif.lua \
silkmvc/core/sqlite.lua silkmvc/core/sqlite.lua
# lua libraris & modules # lua libraris & modules

View File

@ -208,7 +208,7 @@ static int l_base64_decode(lua_State *L)
// decode data to a byte array // decode data to a byte array
lua_new_slice(L, len); lua_new_slice(L, len);
slice_t *vec = NULL; slice_t *vec = NULL;
vec = lua_check_slice(L, 2); vec = lua_check_slice(L, -1);
len = Base64decode((char *)vec->data, s); len = Base64decode((char *)vec->data, s);
vec->len = len; vec->len = len;
// lua_pushstring(L,dst); // lua_pushstring(L,dst);

View File

@ -14,15 +14,43 @@
#define SLICE "slice" #define SLICE "slice"
typedef struct { typedef struct {
size_t magic;
size_t len; size_t len;
uint8_t* data; uint8_t* data;
} slice_t; } slice_t;
#ifndef LUA_SLICE_MAGIC
/**
* @brief Send data to the server via fastCGI protocol
* This function is defined by the luad fcgi server
*
* @param fd the socket fd
* @param id the request id
* @param ptr data pointer
* @param size data size
* @return int
*/
int fcgi_send_slice(int fd, uint16_t id, uint8_t* ptr, size_t size);
/**
* @brief Get the magic number of the slice
* This value is defined by the luad fastCGI server
*
* @return size_t
*/
size_t lua_slice_magic();
#else
#define lua_slice_magic() (LUA_SLICE_MAGIC)
#define fcgi_send_slice(fd,id,ptr,size) (-1)
#endif
void lua_new_slice(lua_State*L, int n) void lua_new_slice(lua_State*L, int n)
{ {
size_t nbytes = sizeof(slice_t) + n * 1U; size_t nbytes = sizeof(slice_t) + n * 1U;
slice_t *a = (slice_t *)lua_newuserdata(L, nbytes); slice_t *a = (slice_t *)lua_newuserdata(L, nbytes);
a->data = &((char *)a)[sizeof(slice_t)]; a->data = &((char *)a)[sizeof(slice_t)];
a->magic = lua_slice_magic();
luaL_getmetatable(L, SLICE); luaL_getmetatable(L, SLICE);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);

View File

@ -1,4 +1,5 @@
#include "lua/lualib.h" #include "lua/lualib.h"
static int l_slice_send_to(lua_State* L);
void lua_new_light_slice(lua_State *L, int n, char *ptr) void lua_new_light_slice(lua_State *L, int n, char *ptr)
{ {
@ -6,6 +7,7 @@ void lua_new_light_slice(lua_State *L, int n, char *ptr)
slice_t *a = (slice_t *)lua_newuserdata(L, nbytes); slice_t *a = (slice_t *)lua_newuserdata(L, nbytes);
a->len = n; a->len = n;
a->data = ptr; a->data = ptr;
a->magic = lua_slice_magic();
luaL_getmetatable(L, SLICE); luaL_getmetatable(L, SLICE);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);
} }
@ -78,6 +80,14 @@ static int l_slice_write(lua_State *L)
return 1; return 1;
} }
static int l_slice_ptr(lua_State *L)
{
slice_t *a = lua_check_slice(L, 1);
lua_pushnumber(L, (size_t)a);
return 1;
}
static int l_slice_index(lua_State *L) static int l_slice_index(lua_State *L)
{ {
if(lua_isnumber(L,2)) if(lua_isnumber(L,2))
@ -91,10 +101,18 @@ static int l_slice_index(lua_State *L)
{ {
lua_pushcfunction(L, l_get_slice_size); lua_pushcfunction(L, l_get_slice_size);
} }
else if(strcmp(string, "write") == 0) else if(strcmp(string, "fileout") == 0)
{ {
lua_pushcfunction(L, l_slice_write); lua_pushcfunction(L, l_slice_write);
} }
else if(strcmp(string,"out") == 0)
{
lua_pushcfunction(L, l_slice_send_to);
}
else if(strcmp(string,"ptr") == 0)
{
lua_pushcfunction(L, l_slice_ptr);
}
else else
{ {
lua_pushnil(L); lua_pushnil(L);
@ -120,6 +138,16 @@ static int l_slice_to_string(lua_State *L)
return 1; return 1;
} }
static int l_slice_send_to(lua_State* L)
{
slice_t *a = lua_check_slice(L, 1);
int fd = (int) luaL_checknumber(L, 2);
uint16_t id = (uint16_t) luaL_checknumber(L, 3);
lua_pushboolean(L, fcgi_send_slice(fd, id, a->data, a->len) == 0);
return 1;
}
static const struct luaL_Reg slicemetalib[] = { static const struct luaL_Reg slicemetalib[] = {
{"unew", l_new_lightslice}, {"unew", l_new_lightslice},
{"new", l_new_slice}, {"new", l_new_slice},

View File

@ -83,6 +83,9 @@ static int l_db_query(lua_State *L)
int cols = sqlite3_column_count(statement); int cols = sqlite3_column_count(statement);
int result = 0; int result = 0;
int cnt = 1; int cnt = 1;
uint8_t* data = NULL;
size_t len = 0;
slice_t* vec = NULL;
// new table for data // new table for data
lua_newtable(L); lua_newtable(L);
while ((result = sqlite3_step(statement)) == SQLITE_ROW) while ((result = sqlite3_step(statement)) == SQLITE_ROW)
@ -91,10 +94,37 @@ static int l_db_query(lua_State *L)
lua_newtable(L); lua_newtable(L);
for (int col = 0; col < cols; col++) for (int col = 0; col < cols; col++)
{ {
const char *value = (const char *)sqlite3_column_text(statement, col);
const char *name = sqlite3_column_name(statement, col); const char *name = sqlite3_column_name(statement, col);
lua_pushstring(L, name); lua_pushstring(L, name);
lua_pushstring(L, value); int type = sqlite3_column_type(statement,col);
switch (type)
{
case SQLITE_INTEGER:
lua_pushnumber(L, sqlite3_column_int64(statement,col));
break;
case SQLITE_FLOAT:
lua_pushnumber(L, sqlite3_column_double(statement,col));
break;
case SQLITE_BLOB:
data = (uint8_t*)sqlite3_column_blob(statement, col);
len = sqlite3_column_bytes(statement, col);
if(len > 0)
{
lua_new_slice(L, len);
vec = lua_check_slice(L, -1);
(void)memcpy(vec->data, data, len);
}
else
{
lua_pushnil(L);
}
break;
default:
lua_pushstring(L, (const char *)sqlite3_column_text(statement, col));
break;
}
lua_settable(L, -3); lua_settable(L, -3);
} }
lua_settable(L, -3); lua_settable(L, -3);

View File

@ -41,11 +41,12 @@ function BaseController:switchLayout(name)
if self.main then if self.main then
self.registry.layout = name self.registry.layout = name
else else
self:log("Cannot switch layout since the controller "..self.class.." is not the main controller") self:warn("Cannot switch layout since the controller "..self.class.." is not the main controller")
end end
end end
function BaseController:setSession(key, value) function BaseController:setSession(key, value)
SESSION[key] = value SESSION[key] = value
end end
function BaseController:getSession(key) function BaseController:getSession(key)

View File

@ -30,9 +30,7 @@ function BaseModel:find(cond)
end end
function BaseModel:get(id) function BaseModel:get(id)
local data, order = self:find({exp = {["="] = {id = id}}}) return self.db:get(self.name, id)
if not data or #order == 0 then return false end
return data[1]
end end
function BaseModel:findAll() function BaseModel:findAll()
@ -46,6 +44,7 @@ function BaseModel:query(sql)
end end
function BaseModel:select(sel, sql_cnd) function BaseModel:select(sel, sql_cnd)
if self.db then return self.db:select(self.name, sel, sql_cnd) end local sql = string.format("SELECT %s FROM %s WHERE %s;", sel, self.name, sql_cnd)
if self.db then return self.db:query(sql) end
return nil return nil
end end

View File

@ -4,31 +4,34 @@ function BaseObject:subclass(name, args)
_G[name].class = name _G[name].class = name
end end
function BaseObject:log(msg, level) function BaseObject:log(level,msg,...)
level = level or "INFO"
if self.registry.logger then if self.registry.logger then
self.registry.logger:log(msg,level) self.registry.logger:log(level, msg,...)
end end
end end
function BaseObject:debug(msg) function BaseObject:debug(msg,...)
self:log(msg, "DEBUG") self:log(Logger.DEBUG, msg,...)
end
function BaseObject:info(msg,...)
self:log(Logger.INFO, msg,...)
end
function BaseObject:warn(msg,...)
self:log(Logger.WARN, msg,...)
end end
function BaseObject:print() function BaseObject:print()
print(self.class) self:debug(self.class)
end end
function BaseObject:error(msg, trace) function BaseObject:error(msg,...)
html() html()
--local line = debug.getinfo(1).currentline --local line = debug.getinfo(1).currentline
echo(msg) local emsg = string.format(msg or "ERROR",...)
self:log(msg,"ERROR") echo(emsg)
if trace then self:log(Logger.ERROR, msg,...)
debug.traceback=nil error(emsg)
error(msg)
else
error(msg)
end
return false return false
end end

View File

@ -1,144 +0,0 @@
sqlite = modules.sqlite()
if sqlite == nil then return 0 end
-- create class
BaseObject:subclass("DBHelper", {db = {}})
function DBHelper:createTable(tbl, m)
if self:available(tbl) then return true end
local sql = "CREATE TABLE " .. tbl .. "(id INTEGER PRIMARY KEY"
for k, v in pairs(m) do
if k ~= "id" then sql = sql .. "," .. k .. " " .. v end
end
sql = sql .. ");"
return sqlite.query(self.db, sql) == 1
end
function DBHelper:insert(tbl, m)
local keys = {}
local values = {}
for k, v in pairs(m) do
if k ~= "id" then
table.insert(keys, k)
if type(v) == "number" then
table.insert(values, v)
else
local t = "\"" .. v:gsub('"', '""') .. "\""
table.insert(values, t)
end
end
end
local sql = "INSERT INTO " .. tbl .. " (" .. table.concat(keys, ',') ..
') VALUES ('
sql = sql .. table.concat(values, ',') .. ');'
return sqlite.query(self.db, sql) == 1
end
function DBHelper:get(tbl, id)
return sqlite.select(self.db, tbl, "*", "id=" .. id)[1]
end
function DBHelper:getAll(tbl)
local data = sqlite.select(self.db, tbl, "*", "1=1")
if data == nil then return nil end
local a = {}
for n in pairs(data) do table.insert(a, n) end
table.sort(a)
return data, a
end
function DBHelper:find(tbl, cond)
local cnd = "1=1"
local sel = "*"
if cond.exp then cnd = self:gencond(cond.exp) end
if cond.order then
cnd = cnd .. " ORDER BY "
local l = {}
local i = 1
for k, v in pairs(cond.order) do
l[i] = k .. " " .. v
i = i + 1
end
cnd = cnd .. table.concat(l, ",")
end
if cond.limit then cnd = cnd .. " LIMIT " .. cond.limit end
if cond.fields then
sel = table.concat(cond.fields, ",")
-- print(sel)
end
local data = sqlite.select(self.db, tbl, sel, cnd)
if data == nil then return nil end
local a = {}
for n in pairs(data) do table.insert(a, n) end
table.sort(a)
return data, a
end
function DBHelper:select(tbl, sel, cnd)
local data = sqlite.select(self.db, tbl, sel, cnd)
if data == nil then return nil end
local a = {}
for n in pairs(data) do table.insert(a, n) end
table.sort(a)
return data, a
end
function DBHelper:query(sql) return sqlite.query(self.db, sql) == 1 end
function DBHelper:update(tbl, m)
local id = m['id']
if id ~= nil then
local lst = {}
for k, v in pairs(m) do
if (type(v) == "number") then
table.insert(lst, k .. "=" .. v)
else
table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"")
end
end
local sql = "UPDATE " .. tbl .. " SET " .. table.concat(lst, ",") ..
" WHERE id=" .. id .. ";"
return sqlite.query(self.db, sql) == 1
end
return false
end
function DBHelper:available(tbl) return sqlite.hasTable(self.db, tbl) == 1 end
function DBHelper:deleteByID(tbl, id)
local sql = "DELETE FROM " .. tbl .. " WHERE id=" .. id .. ";"
return sqlite.query(self.db, sql) == 1
end
function DBHelper:gencond(o)
for k, v in pairs(o) do
if k == "and" or k == "or" then
local cnd = {}
local i = 1
for k1, v1 in pairs(v) do
cnd[i] = self:gencond(v1)
i = i + 1
end
return " (" .. table.concat(cnd, " " .. k .. " ") .. ") "
else
for k1, v1 in pairs(v) do
local t = type(v1)
if (t == "string") then
return
" (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') ..
'") '
end
return " (" .. k1 .. " " .. k .. " " .. v1 .. ") "
end
end
end
end
function DBHelper:delete(tbl, cond)
local sql = "DELETE FROM " .. tbl .. " WHERE " .. self:gencond(cond) .. ";"
return sqlite.query(self.db, sql) == 1
end
function DBHelper:lastInsertID() return sqlite.lastInsertID(self.db) end
function DBHelper:close() if self.db then sqlite.dbclose(self.db) end end
function DBHelper:open()
if self.db ~= nil then self.db = sqlite.getdb(self.db) end
end

View File

@ -1,30 +1,43 @@
Logger = Object:extends{levels = {}} Logger = Object:extends{}
Logger.ERROR = 1
Logger.WARN = 2
Logger.INFO = 3
Logger.DEBUG = 4
Logger.handles = {
[1] = LOG_ERROR,
[2] = LOG_WARN,
[3] = LOG_INFO,
[4] = LOG_DEBUG
}
function Logger:initialize() function Logger:initialize()
end if not self.level then
self.level = Logger.INFO
function Logger:log(msg,level)
if self.levels[level] and ulib.exists(LOG_ROOT) then
local path = LOG_ROOT..DIR_SEP..level..'.txt'
local f = io.open(path, 'a')
local text = '['..level.."]: "..msg
if f then
f:write(text..'\n')
f:close()
end
print(text)
end end
end end
function Logger:info(msg) function Logger:log(verb,msg,...)
self:log(msg, "INFO") local level = verb
if level > self.level then return end
if level > Logger.DEBUG then
level = Logger.DEBUG
end
Logger.handles[level](msg,...)
end end
function Logger:debug(msg) function Logger:info(msg,...)
self:log(msg, "DEBUG") self:log(Logger.INFO, msg,...)
end end
function Logger:debug(msg,...)
function Logger:error(msg) self:log(Logger.DEBUG, msg,...)
self:log(msg, "ERROR") end
function Logger:error(msg,...)
self:log(Logger.ERROR, msg,...)
end
function Logger:warn(msg,...)
self:log(Logger.WARN, msg,...)
end end

View File

@ -7,6 +7,7 @@ end
function Router:initialize() function Router:initialize()
self.routes = {} self.routes = {}
self.remaps = {} self.remaps = {}
self.path = CONTROLLER_ROOT
end end
--function Router:setArgs(args) --function Router:setArgs(args)
@ -25,7 +26,7 @@ function Router:infer(url)
-- if user dont provide the url, try to infer it -- if user dont provide the url, try to infer it
-- from the REQUEST -- from the REQUEST
url = url or REQUEST.r or "" url = url or REQUEST.r or ""
url = std.trim(url, "/") url = ulib.trim(url, "/")
local args = explode(url, "/") local args = explode(url, "/")
local data = { local data = {
name = "index", name = "index",
@ -79,7 +80,7 @@ function Router:infer(url)
end end
end end
self:log("Controller: " .. data.controller.class .. ", action: "..data.action..", args: ".. JSON.encode(data.args)) self:info("Controller: " .. data.controller.class .. ", action: "..data.action..", args: ".. JSON.encode(data.args))
return data return data
end end
@ -90,7 +91,7 @@ function Router:delegate()
data.controller.main = true data.controller.main = true
views.__main__ = self:call(data) views.__main__ = self:call(data)
if not views.__main__ then if not views.__main__ then
--self:error("No view available for this action") self:info("No view available for action: %s:%s", data.controller.class, data.action)
return return
end end
-- get all visible routes -- get all visible routes
@ -106,8 +107,8 @@ function Router:delegate()
table.insert( view_args, k ) table.insert( view_args, k )
table.insert( view_argv, v ) table.insert( view_argv, v )
end end
local script_path = VIEW_ROOT .. DIR_SEP .. self.registry.layout .. DIR_SEP .. "layout.ls"
local fn, e = loadscript(VIEW_ROOT .. DIR_SEP .. self.registry.layout .. DIR_SEP .. "layout.ls", view_args) local fn, e = loadscript(script_path, view_args)
html() html()
if fn then if fn then
local r, o = pcall(fn, table.unpack(view_argv)) local r, o = pcall(fn, table.unpack(view_argv))
@ -116,7 +117,7 @@ function Router:delegate()
end end
else else
e = e or "" e = e or ""
self:error("The index page is not found for layout: " .. self.registry.layout..": "..e) self:error("The index page is not found for layout (%s: %s): %s" ,self.registry.layout, script_path,e)
end end
end end
@ -125,9 +126,8 @@ function Router:dependencies(url)
return {} return {}
end end
local list = {} local list = {}
--self:log("comparing "..url)
for k, v in pairs(self.routes[self.registry.layout]) do for k, v in pairs(self.routes[self.registry.layout]) do
v.url = std.trim(v.url, "/") v.url = ulib.trim(v.url, "/")
if v.visibility == "ALL" then if v.visibility == "ALL" then
list[k] = v.url list[k] = v.url
elseif v.visibility.routes then elseif v.visibility.routes then

View File

@ -1,19 +1,21 @@
require("OOP") require("silk.core.hook")
ulib = require("ulib") require("silk.core.OOP")
require(BASE_FRW.."silk.BaseObject") require("silk.core.sqlite")
require(BASE_FRW.."silk.DBHelper")
require(BASE_FRW.."silk.Router") require("silk.BaseObject")
require(BASE_FRW.."silk.BaseController") require("silk.Router")
require(BASE_FRW.."silk.BaseModel") require("silk.BaseController")
require(BASE_FRW.."silk.Logger") require("silk.BaseModel")
require(BASE_FRW.."silk.Template") require("silk.Logger")
require("silk.Template")
DIR_SEP = "/"
-- mime type allows -- mime type allows
-- this will bypass the default server security -- this will bypass the default server security
-- the default list is from the server setting -- the default list is from the server setting
POLICY = {} POLICY = {}
POLICY.mimes = { POLICY.mimes = {
["application/javascript"] = true,
["image/bmp"] = true, ["image/bmp"] = true,
["image/jpeg"] = true, ["image/jpeg"] = true,
["image/png"] = true, ["image/png"] = true,
@ -38,20 +40,12 @@ POLICY.mimes = {
["application/vnd.ms-fontobject"] = true, ["application/vnd.ms-fontobject"] = true,
["application/x-font-woff"] = true, ["application/x-font-woff"] = true,
["application/x-font-otf"] = true, ["application/x-font-otf"] = true,
["audio/mpeg"] = true, ["audio/mpeg"] = true
} }
HEADER_FLAG = false
function html() function html()
if not HEADER_FLAG then if not RESPONSE_HEADER.sent then
std.chtml(SESSION) std.html()
HEADER_FLAG = true
end end
end end
function import(module)
return require(BASE_FRW.."silk.api."..module)
end

View File

@ -4,6 +4,7 @@ function Object:prototype(o)
o = o or {} -- create table if user does not provide one o = o or {} -- create table if user does not provide one
setmetatable(o, self) setmetatable(o, self)
self.__index = self self.__index = self
self.__tostring = o:tostring()
return o return o
end end
@ -13,8 +14,12 @@ function Object:new(o)
return obj return obj
end end
function Object:tostring()
return ""
end
function Object:print() function Object:print()
print('an Object') print(self:tostring())
end end
function Object:initialize() function Object:initialize()
@ -28,13 +33,7 @@ function Object:inherit(o)
return self:prototype(o) return self:prototype(o)
end end
function Object:extends(o) function Object:extends(o)
return self:inherit(o) return self:inherit(o)
end end
Test = Object:inherit{dummy = 0}
function Test:toWeb()
wio.t(self.dummy)
end

View File

@ -1,260 +0,0 @@
math.randomseed(os.clock())
package.cpath = __api__.apiroot..'/?.so'
require("antd")
std = modules.std()
local read_header =function()
local l
repeat
l = std.antd_recv(HTTP_REQUEST.id)
if l and l ~= '\r' then
if l == "HTTP_REQUEST" or l == "request" or l == "COOKIE" or l == "REQUEST_HEADER" or l == "REQUEST_DATA" then
coroutine.yield(l, "LUA_TABLE")
else
local l1 = std.antd_recv(HTTP_REQUEST.id)
if l1 ~= '\r' then
coroutine.yield(l, l1)
end
l = l1
end
end
until not l or l == '\r'
end
local read_headers = function()
local co = coroutine.create(function () read_header() end)
return function () -- iterator
local code, k, v = coroutine.resume(co)
return k,v
end
end
local parse_headers =function()
local lut = {
HTTP_REQUEST = HTTP_REQUEST
}
local curr_tbl = "HTTP_REQUEST"
for k,v in read_headers() do
if v == "LUA_TABLE" then
if not lut[k] then
lut[k] = {}
end
curr_tbl = k
else
lut[curr_tbl][k] = v
end
end
HTTP_REQUEST.request = lut.request
HTTP_REQUEST.request.COOKIE = lut.COOKIE
HTTP_REQUEST.request.REQUEST_HEADER = lut.REQUEST_HEADER
HTTP_REQUEST.request.REQUEST_DATA = lut.REQUEST_DATA
end
-- parsing the header
parse_headers()
-- root dir
__ROOT__ = HTTP_REQUEST.request.SERVER_WWW_ROOT
-- set require path
package.path = __ROOT__ .. '/?.lua;'..__api__.apiroot..'/?.lua'
require("std")
require("utils")
require("extra_mime")
ulib = require("ulib")
-- set session
SESSION = {}
REQUEST = HTTP_REQUEST.request.REQUEST_DATA
REQUEST.method = HTTP_REQUEST.request.METHOD
if HTTP_REQUEST.request.COOKIE then
SESSION = HTTP_REQUEST.request.COOKIE
end
HEADER = HTTP_REQUEST.request.REQUEST_HEADER
HEADER.mobile = false
if HEADER["User-Agent"] and HEADER["User-Agent"]:match("Mobi") then
HEADER.mobile = true
end
function LOG_INFO(fmt,...)
ulib.syslog(5,string.format(fmt or "LOG",...))
end
function LOG_ERROR(fmt,...)
ulib.syslog(3,string.format(fmt or "ERROR",...))
end
function has_module(m)
if utils.file_exists(__ROOT__..'/'..m) then
if m:find("%.ls$") then
return true, true, __ROOT__..'/'..m
else
return true, false, m:gsub(".lua$","")
end
elseif utils.file_exists(__ROOT__..'/'..string.gsub(m,'%.','/')..'.lua') then
return true, false, m
elseif utils.file_exists(__ROOT__..'/'..string.gsub(m,'%.','/')..'.ls') then
return true, true, __ROOT__..'/'..string.gsub(m,'%.','/')..'.ls'
end
return false, false, nil
end
function echo(m)
if m then std.t(m) else std.t("Undefined value") end
end
function loadscript(file, args)
local f = io.open(file, "rb")
local content = ""
if f then
local html = ""
local pro = "local fn = function(...)"
local s,e, mt
local mtbegin = true -- find begin of scrit, 0 end of scrit
local i = 1
if args then
pro = "local fn = function("..table.concat( args, ",")..")"
end
for line in io.lines(file) do
line = std.trim(line, " ")
if(line ~= "") then
if(mtbegin) then
mt = "^%s*<%?lua"
else
mt = "%?>%s*$"
end
s,e = line:find(mt)
if(s) then
if mtbegin then
if html ~= "" then
pro= pro.."echo(\""..utils.escape(html).."\")\n"
html = ""
end
local b,f = line:find("%?>%s*$")
if b then
pro = pro..line:sub(e+1,b-1).."\n"
else
pro = pro..line:sub(e+1).."\n"
mtbegin = not mtbegin
end
else
pro = pro..line:sub(0,s-1).."\n"
mtbegin = not mtbegin
end
else -- no match
if mtbegin then
-- detect if we have inline lua with format <?=..?>
local b,f = line:find("<%?=")
if b then
local tmp = line
pro= pro.."echo("
while(b) do
-- find the close
local x,y = tmp:find("%?>")
if x then
pro = pro.."\""..utils.escape(html..tmp:sub(0,b-1):gsub("%%","%%%%")).."\".."
pro = pro..tmp:sub(f+1,x-1)..".."
html = ""
tmp = tmp:sub(y+1)
b,f = tmp:find("<%?=")
else
error("Syntax error near line "..i)
end
end
pro = pro.."\""..utils.escape(tmp:gsub("%%","%%%%")).."\")\n"
else
html = html..std.trim(line," "):gsub("%%","%%%%").."\n"
end
else
if line ~= "" then pro = pro..line.."\n" end
end
end
end
i = i+ 1
end
f:close()
if(html ~= "") then
pro = pro.."echo(\""..utils.escape(html).."\")\n"
end
pro = pro.."\nend \n return fn"
local r,e = load(pro)
if r then return r(), e else return nil,e end
end
end
-- decode post data if any
local decode_request_data = function()
if (not REQUEST.method)
or (REQUEST.method ~= "POST"
and REQUEST.method ~= "PUT"
and REQUEST.method ~= "PATCH")
or (not REQUEST.HAS_RAW_BODY) then
return 0
end
local ctype = HEADER['Content-Type']
local clen = HEADER['Content-Length'] or -1
if clen then
clen = tonumber(clen)
end
if not ctype or clen == -1 then
LOG_ERROR("Invalid content type %s or content length %d", ctype, clen)
return 400, "Bad Request, missing content description"
end
local raw_data, len = std.antd_recv(HTTP_REQUEST.id, clen)
if len ~= clen then
LOG_ERROR("Unable to read all data: read %d expected %d", len, clen)
return 400, "Bad Request, missing content data"
end
if ctype:find("application/json") then
REQUEST.json = bytes.__tostring(raw_data)
else
REQUEST[ctype] = raw_data
end
REQUEST.HAS_RAW_BODY = nil
return 0
end
-- set compression level
local accept_encoding = HEADER["Accept-Encoding"]
if accept_encoding then
if accept_encoding:find("gzip") then
std.antd_set_zlevel(HTTP_REQUEST.id, "gzip")
elseif accept_encoding:find("deflate") then
std.antd_set_zlevel(HTTP_REQUEST.id, "deflate")
end
end
local code, error = decode_request_data()
if code ~= 0 then
LOG_ERROR(error)
std.error(code, error)
return
end
-- LOG_INFO(JSON.encode(REQUEST))
-- OOP support
--require("OOP")
-- load sqlite helper
--require("sqlite")
-- enable extra mime
-- run the file
local m, s, p = has_module(HTTP_REQUEST.request.RESOURCE_PATH)
if m then
-- run the correct module
if s then
local r,e = loadscript(p)
if r then r() else unknow(e) end
else
LOG_INFO("RUNNING MODULE %s", p)
require(p)
end
else
unknow("Resource not found for request "..HTTP_REQUEST.request.RESOURCE_PATH)
end
--require('router')

View File

@ -1,63 +0,0 @@
FFI = require("ffi")
FFI.type = {}
FFI.type.VOID = 0
FFI.type.UINT8 = 1
FFI.type.SINT8 = 2
FFI.type.UINT16 = 3
FFI.type.SINT16 = 4
FFI.type.UINT32 = 5
FFI.type.SINT32 = 6
FFI.type.UINT64 = 7
FFI.type.SINT64 = 8
FFI.type.FLOAT = 9
FFI.type.DOUBLE = 10
FFI.type.UCHAR = 11
FFI.type.SCHAR = 12
FFI.type.USHORT = 13
FFI.type.SSHORT = 14
FFI.type.UINT = 15
FFI.type.SINT = 16
FFI.type.ULONG = 17
FFI.type.SLONG = 18
FFI.type.LONGDOUBLE = 19
FFI.type.POINTER = 20
FFI.cache = {}
FFI.load = function(path)
if FFI.cache[path] then
return FFI.cache[path]
else
print("Loading: "..path)
local lib = FFI.dlopen(path)
if lib then
FFI.cache[path] = {ref = lib, fn= {}}
end
return FFI.cache[path]
end
end
FFI.unload = function(path)
local lib = FFI.cache[path]
if lib then
FFI.dlclose(lib.ref)
FFI.cache[path] = false
end
end
FFI.unloadAll = function()
for k,v in pairs(FFI.cache) do
FFI.dlclose(v.ref)
end
FFI.cache = {}
end
FFI.lookup = function(lib, name)
local fn = lib.fn[name]
if fn then return fn end
fn = FFI.dlsym(lib.ref, name)
if fn then
lib.fn[name] = fn
return fn
end
return nil
end

View File

@ -1,79 +0,0 @@
function std.extra_mime(name)
local ext = std.ext(name)
local mpath = __ROOT__.."/".."mimes.json"
local xmimes = {}
if utils.file_exists(mpath) then
local f = io.open(mpath, "r")
if f then
xmimes = JSON.decodeString(f:read("*all"))
f:close()
end
end
if(name:find("Makefile$")) then return "text/makefile",false
elseif ext == "php" then return "text/php",false
elseif ext == "c" or ext == "h" then return "text/c",false
elseif ext == "cpp" or ext == "hpp" then return "text/cpp",false
elseif ext == "md" then return "text/markdown",false
elseif ext == "lua" then return "text/lua",false
elseif ext == "yml" then return "application/x-yaml", false
elseif xmimes[ext] then return xmimes[ext].mime, xmimes[ext].binary
--elseif ext == "pgm" then return "image/x-portable-graymap", true
else
return "application/octet-stream",true
end
end
function std.mimeOf(name)
local mime = std.mime(name)
if mime ~= "application/octet-stream" then
return mime
else
return std.extra_mime(name)
end
end
--[[ function std.isBinary(name)
local mime = std.mime(name)
if mime ~= "application/octet-stream" then
return std.is_bin(name)
else
local xmime,bin = std.extra_mime(name)
return bin
end
end ]]
function std.sendFile(m)
local mime = std.mimeOf(m)
local finfo = ulib.file_stat(m)
local len = tostring(math.floor(finfo.size))
local len1 = tostring(math.floor(finfo.size - 1))
if mime == "audio/mpeg" then
std.status(200)
std.header("Pragma", "public")
std.header("Expires", "0")
std.header("Content-Type", mime)
std.header("Content-Length", len)
std.header("Content-Disposition", "inline; filename=" .. std.basename(m))
std.header("Content-Range:", "bytes 0-" .. len1 .. "/" .. len)
std.header("Accept-Ranges", "bytes")
std.header("X-Pad", "avoid browser bug")
std.header("Content-Transfer-Encoding", "binary")
std.header("Cache-Control", "no-cache, no-store")
std.header("Connection", "Keep-Alive")
std.header_flush()
std.f(m)
else
if HEADER['If-Modified-Since'] and HEADER['If-Modified-Since'] == finfo.ctime then
std.status(304)
std.header_flush()
else
std.status(200)
std.header("Content-Type", mime)
--std.header("Content-Length", len)
std.header("Cache-Control", "no-cache")
std.header("Last-Modified", finfo.ctime)
std.header_flush()
std.f(m)
end
end
end

210
silkmvc/core/hook.lua Normal file
View File

@ -0,0 +1,210 @@
math.randomseed(os.time())
-- define some legacy global variables to provide backward support
-- for some old web application
__api__ = {
apiroot = string.format("%s/lua", _SERVER["LIB_DIR"]),
tmpdir = _SERVER["TMP_DIR"],
dbpath = _SERVER["DB_DIR"]
}
-- root dir
__ROOT__ = _SERVER["DOCUMENT_ROOT"]
-- set require path
package.cpath = __api__.apiroot .. '/?.so'
package.path = string.format("%s/?.lua;%s/?.lua",__api__.apiroot,__ROOT__)
ulib = require("ulib")
slice = require("slice")
require("silk.core.utils")
require("silk.core.std")
-- global helper functions for lua page script
function has_module(m)
if utils.file_exists(__ROOT__ .. '/' .. m) then
if m:find("%.ls$") then
return true, true, __ROOT__ .. '/' .. m
else
return true, false, m:gsub(".lua$", "")
end
elseif utils.file_exists(__ROOT__ .. '/' .. string.gsub(m, '%.', '/') .. '.lua') then
return true, false, m
elseif utils.file_exists(__ROOT__ .. '/' .. string.gsub(m, '%.', '/') .. '.ls') then
return true, true, __ROOT__ .. '/' .. string.gsub(m, '%.', '/') .. '.ls'
end
return false, false, nil
end
--- Send data to client
function echo(...)
fcgio:echo(...)
end
--- luad lua page script
function loadscript(file, args)
local f = io.open(file, "rb")
local content = ""
if f then
local html = ""
local pro = "local fn = function(...)"
local s, e, mt
local mtbegin = true -- find begin of scrit, 0 end of scrit
local i = 1
if args then
pro = "local fn = function(" .. table.concat(args, ",") .. ")"
end
for line in io.lines(file) do
line = ulib.trim(line, " ")
if (line ~= "") then
if (mtbegin) then
mt = "^%s*<%?lua"
else
mt = "%?>%s*$"
end
s, e = line:find(mt)
if (s) then
if mtbegin then
if html ~= "" then
pro = pro .. "echo(\"" .. utils.escape(html) .. "\")\n"
html = ""
end
local b, f = line:find("%?>%s*$")
if b then
pro = pro .. line:sub(e + 1, b - 1) .. "\n"
else
pro = pro .. line:sub(e + 1) .. "\n"
mtbegin = not mtbegin
end
else
pro = pro .. line:sub(0, s - 1) .. "\n"
mtbegin = not mtbegin
end
else -- no match
if mtbegin then
-- detect if we have inline lua with format <?=..?>
local b, f = line:find("<%?=")
if b then
local tmp = line
pro = pro .. "echo("
while (b) do
-- find the close
local x, y = tmp:find("%?>")
if x then
pro = pro .. "\"" .. utils.escape(html .. tmp:sub(0, b - 1):gsub("%%", "%%%%")) ..
"\".."
pro = pro .. tmp:sub(f + 1, x - 1) .. ".."
html = ""
tmp = tmp:sub(y + 1)
b, f = tmp:find("<%?=")
else
error("Syntax error near line " .. i)
end
end
pro = pro .. "\"" .. utils.escape(tmp:gsub("%%", "%%%%")) .. "\")\n"
else
html = html .. ulib.trim(line, " "):gsub("%%", "%%%%") .. "\n"
end
else
if line ~= "" then
pro = pro .. line .. "\n"
end
end
end
end
i = i + 1
end
f:close()
if (html ~= "") then
pro = pro .. "echo(\"" .. utils.escape(html) .. "\")\n"
end
pro = pro .. "\nend \n return fn"
local r, e = load(pro)
if r then
return r(), e
else
return nil, e
end
end
end
-- logging helpers
function LOG_INFO(fmt, ...)
fcgio:log_info(string.format(fmt or "LOG", ...))
end
function LOG_ERROR(fmt, ...)
fcgio:log_error(string.format(fmt or "ERROR", ...))
end
function LOG_DEBUG(fmt, ...)
fcgio:log_debug(string.format(fmt or "ERROR", ...))
end
function LOG_WARN(fmt, ...)
fcgio:log_warn(string.format(fmt or "ERROR", ...))
end
-- decode post data if any
local decode_request_data = function()
-- decode POST request data
if _SERVER["RAW_DATA"] then
if REQUEST.method == "POST" and HEADER["Content-Type"] == "application/x-www-form-urlencoded" then
for k, v in pairs(utils.parse_query(tostring(_SERVER["RAW_DATA"]))) do
REQUEST[k] = v
end
else
local ctype = HEADER['Content-Type']
local clen = HEADER['Content-Length'] or -1
if clen then
clen = tonumber(clen)
end
if not ctype or clen == -1 then
LOG_ERROR("Invalid content type %s or content length %d", ctype, clen)
return 400, "Bad Request, missing content description"
end
if ctype:find("application/json") then
REQUEST.json = tostring(_SERVER["RAW_DATA"])
else
REQUEST[ctype] = _SERVER["RAW_DATA"]
end
end
end
return 0
end
-- define old fashion global request object
HEADER = {}
setmetatable(HEADER, {
__index = function(o, data)
local key = "HTTP_" .. string.upper(data:gsub("-", "_"))
return _SERVER[key]
end
})
HEADER.mobile = false
if HEADER["User-Agent"] and HEADER["User-Agent"]:match("Mobi") then
HEADER.mobile = true
end
REQUEST = {}
-- decode GET request
if _SERVER["QUERY_STRING"] then
REQUEST = utils.parse_query(_SERVER["QUERY_STRING"])
end
REQUEST.method = _SERVER["REQUEST_METHOD"]
-- set session
SESSION = {}
if HEADER["Cookie"] then
for key, val in HEADER["Cookie"]:gmatch("([^;=]+)=*([^;]*)") do
SESSION[ulib.trim(key," ")] = ulib.trim(val, " ")
end
end
local code, error = decode_request_data()
if code ~= 0 then
LOG_ERROR(error)
std.error(code, error)
return false
end
return true

115
silkmvc/core/mimes.lua Normal file
View File

@ -0,0 +1,115 @@
local default_mimes = {
["bmp"] = "image/bmp",
["jpg"] = "image/jpeg",
["jpeg"] = "image/jpeg",
["css"] = "text/css",
["md"] = "text/markdown",
["csv"] = "text/csv",
["pdf"] = "application/pdf",
["gif"] = "image/gif",
["html"] = "text/html",
["htm"] = "text/html",
["chtml"] = "text/html",
["json"] = "application/json",
["js"] = "application/javascript",
["png"] = "image/png",
["ppm"] = "image/x-portable-pixmap",
["rar"] = "application/x-rar-compressed",
["tiff"] = "image/tiff",
["tar"] = "application/x-tar",
["txt"] = "text/plain",
["ttf"] = "application/x-font-ttf",
["xhtml"] = "application/xhtml+xml",
["xml"] = "application/xml",
["zip"] = "application/zip",
["svg"] = "image/svg+xml",
["eot"] = "application/vnd.ms-fontobject",
["woff"] = "application/x-font-woff",
["woff2"] = "application/x-font-woff",
["otf"] = "application/x-font-otf",
["mp3"] = "audio/mpeg",
["mpeg"] = "audio/mpeg"
}
setmetatable(default_mimes, {
__index = function(this, key)
return "application/octet-stream"
end
})
function std.mime(ext)
return default_mimes[ext]
end
function std.extra_mime(name)
local ext = utils.ext(name)
local mpath = __ROOT__ .. "/" .. "mimes.json"
local xmimes = {}
if utils.file_exists(mpath) then
xmimes = JSON.decodeFile(mpath)
end
if (name:find("Makefile$")) then
return "text/makefile", false
elseif ext == "php" then
return "text/php", false
elseif ext == "c" or ext == "h" then
return "text/c", false
elseif ext == "cpp" or ext == "hpp" then
return "text/cpp", false
elseif ext == "md" then
return "text/markdown", false
elseif ext == "lua" then
return "text/lua", false
elseif ext == "yml" then
return "application/x-yaml", false
elseif xmimes[ext] then
return xmimes[ext].mime, xmimes[ext].binary
-- elseif ext == "pgm" then return "image/x-portable-graymap", true
else
return "application/octet-stream", true
end
end
function std.mimeOf(name)
local mime = std.mime(utils.ext(name))
if mime ~= "application/octet-stream" then
return mime
else
return std.extra_mime(name)
end
end
function std.sendFile(m)
local mime = std.mimeOf(m)
local finfo = ulib.file_stat(m)
local len = tostring(math.floor(finfo.size))
local len1 = tostring(math.floor(finfo.size - 1))
if mime == "audio/mpeg" then
std.status(200)
std.header("Pragma", "public")
std.header("Expires", "0")
std.header("Content-Type", mime)
std.header("Content-Length", len)
std.header("Content-Disposition", "inline; filename=" .. utils.basename(m))
std.header("Content-Range:", "bytes 0-" .. len1 .. "/" .. len)
std.header("Accept-Ranges", "bytes")
std.header("X-Pad", "avoid browser bug")
std.header("Content-Transfer-Encoding", "binary")
std.header("Cache-Control", "no-cache, no-store")
std.header("Connection", "Keep-Alive")
std.header_flush()
std.f(m)
else
if HEADER['If-Modified-Since'] and HEADER['If-Modified-Since'] == finfo.ctime then
std.status(304)
std.header_flush()
else
std.status(200)
std.header("Content-Type", mime)
-- std.header("Content-Length", len)
std.header("Cache-Control", "no-cache")
std.header("Last-Modified", finfo.ctime)
std.header_flush()
std.f(m)
end
end
end

View File

@ -1,23 +1,42 @@
sqlite = modules.sqlite() sqlite = require("sqlitedb")
if sqlite == nil then
return 0
end
require("silk.core.OOP")
sqlite.getdb = function(name)
if name:find("%.db$") then
return sqlite.db(name)
elseif name:find("/") then
LOG_ERROR("Invalid database name %s", name)
return nil
else
return sqlite.db(__api__.dbpath .. "/" .. name .. ".db")
end
end
if sqlite == nil then return 0 end
require("OOP")
-- create class -- create class
DBModel = Object:inherit{db=nil, name=''} DBModel = Object:inherit{
db = nil,
}
function DBModel:createTable(m) function DBModel:createTable(name, m)
if self:available() then return true end if self:available() then
local sql = "CREATE TABLE "..self.name.."(id INTEGER PRIMARY KEY" return true
end
local sql = "CREATE TABLE " .. name .. "(id INTEGER PRIMARY KEY"
for k, v in pairs(m) do for k, v in pairs(m) do
if k ~= "id" then if k ~= "id" then
sql = sql .. "," .. k .. " " .. v sql = sql .. "," .. k .. " " .. v
end end
end end
sql = sql .. ");" sql = sql .. ");"
return sqlite.query(self.db,sql) == 1 return self:exec(sql)
end end
function DBModel:insert(m) function DBModel:insert(name, m)
local keys = {} local keys = {}
local values = {} local values = {}
for k, v in pairs(m) do for k, v in pairs(m) do
@ -33,27 +52,33 @@ function DBModel:insert(m)
end end
end end
end end
local sql = "INSERT INTO "..self.name.." ("..table.concat(keys,',')..') VALUES (' local sql = "INSERT INTO " .. name .. " (" .. table.concat(keys, ',') .. ') VALUES ('
sql = sql .. table.concat(values, ',') .. ');' sql = sql .. table.concat(values, ',') .. ');'
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
function DBModel:get(id) function DBModel:get(name, id)
return sqlite.select(self.db, self.name, "*","id="..id)[1] local records = self:query( string.format("SELECT * FROM %s WHERE id=%d", name, id))
if records and #records == 1 then
return records[1]
end
return nil
end end
function DBModel:getAll() function DBModel:getAll(name)
--local sql = "SELECT * FROM "..self.name local data = self:query( "SELECT * FROM " .. name)
--return sqlite.select(self.db, self.name, "1=1") if not data then
local data = sqlite.select(self.db, self.name, "*", "1=1") return nil
if data == nil then return nil end end
local a = {} local a = {}
for n in pairs(data) do table.insert(a, n) end for n in pairs(data) do
table.insert(a, n)
end
table.sort(a) table.sort(a)
return data, a return data, a
end end
function DBModel:find(cond) function DBModel:find(name, cond)
local cnd = "1=1" local cnd = "1=1"
local sel = "*" local sel = "*"
if cond.exp then if cond.exp then
@ -77,19 +102,38 @@ function DBModel:find(cond)
-- print(sel) -- print(sel)
end end
-- print(cnd) -- print(cnd)
local data = sqlite.select(self.db, self.name, sel, cnd) local data = self:query( string.format("SELECT %s FROM %s WHERE %s", sel, name, cnd))
if data == nil then return nil end if data == nil then
return nil
end
local a = {} local a = {}
for n in pairs(data) do table.insert(a, n) end for n in pairs(data) do
table.insert(a, n)
end
table.sort(a) table.sort(a)
return data, a return data, a
end end
function DBModel:query(sql) function DBModel:query(sql)
return sqlite.query(self.db, sql) == 1 local data, error = sqlite.query(self.db, sql)
--LOG_DEBUG(sql)
if not data then
LOG_ERROR("Error querying recorda SQL[%s]: %s", sql, error or "")
return nil
end
return data
end end
function DBModel:update(m) function DBModel:exec(sql)
--LOG_DEBUG(sql)
local ret, err = sqlite.exec(self.db, sql)
if not ret then
LOG_ERROR("Error execute [%s]: %s", sql, err or "")
end
return ret == true
end
function DBModel:update(name, m)
local id = m['id'] local id = m['id']
if id ~= nil then if id ~= nil then
local lst = {} local lst = {}
@ -102,18 +146,19 @@ function DBModel:update(m)
table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"") table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"")
end end
end end
local sql = "UPDATE "..self.name.." SET "..table.concat(lst,",").." WHERE id="..id..";" local sql = "UPDATE " .. name .. " SET " .. table.concat(lst, ",") .. " WHERE id=" .. id .. ";"
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
return false return false
end end
function DBModel:available() function DBModel:available(name)
return sqlite.hasTable(self.db, self.name) == 1 local records = self:query(string.format("SELECT * FROM sqlite_master WHERE type='table' and name='%s'", name))
return #records == 1
end end
function DBModel:deleteByID(id) function DBModel:deleteByID(name, id)
local sql = "DELETE FROM "..self.name.." WHERE id="..id..";" local sql = "DELETE FROM " .. name .. " WHERE id=" .. id .. ";"
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
function DBModel:gencond(o) function DBModel:gencond(o)
for k, v in pairs(o) do for k, v in pairs(o) do
@ -136,13 +181,13 @@ function DBModel:gencond(o)
end end
end end
end end
function DBModel:delete(cond) function DBModel:delete(name, cond)
local sql = "DELETE FROM "..self.name.." WHERE "..self:gencond(cond)..";" local sql = "DELETE FROM " .. name .. " WHERE " .. self:gencond(cond) .. ";"
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
function DBModel:lastInsertID() function DBModel:lastInsertID()
return sqlite.lastInsertID(self.db) return sqlite.last_insert_id(self.db)
end end
function DBModel:close() function DBModel:close()

View File

@ -1,22 +1,5 @@
bytes = modules.bytes() std = {}
array = modules.array() require("silk.core.mimes")
modules.sqlite = function()
if not sqlite then
sqlite = require("sqlitedb")
sqlite.getdb = function(name)
if name:find("%.db$") then
return sqlite._getdb(name)
elseif name:find("/") then
LOG_ERROR("Invalid database name %s", name)
return nil
else
return sqlite._getdb(__api__.dbpath.."/"..name..".db")
end
end
end
return sqlite
end
RESPONSE_HEADER = { RESPONSE_HEADER = {
status = 200, status = 200,
@ -25,74 +8,135 @@ RESPONSE_HEADER = {
sent = false sent = false
} }
local http_status = {
[100] = "Continue",
[101] = "Switching Protocols",
[102] = "Processing",
[103] = "Early Hints",
[200] = "OK",
[201] = "Created",
[202] = "Accepted",
[203] = "Non-Authoritative Information",
[204] = "No Content",
[205] = "Reset Content",
[206] = "Partial Content",
[207] = "Multi-Status",
[208] = "Already Reported",
[226] = "IM Used",
[300] = "Multiple Choices",
[301] = "Moved Permanently",
[302] = "Found",
[303] = "See Other",
[304] = "Not Modified",
[305] = "Use Proxy",
[306] = "Switch Proxy",
[307] = "Temporary Redirect",
[308] = "Permanent Redirect",
[400] = "Bad Request",
[401] = "Unauthorized",
[402] = "Payment Required",
[403] = "Forbidden",
[404] = "Not Found",
[405] = "Method Not Allowed",
[406] = "Not Acceptable",
[407] = "Proxy Authentication Required",
[408] = "Request Timeout",
[409] = "Conflict",
[410] = "Gone",
[411] = "Length Required",
[412] = "Precondition Failed",
[413] = "Payload Too Large",
[414] = "URI Too Long",
[415] = "Unsupported Media Type",
[416] = "Range Not Satisfiable",
[417] = "Expectation Failed",
[421] = "Misdirected Request",
[422] = "Unprocessable Entity",
[423] = "Locked",
[424] = "Failed Dependency",
[425] = "Too Early",
[426] = "Upgrade Required",
[428] = "Precondition Required",
[429] = "Too Many Requests",
[431] = "Request Header Fields Too Large",
[451] = "Unavailable For Legal Reasons",
[500] = "Internal Server Error",
[501] = "Not Implemented",
[502] = "Bad Gateway",
[503] = "Service Unavailable",
[504] = "Gateway Timeout",
[505] = "HTTP Version Not Supported",
[506] = "Variant Also Negotiates",
[507] = "Insufficient Storage",
[508] = "Loop Detected",
[510] = "Not Extended",
[511] = "Network Authentication Required"
}
setmetatable(http_status, {
__index = function(this, key)
return "Unofficial Status"
end
})
function std.status(code) function std.status(code)
RESPONSE_HEADER.status = code RESPONSE_HEADER.status = code
end end
function std.custom_header(k, v) function std.custom_header(k, v)
std.header(k, v) std.header(k, v)
end end
function std.header_flush()
std._send_header(HTTP_REQUEST.id,RESPONSE_HEADER.status, RESPONSE_HEADER.header, RESPONSE_HEADER.cookie)
RESPONSE_HEADER.sent = true
end
function std.header(k, v) function std.header(k, v)
RESPONSE_HEADER.header[k] = v RESPONSE_HEADER.header[k] = v
end end
function std.cjson(ck) function std.header_flush()
for k,v in pairs(ck) do -- send out status
std.setCookie(k.."="..v.."; Path=/") echo("Status: ", RESPONSE_HEADER.status, " ", http_status[RESPONSE_HEADER.status], "\r\n")
-- send out header
for key, val in pairs(RESPONSE_HEADER.header) do
echo(key, ": ", val, "\r\n")
end end
std.header("Content-Type","application/json; charset=utf-8") -- send out cookie
std.header_flush() for key, val in ipairs(RESPONSE_HEADER.cookie) do
echo("Set-Cookie: ", val, "\r\n")
end end
function std.chtml(ck) echo("\r\n")
for k,v in pairs(ck) do RESPONSE_HEADER.sent = true
std.setCookie(k.."="..v.."; Path=/") RESPONSE_HEADER.header = {}
end RESPONSE_HEADER.cookie = {}
std.header("Content-Type","text/html; charset=utf-8")
std.header_flush()
end
function std.t(s)
if RESPONSE_HEADER.sent == false then
std.header_flush()
end
std._t(HTTP_REQUEST.id,s)
end
function std.b(s)
if RESPONSE_HEADER.sent == false then
std.header_flush()
end
std._b(HTTP_REQUEST.id,s)
end
function std.f(v)
std._f(HTTP_REQUEST.id,v)
--ulib.send_file(v, HTTP_REQUEST.socket)
end end
function std.setCookie(v) function std.setCookie(...)
RESPONSE_HEADER.cookie[#RESPONSE_HEADER.cookie] = v local args = table.pack(...)
cookie = table.concat(args,";")
RESPONSE_HEADER.cookie[#RESPONSE_HEADER.cookie + 1] = cookie
end end
function std.error(status, msg) function std.error(status, msg)
std._error(HTTP_REQUEST.id, status, msg) std.status(status)
std.header("Content-Type", "text/html")
std.header_flush()
echo(string.format("<HTML><HEAD><TITLE>%s</TITLE></HEAD><BODY><h2>%s</h2></BODY></HTML>",msg, msg))
end end
--_upload
--_route
function std.unknow(s) function std.unknow(s)
std.error(404, "Unknown request") std.error(404, "Unknown request")
end end
--_redirect function std.f(path)
--[[ function std.redirect(s) fcgio:send_file(path)
std._redirect(HTTP_REQUEST.id,s) end
end ]]
function std.html() function std.html()
std.header("Content-Type", "text/html; charset=utf-8") std.header("Content-Type", "text/html; charset=utf-8")
std.header_flush() std.header_flush()
end end
function std.text() function std.text()
std.header("Content-Type", "text/plain; charset=utf-8") std.header("Content-Type", "text/plain; charset=utf-8")
std.header_flush() std.header_flush()
@ -102,34 +146,24 @@ function std.json()
std.header("Content-Type", "application/json; charset=utf-8") std.header("Content-Type", "application/json; charset=utf-8")
std.header_flush() std.header_flush()
end end
function std.jpeg() function std.jpeg()
std.header("Content-Type", "image/jpeg") std.header("Content-Type", "image/jpeg")
std.header_flush() std.header_flush()
end end
function std.octstream(s) function std.octstream(s)
std.header("Content-Type", "application/octet-stream") std.header("Content-Type", "application/octet-stream")
std.header("Content-Disposition", 'attachment; filename="' .. s .. '"') std.header("Content-Disposition", 'attachment; filename="' .. s .. '"')
std.header_flush() std.header_flush()
end end
--[[ function std.textstream()
std._textstream(HTTP_REQUEST.id)
end ]]
function std.is_file(f)
function std.readOnly(t) -- bugging return ulib.is_dir(f) == false
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end end
-- TODO provide web socket support
-- web socket -- use coroutine to read socket message
std.ws = {} std.ws = {}
function std.ws.header() function std.ws.header()
local h = std.ws_header(HTTP_REQUEST.id) local h = std.ws_header(HTTP_REQUEST.id)
@ -158,14 +192,6 @@ end
function std.ws.close(code) function std.ws.close(code)
std.ws_close(HTTP_REQUEST.id, code) std.ws_close(HTTP_REQUEST.id, code)
end end
function std.basename(str)
local name = string.gsub(std.trim(str,"/"), "(.*/)(.*)", "%2")
return name
end
function std.is_file(f)
return std.is_dir(f) == false
end
std.ws.TEXT = 1 std.ws.TEXT = 1
std.ws.BIN = 2 std.ws.BIN = 2
std.ws.CLOSE = 8 std.ws.CLOSE = 8

View File

@ -5,7 +5,9 @@ function utils.is_array(table)
local count = 0 local count = 0
for k, v in pairs(table) do for k, v in pairs(table) do
if type(k) == "number" then if type(k) == "number" then
if k > max then max = k end if k > max then
max = k
end
count = count + 1 count = count + 1
else else
return false return false
@ -45,14 +47,20 @@ function utils.hex_to_char(x)
end end
function utils.decodeURI(url) function utils.decodeURI(url)
return url:gsub("%%(%x%x)", utils.hex_to_char) return url:gsub("%%(%x%x)", utils.hex_to_char):gsub('+', ' ')
end end
function utils.unescape(s) function utils.unescape(s)
local str = "" local str = ""
local escape = false local escape = false
local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} local esc_map = {
for c in s:gmatch"." do b = '\b',
f = '\f',
n = '\n',
r = '\r',
t = '\t'
}
for c in s:gsub("%%%%", "%%"):gmatch "." do
if c ~= '\\' then if c ~= '\\' then
if escape then if escape then
if esc_map[c] then if esc_map[c] then
@ -78,7 +86,67 @@ end
function utils.file_exists(name) function utils.file_exists(name)
local f = io.open(name, "r") local f = io.open(name, "r")
if f~=nil then io.close(f) return true else return false end if f ~= nil then
io.close(f)
return true
else
return false
end
end
function utils.parse_query(str, sep)
if not sep then
sep = '&'
end
local values = {}
for key, val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
local key = utils.decodeURI(key)
local keys = {}
key = key:gsub('%[([^%]]*)%]', function(v)
-- extract keys between balanced brackets
if string.find(v, "^-?%d+$") then
v = tonumber(v)
else
v = utils.decodeURI(v)
end
table.insert(keys, v)
return "="
end)
key = key:gsub('=+.*$', "")
key = key:gsub('%s', "_") -- remove spaces in parameter name
val = val:gsub('^=+', "")
if not values[key] then
values[key] = {}
end
if #keys > 0 and type(values[key]) ~= 'table' then
values[key] = {}
elseif #keys == 0 and type(values[key]) == 'table' then
values[key] = utils.decodeURI(val)
elseif type(values[key]) == 'string' then
values[key] = {values[key]}
table.insert(values[key], utils.decodeURI(val))
end
local t = values[key]
for i, k in ipairs(keys) do
if type(t) ~= 'table' then
t = {}
end
if k == "" then
k = #t + 1
end
if not t[k] then
t[k] = {}
end
if i == #keys then
t[k] = val
end
t = t[k]
end
end
return values
end end
function utils.url_parser(uri) function utils.url_parser(uri)
@ -89,8 +157,14 @@ function utils.url_parser(uri)
obj.port = uri:gsub(pattern, "%3") obj.port = uri:gsub(pattern, "%3")
obj.query = uri:gsub(pattern, "%4") obj.query = uri:gsub(pattern, "%4")
if obj.port == "" then obj.port = 80 else obj.port = tonumber(obj.port) end if obj.port == "" then
if obj.query == "" then obj.query="/" end obj.port = 80
else
obj.port = tonumber(obj.port)
end
if obj.query == "" then
obj.query = "/"
end
return obj return obj
end end
@ -109,7 +183,9 @@ function JSON.encode(obj)
else else
local lst = {} local lst = {}
local a = {} local a = {}
for n in pairs(obj) do table.insert(a, n) end for n in pairs(obj) do
table.insert(a, n)
end
table.sort(a) table.sort(a)
for i, v in pairs(a) do for i, v in pairs(a) do
table.insert(lst, JSON.encode(obj[v])) table.insert(lst, JSON.encode(obj[v]))
@ -129,10 +205,14 @@ function JSON.encode(obj)
end end
function explode(str, div) -- credit: http://richard.warburton.it function explode(str, div) -- credit: http://richard.warburton.it
if (div=='') then return false end if (div == '') then
return false
end
local pos, arr = 0, {} local pos, arr = 0, {}
-- for each divider found -- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do for st, sp in function()
return string.find(str, div, pos, true)
end do
table.insert(arr, string.sub(str, pos, st - 1)) -- Attach chars left of current divider table.insert(arr, string.sub(str, pos, st - 1)) -- Attach chars left of current divider
pos = sp + 1 -- Jump past current divider pos = sp + 1 -- Jump past current divider
end end
@ -147,7 +227,6 @@ function firstToUpper(str)
return (str:gsub("^%l", string.upper)) return (str:gsub("^%l", string.upper))
end end
local charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" local charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
function utils.generate_salt(length) function utils.generate_salt(length)
@ -159,3 +238,12 @@ function utils.generate_salt(length)
end end
return table.concat(ret) return table.concat(ret)
end end
function utils.ext(path)
return path:match("%.([^%.]*)$")
end
function utils.basename(str)
local name = string.gsub(ulib.trim(str, "/"), "(.*/)(.*)", "%2")
return name
end

View File

@ -2,26 +2,26 @@
-- the rewrite rule for the framework -- the rewrite rule for the framework
-- should be something like this -- should be something like this
-- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&<query> -- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&<query>
-- require needed library
BASE_FRW = ""
require(BASE_FRW.."api")
-- some global variables -- some global variables
DIR_SEP = "/"
WWW_ROOT = "/opt/www/htdocs/apps" WWW_ROOT = "/opt/www/htdocs/apps"
HTTP_ROOT = "https://apps.localhost:9195/" HTTP_ROOT = "https://apps.localhost:9195/"
-- class path: path.to.class
BASE_FRW = ""
-- class path: path.to.class -- class path: path.to.class
CONTROLLER_ROOT = BASE_FRW.."apps.controllers" CONTROLLER_ROOT = BASE_FRW.."apps.controllers"
MODEL_ROOT = BASE_FRW.."apps.models" MODEL_ROOT = BASE_FRW.."apps.models"
-- file path: path/to/file -- file path: path/to/file
VIEW_ROOT = WWW_ROOT..DIR_SEP.."views" VIEW_ROOT = WWW_ROOT..DIR_SEP.."views"
LOG_ROOT = WWW_ROOT..DIR_SEP.."logs"
-- require needed library
require(BASE_FRW.."silk.api")
-- registry object store global variables -- registry object store global variables
local REGISTRY = {} local REGISTRY = {}
-- set logging level -- set logging level
REGISTRY.logger = Logger:new{ levels = {INFO = true, ERROR = true, DEBUG = true}} REGISTRY.logger = Logger:new{ level = Logger.INFO }
REGISTRY.db = DBHelper:new{db="iosapps"} REGISTRY.db = DBHelper:new{db="iosapps"}
REGISTRY.layout = 'default' REGISTRY.layout = 'default'
@ -31,7 +31,7 @@ REGISTRY.router = router
router:setPath(CONTROLLER_ROOT) router:setPath(CONTROLLER_ROOT)
--router:route('edit', 'post/edit', "ALL" ) --router:route('edit', 'post/edit', "ALL" )
-- example of depedencies to the current main route -- example of dependencies to the current main route
-- each layout may have different dependencies -- each layout may have different dependencies
local default_routes_dependencies = { local default_routes_dependencies = {
edit = { edit = {

3
test/ad.ls Normal file
View File

@ -0,0 +1,3 @@
<?lua
echo(ad)
?>

3
test/detail.ls Normal file
View File

@ -0,0 +1,3 @@
<?lua
echo("Post ID:", data.id, "\n", "Content:", data.content,"\n")
?>

4
test/layout.ls Normal file
View File

@ -0,0 +1,4 @@
<?lua
__main__:render()
ad:render()
?>

46
test/lunit.lua Normal file
View File

@ -0,0 +1,46 @@
TESTS = {}
function test(description, fn)
TESTS[#TESTS + 1] = {description = description, test = fn }
end
function run()
local report = {
ok = 0,
fail= 0,
total = #TESTS
}
for l,ts in ipairs(TESTS) do
io.write(string.format("Executing: %s...",ts.description))
local status,err = pcall(ts.test)
if status then
io.write("\27[32mOK\27[0m\n")
report.ok = report.ok + 1
else
io.write("\27[31mFAIL\27[0m\n")
print(err)
report.fail = report.fail + 1
end
end
print("----------------------------")
print(string.format("Total tests: %d", report.total))
print(string.format("Tests passed: %d", report.ok))
print(string.format("Tests failed: %d", report.fail))
TESTS = {}
end
function assert(b, e,...)
if not b then
error(string.format(e,...))
print(debug.traceback())
end
end
function expect(v1,v2)
assert(v1 == v2, "Expect: [%s] get: [%s]", tostring(v2), tostring(v1))
end
function unexpect(v1,v2)
assert(v1 ~= v2, "Unexpect value", tostring(v2))
end

37
test/request.json Normal file
View File

@ -0,0 +1,37 @@
{
"PATH_INFO": "luacgi/lua/test.lua",
"REDIRECT_STATUS": "200",
"SCRIPT_NAME": "test.lua",
"HTTP_ACCEPT_ENCODING": "gzip, deflate",
"DOCUMENT_ROOT": "/tmp/www",
"REMOTE_ADDR": "192.168.1.44",
"REQUEST_URI": "/luacgi/lua/test.lua?r=1&id=3&name=John",
"SERVER_PROTOCOL": "HTTP/1.1",
"SERVER_NAME": "Antd",
"PATH_TRANSLATED": "/opt/www/htdocs/lua/test.lua",
"RAW_DATA": "firstname=Dany&lastname=LE&form_submitted=1",
"CONTENT_TYPE": "application/x-www-form-urlencoded",
"HTTP_ORIGIN": "http://192.168.1.27",
"HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"REMOTE_HOST": "192.168.1.44",
"HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9",
"HTTP_CONTENT_LENGTH": "43",
"HTTP_CONNECTION": "keep-alive",
"HTTP_COOKIE": "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1",
"LIB_DIR": "/tmp/lib",
"REQUEST_METHOD": "POST",
"HTTP_REFERER": "http://192.168.1.27/php/sign.html",
"SERVER_SOFTWARE": "Antd",
"QUERY_STRING": "r=post/id/1&id=3&name=John",
"TMP_DIR": "/tmp",
"HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
"HTTP_CONTENT_TYPE": "application/x-www-form-urlencoded",
"HTTP_UPGRADE_INSECURE_REQUESTS": "1",
"CONTENT_LENGTH": "43",
"GATEWAY_INTERFACE": "CGI/1.1",
"HTTP_HOST": "192.168.1.27",
"SERVER_PORT": "80",
"SCRIPT_FILENAME": "/opt/www/htdocs/lua/test.lua",
"DB_DIR": "/tmp",
"HTTP_CACHE_CONTROL": "max-age=0"
}

396
test/test_core.lua Normal file
View File

@ -0,0 +1,396 @@
--- the binary shall be compiled with
--- make CFLAGS=-DLUA_SLICE_MAGIC=0x8AD73B9F
--- otherwise the tests unable to load C modules
require("lunit")
package.cpath = "/tmp/lib/lua/?.so"
package.path = ""
test("IO setup", function()
fcgio = { OUTPUT = "",
LOG = {
INFO = "",
ERROR = "",
DEBUG = "",
WARN = ""
}
}
function fcgio:flush()
fcgio.OUTPUT = ""
end
function fcgio:echo(...)
local args = table.pack(...)
for i=1,args.n do
-- do something with args[i], careful, it might be nil!
fcgio.OUTPUT = fcgio.OUTPUT..tostring(args[i])
end
end
function fcgio:log_info(fmt,...)
fcgio.LOG.INFO = string.format(fmt,...)
io.stderr:write("INFO: ", fcgio.LOG.INFO)
io.stderr:write("\n")
end
function fcgio:log_error(fmt,...)
fcgio.LOG.ERROR = string.format(fmt,...)
io.stderr:write("ERROR: ",fcgio.LOG.ERROR)
io.stderr:write("\n")
end
function fcgio:log_debug(fmt,...)
fcgio.LOG.DEBUG = string.format(fmt,...)
io.stderr:write("DEBUG: ", fcgio.LOG.DEBUG)
io.stderr:write("\n")
end
function fcgio:log_warn(fmt,...)
fcgio.LOG.WARN = string.format(fmt,...)
io.stderr:write("WARN: ", fcgio.LOG.WARN)
io.stderr:write("\n")
end
function fcgio:send_file(path)
local f = io.open(path, "rb")
local content = f:read("*all")
f:close()
fcgio.OUTPUT = fcgio.OUTPUT..content
end
end)
test("Setup request", function()
local json = require("json")
_SERVER = json.decodeFile("request.json")
assert(_SERVER ~= nil, "Global _SERVER object not found")
end)
test("SEVER PATH", function()
expect(_SERVER["LIB_DIR"], "/tmp/lib")
expect(_SERVER["TMP_DIR"], "/tmp")
expect(_SERVER["DB_DIR"], "/tmp")
expect(_SERVER["DOCUMENT_ROOT"], "/tmp/www")
end)
test("Import the hook", function()
package.path = "../silkmvc/?.lua"
local ret = require("core.hook")
expect(ret, true)
end)
test("Lua path", function()
expect(package.cpath, "/tmp/lib/lua/?.so")
expect(package.path, "/tmp/lib/lua/?.lua;/tmp/www/?.lua")
unexpect(ulib, nil)
unexpect(utils, nil)
unexpect(std, nil)
end)
test("HTTP Headers", function()
expect(HEADER["mobile"], false)
for k,v in pairs(_SERVER) do
if k:match("^HTTP_.*") then
local key = (k:gsub("HTTP_",""):gsub("_","-")):lower()
expect(HEADER[key],v)
end
end
end)
test("HTTP request", function()
expect(REQUEST.method, "POST")
expect(REQUEST.r, "post/id/1")
expect(REQUEST.id, "3")
expect(REQUEST.name, "John")
expect(REQUEST.firstname, "Dany")
expect(REQUEST.lastname, "LE")
expect(REQUEST.form_submitted, "1")
end)
test('HTTP COOKIE', function()
unexpect(SESSION, nil)
expect(SESSION.PHPSESSID, "298zf09hf012fh2")
expect(SESSION.csrftoken, "u32t4o3tb3gg43")
expect(SESSION._gat, "1")
end)
test("Echo", function()
echo("Hello ", "World: ", 10, true)
expect(fcgio.OUTPUT, "Hello World: 10true")
end)
test("STD response", function()
std.status(500)
expect(RESPONSE_HEADER.status, 500)
std.header("Content-Type", "text/html")
expect(RESPONSE_HEADER.header["Content-Type"], "text/html")
end)
test("STD Error", function()
fcgio:flush()
std.error(404, "No page found")
expect(fcgio.OUTPUT, "Status: 404 Not Found\r\nContent-Type: text/html\r\n\r\n<HTML><HEAD><TITLE>No page found</TITLE></HEAD><BODY><h2>No page found</h2></BODY></HTML>")
end)
test("STD header with cookie", function()
RESPONSE_HEADER.sent = false
fcgio:flush()
std.status(200)
std.header("Content-Type", "text/html")
std.setCookie("sessionid=12345;user=dany; path=/")
std.setCookie("date=now", "_gcat=1")
--print(JSON.encode(RESPONSE_HEADER))
std.header_flush()
echo("hello")
expect(fcgio.OUTPUT, "Status: 200 OK\r\nContent-Type: text/html\r\nSet-Cookie: sessionid=12345;user=dany; path=/\r\nSet-Cookie: date=now;_gcat=1\r\n\r\nhello")
end)
--- mimes test
test("STD Mime", function()
expect(std.mimeOf("request.json"), "application/json")
expect(std.mimeOf("test.exe"), "application/octet-stream")
end)
test("STD send file", function()
RESPONSE_HEADER.sent = false
fcgio:flush()
std.sendFile("request.json")
print(fcgio.OUTPUT)
end)
test("utils.is_array", function()
local tb = { name = "Dany", test = true}
expect(utils.is_array(tb), false)
local arr = {[1] = "Dany", [2] = true}
expect(utils.is_array(arr), true)
end)
test("utils.escape and utils.unescape", function()
local before = 'this is a escape string \\ " % \n \t \r'
local escaped = utils.escape(before)
expect(escaped, 'this is a escape string \\\\ \\" %% \\n \\t \\r')
expect(utils.unescape(escaped), before)
end)
test("utils.decodeURI", function()
local uri = "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
local decoded = utils.decodeURI(uri)
expect(decoded, "https://mozilla.org/?x=шеллы")
end)
test("utils.file_exists", function()
expect(utils.file_exists("request.json"), true)
expect(utils.file_exists("test1.json"), false)
end)
test("utils.parse_query", function()
local query = "r=1&id=3&name=John&desc=some%20thing&enc=this+is+encode"
local tb = utils.parse_query(query)
expect(tb.r, "1")
expect(tb.id, "3")
expect(tb.desc, "some thing")
expect(tb.enc, "this is encode")
expect(tb.name, "John")
end)
test("utils.url_parser", function()
local uri = "https://mozilla.org:9000/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
local obj = utils.url_parser(uri)
expect(obj.query, "/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B")
expect(obj.hostname, "mozilla.org")
expect(obj.protocol, "https")
expect(obj.port, 9000)
end)
test("utils explode/implode", function()
local str = "this is a test"
tbl = explode(str, " ")
expect(tbl[1], "this")
expect(tbl[2], "is")
expect(tbl[3], "a")
expect(tbl[4], "test")
local str1 = implode(tbl, "|")
expect(str1, "this|is|a|test")
end)
test("utils firstToUpper", function()
local str = "this is a test"
expect(firstToUpper(str), "This is a test")
end)
test("utils.ext", function()
expect(utils.ext("foo.bar"), "bar")
expect(utils.ext("foo.bar.baz"), "baz")
expect(utils.ext("foo"), nil)
end)
test("utils.basename", function()
expect(utils.basename("path/to/foo.bar"), "foo.bar")
end)
--- Test for sqlite database
test("sqlite.getdb", function()
require("silk.core.sqlite")
local path = "/tmp/test.db"
local db = sqlite.getdb("/tmp/test.db")
sqlite.dbclose(db)
expect(ulib.exists(path), true)
db = sqlite.getdb("system")
sqlite.dbclose(db)
expect(ulib.exists("/tmp/system.db"), true)
ulib.delete("/tmp/secret.db")
expect(ulib.exists("/tmp/secret.db"), false)
DB = DBModel:new{db="secret"}
DB:open()
expect(ulib.exists("/tmp/secret.db"), true)
unexpect(DB.db, nil)
unexpect(DB.db,"secret")
end)
test("DBModel:createTable", function()
ret = DB:createTable("test", {
first_name = "TEXT NOT NULL",
last_name = "TEXT NOT NULL",
age = "INTEGER"
})
expect(ret, true)
end)
test("DBModel:available", function()
expect(DB:available("test"), true)
end)
test("DBModel:insert", function()
local data = {
first_name = "Dany",
last_name = "LE",
age = 30,
phone = "Unknown"
}
expect(DB:insert("test",data), false)
data.phone = nil
expect(DB:insert("test",data), true)
data = {
first_name = "Lisa",
last_name = "LE",
age = 5
}
expect(DB:insert("test",data), true)
end)
test("DBModel:lastInsertID", function()
local id = DB:lastInsertID()
expect(id, 2)
end)
test("DBModel:get", function()
local record = DB:get("test", 2)
expect(record.id, 2)
expect(record.first_name, "Lisa")
expect(record.last_name, "LE")
expect(record.age, 5)
end)
test("DBModel:getAll", function()
local records = DB:getAll("test")
expect(#records, 2)
expect(records[1].id, 1)
expect(records[1].first_name, "Dany")
expect(records[1].last_name, "LE")
expect(records[1].age, 30)
expect(records[2].id, 2)
expect(records[2].first_name, "Lisa")
expect(records[2].last_name, "LE")
expect(records[2].age, 5)
end)
test("DBModel:find", function()
local cond = {
exp = {
["and"] = {
{
["="] = {
first_name = "Dany"
}
},
{
["="] = {
age = 25
}
}
}
}
}
local records = DB:find("test", cond)
expect(#records, 0)
cond.exp["and"][2]["="].age = 30
records = DB:find("test", cond)
expect(#records, 1)
cond = {
exp = {
["="] = {
last_name = "LE"
}
},
order = {
id = "DESC"
}
}
records = DB:find("test", cond)
expect(#records, 2)
expect(records[1].id, 2)
expect(records[1].first_name, "Lisa")
expect(records[1].last_name, "LE")
expect(records[1].age, 5)
end)
test("DBModel:update", function()
local data = {
id = 1,
first_name = "Dany Xuan-Sang",
age = 35,
}
expect(DB:update("test", data), true)
local record = DB:get("test", 1)
unexpect(record, nil)
expect(record.age , 35)
expect(record.first_name, "Dany Xuan-Sang")
end)
test("DBModel:deleteByID", function()
expect(DB:deleteByID("test", 1), true)
local record = DB:get("test", 1)
expect(record, nil)
end)
test("DBModel:delete", function()
local cond = {
["="] = {
last_name = "LE"
}
}
expect(DB:delete("test", cond), true)
local records = DB:getAll("test")
expect(#records, 0)
end)
--- test enc module
test("Base64 encode/decode", function()
enc = require("enc")
local string = "this is the test"
local encode = enc.b64encode(string)
expect(encode,"dGhpcyBpcyB0aGUgdGVzdA==")
local buf = enc.b64decode(encode)
unexpect(buf,nil)
expect(tostring(buf), string)
end)
test("md5 encode", function()
expect(enc.md5("this is a test"), "54b0c58c7ce9f2a8b551351102ee0938")
end)
test("sha1 encode", function()
expect(enc.sha1("this is a test"), "fa26be19de6bff93f70bc2308434e4a440bbad02")
end)
--- run all unit tests
run()

270
test/test_silk.lua Normal file
View File

@ -0,0 +1,270 @@
--- the binary shall be compiled with
--- make CFLAGS=-DLUA_SLICE_MAGIC=0x8AD73B9F
--- otherwise the tests unable to load C modules
require("lunit")
package.cpath = "/tmp/lib/lua/?.so"
package.path = "/tmp/lib/lua/?.lua"
test("IO setup", function()
fcgio = { OUTPUT = "",
LOG = {
INFO = "",
ERROR = "",
DEBUG = "",
WARN = ""
}
}
function fcgio:flush()
fcgio.OUTPUT = ""
fcgio.LOG.INFO = ""
fcgio.LOG.DEBUG = ""
fcgio.LOG.WARN = ""
fcgio.LOG.ERROR = ""
RESPONSE_HEADER.sent = false
end
function fcgio:echo(...)
local args = table.pack(...)
for i=1,args.n do
-- do something with args[i], careful, it might be nil!
fcgio.OUTPUT = fcgio.OUTPUT..tostring(args[i])
end
end
function fcgio:log_info(fmt,...)
fcgio.LOG.INFO = string.format(fmt,...)
io.stderr:write("INFO: ", fcgio.LOG.INFO)
io.stderr:write("\n")
end
function fcgio:log_error(fmt,...)
fcgio.LOG.ERROR = string.format(fmt,...)
io.stderr:write("ERROR: ",fcgio.LOG.ERROR)
io.stderr:write("\n")
end
function fcgio:log_debug(fmt,...)
fcgio.LOG.DEBUG = string.format(fmt,...)
io.stderr:write("DEBUG: ", fcgio.LOG.DEBUG)
io.stderr:write("\n")
end
function fcgio:log_warn(fmt,...)
fcgio.LOG.WARN = string.format(fmt,...)
io.stderr:write("WARN: ", fcgio.LOG.WARN)
io.stderr:write("\n")
end
function fcgio:send_file(path)
local f = io.open(path, "rb")
local content = f:read("*all")
f:close()
fcgio.OUTPUT = fcgio.OUTPUT..content
end
end)
test("Setup request", function()
local json = require("json")
_SERVER = json.decodeFile("request.json")
assert(_SERVER ~= nil, "Global _SERVER object not found")
end)
test("SEVER PATH", function()
expect(_SERVER["LIB_DIR"], "/tmp/lib")
expect(_SERVER["TMP_DIR"], "/tmp")
expect(_SERVER["DB_DIR"], "/tmp")
expect(_SERVER["DOCUMENT_ROOT"], "/tmp/www")
end)
test("Import the api", function()
local ret = require("silk.api")
end)
test("Lua path", function()
expect(package.cpath, "/tmp/lib/lua/?.so")
expect(package.path, "/tmp/lib/lua/?.lua;/tmp/www/?.lua")
unexpect(ulib, nil)
unexpect(utils, nil)
unexpect(std, nil)
end)
test("Logger", function()
local logger = Logger:new{ level = Logger.ERROR}
logger:info("Info message")
logger:debug("Debug message")
logger:warn("Warning message")
logger:error("Error message")
expect(fcgio.LOG.INFO, "")
expect(fcgio.LOG.DEBUG, "")
expect(fcgio.LOG.WARN, "")
expect(fcgio.LOG.ERROR, "Error message")
logger.level = Logger.INFO
logger:info("Info message")
logger:debug("Debug message")
logger:warn("Warning message")
logger:error("Error message")
expect(fcgio.LOG.ERROR, "Error message")
expect(fcgio.LOG.DEBUG, "")
expect(fcgio.LOG.WARN, "Warning message")
expect(fcgio.LOG.INFO, "Info message")
end)
test("BaseObject", function()
fcgio:flush()
local obj = BaseObject:new{ registry = {
logger = Logger:new{level = Logger.DEBUG}
} }
obj:info('Info message')
expect(fcgio.LOG.INFO, "Info message")
obj:debug("Debug message")
expect(fcgio.LOG.DEBUG, "Debug message")
obj:warn("Warning message")
expect(fcgio.LOG.WARN, "Warning message")
obj:print()
expect(fcgio.LOG.DEBUG, "BaseObject")
--obj:error("Error message")
end)
test("Silk define env", function()
DIR_SEP = "/"
BASE_FRW = ""
WWW_ROOT = "/tmp/www"
HTTP_ROOT = "https://apps.localhost:9195/"
CONTROLLER_ROOT = ""
-- class path: path.to.class
MODEL_ROOT = BASE_FRW
-- file path: path/to/file
VIEW_ROOT = WWW_ROOT
ulib.delete(WWW_ROOT)
expect(ulib.mkdir(WWW_ROOT), true)
expect(ulib.mkdir(WWW_ROOT.."/post"), true)
expect(ulib.send_file("request.json", WWW_ROOT.."/rq.json"), true)
expect(ulib.send_file("layout.ls", WWW_ROOT.."/layout.ls"), true)
expect(ulib.send_file("detail.ls", WWW_ROOT.."/post/detail.ls"), true)
expect(ulib.send_file("ad.ls", WWW_ROOT.."/post/ad.ls"), true)
end)
test("Define model", function()
BaseModel:subclass("NewsModel",{
registry = {},
name = "news",
fields = {
content = "TEXT"
}
})
local REGISTRY = {}
ulib.delete("/tmp/news.db")
-- set logging level
REGISTRY.logger = Logger:new{ level = Logger.INFO }
REGISTRY.layout = '/'
REGISTRY.db = DBModel:new {db = "news"}
REGISTRY.db:open()
local model = NewsModel:new{registry = REGISTRY}
-- insert data
expect(model:create({content = "Hello HELL"}), true)
expect(model:create({content = "Goodbye"}), true)
local records = model:findAll()
expect(#records, 2)
expect(model:update({id =1, content = "Hello World"}), true)
expect(model:delete({ ["="] = {id = 2} }), true)
records = model:findAll()
expect(#records, 1)
local record = model:get(1)
unexpect(record, nil)
expect(record.content, "Hello World")
records = model:select("id as ID, content", "1=1")
unexpect(records, nil)
expect(#records, 1)
expect(records[1].ID,1)
REGISTRY.db:close()
end)
test("Define controller", function()
BaseController:subclass("PostController",{
registry = {},
models = {"news"}
})
function PostController:id(n)
local record = self.news:get(n)
self.template:set("data", record)
--self.template:set("id", n)
self.template:setView("detail")
return true
end
function PostController:ad()
self.template:set("ad", "AD HERE")
return true
end
end)
test("Router infer controller", function()
local router = Router:new{}
local action = router:infer()
expect(action.controller.class, "PostController")
expect(action.action, "id")
expect(action.args[1], "1")
end)
test("Router infer asset", function()
fcgio:flush()
local router = Router:new{registry = {
fileaccess = true
}}
local action = router:infer("/rq.json")
expect(action.controller.class, "AssetController")
expect(action.action, "get")
expect(action.args[1], "rq.json")
local ret = router:call(action)
expect(ret, false)
io.stderr:write(fcgio.OUTPUT)
io.stderr:write("\n")
end)
test("Router fetch views with dependencies", function()
fcgio:flush()
local REGISTRY = {}
REGISTRY.db = DBModel:new {db = "news"}
REGISTRY.db:open()
-- set logging level
REGISTRY.logger = Logger:new{ level = Logger.INFO }
REGISTRY.layout = '/'
local default_routes_dependencies = {
ad = {
url = "post/ad",
visibility = {
shown = true,
routes = {
["post/id"] = true
}
}
}
}
local router = Router:new{registry = REGISTRY}
router:route('/', default_routes_dependencies )
router:delegate()
REGISTRY.db:close()
expect(fcgio.OUTPUT, "Status: 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\nPost ID:1.0\nContent:Hello World\nAD HERE")
end)
test("Controller action not found", function()
fcgio:flush()
REQUEST.r = "/post/all"
local router = Router:new{registry = {}}
local s,e = pcall(router.delegate, router)
expect(s, false)
expect(fcgio.OUTPUT,"Status: 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n#action all is not found in controller PostController")
end)
test("Controller not found", function()
fcgio:flush()
REQUEST.r = "/user/dany"
local REGISTRY = {}
-- set logging level
--REGISTRY.logger = Logger:new{ level = Logger.INFO }
REGISTRY.layout = '/'
local router = Router:new{registry = REGISTRY}
local s,e = pcall(router.delegate, router)
expect(s, false)
print(fcgio.OUTPUT)
end)
-- run all test
run()