local lu = require('luaunit')
local common_def = require('common_def')
local orig_storageconfig = package.loaded['storageconfig.storageconfig_object']
local orig_rpc_controller = package.loaded['rpc_services.rpc_service_controller']
local skynet = require('skynet')
local ctrl_backup
local orig_fork = skynet.fork
local rpc_results = {}
local rpc_controller_stub = {}
for _, m in ipairs({
'set_copyback_state',
'set_smart_copyback_state',
'set_jbod_state',
'set_maintain_pd_fail_history_state',
'set_no_battery_write_cache',
'set_read_cache_percent',
'set_work_mode',
'set_write_cache_policy',
}) do
rpc_controller_stub[m] = function(...)
if rpc_results[m] == nil then
return true
end
if type(rpc_results[m]) == 'table' then
error(rpc_results[m])
end
return rpc_results[m]
end
end
local fail_save = false
local fail_delete = false
local mock_tables = {}
local mock_db
local function reset_mock_db()
mock_tables = {}
fail_save = false
fail_delete = false
end
local function match_conds(rec, conds)
for k, v in pairs(conds) do
if rec[k] ~= v then
return false
end
end
return true
end
local function normalize_where(...)
local conds = {}
local args = { ... }
if #args == 1 and type(args[1]) == 'table' then
for k, v in pairs(args[1]) do
conds[k] = v
end
else
for _, c in ipairs(args) do
if type(c) == 'table' and c.__field then
conds[c.__field] = c.__value
end
end
end
return conds
end
local function make_mock_db()
local db = {}
local function make_chain(tbl_name)
local chain = {}
local conds = {}
function chain:where(...)
for k, v in pairs(normalize_where(...)) do
conds[k] = v
end
return chain
end
function chain:first()
for _, r in ipairs(mock_tables[tbl_name] or {}) do
if match_conds(r, conds) then
return r
end
end
return nil
end
function chain:all()
local out = {}
for _, r in ipairs(mock_tables[tbl_name] or {}) do
if match_conds(r, conds) then
out[#out + 1] = r
end
end
return out
end
return chain
end
local function make_tbl(tbl_name)
local t = { __tbl_name = tbl_name }
setmetatable(t, {
__call = function(_, snapshot)
local rec = {}
for k, v in pairs(snapshot) do
rec[k] = v
end
function rec:save()
if fail_save then
error('mock save failed')
end
local list = mock_tables[tbl_name]
for _, r in ipairs(list or {}) do
if r == rec then
return
end
end
mock_tables[tbl_name] = mock_tables[tbl_name] or {}
mock_tables[tbl_name][#mock_tables[tbl_name] + 1] = rec
end
function rec:delete()
if fail_delete then
error('mock delete failed')
end
local list = mock_tables[tbl_name] or {}
for i, r in ipairs(list) do
if r == rec then
table.remove(list, i)
return
end
end
end
return rec
end,
})
for _, f in ipairs({
'ControllerTypeId',
'ControllerSlotId',
'BackupTimestamp',
'DriveModel',
'DriveSlotNumber',
}) do
t[f] = {
eq = function(_, v)
return { __field = f, __value = v }
end,
}
end
return t
end
db.ControllerConfigBackup = make_tbl('ControllerConfigBackup')
db.DriveConfigBackup = make_tbl('DriveConfigBackup')
function db:select(tbl)
return make_chain(tbl.__tbl_name)
end
return db
end
local storageconfig_stub
local function reset_storageconfig_stub()
storageconfig_stub = {
get_instance = function()
return { db = mock_db }
end,
}
end
local signals = {}
local function make_bus()
return {
signal = function(bus, path, intf, name, sig, args, value)
signals[#signals + 1] = { path = path, intf = intf, name = name, value = value }
end,
}
end
local function make_controller(overrides)
local ctrl = {
Id = 0,
SlotId = 0,
TypeId = 1,
Name = 'Ctrl0',
obj = { path = '/Storage/Controller0' },
bus = make_bus(),
config_backed_up = false,
CopyBackState = 1,
SmarterCopyBackState = 1,
JBODState = 0,
MaintainPDFailHistrory = 1,
NoBatteryWriteCacheEnabled = 0,
ReadCachePercent = 30,
WorkMode = 'RAID',
UnconfiguredDriveWriteCachePolicy = 'WriteThrough',
HBADriveWriteCachePolicy = 'WriteThrough',
ConfiguredDriveWriteCachePolicy = 'WriteThrough',
}
if overrides then
for k, v in pairs(overrides) do
ctrl[k] = v
end
end
return ctrl
end
local function reload_module()
package.loaded['storageconfig.storageconfig_object'] = nil
package.loaded['rpc_services.rpc_service_controller'] = nil
package.loaded['controller.ctrl_config_backup'] = nil
package.loaded['storageconfig.storageconfig_object'] = storageconfig_stub
package.loaded['rpc_services.rpc_service_controller'] = rpc_controller_stub
ctrl_backup = require('controller.ctrl_config_backup').get_instance()
end
TestCtrlConfigBackup = {}
function TestCtrlConfigBackup:setUp()
reset_mock_db()
mock_db = make_mock_db()
reset_storageconfig_stub()
rpc_results = {}
signals = {}
skynet.fork = orig_fork
reload_module()
end
function TestCtrlConfigBackup:tearDown()
skynet.fork = orig_fork
package.loaded['storageconfig.storageconfig_object'] = orig_storageconfig
package.loaded['rpc_services.rpc_service_controller'] = orig_rpc_controller
end
function TestCtrlConfigBackup:test_compare_config_u8_nil_equals_invalid()
local controller = make_controller()
controller.ReadCachePercent = nil
local persisted = {
ReadCachePercent = common_def.INVALID_U8,
CopyBackState = 1,
SmarterCopyBackState = 1,
JBODState = 0,
MaintainPDFailHistory = 1,
NoBatteryWriteCacheEnabled = 0,
WorkMode = 'RAID',
UnconfiguredDriveWriteCachePolicy = 'WriteThrough',
HBADriveWriteCachePolicy = 'WriteThrough',
ConfiguredDriveWriteCachePolicy = 'WriteThrough',
}
lu.assertNil(ctrl_backup:compare_config(controller, persisted))
end
function TestCtrlConfigBackup:test_compare_config_str_nil_equals_na()
local controller = make_controller()
controller.WorkMode = nil
local persisted = {
ReadCachePercent = 30,
CopyBackState = 1,
SmarterCopyBackState = 1,
JBODState = 0,
MaintainPDFailHistory = 1,
NoBatteryWriteCacheEnabled = 0,
WorkMode = common_def.INVALID_STRING,
UnconfiguredDriveWriteCachePolicy = 'WriteThrough',
HBADriveWriteCachePolicy = 'WriteThrough',
ConfiguredDriveWriteCachePolicy = 'WriteThrough',
}
lu.assertNil(ctrl_backup:compare_config(controller, persisted))
end
function TestCtrlConfigBackup:test_compare_config_detects_diff()
local controller = make_controller({ CopyBackState = 0 })
local persisted = {
ReadCachePercent = 30,
CopyBackState = 1,
SmarterCopyBackState = 1,
JBODState = 0,
MaintainPDFailHistory = 1,
NoBatteryWriteCacheEnabled = 0,
WorkMode = 'RAID',
UnconfiguredDriveWriteCachePolicy = 'WriteThrough',
HBADriveWriteCachePolicy = 'WriteThrough',
ConfiguredDriveWriteCachePolicy = 'WriteThrough',
}
local diff = ctrl_backup:compare_config(controller, persisted)
lu.assertNotNil(diff)
lu.assertEquals(diff.CopyBackState.current, 0)
lu.assertEquals(diff.CopyBackState.persisted, 1)
end
function TestCtrlConfigBackup:test_compare_config_missing_persisted_field_no_diff()
local controller = make_controller({
ReadCachePercent = common_def.INVALID_U8,
SmarterCopyBackState = common_def.INVALID_U8,
JBODState = common_def.INVALID_U8,
MaintainPDFailHistrory = common_def.INVALID_U8,
NoBatteryWriteCacheEnabled = common_def.INVALID_U8,
WorkMode = common_def.INVALID_STRING,
UnconfiguredDriveWriteCachePolicy = common_def.INVALID_STRING,
HBADriveWriteCachePolicy = common_def.INVALID_STRING,
ConfiguredDriveWriteCachePolicy = common_def.INVALID_STRING,
})
local persisted = { CopyBackState = 1 }
lu.assertNil(ctrl_backup:compare_config(controller, persisted))
end
function TestCtrlConfigBackup:test_upsert_backup_insert_normalizes()
local controller = make_controller()
controller.ReadCachePercent = nil
controller.WorkMode = nil
local ok = ctrl_backup:upsert_backup(controller)
lu.assertTrue(ok)
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(#records, 1)
lu.assertEquals(records[1].ControllerTypeId, controller.TypeId)
lu.assertEquals(records[1].ControllerSlotId, controller.SlotId)
lu.assertEquals(records[1].ReadCachePercent, common_def.INVALID_U8)
lu.assertEquals(records[1].WorkMode, common_def.INVALID_STRING)
lu.assertEquals(records[1].CopyBackState, 1)
end
function TestCtrlConfigBackup:test_upsert_backup_update()
mock_db
.ControllerConfigBackup({
ControllerTypeId = 1,
ControllerSlotId = 0,
CopyBackState = 1,
ReadCachePercent = 30,
WorkMode = 'RAID',
BackupTimestamp = 1,
})
:save()
local controller = make_controller({ ReadCachePercent = 60 })
local ok = ctrl_backup:upsert_backup(controller)
lu.assertTrue(ok)
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(#records, 1)
lu.assertEquals(records[1].ReadCachePercent, 60)
lu.assertTrue(records[1].BackupTimestamp > 0)
end
function TestCtrlConfigBackup:test_upsert_backup_insert_save_failure_returns_false()
fail_save = true
local controller = make_controller()
lu.assertFalse(ctrl_backup:upsert_backup(controller))
end
function TestCtrlConfigBackup:test_evict_if_needed_evicts_oldest_per_slot()
for i = 1, 5 do
mock_db
.ControllerConfigBackup({
ControllerTypeId = i,
ControllerSlotId = 1,
BackupTimestamp = i,
})
:save()
end
ctrl_backup:evict_if_needed(1)
ctrl_backup:upsert_backup(make_controller({ TypeId = 6, SlotId = 1 }))
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(#records, 5)
local has_oldest = false
for _, r in ipairs(records) do
if r.BackupTimestamp == 1 then
has_oldest = true
end
end
lu.assertFalse(has_oldest)
mock_db.ControllerConfigBackup({ ControllerTypeId = 9, ControllerSlotId = 2, BackupTimestamp = 9 }):save()
ctrl_backup:evict_if_needed(2)
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 6)
end
function TestCtrlConfigBackup:test_evict_if_needed_delete_failure_breaks()
for i = 1, 5 do
mock_db
.ControllerConfigBackup({
ControllerTypeId = i,
ControllerSlotId = 1,
BackupTimestamp = i,
})
:save()
end
fail_delete = true
ctrl_backup:evict_if_needed(1)
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 5)
end
function TestCtrlConfigBackup:test_set_need_recover_signals()
local controller = make_controller()
ctrl_backup:set_need_recover(controller, true)
lu.assertEquals(signals[#signals].value, true)
ctrl_backup:set_need_recover(controller, false)
lu.assertEquals(signals[#signals].value, false)
end
function TestCtrlConfigBackup:test_on_first_config_loaded_new_controller_backs_up()
local controller = make_controller()
ctrl_backup:on_first_config_loaded(controller)
lu.assertTrue(controller.config_backed_up)
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
lu.assertEquals(signals[#signals].value, false)
end
function TestCtrlConfigBackup:test_on_first_config_loaded_no_diff_skips()
ctrl_backup:upsert_backup(make_controller())
local controller = make_controller()
ctrl_backup:on_first_config_loaded(controller)
lu.assertTrue(controller.config_backed_up)
lu.assertEquals(signals[#signals].value, false)
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
end
function TestCtrlConfigBackup:test_on_first_config_loaded_diff_notifies_recover()
ctrl_backup:upsert_backup(make_controller({ CopyBackState = 1 }))
local controller = make_controller({ CopyBackState = 0 })
ctrl_backup:on_first_config_loaded(controller)
lu.assertTrue(controller.config_backed_up)
lu.assertEquals(signals[#signals].value, true)
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
end
function TestCtrlConfigBackup:test_update_backup_prop_single_prop_update()
local controller = make_controller({ CopyBackState = 1 })
ctrl_backup:upsert_backup(controller)
controller.CopyBackState = 0
ctrl_backup:update_backup_prop(controller, 'CopyBackState')
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 0)
lu.assertEquals(records[1].ReadCachePercent, 30)
lu.assertNotNil(records[1].BackupTimestamp)
end
function TestCtrlConfigBackup:test_update_backup_prop_no_record_fallback()
local controller = make_controller()
controller.config_backed_up = true
ctrl_backup:update_backup_prop(controller, 'CopyBackState')
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
end
function TestCtrlConfigBackup:test_update_backup_prop_aligned_clears_need_recover()
ctrl_backup:upsert_backup(make_controller({ CopyBackState = 1 }))
local controller = make_controller({ CopyBackState = 0 })
ctrl_backup:on_first_config_loaded(controller)
lu.assertEquals(signals[#signals].value, true)
controller.CopyBackState = 1
ctrl_backup:update_backup_prop(controller, 'CopyBackState')
lu.assertEquals(signals[#signals].value, false)
end
function TestCtrlConfigBackup:test_recover_success()
ctrl_backup:upsert_backup(make_controller({ CopyBackState = 1, ReadCachePercent = 30 }))
local controller = make_controller({ CopyBackState = 0, ReadCachePercent = 30 })
controller.config_backed_up = true
lu.assertTrue(ctrl_backup:recover(controller))
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 1)
lu.assertEquals(signals[#signals].value, false)
lu.assertNil(ctrl_backup.recovering_ctrl_ids[controller.Id])
end
function TestCtrlConfigBackup:test_recover_failure_resets_flag()
ctrl_backup:upsert_backup(make_controller({ CopyBackState = 1, ReadCachePercent = 30 }))
rpc_results.set_copyback_state = { 'rpc err' }
local controller = make_controller({ CopyBackState = 0, ReadCachePercent = 30 })
controller.config_backed_up = true
lu.assertFalse(ctrl_backup:recover(controller))
lu.assertFalse(controller.config_backed_up)
lu.assertNil(ctrl_backup.recovering_ctrl_ids[controller.Id])
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 1)
end
function TestCtrlConfigBackup:test_recover_no_diff_clears_need_recover()
local controller = make_controller()
ctrl_backup:upsert_backup(controller)
lu.assertTrue(ctrl_backup:recover(controller))
lu.assertEquals(signals[#signals].value, false)
end
function TestCtrlConfigBackup:test_restore_item_work_mode()
local controller = make_controller()
local ok = ctrl_backup:restore_item(controller, 'WorkMode', 'JBOD')
lu.assertTrue(ok)
ok = ctrl_backup:restore_item(controller, 'WorkMode', 'NotExistMode')
lu.assertFalse(ok)
end
function TestCtrlConfigBackup:test_restore_item_unknown_prop()
local controller = make_controller()
lu.assertFalse(ctrl_backup:restore_item(controller, 'NoSuchProp', 1))
end
function TestCtrlConfigBackup:test_restore_item_rpc_error_caught()
rpc_results.set_jbod_state = { 'rpc err' }
local controller = make_controller()
local ok = ctrl_backup:restore_item(controller, 'JBODState', 1)
lu.assertFalse(ok)
end
function TestCtrlConfigBackup:test_sync_inband_config_no_diff()
local controller = make_controller()
ctrl_backup:upsert_backup(controller)
controller.config_backed_up = true
ctrl_backup:sync_inband_config(controller)
ctrl_backup:sync_inband_config(controller)
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 1)
lu.assertEquals(records[1].ReadCachePercent, 30)
lu.assertEquals(#signals, 0)
end
function TestCtrlConfigBackup:test_sync_inband_config_updates_single_prop()
local controller = make_controller({ CopyBackState = 1 })
ctrl_backup:upsert_backup(controller)
controller.config_backed_up = true
ctrl_backup:sync_inband_config(controller)
controller.CopyBackState = 0
ctrl_backup:sync_inband_config(controller)
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 0)
lu.assertEquals(records[1].ReadCachePercent, 30)
end
function TestCtrlConfigBackup:test_sync_inband_config_unchanged_keeps_db_baseline()
ctrl_backup:upsert_backup(make_controller({ CopyBackState = 1, ReadCachePercent = 30 }))
local controller = make_controller({ CopyBackState = 0, ReadCachePercent = 30 })
controller.config_backed_up = true
ctrl_backup:sync_inband_config(controller)
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 1)
ctrl_backup:sync_inband_config(controller)
ctrl_backup:sync_inband_config(controller)
lu.assertEquals(records[1].CopyBackState, 1)
lu.assertEquals(#signals, 0)
end
function TestCtrlConfigBackup:test_sync_inband_config_skipped_during_recover()
local controller = make_controller({ CopyBackState = 1 })
ctrl_backup:upsert_backup(controller)
controller.config_backed_up = true
ctrl_backup.recovering_ctrl_ids[controller.Id] = true
controller.CopyBackState = 0
ctrl_backup:sync_inband_config(controller)
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(records[1].CopyBackState, 1)
lu.assertEquals(#signals, 0)
end
function TestCtrlConfigBackup:test_cleanup_controller_state()
local controller = make_controller()
ctrl_backup.recovering_ctrl_ids[controller.Id] = true
ctrl_backup.last_read_ctrl_configs[controller.Id] = { CopyBackState = 0 }
ctrl_backup:cleanup_controller_state(controller.Id)
lu.assertNil(ctrl_backup.recovering_ctrl_ids[controller.Id])
lu.assertNil(ctrl_backup.last_read_ctrl_configs[controller.Id])
end
function TestCtrlConfigBackup:test_cleanup_controller_state_nil_id()
lu.assertNil(ctrl_backup:cleanup_controller_state(nil))
end
function TestCtrlConfigBackup:test_upsert_backup_uses_slotid_not_id()
ctrl_backup:upsert_backup(make_controller({ Id = 3, SlotId = 7 }))
local records = mock_tables['ControllerConfigBackup']
lu.assertEquals(#records, 1)
lu.assertEquals(records[1].ControllerSlotId, 7)
end
function TestCtrlConfigBackup:test_on_first_config_loaded_matches_by_slotid_across_id_change()
ctrl_backup:upsert_backup(make_controller({ Id = 3, SlotId = 7, CopyBackState = 1 }))
local controller = make_controller({ Id = 9, SlotId = 7, CopyBackState = 0 })
ctrl_backup:on_first_config_loaded(controller)
lu.assertEquals(signals[#signals].value, true)
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
end
function TestCtrlConfigBackup:test_on_first_config_loaded_invalid_slot_skips()
local controller = make_controller({ SlotId = common_def.INVALID_U8 })
ctrl_backup:on_first_config_loaded(controller)
lu.assertFalse(controller.config_backed_up)
lu.assertNil(mock_tables['ControllerConfigBackup'])
lu.assertEquals(#signals, 0)
end
function TestCtrlConfigBackup:test_upsert_backup_update_failure_returns_false()
ctrl_backup:upsert_backup(make_controller({ CopyBackState = 1 }))
fail_save = true
lu.assertFalse(ctrl_backup:upsert_backup(make_controller({ CopyBackState = 0 })))
end
function TestCtrlConfigBackup:test_update_backup_prop_save_failure_returns_silently()
ctrl_backup:upsert_backup(make_controller())
fail_save = true
ctrl_backup:update_backup_prop(make_controller({ CopyBackState = 0 }), 'CopyBackState')
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
end
function TestCtrlConfigBackup:test_update_backup_timestamp_save_failure_returns_silently()
ctrl_backup:upsert_backup(make_controller())
fail_save = true
ctrl_backup:update_backup_timestamp(make_controller())
lu.assertEquals(#mock_tables['ControllerConfigBackup'], 1)
end