mirror of https://github.com/koreader/koreader
BackgroundRunner (#3008)
* Use getCapacityHW() to ensure latest battery capacity can be retrieved * BackgroundRunner * Start background_runner_spec.lua * AutofrontLight plugin now uses BackgroundRunner pluginpull/3034/head
parent
ba96506483
commit
c9a997f42c
@ -0,0 +1,90 @@
|
||||
local logger = require("logger")
|
||||
|
||||
local CommandRunner = {
|
||||
pio = nil,
|
||||
job = nil,
|
||||
}
|
||||
|
||||
function CommandRunner:createEnvironmentFromTable(t)
|
||||
if t == nil then return "" end
|
||||
|
||||
local r = ""
|
||||
for k, v in pairs(t) do
|
||||
r = r .. k .. "=" .. v .. " "
|
||||
end
|
||||
|
||||
if string.len(r) > 0 then r = "export " .. r .. ";" end
|
||||
return r
|
||||
end
|
||||
|
||||
function CommandRunner:createEnvironment()
|
||||
if type(self.job.environment) == "table" then
|
||||
return self:createEnvironmentFromTable(self.job.environment)
|
||||
end
|
||||
if type(self.job.environment) == "function" then
|
||||
local status, result = pcall(self.job.environment)
|
||||
if status then
|
||||
return self:createEnvironmentFromTable(result)
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
function CommandRunner:start(job)
|
||||
assert(self ~= nil)
|
||||
assert(self.pio == nil)
|
||||
assert(self.job == nil)
|
||||
self.job = job
|
||||
self.job.start_sec = os.time()
|
||||
assert(type(self.job.executable) == "string")
|
||||
local command = self:createEnvironment() .. " " ..
|
||||
"sh plugins/backgroundrunner.koplugin/luawrapper.sh " ..
|
||||
"\"" .. self.job.executable .. "\""
|
||||
logger.dbg("CommandRunner: Will execute command " .. command)
|
||||
self.pio = io.popen(command)
|
||||
end
|
||||
|
||||
--- Polls the status of self.pio.
|
||||
-- @return a table contains the result from luawrapper.sh. Returns nil if the
|
||||
-- command has not been finished.
|
||||
function CommandRunner:poll()
|
||||
assert(self ~= nil)
|
||||
assert(self.pio ~= nil)
|
||||
assert(self.job ~= nil)
|
||||
local line = self.pio:read()
|
||||
if line == "" then
|
||||
return nil
|
||||
else
|
||||
if line == nil then
|
||||
-- The binary crashes without output. This should not happen.
|
||||
self.job.result = 223
|
||||
else
|
||||
line = line .. self.pio:read("*a")
|
||||
logger.dbg("CommandRunner: Receive output " .. line)
|
||||
local status, result = pcall(loadstring(line))
|
||||
if status and result ~= nil then
|
||||
for k, v in pairs(result) do
|
||||
self.job[k] = v
|
||||
end
|
||||
else
|
||||
-- The output from binary is invalid.
|
||||
self.job.result = 222
|
||||
end
|
||||
end
|
||||
self.pio:close()
|
||||
self.pio = nil
|
||||
self.job.end_sec = os.time()
|
||||
local job = self.job
|
||||
self.job = nil
|
||||
return job
|
||||
end
|
||||
end
|
||||
|
||||
--- Whether this is a running job.
|
||||
-- @treturn boolean
|
||||
function CommandRunner:pending()
|
||||
assert(self ~= nil)
|
||||
return self.pio ~= nil
|
||||
end
|
||||
|
||||
return CommandRunner
|
@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Converts the return of "sh wrapper.sh $@" into Lua format.
|
||||
|
||||
CURRENT_DIR=$(dirname "$0")
|
||||
sh "$CURRENT_DIR/wrapper.sh" "$@" >/dev/null 2>&1 &
|
||||
JOB_ID=$!
|
||||
|
||||
while true; do
|
||||
if ps -p $JOB_ID >/dev/null 2>&1; then
|
||||
# Unblock f:read().
|
||||
echo
|
||||
else
|
||||
wait $JOB_ID
|
||||
EXIT_CODE=$?
|
||||
if [ "$EXIT_CODE" -eq "255" ]; then
|
||||
TIMEOUT="true"
|
||||
else
|
||||
TIMEOUT="false"
|
||||
fi
|
||||
|
||||
if [ "$EXIT_CODE" -eq "127" ]; then
|
||||
BADCOMMAND="true"
|
||||
else
|
||||
BADCOMMAND="false"
|
||||
fi
|
||||
|
||||
echo "return { \
|
||||
result = $EXIT_CODE, \
|
||||
timeout = $TIMEOUT, \
|
||||
bad_command = $BADCOMMAND, \
|
||||
}"
|
||||
exit 0
|
||||
fi
|
||||
done
|
@ -0,0 +1,234 @@
|
||||
local CommandRunner = require("commandrunner")
|
||||
local PluginShare = require("pluginshare")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
|
||||
-- BackgroundRunner is an experimental feature to execute non-critical jobs in
|
||||
-- background. A job is defined as a table in PluginShare.backgroundJobs table.
|
||||
-- It contains at least following items:
|
||||
-- when: number, string or function
|
||||
-- number: the delay in seconds
|
||||
-- string: "best-effort" - the job will be started when there is no other jobs
|
||||
-- to be executed.
|
||||
-- "idle" - the job will be started when the device is idle.
|
||||
-- function: if the return value of the function is true, the job will be
|
||||
-- executed immediately.
|
||||
--
|
||||
-- repeated: boolean or function or nil or number
|
||||
-- boolean: true to repeated the job once it finished.
|
||||
-- function: if the return value of the function is true, repeated the job
|
||||
-- once it finished. If the function throws an error, it equals to
|
||||
-- return false.
|
||||
-- nil: same as false.
|
||||
-- number: times to repeat.
|
||||
--
|
||||
-- executable: string or function
|
||||
-- string: the command line to be executed. The command or binary will be
|
||||
-- executed in the lowest priority. Command or binary will be killed
|
||||
-- if it executes for over 1 hour.
|
||||
-- function: the action to be executed. The execution cannot be killed, but it
|
||||
-- will be considered as timeout if it executes for more than 1
|
||||
-- second.
|
||||
-- If the executable times out, the job will be blocked, i.e. the repeated
|
||||
-- field will be ignored.
|
||||
--
|
||||
-- environment: table or function or nil
|
||||
-- table: the key-value pairs of all environments set for string executable.
|
||||
-- function: the function to return a table of environments.
|
||||
-- nil: ignore.
|
||||
--
|
||||
-- callback: function or nil
|
||||
-- function: the action to be executed when executable has been finished.
|
||||
-- Errors thrown from this function will be ignored.
|
||||
-- nil: ignore.
|
||||
--
|
||||
-- If a job does not contain enough information, it will be ignored.
|
||||
--
|
||||
-- Once the job is finished, several items will be added to the table:
|
||||
-- result: number, the return value of the command. In general, 0 means
|
||||
-- succeeded.
|
||||
-- For function executable, 1 if the function throws an error.
|
||||
-- For string executable, several predefined values indicate the
|
||||
-- internal errors. E.g. 223: the binary crashes. 222: the output is
|
||||
-- invalid. 127: the command is invalid. 255: the command timed out.
|
||||
-- Typically, consumers can use following states instead of hardcodeing
|
||||
-- the error codes.
|
||||
-- exception: error, the error returned from function executable. Not available
|
||||
-- for string executable.
|
||||
-- timeout: boolean, whether the command times out.
|
||||
-- bad_command: boolean, whether the command is not found. Not available for
|
||||
-- function executable.
|
||||
-- blocked: boolean, whether the job is blocked.
|
||||
-- start_sec: number, the os.time() when the job was started.
|
||||
-- end_sec: number, the os.time() when the job was stopped.
|
||||
-- insert_sec: number, the os.time() when the job was inserted into queue.
|
||||
|
||||
PluginShare.backgroundJobs = PluginShare.backgroundJobs or {}
|
||||
|
||||
local BackgroundRunner = {
|
||||
jobs = PluginShare.backgroundJobs,
|
||||
}
|
||||
|
||||
--- Copies required fields from |job|.
|
||||
-- @return a new table with required fields of a valid job.
|
||||
function BackgroundRunner:_clone(job)
|
||||
assert(job ~= nil)
|
||||
local result = {}
|
||||
result.when = job.when
|
||||
result.repeated = job.repeated
|
||||
result.executable = job.executable
|
||||
result.callback = job.callback
|
||||
result.environment = job.environment
|
||||
return result
|
||||
end
|
||||
|
||||
function BackgroundRunner:_shouldRepeat(job)
|
||||
if type(job.repeated) == "nil" then return false end
|
||||
if type(job.repeated) == "boolean" then return job.repeated end
|
||||
if type(job.repeated) == "function" then
|
||||
local status, result = pcall(job.repeated)
|
||||
if status then
|
||||
return result
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
if type(job.repeated) == "number" then
|
||||
job.repeated = job.repeated - 1
|
||||
return job.repeated > 0
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function BackgroundRunner:_finishJob(job)
|
||||
assert(self ~= nil)
|
||||
if type(job.executable) == "function" then
|
||||
job.timeout = ((job.end_sec - job.start_sec) > 1)
|
||||
end
|
||||
job.blocked = job.timeout
|
||||
if not job.blocked and self:_shouldRepeat(job) then
|
||||
self:_insert(self:_clone(job))
|
||||
end
|
||||
if type(job.callback) == "function" then
|
||||
pcall(job.callback)
|
||||
end
|
||||
end
|
||||
|
||||
--- Executes |job|.
|
||||
-- @treturn boolean true if job is valid.
|
||||
function BackgroundRunner:_executeJob(job)
|
||||
assert(not CommandRunner:pending())
|
||||
if job == nil then return false end
|
||||
if job.executable == nil then return false end
|
||||
|
||||
if type(job.executable) == "string" then
|
||||
CommandRunner:start(job)
|
||||
return true
|
||||
elseif type(job.executable) == "function" then
|
||||
job.start_sec = os.time()
|
||||
local status, err = pcall(job.executable)
|
||||
if status then
|
||||
job.result = 0
|
||||
else
|
||||
job.result = 1
|
||||
job.exception = err
|
||||
end
|
||||
job.end_sec = os.time()
|
||||
self:_finishJob(job)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Polls the status of the pending CommandRunner.
|
||||
function BackgroundRunner:_poll()
|
||||
assert(self ~= nil)
|
||||
assert(CommandRunner:pending())
|
||||
local result = CommandRunner:poll()
|
||||
if result == nil then return end
|
||||
|
||||
self:_finishJob(result)
|
||||
end
|
||||
|
||||
function BackgroundRunner:_execute()
|
||||
logger.dbg("BackgroundRunner: _execute() @ ", os.time())
|
||||
assert(self ~= nil)
|
||||
if CommandRunner:pending() then
|
||||
self:_poll()
|
||||
else
|
||||
local round = 0
|
||||
while #self.jobs > 0 do
|
||||
local job = table.remove(self.jobs, 1)
|
||||
if job.insert_sec == nil then
|
||||
-- Jobs are first inserted to jobs table from external users. So
|
||||
-- they may not have insert_sec field.
|
||||
job.insert_sec = os.time()
|
||||
end
|
||||
local should_execute = false
|
||||
local should_ignore = false
|
||||
if type(job.when) == "function" then
|
||||
local status, result = pcall(job.when)
|
||||
if status then
|
||||
should_execute = result
|
||||
else
|
||||
should_ignore = true
|
||||
end
|
||||
elseif type(job.when) == "number" then
|
||||
if job.when >= 0 then
|
||||
should_execute = ((os.time() - job.insert_sec) >= job.when)
|
||||
else
|
||||
should_ignore = true
|
||||
end
|
||||
elseif type(job.when) == "string" then
|
||||
-- TODO(Hzj_jie): Implement "idle" mode
|
||||
if job.when == "best-effort" then
|
||||
should_execute = (round > 0)
|
||||
elseif job.when == "idle" then
|
||||
should_execute = (round > 1)
|
||||
else
|
||||
should_ignore = true
|
||||
end
|
||||
else
|
||||
should_ignore = true
|
||||
end
|
||||
|
||||
if should_execute then
|
||||
assert(not should_ignore)
|
||||
self:_executeJob(job)
|
||||
break
|
||||
elseif not should_ignore then
|
||||
table.insert(self.jobs, job)
|
||||
end
|
||||
|
||||
round = round + 1
|
||||
if round > 2 then break end
|
||||
end
|
||||
end
|
||||
|
||||
if PluginShare.stopBackgroundRunner == nil then
|
||||
self:_schedule()
|
||||
end
|
||||
end
|
||||
|
||||
function BackgroundRunner:_schedule()
|
||||
assert(self ~= nil)
|
||||
UIManager:scheduleIn(2, function() self:_execute() end)
|
||||
end
|
||||
|
||||
function BackgroundRunner:_insert(job)
|
||||
assert(self ~= nil)
|
||||
job.insert_sec = os.time()
|
||||
table.insert(self.jobs, job)
|
||||
end
|
||||
|
||||
BackgroundRunner:_schedule()
|
||||
|
||||
local BackgroundRunnerWidget = WidgetContainer:new{
|
||||
name = "backgroundrunner",
|
||||
runner = BackgroundRunner,
|
||||
}
|
||||
|
||||
return BackgroundRunnerWidget
|
@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Starts the arguments as a bash command in background with low priorty. The
|
||||
# command will be killed if it executes for over 1 hour. If the command failed
|
||||
# to start, this script returns 127. If the command is timed out, this script
|
||||
# returns 255. Otherwise the return value of the command will be returned.
|
||||
|
||||
echo "TIMEOUT in environment: $TIMEOUT"
|
||||
|
||||
if [ -z "$TIMEOUT" ]; then
|
||||
TIMEOUT=3600
|
||||
fi
|
||||
|
||||
echo "Timeout has been set to $TIMEOUT seconds"
|
||||
|
||||
echo "Will start command $*"
|
||||
|
||||
echo "$@" | nice -n 19 sh &
|
||||
JOB_ID=$!
|
||||
echo "Job id: $JOB_ID"
|
||||
|
||||
for i in $(seq 1 1 $TIMEOUT); do
|
||||
if ps -p $JOB_ID >/dev/null 2>&1; then
|
||||
# Job is still running.
|
||||
sleep 1
|
||||
ROUND=$(printf "%s" "$i" | tail -c 1)
|
||||
if [ "$ROUND" -eq "0" ]; then
|
||||
echo "Job $JOB_ID is still running ... waited for $i seconds."
|
||||
fi
|
||||
else
|
||||
wait $JOB_ID
|
||||
exit $?
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Command $* has timed out"
|
||||
|
||||
kill -9 $JOB_ID
|
||||
exit 255
|
@ -0,0 +1,292 @@
|
||||
describe("BackgroundRunner widget tests", function()
|
||||
local Device, PluginShare, MockTime, UIManager
|
||||
|
||||
setup(function()
|
||||
require("commonrequire")
|
||||
package.unloadAll()
|
||||
-- Device needs to be loaded before UIManager.
|
||||
Device = require("device")
|
||||
Device.input.waitEvent = function() end
|
||||
PluginShare = require("pluginshare")
|
||||
MockTime = require("mock_time")
|
||||
MockTime:install()
|
||||
UIManager = require("ui/uimanager")
|
||||
UIManager._run_forever = true
|
||||
requireBackgroundRunner()
|
||||
end)
|
||||
|
||||
teardown(function()
|
||||
MockTime:uninstall()
|
||||
package.unloadAll()
|
||||
stopBackgroundRunner()
|
||||
end)
|
||||
|
||||
it("should start job", function()
|
||||
local executed = false
|
||||
table.insert(PluginShare.backgroundJobs, {
|
||||
when = 10,
|
||||
executable = function()
|
||||
executed = true
|
||||
end,
|
||||
})
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
MockTime:increase(9)
|
||||
UIManager:handleInput()
|
||||
assert.is_false(executed)
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.is_true(executed)
|
||||
end)
|
||||
|
||||
it("should repeat job", function()
|
||||
local executed = 0
|
||||
table.insert(PluginShare.backgroundJobs, {
|
||||
when = 1,
|
||||
repeated = function() return executed < 10 end,
|
||||
executable = function()
|
||||
executed = executed + 1
|
||||
end,
|
||||
})
|
||||
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
|
||||
for i = 1, 10 do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(i, executed)
|
||||
end
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(10, executed)
|
||||
end)
|
||||
|
||||
it("should repeat job for predefined times", function()
|
||||
local executed = 0
|
||||
table.insert(PluginShare.backgroundJobs, {
|
||||
when = 1,
|
||||
repeated = 10,
|
||||
executable = function()
|
||||
executed = executed + 1
|
||||
end,
|
||||
})
|
||||
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
|
||||
for i = 1, 10 do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(i, executed)
|
||||
end
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(10, executed)
|
||||
end)
|
||||
|
||||
it("should block long job", function()
|
||||
local executed = 0
|
||||
local job = {
|
||||
when = 1,
|
||||
repeated = true,
|
||||
executable = function()
|
||||
executed = executed + 1
|
||||
MockTime:increase(2)
|
||||
end,
|
||||
}
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(1, executed)
|
||||
assert.is_true(job.timeout)
|
||||
assert.is_true(job.blocked)
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(1, executed)
|
||||
end)
|
||||
|
||||
it("should execute binary", function()
|
||||
local executed = false
|
||||
local job = {
|
||||
when = 1,
|
||||
executable = "ls | grep this-should-not-be-a-file",
|
||||
callback = function()
|
||||
executed = true
|
||||
end,
|
||||
}
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
while job.end_sec == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
-- grep should return 1 when there is no match.
|
||||
assert.are.equal(1, job.result)
|
||||
assert.is_false(job.timeout)
|
||||
assert.is_false(job.bad_command)
|
||||
assert.is_true(executed)
|
||||
end)
|
||||
|
||||
it("should forward string environment to the executable", function()
|
||||
local job = {
|
||||
when = 1,
|
||||
repeated = false,
|
||||
executable = "echo $ENV1 | grep $ENV2",
|
||||
environment = {
|
||||
ENV1 = "yes",
|
||||
ENV2 = "yes",
|
||||
}
|
||||
}
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
while job.end_sec == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
-- grep should return 0 when there is a match.
|
||||
assert.are.equal(0, job.result)
|
||||
assert.is_false(job.timeout)
|
||||
assert.is_false(job.bad_command)
|
||||
|
||||
job.environment = {
|
||||
ENV1 = "yes",
|
||||
ENV2 = "no",
|
||||
}
|
||||
job.end_sec = nil
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
while job.end_sec == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
-- grep should return 1 when there is no match.
|
||||
assert.are.equal(1, job.result)
|
||||
assert.is_false(job.timeout)
|
||||
assert.is_false(job.bad_command)
|
||||
|
||||
assert.are.not_equal(os.getenv("ENV1"), "yes")
|
||||
assert.are.not_equal(os.getenv("ENV2"), "yes")
|
||||
assert.are.not_equal(os.getenv("ENV2"), "no")
|
||||
end)
|
||||
|
||||
it("should forward function environment to the executable", function()
|
||||
local env2 = "yes"
|
||||
local job = {
|
||||
when = 1,
|
||||
repeated = false,
|
||||
executable = "echo $ENV1 | grep $ENV2",
|
||||
environment = function()
|
||||
return {
|
||||
ENV1 = "yes",
|
||||
ENV2 = env2,
|
||||
}
|
||||
end,
|
||||
}
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
while job.end_sec == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
-- grep should return 0 when there is a match.
|
||||
assert.are.equal(0, job.result)
|
||||
assert.is_false(job.timeout)
|
||||
assert.is_false(job.bad_command)
|
||||
|
||||
job.end_sec = nil
|
||||
env2 = "no"
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
while job.end_sec == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
-- grep should return 1 when there is no match.
|
||||
assert.are.equal(1, job.result)
|
||||
assert.is_false(job.timeout)
|
||||
assert.is_false(job.bad_command)
|
||||
end)
|
||||
|
||||
it("should block long binary job", function()
|
||||
local executed = 0
|
||||
local job = {
|
||||
when = 1,
|
||||
repeated = true,
|
||||
executable = "sleep 1h",
|
||||
environment = {
|
||||
TIMEOUT = 1
|
||||
}
|
||||
}
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
|
||||
while job.end_sec == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
assert.are.equal(255, job.result)
|
||||
assert.is_true(job.timeout)
|
||||
assert.is_true(job.blocked)
|
||||
end)
|
||||
|
||||
it("should execute callback", function()
|
||||
local executed = 0
|
||||
table.insert(PluginShare.backgroundJobs, {
|
||||
when = 1,
|
||||
repeated = 10,
|
||||
executable = function() end,
|
||||
callback = function()
|
||||
executed = executed + 1
|
||||
end,
|
||||
})
|
||||
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
|
||||
for i = 1, 10 do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(i, executed)
|
||||
end
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(10, executed)
|
||||
end)
|
||||
|
||||
it("should not execute two jobs sequentially", function()
|
||||
local executed = 0
|
||||
table.insert(PluginShare.backgroundJobs, {
|
||||
when = 1,
|
||||
executable = function()
|
||||
executed = executed + 1
|
||||
end,
|
||||
})
|
||||
table.insert(PluginShare.backgroundJobs, {
|
||||
when = 1,
|
||||
executable = function()
|
||||
executed = executed + 1
|
||||
end,
|
||||
})
|
||||
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(1, executed)
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(2, executed)
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
assert.are.equal(2, executed)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue