mirror of
https://github.com/Reuh/classtoi.git
synced 2025-10-27 12:19:31 +00:00
Added tests, fixed __index, custom constructor return values, 0.1.4
This commit is contained in:
parent
0214c99cc4
commit
cc53045d4d
4 changed files with 426 additions and 11 deletions
|
|
@ -1,3 +1,7 @@
|
|||
0.1.4:
|
||||
- :new non-nil custom returns values now replace the usual returned instance
|
||||
- Custom inherited __index metamethods are now called on the correct class instead of the class it is defined in.
|
||||
- Added tests.
|
||||
0.1.3:
|
||||
- Added __name attribute in BaseClass for a more informative __tostring.
|
||||
- Fixed instanciation error when no custom :new method is defined.
|
||||
|
|
|
|||
31
classtoi.lua
31
classtoi.lua
|
|
@ -1,4 +1,4 @@
|
|||
--- Reuh's class library version 0.1.3. Lua 5.1-5.3 and LuaJit compatible.
|
||||
--- Reuh's class library version 0.1.4. Lua 5.1-5.3 and LuaJit compatible.
|
||||
-- Objects and classes behavior are identical, so you can consider this to be somewhat prototype-based.
|
||||
-- Features:
|
||||
-- * Multiple inheritance with class(parents...) or someclass(newstuff...)
|
||||
|
|
@ -8,11 +8,12 @@
|
|||
-- * Instanciate with class:new(...)
|
||||
-- * Test inheritance relations with class/object.is(thing, isThis)
|
||||
-- * Call object:new(...) on instanciation
|
||||
-- * If object:new(...) returns non-nil values, they will be returned instead of the instance
|
||||
-- * Call class.__inherit(class, inheritingClass) when creating a class inheriting the previous class. If class.__inherit returns a value, it will
|
||||
-- be used as the parent table instead of class, allowing some pretty fancy behavior (it's like an inheritance metamethod).
|
||||
-- * Implements Class Commons
|
||||
-- * I don't like to do this, but you can redefine every field and metamethod after class creation (except __index and __super).
|
||||
-- Not features (things you may want to know):
|
||||
-- Not features / Things you may want to know:
|
||||
-- * Will set the metatable of all parent classes/tables if no metatable is set (the table will be its own metatable).
|
||||
-- * You can't redefine __super (any __super you define will be only avaible by searching in the default __super contents).
|
||||
-- * Redefining __super or __index after class creation will break everything (though it should be ok with new, is, __call and everything else).
|
||||
|
|
@ -20,6 +21,8 @@
|
|||
-- will return the default method and not the one you've defined. However, theses defaults will be replaced by yours automatically on instanciation,
|
||||
-- except __super and __index, but __index should call your __index and act like you expect. __super will however always be the default one
|
||||
-- and doesn't proxy in any way yours.
|
||||
-- * __index metamethods will be called with an extra third argument, which is the current class being searched in the inheritance tree.
|
||||
-- You can safely ignore it.
|
||||
--
|
||||
-- Please also note that the last universal ancestor of the classes (defined here in BaseClass) sets the default __tostring method
|
||||
-- and __name attribute for nice class-name-printing. Unlike the previoulsy described attributes and methods however, it is done in a normal
|
||||
|
|
@ -59,17 +62,22 @@ methods = {
|
|||
-- A new object will only be created if calling the method "class:new(...)", if you call for example "class.new(someTable, ...)", it
|
||||
-- will only execute the constructor defined in the class on someTable. This can be used to execute the parent constructor in a child
|
||||
-- object, for example.
|
||||
-- It should also be noted that if the new method returns non-nil value(s), they will be returned instead of the object.
|
||||
["!new"] = function(self, ...)
|
||||
if lastIndexed == self then
|
||||
local obj = self()
|
||||
local obj, ret = self(), nil
|
||||
-- Setting class methods to the ones found in parents (we use rawset in order to avoid calling the __newindex metamethod)
|
||||
different = methods["!new"] rawset(obj, "new", obj:__index("new") or nil)
|
||||
different = methods["!is"] rawset(obj, "is", obj:__index("is") or nil)
|
||||
different = methods.__call rawset(obj, "__call", obj:__index("__call") or nil)
|
||||
different = nil
|
||||
-- Call constructor
|
||||
if obj.new ~= methods["!new"] and type(obj.new) == "function" then obj:new(...) end
|
||||
return obj
|
||||
if obj.new ~= methods["!new"] and type(obj.new) == "function" then ret = { obj:new(...) } end
|
||||
if not ret or #ret == 0 then
|
||||
return obj
|
||||
else
|
||||
return unpack(ret)
|
||||
end
|
||||
else
|
||||
different = methods["!new"]
|
||||
local new = lastIndexed:__index("new") or nil
|
||||
|
|
@ -104,18 +112,19 @@ methods = {
|
|||
-- When getting a value from the class, it will be first searched in stuff, then in Base1, then in all Base1 parents,
|
||||
-- then in Base2, then in Base2 parents.
|
||||
-- A way to describe this will be search in the latest added tables (from the farthest child to the first parents), from left-to-right.
|
||||
__index = function(self, k)
|
||||
-- self always refer to the initial table the metamethod was called on, super refers to the class currently being searched for a value.
|
||||
__index = function(self, k, super)
|
||||
local proxied = methods["!"..tostring(k)]
|
||||
if proxied ~= nil and proxied ~= different then -- proxied methods
|
||||
lastIndexed = self
|
||||
return proxied
|
||||
end
|
||||
for _, t in ipairs(self.__super) do -- search in super (will auto-follow __index metamethods)
|
||||
local val = t[k]
|
||||
for _, t in ipairs((super or self).__super) do -- search in super (will follow __index metamethods)
|
||||
local val = rawget(t, k)
|
||||
if val ~= nil and val ~= different then return val end
|
||||
-- If different search is on and the direct t[k] returns an identical value, force the __index metamethod search.
|
||||
if different ~= nil and getmetatable(t) and getmetatable(t).__index then
|
||||
val = getmetatable(t):__index(k)
|
||||
-- Also covers the case when different search is enabled and the raw t[k] returns an identical value, so the __index metamethod search will be tried for another value.
|
||||
if getmetatable(t) and getmetatable(t).__index then
|
||||
val = getmetatable(t).__index(self, k, t)
|
||||
if val ~= nil and val ~= different then return val end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
156
knife-test.lua
Normal file
156
knife-test.lua
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
--[[
|
||||
knife.test - A fixture-free test framework.
|
||||
|
||||
https://github.com/airstruck/knife
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 airstruck
|
||||
|
||||
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.
|
||||
]]
|
||||
|
||||
local test, testAssert, testError
|
||||
|
||||
-- Create a node representing a test section
|
||||
local function createNode (parent, description, process)
|
||||
return setmetatable({
|
||||
parent = parent,
|
||||
description = description,
|
||||
process = process,
|
||||
nodes = {},
|
||||
activeNodeIndex = 1,
|
||||
currentNodeIndex = 0,
|
||||
assert = testAssert,
|
||||
error = testError,
|
||||
}, { __call = test })
|
||||
end
|
||||
|
||||
-- Run a node
|
||||
local function runNode (node)
|
||||
node.currentNodeIndex = 0
|
||||
return node:process()
|
||||
end
|
||||
|
||||
-- Get the root node for a given node
|
||||
local function getRootNode (node)
|
||||
local parent = node.parent
|
||||
return parent and getRootNode(parent) or node
|
||||
end
|
||||
|
||||
-- Update the active child node of the given node
|
||||
local function updateActiveNode (node, description, process)
|
||||
local activeNodeIndex = node.activeNodeIndex
|
||||
local nodes = node.nodes
|
||||
local activeNode = nodes[activeNodeIndex]
|
||||
|
||||
if not activeNode then
|
||||
activeNode = createNode(node, description, process)
|
||||
nodes[activeNodeIndex] = activeNode
|
||||
else
|
||||
activeNode.process = process
|
||||
end
|
||||
|
||||
getRootNode(node).lastActiveLeaf = activeNode
|
||||
|
||||
return activeNode
|
||||
end
|
||||
|
||||
-- Run the active child node of the given node
|
||||
local function runActiveNode (node, description, process)
|
||||
local activeNode = updateActiveNode(node, description, process)
|
||||
return runNode(activeNode)
|
||||
end
|
||||
|
||||
-- Get ancestors of a node, including the node
|
||||
local function getAncestors (node)
|
||||
local ancestors = { node }
|
||||
for ancestor in function () return node.parent end do
|
||||
ancestors[#ancestors + 1] = ancestor
|
||||
node = ancestor
|
||||
end
|
||||
return ancestors
|
||||
end
|
||||
|
||||
-- Print a message describing one execution path in the test scenario
|
||||
local function printScenario (node)
|
||||
local ancestors = getAncestors(node)
|
||||
for i = #ancestors, 1, -1 do
|
||||
io.stderr:write(ancestors[i].description or '')
|
||||
io.stderr:write('\n')
|
||||
end
|
||||
end
|
||||
|
||||
-- Print a message and stop the test scenario when an assertion fails
|
||||
local function failAssert (node, description, message)
|
||||
io.stderr:write(message or '')
|
||||
io.stderr:write('\n\n')
|
||||
printScenario(node)
|
||||
io.stderr:write(description or '')
|
||||
io.stderr:write('\n\n')
|
||||
error(message or '', 2)
|
||||
end
|
||||
|
||||
-- Create a branch node for a test scenario
|
||||
test = function (node, description, process)
|
||||
node.currentNodeIndex = node.currentNodeIndex + 1
|
||||
if node.currentNodeIndex == node.activeNodeIndex then
|
||||
return runActiveNode(node, description, process)
|
||||
end
|
||||
end
|
||||
|
||||
-- Test an assertion
|
||||
testAssert = function (self, value, description)
|
||||
if not value then
|
||||
return failAssert(self, description, 'Test failed: assertion failed')
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
-- Expect function f to fail
|
||||
testError = function (self, f, description)
|
||||
if pcall(f) then
|
||||
return failAssert(self, description, 'Test failed: expected error')
|
||||
end
|
||||
end
|
||||
|
||||
-- Create the root node for a test scenario
|
||||
local function T (description, process)
|
||||
local root = createNode(nil, description, process)
|
||||
|
||||
runNode(root)
|
||||
while root.activeNodeIndex <= #root.nodes do
|
||||
local lastActiveBranch = root.lastActiveLeaf.parent
|
||||
lastActiveBranch.activeNodeIndex = lastActiveBranch.activeNodeIndex + 1
|
||||
runNode(root)
|
||||
end
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
-- Run any other files passed from CLI.
|
||||
if arg and arg[0] and arg[0]:gmatch('test.lua') then
|
||||
_G.T = T
|
||||
for i = 1, #arg do
|
||||
dofile(arg[i])
|
||||
end
|
||||
_G.T = nil
|
||||
end
|
||||
|
||||
return T
|
||||
246
test.lua
Normal file
246
test.lua
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
local T = require("knife-test")
|
||||
|
||||
-- luacheck: ignore T
|
||||
T("Given the base class", function(T)
|
||||
local class = require("classtoi")
|
||||
|
||||
-- Inheritance
|
||||
T("When subclassed with an attribute", function(T)
|
||||
local Thing = class {
|
||||
attribute = "thing"
|
||||
}
|
||||
|
||||
T:assert(Thing.attribute == "thing", "Then the attribute should be set on the subclass")
|
||||
T:assert(class.attribute == nil, "Then the attribute shouldn't be set on the base class'")
|
||||
|
||||
T:assert(class.is(Thing), "Then the subclass should be a subclass of the base class (class.is)")
|
||||
T:assert(Thing:is(class), "Then the subclass should be a subclass of the base class (subclass:is)")
|
||||
|
||||
T("When the subclass is instanced", function(T)
|
||||
local thing = Thing:new()
|
||||
|
||||
T:assert(thing.attribute == "thing", "Then the attribute should be kept")
|
||||
|
||||
T:assert(class.is(thing), "Then the object should be a subclass of the base class (class.is(object))")
|
||||
T:assert(thing:is(class), "Then the object should be a subclass of the base class (object:is(class))")
|
||||
T:assert(thing:is(Thing), "Then the object should be a subclass of the subclass (object:is(subclass))")
|
||||
T:assert(Thing.is(thing), "Then the object should be a subclass of the subclass (subclass.is(object))")
|
||||
|
||||
T("When setting the attribute on the instance", function(T)
|
||||
thing.attribute = "not the same thing"
|
||||
|
||||
T:assert(thing.attribute == "not the same thing", "Then the attribute should be set for the object")
|
||||
T:assert(Thing.attribute == "thing", "Then the attribute should be kept for the subclass")
|
||||
end)
|
||||
end)
|
||||
|
||||
T("When the subclassed is subclassed with two parents", function(T)
|
||||
local OtherThing = class {
|
||||
attribute = "other thing",
|
||||
other = true
|
||||
}
|
||||
|
||||
local SubThing = Thing(OtherThing)
|
||||
|
||||
T:assert(SubThing.attribute == "other thing", "Then the last added parent should have priority")
|
||||
|
||||
local SubOtherThing = class(Thing, OtherThing)
|
||||
|
||||
T:assert(SubOtherThing.attribute == "thing", "Then the left-most class should have priority")
|
||||
|
||||
T:assert(SubThing.other and SubOtherThing.other, "Then new attribute should be always inherited")
|
||||
|
||||
T("When adding a method to the first subclass", function(T)
|
||||
Thing.action = function(self, arg)
|
||||
self.attribute = arg
|
||||
end
|
||||
|
||||
T("When calling it on the first subclass", function(T)
|
||||
Thing:action("new thing")
|
||||
|
||||
T:assert(Thing.attribute == "new thing", "Then it affect the first subclass")
|
||||
T:assert(SubOtherThing.attribute == "new thing", "Then it affect children wich inherit the modified attribute")
|
||||
T:assert(SubThing.attribute == "other thing", "Then it doesn't affect children which doesn't inherit the modified attribute")
|
||||
end)
|
||||
|
||||
T("When calling it on another subclass", function(T)
|
||||
SubOtherThing:action("new thing")
|
||||
|
||||
T:assert(Thing.attribute == "thing", "Then it doesn't affect the parent subclass")
|
||||
T:assert(SubOtherThing.attribute == "new thing", "Then it affect the subclass")
|
||||
T:assert(SubThing.attribute == "other thing", "Then it doesn't affect other subclasses")
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Constructor
|
||||
T("When subclassed with a constructor", function(T)
|
||||
local Thing = class {
|
||||
attribute = "class",
|
||||
|
||||
new = function(self, arg)
|
||||
self.attribute = arg or "object"
|
||||
end
|
||||
}
|
||||
|
||||
T:assert(Thing.attribute == "class", "Then the class should not call the constructor itself")
|
||||
|
||||
T("When the subclass is instanced without arguments", function(T)
|
||||
local thing = Thing:new()
|
||||
|
||||
T:assert(thing.attribute == "object", "Then the constructor should have been called on the object without arguments")
|
||||
T:assert(Thing.attribute == "class", "Then the constructor should not be called on the class")
|
||||
end)
|
||||
|
||||
T("When the subclass is instanced without arguments", function(T)
|
||||
local thing = Thing:new("stuff")
|
||||
|
||||
T:assert(thing.attribute == "stuff", "Then the constructor should have been called on the object with arguments")
|
||||
T:assert(Thing.attribute == "class", "Then the constructor should not be called on the class")
|
||||
end)
|
||||
|
||||
T("When the subclass is subclassed and instanced with another constructor", function(T)
|
||||
local SubThing = Thing {
|
||||
new = function(self)
|
||||
self.sub = true
|
||||
Thing.new(self, "a whole new thing")
|
||||
end
|
||||
}
|
||||
|
||||
local subthing = SubThing:new()
|
||||
|
||||
T:assert(subthing.sub, "Then the new constructor is called on the new instance")
|
||||
T:assert(not SubThing.sub, "Then the new constructor is not called on the class")
|
||||
|
||||
T:assert(subthing.attribute == "a whole new thing", "Then the parent new method was correctly accessed and called on the new instance")
|
||||
T:assert(SubThing.attribute == "class", "Then the parent new method was not called on the class")
|
||||
end)
|
||||
end)
|
||||
T("When subclassed with a constructor returning non-nil values and instanced", function(T)
|
||||
local Thing = class {
|
||||
new = function(self, arg)
|
||||
return true, arg
|
||||
end
|
||||
}
|
||||
|
||||
local thing, other = Thing:new("mostly useless but cool")
|
||||
|
||||
T:assert(thing == true and other == "mostly useless but cool", "Then the constructor return value should be returned instead of an object")
|
||||
end)
|
||||
|
||||
-- Usual metamethods
|
||||
T("When subclassed with a metamethod", function(T)
|
||||
local Thing = class {
|
||||
value = 0,
|
||||
__add = function(self, other)
|
||||
self.value = self.value + other
|
||||
return self
|
||||
end
|
||||
}
|
||||
|
||||
T:assert(Thing.value == 0, "Then the attribute is set on the subclass")
|
||||
|
||||
Thing = Thing + 5
|
||||
|
||||
T:assert(Thing.value == 5, "Then the metamethod is correctly called on the subclass")
|
||||
T:assert(class.value == nil, "Then the metamethod doesn't affect the base class")
|
||||
|
||||
T("When the subclass is instancied", function(T)
|
||||
local thing = Thing:new()
|
||||
thing = thing + 5
|
||||
|
||||
T:assert(thing.value == 10, "Then the metamethod is correctly called on the instance")
|
||||
T:assert(Thing.value == 5, "Then the metamethod doesn't affect the parent class")
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Redefining usual special class methods
|
||||
T("When a subclass redefine one of the default class methods", function(T)
|
||||
local Thing = class {
|
||||
is = function(self)
|
||||
return "mashed potatoes"
|
||||
end,
|
||||
|
||||
__call = function(self, arg)
|
||||
return "hot potatoes with "..arg
|
||||
end
|
||||
}
|
||||
|
||||
T:assert(Thing:is(class) == true, "Then defaults methods are still used on the class")
|
||||
T:assert(Thing():is(class) == true, "Then defaults metamethods are still used on the class")
|
||||
|
||||
T("When the subclass is instancied", function(T)
|
||||
local thing = Thing:new()
|
||||
|
||||
T:assert(thing:is(class) == "mashed potatoes", "Then the redefined method is used on the instance")
|
||||
T:assert(thing("melted raclette") == "hot potatoes with melted raclette", "Then the redefined metamethod is used on the instance")
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Redefining __index
|
||||
T("When subclassed with an __index metamethod", function(T)
|
||||
local Thing = class {
|
||||
value = "other",
|
||||
|
||||
__index = function(self, key)
|
||||
if key == "attribute" then
|
||||
return "thing"
|
||||
elseif key == "another" then
|
||||
return self.value
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
T:assert(Thing.attribute == "thing", "Then the metamethod is called as expected")
|
||||
T:assert(Thing.value == "other", "Then attribute access still works as expected")
|
||||
T:assert(Thing.another == "other", "Then the metamethod seems to correctly pass a class as the first argument")
|
||||
|
||||
T("When the subclass is instanced", function(T)
|
||||
local thing = Thing:new()
|
||||
|
||||
T:assert(thing.attribute == "thing", "Then the metamethod is called as expected")
|
||||
T:assert(thing.value == "other", "Then attribute access still works as expected")
|
||||
T:assert(thing.another == "other", "Then the metamethod seems to correctly pass a class as the first argument")
|
||||
|
||||
T("When changing a value returned by the metamethod on the instance", function(T)
|
||||
thing.value = "new thing"
|
||||
|
||||
T:assert(thing.another == "new thing", "Then the metamethod was correctly called with the instance as the first argument")
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Defining __inherit
|
||||
T("When subclassed with an __inherit method", function(T)
|
||||
local Thing = class {
|
||||
attribute = "thing",
|
||||
replace = false,
|
||||
__inherit = function(self, inheritingClass)
|
||||
inheritingClass.works = true
|
||||
if self.replace then
|
||||
return { attribute = "other thing" }
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
T:assert(Thing.works == true, "Then the __inherit method was correctly called on the class creation")
|
||||
|
||||
T("When subclassing the subclass", function(T)
|
||||
Thing.works = "ok"
|
||||
local SubThing = Thing()
|
||||
|
||||
T:assert(SubThing.attribute == "thing", "Then the child class still inherit the subclass")
|
||||
T:assert(SubThing.works == true, "Then the __inherit method was correctly called on the inheriting class")
|
||||
T:assert(Thing.works == "ok", "Then the __inherit method was correctly called on the inherited class")
|
||||
end)
|
||||
|
||||
T("When subclassing the subclass with a return value", function(T)
|
||||
Thing.replace = true
|
||||
local SubThing = Thing()
|
||||
|
||||
T:assert(SubThing.attribute == "other thing", "Then the child class inherited the return value")
|
||||
T:assert(SubThing.works == true, "Then the __inherit method was correctly called on the inheriting class")
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue