Download
377 lines 10.8 KiBLuaModified 2018-06-05 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
local bright = string.char(27).."[1m"
local dim = string.char(27).."[2m"
local underscore = string.char(27).."[4m"
local red = string.char(27).."[31m"
local green = string.char(27).."[32m"
local reset = string.char(27).."[0m"
print(bright..underscore.."\nTesting with ".._VERSION..":"..reset)
repr = function(t)
    local buff = tostring(t)
    if type(t) == 'table' then
        buff = "{"..buff.." "
        for k, v in pairs(t) do
            buff = buff..("%s = %s, "):format(k,v)
        end
        buff = buff.."}"
    end
    return buff
end

local num_errors = 0
local test_number = 0
local function test(description, fn)
    test_number = test_number + 1
    description = (bright.."% 3d. "..reset.."%s"):format(test_number, description)
    io.write(description)
    io.write(red)
    local ok, err = pcall(fn)
    if not ok then
        io.write(reset..dim.."\r.......................................")
        io.write(reset.."["..bright..red.."FAILED"..reset.."]\r")
        io.write("\r"..description.."\n"..reset)
        print(reset..red..(err or "")..reset)
        num_errors = num_errors + 1
        --os.exit(true)
    else
        io.write(reset..dim.."\r.......................................")
        io.write(reset.."["..green.."PASSED"..reset.."]\r")
        io.write(description.."\n")
    end
end

test("Loading module", function()
    package.cpath = "./immutable.so;"..package.cpath
    immutable = require"immutable"
end)

test("Creating class", function()
    Vec = immutable({"x","y"}, {
        name="Vec",
        len2=function(self)
            return self.x*self.x + self.y*self.y
        end,
        __add=function(self, other)
            local cls = getmetatable(self)
            return cls(self.x+other.x, self.y+other.y)
        end,
    })
end)

test("Instantiating class", function()
    v = assert(Vec(1,3))
end)

test("Testing indexing", function()
    assert(v.x == 1)
end)

