Module:User:Mr. Stradivarius/gambiarra

--[[
--  MIT LICENSE
--
--  Copyright (c) 2015 Serge Zaitsev
--
--  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 function TERMINAL_HANDLER(e, test, msg)
	if e == 'pass' then
		mw.log("�[32m✔�[0m "..test..': '..msg)
	elseif e == 'fail' then
		mw.log("�[31m✘�[0m "..test..': '..msg)
	elseif e == 'except' then
		mw.log("�[31m✘�[0m "..test..': '..msg)
	end
end

local function deepeq(a, b)
	-- Different types: false
	if type(a) ~= type(b) then return false end
	-- Functions
	if type(a) == 'function' then
		return string.dump(a) == string.dump(b)
	end
	-- Primitives and equal pointers
	if a == b then return true end
	-- Only equal tables could have passed previous tests
	if type(a) ~= 'table' then return false end
	-- Compare tables field by field
	for k,v in pairs(a) do
		if b[k] == nil or not deepeq(v, b[k]) then return false end
	end
	for k,v in pairs(b) do
		if a[k] == nil or not deepeq(v, a[k]) then return false end
	end
	return true
end

-- Compatibility for Lua 5.1 and Lua 5.2
local function args(...)
	return {n=select('#', ...), ...}
end

local function spy(f)
	local s = {}
	setmetatable(s, {__call = function(s, ...)
		s.called = s.called or {}
		local a = args(...)
		table.insert(s.called, {...})
		if f then
			local r
			r = args(pcall(f, (unpack or table.unpack)(a, 1, a.n)))
			if not r[1] then
				s.errors = s.errors or {}
				s.errors[#s.called] = r[2]
			else
				return (unpack or table.unpack)(r, 2, r.n)
			end
		end
	end})
	return s
end


return function(handler, env)

	local pendingtests = {}

	local function runpending()
		if pendingtests[1] ~= nil then pendingtests[1](runpending) end
	end

	local function test(name, f, async)
		local testfn = function(next)

			local prev = {
				ok = env.ok,
				spy = env.spy,
				eq = env.eq
			}

			local restore = function()
				env.ok = prev.ok
				env.spy = prev.spy
				env.eq = prev.eq
				env.gambiarrahandler('end', name)
				table.remove(pendingtests, 1)
				if next then next() end
			end

			local handler = env.gambiarrahandler

			env.eq = deepeq
			env.spy = spy
			env.ok = function(cond, msg)
				if cond then
					handler('pass', name, msg)
				else
					handler('fail', name, msg)
				end
			end

			handler('begin', name);
			local ok, err = pcall(f, restore)
			if not ok then
				handler('except', name, err)
			end

			if not async then
				handler('end', name);
				env.ok = prev.ok;
				env.spy = prev.spy;
				env.eq = prev.eq;
			end
		end

		if not async then
			testfn()
		else
			table.insert(pendingtests, testfn)
			if #pendingtests == 1 then
				runpending()
			end
		end
	end

	env = env or _G
	env.gambiarrahandler = handler or TERMINAL_HANDLER

	env.test = test
end