🔧 build: 调整oop相关库
parent
2f2409bad9
commit
7540100b4b
@ -1,33 +0,0 @@
|
||||
-- https://github.com/Tjakka5/Enum
|
||||
|
||||
local Enum = {}
|
||||
local Meta = {
|
||||
__index = function(_, k)
|
||||
error("Attempt to index non-existant enum '" .. tostring(k) .. "'.", 2)
|
||||
end,
|
||||
__newindex = function()
|
||||
error("Attempt to write to static enum", 2)
|
||||
end,
|
||||
}
|
||||
|
||||
function Enum.new(...)
|
||||
local values = {...}
|
||||
|
||||
if type(values[1]) == "table" then
|
||||
values = values[1]
|
||||
end
|
||||
|
||||
local enum = {}
|
||||
|
||||
for i = 1, #values do
|
||||
enum[values[i]] = values[i]
|
||||
end
|
||||
|
||||
return setmetatable(enum, Meta)
|
||||
end
|
||||
|
||||
return setmetatable(Enum, {
|
||||
__call = function(_, ...)
|
||||
return Enum.new(...)
|
||||
end,
|
||||
})
|
||||
@ -1,199 +0,0 @@
|
||||
-- https://github.com/jojo59516/middleclass
|
||||
local middleclass = {}
|
||||
|
||||
local function _createIndexWrapper(aClass, f)
|
||||
if f == nil then
|
||||
return aClass.__instanceDict
|
||||
elseif type(f) == "function" then
|
||||
return function(self, name)
|
||||
local value = aClass.__instanceDict[name]
|
||||
|
||||
if value ~= nil then
|
||||
return value
|
||||
else
|
||||
return (f(self, name))
|
||||
end
|
||||
end
|
||||
else -- if type(f) == "table" then
|
||||
return function(self, name)
|
||||
local value = aClass.__instanceDict[name]
|
||||
|
||||
if value ~= nil then
|
||||
return value
|
||||
else
|
||||
return f[name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _propagateInstanceMethod(aClass, name, f)
|
||||
f = name == "__index" and _createIndexWrapper(aClass, f) or f
|
||||
aClass.__instanceDict[name] = f
|
||||
|
||||
for subclass in pairs(aClass.subclasses) do
|
||||
if rawget(subclass.__declaredMethods, name) == nil then
|
||||
_propagateInstanceMethod(subclass, name, f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _declareInstanceMethod(aClass, name, f)
|
||||
aClass.__declaredMethods[name] = f
|
||||
|
||||
if f == nil and aClass.super then
|
||||
f = aClass.super.__instanceDict[name]
|
||||
end
|
||||
|
||||
_propagateInstanceMethod(aClass, name, f)
|
||||
end
|
||||
|
||||
local function _tostring(self)
|
||||
return "class " .. self.name
|
||||
end
|
||||
local function _call(self, ...)
|
||||
return self:new(...)
|
||||
end
|
||||
|
||||
local function _createClass(name, super)
|
||||
local dict = {}
|
||||
dict.__index = dict
|
||||
|
||||
local aClass = {
|
||||
name = name,
|
||||
super = super,
|
||||
static = {},
|
||||
__instanceDict = dict,
|
||||
__declaredMethods = {},
|
||||
subclasses = setmetatable({}, {
|
||||
__mode = 'k',
|
||||
}),
|
||||
}
|
||||
|
||||
if super then
|
||||
setmetatable(aClass.static, {
|
||||
__index = function(_, k)
|
||||
local result = rawget(dict, k)
|
||||
if result == nil then
|
||||
return super.static[k]
|
||||
end
|
||||
return result
|
||||
end,
|
||||
})
|
||||
else
|
||||
setmetatable(aClass.static, {
|
||||
__index = function(_, k)
|
||||
return rawget(dict, k)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
setmetatable(aClass, {
|
||||
__index = aClass.static,
|
||||
__tostring = _tostring,
|
||||
__call = _call,
|
||||
__newindex = _declareInstanceMethod,
|
||||
})
|
||||
|
||||
return aClass
|
||||
end
|
||||
|
||||
local function _includeMixin(aClass, mixin)
|
||||
assert(type(mixin) == 'table', "mixin must be a table")
|
||||
|
||||
for name, method in pairs(mixin) do
|
||||
if name ~= "included" and name ~= "static" then
|
||||
aClass[name] = method
|
||||
end
|
||||
end
|
||||
|
||||
for name, method in pairs(mixin.static or {}) do
|
||||
aClass.static[name] = method
|
||||
end
|
||||
|
||||
if type(mixin.included) == "function" then
|
||||
mixin:included(aClass)
|
||||
end
|
||||
return aClass
|
||||
end
|
||||
|
||||
local DefaultMixin = {
|
||||
__tostring = function(self)
|
||||
return "instance of " .. tostring(self.class)
|
||||
end,
|
||||
|
||||
initialize = function(self, ...)
|
||||
end,
|
||||
|
||||
isInstanceOf = function(self, aClass)
|
||||
return type(aClass) == 'table' and type(self) == 'table' and
|
||||
(self.class == aClass or type(self.class) == 'table' and type(self.class.isSubclassOf) == 'function' and
|
||||
self.class:isSubclassOf(aClass))
|
||||
end,
|
||||
|
||||
static = {
|
||||
allocate = function(self)
|
||||
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
|
||||
return setmetatable({
|
||||
class = self,
|
||||
}, self.__instanceDict)
|
||||
end,
|
||||
|
||||
new = function(self, ...)
|
||||
assert(type(self) == 'table', "Make sure that you are using 'Class:new' instead of 'Class.new'")
|
||||
local instance = self:allocate()
|
||||
instance:initialize(...)
|
||||
return instance
|
||||
end,
|
||||
|
||||
subclass = function(self, name)
|
||||
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")
|
||||
assert(type(name) == "string", "You must provide a name(string) for your class")
|
||||
|
||||
local subclass = _createClass(name, self)
|
||||
|
||||
for methodName, f in pairs(self.__instanceDict) do
|
||||
if not (methodName == "__index" and type(f) == "table") then
|
||||
_propagateInstanceMethod(subclass, methodName, f)
|
||||
end
|
||||
end
|
||||
subclass.initialize = function(instance, ...)
|
||||
return self.initialize(instance, ...)
|
||||
end
|
||||
|
||||
self.subclasses[subclass] = true
|
||||
self:subclassed(subclass)
|
||||
|
||||
return subclass
|
||||
end,
|
||||
|
||||
subclassed = function(self, other)
|
||||
end,
|
||||
|
||||
isSubclassOf = function(self, other)
|
||||
return type(other) == 'table' and type(self.super) == 'table' and
|
||||
(self.super == other or self.super:isSubclassOf(other))
|
||||
end,
|
||||
|
||||
include = function(self, ...)
|
||||
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")
|
||||
for _, mixin in ipairs({...}) do
|
||||
_includeMixin(self, mixin)
|
||||
end
|
||||
return self
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
function middleclass.class(name, super)
|
||||
assert(type(name) == 'string', "A name (string) is needed for the new class")
|
||||
return super and super:subclass(name) or _includeMixin(_createClass(name), DefaultMixin)
|
||||
end
|
||||
|
||||
setmetatable(middleclass, {
|
||||
__call = function(_, ...)
|
||||
return middleclass.class(...)
|
||||
end,
|
||||
})
|
||||
|
||||
return middleclass
|
||||
@ -1,24 +0,0 @@
|
||||
-- https://github.com/ichesnokov/middleclass-mixin-singleton
|
||||
|
||||
local singleton = {
|
||||
static = {},
|
||||
}
|
||||
|
||||
function singleton:included(class)
|
||||
-- Override new to throw an error, but store a reference to the old "new" method
|
||||
class.static._new = class.static.new
|
||||
class.static.new = function()
|
||||
error("Use " .. class.name .. ":instance() instead of :new()")
|
||||
end
|
||||
end
|
||||
|
||||
function singleton.static:instance(...)
|
||||
self._instance = self._instance or self._new(self, ...) -- use old "new" method
|
||||
return self._instance
|
||||
end
|
||||
|
||||
function singleton.static:clear_instance()
|
||||
self._instance = nil
|
||||
end
|
||||
|
||||
return singleton
|
||||
@ -0,0 +1,294 @@
|
||||
local skynet = require "skynet"
|
||||
|
||||
local type = type
|
||||
local pcall = pcall
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
local rawget = rawget
|
||||
local rawset = rawset
|
||||
local tostring = tostring
|
||||
local ssub = string.sub
|
||||
local sformat = string.format
|
||||
local dgetinfo = debug.getinfo
|
||||
local setmetatable = setmetatable
|
||||
local tinsert = table.insert
|
||||
|
||||
local is_class
|
||||
|
||||
-- 类模板
|
||||
local class_tpls = {}
|
||||
|
||||
local function deep_copy(src, dst)
|
||||
local ndst = dst or {}
|
||||
for key, value in pairs(src or {}) do
|
||||
if is_class(value) then
|
||||
ndst[key] = value()
|
||||
elseif (type(value) == "table") then
|
||||
ndst[key] = deep_copy(value)
|
||||
else
|
||||
ndst[key] = value
|
||||
end
|
||||
end
|
||||
return ndst
|
||||
end
|
||||
|
||||
local function mixin_init(class, object, ...)
|
||||
if class.__super then
|
||||
mixin_init(class.__super, object, ...)
|
||||
end
|
||||
for _, mixin in ipairs(class.__mixins) do
|
||||
if type(mixin.__init) == "function" then
|
||||
mixin.__init(object, ...)
|
||||
end
|
||||
end
|
||||
return object
|
||||
end
|
||||
|
||||
local function object_init(class, object, ...)
|
||||
if class.__super then
|
||||
object_init(class.__super, object, ...)
|
||||
end
|
||||
if type(class.__init) == "function" then
|
||||
class.__init(object, ...)
|
||||
end
|
||||
return object
|
||||
end
|
||||
|
||||
local function object_release(class, object, ...)
|
||||
if type(class.__release) == "function" then
|
||||
class.__release(object, ...)
|
||||
end
|
||||
if class.__super then
|
||||
object_release(class.__super, object, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function object_defer(class, object, ...)
|
||||
if type(class.__defer) == "function" then
|
||||
class.__defer(object, ...)
|
||||
end
|
||||
if class.__super then
|
||||
object_defer(class.__super, object, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function object_default(class, object)
|
||||
if class.__super then
|
||||
object_default(class.__super, object)
|
||||
end
|
||||
local defaults = deep_copy(class.__default)
|
||||
for name, param in pairs(defaults) do
|
||||
object[name] = param[1]
|
||||
end
|
||||
end
|
||||
|
||||
local function object_tostring(object)
|
||||
if type(object.tostring) == "function" then
|
||||
return object:tostring()
|
||||
end
|
||||
return sformat("class:%s(%s)", object.__moudle, object.__addr)
|
||||
end
|
||||
|
||||
local function object_constructor(class, ...)
|
||||
local obj = {}
|
||||
object_default(class, obj)
|
||||
obj.__addr = ssub(tostring(obj), 7)
|
||||
local object = setmetatable(obj, class.__vtbl)
|
||||
object_init(class, object, ...)
|
||||
mixin_init(class, object, ...)
|
||||
return object
|
||||
end
|
||||
|
||||
local function new(class, ...)
|
||||
if class.__singleton then
|
||||
local inst_obj = rawget(class, "__inst")
|
||||
if not inst_obj then
|
||||
inst_obj = object_constructor(class, ...)
|
||||
-- 定义单例方法
|
||||
local inst_func = function()
|
||||
return inst_obj
|
||||
end
|
||||
rawset(class, "__inst", inst_obj)
|
||||
rawset(class, "inst", inst_func)
|
||||
end
|
||||
return inst_obj
|
||||
else
|
||||
return object_constructor(class, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function index(class, field)
|
||||
return class.__vtbl[field]
|
||||
end
|
||||
|
||||
local function newindex(class, field, value)
|
||||
class.__vtbl[field] = value
|
||||
end
|
||||
|
||||
local function release(obj)
|
||||
object_release(obj.__class, obj)
|
||||
end
|
||||
|
||||
local function defer(obj)
|
||||
object_defer(obj.__class, obj)
|
||||
end
|
||||
|
||||
local classMT = {
|
||||
__call = new,
|
||||
__index = index,
|
||||
__newindex = newindex,
|
||||
}
|
||||
|
||||
local function invoke(object, method, ...)
|
||||
local class = object.__class
|
||||
for _, mixin in ipairs(class.__mixins) do
|
||||
local mixin_method = mixin[method]
|
||||
if mixin_method then
|
||||
local ok, res = pcall(mixin_method, object, ...)
|
||||
if not ok then
|
||||
error(sformat("mixin: %s invoke '%s' failed: %s.", mixin.__moudle, method, res))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 返回true表示所有接口都完成
|
||||
local function collect(object, method, ...)
|
||||
local class = object.__class
|
||||
for _, mixin in ipairs(class.__mixins) do
|
||||
local mixin_method = mixin[method]
|
||||
if mixin_method then
|
||||
local ok, res = pcall(mixin_method, object, ...)
|
||||
if not ok then
|
||||
error(sformat("mixin: %s collect '%s' failed: %s.", mixin.__moudle, method, res))
|
||||
return false
|
||||
end
|
||||
if not res then
|
||||
skynet.error(sformat("mixin: %s collect '%s' failed: %s.", mixin.__moudle, method, res))
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- 代理一个类的所有接口,并检测接口是否实现
|
||||
local function implemented(class, mixins)
|
||||
class.invoke = invoke
|
||||
class.collect = collect
|
||||
for _, mixin in ipairs(mixins) do
|
||||
-- 属性处理
|
||||
for name, value in pairs(mixin.__default) do
|
||||
if class.__default[name] then
|
||||
skynet.error(sformat("the mixin default %s has repeat defined.", name))
|
||||
end
|
||||
class.__default[name] = value
|
||||
local access_prefix = {"is_", "get_", "set_"}
|
||||
for _, prefix in pairs(access_prefix) do
|
||||
local access_method = prefix .. name
|
||||
if mixin[access_method] then
|
||||
tinsert(mixin.__methods, access_method)
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, method in pairs(mixin.__methods) do
|
||||
if not mixin[method] then
|
||||
skynet.error(sformat("the mixin method %s hasn't implemented.", method))
|
||||
mixin[method] = function()
|
||||
skynet.error(sformat("the mixin method %s hasn't implemented.", method))
|
||||
end
|
||||
end
|
||||
if class[method] then
|
||||
skynet.error(sformat("the mixin method %s has repeat implemented.", method))
|
||||
goto continue
|
||||
end
|
||||
-- 接口代理
|
||||
class[method] = function(...)
|
||||
return mixin[method](...)
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
tinsert(class.__mixins, mixin)
|
||||
end
|
||||
end
|
||||
|
||||
local function class_constructor(class, super, ...)
|
||||
local info = dgetinfo(2, "S")
|
||||
local moudle = info.short_src
|
||||
local class_tpl = class_tpls[moudle]
|
||||
if not class_tpl then
|
||||
local vtbl = {
|
||||
__class = class,
|
||||
__moudle = moudle,
|
||||
__tostring = object_tostring,
|
||||
}
|
||||
vtbl.__gc = release
|
||||
vtbl.__close = defer
|
||||
vtbl.__index = vtbl
|
||||
if super then
|
||||
setmetatable(vtbl, {
|
||||
__index = super,
|
||||
})
|
||||
end
|
||||
class.__vtbl = vtbl
|
||||
class.__super = super
|
||||
class.__default = {}
|
||||
class.__mixins = {}
|
||||
class_tpl = setmetatable(class, classMT)
|
||||
implemented(class, {...})
|
||||
class_tpls[moudle] = class_tpl
|
||||
end
|
||||
return class_tpl
|
||||
end
|
||||
|
||||
function class(super, ...)
|
||||
return class_constructor({}, super, ...)
|
||||
end
|
||||
|
||||
function singleton(super, ...)
|
||||
return class_constructor({
|
||||
__singleton = true,
|
||||
}, super, ...)
|
||||
end
|
||||
|
||||
-- function super(class)
|
||||
-- return rawget(class, "__super")
|
||||
-- end
|
||||
|
||||
-- function classof(object)
|
||||
-- return object.__class
|
||||
-- end
|
||||
|
||||
is_class = function(class)
|
||||
return classMT == getmetatable(class)
|
||||
end
|
||||
|
||||
local function is_subclass(class, super)
|
||||
while class do
|
||||
if class == super then
|
||||
return true
|
||||
end
|
||||
class = rawget(class, "__super")
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function instanceof(object, class)
|
||||
if not object or not class then
|
||||
return false
|
||||
end
|
||||
local obj_class = object.__class
|
||||
if obj_class then
|
||||
return is_subclass(obj_class, class)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- function conv_class(name)
|
||||
-- local runtime = sformat("local obj = %s() return obj", name)
|
||||
-- local ok, obj = pcall(load(runtime))
|
||||
-- if ok then
|
||||
-- return obj
|
||||
-- end
|
||||
-- end
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
local type = type
|
||||
|
||||
-- 支持 function 和 对象内部 handler 不过后者热更会更友好
|
||||
return function (method, obj, params)
|
||||
return function(...)
|
||||
if type(method) == "string" then
|
||||
@ -0,0 +1,8 @@
|
||||
require "oop.class"
|
||||
require "oop.mixin"
|
||||
require "oop.property"
|
||||
require "oop.enum"
|
||||
|
||||
handler = require "oop.handler"
|
||||
Option = require "oop.option"
|
||||
Try = require "oop.try"
|
||||
@ -0,0 +1,60 @@
|
||||
require "oop.init"
|
||||
|
||||
local IObject = mixin("test1", "test2", "test3", "test4")
|
||||
|
||||
local prop = Property(IObject)
|
||||
prop:accessor("key1", 1)
|
||||
prop:accessor("key2", 2)
|
||||
|
||||
function IObject:__init()
|
||||
end
|
||||
|
||||
function IObject:test1()
|
||||
print("key1", self:get_key1())
|
||||
self:set_key2(4)
|
||||
print("key2", self:get_key2())
|
||||
self:set_key3(6)
|
||||
print("key3", self:get_key3())
|
||||
end
|
||||
|
||||
function IObject:test2()
|
||||
print("key2", self.key2)
|
||||
end
|
||||
|
||||
function IObject:test3()
|
||||
print("key3", self.key3)
|
||||
end
|
||||
|
||||
local Object = class(nil, IObject)
|
||||
local prop2 = Property(Object)
|
||||
prop2:accessor("key3", 3)
|
||||
function Object:__init()
|
||||
end
|
||||
|
||||
function Object:__release()
|
||||
print("release", self)
|
||||
end
|
||||
|
||||
function Object:run()
|
||||
print("key3", self:get_key3())
|
||||
print("key1", self:get_key1())
|
||||
print("key2", self:get_key2())
|
||||
self:invoke("test1")
|
||||
end
|
||||
|
||||
local TEST1 = Enum("TEST1", 0, "ONE", "THREE", "TWO")
|
||||
print(TEST1.TWO)
|
||||
local TEST2 = Enum("TEST2", 1, "ONE", "THREE", "TWO")
|
||||
TEST2.FOUR = TEST2()
|
||||
print(TEST2.TWO, TEST2.FOUR)
|
||||
local TEST3 = Enum("TEST3", 0)
|
||||
TEST3("ONE")
|
||||
TEST3("TWO")
|
||||
TEST3("FOUR", 4)
|
||||
local five = TEST3("FIVE")
|
||||
print(TEST3.TWO, TEST3.FOUR, TEST3.FIVE, five)
|
||||
|
||||
local obj = Object()
|
||||
obj:run()
|
||||
|
||||
return Object
|
||||
@ -0,0 +1,56 @@
|
||||
--[[property.lua
|
||||
local Object = class()
|
||||
prop = property(Object)
|
||||
prop:reader("id", 0)
|
||||
prop:accessor("name", "")
|
||||
--]]
|
||||
local ACCESSOR = 1
|
||||
local WRITER = 2
|
||||
local READER = 3
|
||||
|
||||
local function prop_accessor(_, class, name, default, mode, cb)
|
||||
class.__default[name] = {default}
|
||||
if mode <= WRITER then
|
||||
class["set_" .. name] = function(self, value)
|
||||
if self[name] == nil or self[name] ~= value then
|
||||
self[name] = value
|
||||
if cb then
|
||||
cb(self, name, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
mode = mode + 2
|
||||
end
|
||||
if mode <= READER then
|
||||
class["get_" .. name] = function(self)
|
||||
if self[name] == nil then
|
||||
return default
|
||||
end
|
||||
return self[name]
|
||||
end
|
||||
if type(default) == "boolean" then
|
||||
class["is_" .. name] = class["get_" .. name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local property_reader = function(self, name, default)
|
||||
prop_accessor(self, self.__class, name, default, READER)
|
||||
end
|
||||
local property_writer = function(self, name, default, cb)
|
||||
prop_accessor(self, self.__class, name, default, WRITER, cb)
|
||||
end
|
||||
local property_accessor = function(self, name, default, cb)
|
||||
prop_accessor(self, self.__class, name, default, ACCESSOR, cb)
|
||||
end
|
||||
|
||||
function Property(class)
|
||||
local prop = {
|
||||
__class = class,
|
||||
reader = property_reader,
|
||||
writer = property_writer,
|
||||
accessor = property_accessor,
|
||||
}
|
||||
return prop
|
||||
end
|
||||
|
||||
@ -1,473 +0,0 @@
|
||||
--[[
|
||||
Minimal test framework for Lua.
|
||||
lester - v0.1.2 - 15/Feb/2021
|
||||
Eduardo Bart - edub4rt@gmail.com
|
||||
https://github.com/edubart/lester
|
||||
Minimal Lua test framework.
|
||||
See end of file for LICENSE.
|
||||
]] --[[--
|
||||
Lester is a minimal unit testing framework for Lua with a focus on being simple to use.
|
||||
|
||||
## Features
|
||||
|
||||
* Minimal, just one file.
|
||||
* Self contained, no external dependencies.
|
||||
* Simple and hackable when needed.
|
||||
* Use `describe` and `it` blocks to describe tests.
|
||||
* Supports `before` and `after` handlers.
|
||||
* Colored output.
|
||||
* Configurable via the script or with environment variables.
|
||||
* Quiet mode, to use in live development.
|
||||
* Optionally filter tests by name.
|
||||
* Show traceback on errors.
|
||||
* Show time to complete tests.
|
||||
* Works with Lua 5.1+.
|
||||
* Efficient.
|
||||
|
||||
## Usage
|
||||
|
||||
Copy `lester.lua` file to a project and require it,
|
||||
which returns a table that includes all of the functionality:
|
||||
|
||||
```lua
|
||||
local lester = require 'lester'
|
||||
local describe, it, expect = lester.describe, lester.it, lester.expect
|
||||
|
||||
-- Customize lester configuration.
|
||||
lester.show_traceback = false
|
||||
|
||||
describe('my project', function()
|
||||
lester.before(function()
|
||||
-- This function is run before every test.
|
||||
end)
|
||||
|
||||
describe('module1', function() -- Describe blocks can be nested.
|
||||
it('feature1', function()
|
||||
expect.equal('something', 'something') -- Pass.
|
||||
end)
|
||||
|
||||
it('feature2', function()
|
||||
expect.truthy(false) -- Fail.
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
lester.report() -- Print overall statistic of the tests run.
|
||||
lester.exit() -- Exit with success if all tests passed.
|
||||
```
|
||||
|
||||
## Customizing output with environment variables
|
||||
|
||||
To customize the output of lester externally,
|
||||
you can set the following environment variables before running a test suite:
|
||||
|
||||
* `LESTER_QUIET="true"`, omit print of passed tests.
|
||||
* `LESTER_COLORED="false"`, disable colored output.
|
||||
* `LESTER_SHOW_TRACEBACK="false"`, disable traceback on test failures.
|
||||
* `LESTER_SHOW_ERROR="false"`, omit print of error description of failed tests.
|
||||
* `LESTER_STOP_ON_FAIL="true"`, stop on first test failure.
|
||||
* `LESTER_UTF8TERM="false"`, disable printing of UTF-8 characters.
|
||||
* `LESTER_FILTER="some text"`, filter the tests that should be run.
|
||||
|
||||
Note that these configurations can be changed via script too, check the documentation.
|
||||
|
||||
]] -- Returns whether the terminal supports UTF-8 characters.
|
||||
local function is_utf8term()
|
||||
local lang = os.getenv('LANG')
|
||||
return (lang and lang:lower():match('utf%-8$')) and true or false
|
||||
end
|
||||
|
||||
-- Returns whether a system environment variable is "true".
|
||||
local function getboolenv(varname, default)
|
||||
local val = os.getenv(varname)
|
||||
if val == 'true' then
|
||||
return true
|
||||
elseif val == 'false' then
|
||||
return false
|
||||
end
|
||||
return default
|
||||
end
|
||||
|
||||
-- The lester module.
|
||||
local lester = {
|
||||
--- Weather lines of passed tests should not be printed. False by default.
|
||||
quiet = getboolenv('LESTER_QUIET', false),
|
||||
--- Weather the output should be colorized. True by default.
|
||||
colored = getboolenv('LESTER_COLORED', true),
|
||||
--- Weather a traceback must be shown on test failures. True by default.
|
||||
show_traceback = getboolenv('LESTER_SHOW_TRACEBACK', true),
|
||||
--- Weather the error description of a test failure should be shown. True by default.
|
||||
show_error = getboolenv('LESTER_SHOW_ERROR', true),
|
||||
--- Weather test suite should exit on first test failure. False by default.
|
||||
stop_on_fail = getboolenv('LESTER_STOP_ON_FAIL', false),
|
||||
--- Weather we can print UTF-8 characters to the terminal. True by default when supported.
|
||||
utf8term = getboolenv('LESTER_UTF8TERM', is_utf8term()),
|
||||
--- A string with a lua pattern to filter tests. Nil by default.
|
||||
filter = os.getenv('LESTER_FILTER'),
|
||||
--- Function to retrieve time in seconds with milliseconds precision, `os.clock` by default.
|
||||
seconds = os.clock,
|
||||
}
|
||||
|
||||
-- Variables used internally for the lester state.
|
||||
local lester_start = nil
|
||||
local last_succeeded = false
|
||||
local level = 0
|
||||
local successes = 0
|
||||
local total_successes = 0
|
||||
local failures = 0
|
||||
local total_failures = 0
|
||||
local start = 0
|
||||
local befores = {}
|
||||
local afters = {}
|
||||
local names = {}
|
||||
|
||||
-- Color codes.
|
||||
local color_codes = {
|
||||
reset = string.char(27) .. '[0m',
|
||||
bright = string.char(27) .. '[1m',
|
||||
red = string.char(27) .. '[31m',
|
||||
green = string.char(27) .. '[32m',
|
||||
blue = string.char(27) .. '[34m',
|
||||
magenta = string.char(27) .. '[35m',
|
||||
}
|
||||
|
||||
-- Colors table, returning proper color code if colored mode is enabled.
|
||||
local colors = setmetatable({}, {
|
||||
__index = function(_, key)
|
||||
return lester.colored and color_codes[key] or ''
|
||||
end,
|
||||
})
|
||||
|
||||
--- Table of terminal colors codes, can be customized.
|
||||
lester.colors = colors
|
||||
|
||||
--- Describe a block of tests, which consists in a set of tests.
|
||||
-- Describes can be nested.
|
||||
-- @param name A string used to describe the block.
|
||||
-- @param func A function containing all the tests or other describes.
|
||||
function lester.describe(name, func)
|
||||
if level == 0 then -- Get start time for top level describe blocks.
|
||||
start = lester.seconds()
|
||||
if not lester_start then
|
||||
lester_start = start
|
||||
end
|
||||
end
|
||||
-- Setup describe block variables.
|
||||
failures = 0
|
||||
successes = 0
|
||||
level = level + 1
|
||||
names[level] = name
|
||||
-- Run the describe block.
|
||||
func()
|
||||
-- Cleanup describe block.
|
||||
afters[level] = nil
|
||||
befores[level] = nil
|
||||
names[level] = nil
|
||||
level = level - 1
|
||||
-- Pretty print statistics for top level describe block.
|
||||
if level == 0 and not lester.quiet and (successes > 0 or failures > 0) then
|
||||
local io_write = io.write
|
||||
local colors_reset, colors_green = colors.reset, colors.green
|
||||
io_write(failures == 0 and colors_green or colors.red, '[====] ', colors.magenta, name, colors_reset, ' | ',
|
||||
colors_green, successes, colors_reset, ' successes / ')
|
||||
if failures > 0 then
|
||||
io_write(colors.red, failures, colors_reset, ' failures / ')
|
||||
end
|
||||
io_write(colors.bright, string.format('%.6f', lester.seconds() - start), colors_reset, ' seconds\n')
|
||||
end
|
||||
end
|
||||
|
||||
-- Error handler used to get traceback for errors.
|
||||
local function xpcall_error_handler(err)
|
||||
return debug.traceback(tostring(err), 2)
|
||||
end
|
||||
|
||||
-- Pretty print the line on the test file where an error happened.
|
||||
local function show_error_line(err)
|
||||
local info = debug.getinfo(3)
|
||||
local io_write = io.write
|
||||
local colors_reset = colors.reset
|
||||
local short_src, currentline = info.short_src, info.currentline
|
||||
io_write(' (', colors.blue, short_src, colors_reset, ':', colors.bright, currentline, colors_reset)
|
||||
if err and lester.show_traceback then
|
||||
local fnsrc = short_src .. ':' .. currentline
|
||||
for cap1, cap2 in err:gmatch('\t[^\n:]+:(%d+): in function <([^>]+)>\n') do
|
||||
if cap2 == fnsrc then
|
||||
io_write('/', colors.bright, cap1, colors_reset)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
io_write(')')
|
||||
end
|
||||
|
||||
-- Pretty print the test name, with breadcrumb for the describe blocks.
|
||||
local function show_test_name(name)
|
||||
local io_write = io.write
|
||||
local colors_reset = colors.reset
|
||||
for _, descname in ipairs(names) do
|
||||
io_write(colors.magenta, descname, colors_reset, ' | ')
|
||||
end
|
||||
io_write(colors.bright, name, colors_reset)
|
||||
end
|
||||
|
||||
--- Declare a test, which consists of a set of assertions.
|
||||
-- @param name A name for the test.
|
||||
-- @param func The function containing all assertions.
|
||||
function lester.it(name, func)
|
||||
-- Skip the test if it does not match the filter.
|
||||
if lester.filter then
|
||||
local fullname = table.concat(names, ' | ') .. ' | ' .. name
|
||||
if not fullname:match(lester.filter) then
|
||||
return
|
||||
end
|
||||
end
|
||||
-- Execute before handlers.
|
||||
for _, levelbefores in ipairs(befores) do
|
||||
for _, beforefn in ipairs(levelbefores) do
|
||||
beforefn(name)
|
||||
end
|
||||
end
|
||||
-- Run the test, capturing errors if any.
|
||||
local success, err
|
||||
if lester.show_traceback then
|
||||
success, err = xpcall(func, xpcall_error_handler)
|
||||
else
|
||||
success, err = pcall(func)
|
||||
if not success and err then
|
||||
err = tostring(err)
|
||||
end
|
||||
end
|
||||
-- Count successes and failures.
|
||||
if success then
|
||||
successes = successes + 1
|
||||
total_successes = total_successes + 1
|
||||
else
|
||||
failures = failures + 1
|
||||
total_failures = total_failures + 1
|
||||
end
|
||||
local io_write = io.write
|
||||
local colors_reset = colors.reset
|
||||
-- Print the test run.
|
||||
if not lester.quiet then -- Show test status and complete test name.
|
||||
if success then
|
||||
io_write(colors.green, '[PASS] ', colors_reset)
|
||||
else
|
||||
io_write(colors.red, '[FAIL] ', colors_reset)
|
||||
end
|
||||
show_test_name(name)
|
||||
if not success then
|
||||
show_error_line(err)
|
||||
end
|
||||
io_write('\n')
|
||||
else
|
||||
if success then -- Show just a character hinting that the test succeeded.
|
||||
local o = (lester.utf8term and lester.colored) and string.char(226, 151, 143) or 'o'
|
||||
io_write(colors.green, o, colors_reset)
|
||||
else -- Show complete test name on failure.
|
||||
io_write(last_succeeded and '\n' or '', colors.red, '[FAIL] ', colors_reset)
|
||||
show_test_name(name)
|
||||
show_error_line(err)
|
||||
io_write('\n')
|
||||
end
|
||||
end
|
||||
-- Print error message, colorizing its output if possible.
|
||||
if err and lester.show_error then
|
||||
if lester.colored then
|
||||
local errfile, errline, errmsg, rest = err:match('^([^:\n]+):(%d+): ([^\n]+)(.*)')
|
||||
if errfile and errline and errmsg and rest then
|
||||
io_write(colors.blue, errfile, colors_reset, ':', colors.bright, errline, colors_reset, ': ')
|
||||
if errmsg:match('^%w([^:]*)$') then
|
||||
io_write(colors.red, errmsg, colors_reset)
|
||||
else
|
||||
io_write(errmsg)
|
||||
end
|
||||
err = rest
|
||||
end
|
||||
end
|
||||
io_write(err, '\n\n')
|
||||
end
|
||||
io.flush()
|
||||
-- Stop on failure.
|
||||
if not success and lester.stop_on_fail then
|
||||
if lester.quiet then
|
||||
io_write('\n')
|
||||
io.flush()
|
||||
end
|
||||
lester.exit()
|
||||
end
|
||||
-- Execute after handlers.
|
||||
for _, levelafters in ipairs(afters) do
|
||||
for _, afterfn in ipairs(levelafters) do
|
||||
afterfn(name)
|
||||
end
|
||||
end
|
||||
last_succeeded = success
|
||||
end
|
||||
|
||||
--- Set a function that is called before every test inside a describe block.
|
||||
-- A single string containing the name of the test about to be run will be passed to `func`.
|
||||
function lester.before(func)
|
||||
local levelbefores = befores[level]
|
||||
if not levelbefores then
|
||||
levelbefores = {}
|
||||
befores[level] = levelbefores
|
||||
end
|
||||
levelbefores[#levelbefores + 1] = func
|
||||
end
|
||||
|
||||
--- Set a function that is called after every test inside a describe block.
|
||||
-- A single string containing the name of the test that was finished will be passed to `func`.
|
||||
-- The function is executed independently if the test passed or failed.
|
||||
function lester.after(func)
|
||||
local levelafters = afters[level]
|
||||
if not levelafters then
|
||||
levelafters = {}
|
||||
afters[level] = levelafters
|
||||
end
|
||||
levelafters[#levelafters + 1] = func
|
||||
end
|
||||
|
||||
--- Pretty print statistics of all test runs.
|
||||
-- With total success, total failures and run time in seconds.
|
||||
function lester.report()
|
||||
local now = lester.seconds()
|
||||
local colors_reset = colors.reset
|
||||
io.write(lester.quiet and '\n' or '', colors.green, total_successes, colors_reset, ' successes / ', colors.red,
|
||||
total_failures, colors_reset, ' failures / ', colors.bright, string.format('%.6f', now - (lester_start or now)),
|
||||
colors_reset, ' seconds\n')
|
||||
io.flush()
|
||||
return total_failures == 0
|
||||
end
|
||||
|
||||
--- Exit the application with success code if all tests passed, or failure code otherwise.
|
||||
function lester.exit()
|
||||
os.exit(total_failures == 0)
|
||||
end
|
||||
|
||||
local expect = {}
|
||||
--- Expect module, containing utility function for doing assertions inside a test.
|
||||
lester.expect = expect
|
||||
|
||||
--- Check if a function fails with an error.
|
||||
-- If `expected` is nil then any error is accepted.
|
||||
-- If `expected` is a string then we check if the error contains that string.
|
||||
-- If `expected` is anything else then we check if both are equal.
|
||||
function expect.fail(func, expected)
|
||||
local ok, err = pcall(func)
|
||||
if ok then
|
||||
error('expected function to fail', 2)
|
||||
elseif expected ~= nil then
|
||||
local found = expected == err
|
||||
if not found and type(expected) == 'string' then
|
||||
found = string.find(tostring(err), expected, 1, true)
|
||||
end
|
||||
if not found then
|
||||
error('expected function to fail\nexpected:\n' .. tostring(expected) .. '\ngot:\n' .. tostring(err), 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if a function does not fail with a error.
|
||||
function expect.not_fail(func)
|
||||
local ok, err = pcall(func)
|
||||
if not ok then
|
||||
error('expected function to not fail\ngot error:\n' .. tostring(err), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if a value is not `nil`.
|
||||
function expect.exist(v)
|
||||
if v == nil then
|
||||
error('expected value to exist\ngot:\n' .. tostring(v), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if a value is `nil`.
|
||||
function expect.not_exist(v)
|
||||
if v ~= nil then
|
||||
error('expected value to not exist\ngot:\n' .. tostring(v), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if an expression is evaluates to `true`.
|
||||
function expect.truthy(v)
|
||||
if not v then
|
||||
error('expected expression to be true\ngot:\n' .. tostring(v), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if an expression is evaluates to `false`.
|
||||
function expect.falsy(v)
|
||||
if v then
|
||||
error('expected expression to be false\ngot:\n' .. tostring(v), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Compare if two values are equal, considering nested tables.
|
||||
local function strict_eq(t1, t2)
|
||||
if rawequal(t1, t2) then
|
||||
return true
|
||||
end
|
||||
if type(t1) ~= type(t2) then
|
||||
return false
|
||||
end
|
||||
if type(t1) ~= 'table' then
|
||||
return t1 == t2
|
||||
end
|
||||
if getmetatable(t1) ~= getmetatable(t2) then
|
||||
return false
|
||||
end
|
||||
for k, v1 in pairs(t1) do
|
||||
if not strict_eq(v1, t2[k]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for k, v2 in pairs(t2) do
|
||||
if not strict_eq(v2, t1[k]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Check if two values are equal.
|
||||
function expect.equal(v1, v2)
|
||||
if not strict_eq(v1, v2) then
|
||||
error('expected values to be equal\nfirst value:\n' .. tostring(v1) .. '\nsecond value:\n' .. tostring(v2), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if two values are not equal.
|
||||
function expect.not_equal(v1, v2)
|
||||
if strict_eq(v1, v2) then
|
||||
error('expected values to be not equal\nfirst value:\n' .. tostring(v1) .. '\nsecond value:\n' .. tostring(v2),
|
||||
2)
|
||||
end
|
||||
end
|
||||
|
||||
return lester
|
||||
|
||||
--[[
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Eduardo Bart (https://github.com/edubart)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
Loading…
Reference in New Issue