From 3283b1d14167b3d4b2b94432edf29708070a23de Mon Sep 17 00:00:00 2001 From: Jason Perkins Date: Wed, 25 May 2016 16:55:49 -0400 Subject: [PATCH] Consolidate and clean up the test runner logic and associated environment hooks --- modules/self-test/_manifest.lua | 2 +- modules/self-test/self-test.lua | 15 +- modules/self-test/test_declare.lua | 27 ++++ modules/self-test/test_hooks.lua | 77 --------- modules/self-test/test_runner.lua | 246 +++++++++++++++++++++++++++++ modules/self-test/testfx.lua | 179 --------------------- 6 files changed, 284 insertions(+), 262 deletions(-) delete mode 100644 modules/self-test/test_hooks.lua create mode 100644 modules/self-test/test_runner.lua diff --git a/modules/self-test/_manifest.lua b/modules/self-test/_manifest.lua index d706943d..6709ce44 100644 --- a/modules/self-test/_manifest.lua +++ b/modules/self-test/_manifest.lua @@ -1,6 +1,6 @@ return { "self-test.lua", "test_declare.lua", - "test_hooks.lua", + "test_runner.lua", "testfx.lua", } diff --git a/modules/self-test/self-test.lua b/modules/self-test/self-test.lua index 10c8902d..ab19eeb6 100644 --- a/modules/self-test/self-test.lua +++ b/modules/self-test/self-test.lua @@ -43,15 +43,20 @@ function m.executeSelfTest() m.loadTestsFromManifests() - local result, err = m.runTests(_OPTIONS["test-only"]) + local test, err = m.getTestWithIdentifier(_OPTIONS["test-only"]) if err then error(err, 0) end - printf("%d tests passed, %d failed in %0.02f seconds", - result.passed, result.failed, result.elapsed) + local startTime = os.clock() - if result.failed > 0 then + local passed, failed = m.runTest(test) + + local elapsed = os.clock() - startTime + + printf("%d tests passed, %d failed in %0.02f seconds", passed, failed, elapsed) + + if failed > 0 then os.exit(5) end end @@ -84,7 +89,7 @@ dofile("test_declare.lua") - dofile("test_hooks.lua") + dofile("test_runner.lua") diff --git a/modules/self-test/test_declare.lua b/modules/self-test/test_declare.lua index 942bbfea..ae876d6f 100644 --- a/modules/self-test/test_declare.lua +++ b/modules/self-test/test_declare.lua @@ -14,6 +14,7 @@ _.suites = {} + _.suppressed = {} T = _.suites @@ -48,6 +49,32 @@ +--- +-- 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. --- diff --git a/modules/self-test/test_hooks.lua b/modules/self-test/test_hooks.lua deleted file mode 100644 index 085ad198..00000000 --- a/modules/self-test/test_hooks.lua +++ /dev/null @@ -1,77 +0,0 @@ ---- --- test_hooks.lua --- --- Wire into the Lua runtime environment to support testing. --- --- 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.installTestingHooks() - local hooks = {} - - 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 - - io.open = _.stub_io_open - io.output = _.stub_io_output - os.writefile_ifnotequal = _.stub_os_writefile_ifnotequal - print = _.stub_print - p.utf8 = _.stub_utf8 - - return hooks - end - - - - function m.removeTestingHooks(hooks) - 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 - end - - - - function _.stub_io_open(fname, mode) - test.value_openedfilename = fname - test.value_openedfilemode = mode - return { - close = function() - test.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_utf8() - end diff --git a/modules/self-test/test_runner.lua b/modules/self-test/test_runner.lua new file mode 100644 index 00000000..546a9d9b --- /dev/null +++ b/modules/self-test/test_runner.lua @@ -0,0 +1,246 @@ +--- +-- 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(test) + local scopedTestCall + + if test.testFunction then + scopedTestCall = _.runTest + elseif test.suite then + scopedTestCall = _.runTestSuite + else + scopedTestCall = _.runAllTests + end + + return scopedTestCall(test) + end + + + + function _.runAllTests() + local passed = 0 + local failed = 0 + + local suites = m.getSuites() + for suiteName, suite in pairs(suites) do + if not m.isSuppressed(suiteName) then + local test = {} + + test.suiteName = suiteName + test.suite = suite + + local suitePassed, suiteFailed = _.runTestSuite(test) + + passed = passed + suitePassed + failed = failed + suiteFailed + end + end + + return passed, failed + end + + + + function _.runTestSuite(test) + local passed = 0 + local failed = 0 + + for testName, testFunction in pairs(test.suite) do + test.testName = testName + test.testFunction = testFunction + + if m.isValid(test) and not m.isSuppressed(test.suiteName .. "." .. test.testName) then + if _.runTest(test) then + passed = passed + 1 + else + failed = failed + 1 + end + end + end + + return passed, failed + end + + + + function _.runTest(test) + local hooks = _.installTestingHooks() + + _TESTS_DIR = test.suite._TESTS_DIR + _SCRIPT_DIR = test.suite._SCRIPT_DIR + + 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 + + if not ok then + m.print(string.format("%s.%s: %s", test.suiteName, test.testName, err)) + end + + _.removeTestingHooks(hooks) + + return ok, err + end + + + + function _.installTestingHooks() + local hooks = {} + + hooks.action = _ACTION + hooks.options = _OPTIONS + hooks.os = _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 + + _OPTIONS = {} + 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 + + stderr_capture = nil + + p.clearWarnings() + p.eol("\n") + p.escaper(nil) + p.indent("\t") + p.api.reset() + + m.value_openedfilename = nil + m.value_openedfilemode = nil + m.value_closedfile = false + + return hooks + end + + + + function _.removeTestingHooks(hooks) + _ACTION = hooks.action + _OPTIONS = hooks.options + _OS = hooks.os + + 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 + 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 { + 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_utf8() + end diff --git a/modules/self-test/testfx.lua b/modules/self-test/testfx.lua index 6038ab04..0a3ebbde 100644 --- a/modules/self-test/testfx.lua +++ b/modules/self-test/testfx.lua @@ -11,12 +11,6 @@ local _ = {} --- --- Define a namespace for the testing functions --- - - m.suppressed = {} - -- -- Capture stderr for testing. @@ -299,177 +293,4 @@ --- --- Test execution function --- - local _OS_host = _OS - local _OPTIONS_host = _OPTIONS - - local function error_handler(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 - - - local function test_setup(suite, fn) - _ACTION = "test" - _OS = _OS_host - - _OPTIONS = {} - setmetatable(_OPTIONS, getmetatable(_OPTIONS_host)) - - stderr_capture = nil - - premake.clearWarnings() - premake.eol("\n") - premake.escaper(nil) - premake.indent("\t") - premake.api.reset() - - -- reset captured I/O values - m.value_openedfilename = nil - m.value_openedfilemode = nil - m.value_closedfile = false - - if suite.setup then - return xpcall(suite.setup, error_handler) - else - return true - end - end - - - local function test_run(suite, fn) - local result, err - premake.capture(function() - result, err = xpcall(fn, error_handler) - end) - return result, err - end - - - local function test_teardown(suite, fn) - if suite.teardown then - return xpcall(suite.teardown, error_handler) - else - return true - end - end - - - - function m.suppress(id) - if type(id) == "table" then - for i = 1, #id do - m.suppress(id[i]) - end - else - m.suppressed[id] = true - end - end - - - m.print = print - - - - function m.runTests(identifier) - local test, err = m.getTestWithIdentifier(identifier) - if not test then - return nil, err - end - - local hooks = m.installTestingHooks() - - test.passed = 0 - test.failed = 0 - - local startTime = os.clock() - - if test.testFunction then - _.runTest(test) - elseif test.suite then - _.runSuite(test) - else - _.runAll(test) - end - - local result = { - passed = test.passed, - failed = test.failed, - start = startTime, - elapsed = os.clock() - startTime - } - - m.removeTestingHooks(hooks) - - return result - end - - - - function _.runAll(test) - local suites = m.getSuites() - for suiteName, suite in pairs(suites) do - if not m.suppressed[suiteName] then - test.suiteName = suiteName - test.suite = suite - _.runSuite(test) - end - end - end - - - - function _.runSuite(test) - for testName, testFunction in pairs(test.suite) do - test.testName = testName - test.testFunction = testFunction - if m.isValid(test) and not m.suppressed[test.suiteName .. "." .. test.testName] then - _.runTest(test) - end - end - end - - - - function _.runTest(test) - _TESTS_DIR = test.suite._TESTS_DIR - _SCRIPT_DIR = test.suite._SCRIPT_DIR - - local ok, err = test_setup(test.suite, test.testFunction) - - if ok then - ok, err = test_run(test.suite, test.testFunction) - end - - local tok, terr = test_teardown(test.suite, test.testFunction) - ok = ok and tok - err = err or terr - - if not ok then - m.print(string.format("%s.%s: %s", test.suiteName, test.testName, err)) - test.failed = test.failed + 1 - else - test.passed = test.passed + 1 - end - end