From e257c4e45ee6be7d21cd0358018ca577a8fbafce Mon Sep 17 00:00:00 2001 From: Frans de Jonge Date: Thu, 12 Sep 2019 14:15:08 +0200 Subject: [PATCH] [feat, Kobo] Autoshutdown (#5335) The methods used here will likely work on most embedded devices, which is why I put them in their own WakeupMgr interface/scheduler module, separate from Kobo. See https://www.mobileread.com/forums/showthread.php?p=3886403#post3886403 for more context. Fixes #3806. --- frontend/device/kobo/device.lua | 48 +++-- frontend/device/wakeupmgr.lua | 187 ++++++++++++++++++ .../ui/elements/filemanager_menu_order.lua | 1 + frontend/ui/elements/reader_menu_order.lua | 1 + frontend/ui/uimanager.lua | 14 +- plugins/autosuspend.koplugin/_meta.lua | 1 + plugins/autosuspend.koplugin/main.lua | 153 ++++++++------ spec/unit/autosuspend_spec.lua | 169 ++++++++++------ spec/unit/uimanager_spec.lua | 38 ++-- spec/unit/wakeupmgr_spec.lua | 55 ++++++ 10 files changed, 503 insertions(+), 164 deletions(-) create mode 100644 frontend/device/wakeupmgr.lua create mode 100644 spec/unit/wakeupmgr_spec.lua diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index 35edf5d30..664f9d009 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -1,9 +1,10 @@ local Generic = require("device/generic/device") -local TimeVal = require("ui/timeval") local Geom = require("ui/geometry") +local TimeVal = require("ui/timeval") +local WakeupMgr = require("device/wakeupmgr") +local logger = require("logger") local util = require("ffi/util") local _ = require("gettext") -local logger = require("logger") local function yes() return true end local function no() return false end @@ -313,6 +314,7 @@ function Kobo:init() end, } } + self.wakeup_mgr = WakeupMgr:new() Generic.init(self) @@ -535,20 +537,34 @@ end local unexpected_wakeup_count = 0 local function check_unexpected_wakeup() - logger.dbg("Kobo suspend: checking unexpected wakeup:", - unexpected_wakeup_count) - if unexpected_wakeup_count == 0 or unexpected_wakeup_count > 20 then - -- Don't put device back to sleep under the following two cases: - -- 1. a resume event triggered Kobo:resume() function - -- 2. trying to put device back to sleep more than 20 times after unexpected wakeup - return - end - - logger.err("Kobo suspend: putting device back to sleep, unexpected wakeups:", - unexpected_wakeup_count) + local UIManager = require("ui/uimanager") -- just in case other events like SleepCoverClosed also scheduled a suspend - require("ui/uimanager"):unschedule(Kobo.suspend) - Kobo.suspend() + UIManager:unschedule(Kobo.suspend) + + if WakeupMgr:isWakeupAlarmScheduled() and WakeupMgr:validateWakeupAlarmByProximity() then + logger.dbg("Kobo suspend: scheduled wakeup.") + local res = WakeupMgr:wakeupAction() + if not res then + logger.err("Kobo suspend: wakeup action failed.") + end + logger.dbg("Kobo suspend: putting device back to sleep.") + -- Most wakeup actions are linear, but we need some leeway for the + -- poweroff action to send out close events to all requisite widgets. + UIManager:scheduleIn(30, Kobo.suspend) + else + logger.dbg("Kobo suspend: checking unexpected wakeup:", + unexpected_wakeup_count) + if unexpected_wakeup_count == 0 or unexpected_wakeup_count > 20 then + -- Don't put device back to sleep under the following two cases: + -- 1. a resume event triggered Kobo:resume() function + -- 2. trying to put device back to sleep more than 20 times after unexpected wakeup + return + end + + logger.err("Kobo suspend: putting device back to sleep. Unexpected wakeups:", + unexpected_wakeup_count) + Kobo.suspend() + end end function Kobo:getUnexpectedWakeup() return unexpected_wakeup_count end @@ -683,7 +699,7 @@ function Kobo:suspend() -- expected wakeup, which gets checked in check_unexpected_wakeup(). unexpected_wakeup_count = unexpected_wakeup_count + 1 -- assuming Kobo:resume() will be called in 15 seconds - logger.dbg("Kobo suspend: scheduing unexpected wakeup guard") + logger.dbg("Kobo suspend: scheduling unexpected wakeup guard") UIManager:scheduleIn(15, check_unexpected_wakeup) end diff --git a/frontend/device/wakeupmgr.lua b/frontend/device/wakeupmgr.lua new file mode 100644 index 000000000..f3d1c6ad6 --- /dev/null +++ b/frontend/device/wakeupmgr.lua @@ -0,0 +1,187 @@ +--[[-- +RTC wakeup interface. + +Many devices can schedule hardware wakeups with a real time clock alarm. +On embedded devices this can typically be easily manipulated by the user +through `/sys/class/rtc/rtc0/wakealarm`. Some, like the Kobo Aura H2O, +can only schedule wakeups through ioctl. + +See @{ffi.rtc} for implementation details. + +See also: . +--]] + +local RTC = require("ffi/rtc") +local logger = require("logger") + +--[[-- +WakeupMgr base class. + +@table WakeupMgr +--]] +local WakeupMgr = { + dev_rtc = "/dev/rtc0", -- RTC device + _task_queue = {}, -- Table with epoch at which to schedule the task and the function to be scheduled. +} + +--[[-- +Initiate a WakeupMgr instance. + +@usage +local WakeupMgr = require("device/wakeupmgr") +local wakeup_mgr = WakeupMgr:new{ + -- The default is `/dev/rtc0`, but some devices have more than one RTC. + -- You might therefore need to use `/dev/rtc1`, etc. + dev_rtc = "/dev/rtc0", +} +--]] +function WakeupMgr:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + if o.init then o:init() end + return o +end + +--[[-- +Add a task to the queue. + +@todo Group by type to avoid useless wakeups. +For example, maintenance, sync, and shutdown. +I'm not sure if the distinction between maintenance and sync makes sense +but it's wifi on vs. off. +--]] +function WakeupMgr:addTask(seconds_from_now, callback) + if not type(seconds_from_now) == "number" and not type(callback) == "function" then return end + + local epoch = RTC:secondsFromNowToEpoch(seconds_from_now) + + local old_upcoming_task = (self._task_queue[1] or {}).epoch + + table.insert(self._task_queue, { + epoch = epoch, + callback = callback, + }) + --- @todo Binary insert? This table should be so small that performance doesn't matter. + -- It might be useful to have that available as a utility function regardless. + table.sort(self._task_queue, function(a, b) return a.epoch < b.epoch end) + + local new_upcoming_task = self._task_queue[1].epoch + + if not old_upcoming_task or (new_upcoming_task < old_upcoming_task) then + self:setWakeupAlarm(self._task_queue[1].epoch) + end +end + +--[[-- +Remove task from queue. + +This method removes a task by either index, scheduled time or callback. + +@int idx Task queue index. Mainly useful within this module. +@int epoch The epoch for when this task is scheduled to wake up. +Normally the preferred method for outside callers. +@int callback A scheduled callback function. Store a reference for use +with anonymous functions. +--]] +function WakeupMgr:removeTask(idx, epoch, callback) + if not type(idx) == "number" + and not type(epoch) == "number" + and not type(callback) == "function" then return end + + if #self._task_queue < 1 then return end + + for k, v in ipairs(self._task_queue) do + if k == idx or epoch == v.epoch or callback == v.callback then + table.remove(self._task_queue, k) + return true + end + end +end + +--[[-- +Execute wakeup action. + +This method should be called by the device resume logic in case of a scheduled wakeup. + +It checks if the wakeup was scheduled by us using @{validateWakeupAlarmByProximity}, +executes the task, and schedules the next wakeup if any. + +@treturn bool +--]] +function WakeupMgr:wakeupAction() + if #self._task_queue > 0 then + local task = self._task_queue[1] + if self:validateWakeupAlarmByProximity(task.epoch) then + task.callback() + self:removeTask(1) + if self._task_queue[1] then + -- Set next scheduled wakeup, if any. + self:setWakeupAlarm(self._task_queue[1].epoch) + end + return true + else + return false + end + end +end + +--[[-- +Set wakeup alarm. + +Simple wrapper for @{ffi.rtc.setWakeupAlarm}. +--]] +function WakeupMgr:setWakeupAlarm(epoch, enabled) + return RTC:setWakeupAlarm(epoch, enabled) +end + +--[[-- +Unset wakeup alarm. + +Simple wrapper for @{ffi.rtc.unsetWakeupAlarm}. +--]] +function WakeupMgr:unsetWakeupAlarm() + return RTC:unsetWakeupAlarm() +end + +--[[-- +Get wakealarm as set by us. + +Simple wrapper for @{ffi.rtc.getWakeupAlarm}. +--]] +function WakeupMgr:getWakeupAlarm() + return RTC:getWakeupAlarm() +end + +--[[-- +Get RTC wakealarm from system. + +Simple wrapper for @{ffi.rtc.getWakeupAlarm}. +--]] +function WakeupMgr:getWakeupAlarmSys() + return RTC:getWakeupAlarmSys() +end + +--[[-- +Validate wakeup alarm. + +Checks if we set the alarm. + +Simple wrapper for @{ffi.rtc.validateWakeupAlarmByProximity}. +--]] +function WakeupMgr:validateWakeupAlarmByProximity() + return RTC:validateWakeupAlarmByProximity() +end + +--[[-- +Check if a wakeup is scheduled. + +Simple wrapper for @{ffi.rtc.isWakeupAlarmScheduled}. +--]] +function WakeupMgr:isWakeupAlarmScheduled() + local wakeup_scheduled = RTC:isWakeupAlarmScheduled() + logger.dbg("isWakeupAlarmScheduled", wakeup_scheduled) + return wakeup_scheduled +end + +return WakeupMgr diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index 27bfbdbf3..ab1df71e6 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -42,6 +42,7 @@ local order = { "time", "battery", "autosuspend", + "autoshutdown", "ignore_sleepcover", "ignore_open_sleepcover", "mass_storage_settings", diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index ea0876e88..0b326aad9 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -61,6 +61,7 @@ local order = { "time", "battery", "autosuspend", + "autoshutdown", "ignore_sleepcover", "ignore_open_sleepcover", "mass_storage_settings", diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 7cb9f8b56..26ff2a770 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -386,7 +386,7 @@ function UIManager:close(widget, refreshtype, refreshregion, refreshdither) end -- schedule an execution task, task queue is in ascendant order -function UIManager:schedule(time, action) +function UIManager:schedule(time, action, ...) local p, s, e = 1, 1, #self._task_queue if e ~= 0 then local us = time[1] * MILLION + time[2] @@ -416,7 +416,11 @@ function UIManager:schedule(time, action) end until e < s end - table.insert(self._task_queue, p, { time = time, action = action }) + table.insert(self._task_queue, p, { + time = time, + action = action, + args = {...}, + }) self._task_queue_dirty = true end dbg:guard(UIManager, 'schedule', @@ -426,7 +430,7 @@ dbg:guard(UIManager, 'schedule', end) --- Schedules task in a certain amount of seconds (fractions allowed) from now. -function UIManager:scheduleIn(seconds, action) +function UIManager:scheduleIn(seconds, action, ...) local when = { util.gettime() } local s = math.floor(seconds) local usecs = (seconds - s) * MILLION @@ -436,7 +440,7 @@ function UIManager:scheduleIn(seconds, action) when[1] = when[1] + 1 when[2] = when[2] - MILLION end - self:schedule(when, action) + self:schedule(when, action, ...) end dbg:guard(UIManager, 'scheduleIn', function(self, seconds, action) @@ -743,7 +747,7 @@ function UIManager:_checkTasks() -- task is pending to be executed right now. do it. -- NOTE: be careful that task.action() might modify -- _task_queue here. So need to avoid race condition - task.action() + task.action(unpack(task.args or {})) else -- queue is sorted in ascendant order, safe to assume all items -- are future tasks for now diff --git a/plugins/autosuspend.koplugin/_meta.lua b/plugins/autosuspend.koplugin/_meta.lua index 573b9fde8..bd3dd16bb 100644 --- a/plugins/autosuspend.koplugin/_meta.lua +++ b/plugins/autosuspend.koplugin/_meta.lua @@ -3,4 +3,5 @@ return { name = "autosuspend", fullname = _("Auto suspend"), description = _([[Suspends the device after a period of inactivity.]]), + sorting_hint = "device", } diff --git a/plugins/autosuspend.koplugin/main.lua b/plugins/autosuspend.koplugin/main.lua index 690fe510a..a555c3eb7 100644 --- a/plugins/autosuspend.koplugin/main.lua +++ b/plugins/autosuspend.koplugin/main.lua @@ -11,10 +11,15 @@ local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") local _ = require("gettext") +local T = require("ffi/util").template -local AutoSuspend = { +local default_autoshutdown_timeout_seconds = 3*24*60*60 + +local AutoSuspend = WidgetContainer:new{ + name = "autosuspend", + is_doc_only = false, + autoshutdown_sec = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds, settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/koboautosuspend.lua"), - settings_id = 0, last_action_sec = os.time(), } @@ -43,54 +48,65 @@ function AutoSuspend:_enabled() return self.auto_suspend_sec > 0 end -function AutoSuspend:_schedule(settings_id) +function AutoSuspend:_enabledShutdown() + return Device:canPowerOff() and self.autoshutdown_sec > 0 +end + +function AutoSuspend:_schedule() if not self:_enabled() then logger.dbg("AutoSuspend:_schedule is disabled") return end - if self.settings_id ~= settings_id then - logger.dbg("AutoSuspend:_schedule registered settings_id ", - settings_id, - " does not equal to current one ", - self.settings_id) - return - end - local delay + local delay_suspend, delay_shutdown if PluginShare.pause_auto_suspend then - delay = self.auto_suspend_sec + delay_suspend = self.auto_suspend_sec + delay_shutdown = self.autoshutdown_sec else - delay = self.last_action_sec + self.auto_suspend_sec - os.time() + delay_suspend = self.last_action_sec + self.auto_suspend_sec - os.time() + delay_shutdown = self.last_action_sec + self.autoshutdown_sec - os.time() end - if delay <= 0 then + if delay_suspend <= 0 then logger.dbg("AutoSuspend: will suspend the device") UIManager:suspend() + elseif delay_shutdown <= 0 then + logger.dbg("AutoSuspend: initiating shutdown") + UIManager:poweroff_action() else - logger.dbg("AutoSuspend: schedule at ", os.time() + delay) - UIManager:scheduleIn(delay, function() self:_schedule(settings_id) end) + if self:_enabled() then + logger.dbg("AutoSuspend: schedule suspend at ", os.time() + delay_suspend) + UIManager:scheduleIn(delay_suspend, self._schedule, self) + end + if self:_enabledShutdown() then + logger.dbg("AutoSuspend: schedule shutdown at ", os.time() + delay_shutdown) + UIManager:scheduleIn(delay_shutdown, self._schedule, self) + end end end -function AutoSuspend:_deprecateLastTask() - self.settings_id = self.settings_id + 1 - logger.dbg("AutoSuspend: deprecateLastTask ", self.settings_id) +function AutoSuspend:_unschedule() + logger.dbg("AutoSuspend: unschedule") + UIManager:unschedule(self._schedule) end function AutoSuspend:_start() - if self:_enabled() then + if self:_enabled() or self:_enabledShutdown() then logger.dbg("AutoSuspend: start at ", os.time()) self.last_action_sec = os.time() - self:_schedule(self.settings_id) + self:_schedule() end end function AutoSuspend:init() UIManager.event_hook:registerWidget("InputEvent", self) self.auto_suspend_sec = self:_readTimeoutSec() - self:_deprecateLastTask() + self:_unschedule() self:_start() + -- self.ui is nil in the testsuite + if not self.ui or not self.ui.menu then return end + self.ui.menu:registerToMainMenu(self) end function AutoSuspend:onInputEvent() @@ -98,11 +114,14 @@ function AutoSuspend:onInputEvent() self.last_action_sec = os.time() end --- We do not want auto suspend procedure to waste battery during suspend. So let's unschedule it --- when suspending and restart it after resume. function AutoSuspend:onSuspend() logger.dbg("AutoSuspend: onSuspend") - self:_deprecateLastTask() + -- We do not want auto suspend procedure to waste battery during suspend. So let's unschedule it + -- when suspending and restart it after resume. + self:_unschedule() + if self:_enabledShutdown() and Device.wakeup_mgr then + Device.wakeup_mgr:addTask(self.autoshutdown_sec, UIManager.poweroff_action) + end end function AutoSuspend:onResume() @@ -110,23 +129,9 @@ function AutoSuspend:onResume() self:_start() end -AutoSuspend:init() - -local AutoSuspendWidget = WidgetContainer:new{ - name = "autosuspend", -} - -function AutoSuspendWidget:addToMainMenu(menu_items) +function AutoSuspend:addToMainMenu(menu_items) menu_items.autosuspend = { text = _("Autosuspend timeout"), - -- This won't ever be registered if the plugin is disabled ;). - --[[ - enabled_func = function() - -- NOTE: Pilfered from frontend/pluginloader.lua - local plugins_disabled = G_reader_settings:readSetting("plugins_disabled") or {} - return plugins_disabled["autosuspend"] ~= true - end, - --]] callback = function() local InfoMessage = require("ui/widget/infomessage") local Screen = Device.screen @@ -141,11 +146,53 @@ function AutoSuspendWidget:addToMainMenu(menu_items) ok_text = _("Set timeout"), title_text = _("Timeout in minutes"), callback = function(autosuspend_spin) - G_reader_settings:saveSetting("auto_suspend_timeout_seconds", autosuspend_spin.value * 60) - -- NOTE: Will only take effect after a restart, as we don't have a method to set this live... + local autosuspend_timeout_seconds = autosuspend_spin.value * 60 + self.auto_suspend_sec = autosuspend_timeout_seconds + G_reader_settings:saveSetting("auto_suspend_timeout_seconds", autosuspend_timeout_seconds) UIManager:show(InfoMessage:new{ - text = _("This will take effect on next restart."), + text = T(_("The system will automatically suspend after %1 minutes of inactivity."), + string.format("%.2f", autosuspend_timeout_seconds/60)), + timeout = 3, }) + self:_unschedule() + self:_start() + end + } + UIManager:show(autosuspend_spin) + end, + } + if not (Device:canPowerOff() or Device:isEmulator()) then return end + menu_items.autoshutdown = { + text = _("Autoshutdown timeout"), + callback = function() + local InfoMessage = require("ui/widget/infomessage") + local Screen = Device.screen + local SpinWidget = require("ui/widget/spinwidget") + local curr_items = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds + local autosuspend_spin = SpinWidget:new { + width = Screen:getWidth() * 0.6, + value = curr_items / 60 / 60, + -- About a minute, good for testing and battery life fanatics. + -- Just high enough to avoid an instant shutdown death scenario. + value_min = 0.017, + -- More than three weeks seems a bit excessive if you want to enable authoshutdown, + -- even if the battery can last up to three months. + value_max = 28*24, + value_hold_step = 24, + precision = "%.2f", + ok_text = _("Set timeout"), + title_text = _("Timeout in hours"), + callback = function(autosuspend_spin) + local autoshutdown_timeout_seconds = math.floor(autosuspend_spin.value * 60*60) + self.autoshutdown_timeout_seconds = autoshutdown_timeout_seconds + G_reader_settings:saveSetting("autoshutdown_timeout_seconds", autoshutdown_timeout_seconds) + UIManager:show(InfoMessage:new{ + text = T(_("The system will automatically shut down after %1 hours of inactivity."), + string.format("%.2f", autoshutdown_timeout_seconds/60/60)), + timeout = 3, + }) + self:_unschedule() + self:_start() end } UIManager:show(autosuspend_spin) @@ -153,22 +200,4 @@ function AutoSuspendWidget:addToMainMenu(menu_items) } end -function AutoSuspendWidget:init() - -- self.ui is nil in the testsuite - if not self.ui or not self.ui.menu then return end - self.ui.menu:registerToMainMenu(self) -end - -function AutoSuspendWidget:onInputEvent() - AutoSuspend:onInputEvent() -end - -function AutoSuspendWidget:onSuspend() - AutoSuspend:onSuspend() -end - -function AutoSuspendWidget:onResume() - AutoSuspend:onResume() -end - -return AutoSuspendWidget +return AutoSuspend diff --git a/spec/unit/autosuspend_spec.lua b/spec/unit/autosuspend_spec.lua index be62d164d..1c1794294 100644 --- a/spec/unit/autosuspend_spec.lua +++ b/spec/unit/autosuspend_spec.lua @@ -1,75 +1,120 @@ -describe("AutoSuspend widget tests", function() +describe("AutoSuspend", function() setup(function() require("commonrequire") package.unloadAll() require("document/canvascontext"):init(require("device")) end) - before_each(function() - local Device = require("device") - stub(Device, "isKobo") - Device.isKobo.returns(true) - Device.input.waitEvent = function() end - local UIManager = require("ui/uimanager") - stub(UIManager, "suspend") - UIManager._run_forever = true - G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10) - require("mock_time"):install() - end) + describe("suspend", function() + before_each(function() + local Device = require("device") + stub(Device, "isKobo") + Device.isKobo.returns(true) + Device.input.waitEvent = function() end + local UIManager = require("ui/uimanager") + stub(UIManager, "suspend") + UIManager._run_forever = true + G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10) + require("mock_time"):install() + end) - after_each(function() - require("device").isKobo:revert() - require("ui/uimanager").suspend:revert() - G_reader_settings:delSetting("auto_suspend_timeout_seconds") - require("mock_time"):uninstall() - end) + after_each(function() + require("device").isKobo:revert() + require("ui/uimanager").suspend:revert() + G_reader_settings:delSetting("auto_suspend_timeout_seconds") + require("mock_time"):uninstall() + end) - it("should be able to execute suspend when timing out", function() - local mock_time = require("mock_time") - local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") - local widget = widget_class:new() --luacheck: ignore - local UIManager = require("ui/uimanager") - mock_time:increase(5) - UIManager:handleInput() - assert.stub(UIManager.suspend).was.called(0) - mock_time:increase(6) - UIManager:handleInput() - assert.stub(UIManager.suspend).was.called(1) - mock_time:uninstall() - end) + it("should be able to execute suspend when timing out", function() + local mock_time = require("mock_time") + local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") + local widget = widget_class:new() --luacheck: ignore + local UIManager = require("ui/uimanager") + mock_time:increase(5) + UIManager:handleInput() + assert.stub(UIManager.suspend).was.called(0) + mock_time:increase(6) + UIManager:handleInput() + assert.stub(UIManager.suspend).was.called(1) + mock_time:uninstall() + end) - it("should be able to initialize several times", function() - local mock_time = require("mock_time") - -- AutoSuspend plugin set the last_action_sec each time it is initialized. - local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") - local widget1 = widget_class:new() --luacheck: ignore - -- So if one more initialization happens, it won't sleep after another 5 seconds. - mock_time:increase(5) - local widget2 = widget_class:new() --luacheck: ignore - local UIManager = require("ui/uimanager") - mock_time:increase(6) - UIManager:handleInput() - assert.stub(UIManager.suspend).was.called(1) - mock_time:uninstall() + it("should be able to deprecate last task", function() + local mock_time = require("mock_time") + local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") + local widget = widget_class:new() + mock_time:increase(5) + local UIManager = require("ui/uimanager") + UIManager:handleInput() + assert.stub(UIManager.suspend).was.called(0) + widget:onInputEvent() + widget:onSuspend() + widget:onResume() + mock_time:increase(6) + UIManager:handleInput() + assert.stub(UIManager.suspend).was.called(0) + mock_time:increase(5) + UIManager:handleInput() + assert.stub(UIManager.suspend).was.called(1) + mock_time:uninstall() + end) end) - it("should be able to deprecate last task", function() - local mock_time = require("mock_time") - local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") - local widget = widget_class:new() - mock_time:increase(5) - local UIManager = require("ui/uimanager") - UIManager:handleInput() - assert.stub(UIManager.suspend).was.called(0) - widget:onInputEvent() - widget:onSuspend() - widget:onResume() - mock_time:increase(6) - UIManager:handleInput() - assert.stub(UIManager.suspend).was.called(0) - mock_time:increase(5) - UIManager:handleInput() - assert.stub(UIManager.suspend).was.called(1) - mock_time:uninstall() + describe("shutdown", function() + --- @todo duplicate with above, elegant way to DRY? + before_each(function() + local Device = require("device") + stub(Device, "isKobo") + Device.isKobo.returns(true) + stub(Device, "canPowerOff") + Device.canPowerOff.returns(true) + Device.input.waitEvent = function() end + local UIManager = require("ui/uimanager") + stub(UIManager, "poweroff_action") + UIManager._run_forever = true + G_reader_settings:saveSetting("autoshutdown_timeout_seconds", 10) + require("mock_time"):install() + end) + + after_each(function() + require("device").isKobo:revert() + require("ui/uimanager").poweroff_action:revert() + G_reader_settings:delSetting("autoshutdown_timeout_seconds") + require("mock_time"):uninstall() + end) + + it("should be able to execute suspend when timing out", function() + local mock_time = require("mock_time") + local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") + local widget = widget_class:new() --luacheck: ignore + local UIManager = require("ui/uimanager") + mock_time:increase(5) + UIManager:handleInput() + assert.stub(UIManager.poweroff_action).was.called(0) + mock_time:increase(6) + UIManager:handleInput() + assert.stub(UIManager.poweroff_action).was.called(1) + mock_time:uninstall() + end) + + it("should be able to deprecate last task", function() + local mock_time = require("mock_time") + local widget_class = dofile("plugins/autosuspend.koplugin/main.lua") + local widget = widget_class:new() + mock_time:increase(5) + local UIManager = require("ui/uimanager") + UIManager:handleInput() + assert.stub(UIManager.poweroff_action).was.called(0) + widget:onInputEvent() + widget:onSuspend() + widget:onResume() + mock_time:increase(6) + UIManager:handleInput() + assert.stub(UIManager.poweroff_action).was.called(0) + mock_time:increase(5) + UIManager:handleInput() + assert.stub(UIManager.poweroff_action).was.called(1) + mock_time:uninstall() + end) end) end) diff --git a/spec/unit/uimanager_spec.lua b/spec/unit/uimanager_spec.lua index 47073ed7e..e22632d3f 100644 --- a/spec/unit/uimanager_spec.lua +++ b/spec/unit/uimanager_spec.lua @@ -22,9 +22,9 @@ describe("UIManager spec", function() { time = future2, action = noop }, } UIManager:_checkTasks() - assert.are.same(#UIManager._task_queue, 2) - assert.are.same(UIManager._task_queue[1].time, future) - assert.are.same(UIManager._task_queue[2].time, future2) + assert.are.same(2, #UIManager._task_queue, 2) + assert.are.same(future, UIManager._task_queue[1].time) + assert.are.same(future2, UIManager._task_queue[2].time) end) it("should calcualte wait_until properly in checkTasks routine", function() @@ -39,7 +39,7 @@ describe("UIManager spec", function() { time = {future[1] + 5, future[2]}, action = noop }, } wait_until, now = UIManager:_checkTasks() - assert.are.same(wait_until, future) + assert.are.same(future, wait_until) end) it("should return nil wait_until properly in checkTasks routine", function() @@ -51,7 +51,7 @@ describe("UIManager spec", function() { time = now, action = noop }, } wait_until, now = UIManager:_checkTasks() - assert.are.same(wait_until, nil) + assert.are.same(nil, wait_until) end) it("should insert new task properly in empty task queue", function() @@ -61,7 +61,7 @@ describe("UIManager spec", function() assert.are.same(0, #UIManager._task_queue) UIManager:scheduleIn(50, 'foo') assert.are.same(1, #UIManager._task_queue) - assert.are.same(UIManager._task_queue[1].action, 'foo') + assert.are.same('foo', UIManager._task_queue[1].action) end) it("should insert new task properly in single task queue", function() @@ -74,7 +74,7 @@ describe("UIManager spec", function() assert.are.same(1, #UIManager._task_queue) UIManager:scheduleIn(150, 'quz') assert.are.same(2, #UIManager._task_queue) - assert.are.same(UIManager._task_queue[1].action, 'quz') + assert.are.same('quz', UIManager._task_queue[1].action) UIManager:quit() UIManager._task_queue = { @@ -83,10 +83,10 @@ describe("UIManager spec", function() assert.are.same(1, #UIManager._task_queue) UIManager:scheduleIn(150, 'foo') assert.are.same(2, #UIManager._task_queue) - assert.are.same(UIManager._task_queue[2].action, 'foo') + assert.are.same('foo', UIManager._task_queue[2].action) UIManager:scheduleIn(155, 'bar') assert.are.same(3, #UIManager._task_queue) - assert.are.same(UIManager._task_queue[3].action, 'bar') + assert.are.same('bar', UIManager._task_queue[3].action) end) it("should insert new task in ascendant order", function() @@ -151,7 +151,7 @@ describe("UIManager spec", function() { time = now, action = task_to_remove }, } UIManager:_checkTasks() - assert.are.same(run_count, 2) + assert.are.same(2, run_count) end) it("should clear _task_queue_dirty bit before looping", function() @@ -181,8 +181,8 @@ describe("UIManager spec", function() end }) - assert.equals(UIManager._window_stack[1].widget.x_prefix_test_number, 2) - assert.equals(UIManager._window_stack[2].widget.x_prefix_test_number, 1) + assert.equals(2, UIManager._window_stack[1].widget.x_prefix_test_number) + assert.equals(1, UIManager._window_stack[2].widget.x_prefix_test_number) end) it("should insert second modal widget on top of first modal widget", function() UIManager:show({ @@ -193,9 +193,9 @@ describe("UIManager spec", function() end }) - assert.equals(UIManager._window_stack[1].widget.x_prefix_test_number, 2) - assert.equals(UIManager._window_stack[2].widget.x_prefix_test_number, 1) - assert.equals(UIManager._window_stack[3].widget.x_prefix_test_number, 3) + assert.equals(2, UIManager._window_stack[1].widget.x_prefix_test_number) + assert.equals(1, UIManager._window_stack[2].widget.x_prefix_test_number) + assert.equals(3, UIManager._window_stack[3].widget.x_prefix_test_number) end) end) @@ -306,9 +306,9 @@ describe("UIManager spec", function() } UIManager:sendEvent("foo") - assert.is.same(call_signals[1], 1) - assert.is.same(call_signals[2], 1) - assert.is.same(call_signals[3], 1) + assert.is.same(1, call_signals[1]) + assert.is.same(1, call_signals[2]) + assert.is.same(1, call_signals[3]) end) it("should handle stack change when broadcasting events", function() @@ -350,7 +350,7 @@ describe("UIManager spec", function() }, } UIManager:broadcastEvent("foo") - assert.is.same(#UIManager._window_stack, 0) + assert.is.same(0, #UIManager._window_stack) end) it("should handle stack change when closing widgets", function() diff --git a/spec/unit/wakeupmgr_spec.lua b/spec/unit/wakeupmgr_spec.lua new file mode 100644 index 000000000..4cd3191ca --- /dev/null +++ b/spec/unit/wakeupmgr_spec.lua @@ -0,0 +1,55 @@ +describe("WakeupMgr", function() + local RTC + local WakeupMgr + local epoch1, epoch2, epoch3 + + setup(function() + require("commonrequire") + package.unloadAll() + RTC = require("ffi/rtc") + WakeupMgr = require("device/wakeupmgr") + -- We could theoretically test this by running the tests as root locally. + stub(WakeupMgr, "setWakeupAlarm") + WakeupMgr.validateWakeupAlarmByProximity = spy.new(function() return true end) + + epoch1 = RTC:secondsFromNowToEpoch(1234) + epoch2 = RTC:secondsFromNowToEpoch(123) + epoch3 = RTC:secondsFromNowToEpoch(9999) + end) + + it("should add a task", function() + WakeupMgr:addTask(1234, function() end) + assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch) + assert.stub(WakeupMgr.setWakeupAlarm).was.called(1) + end) + it("should add a task in order", function() + WakeupMgr:addTask(9999, function() end) + assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch) + assert.stub(WakeupMgr.setWakeupAlarm).was.called(1) + + WakeupMgr:addTask(123, function() end) + assert.is_equal(epoch2, WakeupMgr._task_queue[1].epoch) + assert.stub(WakeupMgr.setWakeupAlarm).was.called(2) + end) + it("should execute top task", function() + assert.is_true(WakeupMgr:wakeupAction()) + end) + it("should have removed executed task from stack", function() + assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch) + assert.is_equal(epoch3, WakeupMgr._task_queue[2].epoch) + end) + it("should have scheduled next task after execution", function() + assert.stub(WakeupMgr.setWakeupAlarm).was.called(3) + end) + it("should remove arbitrary task from stack", function() + WakeupMgr:removeTask(2) + assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch) + assert.is_equal(nil, WakeupMgr._task_queue[2]) + end) + it("should execute last task", function() + assert.is_true(WakeupMgr:wakeupAction()) + end) + it("should not have scheduled a wakeup without a task", function() + assert.stub(WakeupMgr.setWakeupAlarm).was.called(3) + end) +end)