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