local C=require(script.Parent.TestEnum)local K=require(script.Parent.TestPlanBuilder)local y={}local function E(K,y,E)local t=getfenv(y.method)for C,K in pairs(E)do t[C]=K end local m=#y.path for E=m,1,-1 do local t=y.path[E]K:pushNode(t,C.NodeType.Describe)end local S,n=xpcall(y.method,function(C)return C..("\n"..debug.traceback())end)if not S then local C=K:getCurrentNode()C.loadError=n end for C=1,m,1 do K:popNode()end end function y.createEnvironment(K)local y={}function y.describeFOCUS(K,E)return y.describe(K,E,C.NodeModifier.Focus)end function y.describeSKIP(K,E)return y.describe(K,E,C.NodeModifier.Skip)end function y.describe(y,E,t)local m=K:pushNode(y,C.NodeType.Describe,t)local S,n=pcall(E)if not S then m.loadError=n end K:popNode()end function y.try(y,E)local t=K:pushNode(y,C.NodeType.Try)local m,S=pcall(E)if not m then t.loadError=S end K:popNode()end function y.it(y,E)local t=K:pushNode(y,C.NodeType.It)t.callback=E K:popNode()end function y.itFOCUS(y,E)local t=K:pushNode(y,C.NodeType.It,C.NodeModifier.Focus)t.callback=E K:popNode()end function y.itSKIP(y,E)local t=K:pushNode(y,C.NodeType.It,C.NodeModifier.Skip)t.callback=E K:popNode()end function y.itFIXME(y,E)local t=K:pushNode(y,C.NodeType.It,C.NodeModifier.Skip)warn("\070\073\088\077\069\058 \098\114\111\107\101\110 \116\101\115\116",t:getFullName())t.callback=E K:popNode()end function y.FIXME(y)local E=K:getCurrentNode()warn("\070\073\088\077\069\058 \098\114\111\107\101\110 \116\101\115\116",E:getFullName(),y or"")E.modifier=C.NodeModifier.Skip end function y.FOCUS()local y=K:getCurrentNode()y.modifier=C.NodeModifier.Focus end function y.SKIP()local y=K:getCurrentNode()y.modifier=C.NodeModifier.Skip end function y.HACK_NO_XPCALL()local C=K:getCurrentNode()C.HACK_NO_XPCALL=true end y.step=y.it y.fit=y.itFOCUS y.xit=y.itSKIP y.fdescribe=y.describeFOCUS y.xdescribe=y.describeSKIP function y.include(...)local C={...}local t,m if#C==1 then t=C[1]m={}elseif#C==2 then t=C[2]m={C[1]}end E(K,{path=m,method=t},y)end return y end function y.createPlan(C,t,m)local S=K.new()S.noXpcallByDefault=t S.testNamePattern=m local n=y.createEnvironment(S)for C,K in ipairs(C)do E(S,K,n)end return S:finalize()end return y
--[[
	Turns a series of specification functions into a test plan.

	Uses a TestPlanBuilder to keep track of the state of the tree being built.
]]

local TestEnum = require(script.Parent.TestEnum)
local TestPlanBuilder = require(script.Parent.TestPlanBuilder)

local TestPlanner = {}

local function buildPlan(builder, module, env)
	local currentEnv = getfenv(module.method)

	for key, value in pairs(env) do
		currentEnv[key] = value
	end

	local nodeCount = #module.path

	-- Dive into auto-named nodes for this module
	for i = nodeCount, 1, -1 do
		local name = module.path[i]
		builder:pushNode(name, TestEnum.NodeType.Describe)
	end

	local ok, err = xpcall(module.method, function(err)
		return err .. "\n" .. debug.traceback()
	end)

	-- This is an error outside of any describe/it blocks.
	-- We attach it to the node we generate automatically per-file.
	if not ok then
		local node = builder:getCurrentNode()
		node.loadError = err
	end

	-- Back out of auto-named nodes
	for _ = 1, nodeCount do
		builder:popNode()
	end
end

--[[
	Create a new environment with functions for defining the test plan structure
	using the given TestPlanBuilder.

	These functions illustrate the advantage of the stack-style tree navigation
	as state doesn't need to be passed around between functions or explicitly
	global.
]]
function TestPlanner.createEnvironment(builder)
	local env = {}

	function env.describeFOCUS(phrase, callback)
		return env.describe(phrase, callback, TestEnum.NodeModifier.Focus)
	end

	function env.describeSKIP(phrase, callback)
		return env.describe(phrase, callback, TestEnum.NodeModifier.Skip)
	end

	function env.describe(phrase, callback, nodeModifier)
		local node = builder:pushNode(phrase, TestEnum.NodeType.Describe, nodeModifier)

		local ok, err = pcall(callback)

		-- loadError on a TestPlan node is an automatic failure
		if not ok then
			node.loadError = err
		end

		builder:popNode()
	end

	function env.try(phrase, callback)
		local node = builder:pushNode(phrase, TestEnum.NodeType.Try)

		local ok, err = pcall(callback)

		-- loadError on a TestPlan node is an automatic failure
		if not ok then
			node.loadError = err
		end

		builder:popNode()
	end

	function env.it(phrase, callback)
		local node = builder:pushNode(phrase, TestEnum.NodeType.It)

		node.callback = callback

		builder:popNode()
	end

	function env.itFOCUS(phrase, callback)
		local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus)

		node.callback = callback

		builder:popNode()
	end

	function env.itSKIP(phrase, callback)
		local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)

		node.callback = callback

		builder:popNode()
	end

	function env.itFIXME(phrase, callback)
		local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)

		warn("FIXME: broken test", node:getFullName())
		node.callback = callback

		builder:popNode()
	end

	function env.FIXME(optionalMessage)
		local currentNode = builder:getCurrentNode()
		warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "")

		currentNode.modifier = TestEnum.NodeModifier.Skip
	end

	function env.FOCUS()
		local currentNode = builder:getCurrentNode()

		currentNode.modifier = TestEnum.NodeModifier.Focus
	end

	function env.SKIP()
		local currentNode = builder:getCurrentNode()

		currentNode.modifier = TestEnum.NodeModifier.Skip
	end

	--[[
		These method is intended to disable the use of xpcall when running
		nodes contained in the same node that this function is called in.
		This is because xpcall breaks badly if the method passed yields.

		This function is intended to be hideous and seldom called.

		Once xpcall is able to yield, this function is obsolete.
	]]
	function env.HACK_NO_XPCALL()
		local currentNode = builder:getCurrentNode()

		currentNode.HACK_NO_XPCALL = true
	end

	env.step = env.it

	env.fit = env.itFOCUS
	env.xit = env.itSKIP
	env.fdescribe = env.describeFOCUS
	env.xdescribe = env.describeSKIP

	function env.include(...)
		local args = {...}
		local method, path
		if #args == 1 then
			method = args[1]
			path = {}
		elseif #args == 2 then
			method = args[2]
			path = {args[1]}
		end
		buildPlan(builder, {path = path, method = method}, env)
	end

	return env
end

--[[
	Create a new TestPlan from a list of specification functions.

	These functions should call a combination of `describe` and `it` (and their
	variants), which will be turned into a test plan to be executed.
]]
function TestPlanner.createPlan(specFunctions, noXpcallByDefault, testNamePattern)
	local builder = TestPlanBuilder.new()
	builder.noXpcallByDefault = noXpcallByDefault
	builder.testNamePattern = testNamePattern
	local env = TestPlanner.createEnvironment(builder)

	for _, module in ipairs(specFunctions) do
		buildPlan(builder, module, env)
	end

	return builder:finalize()
end

return TestPlanner