Initial community commit

This commit is contained in:
Jef 2024-09-24 14:54:57 +02:00
parent 537bcbc862
commit fc06254474
16440 changed files with 4239995 additions and 2 deletions

View file

@ -0,0 +1,7 @@
return {
"self-test.lua",
"test_assertions.lua",
"test_declare.lua",
"test_helpers.lua",
"test_runner.lua"
}

View file

@ -0,0 +1,106 @@
---
-- self-test/self-test.lua
--
-- An automated test framework for Premake and its add-on modules.
--
-- Author Jason Perkins
-- Copyright (c) 2008-2016 Jason Perkins and the Premake project.
---
local p = premake
p.modules.self_test = {}
local m = p.modules.self_test
m._VERSION = p._VERSION
newaction {
trigger = "self-test",
shortname = "Test Premake",
description = "Run Premake's own local unit test suites",
execute = function()
m.executeSelfTest()
end
}
newoption {
trigger = "test-only",
value = "suite[.test]",
description = "For self-test action; run specific suite or test"
}
function m.executeSelfTest()
m.detectDuplicateTests = true
m.loadTestsFromManifests()
m.detectDuplicateTests = false
local tests = {}
local isAction = true
for i, arg in ipairs(_ARGS) do
local _tests, err = m.getTestsWithIdentifier(arg)
if err then
error(err, 0)
end
tests = table.join(tests, _tests)
end
if #tests == 0 or _OPTIONS["test-only"] ~= nil then
local _tests, err = m.getTestsWithIdentifier(_OPTIONS["test-only"])
if err then
error(err, 0)
end
tests = table.join(tests, _tests)
end
local passed, failed = m.runTest(tests)
if failed > 0 then
printf("\n %d FAILED TEST%s", failed, iif(failed > 1, "S", ""))
os.exit(5)
end
end
function m.loadTestsFromManifests()
local mask = path.join(_MAIN_SCRIPT_DIR, "**/tests/_tests.lua")
local manifests = os.matchfiles(mask)
-- TODO: "**" should also match "." but doesn't currently
local top = path.join(_MAIN_SCRIPT_DIR, "tests/_tests.lua")
if os.isfile(top) then
table.insert(manifests, 1, top)
end
for i = 1, #manifests do
local manifest = manifests[i]
_TESTS_DIR = path.getdirectory(manifest)
local files = dofile(manifest)
for i = 1, #files do
local filename = path.join(_TESTS_DIR, files[i])
dofile(filename)
end
end
end
dofile("test_assertions.lua")
dofile("test_declare.lua")
dofile("test_helpers.lua")
dofile("test_runner.lua")
return m

View file