test("Testing # operator", function()
    assert(#v == 0)
    local T = immutable()
    assert(#T(1,2,3) == 3)
end)

test("Testing # operator for mixed table", function()
    assert(#Vec(1,2,3,4,5) == 3)
end)

test("Testing method", function()
    assert(v:len2() == 10)
end)

test("Testing tostring", function()
    assert(tostring(v) == "Vec(1, 3)")
end)

test("Testing from_table", function()
    assert(Vec:from_table({x=1,y=2}) == Vec(1,2), "assertion 1 failed!")
    assert(Vec:from_table({3, x=1,y=2}) == Vec(1,2,3), "assertion 2 failed!")
    assert(Vec:from_table(setmetatable({y=3}, {__index={x=1}})) == v, "assertion 3 failed!")
    assert(Vec:from_table({x=1}) == Vec(1, nil), "assertion 4 failed!")
end)

test("Testing from_table for tuples", function()
    local T = immutable(nil)
    assert(T(1,2) == T:from_table({1,2,n=2}))
    assert(T(1,2) == T:from_table({1,2}))
end)

test("Testing equality", function()
    also_v = Vec(1,3)
    assert(v == also_v)
    assert(tostring(v) == tostring(also_v))
    -- Hash collision in the current implementation
    not_v = Vec(true,3)
    assert(v ~= not_v)
    assert(tostring(not_v) == "Vec(true, 3)")
    also_not_v = Vec(true,3)
    assert(not_v == also_not_v)
end)

test("Testing singletons", function()
    local T1 = immutable()
    local T2 = immutable()
    assert(T1() == T1())
    assert(T1() ~= T2())
end)

test("Testing __add metamethod", function()
    assert(v + Vec(5,6) == Vec(6,9))
end)
collectgarbage()
test("Testing string members", function()
    Vec("hello", "world")
end)
test("Testing table members", function()
    Vec({}, {})
end)
test("Testing function members", function()
    Vec(function() end, function() end)
end)
test("Testing immutable members", function()
    Vec(v, v)
end)
collectgarbage()
collectgarbage()

test("Testing garbage collection", function()
    local Foo = immutable({"x"})
    local function countFoos()
        collectgarbage()
        collectgarbage()
        local n = 0
        local buckets = 0
        for h,bucket in pairs(Foo.__instances) do
            buckets = buckets + 1
            for _ in pairs(bucket) do
                n = n + 1
            end
        end
        collectgarbage()
        collectgarbage()
        return n, buckets
    end
    local f1, f2, also_f2 = Foo(1), Foo(2), Foo(2)
    local foos, buckets = countFoos()
    assert(foos == 2, "WTF? "..tostring(foos))
    f1, f2 = nil, nil
    foos, buckets = countFoos()
    assert(foos == 1)
    also_f2 = nil
    foos, buckets = countFoos()
    assert(foos == 0, "Leaking instances")
    assert(buckets == 0, "Leaking hash buckets")
    assert(next(Foo.__instances) == nil)
end)

test("Testing __index", function()
    local Baz = immutable({"x","y"}, {z=99})
    local b = Baz(1,nil)
    assert(b.x == 1 and b.y == nil and b.z == 99 and b.asdf == nil)
    local Foo = immutable({"x","y"}, {z=99, __index=function() return "foo" end})
    local f = Foo(1,nil)
    assert(f.x == "foo" and f.y == "foo" and f.z == "foo" and f.asdf == "foo")
end)

test("Testing __rawindex", function()
    local Foo = immutable({"x","y"}, {
        classvar = 99,
        __index=function(self,k)
            local cls = getmetatable(self)
            local inst_val, found = cls.__super.__index(self, k)
            if found then return inst_val end
            return cls[k] or tostring(k)..tostring(cls.__super.__index(self, 'x'))
        end,
    })
    local f = Foo(23, nil)
    assert(f.x == 23 and f.y == nil and f.classvar == 99 and f.asdf == "asdf23")
end)

test("Testing similar class", function()
    FooVec = immutable({"x","y"}, {
        len2=function(self)
            return self.x*self.x + self.y*self.y
        end,
        __add=function(self, other)
            local cls = getmetatable(self)
            return cls(self.x+other.x, self.y+other.y)
        end,
        __tostring=function(self)
            return "Vec("..tostring(self.x)..", "..tostring(self.y)..")"
        end,
    })
    assert(FooVec(1,1) ~= Vec(1,1))
end)

test("Testing is_instance", function()
    fv = FooVec(1,2)
    assert(FooVec:is_instance(fv))
    assert(not FooVec:is_instance(v))
    assert(not FooVec:is_instance("asdf"))
    assert(not FooVec:is_instance({}))
    assert(not FooVec:is_instance(5))
end)

test("Testing spoofing", function()
    local t = {99,100}
    setmetatable(t, Vec)
    assert(not pcall(function() return t.x end))
    assert(not pcall(function() return Vec.__index(88, 9) end))
    assert(not pcall(function() return Vec.__tostring(t) end))
end)

if _VERSION == "Lua 5.3" then
    test("Testing unpacking", function()
        local Tuple = immutable()
        if table.unpack then
            local a, b = table.unpack(Tuple(5,6))
            assert(a == 5 and b == 6)
        end
    end)
end

test("Testing immutable(nil)", function()
    local Tup3 = immutable(nil, {name="Tuple"})
    assert(tostring(Tup3(1,2,3)) == "Tuple(1, 2, 3)")
end)

test("Testing tostring(class)", function()
    local C1 = immutable(nil, {name="MYNAME"})
    assert(tostring(C1) == "MYNAME")
    local C2 = immutable()
    assert(tostring(C2):match("immutable type: 0x.*"))
end)

test("Testing tuple tostring", function()
    local tup3 = immutable(nil)
    assert(tostring(tup3(1,2,3)) == "(1, 2, 3)")
    assert(tostring(tup3(1,tup3(2,3,4),5)) == "(1, (2, 3, 4), 5)")
end)

test("Testing giant immutable table", function()
    local keys = {}
    local N = 100000
    for i=1,N do keys[i] = "key_"..tostring(i) end
    local T = immutable(keys)
    local values = {}
    for i,key in ipairs(keys) do values[key] = i*i end
    pcall(function() T:from_table(values) end)
end)

if _VERSION == "Lua 5.2" or _VERSION == "Lua 5.3" then
    test("Testing tuple iteration", function()
        local T = immutable()
        local t = T(1,4,9,nil,16,nil)
        local checks = {1,4,9,nil,16,nil}
        local passed, iterations = 0, 0
        for i,v in ipairs(t) do
            assert(checks[i] == v)
            passed = passed + 1
            iterations = iterations + 1
        end
        assert(passed == 6 and iterations == passed)
        passed, iterations = 0, 0
        for k,v in pairs(t) do
            assert(checks[k] == v)
            passed = passed + 1
            iterations = iterations + 1
        end
        assert(passed == 6 and iterations == passed)
    end)

    test("Testing table iteration", function()
        local Foo = immutable({"x", "y", "z","w"})
        local f = Foo(1,nil,2,nil)
        local checks = {1,nil,2,nil}
        local passed, iterations = 0, 0
        checks = {x=1,y=nil,z=2,w=nil}
        for k,v in pairs(f) do
            assert(checks[k] == v)
            passed = passed + 1
            iterations = iterations + 1
        end
        assert(passed == 4 and iterations == passed)
    end)

    test("Testing mixed iteration", function()
        local T = immutable({"skip"})
        local t = T('a','b','c')
        local checks = {'b','c'}
        local passed, iterations = 0, 0
        for i,v in ipairs(t) do
            assert(checks[i] == v, "checks["..i.."] ~= "..v)
            passed = passed + 1
            iterations = iterations + 1
        end
        assert(passed == 2 and iterations == passed, "ipairs failed")
        checks = {skip='a','b','c'}
        passed = 0
        for k,v in pairs(t) do
            assert(checks[k] == v, "checks["..k.."] ~= "..v)
            passed = passed + 1
        end
        assert(passed == 3, "pairs failed")
    end)
end

test("Testing __new", function()
    local Foo = immutable({"x","y"}, {
        __new=function(cls,x,y)
            return x+1,y+1
        end
    })
    local f = Foo(2,3)
    assert(f.x == 3 and f.y == 4)
end)

test("Testing __new with #args", function()
    local Foo = immutable({"x","y"}, {
        __new=function(cls,x,y,z)
            return x+z,y+z
        end
    })
    local f = Foo(2,3,1)
    assert(f.x == 3 and f.y == 4)
end)

test("Testing __new with varargs", function()
    local Foo = immutable(nil, {
        __new=function(cls,...)
            return "prefix",...
        end
    })
    local f = Foo(1,2)
    assert(f[1] == "prefix" and f[2] == 1 and f[3] == 2 and f[4] == nil)
end)

test("Testing __hash", function()
    local T = immutable()
    local t = T(1,2,3)
    local h = t:__hash()
    assert(type(h) == 'number')
    assert(T.__instances[h][t])
end)

test("Testing __rawindex", function()
    local T = immutable({"x"}, {
        classvar = 99,
        __index = function(self, key)
            local cls = getmetatable(self)
            local value, found = cls.__super.__index(self, key)
            if not found then
                return cls[key]
            else
                return tostring(value)
            end
        end
    })
    assert(T(3).x == "3" and T(nil).x == "nil" and T(1).classvar == 99)
end)

if num_errors == 0 then
    print(green.."All tests passed!"..reset)
else
    print(bright..red.."*** "..tostring(num_errors).." test"..(num_errors > 1 and "s" or "").." failed! ***"..reset)
    io.write(reset)
    error()
end