From 19df8040df98cdf4912aef110d1e99903616df14 Mon Sep 17 00:00:00 2001
From: ell <77150506+ellraiser@users.noreply.github.com>
Date: Wed, 4 Oct 2023 14:43:44 +0100
Subject: [PATCH 01/54] 0.1
Initial commit of a basic test framework, see readme.md for more
Most modules are covered with basic unit tests, and there's an example test for a graphics draw (rectangle) and object (File) - for object tests doing more scenario based so we can check multiple things together
---
testing/classes/TestMethod.lua | 359 ++++++++++++++++
testing/classes/TestModule.lua | 114 ++++++
testing/classes/TestSuite.lua | 159 ++++++++
testing/conf.lua | 24 ++
testing/main.lua | 172 ++++++++
testing/output/lovetest_runAllTests.html | 1 +
testing/output/lovetest_runAllTests.xml | 385 ++++++++++++++++++
testing/readme.md | 139 +++++++
testing/resources/click.ogg | Bin 0 -> 7824 bytes
testing/resources/font.ttf | Bin 0 -> 10390 bytes
testing/resources/love.dxt1 | Bin 0 -> 2872 bytes
testing/resources/love.png | Bin 0 -> 680 bytes
.../love_test_graphics_rectangle_expected.png | Bin 0 -> 135 bytes
testing/resources/sample.ogv | Bin 0 -> 23845 bytes
testing/resources/test.txt | 1 +
testing/resources/test.zip | Bin 0 -> 150 bytes
testing/tests/audio.lua | 296 ++++++++++++++
testing/tests/data.lua | 182 +++++++++
testing/tests/event.lua | 73 ++++
testing/tests/filesystem.lua | 354 ++++++++++++++++
testing/tests/font.lua | 54 +++
testing/tests/graphics.lua | 158 +++++++
testing/tests/image.lua | 31 ++
testing/tests/math.lua | 178 ++++++++
testing/tests/objects.lua | 175 ++++++++
testing/tests/physics.lua | 306 ++++++++++++++
testing/tests/sound.lua | 19 +
testing/tests/system.lua | 68 ++++
testing/tests/thread.lua | 28 ++
testing/tests/timer.lua | 45 ++
testing/tests/video.lua | 10 +
testing/tests/window.lua | 336 +++++++++++++++
32 files changed, 3667 insertions(+)
create mode 100644 testing/classes/TestMethod.lua
create mode 100644 testing/classes/TestModule.lua
create mode 100644 testing/classes/TestSuite.lua
create mode 100644 testing/conf.lua
create mode 100644 testing/main.lua
create mode 100644 testing/output/lovetest_runAllTests.html
create mode 100644 testing/output/lovetest_runAllTests.xml
create mode 100644 testing/readme.md
create mode 100644 testing/resources/click.ogg
create mode 100644 testing/resources/font.ttf
create mode 100644 testing/resources/love.dxt1
create mode 100644 testing/resources/love.png
create mode 100644 testing/resources/love_test_graphics_rectangle_expected.png
create mode 100644 testing/resources/sample.ogv
create mode 100644 testing/resources/test.txt
create mode 100644 testing/resources/test.zip
create mode 100644 testing/tests/audio.lua
create mode 100644 testing/tests/data.lua
create mode 100644 testing/tests/event.lua
create mode 100644 testing/tests/filesystem.lua
create mode 100644 testing/tests/font.lua
create mode 100644 testing/tests/graphics.lua
create mode 100644 testing/tests/image.lua
create mode 100644 testing/tests/math.lua
create mode 100644 testing/tests/objects.lua
create mode 100644 testing/tests/physics.lua
create mode 100644 testing/tests/sound.lua
create mode 100644 testing/tests/system.lua
create mode 100644 testing/tests/thread.lua
create mode 100644 testing/tests/timer.lua
create mode 100644 testing/tests/video.lua
create mode 100644 testing/tests/window.lua
diff --git a/testing/classes/TestMethod.lua b/testing/classes/TestMethod.lua
new file mode 100644
index 000000000..b0c763082
--- /dev/null
+++ b/testing/classes/TestMethod.lua
@@ -0,0 +1,359 @@
+-- @class - TestMethod
+-- @desc - used to run a specific method from a module's /test/ suite
+-- each assertion is tracked and then printed to output
+TestMethod = {
+
+
+ -- @method - TestMethod:new()
+ -- @desc - create a new TestMethod object
+ -- @param {string} method - string of method name to run
+ -- @param {TestMethod} testmethod - parent testmethod this test belongs to
+ -- @return {table} - returns the new Test object
+ new = function(self, method, testmodule)
+ local test = {
+ testmodule = testmodule,
+ method = method,
+ asserts = {},
+ start = love.timer.getTime(),
+ finish = 0,
+ count = 0,
+ passed = false,
+ skipped = false,
+ skipreason = '',
+ fatal = '',
+ message = nil,
+ result = {},
+ colors = {
+ red = {1, 0, 0, 1},
+ green = {0, 1, 0, 1},
+ blue = {0, 0, 1, 1},
+ black = {0, 0, 0, 1},
+ white = {1, 1, 1, 1}
+ }
+ }
+ setmetatable(test, self)
+ self.__index = self
+ return test
+ end,
+
+
+ -- @method - TestMethod:assertEquals()
+ -- @desc - used to assert two values are equals
+ -- @param {any} expected - expected value of the test
+ -- @param {any} actual - actual value of the test
+ -- @param {string} label - label for this test to use in exports
+ -- @return {nil}
+ assertEquals = function(self, expected, actual, label)
+ self.count = self.count + 1
+ table.insert(self.asserts, {
+ key = 'assert #' .. tostring(self.count),
+ passed = expected == actual,
+ message = 'expected \'' .. tostring(expected) .. '\' got \'' ..
+ tostring(actual) .. '\'',
+ test = label
+ })
+ end,
+
+
+ -- @method - TestMethod:assertPixels()
+ -- @desc - checks a list of coloured pixels agaisnt given imgdata
+ -- @param {ImageData} imgdata - image data to check
+ -- @param {table} pixels - map of colors to list of pixel coords, i.e.
+ -- { blue = { {1, 1}, {2, 2}, {3, 4} } }
+ -- @return {nil}
+ assertPixels = function(self, imgdata, pixels, label)
+ for i, v in pairs(pixels) do
+ local col = self.colors[i]
+ local pixels = v
+ for p=1,#pixels do
+ local coord = pixels[p]
+ local tr, tg, tb, ta = imgdata:getPixel(coord[1], coord[2])
+ local compare_id = tostring(coord[1]) .. ',' .. tostring(coord[2])
+ -- @TODO add some sort pixel tolerance to the coords
+ self:assertEquals(col[1], tr, 'check pixel r for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
+ self:assertEquals(col[2], tg, 'check pixel g for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
+ self:assertEquals(col[3], tb, 'check pixel b for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
+ self:assertEquals(col[4], ta, 'check pixel a for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')')
+ end
+ end
+ end,
+
+
+ -- @method - TestMethod:assertNotEquals()
+ -- @desc - used to assert two values are not equal
+ -- @param {any} expected - expected value of the test
+ -- @param {any} actual - actual value of the test
+ -- @param {string} label - label for this test to use in exports
+ -- @return {nil}
+ assertNotEquals = function(self, expected, actual, label)
+ self.count = self.count + 1
+ table.insert(self.asserts, {
+ key = 'assert #' .. tostring(self.count),
+ passed = expected ~= actual,
+ message = 'avoiding \'' .. tostring(expected) .. '\' got \'' ..
+ tostring(actual) .. '\'',
+ test = label
+ })
+ end,
+
+
+ -- @method - TestMethod:assertRange()
+ -- @desc - used to check a value is within an expected range
+ -- @param {number} actual - actual value of the test
+ -- @param {number} min - minimum value the actual should be >= to
+ -- @param {number} max - maximum value the actual should be <= to
+ -- @param {string} label - label for this test to use in exports
+ -- @return {nil}
+ assertRange = function(self, actual, min, max, label)
+ self.count = self.count + 1
+ table.insert(self.asserts, {
+ key = 'assert #' .. tostring(self.count),
+ passed = actual >= min and actual <= max,
+ message = 'value \'' .. tostring(actual) .. '\' out of range \'' ..
+ tostring(min) .. '-' .. tostring(max) .. '\'',
+ test = label
+ })
+ end,
+
+
+ -- @method - TestMethod:assertMatch()
+ -- @desc - used to check a value is within a list of values
+ -- @param {number} list - list of valid values for the test
+ -- @param {number} actual - actual value of the test to check is in the list
+ -- @param {string} label - label for this test to use in exports
+ -- @return {nil}
+ assertMatch = function(self, list, actual, label)
+ self.count = self.count + 1
+ local found = false
+ for l=1,#list do
+ if list[l] == actual then found = true end;
+ end
+ table.insert(self.asserts, {
+ key = 'assert #' .. tostring(self.count),
+ passed = found == true,
+ message = 'value \'' .. tostring(actual) .. '\' not found in \'' ..
+ table.concat(list, ',') .. '\'',
+ test = label
+ })
+ end,
+
+
+ -- @method - TestMethod:assertGreaterEqual()
+ -- @desc - used to check a value is >= than a certain target value
+ -- @param {any} target - value to check the test agaisnt
+ -- @param {any} actual - actual value of the test
+ -- @param {string} label - label for this test to use in exports
+ -- @return {nil}
+ assertGreaterEqual = function(self, target, actual, label)
+ self.count = self.count + 1
+ local passing = false
+ if target ~= nil and actual ~= nil then
+ passing = actual >= target
+ end
+ table.insert(self.asserts, {
+ key = 'assert #' .. tostring(self.count),
+ passed = passing,
+ message = 'value \'' .. tostring(actual) .. '\' not >= \'' ..
+ tostring(target) .. '\'',
+ test = label
+ })
+ end,
+
+
+ -- @method - TestMethod:assertLessEqual()
+ -- @desc - used to check a value is <= than a certain target value
+ -- @param {any} target - value to check the test agaisnt
+ -- @param {any} actual - actual value of the test
+ -- @param {string} label - label for this test to use in exports
+ -- @return {nil}
+ assertLessEqual = function(self, target, actual, label)
+ self.count = self.count + 1
+ local passing = false
+ if target ~= nil and actual ~= nil then
+ passing = actual <= target
+ end
+ table.insert(self.asserts, {
+ key = 'assert #' .. tostring(self.count),
+ passed = passing,
+ message = 'value \'' .. tostring(actual) .. '\' not <= \'' ..
+ tostring(target) .. '\'',
+ test = label
+ })
+ end,
+
+
+ -- @method - TestMethod:assertObject()
+ -- @desc - used to check a table is a love object, this runs 3 seperate
+ -- tests to check table has the basic properties of an object
+ -- @note - actual object functionality tests are done in the objects module
+ -- @param {table} obj - table to check is a valid love object
+ -- @return {nil}
+ assertObject = function(self, obj)
+ self:assertNotEquals(nil, obj, 'check not nill')
+ self:assertEquals('userdata', type(obj), 'check is userdata')
+ if obj ~= nil then
+ self:assertNotEquals(nil, obj:type(), 'check has :type()')
+ end
+ end,
+
+
+
+ -- @method - TestMethod:skipTest()
+ -- @desc - used to mark this test as skipped for a specific reason
+ -- @param {string} reason - reason why method is being skipped
+ -- @return {nil}
+ skipTest = function(self, reason)
+ self.skipped = true
+ self.skipreason = reason
+ end,
+
+
+ -- @method - TestMethod:evaluateTest()
+ -- @desc - evaluates the results of all assertions for a final restult
+ -- @return {nil}
+ evaluateTest = function(self)
+ local failure = ''
+ local failures = 0
+ for a=1,#self.asserts do
+ -- @TODO just return first failed assertion msg? or all?
+ -- currently just shows the first assert that failed
+ if self.asserts[a].passed == false and self.skipped == false then
+ if failure == '' then failure = self.asserts[a] end
+ failures = failures + 1
+ end
+ end
+ if self.fatal ~= '' then failure = self.fatal end
+ local passed = tostring(#self.asserts - failures)
+ local total = '(' .. passed .. '/' .. tostring(#self.asserts) .. ')'
+ if self.skipped == true then
+ self.testmodule.skipped = self.testmodule.skipped + 1
+ love.test.totals[3] = love.test.totals[3] + 1
+ self.result = {
+ total = '',
+ result = "SKIP",
+ passed = false,
+ message = '(0/0) - method skipped [' .. self.skipreason .. ']'
+ }
+ else
+ if failure == '' and #self.asserts > 0 then
+ self.passed = true
+ self.testmodule.passed = self.testmodule.passed + 1
+ love.test.totals[1] = love.test.totals[1] + 1
+ self.result = {
+ total = total,
+ result = 'PASS',
+ passed = true,
+ message = nil
+ }
+ else
+ self.passed = false
+ self.testmodule.failed = self.testmodule.failed + 1
+ love.test.totals[2] = love.test.totals[2] + 1
+ if #self.asserts == 0 then
+ local msg = 'no asserts defined'
+ if self.fatal ~= '' then msg = self.fatal end
+ self.result = {
+ total = total,
+ result = 'FAIL',
+ passed = false,
+ key = 'test',
+ message = msg
+ }
+ else
+ local key = failure['key']
+ if failure['test'] ~= nil then
+ key = key .. ' [' .. failure['test'] .. ']'
+ end
+ self.result = {
+ total = total,
+ result = 'FAIL',
+ passed = false,
+ key = key,
+ message = failure['message']
+ }
+ end
+ end
+ end
+ self:printResult()
+ end,
+
+
+ -- @method - TestMethod:printResult()
+ -- @desc - prints the result of the test to the console as well as appends
+ -- the XML + HTML for the test to the testsuite output
+ -- @return {nil}
+ printResult = function(self)
+
+ -- get total timestamp
+ -- @TODO make nicer, just need a 3DP ms value
+ self.finish = love.timer.getTime() - self.start
+ love.test.time = love.test.time + self.finish
+ self.testmodule.time = self.testmodule.time + self.finish
+ local endtime = tostring(math.floor((love.timer.getTime() - self.start)*1000))
+ if string.len(endtime) == 1 then endtime = ' ' .. endtime end
+ if string.len(endtime) == 2 then endtime = ' ' .. endtime end
+ if string.len(endtime) == 3 then endtime = ' ' .. endtime end
+
+ -- get failure/skip message for output (if any)
+ local failure = ''
+ local output = ''
+ if self.passed == false and self.skipped == false then
+ failure = '\t\t\t\n'
+ output = self.result.key .. ' ' .. self.result.message
+ end
+ if output == '' and self.skipped == true then
+ output = self.skipreason
+ end
+
+ -- append XML for the test class result
+ self.testmodule.xml = self.testmodule.xml .. '\t\t\n' ..
+ failure .. '\t\t\n'
+
+ -- unused currently, adds a preview image for certain graphics methods to the output
+ local preview = ''
+ -- if self.testmodule.module == 'graphics' then
+ -- local filename = 'love_test_graphics_rectangle'
+ -- preview = '
' .. '
Expected
' ..
+ -- 'Actual
'
+ -- end
+
+ -- append HTML for the test class result
+ local status = '🔴'
+ local cls = 'red'
+ if self.passed == true then status = '🟢'; cls = '' end
+ if self.skipped == true then status = '🟡'; cls = '' end
+ self.testmodule.html = self.testmodule.html ..
+ '' ..
+ '' .. status .. ' | ' ..
+ '' .. self.method .. ' | ' ..
+ '' .. tostring(self.finish*1000) .. 'ms | ' ..
+ '' .. output .. preview .. ' | ' ..
+ '
'
+
+ -- add message if assert failed
+ local msg = ''
+ if self.result.message ~= nil and self.skipped == false then
+ msg = ' - ' .. self.result.key ..
+ ' failed - (' .. self.result.message .. ')'
+ end
+ if self.skipped == true then
+ msg = self.result.message
+ end
+
+ -- log final test result to console
+ -- i know its hacky but its neat soz
+ local tested = 'love.' .. self.testmodule.module .. '.' .. self.method .. '()'
+ local matching = string.sub(self.testmodule.spacer, string.len(tested), 40)
+ self.testmodule:log(
+ self.testmodule.colors[self.result.result],
+ ' ' .. tested .. matching,
+ ' ==> ' .. self.result.result .. ' - ' .. endtime .. 'ms ' ..
+ self.result.total .. msg
+ )
+ end
+
+
+}
\ No newline at end of file
diff --git a/testing/classes/TestModule.lua b/testing/classes/TestModule.lua
new file mode 100644
index 000000000..379aac360
--- /dev/null
+++ b/testing/classes/TestModule.lua
@@ -0,0 +1,114 @@
+-- @class - TestModule
+-- @desc - used to run tests for a given module, each test method will spawn
+-- a love.test.Test object
+TestModule = {
+
+
+ -- @method - TestModule:new()
+ -- @desc - create a new Suite object
+ -- @param {string} module - string of love module the suite is for
+ -- @return {table} - returns the new Suite object
+ new = function(self, module, method)
+ local testmodule = {
+ timer = 0,
+ time = 0,
+ delay = 0.1,
+ spacer = ' ',
+ colors = {
+ PASS = 'green', FAIL = 'red', SKIP = 'grey'
+ },
+ colormap = {
+ grey = '\27[37m',
+ green = '\27[32m',
+ red = '\27[31m',
+ yellow = '\27[33m'
+ },
+ xml = '',
+ html = '',
+ tests = {},
+ running = {},
+ called = {},
+ passed = 0,
+ failed = 0,
+ skipped = 0,
+ module = module,
+ method = method,
+ index = 1,
+ start = false,
+ }
+ setmetatable(testmodule, self)
+ self.__index = self
+ return testmodule
+ end,
+
+
+ -- @method - TestModule:log()
+ -- @desc - log to console with specific colors, split out to make it easier
+ -- to adjust all console output across the tests
+ -- @param {string} color - color key to use for the log
+ -- @param {string} line - main message to write (LHS)
+ -- @param {string} result - result message to write (RHS)
+ -- @return {nil}
+ log = function(self, color, line, result)
+ if result == nil then result = '' end
+ print(self.colormap[color] .. line .. result)
+ end,
+
+
+ -- @method - TestModule:runTests()
+ -- @desc - starts the running of tests and sets up the list of methods to test
+ -- @param {string} module - module to set for the test suite
+ -- @param {string} method - specific method to test, if nil all methods tested
+ -- @return {nil}
+ runTests = function(self)
+ self.running = {}
+ self.passed = 0
+ self.failed = 0
+ if self.method ~= nil then
+ table.insert(self.running, self.method)
+ else
+ for i,_ in pairs(love.test[self.module]) do
+ table.insert(self.running, i)
+ end
+ table.sort(self.running)
+ end
+ self.index = 1
+ self.start = true
+ self:log('yellow', '\nlove.' .. self.module .. '.testmodule.start')
+ end,
+
+
+ -- @method - TestModule:printResult()
+ -- @desc - prints the result of the module to the console as well as appends
+ -- the XML + HTML for the test to the testsuite output
+ -- @return {nil}
+ printResult = function(self)
+ -- add xml to main output
+ love.test.xml = love.test.xml .. '\t\n' .. self.xml .. '\t\n'
+ -- add html to main output
+ local status = '🔴'
+ if self.failed == 0 then status = '🟢' end
+ love.test.html = love.test.html .. '' .. status .. ' love.' .. self.module .. '
' ..
+ '- 🟢 ' .. tostring(self.passed) .. ' Tests
' ..
+ '- 🔴 ' .. tostring(self.failed) .. ' Failures
' ..
+ '- 🟡 ' .. tostring(self.skipped) .. ' Skipped
' ..
+ '- ' .. tostring(self.time*1000) .. 'ms
' .. '
' ..
+ ' | Method | Time | Details |
' ..
+ self.html .. '
'
+ -- print module results to console
+ self:log('yellow', 'love.' .. self.module .. '.testmodule.end')
+ local failedcol = '\27[31m'
+ if self.failed == 0 then failedcol = '\27[37m' end
+ self:log('green', tostring(self.passed) .. ' PASSED' .. ' || ' ..
+ failedcol .. tostring(self.failed) .. ' FAILED || \27[37m' ..
+ tostring(self.skipped) .. ' SKIPPED')
+ self.start = false
+ self.fakequit = false
+ end
+
+
+}
\ No newline at end of file
diff --git a/testing/classes/TestSuite.lua b/testing/classes/TestSuite.lua
new file mode 100644
index 000000000..234019c8f
--- /dev/null
+++ b/testing/classes/TestSuite.lua
@@ -0,0 +1,159 @@
+TestSuite = {
+
+
+ -- @method - TestSuite:new()
+ -- @desc - creates a new TestSuite object that handles all the tests
+ -- @return {table} - returns the new TestSuite object
+ new = function(self)
+ local test = {
+
+ -- testsuite internals
+ modules = {},
+ module = nil,
+ testcanvas = love.graphics.newCanvas(16, 16),
+ current = 1,
+ output = '',
+ totals = {0, 0, 0},
+ time = 0,
+ xml = '',
+ html = '',
+ fakequit = false,
+ windowmode = true,
+
+ -- love modules to test
+ audio = {},
+ data = {},
+ event = {},
+ filesystem = {},
+ font = {},
+ graphics = {},
+ image = {},
+ joystick = {},
+ math = {},
+ mouse = {},
+ objects = {}, -- special for all object class contructor tests
+ physics = {},
+ sound = {},
+ system = {},
+ thread = {},
+ timer = {},
+ touch = {},
+ video = {},
+ window = {}
+
+ }
+ setmetatable(test, self)
+ self.__index = self
+ return test
+ end,
+
+
+ -- @method - TestSuite:runSuite()
+ -- @desc - called in love.update, runs through every method or every module
+ -- @param {number} delta - delta from love.update to track time elapsed
+ -- @return {nil}
+ runSuite = function(self, delta)
+
+ -- stagger 0.1s between tests
+ if self.module ~= nil then
+ self.module.timer = self.module.timer + delta
+ if self.module.timer >= self.module.delay then
+ self.module.timer = self.module.timer - self.module.delay
+ if self.module.start == true then
+
+ -- work through each test method 1 by 1
+ if self.module.index <= #self.module.running then
+
+ -- run method once
+ if self.module.called[self.module.index] == nil then
+ self.module.called[self.module.index] = true
+ local method = self.module.running[self.module.index]
+ local test = TestMethod:new(method, self.module)
+
+ -- check method exists in love first
+ if self.module.module ~= 'objects' and (love[self.module.module] == nil or love[self.module.module][method] == nil) then
+ local tested = 'love.' .. self.module.module .. '.' .. method .. '()'
+ local matching = string.sub(self.module.spacer, string.len(tested), 40)
+ self.module:log(self.module.colors['FAIL'],
+ tested .. matching,
+ ' ==> FAIL (0/0) - call failed - method does not exist'
+ )
+ -- otherwise run the test method then eval the asserts
+ else
+ local ok, chunk, err = pcall(self[self.module.module][method], test)
+ if ok == false then
+ print("FATAL", chunk, err)
+ test.fatal = tostring(chunk) .. tostring(err)
+ end
+ local ok, chunk, err = pcall(test.evaluateTest, test)
+ if ok == false then
+ print("FATAL", chunk, err)
+ test.fatal = tostring(chunk) .. tostring(err)
+ end
+ end
+ -- move onto the next test
+ self.module.index = self.module.index + 1
+ end
+
+ else
+
+ -- print module results and add to output
+ self.module:printResult()
+
+ -- if we have more modules to go run the next one
+ self.current = self.current + 1
+ if #self.modules >= self.current then
+ self.module = self.modules[self.current]
+ self.module:runTests()
+
+ -- otherwise print the final results and export output
+ else
+ self:printResult()
+ love.event.quit(0)
+ end
+
+ end
+ end
+ end
+ end
+ end,
+
+
+ -- @method - TestSuite:printResult()
+ -- @desc - prints the result of the whole test suite as well as writes
+ -- the XML + HTML of the testsuite output
+ -- @return {nil}
+ printResult = function(self)
+ local finaltime = tostring(math.floor(self.time*1000))
+ if string.len(finaltime) == 1 then finaltime = ' ' .. finaltime end
+ if string.len(finaltime) == 2 then finaltime = ' ' .. finaltime end
+ if string.len(finaltime) == 3 then finaltime = ' ' .. finaltime end
+
+ local xml = '\n'
+
+ local status = '🔴'
+ if self.totals[2] == 0 then status = '🟢' end
+ local html = '' .. status .. ' love.test
'
+ html = html ..
+ '- 🟢 ' .. tostring(self.totals[1]) .. ' Tests
' ..
+ '- 🔴 ' .. tostring(self.totals[2]) .. ' Failures
' ..
+ '- 🟡 ' .. tostring(self.totals[3]) .. ' Skipped
' ..
+ '- ' .. tostring(self.time*1000) .. 'ms
'
+
+ -- @TODO use mountFullPath to write output to src?
+ love.filesystem.createDirectory('output')
+ love.filesystem.write('output/' .. self.output .. '.xml', xml .. self.xml .. '')
+ love.filesystem.write('output/' .. self.output .. '.html', html .. self.html .. '
')
+
+ self.module:log('grey', '\nFINISHED - ' .. finaltime .. 'ms\n')
+ local failedcol = '\27[31m'
+ if self.totals[2] == 0 then failedcol = '\27[37m' end
+ self.module:log('green', tostring(self.totals[1]) .. ' PASSED' .. ' || ' .. failedcol .. tostring(self.totals[2]) .. ' FAILED || \27[37m' .. tostring(self.totals[3]) .. ' SKIPPED')
+
+ end
+
+
+}
\ No newline at end of file
diff --git a/testing/conf.lua b/testing/conf.lua
new file mode 100644
index 000000000..9f1ba5789
--- /dev/null
+++ b/testing/conf.lua
@@ -0,0 +1,24 @@
+function love.conf(t)
+ t.console = true
+ t.window.name = 'love.test'
+ t.window.width = 256
+ t.window.height = 256
+ t.window.resizable = true
+ t.renderers = {"opengl"}
+ t.modules.audio = true
+ t.modules.data = true
+ t.modules.event = true
+ t.modules.filesystem = true
+ t.modules.font = true
+ t.modules.graphics = true
+ t.modules.image = true
+ t.modules.math = true
+ t.modules.objects = true
+ t.modules.physics = true
+ t.modules.sound = true
+ t.modules.system = true
+ t.modules.thread = true
+ t.modules.timer = true
+ t.modules.video = true
+ t.modules.window = true
+end
\ No newline at end of file
diff --git a/testing/main.lua b/testing/main.lua
new file mode 100644
index 000000000..205f0b786
--- /dev/null
+++ b/testing/main.lua
@@ -0,0 +1,172 @@
+-- & 'c:\Program Files\LOVE\love.exe' ./ --console
+-- /Applications/love.app/Contents/MacOS/love ./
+
+-- load test objs
+require('classes.TestSuite')
+require('classes.TestModule')
+require('classes.TestMethod')
+
+-- create testsuite obj
+love.test = TestSuite:new()
+
+-- load test scripts if module is active
+if love.audio ~= nil then require('tests.audio') end
+if love.data ~= nil then require('tests.data') end
+if love.event ~= nil then require('tests.event') end
+if love.filesystem ~= nil then require('tests.filesystem') end
+if love.font ~= nil then require('tests.font') end
+if love.graphics ~= nil then require('tests.graphics') end
+if love.image ~= nil then require('tests.image') end
+if love.math ~= nil then require('tests.math') end
+if love.physics ~= nil then require('tests.physics') end
+if love.sound ~= nil then require('tests.sound') end
+if love.system ~= nil then require('tests.system') end
+if love.thread ~= nil then require('tests.thread') end
+if love.timer ~= nil then require('tests.timer') end
+if love.video ~= nil then require('tests.video') end
+if love.window ~= nil then require('tests.window') end
+require('tests.objects')
+
+-- love.load
+-- load given arguments and run the test suite
+love.load = function(args)
+
+ -- setup basic img to display
+ if love.window ~= nil then
+ love.window.setMode(256, 256, {
+ fullscreen = false,
+ resizable = true,
+ centered = true
+ })
+ if love.graphics ~= nil then
+ love.graphics.setDefaultFilter("nearest", "nearest")
+ love.graphics.setLineStyle('rough')
+ love.graphics.setLineWidth(1)
+ Logo = {
+ texture = love.graphics.newImage('resources/love.png'),
+ img = nil
+ }
+ Logo.img = love.graphics.newQuad(0, 0, 64, 64, Logo.texture)
+ end
+ end
+
+ -- get all args with any comma lists split out as seperate
+ local arglist = {}
+ for a=1,#args do
+ local splits = UtilStringSplit(args[a], '([^,]+)')
+ for s=1,#splits do
+ table.insert(arglist, splits[s])
+ end
+ end
+
+ -- convert args to the cmd to run, modules, method (if any) and disabled
+ local testcmd = '--runAllTests'
+ local module = ''
+ local method = ''
+ local modules = {
+ 'audio', 'data', 'event', 'filesystem', 'font', 'graphics',
+ 'image', 'math', 'objects', 'physics', 'sound', 'system',
+ 'thread', 'timer', 'video', 'window'
+ }
+ for a=1,#arglist do
+ if testcmd == '--runSpecificMethod' then
+ if module == '' and love[ arglist[a] ] ~= nil then
+ module = arglist[a]
+ table.insert(modules, module)
+ end
+ if module ~= '' and love[module][ arglist[a] ] ~= nil and method == '' then
+ method = arglist[a]
+ end
+ end
+ if testcmd == '--runSpecificModules' then
+ if love[ arglist[a] ] ~= nil or arglist[a] == 'objects' then
+ table.insert(modules, arglist[a])
+ end
+ end
+ if arglist[a] == '--runSpecificMethod' then
+ testcmd = arglist[a]
+ modules = {}
+ end
+ if arglist[a] == '--runSpecificModules' then
+ testcmd = arglist[a]
+ modules = {}
+ end
+ end
+
+ -- runSpecificMethod uses the module + method given
+ if testcmd == '--runSpecificMethod' then
+ local testmodule = TestModule:new(module, method)
+ table.insert(love.test.modules, testmodule)
+ love.test.module = testmodule
+ love.test.module:log('grey', '--runSpecificMethod "' .. module .. '" "' .. method .. '"')
+ love.test.output = 'lovetest_runSpecificMethod_' .. module .. '_' .. method
+ end
+
+ -- runSpecificModules runs all methods for all the modules given
+ if testcmd == '--runSpecificModules' then
+ local modulelist = {}
+ for m=1,#modules do
+ local testmodule = TestModule:new(modules[m])
+ table.insert(love.test.modules, testmodule)
+ table.insert(modulelist, modules[m])
+ end
+
+ love.test.module = love.test.modules[1]
+ love.test.module:log('grey', '--runSpecificModules "' .. table.concat(modulelist, '" "') .. '"')
+ love.test.output = 'lovetest_runSpecificModules_' .. table.concat(modulelist, '_')
+ end
+
+ -- otherwise default runs all methods for all modules
+ if arglist[1] == nil or arglist[1] == '' or arglist[1] == '--runAllTests' then
+ for m=1,#modules do
+ local testmodule = TestModule:new(modules[m])
+ table.insert(love.test.modules, testmodule)
+ end
+ love.test.module = love.test.modules[1]
+ love.test.module:log('grey', '--runAllTests')
+ love.test.output = 'lovetest_runAllTests'
+ end
+
+ -- invalid command
+ if love.test.module == nil then
+ print("Wrong flags used")
+ end
+
+ -- start first module
+ love.test.module:runTests()
+
+end
+
+-- love.update
+-- run test suite logic
+love.update = function(delta)
+ love.test:runSuite(delta)
+end
+
+
+-- love.draw
+-- draw a little logo to the screen
+love.draw = function()
+ love.graphics.draw(Logo.texture, Logo.img, 64, 64, 0, 2, 2)
+end
+
+
+-- love.quit
+-- add a hook to allow test modules to fake quit
+love.quit = function()
+ if love.test.module ~= nil and love.test.module.fakequit == true then
+ return true
+ else
+ return false
+ end
+end
+
+
+-- string split helper
+function UtilStringSplit(str, splitter)
+ local splits = {}
+ for word in string.gmatch(str, splitter) do
+ table.insert(splits, word)
+ end
+ return splits
+end
\ No newline at end of file
diff --git a/testing/output/lovetest_runAllTests.html b/testing/output/lovetest_runAllTests.html
new file mode 100644
index 000000000..164ade985
--- /dev/null
+++ b/testing/output/lovetest_runAllTests.html
@@ -0,0 +1 @@
+🔴 love.test
- 🟢 157 Tests
- 🔴 5 Failures
- 🟡 11 Skipped
- 7341.71ms
🟢 love.audio
- 🟢 26 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 0.40991666666712ms
| Method | Time | Details |
🟢 | getActiveEffects | 0.045083333333418ms | |
🟢 | getActiveSourceCount | 1.1385ms | |
🟢 | getDistanceModel | 0.018125000000202ms | |
🟢 | getDopplerScale | 0.012208333333374ms | |
🟢 | getEffect | 0.035250000000042ms | |
🟢 | getMaxSceneEffects | 0.0089583333331422ms | |
🟢 | getMaxSourceEffects | 0.022874999999978ms | |
🟢 | getOrientation | 0.037541666666696ms | |
🟢 | getPosition | 0.024500000000316ms | |
🟢 | getRecordingDevices | 0.039250000000157ms | |
🟢 | getVelocity | 0.021333333333207ms | |
🟢 | getVolume | 0.046083333333335ms | |
🟢 | isEffectsSupported | 0.016749999999899ms | |
🟢 | newQueueableSource | 0.041958333333314ms | |
🟢 | newSource | 2.8378333333332ms | |
🟢 | pause | 2.6265416666664ms | |
🟢 | play | 1.7924166666665ms | |
🟢 | setDistanceModel | 0.023249999999919ms | |
🟢 | setDopplerScale | 0.094375000000646ms | |
🟢 | setEffect | 0.024166666666936ms | |
🟢 | setMixWithSystem | 0.0052083333330621ms | |
🟢 | setOrientation | 0.017000000000156ms | |
🟢 | setPosition | 0.0091666666666157ms | |
🟢 | setVelocity | 0.0070416666657636ms | |
🟢 | setVolume | 0.0075833333332831ms | |
🟢 | stop | 1.787666666667ms | |
🟢 love.data
- 🟢 7 Tests
- 🔴 0 Failures
- 🟡 3 Skipped
- 0.34008333333449ms
| Method | Time | Details |
🟢 | compress | 0.39495833333403ms | |
🟢 | decode | 0.027791666666666ms | |
🟢 | decompress | 0.28366666666635ms | |
🟢 | encode | 0.044000000000377ms | |
🟡 | getPackedSize | 0.0069999999996462ms | dont understand lua packing types |
🟢 | hash | 0.12045833333341ms | |
🟢 | newByteData | 0.021916666666844ms | |
🟢 | newDataView | 0.025416666666889ms | |
🟡 | pack | 0.0070833333332132ms | dont understand lua packing types |
🟡 | unpack | 0.0091250000000542ms | dont understand lua packing types |
🟢 love.event
- 🟢 4 Tests
- 🔴 0 Failures
- 🟡 2 Skipped
- 0.47999999999884ms
| Method | Time | Details |
🟢 | clear | 0.028666666666233ms | |
🟢 | poll | 0.022374999999464ms | |
🟡 | pump | 0.0079583333327804ms | not sure we can test when its internal? |
🟢 | push | 0.022500000000036ms | |
🟢 | quit | 0.014125000000753ms | |
🟡 | wait | 0.0069166666669673ms | not sure on best way to test this |
🔴 love.filesystem
- 🟢 26 Tests
- 🔴 1 Failures
- 🟡 2 Skipped
- 1.0150000000023ms
| Method | Time | Details |
🟢 | append | 1.3135833333333ms | |
🟢 | areSymlinksEnabled | 0.01433333333356ms | |
🟢 | createDirectory | 0.39929166666663ms | |
🟢 | getAppdataDirectory | 0.014166666668203ms | |
🟢 | getCRequirePath | 0.015749999999315ms | |
🟢 | getDirectoryItems | 1.0962083333332ms | |
🟢 | getIdentity | 0.10579166666691ms | |
🟢 | getInfo | 0.9892916666665ms | |
🟢 | getRealDirectory | 0.98024999999957ms | |
🟢 | getRequirePath | 0.097583333333873ms | |
🟢 | getSaveDirectory | 0.01891666666598ms | |
🟡 | getSource | 0.020666666666003ms | not sure we can test when its internal? |
🟢 | getSourceBaseDirectory | 0.015083333334331ms | |
🟢 | getUserDirectory | 0.043666666666553ms | |
🟢 | getWorkingDirectory | 0.018791666667184ms | |
🟢 | isFused | 0.017999999998963ms | |
🟢 | lines | 13.630166666666ms | |
🟢 | load | 7.9870833333331ms | |
🟢 | mount | 0.48216666666789ms | |
🔴 | newFile | 0.37395833333331ms | assert #2 [check file made] avoiding 'nil' got 'nil' |
🟢 | newFileData | 0.030666666666512ms | |
🟢 | read | 0.19545833333368ms | |
🟢 | remove | 0.81487500000055ms | |
🟢 | setCRequirePath | 0.017416666667103ms | |
🟢 | setIdentity | 0.086666666666346ms | |
🟢 | setRequirePath | 0.014500000001583ms | |
🟡 | setSource | 0.0082916666652721ms | not sure we can test when its internal? |
🟢 | unmount | 1.1363333333341ms | |
🟢 | write | 1.0965416666666ms | |
🟢 love.font
- 🟢 4 Tests
- 🔴 0 Failures
- 🟡 1 Skipped
- 0.11233333333444ms
| Method | Time | Details |
🟡 | newBMFontRasterizer | 0.007625000002065ms | wiki and source dont match, not sure expected usage |
🟢 | newGlyphData | 0.28787499999972ms | |
🟢 | newImageRasterizer | 0.22408333333424ms | |
🟢 | newRasterizer | 0.18954166666596ms | |
🟢 | newTrueTypeRasterizer | 0.18991666666501ms | |
🔴 love.graphics
- 🟢 0 Tests
- 🔴 1 Failures
- 🟡 0 Skipped
- 0.94387500000009ms
| Method | Time | Details |
🔴 | rectangle | 3.7313333333344ms | assert #2 [check 0x,0y G] expected '1' got '0' |
🟢 love.image
- 🟢 3 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 1.2476249999999ms
| Method | Time | Details |
🟢 | isCompressed | 0.21679166666644ms | |
🟢 | newCompressedData | 0.19920833333309ms | |
🟢 | newImageData | 0.40049999999958ms | |
🟢 love.math
- 🟢 16 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 0.062999999998231ms
| Method | Time | Details |
🟢 | colorFromBytes | 0.29325000000036ms | |
🟢 | colorToBytes | 0.27899999999903ms | |
🟢 | gammaToLinear | 0.021624999998693ms | |
🟢 | getRandomSeed | 0.014708333333502ms | |
🟢 | getRandomState | 0.071791666666599ms | |
🟢 | isConvex | 0.056666666665706ms | |
🟢 | linearToGamma | 0.01791666666584ms | |
🟢 | newBezierCurve | 0.053125000000875ms | |
🟢 | newRandomGenerator | 0.019874999999558ms | |
🟢 | newTransform | 0.02287499999909ms | |
🟢 | noise | 0.076916666666094ms | |
🟢 | random | 0.17783333333377ms | |
🟢 | randomNormal | 0.020458333334972ms | |
🟢 | setRandomSeed | 0.019541666667067ms | |
🟢 | setRandomState | 0.053750000001074ms | |
🟢 | triangulate | 0.023874999998341ms | |
🟢 love.objects
- 🟢 0 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 1.6546249999983ms
🔴 love.physics
- 🟢 21 Tests
- 🔴 1 Failures
- 🟡 0 Skipped
- 0.014583333332624ms
| Method | Time | Details |
🟢 | getDistance | 0.075333333334981ms | |
🟢 | getMeter | 0.015125000000893ms | |
🟢 | newBody | 0.03362500000037ms | |
🟢 | newChainShape | 0.028624999998783ms | |
🟢 | newCircleShape | 0.020624999999441ms | |
🟢 | newDistanceJoint | 0.037833333333737ms | |
🟢 | newEdgeShape | 0.020624999999441ms | |
🟢 | newFixture | 0.077750000000876ms | |
🟢 | newFrictionJoint | 0.031708333333214ms | |
🔴 | newGearJoint | 0.12670833333139ms | test tests/physics.lua:134: Box2D assertion failed: m_bodyA->m_type == b2_dynamicBody |
🟢 | newMotorJoint | 0.092416666667816ms | |
🟢 | newMouseJoint | 0.050208333334467ms | |
🟢 | newPolygonShape | 0.025333333333322ms | |
🟢 | newPrismaticJoint | 0.034124999999108ms | |
🟢 | newPulleyJoint | 0.035041666665236ms | |
🟢 | newRectangleShape | 0.077833333332222ms | |
🟢 | newRevoluteJoint | 0.040416666667653ms | |
🟢 | newRopeJoint | 0.03037500000147ms | |
🟢 | newWeldJoint | 0.074916666664038ms | |
🟢 | newWheelJoint | 0.1102083333322ms | |
🟢 | newWorld | 0.075416666666328ms | |
🟢 | setMeter | 0.031208333336252ms | |
🟢 love.sound
- 🟢 2 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 0.93524999999842ms
| Method | Time | Details |
🟢 | newDecoder | 0.31383333333501ms | |
🟢 | newSoundData | 1.1787083333292ms | |
🟢 love.system
- 🟢 6 Tests
- 🔴 0 Failures
- 🟡 2 Skipped
- 1.3057500000059ms
| Method | Time | Details |
🟢 | getClipboardText | 1.6217916666665ms | |
🟢 | getOS | 0.034041666669538ms | |
🟢 | getPowerInfo | 0.080041666665309ms | |
🟢 | getProcessorCount | 0.017791666669709ms | |
🟢 | hasBackgroundMusic | 0.086708333334684ms | |
🟡 | openURL | 0.016333333334728ms | gets annoying to test everytime |
🟢 | setClipboardText | 0.59291666666716ms | |
🟡 | vibrate | 0.0090416666651549ms | cant really test this |
🟢 love.thread
- 🟢 3 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 0.96195833333323ms
| Method | Time | Details |
🟢 | getChannel | 0.47320833333231ms | |
🟢 | newChannel | 0.028374999999414ms | |
🟢 | newThread | 0.2556250000012ms | |
🟢 love.timer
- 🟢 6 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 3713.5796666667ms
| Method | Time | Details |
🟢 | getAverageDelta | 0.023208333331581ms | |
🟢 | getDelta | 0.015708333332753ms | |
🟢 | getFPS | 0.011125000000334ms | |
🟢 | getTime | 1001.1531666667ms | |
🟢 | sleep | 1000.4330833333ms | |
🟢 | step | 0.032958333331834ms | |
🟢 love.video
- 🟢 1 Tests
- 🔴 0 Failures
- 🟡 0 Skipped
- 0.67754166666772ms
| Method | Time | Details |
🟢 | newVideoStream | 3.4201249999981ms | |
🔴 love.window
- 🟢 32 Tests
- 🔴 2 Failures
- 🟡 1 Skipped
- 7985.3964583333ms
| Method | Time | Details |
🟢 | close | 11.641583333336ms | |
🟢 | fromPixels | 0.015333333330148ms | |
🟢 | getDPIScale | 0.014499999998918ms | |
🟢 | getDesktopDimensions | 0.015083333330779ms | |
🟢 | getDisplayCount | 0.010291666665552ms | |
🟢 | getDisplayName | 0.014875000001524ms | |
🟢 | getDisplayOrientation | 0.016250000001605ms | |
🟢 | getFullscreen | 1305.7451666667ms | |
🟢 | getFullscreenModes | 0.48033333332853ms | |
🟢 | getIcon | 2.0577083333322ms | |
🟢 | getMode | 0.10416666667012ms | |
🟢 | getPosition | 4.9660833333327ms | |
🟢 | getSafeArea | 0.067875000002715ms | |
🟢 | getTitle | 0.55745833333276ms | |
🟢 | getVSync | 0.095124999997864ms | |
🟢 | hasFocus | 0.055624999998116ms | |
🟢 | hasMouseFocus | 0.022541666666598ms | |
🟢 | isDisplaySleepEnabled | 0.14775000000355ms | |
🔴 | isMaximized | 642.02083333333ms | assert #2 [check window not maximized] expected 'true' got 'false' |
🟢 | isMinimized | 641.41475ms | |
🟢 | isOpen | 25.519625000001ms | |
🟢 | isVisible | 18.191791666666ms | |
🔴 | maximize | 0.23570833333508ms | assert #1 [check window maximized] expected 'true' got 'false' |
🟢 | minimize | 640.26066666666ms | |
🟢 | restore | 643.01916666667ms | |
🟢 | setDisplaySleepEnabled | 0.59508333333014ms | |
🟢 | setFullscreen | 1329.778375ms | |
🟢 | setIcon | 2.0223750000028ms | |
🟢 | setMode | 4.5707499999992ms | |
🟢 | setPosition | 0.16512499999521ms | |
🟢 | setTitle | 0.47262500000045ms | |
🟢 | setVSync | 0.022583333333159ms | |
🟡 | showMessageBox | 0.068958333333313ms | skipping cos annoying to test with |
🟢 | toPixels | 0.067666666666355ms | |
🟢 | updateMode | 6.8227083333348ms | |
\ No newline at end of file
diff --git a/testing/output/lovetest_runAllTests.xml b/testing/output/lovetest_runAllTests.xml
new file mode 100644
index 000000000..a30afbbc2
--- /dev/null
+++ b/testing/output/lovetest_runAllTests.xml
@@ -0,0 +1,385 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testing/readme.md b/testing/readme.md
new file mode 100644
index 000000000..4533e8c45
--- /dev/null
+++ b/testing/readme.md
@@ -0,0 +1,139 @@
+# löve.test
+Basic testing suite for the löve APIs, based off of [this issue](https://github.com/love2d/love/issues/1745)
+
+Currently written for löve 12
+
+---
+
+## Primary Goals
+- [x] Simple pass/fail tests in Lua with minimal setup
+- [x] Ability to run all tests with a simple command.
+- [x] Ability to see how many tests are passing/failing
+- [x] No platform-specific dependencies / scripts
+- [x] Ability to run a subset of tests
+- [x] Ability to easily run an individual test.
+
+---
+
+## Running Tests
+The initial pass is to keep things as simple as possible, and just run all the tests inside Löve to match how they'd be used by developers in-engine.
+To run the tests, download the repo and then run the main.lua as you would a löve game, i.e:
+
+WINDOWS: `& 'c:\Program Files\LOVE\love.exe' PATH_TO_TESTING_FOLDER --console`
+MACOS: `/Applications/love.app/Contents/MacOS/love PATH_TO_TESTING_FOLDER`
+
+By default all tests will be run for all modules.
+
+If you want to specify a module you can add:
+`--runSpecificModules filesystem`
+For multiple modules, provide a comma seperate list:
+`--runSpecificModules filesystem,audio,data"`
+
+If you want to specify only 1 specific method only you can use:
+`--runSpecificMethod filesystem write`
+
+All results will be printed in the console per method as PASS, FAIL, or SKIP with total assertions met on a module level and overall level.
+
+An `XML` file in the style of [JUnit XML](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format) will be generated in your save directory, along with a `HTML` file with a summary of all tests (including visuals for love.graphics tests).
+> Note that this can only be viewed properly locally as the generated images are written to the save directory.
+> An example of both types of output can be found in the `/output` folder
+
+---
+
+## Architecture
+Each method has it's own test method written in `/tests` under the matching module name.
+
+When you run the tests, a single TestSuite object is created which handles the progress + totals for all the tests.
+Each module has a TestModule object created, and each test method has a TestMethod object created which keeps track of assertions for that method. You can currently do the following assertions:
+- **assertEquals**(expected, actual)
+- **assertNotEquals**(expected, actual)
+- **assertRange**(actual, min, max)
+- **assertMatch**({option1, option2, option3 ...}, actual)
+- **assertGreaterEqual**(expected, actual)
+- **assertLessEqual**(expected, actual)
+- **assertObject**(table)
+
+Example test method:
+```lua
+-- love.filesystem.read test method
+-- all methods should be put under love.test.MODULE.METHOD, matching the API
+love.test.filesystem.read = function(test)
+ -- setup any data needed then run any asserts using the passed test object
+ local content, size = love.filesystem.read('resources/test.txt')
+ test:assertNotEquals(nil, content, 'check not nil')
+ test:assertEquals('helloworld', content, 'check content match')
+ test:assertEquals(10, size, 'check size match')
+ content, size = love.filesystem.read('resources/test.txt', 5)
+ test:assertNotEquals(nil, content, 'check not nil')
+ test:assertEquals('hello', content, 'check content match')
+ test:assertEquals(5, size, 'check size match')
+ -- no need to return anything just cleanup any objs if needed
+end
+```
+
+After each test method is ran, the assertions are totalled up, printed, and we move onto the next method! Once all methods in the suite are run a total pass/fail/skip is given for that module and we move onto the next module (if any)
+
+For sanity-checking, if it's currently not covered or we're not sure how to test yet we can set the test to be skipped with `test:skipTest(reason)` - this way we still see the method listed in the tests without it affected the pass/fail totals
+
+---
+
+## Coverage
+This is the status of all module tests currently.
+"objects" is a special module to cover any object specific tests, i.e. testing a File object functions as expected
+```lua
+-- [x] audio 26 PASSED | 0 FAILED | 0 SKIPPED
+-- [x] data 7 PASSED | 0 FAILED | 3 SKIPPED [SEE BELOW]
+-- [x] event 4 PASSED | 0 FAILED | 2 SKIPPED [SEE BELOW]
+-- [x] filesystem 26 PASSED | 1 FAILED | 2 SKIPPED [SEE BELOW]
+-- [x] font 4 PASSED | 0 FAILED | 1 SKIPPED [SEE BELOW]
+-- [ ] graphics STILL TO BE DONE
+-- [x] image 3 PASSED | 0 FAILED | 0 SKIPPED
+-- [x] math 16 PASSED | 0 FAILED | 0 SKIPPED [SEE BELOW]
+-- [x] physics 21 PASSED | 1 FAILED | 0 SKIPPED [SEE BELOW]
+-- [x] sound 2 PASSED | 0 FAILED | 0 SKIPPED
+-- [x] system 7 PASSED | 0 FAILED | 1 SKIPPED
+-- [ ] thread 3 PASSED | 0 FAILED | 0 SKIPPED
+-- [x] timer 6 PASSED | 0 FAILED | 0 SKIPPED [SEE BELOW]
+-- [x] video 1 PASSED | 0 FAILED | 0 SKIPPED
+-- [x] window 32 PASSED | 2 FAILED | 1 SKIPPED [SEE BELOW]
+-- [ ] objects STILL TO BE DONE
+```
+
+The following modules are not covered as we can't really emulate input nicely:
+`joystick`, `keyboard`, `mouse`, and `touch`
+
+---
+
+## Todo / Skipped
+Modules with some small bits needed or needing sense checking:
+- **love.data** - packing methods need writing cos i dont really get what they are
+- **love.event** - love.event.wait or love.event.pump need writing if possible I dunno how to check
+- **love.filesystem** - getSource() / setSource() dont think we can test
+- **love.font** - newBMFontRasterizer() wiki entry is wrong so not sure whats expected
+- **love.timer** - couple methods I don't know if you could reliably test specific values
+- **love.image** - ideally isCompressed should have an example of all compressed files love can take
+- **love.math** - linearToGamma + gammaToLinear using direct formulas don't get same value back
+- **love.window** - couple stuff just nil checked as I think it's hardware dependent, needs checking
+
+Modules still to be completed or barely started
+- **love.graphics** - done 1 as an example of how we can test the drawing but not really started
+- **love.objects** - done 1 as an example of how we can test objs with mini scenarios
+
+---
+
+## Failures
+- **love.window.isMaximized()** - returns false after calling love.window.maximize?
+- **love.window.maximize()** - same as above
+- **love.filesystem.newFile()** - something changed in 12
+- **love.physics.newGearJoint()** - something changed in 12
+
+---
+
+## Stretch Goals
+- [ ] Tests can compare visual results to a reference image
+- [ ] Ability to see all visual results at a glance
+- [ ] Automatic testing that happens after every commit
+- [ ] Ability to test loading different combinations of modules
+- [ ] Performance tests
+
+There is some unused code in the Test.lua class to add preview vs actual images to the HTML output
\ No newline at end of file
diff --git a/testing/resources/click.ogg b/testing/resources/click.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..49707b3e817594afb36299302c2b80821a0d7f02
GIT binary patch
literal 7824
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
Mi>!|s@_-or2aq-L^8f$<
literal 0
HcmV?d00001
diff --git a/testing/resources/font.ttf b/testing/resources/font.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..0ff4bf7d387cf3eb60bc34fcfb3298c8a181b4a2
GIT binary patch
literal 10390
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
J-`%Ix{tX`1!SMh9
literal 0
HcmV?d00001
diff --git a/testing/resources/love.dxt1 b/testing/resources/love.dxt1
new file mode 100644
index 0000000000000000000000000000000000000000..88cb9e6890bafb1574f69e997c51c811386fa3c9
GIT binary patch
literal 2872
zcmcIl&ubJh6wd4zY!`$=8N7(};zjh}e-Lp+iU@&D+R~x2N9kWsw1@c%ym?YeK?-)I
zmpyp1%+33h?Mc|Pc-K7$@qGzzcN_P|bh-vIA9=}p-qe<(QR@5YOEnBw~!Wbz$>RK`CIDs#bk?8+^
zP*`e=u%}wp30cW!8ZYxRE?P%bUgl@Ku8z9ABmT@#H7k6taxT+&t?ibE&K-zTc4G?c
zkO~4Av7PFmTV_V<)C~H6R#7MObj;#SB;s3(LaC0KNwrqY&lu`(yhOlx3(@T>ifZvI
zYRns=(?J5kFJ+R1ygc^&QdZwL=P5HvX&v(N82ofTDRPPi9U_wd=m39`7z4zL`8p6;
zP+d^|Q!_mOabBVHkeAk8YO{36#Pob6P0}&qrLwy+?&Gq+D_`~nc{+<(&11<
ziAR+>r0Hw(hC1A?=v+8|Rncc-dY(g`dE=@&7R?*#nx^A_^G?(ujd8r%pyn6SPD
z36P#!SBF}8B>T*JGw#Rl2P5Jk&c%Bde;6m^>(f0q=G{HqcVk``9#3Ou?_CVAU&H}I
z59?$eYgf;g<2bhV(9$w`uMe&NFTB}p7e&GK%W>e3hiBy9E#qjJ`1u`U%uJ!$+vEOp
zFWvi@kPwc8dz$C%m+qZtJnT8l_qXnTqxXo`x$`1FAjV)vLtYTa&qtDZ7^nGp@6i|Q
z#^cQM3-gaL^84kZ&+j>nQ3viEe#8xH%hLSJ^K93z!#whb`9^%}EB2cg`&-N#?u+d}
x?7vTc)#H;+6S%Y2IYPFZ_fL+2N1yg~Ts~hNY;JzP^LFsd+B19m!{<(?^B2-vj_CjZ
literal 0
HcmV?d00001
diff --git a/testing/resources/love.png b/testing/resources/love.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2612a857c850afa83db824f1b5a30d226508df0
GIT binary patch
literal 680
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
t3#NCinwTS%D6GI62Y@o*k)V9v
z?E4g*X$Ude9ZUgM=gsJ_xj&A3H#|UWN+KQm_iaYlBmuD|I5#;r%!*zJ&A{YG2C?c&YXeON%_
zY`YD-SlMnOHjwl-@0};^a--CFj*)ct52XRlbpJ0f@}DNN98}wG^8VG)ME7=07w9WS
z!6;VJ**<9m-Lcj
zCHs6-`2=p=GDQ5hD3fy>>zpu9_q1p^m=9cMA)%nbt=1%t%T@chCMwbmdT
z|H2}Frj8Ji1eA3D>0gAJ=QaK7{{kyDrH@qA)jV_ENDBI7!#O<%ekjLVF@NG?#%n|m
z;SuEU4!)B~^f`Riu5^3`QG55x&bRt|4zEien;34nQ#u#ld&Kq?iv@D<1{Qr~YRc;%
zQ>d_9LT9^tgjM)en$~6`>Zo#b@0C`oD0oK#pAJIQqf?DuMuXF)7F+(Lgj$729Ra_<
zu^DJgLDX@FH3B3B4hnTU7;;4K!*Ul;I6#hTGY+#swncLaM5TM;nY_#UT?4aHXE1p@$HxmnOB4mjk6FA)aY&;F}84LOds91?b
z(Zw?wV{_+H3(j3Lxw|nOa7cHrj0FmVj6tT=)z@uJI2wG>-3@T4?gpUQ$IrxW3p9TF
z)?-o<{O#Mfj$a4C*?tfJkLPMv`4qLlh<z$tWZMVM?fs$9yXYWZ%uNOjf%_
z0ythh2KeIga?6LgwVCDR>MM`_<&;tA&*_1*;Bn5YGbGYRy5e
z-!n09dgDF`M4EGRgM%h1L)#GIUKVe*_HPtTJ^uk6AzH`+r(z=)k&9k
zmwDOQ_j*=Mx^80r;7)j_k>-N@)JFH@&dIxHT<^94o;G&qj!CXn)KW&!>d}s{hIc2n
zGA^K+3K|9!zv$~5KF@foS?B#WGcyaE;NNBzR8Lh_2LZ@P;*9=8j07koKq*^m033j5
zH(-r`?75^u;BlhJ+@)$#+gux)Je+uYC^64B6mF=U`h7VP=@zE|Ky8^jz;XHZ4se6)
z1f)A9Lke$0=7k$t#L}qm%xog3Bg;=ZG-mPe(L)hVnGs9N_GHb1bUt8#Jd~bSdL7X|
z6WWFV6n6)`Ss;R9RJUPb9?I|jo4V;YKASL``AaP>`7MH+BgJW_k908nr=cTb%e^<;
z)D3bgA+8$Z8c%%_s<>`mO86$z)S?r3_tMHp^i(@K5`21n0BiIC{81UgiNCN|e-fw#
zK0u8b%FA~Z*E>m*oiP)zK>ruJiNvq}!J7~MuU@@ci3$h^4hT5k*Eg{D$(@Vafz?P^
z@JC>HsTuw+wymvAEetI@!4PzWpidxCJCL3hu0kOIS?mG0yZ6+9hi1%rbR6FpV9kk>T}ui_%2w8aB?EEl
z9WMir$L`eBYw7PgxaNUyyRgjQ2>xJ?-d})WA^<%o+&^*WA6NY9@=)4;11$H5JYPrc
zvoyv?e7CV-vBz87Z0h`){EzC_Uv{0BZHW~^)^qCgtJB-a
zwWGnUGqs{=I#nT_D7b{Ta*)I9yA;tz5gAo}&5d@KY5t_`e(A0&2KprAXm7tm9)F^c
zW4>M)t8bR>!~sPRy*j0-iPg1R8DOPebE)Rc2HQ9E}#-pkS|?m}-1C+DJEuOx3y`ciLi-Tg(|{VV&-J@5-v
z@9>A0G5G|0LiZ`%+!4&-QA3?+Pl9$8fPqA8Ob_
z_Z$^zW>U?qJ3d=
zB_41_@NKgL5y6tkB8w!Z0E%RT9wmT5LVytvNq`#xk%Ty-Dg^+OAP+*3q(DG9s`2aA
zty|mMU$?~jciZGN#eej+29MCSM*kAG0BGQz7-TBY3}l0bt{wWs-!C!#|ML>#ZkV&_?spEW{JO9;%}Dtnvw95nqO+5Wn}2PNDZA1r^E);c&+P1-#_h+W-In
literal 0
HcmV?d00001
diff --git a/testing/resources/test.txt b/testing/resources/test.txt
new file mode 100644
index 000000000..620ffd0fd
--- /dev/null
+++ b/testing/resources/test.txt
@@ -0,0 +1 @@
+helloworld
\ No newline at end of file
diff --git a/testing/resources/test.zip b/testing/resources/test.zip
new file mode 100644
index 0000000000000000000000000000000000000000..46cfaba9798cb68e849b069e89f9d12887c24f17
GIT binary patch
literal 150
zcmWIWW@h1H0D%p8=HXxll;8l;C8@
Date: Wed, 4 Oct 2023 21:59:31 +0100
Subject: [PATCH 02/54] Update readme.md
---
testing/readme.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/testing/readme.md b/testing/readme.md
index 4533e8c45..5740a6b4e 100644
--- a/testing/readme.md
+++ b/testing/readme.md
@@ -124,8 +124,8 @@ Modules still to be completed or barely started
## Failures
- **love.window.isMaximized()** - returns false after calling love.window.maximize?
- **love.window.maximize()** - same as above
-- **love.filesystem.newFile()** - something changed in 12
- **love.physics.newGearJoint()** - something changed in 12
+- **love.objects.File()** - dont think I understand the buffering system
---
From e03b2db08bce57f276ce61fb4169e3cc84984348 Mon Sep 17 00:00:00 2001
From: ell <77150506+ellraiser@users.noreply.github.com>
Date: Thu, 5 Oct 2023 23:03:32 +0100
Subject: [PATCH 03/54] 0.2
- Added tests for all obj creation, transformation, window + system info graphics methods
- Added half the state methods for graphics + added placeholders for missing drawing methods
- Added TestMethod:assertNotNil() for quick nil checking
- Added time total to the end of each module summary in console log to match file output
- Removed a bunch of unessecary nil checks
- Removed :release() from test methods, collectgarbage("collect") is called between methods instead
- Renamed /output to /examples to avoid confusion
- Replaced love.filesystem.newFile with love.filesystem.openFile
- Replaced love.math.noise with love.math.perlinNoise / love.math.simplexNoise
- Fixed newGearJoint throwing an error in 12 as body needs to be dynamic not static now
- Some general cleanup, incl. better comments and time format in file output
---
testing/classes/TestMethod.lua | 26 +-
testing/classes/TestModule.lua | 11 +-
testing/classes/TestSuite.lua | 18 +-
testing/conf.lua | 2 +-
testing/examples/lovetest_runAllTests.html | 1 +
testing/examples/lovetest_runAllTests.xml | 578 +++++++++++
testing/main.lua | 8 +-
testing/output/lovetest_runAllTests.html | 1 -
testing/output/lovetest_runAllTests.xml | 385 --------
testing/readme.md | 32 +-
testing/resources/cubemap.png | Bin 0 -> 27634 bytes
testing/resources/love2.png | Bin 0 -> 680 bytes
testing/resources/love3.png | Bin 0 -> 680 bytes
testing/tests/audio.lua | 109 +--
testing/tests/data.lua | 48 +-
testing/tests/event.lua | 22 +-
testing/tests/filesystem.lua | 121 ++-
testing/tests/font.lua | 19 +-
testing/tests/graphics.lua | 1022 +++++++++++++++++++-
testing/tests/image.lua | 19 +-
testing/tests/math.lua | 75 +-
testing/tests/objects.lua | 26 +-
testing/tests/physics.lua | 103 +-
testing/tests/sound.lua | 11 +-
testing/tests/system.lua | 32 +-
testing/tests/thread.lua | 14 +-
testing/tests/timer.lua | 10 +-
testing/tests/video.lua | 6 +-
testing/tests/window.lua | 111 ++-
testing/todo.md | 26 +
30 files changed, 1976 insertions(+), 860 deletions(-)
create mode 100644 testing/examples/lovetest_runAllTests.html
create mode 100644 testing/examples/lovetest_runAllTests.xml
delete mode 100644 testing/output/lovetest_runAllTests.html
delete mode 100644 testing/output/lovetest_runAllTests.xml
create mode 100644 testing/resources/cubemap.png
create mode 100644 testing/resources/love2.png
create mode 100644 testing/resources/love3.png
create mode 100644 testing/todo.md
diff --git a/testing/classes/TestMethod.lua b/testing/classes/TestMethod.lua
index b0c763082..8fa55080f 100644
--- a/testing/classes/TestMethod.lua
+++ b/testing/classes/TestMethod.lua
@@ -189,14 +189,23 @@ TestMethod = {
-- @param {table} obj - table to check is a valid love object
-- @return {nil}
assertObject = function(self, obj)
- self:assertNotEquals(nil, obj, 'check not nill')
+ self:assertNotNil(obj)
self:assertEquals('userdata', type(obj), 'check is userdata')
- if obj ~= nil then
+ if obj ~= nil then
self:assertNotEquals(nil, obj:type(), 'check has :type()')
end
end,
+ -- @method - TestMethod:assertNotNil()
+ -- @desc - quick assert for value not nil
+ -- @param {any} value - value to check not nil
+ -- @return {nil}
+ assertNotNil = function (self, value)
+ self:assertNotEquals(nil, value, 'check not nil')
+ end,
+
+
-- @method - TestMethod:skipTest()
-- @desc - used to mark this test as skipped for a specific reason
@@ -289,10 +298,7 @@ TestMethod = {
self.finish = love.timer.getTime() - self.start
love.test.time = love.test.time + self.finish
self.testmodule.time = self.testmodule.time + self.finish
- local endtime = tostring(math.floor((love.timer.getTime() - self.start)*1000))
- if string.len(endtime) == 1 then endtime = ' ' .. endtime end
- if string.len(endtime) == 2 then endtime = ' ' .. endtime end
- if string.len(endtime) == 3 then endtime = ' ' .. endtime end
+ local endtime = UtilTimeFormat(love.timer.getTime() - self.start)
-- get failure/skip message for output (if any)
local failure = ''
@@ -309,7 +315,7 @@ TestMethod = {
-- append XML for the test class result
self.testmodule.xml = self.testmodule.xml .. '\t\t\n' ..
+ '" time="' .. endtime .. '">\n' ..
failure .. '\t\t\n'
-- unused currently, adds a preview image for certain graphics methods to the output
@@ -329,7 +335,7 @@ TestMethod = {
'' ..
'' .. status .. ' | ' ..
'' .. self.method .. ' | ' ..
- '' .. tostring(self.finish*1000) .. 'ms | ' ..
+ '' .. endtime .. 's | ' ..
'' .. output .. preview .. ' | ' ..
'
'
@@ -350,10 +356,10 @@ TestMethod = {
self.testmodule:log(
self.testmodule.colors[self.result.result],
' ' .. tested .. matching,
- ' ==> ' .. self.result.result .. ' - ' .. endtime .. 'ms ' ..
+ ' ==> ' .. self.result.result .. ' - ' .. endtime .. 's ' ..
self.result.total .. msg
)
end
-}
\ No newline at end of file
+}
diff --git a/testing/classes/TestModule.lua b/testing/classes/TestModule.lua
index 379aac360..9ee6cd62e 100644
--- a/testing/classes/TestModule.lua
+++ b/testing/classes/TestModule.lua
@@ -83,12 +83,13 @@ TestModule = {
-- the XML + HTML for the test to the testsuite output
-- @return {nil}
printResult = function(self)
+ local finaltime = UtilTimeFormat(self.time)
-- add xml to main output
love.test.xml = love.test.xml .. '\t\n' .. self.xml .. '\t\n'
+ '" time="' .. finaltime .. '">\n' .. self.xml .. '\t\n'
-- add html to main output
local status = '🔴'
if self.failed == 0 then status = '🟢' end
@@ -96,8 +97,8 @@ TestModule = {
'- 🟢 ' .. tostring(self.passed) .. ' Tests
' ..
'- 🔴 ' .. tostring(self.failed) .. ' Failures
' ..
'- 🟡 ' .. tostring(self.skipped) .. ' Skipped
' ..
- '- ' .. tostring(self.time*1000) .. 'ms
' .. '
' ..
- ' | Method | Time | Details |
' ..
+ '- ' .. finaltime .. 's
' .. '
' ..
+ ' | Method | Time | Details |
' ..
self.html .. '
'
-- print module results to console
self:log('yellow', 'love.' .. self.module .. '.testmodule.end')
@@ -105,10 +106,10 @@ TestModule = {
if self.failed == 0 then failedcol = '\27[37m' end
self:log('green', tostring(self.passed) .. ' PASSED' .. ' || ' ..
failedcol .. tostring(self.failed) .. ' FAILED || \27[37m' ..
- tostring(self.skipped) .. ' SKIPPED')
+ tostring(self.skipped) .. ' SKIPPED || ' .. finaltime .. 's')
self.start = false
self.fakequit = false
end
-}
\ No newline at end of file
+}
diff --git a/testing/classes/TestSuite.lua b/testing/classes/TestSuite.lua
index 234019c8f..9552b5aee 100644
--- a/testing/classes/TestSuite.lua
+++ b/testing/classes/TestSuite.lua
@@ -10,7 +10,7 @@ TestSuite = {
-- testsuite internals
modules = {},
module = nil,
- testcanvas = love.graphics.newCanvas(16, 16),
+ testcanvas = nil,
current = 1,
output = '',
totals = {0, 0, 0},
@@ -91,6 +91,9 @@ TestSuite = {
test.fatal = tostring(chunk) .. tostring(err)
end
end
+ -- save having to :release() anything we made in the last test
+ -- 7251ms > 7543ms
+ collectgarbage("collect")
-- move onto the next test
self.module.index = self.module.index + 1
end
@@ -124,15 +127,12 @@ TestSuite = {
-- the XML + HTML of the testsuite output
-- @return {nil}
printResult = function(self)
- local finaltime = tostring(math.floor(self.time*1000))
- if string.len(finaltime) == 1 then finaltime = ' ' .. finaltime end
- if string.len(finaltime) == 2 then finaltime = ' ' .. finaltime end
- if string.len(finaltime) == 3 then finaltime = ' ' .. finaltime end
+ local finaltime = UtilTimeFormat(self.time)
local xml = '\n'
+ '" time="' .. finaltime .. '">\n'
local status = '🔴'
if self.totals[2] == 0 then status = '🟢' end
@@ -141,14 +141,14 @@ TestSuite = {
'- 🟢 ' .. tostring(self.totals[1]) .. ' Tests
' ..
'- 🔴 ' .. tostring(self.totals[2]) .. ' Failures
' ..
'- 🟡 ' .. tostring(self.totals[3]) .. ' Skipped
' ..
- '- ' .. tostring(self.time*1000) .. 'ms
'
+ '- ' .. finaltime .. 's
'
-- @TODO use mountFullPath to write output to src?
love.filesystem.createDirectory('output')
love.filesystem.write('output/' .. self.output .. '.xml', xml .. self.xml .. '')
love.filesystem.write('output/' .. self.output .. '.html', html .. self.html .. '