@ -0,0 +1,240 @@
---
-- test_assertions.lua
--
-- Assertion functions for unit tests.
--
-- Author Jason Perkins
-- Copyright (c) 2008-2016 Jason Perkins and the Premake project.
---
local p = premake
local m = p.modules.self_test
local _ = {}
function m.capture(expected)
local actual = p.captured() .. p.eol()
-- create line-by-line iterators for both values
local ait = actual:gmatch("(.-)" .. p.eol())
local eit = expected:gmatch("(.-)\n")
-- compare each value line by line
local linenum = 1
local atxt = ait()
local etxt = eit()
while etxt do
if (etxt ~= atxt) then
m.fail("(%d) expected:\n%s\n...but was:\n%s\nfulltext:\n%s", linenum, etxt, atxt, actual)
end
linenum = linenum + 1
atxt = ait()
etxt = eit()
end
end
function m.closedfile(expected)
if expected and not m.value_closedfile then
m.fail("expected file to be closed")
elseif not expected and m.value_closedfile then
m.fail("expected file to remain open")
end
end
function m.contains(expected, actual)
if type(expected) == "table" then
for i, v in ipairs(expected) do
m.contains(v, actual)
end
elseif not table.contains(actual, expected) then
m.fail("expected value %s not found", expected)
end
end
function m.excludes(expected, actual)
if type(expected) == "table" then
for i, v in ipairs(expected) do
m.excludes(v, actual)
end
elseif table.contains(actual, expected) then
m.fail("excluded value %s found", expected)
end
end
function m.fail(format, ...)
-- if format is a number then it is the stack depth
local depth = 3
local arg = {...}
if type(format) == "number" then
depth = depth + format
format = table.remove(arg, 1)
end
-- convert nils into something more usefuls
for i = 1, #arg do
if (arg[i] == nil) then
arg[i] = "(nil)"
elseif (type(arg[i]) == "table") then
arg[i] = "{" .. table.concat(arg[i], ", ") .. "}"
end
end
local msg = string.format(format, table.unpack(arg))
error(debug.traceback(msg, depth), depth)
end
function m.filecontains(expected, fn)
local f = io.open(fn)
local actual = f:read("*a")
f:close()
if (expected ~= actual) then
m.fail("expected %s but was %s", expected, actual)
end
end
function m.hasoutput()
local actual = p.captured()
if actual == "" then
m.fail("expected output, received none");
end
end
function m.isemptycapture()
local actual = p.captured()
if actual ~= "" then
m.fail("expected empty capture, but was %s", actual);
end
end
function m.isequal(expected, actual, depth)
depth = depth or 0
if type(expected) == "table" then
if expected and not actual then
m.fail(depth, "expected table, got nil")
end
if #expected < #actual then
m.fail(depth, "expected %d items, got %d", #expected, #actual)
end
for k,v in pairs(expected) do
m.isequal(expected[k], actual[k], depth + 1)
end
else
if (expected ~= actual) then
m.fail(depth, "expected %s but was %s", expected, actual or "nil")
end
end
return true
end
function m.isfalse(value)
if (value) then
m.fail("expected false but was true")
end
end
function m.isnil(value)
if (value ~= nil) then
m.fail("expected nil but was " .. tostring(value))
end
end
function m.isnotnil(value)
if (value == nil) then
m.fail("expected not nil")
end
end
function m.issame(expected, action)
if expected ~= action then
m.fail("expected same value")
end
end
function m.istrue(value)
if (not value) then
m.fail("expected true but was false")
end
end
function m.missing(value, actual)
if table.contains(actual, value) then
m.fail("unexpected value %s found", value)
end
end
function m.openedfile(fname)
if fname ~= m.value_openedfilename then
local msg = "expected to open file '" .. fname .. "'"
if m.value_openedfilename then
msg = msg .. ", got '" .. m.value_openedfilename .. "'"
end
m.fail(msg)
end
end
function m.success(fn, ...)
local ok, err = pcall(fn, ...)
if not ok then
m.fail("call failed: " .. err)
end
end
function m.stderr(expected)
if not expected and m.stderr_capture then
m.fail("Unexpected: " .. m.stderr_capture)
elseif expected then
if not m.stderr_capture or not m.stderr_capture:find(expected) then
m.fail(string.format("expected '%s'; got %s", expected, m.stderr_capture or "(nil)"))
end
end
end
function m.notstderr(expected)
if not expected and not m.stderr_capture then
m.fail("Expected output on stderr; none received")
elseif expected then
if m.stderr_capture and m.stderr_capture:find(expected) then
m.fail(string.format("stderr contains '%s'; was %s", expected, m.stderr_capture))
end
end
end

View file

@ -0,0 +1,246 @@
---
-- test_declare.lua
--
-- Declare unit test suites, and fetch tests from them.
--
-- Author Jason Perkins
-- Copyright (c) 2008-2016 Jason Perkins and the Premake project.
---
local p = premake
local m = p.modules.self_test
local _ = {}
_.suites = {}
_.suppressed = {}
---
-- Declare a new test suite.
--
-- @param suiteName
-- A unique name for the suite. This name will be displayed as part of
-- test failure messages, and also to select the suite when using the
-- `--test-only` command line parameter. Best to avoid spaces and special
-- characters which might not be command line friendly. An error will be
-- raised if the name is not unique.
-- @return
-- The new test suite object.
---
function m.declare(suiteName)
if _.suites[suiteName] then
error('Duplicate test suite "'.. suiteName .. '"', 2)
end
local _suite = {}
-- Setup a metatable for the test suites to use, this will catch duplicate tests
local suite = setmetatable({}, {
__index = _suite,
__newindex = function (table, key, value)
if m.detectDuplicateTests and _suite[key] ~= nil then
error('Duplicate test "'.. key .. '"', 2)
end
_suite[key] = value
end,
__pairs = function (table) return pairs(_suite) end,
__ipairs = function (table) return ipairs(_suite) end,
})
suite._SCRIPT_DIR = _SCRIPT_DIR
suite._TESTS_DIR = _TESTS_DIR
_.suites[suiteName] = suite
return suite
end
---
-- Prevent a particular test or suite of tests from running.
--
-- @param identifier
-- A test or suite identifier, indicating which tests should be suppressed,
-- in the form "suiteName" or "suiteName.testName".
---
function m.suppress(identifier)
if type(identifier) == "table" then
for i = 1, #identifier do
m.suppress(identifier[i])
end
else
_.suppressed[identifier] = true
end
end
function m.isSuppressed(identifier)
return _.suppressed[identifier]
end
---
-- Returns true if the provided test object represents a valid test.
---
function m.isValid(test)
if type(test.testFunction) ~= "function" then
return false
end
if test.testName == "setup" or test.testName == "teardown" then
return false
end
return true
end
---
-- Return the table of declared test suites.
---
function m.getSuites()
return _.suites
end
---
-- Fetch test objects via the string identifier.
--
-- @param identifier
-- An optional test or suite identifier, indicating which tests should be
-- run, in the form "suiteName" or "suiteName.testName". If not specified,
-- the global test object, representing all test suites, will be returned.
-- Use "*" to match any part of a suite or test name
-- @return
-- On success, returns an array of test objects, which should be considered opaque.
-- On failure, returns `nil` and an error.
---
function m.getTestsWithIdentifier(identifier)
local suiteName, testName = m.parseTestIdentifier(identifier)
if suiteName ~= nil and string.contains(suiteName, "*") then
local tests = {}
local pattern = string.gsub(suiteName, "*", ".*")
for _suiteName, suite in pairs(_.suites) do
local length = string.len(_suiteName)
local start, finish = string.find(_suiteName, pattern)
if start == 1 and finish == length then
if testName ~= nil then
if string.contains(testName, "*") then
local testPattern = string.gsub(testName, "*", ".*")
for _testName, test in pairs(suite) do
length = string.len(_testName)
start, finish = string.find(_testName, testPattern)
if start == 1 and finish == length then
table.insert(tests, {
suiteName = _suiteName,
suite = suite,
testName = _testName,
testFunction = test,
})
end
end
else
table.insert(tests, {
suiteName = _suiteName,
suite = suite,
testName = testName,
testFunction = suite[testName],
})
end
else
table.insert(tests, {
suiteName = _suiteName,
suite = suite,
testName = nil,
testFunction = nil,
})
end
end
end
return tests
else
local suite, test, err = _.checkTestIdentifier(_.suites, suiteName, testName)
if err then
return nil, err
end
return {
{
suiteName = suiteName,
suite = suite,
testName = testName,
testFunction = test
}
}
end
end
---
-- Parse a test identifier and split it into separate suite and test names.
--
-- @param identifier
-- A test identifier, which may be nil or an empty string, a test suite
-- name, or a suite and test with the format "suiteName.testName".
-- @return
-- Two values: the suite name and the test name, or nil if not included
-- in the identifier.
---
function m.parseTestIdentifier(identifier)
local suiteName, testName
if identifier then
local parts = string.explode(identifier, ".", true)
suiteName = iif(parts[1] ~= "", parts[1], nil)
testName = iif(parts[2] ~= "", parts[2], nil)
end
return suiteName, testName
end
function _.checkTestIdentifier(suites, suiteName, testName)
local suite, test
if suiteName then
suite = suites[suiteName]
if not suite then
return nil, nil, "No such test suite '" .. suiteName .. "'"
end
if testName then
test = suite[testName]
if not _.isValidTestPair(testName, test) then
return nil, nil, "No such test '" .. suiteName .. "." .. testName .. "'"
end
end
end
return suite, test
end
function _.isValidTestPair(testName, testFunc)
if type(testFunc) ~= "function" then
return false
end
if testName == "setup" or testName == "teardown" then
return false
end
return true
end

View file

@ -0,0 +1,87 @@
---
-- test_helpers.lua
--
-- Helper functions for setting up workspaces and projects, etc.
--
-- Author Jason Perkins
-- Copyright (c) 2008-2016 Jason Perkins and the Premake project.
---
local p = premake
local m = p.modules.self_test
function m.createWorkspace()
local wks = workspace("MyWorkspace")
configurations { "Debug", "Release" }
local prj = m.createProject(wks)
return wks, prj
end
-- Eventually we'll want to deprecate this one and move everyone
-- over to createWorkspace() instead (4 Sep 2015).
function m.createsolution()
local wks = workspace("MySolution")
configurations { "Debug", "Release" }
local prj = m.createproject(wks)
return wks, prj
end
function m.createProject(wks)
local n = #wks.projects + 1
if n == 1 then n = "" end
local prj = project ("MyProject" .. n)
language "C++"
kind "ConsoleApp"
return prj
end
function m.createGroup(wks)
local prj = group ("MyGroup" .. (#wks.groups + 1))
return prj
end
function m.getWorkspace(wks)
p.oven.bake()
return p.global.getWorkspace(wks.name)
end
function m.getRule(name)
p.oven.bake()
return p.global.getRule(name)
end
function m.getProject(wks, i)
wks = m.getWorkspace(wks)
return p.workspace.getproject(wks, i or 1)
end
function m.getConfig(prj, buildcfg, platform)
local wks = m.getWorkspace(prj.workspace)
prj = p.workspace.getproject(wks, prj.name)
return p.project.getconfig(prj, buildcfg, platform)
end
m.print = print
p.alias(m, "createProject", "createproject")
p.alias(m, "getConfig", "getconfig")
p.alias(m, "getProject", "getproject")
p.alias(m, "getWorkspace", "getsolution")

View file

@ -0,0 +1,337 @@
---
-- self-test/test_runner.lua
--
-- Execute unit tests and test suites.
--
-- Author Jason Perkins
-- Copyright (c) 2008-2016 Jason Perkins and the Premake project.
---
local p = premake
local m = p.modules.self_test
local _ = {}
function m.runTest(tests)
local failed = 0
local failedTests = {}
local suites = m.getSuites()
local suitesKeys, suiteTestsKeys, totalTestCount = _.preprocessTests(suites, tests)
_.log(term.lightGreen, "[==========]", string.format(" Running %d tests from %d test suites.", totalTestCount, #suitesKeys))
local startTime = os.clock()
for index, suiteName in ipairs(suitesKeys) do
suite = suites[suiteName]
if not m.isSuppressed(suiteName) then
local test = {
suiteName = suiteName,
suite = suite
}
local suiteFailed, suiteFailedTests = _.runTestSuite(test, suiteTestsKeys[suiteName])
failed = failed + suiteFailed
failedTests = table.join(failedTests, suiteFailedTests)
end
end
_.log(term.lightGreen, "[==========]", string.format(" %d tests from %d test suites ran. (%.0f ms total)", totalTestCount, #suitesKeys, (os.clock() - startTime) * 1000))
_.log(term.lightGreen, "[ PASSED ]", string.format(" %d tests.", totalTestCount - failed))
if failed > 0 then
_.log(term.lightRed, "[ FAILED ]", string.format(" %d tests, listed below:", failed))
for index, testName in ipairs(failedTests) do
_.log(term.lightRed, "[ FAILED ]", " " .. testName)
end
end
return (totalTestCount - failed), failed
end
function _.runTestSuite(test, keys)
local failed = 0
local failedTests = {}
_.log(term.lightGreen, "[----------]", string.format(" %d tests from %s", #keys, test.suiteName))
local startTime = os.clock()
if test.suite ~= nil then
for index, testName in ipairs(keys) do
testFunction = test.suite[testName]
test.testName = testName
test.testFunction = testFunction
if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) then
local err = _.runTest(test)
if err then
failed = failed + 1
table.insert(failedTests, test.suiteName .. "." .. test.testName .. "\n" .. err)
end
end
end
end
_.log(term.lightGreen, "[----------]", string.format(" %d tests from %s (%.0f ms total)\n", #keys, test.suiteName, (os.clock() - startTime) * 1000))
return failed, failedTests
end
function _.runTest(test)
_.log(term.lightGreen, "[ RUN ]", string.format(" %s.%s", test.suiteName, test.testName))
local startTime = os.clock()
local cwd = os.getcwd()
local hooks = _.installTestingHooks()
_TESTS_DIR = test.suite._TESTS_DIR
_SCRIPT_DIR = test.suite._SCRIPT_DIR
m.suiteName = test.suiteName
m.testName = test.testName
local ok, err = _.setupTest(test)
if ok then
ok, err = _.executeTest(test)
end
local tok, terr = _.teardownTest(test)
ok = ok and tok
err = err or terr
_.removeTestingHooks(hooks)
os.chdir(cwd)
if ok then
_.log(term.lightGreen, "[ OK ]", string.format(" %s.%s (%.0f ms)", test.suiteName, test.testName, (os.clock() - startTime) * 1000))
return nil
else
_.log(term.lightRed, "[ FAILED ]", string.format(" %s.%s (%.0f ms)", test.suiteName, test.testName, (os.clock() - startTime) * 1000))
m.print(string.format("%s", err))
return err
end
end
function _.log(color, left, right)
term.pushColor(color)
io.write(left)
term.popColor()
m.print(right)
end
function _.preprocessTests(suites, filters)
local suitesKeys = {}
local suiteTestsKeys = {}
local totalTestCount = 0
for i, filter in ipairs(filters) do
for suiteName, suite in pairs(suites) do
if not m.isSuppressed(suiteName) and suite ~= nil and (not filter.suiteName or filter.suiteName == suiteName) then
local test = {}
test.suiteName = suiteName
test.suite = suite
if not table.contains(suitesKeys, suiteName) then
table.insertsorted(suitesKeys, suiteName)
suiteTestsKeys[suiteName] = {}
end
for testName, testFunction in pairs(suite) do
test.testName = testName
test.testFunction = testFunction
if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) and (not filter.testName or filter.testName == testName) then
if not table.contains(suiteTestsKeys[suiteName], testName) then
table.insertsorted(suiteTestsKeys[suiteName], testName)
totalTestCount = totalTestCount + 1
end
end
end
end
end
end
return suitesKeys, suiteTestsKeys, totalTestCount
end
function _.installTestingHooks()
local hooks = {}
hooks.action = _ACTION
hooks.options = _OPTIONS
hooks.targetOs = _TARGET_OS
hooks.io_open = io.open
hooks.io_output = io.output
hooks.os_writefile_ifnotequal = os.writefile_ifnotequal
hooks.p_utf8 = p.utf8
hooks.print = print
hooks.setTextColor = term.setTextColor
local mt = getmetatable(io.stderr)
_.builtin_write = mt.write
mt.write = _.stub_stderr_write
_OPTIONS = table.shallowcopy(_OPTIONS) or {}
setmetatable(_OPTIONS, getmetatable(hooks.options))
io.open = _.stub_io_open
io.output = _.stub_io_output
os.writefile_ifnotequal = _.stub_os_writefile_ifnotequal
print = _.stub_print
p.utf8 = _.stub_utf8
term.setTextColor = _.stub_setTextColor
stderr_capture = nil
p.clearWarnings()
p.eol("\n")
p.escaper(nil)
p.indent("\t")
p.api.reset()
m.stderr_capture = nil
m.value_openedfilename = nil
m.value_openedfilemode = nil
m.value_closedfile = false
return hooks
end
function _.removeTestingHooks(hooks)
p.action.set(hooks.action)
_OPTIONS = hooks.options
_TARGET_OS = hooks.targetOs
io.open = hooks.io_open
io.output = hooks.io_output
os.writefile_ifnotequal = hooks.os_writefile_ifnotequal
p.utf8 = hooks.p_utf8
print = hooks.print
term.setTextColor = hooks.setTextColor
local mt = getmetatable(io.stderr)
mt.write = _.builtin_write
end
function _.setupTest(test)
if type(test.suite.setup) == "function" then
return xpcall(test.suite.setup, _.errorHandler)
else
return true
end
end
function _.executeTest(test)
local result, err
p.capture(function()
result, err = xpcall(test.testFunction, _.errorHandler)
end)
return result, err
end
function _.teardownTest(test)
if type(test.suite.teardown) == "function" then
return xpcall(test.suite.teardown, _.errorHandler)
else
return true
end
end
function _.errorHandler(err)
local msg = err
-- if the error doesn't include a stack trace, add one
if not msg:find("stack traceback:", 1, true) then
msg = debug.traceback(err, 2)
end
-- trim of the trailing context of the originating xpcall
local i = msg:find("[C]: in function 'xpcall'", 1, true)
if i then
msg = msg:sub(1, i - 3)
end
-- if the resulting stack trace is only one level deep, ignore it
local n = select(2, msg:gsub('\n', '\n'))
if n == 2 then
msg = msg:sub(1, msg:find('\n', 1, true) - 1)
end
return msg
end
function _.stub_io_open(fname, mode)
m.value_openedfilename = fname
m.value_openedfilemode = mode
return {
read = function()
end,
close = function()
m.value_closedfile = true
end
}
end
function _.stub_io_output(f)
end
function _.stub_os_writefile_ifnotequal(content, fname)
m.value_openedfilename = fname
m.value_closedfile = true
return 0
end
function _.stub_print(s)
end
function _.stub_stderr_write(...)
if select(1, ...) == io.stderr then
m.stderr_capture = (m.stderr_capture or "") .. select(2, ...)
else
return _.builtin_write(...)
end
end
function _.stub_utf8()
end
function _.stub_setTextColor()
end