1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2024-12-12 10:04:40 +02:00

Merge pull request #1092 from grilledham/test_framework

Test framework
This commit is contained in:
grilledham 2020-10-04 12:11:57 +01:00 committed by GitHub
commit 4b78d9eae3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2062 additions and 3 deletions

View File

@ -169,6 +169,10 @@ if _DUMP_ENV then
require 'utils.dump_env'
end
if _DEBUG then
require('utils.test.main')
end
-- Needs to be at bottom so tokens are registered last.
if _DEBUG then
require 'features.gui.debug.command'

View File

@ -56,9 +56,7 @@ local function can_select_landfill_tiles(cursor, surface, area)
entity_filters[#entity_filters + 1] = 'character'
end
if surface.count_entities_filtered({area = area, name = entity_filters, invert = invert, limit = 1}) > 0 then
return false
end
return surface.count_entities_filtered({area = area, name = entity_filters, invert = invert, limit = 1}) == 0
end
local function within_reach(tile_position, player_position, radius_squared)

View File

@ -0,0 +1,701 @@
local Declare = require 'utils.test.declare'
local EventFactory = require 'utils.test.event_factory'
local Assert = require 'utils.test.assert'
local Helper = require 'utils.test.helper'
local main_inventory = defines.inventory.character_main
local config = global.config.landfill_remover
local tile_items = {
'stone-brick',
'concrete',
'hazard-concrete',
'refined-concrete',
'refined-hazard-concrete'
}
Declare.module(
{'features', 'landfill remover'},
function()
local teardown
Declare.module_startup(
function(context)
teardown = Helper.startup_test_surface(context)
end
)
Declare.module_teardown(
function()
teardown()
end
)
local function setup_player_with_default_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_valid_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.tile_selection_mode = defines.deconstruction_item.tile_selection_mode.only
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_tile_filter_whitelist_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.tile_filter_mode = defines.deconstruction_item.tile_filter_mode.whitelist
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_tile_filter_blacklist_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.tile_filter_mode = defines.deconstruction_item.tile_filter_mode.blacklist
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_normal_selection_mode_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.tile_selection_mode = defines.deconstruction_item.tile_selection_mode.normal
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_never_selection_mode_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.tile_selection_mode = defines.deconstruction_item.tile_selection_mode.never
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_always_selection_mode_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.tile_selection_mode = defines.deconstruction_item.tile_selection_mode.always
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_no_landfill_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.tile_selection_mode = defines.deconstruction_item.tile_selection_mode.only
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_trees_and_rocks_only_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.trees_and_rocks_only = true
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_entity_filter_whitelist_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.set_entity_filter(1, 'iron-chest')
stack.entity_filter_mode = defines.deconstruction_item.entity_filter_mode.whitelist
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
local function setup_player_with_entity_filter_blacklist_deconstruction_planner(player)
local inventory = player.get_inventory(main_inventory)
inventory.clear()
inventory.insert('deconstruction-planner')
local stack = inventory.find_item_stack('deconstruction-planner')
stack.set_tile_filter(1, 'landfill')
stack.set_entity_filter(1, 'iron-chest')
stack.entity_filter_mode = defines.deconstruction_item.entity_filter_mode.blacklist
local cursor = player.cursor_stack
cursor.set_stack(stack)
return cursor
end
Declare.test(
'can remove landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local cursor = setup_player_with_valid_deconstruction_planner(player)
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal(config.revert_tile, tile.name)
end
)
for _, item_name in pairs(tile_items) do
Declare.test(
'can remove landfill when covered by ' .. item_name,
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
-- Place covering tile.
local cursor = player.cursor_stack
cursor.set_stack(item_name)
player.build_from_cursor({position = position, terrain_building_size = 1})
cursor = setup_player_with_valid_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal(config.revert_tile, tile.name)
end
)
end
Declare.test(
'does not remove landfill when entity present',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
-- Place entity.
local cursor = player.cursor_stack
cursor.set_stack('iron-chest')
player.build_from_cursor({position = position})
cursor = setup_player_with_valid_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal('landfill', tile.name)
local entities = surface.find_entities(area)
local entity = entities[1]
Assert.is_lua_object_with_name(entity, 'iron-chest', 'iron-chest was not valid.')
entity.destroy()
end
)
for _, item_name in pairs(tile_items) do
Declare.test(
'does not remove covered by ' .. item_name .. ' landfill when entity present',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
-- Place covering tile.
local cursor = player.cursor_stack
cursor.set_stack(item_name)
player.build_from_cursor({position = position, terrain_building_size = 1})
local before_tile = surface.get_tile(position[1], position[2])
-- Place entity.
cursor.set_stack('iron-chest')
player.build_from_cursor({position = position})
cursor = setup_player_with_valid_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal(before_tile.name, tile.name)
local entities = surface.find_entities(area)
local entity = entities[1]
Assert.is_lua_object_with_name(entity, 'iron-chest', 'iron-chest was not valid.')
entity.destroy()
end
)
end
Declare.test(
'does not remove landfill when out of reach',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local build_distance = player.build_distance + 5
local position = {build_distance, build_distance}
local area = {
{build_distance + 0.1, build_distance + 0.1},
{build_distance + 0.9, build_distance + 0.9}
}
surface.set_tiles({{name = 'landfill', position = position}})
local cursor = setup_player_with_valid_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal('landfill', tile.name)
end
)
for _, item_name in pairs(tile_items) do
Declare.test(
'does not remove landfill when out of reach and covered by ' .. item_name,
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local build_distance = player.build_distance + 5
local position = {build_distance, build_distance}
local area = {
{build_distance + 0.1, build_distance + 0.1},
{build_distance + 0.9, build_distance + 0.9}
}
surface.set_tiles({{name = 'landfill', position = position}})
-- Place covering tile.
local cursor = player.cursor_stack
cursor.set_stack(item_name)
player.build_from_cursor({position = position, terrain_building_size = 1})
local before_tile = surface.get_tile(position[1], position[2])
cursor = setup_player_with_valid_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal(before_tile.name, tile.name)
end
)
end
Declare.test(
'does not remove landfill when trees and rocks only',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
local cursor = setup_player_with_trees_and_rocks_only_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal('landfill', tile.name)
end
)
Declare.test(
'does not remove landfill when default deconstruction planner',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
local cursor = setup_player_with_default_deconstruction_planner(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal('landfill', tile.name)
end
)
local tile_mode_test_cases = {
{
name = 'only',
setup = setup_player_with_valid_deconstruction_planner,
should_remove = true
},
{
name = 'normal',
setup = setup_player_with_normal_selection_mode_deconstruction_planner,
should_remove = true
},
{
name = 'always',
setup = setup_player_with_always_selection_mode_deconstruction_planner,
should_remove = true
},
{
name = 'never',
setup = setup_player_with_never_selection_mode_deconstruction_planner,
should_remove = false
},
{
name = 'no landfill',
setup = setup_player_with_no_landfill_deconstruction_planner,
should_remove = false
}
}
for _, test_case in pairs(tile_mode_test_cases) do
Declare.test(
'tile mode ' ..
test_case.name .. ' should ' .. (test_case.should_remove and '' or 'not ') .. 'remove landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local cursor = test_case.setup(player)
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
local expected_tile = test_case.should_remove and config.revert_tile or 'landfill'
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal(expected_tile, tile.name)
end
)
end
local tile_filter_test_cases = {
{
name = 'whitelist',
setup = setup_player_with_tile_filter_whitelist_deconstruction_planner,
should_remove = true
},
{
name = 'blacklist',
setup = setup_player_with_tile_filter_blacklist_deconstruction_planner,
should_remove = false
}
}
for _, test_case in pairs(tile_filter_test_cases) do
Declare.test(
'tile filter ' ..
test_case.name .. ' should ' .. (test_case.should_remove and '' or 'not ') .. 'remove landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local cursor = test_case.setup(player)
local position = {2, 2}
local area = {{2.1, 2.1}, {2.9, 2.9}}
surface.set_tiles({{name = 'landfill', position = position}})
local expected_tile = test_case.should_remove and config.revert_tile or 'landfill'
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position[1], position[2])
Assert.equal(expected_tile, tile.name)
end
)
end
local tile_mode_with_entity_test_cases = {
{
name = 'only',
setup = setup_player_with_valid_deconstruction_planner,
should_remove = true
},
{
name = 'normal',
setup = setup_player_with_normal_selection_mode_deconstruction_planner,
should_remove = false
},
{
name = 'always',
setup = setup_player_with_always_selection_mode_deconstruction_planner,
should_remove = true
},
{
name = 'never',
setup = setup_player_with_never_selection_mode_deconstruction_planner,
should_remove = false
},
{
name = 'no landfill',
setup = setup_player_with_no_landfill_deconstruction_planner,
should_remove = false
}
}
for _, test_case in pairs(tile_mode_with_entity_test_cases) do
Declare.test(
'tile mode ' ..
test_case.name ..
' with entity should ' .. (test_case.should_remove and '' or 'not ') .. 'remove landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position1 = {2, 2}
local position2 = {3, 2}
local area = {{2.1, 2.1}, {3.9, 2.9}}
surface.set_tiles(
{{name = 'landfill', position = position1}, {name = 'landfill', position = position2}}
)
local expected_tile = test_case.should_remove and config.revert_tile or 'landfill'
-- Place entity.
local cursor = player.cursor_stack
cursor.set_stack('iron-chest')
player.build_from_cursor({position = position1})
cursor = test_case.setup(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position2[1], position2[2])
Assert.equal(expected_tile, tile.name)
local entities = surface.find_entities(area)
local entity = entities[1]
Assert.is_lua_object_with_name(entity, 'iron-chest', 'iron-chest was not valid.')
entity.destroy()
end
)
end
local tile_filter_with_entity_test_cases = {
{
name = 'whitelist',
setup = setup_player_with_tile_filter_whitelist_deconstruction_planner,
should_remove = false
},
{
name = 'blacklist',
setup = setup_player_with_tile_filter_blacklist_deconstruction_planner,
should_remove = false
}
}
for _, test_case in pairs(tile_filter_with_entity_test_cases) do
Declare.test(
'tile mode ' ..
test_case.name ..
' with entity should ' .. (test_case.should_remove and '' or 'not ') .. 'remove landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position1 = {2, 2}
local position2 = {3, 2}
local area = {{2.1, 2.1}, {3.9, 2.9}}
surface.set_tiles(
{{name = 'landfill', position = position1}, {name = 'landfill', position = position2}}
)
local expected_tile = test_case.should_remove and config.revert_tile or 'landfill'
-- Place entity.
local cursor = player.cursor_stack
cursor.set_stack('iron-chest')
player.build_from_cursor({position = position1})
cursor = test_case.setup(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position2[1], position2[2])
Assert.equal(expected_tile, tile.name)
local entities = surface.find_entities(area)
local entity = entities[1]
Assert.is_lua_object_with_name(entity, 'iron-chest', 'iron-chest was not valid.')
entity.destroy()
end
)
end
local entity_filter_with_entity_test_cases = {
{
name = 'whitelist',
setup = setup_player_with_entity_filter_whitelist_deconstruction_planner,
should_remove = false
},
{
name = 'blacklist',
setup = setup_player_with_entity_filter_blacklist_deconstruction_planner,
should_remove = true
}
}
for _, test_case in pairs(entity_filter_with_entity_test_cases) do
Declare.test(
'entity filter ' ..
test_case.name ..
' with entity should ' .. (test_case.should_remove and '' or 'not ') .. 'remove landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local position1 = {2, 2}
local position2 = {3, 2}
local area = {{2.1, 2.1}, {3.9, 2.9}}
surface.set_tiles(
{{name = 'landfill', position = position1}, {name = 'landfill', position = position2}}
)
local expected_tile = test_case.should_remove and config.revert_tile or 'landfill'
-- Place entity.
local cursor = player.cursor_stack
cursor.set_stack('iron-chest')
player.build_from_cursor({position = position1})
cursor = test_case.setup(player)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
local tile = surface.get_tile(position2[1], position2[2])
Assert.equal(expected_tile, tile.name)
local entities = surface.find_entities(area)
local entity = entities[1]
Assert.is_lua_object_with_name(entity, 'iron-chest', 'iron-chest was not valid.')
entity.destroy()
end
)
end
Declare.test(
'ignore character when removing landfill',
function(context)
-- Arrange
local player = context.player
local surface = player.surface
local cursor = setup_player_with_valid_deconstruction_planner(player)
local positions = {
{-1, -1},
{-1, 0},
{0, -1},
{0, 0}
}
local area = {{-1.5, -1.5}, {0.5, 0.5}}
surface.set_tiles(
{
{name = 'landfill', position = positions[1]},
{name = 'landfill', position = positions[2]},
{name = 'landfill', position = positions[3]},
{name = 'landfill', position = positions[4]}
}
)
-- Act
EventFactory.do_player_deconstruct_area(cursor, player, area)
-- Assert
for _, pos in pairs(positions) do
local tile = surface.get_tile(pos[1], pos[2])
Assert.equal(config.revert_tile, tile.name)
end
end
)
end
)

View File

@ -135,4 +135,6 @@ function Public.get_on_nth_tick_event_handlers()
return on_nth_tick_event_handlers
end
Public.on_event = on_event
return Public

View File

@ -23,6 +23,8 @@ local top_elements = {}
local on_visible_handlers = {}
local on_pre_hidden_handlers = {}
Gui._top_elements = top_elements
function Gui.uid_name()
return tostring(Token.uid())
end

59
utils/gui_tests.lua Normal file
View File

@ -0,0 +1,59 @@
local Declare = require 'utils.test.declare'
local EventFactory = require 'utils.test.event_factory'
local Gui = require 'utils.gui'
local Assert = require 'utils.test.assert'
Declare.module(
{'utils', 'Gui'},
function()
Declare.module(
'can toggle top buttons',
function()
local function count_gui_elements(gui)
return #gui.top.children + #gui.left.children + #gui.center.children
end
for _, name in pairs(Gui._top_elements) do
Declare.test(
Gui.names[name],
function(context)
local player = context.player
local element = player.gui.top[name]
if not element.enabled then
return
end
local event = EventFactory.on_gui_click(element, player.index)
local click_action = function()
EventFactory.raise(event)
end
local before_count = count_gui_elements(player.gui)
-- Open
click_action()
local after_open_count = count_gui_elements(player.gui)
Assert.is_true(
after_open_count > before_count,
'after open count should be greater than before count.'
)
-- Close
context:next(click_action):next(
function()
local after_close_count = count_gui_elements(player.gui)
Assert.equal(
before_count,
after_close_count,
'after close count should be equal to before count.'
)
end
)
end
)
end
end
)
end
)

52
utils/test/assert.lua Normal file
View File

@ -0,0 +1,52 @@
local error = error
local concat = table.concat
local Public = {}
local function append_optional_message(main_message, optional_message)
if optional_message then
return concat {main_message, ' - ', optional_message}
end
return main_message
end
function Public.equal(a, b, optional_message)
if a == b then
return
end
local message = {tostring(a), ' ~= ', tostring(b)}
if optional_message then
message[#message + 1] = ' - '
message[#message + 1] = optional_message
end
message = concat(message)
error(message, 2)
end
function Public.is_true(condition, optional_message)
if not condition then
error(optional_message or 'condition was not true', 2)
end
end
function Public.valid(lua_object, optional_message)
if not lua_object then
error(append_optional_message('lua_object was nil', optional_message), 2)
end
if not lua_object.valid then
error(append_optional_message('lua_object was not valid', optional_message), 2)
end
end
function Public.is_lua_object_with_name(lua_object, name, optional_message)
Public.valid(lua_object, optional_message)
if lua_object.name ~= name then
error(append_optional_message("lua_object did not have name '" .. tostring(name) .. "'", optional_message), 2)
end
end
return Public

154
utils/test/builder.lua Normal file
View File

@ -0,0 +1,154 @@
local ModuleStore = require 'utils.test.module_store'
local Context = require 'utils.test.context'
local Public = {}
local is_init = false
local id_count = 0
local function get_id()
id_count = id_count + 1
return id_count
end
local function init_inner(module, depth)
module.id = get_id()
module.depth = depth
local count = 0
local tests = {}
for name, func in pairs(module.test_funcs) do
count = count + 1
tests[#tests + 1] = {
id = get_id(),
name = name,
module = module,
func = func,
context = nil,
current_step = nil,
passed = nil,
error = nil
}
end
module.tests = tests
for _, child in pairs(module.children) do
count = count + init_inner(child, depth + 1)
end
module.count = count
return count
end
function Public.init()
if is_init then
return
end
is_init = true
init_inner(ModuleStore.root_module, 0)
end
function Public.get_root_modules()
Public.init()
return ModuleStore.root_module
end
local function prepare_pre_module_hooks(module, runnables, player)
local startup_func = module.startup_func
if startup_func then
runnables[#runnables + 1] = {
is_hook = true,
name = 'startup',
module = module,
func = startup_func,
context = Context.new(player),
current_step = 0,
error = nil
}
end
end
local function build_pre_module_hooks(module, runnables, player)
if module == nil then
return
end
build_pre_module_hooks(module.parent, runnables, player)
prepare_pre_module_hooks(module, runnables, player)
end
local function prepare_post_module_hooks(module, runnables, player)
local teardown_func = module.teardown_func
if teardown_func then
runnables[#runnables + 1] = {
is_hook = true,
name = 'teardown',
module = module,
func = teardown_func,
context = Context.new(player),
current_step = 0,
error = nil
}
end
end
local function build_post_module_hooks(module, runnables, player)
if module == nil then
return
end
prepare_post_module_hooks(module, runnables, player)
build_post_module_hooks(module.parent, runnables, player)
end
local function prepare_test(test, player)
test.context = Context.new(player)
test.current_step = 0
test.passed = nil
test.error = nil
return test
end
local function prepare_module(module, runnables, player)
module.passed = nil
prepare_pre_module_hooks(module, runnables, player)
for _, test in pairs(module.tests) do
prepare_test(test, player)
runnables[#runnables + 1] = test
end
for _, child in pairs(module.children) do
prepare_module(child, runnables, player)
end
prepare_post_module_hooks(module, runnables, player)
end
function Public.build_test_for_run(test, player)
Public.init()
local runnables = {}
build_pre_module_hooks(test.module, runnables, player)
runnables[#runnables + 1] = prepare_test(test, player)
build_post_module_hooks(test.module, runnables, player)
return runnables
end
function Public.build_module_for_run(module, player)
Public.init()
local runnables = {}
build_pre_module_hooks(module.parent, runnables, player)
prepare_module(module, runnables, player)
build_post_module_hooks(module.parent, runnables, player)
return runnables
end
return Public

21
utils/test/command.lua Normal file
View File

@ -0,0 +1,21 @@
local Command = require 'utils.command'
local Runner = require 'utils.test.runner'
local Viewer = require 'utils.test.viewer'
Command.add(
'test-runner',
{
description = "Runs tests and opens the test runner, use flag 'open' to skip running tests first.",
arguments = {'open'},
default_values = {open = false},
allowed_by_server = false
},
function(args, player)
local open = args.open
if open == 'open' or open == 'o' then
Viewer.open(player)
else
Runner.run_module(nil, player)
end
end
)

18
utils/test/context.lua Normal file
View File

@ -0,0 +1,18 @@
local Public = {}
Public.__index = Public
function Public.new(player)
return setmetatable({player = player, _steps = {}}, Public)
end
function Public.timeout(self, delay, func)
local steps = self._steps
steps[#steps + 1] = {func = func, delay = delay or 1}
return self
end
function Public.next(self, func)
return self:timeout(1, func)
end
return Public

10
utils/test/declare.lua Normal file
View File

@ -0,0 +1,10 @@
local ModuleStore = require 'utils.test.module_store'
local Public = {}
Public.module = ModuleStore.module
Public.test = ModuleStore.test
Public.module_startup = ModuleStore.module_startup
Public.module_teardown = ModuleStore.module_teardown
return Public

5
utils/test/discovery.lua Normal file
View File

@ -0,0 +1,5 @@
local include = require 'utils.test.include'
for name in pairs(_G.package.loaded) do
include(name .. '_tests')
end

View File

@ -0,0 +1,74 @@
local EventCore = require 'utils.event_core'
local Public = {}
Public.raise = EventCore.on_event
function Public.position(position)
local x = position.x or position[1]
local y = position.y or position[2]
position.x = x
position[1] = x
position.y = y
position[2] = y
return position
end
function Public.area(area)
local left_top = area.left_top or area[1]
local right_bottom = area.right_bottom or area[2]
Public.position(left_top)
Public.position(right_bottom)
area.left_top = left_top
area[1] = left_top
area.right_bottom = right_bottom
area[2] = right_bottom
return area
end
function Public.on_gui_click(element, player_index)
return {
name = defines.events.on_gui_click,
tick = game.tick,
element = element,
player_index = player_index,
button = defines.mouse_button_type.left,
alt = false,
control = false,
shift = false
}
end
function Public.on_player_deconstructed_area(player_index, surface, area, item)
return {
name = defines.events.on_player_deconstructed_area,
tick = game.tick,
player_index = player_index,
surface = surface,
area = Public.area(area),
item = item,
alt = false
}
end
function Public.do_player_deconstruct_area(cursor, player, area, optional_skip_fog_of_war)
cursor.deconstruct_area(
{
surface = player.surface,
force = player.force,
area = area,
by_player = player,
skip_fog_of_war = optional_skip_fog_of_war
}
)
local event = Public.on_player_deconstructed_area(player.index, player.surface, area, cursor.name)
Public.raise(event)
end
return Public

128
utils/test/helper.lua Normal file
View File

@ -0,0 +1,128 @@
local Global = require 'utils.global'
local Public = {}
local surface_count = 0
Global.register(
{surface_count = surface_count},
function(tbl)
surface_count = tbl.surface_count
end
)
local function get_surface_name()
surface_count = surface_count + 1
return 'test_surface' .. surface_count
end
local autoplace_settings = {
tile = {
treat_missing_as_default = false,
settings = {
['grass-1'] = {frequency = 1, size = 1, richness = 1}
}
}
}
local autoplace_controls = {
trees = {
frequency = 1,
richness = 1,
size = 0
},
['enemy-base'] = {
frequency = 1,
richness = 1,
size = 0
},
coal = {
frequency = 1,
richness = 1,
size = 0
},
['copper-ore'] = {
frequency = 1,
richness = 1,
size = 0
},
['crude-oil'] = {
frequency = 1,
richness = 1,
size = 0
},
['iron-ore'] = {
frequency = 1,
richness = 1,
size = 0
},
stone = {
frequency = 1,
richness = 1,
size = 0
},
['uranium-ore'] = {
frequency = 1,
richness = 1,
size = 0
}
}
local cliff_settings = {
cliff_elevation_0 = 1024,
cliff_elevation_interval = 10,
name = 'cliff'
}
function Public.startup_test_surface(context, options)
options = options or {}
local name = options.name or get_surface_name()
local area = options.area or {64, 64}
local player = context.player
local old_surface = player.surface
local old_position = player.position
local old_character = player.character
local surface =
game.create_surface(
name,
{
width = area.x or area[1],
height = area.y or area[2],
autoplace_settings = autoplace_settings,
autoplace_controls = autoplace_controls,
cliff_settings = cliff_settings
}
)
surface.request_to_generate_chunks({0, 0}, 32)
surface.force_generate_chunk_requests()
context:next(
function()
for k, v in pairs(surface.find_entities()) do
v.destroy()
end
surface.destroy_decoratives {area = {{-32, -32}, {32, 32}}}
player.character = nil
player.teleport({0, 0}, surface)
player.create_character()
end
)
return function()
player.character = nil
player.teleport(old_position, old_surface)
if old_character and old_character.valid then
player.character = old_character
end
game.delete_surface(surface)
end
end
return Public

14
utils/test/include.lua Normal file
View File

@ -0,0 +1,14 @@
local require = require
local pcall = pcall
local find = string.find
local function file_is_missing(message)
return find(message, 'no such file') or find(message, 'File was removed to decrease save file size')
end
return function(name)
local s, e = pcall(require, name)
if not s and not file_is_missing(e) then
error(e, 2)
end
end

5
utils/test/main.lua Normal file
View File

@ -0,0 +1,5 @@
local include = require 'utils.test.include'
include 'utils.test.runner'
include 'utils.test.viewer'
include 'utils.test.command'
include 'utils.test.discovery'

145
utils/test/module_store.lua Normal file
View File

@ -0,0 +1,145 @@
local Public = {}
local function new_module(module_name)
return {
id = nil,
name = module_name,
parent = nil,
children = {},
startup_func = nil,
startup_steps = nil,
startup_current_step = nil,
startup_error = nil,
teardown_func = nil,
teardown_steps = nil,
teardown_current_step = nil,
teardown_error = nil,
test_funcs = {},
tests = nil,
is_open = false,
depth = nil,
count = nil,
passed = nil
}
end
local root_module = new_module(nil)
root_module.is_open = true
Public.root_module = root_module
local parent_module = nil
local function add_module(module_name, module_func, parent)
local parent_children = parent.children
local module = parent_children[module_name]
if not module then
module = new_module(module_name)
parent_children[module_name] = module
module.parent = parent_module
end
parent_module = module
module_func()
end
local function no_op()
end
local function add_module_range(modules_names, module_func, parent)
for i = 1, #modules_names - 1 do
local name = modules_names[i]
add_module(name, no_op, parent)
parent = parent_module
end
add_module(modules_names[#modules_names], module_func, parent)
end
function Public.module(module_name, module_func)
local module_name_type = type(module_name)
if module_name_type ~= 'string' and module_name_type ~= 'table' then
error('module_name must be of type string or array of strings.', 2)
end
if module_name_type == 'table' and #module_name == 0 then
error('when module_name is array must be non empty.', 2)
end
if type(module_func) ~= 'function' then
error('module_func must be of type function.', 2)
end
local old_parent = parent_module
local parent = parent_module or root_module
if module_name_type == 'string' then
add_module(module_name, module_func, parent)
else
add_module_range(module_name, module_func, parent)
end
parent_module = old_parent
end
function Public.test(test_name, test_func)
if not parent_module then
error('test can not be declared outisde of a module.', 2)
end
if type(test_name) ~= 'string' then
error('test_name must be of type string.', 2)
end
if type(test_func) ~= 'function' then
error('test_func must be of type function.', 2)
end
local test_funcs = parent_module.test_funcs
if test_funcs[test_name] then
error(
table.concat {
"test '",
test_name,
"' already exists, can not have duplicate test names in the same module."
},
2
)
end
test_funcs[test_name] = test_func
end
function Public.module_startup(startup_func)
if type(startup_func) ~= 'function' then
error('startup_func must be of type function.', 2)
end
if parent_module == nil then
error('root module can not have startup_func.', 2)
end
if parent_module.startup_func ~= nil then
error('startup_func can not be declared twice for the same module.', 2)
end
parent_module.startup_func = startup_func
end
function Public.module_teardown(teardown_func)
if type(teardown_func) ~= 'function' then
error('teardown_func must be of type function.', 2)
end
if parent_module == nil then
error('root module can not have teardown_func.', 2)
end
if parent_module.teardown_func ~= nil then
error('teardown_func can not be declared twice for the same module.', 2)
end
parent_module.teardown_func = teardown_func
end
return Public

244
utils/test/runner.lua Normal file
View File

@ -0,0 +1,244 @@
local Token = require 'utils.token'
local Task = require 'utils.task'
local ModuleStore = require 'utils.test.module_store'
local Builder = require 'utils.test.builder'
local Event = require 'utils.event'
local pcall = pcall
local Public = {}
Public.events = {
tests_run_finished = Event.generate_event_name('test_run_finished')
}
local run_runnables_token
local function print_summary(data)
local pass_count = data.count - data.fail_count
data.player.print(table.concat {pass_count, ' of ', data.count, ' tests passed.'})
end
local function mark_module_for_passed(module)
local any_fails = false
local all_ran = true
for _, child in pairs(module.children) do
local module_any_fails, module_all_ran = mark_module_for_passed(child)
any_fails = any_fails or module_any_fails
all_ran = all_ran and module_all_ran
end
for _, test in pairs(module.tests) do
any_fails = any_fails or (test.passed == false)
all_ran = all_ran and (test.passed ~= nil)
end
if any_fails then
module.passed = false
elseif all_ran then
module.passed = true
else
module.passed = nil
end
return any_fails, all_ran
end
local function mark_modules_for_passed()
mark_module_for_passed(ModuleStore.root_module)
end
local function finish_test_run(data)
print_summary(data)
mark_modules_for_passed()
script.raise_event(Public.events.tests_run_finished, {player = data.player})
end
local function print_error(player, test_name, error_message)
player.print(table.concat {"Failed - '", test_name, "': ", tostring(error_message)}, {r = 1})
end
local function print_success(player, test_name)
player.print(table.concat {"Passed - '", test_name, "'"}, {g = 1})
end
local function print_hook_error(hook)
hook.context.player.print(table.concat {'Failed ', hook.name, " hook -':", tostring(hook.error)}, {r = 1})
end
local function record_hook_error_in_module(hook)
if hook.name == 'startup' then
hook.module.startup_error = hook.error
elseif hook.name == 'teardown' then
hook.module.teardown_error = hook.error
end
end
local function do_termination(data)
if not data.stop_on_first_error then
return false
end
data.player.print('Test run canceled due to stop on first error policy.')
finish_test_run(data)
data.index = -1
return true
end
local function run_hook(hook)
local context = hook.context
local steps = context._steps
local current_step = hook.current_step
local func
if current_step == 0 then
func = hook.func
else
func = steps[current_step].func
end
local success, return_value = pcall(func, context)
if not success then
hook.error = return_value
print_hook_error(hook)
record_hook_error_in_module(hook)
return false
end
if current_step == #steps then
return true
end
return nil
end
local function do_hook(hook, data)
local hook_success = run_hook(hook)
if hook_success == false and do_termination(data) then
return
end
if hook_success == nil then
local step_index = hook.current_step + 1
local step = hook.context._steps[step_index]
hook.current_step = step_index
Task.set_timeout_in_ticks(step.delay or 1, run_runnables_token, data)
return
end
data.index = data.index + 1
Task.set_timeout_in_ticks(1, run_runnables_token, data)
return
end
local function run_test(test)
local context = test.context
local steps = context._steps
local current_step = test.current_step
local func
if current_step == 0 then
func = test.func
else
func = steps[current_step].func
end
local success, return_value = pcall(func, context)
if not success then
print_error(context.player, test.name, return_value)
test.passed = false
test.error = return_value
return false
end
if current_step == #steps then
print_success(context.player, test.name)
test.passed = true
return true
end
return nil
end
local function do_test(test, data)
local success = run_test(test)
if success == false then
data.count = data.count + 1
data.fail_count = data.fail_count + 1
if do_termination(data) then
return
end
data.index = data.index + 1
Task.set_timeout_in_ticks(1, run_runnables_token, data)
return
end
if success == true then
data.count = data.count + 1
data.index = data.index + 1
Task.set_timeout_in_ticks(1, run_runnables_token, data)
return
end
local step_index = test.current_step + 1
test.current_step = step_index
local step = test.context._steps[step_index]
Task.set_timeout_in_ticks(step.delay or 1, run_runnables_token, data)
end
local function run_runnables(data)
local index = data.index
local runnable = data.runnables[index]
if runnable == nil then
finish_test_run(data)
return
end
if runnable.is_hook then
do_hook(runnable, data)
else
do_test(runnable, data)
end
end
run_runnables_token = Token.register(run_runnables)
local function validate_options(options)
options = options or {}
options.stop_on_first_error = options.stop_on_first_error or false
return options
end
local function run(runnables, player, options)
options = validate_options(options)
run_runnables(
{
runnables = runnables,
player = player,
index = 1,
count = 0,
fail_count = 0,
stop_on_first_error = options.stop_on_first_error
}
)
end
function Public.run_module(module, player, options)
local runnables = Builder.build_module_for_run(module or ModuleStore.root_module, player)
run(runnables, player, options)
end
function Public.run_test(test, player, options)
local runnables = Builder.build_test_for_run(test, player)
run(runnables, player, options)
end
return Public

423
utils/test/viewer.lua Normal file
View File

@ -0,0 +1,423 @@
local Gui = require 'utils.gui'
local Builder = require 'utils.test.builder'
local Runner = require 'utils.test.runner'
local Color = require 'resources.color_presets'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Event = require 'utils.event'
local Global = require 'utils.global'
local Public = {}
local info_type_test = {}
local info_type_module = {}
local down_arrow = ''
local right_arrow = ''
local color_success = {g = 1}
local color_failure = {r = 1}
local color_selected = Color.orange
local color_default = Color.white
local main_frame_name = Gui.uid_name()
local close_main_frame_name = Gui.uid_name()
local module_arrow_name = Gui.uid_name()
local module_label_name = Gui.uid_name()
local test_label_name = Gui.uid_name()
local run_all_button_name = Gui.uid_name()
local run_selected_button_name = Gui.uid_name()
local stop_on_error_checkbox_name = Gui.uid_name()
local error_test_box_name = Gui.uid_name()
local selected_test_info_by_player_index = {}
local stop_on_first_error_by_player_index = {}
Global.register(
{
selected_test_info_by_player_index = selected_test_info_by_player_index,
stop_on_first_error_by_player_index = stop_on_first_error_by_player_index
},
function(tbl)
selected_test_info_by_player_index = tbl.selected_test_info_by_player_index
stop_on_first_error_by_player_index = tbl.stop_on_first_error_by_player_index
end
)
local function get_module_state(module)
local passed = module.passed
if passed == false or module.startup_error or module.teardown_error then
return false
end
return passed
end
local function get_test_error(test)
return test.error or ''
end
local function get_module_error(module)
local errors = {}
if module.startup_error then
errors[#errors + 1] = 'startup error: '
errors[#errors + 1] = module.startup_error
errors[#errors + 1] = '\n\n'
end
if module.teardown_error then
errors[#errors + 1] = 'teardown error: '
errors[#errors + 1] = module.teardown_error
end
return table.concat(errors)
end
local function get_text_box_error(player_index)
local test_info = selected_test_info_by_player_index[player_index]
if test_info == nil then
return ''
end
local info_type = test_info.type
if info_type == info_type_test then
return get_test_error(test_info.test)
elseif info_type == info_type_module then
return get_module_error(test_info.module)
end
end
local function set_selected_style(style, selected)
if selected then
style.font_color = color_selected
else
style.font_color = color_default
end
end
local function set_passed_style(style, passed)
if passed == true then
style.font_color = color_success
elseif passed == false then
style.font_color = color_failure
else
style.font_color = color_default
end
end
local function is_test_selected(test, player_index)
local info = selected_test_info_by_player_index[player_index]
if not info then
return false
end
local info_test = info.test
if not info_test then
return false
end
return info_test.id == test.id
end
local function is_module_selected(module, player_index)
local info = selected_test_info_by_player_index[player_index]
if not info then
return false
end
local info_module = info.module
if not info_module then
return false
end
return info_module.id == module.id
end
local function draw_tests_test(container, test, depth)
local flow = container.add {type = 'flow'}
local label = flow.add {type = 'label', name = test_label_name, caption = test.name}
local label_style = label.style
local is_selected = is_test_selected(test, container.player_index)
set_selected_style(label_style, is_selected)
if not is_selected then
set_passed_style(label_style, test.passed)
end
label_style.left_margin = depth * 15 + 10
Gui.set_data(label, {test = test, container = container})
end
local function draw_tests_module(container, module)
local caption = {module.name or 'All Tests', ' (', module.count, ')'}
caption = table.concat(caption)
local flow = container.add {type = 'flow'}
local arrow =
flow.add {
type = 'label',
name = module_arrow_name,
caption = module.is_open and down_arrow or right_arrow
}
arrow.style.left_margin = module.depth * 15
Gui.set_data(arrow, {module = module, container = container})
local label = flow.add {type = 'label', name = module_label_name, caption = caption}
local label_style = label.style
local is_selected = is_module_selected(module, container.player_index)
set_selected_style(label_style, is_selected)
if not is_selected then
set_passed_style(label_style, get_module_state(module))
end
Gui.set_data(label, {module = module, container = container})
if not module.is_open then
return
end
for _, child in pairs(module.children) do
draw_tests_module(container, child)
end
for _, test in pairs(module.tests) do
draw_tests_test(container, test, module.depth + 1)
end
end
local function redraw_tests(container)
Gui.clear(container)
local root_module = Builder.get_root_modules()
draw_tests_module(container, root_module)
end
local function draw_tests(container)
local scroll_pane =
container.add {
type = 'scroll-pane',
horizontal_scroll_policy = 'auto-and-reserve-space',
vertical_scroll_policy = 'auto-and-reserve-space'
}
local scroll_pane_style = scroll_pane.style
scroll_pane_style.horizontally_stretchable = true
scroll_pane_style.height = 350
local list = scroll_pane.add {type = 'flow', direction = 'vertical'}
redraw_tests(list)
end
local function draw_error_text_box(container)
local text = get_text_box_error(container.player_index)
local text_box = container.add {type = 'text-box', name = error_test_box_name, text = text}
local style = text_box.style
style.vertically_stretchable = true
style.horizontally_stretchable = true
style.maximal_width = 800
return text_box
end
local function create_main_frame(center)
local frame = center.add {type = 'frame', name = main_frame_name, caption = 'Test Runner', direction = 'vertical'}
local frame_style = frame.style
frame_style.width = 800
frame_style.height = 600
local top_flow = frame.add {type = 'flow', direction = 'horizontal'}
top_flow.add {type = 'button', name = run_all_button_name, caption = 'Run All'}
top_flow.add {
type = 'button',
name = run_selected_button_name,
caption = 'Run Selected'
}
top_flow.add {
type = 'checkbox',
name = stop_on_error_checkbox_name,
caption = 'Stop on first error',
state = stop_on_first_error_by_player_index[center.player_index] or false
}
draw_tests(frame)
local error_text_box = draw_error_text_box(frame)
Gui.set_data(frame, {error_text_box = error_text_box})
local close_button = frame.add {type = 'button', name = close_main_frame_name, caption = 'Close'}
Gui.set_data(close_button, frame)
end
local function close_main_frame(frame)
Gui.destroy(frame)
end
local function get_error_text_box(player)
local frame = player.gui.center[main_frame_name]
local frame_data = Gui.get_data(frame)
return frame_data.error_text_box
end
local function make_options(player_index)
return {stop_on_first_error = stop_on_first_error_by_player_index[player_index]}
end
local run_module_token =
Token.register(
function(data)
Runner.run_module(data.module, data.player, data.options)
end
)
local run_test_token =
Token.register(
function(data)
Runner.run_test(data.test, data.player, data.options)
end
)
Gui.on_click(
close_main_frame_name,
function(event)
local element = event.element
local frame = Gui.get_data(element)
close_main_frame(frame)
end
)
Gui.on_click(
module_arrow_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local module = data.module
local container = data.container
module.is_open = not module.is_open
redraw_tests(container)
end
)
Gui.on_click(
module_label_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local module = data.module
local container = data.container
local player_index = event.player_index
local is_selected = not is_module_selected(module, player_index)
selected_test_info_by_player_index[player_index] = nil
if is_selected then
selected_test_info_by_player_index[player_index] = {type = info_type_module, module = module}
end
local error_text_box = get_error_text_box(event.player)
if is_selected then
error_text_box.text = get_module_error(module)
else
error_text_box.text = ''
end
redraw_tests(container)
end
)
Gui.on_click(
test_label_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local test = data.test
local container = data.container
local player_index = event.player_index
local is_selected = not is_test_selected(test, player_index)
selected_test_info_by_player_index[player_index] = nil
if is_selected then
selected_test_info_by_player_index[player_index] = {type = info_type_test, test = test}
end
local error_text_box = get_error_text_box(event.player)
if is_selected then
error_text_box.text = get_test_error(test)
else
error_text_box.text = ''
end
redraw_tests(container)
end
)
Gui.on_click(
run_all_button_name,
function(event)
local frame = event.player.gui.center[main_frame_name]
close_main_frame(frame)
local options = make_options(event.player_index)
Task.set_timeout_in_ticks(1, run_module_token, {module = nil, player = event.player, options = options})
end
)
Gui.on_click(
run_selected_button_name,
function(event)
local test_info = selected_test_info_by_player_index[event.player_index]
if test_info == nil then
return
end
local options = make_options(event.player_index)
local info_type = test_info.type
if info_type == info_type_module then
Task.set_timeout_in_ticks(
1,
run_module_token,
{module = test_info.module, player = event.player, options = options}
)
elseif info_type == info_type_test then
Task.set_timeout_in_ticks(
1,
run_test_token,
{test = test_info.test, player = event.player, options = options}
)
else
return
end
local frame = event.player.gui.center[main_frame_name]
close_main_frame(frame)
end
)
Gui.on_checked_state_changed(
stop_on_error_checkbox_name,
function(event)
stop_on_first_error_by_player_index[event.player_index] = event.element.state or nil
end
)
Event.add(
Runner.events.tests_run_finished,
function(event)
Public.open(event.player)
end
)
function Public.open(player)
local center = player.gui.center
local frame = center[main_frame_name]
if frame then
return
end
create_main_frame(center)
end
return Public