| @@ -1,5 +1,8 @@ | |||
| 1 | # Changelog | 1 | # Changelog |
| 2 | All notable changes to this project will be documented in this file. | 2 | All notable changes to this project will be documented in this file. |
| 3 | +## [1.130.17] - 2026-10-26 | ||
| 4 | +新增 PLDM GetDateTime 异步业务适配器,约束 IPC 数量与期限并校验 UTC BCD 时间;传输入口接线另行提交。 | ||
| 5 | + | ||
| 3 | ## [1.130.15] - 2026-08-25 | 6 | ## [1.130.15] - 2026-08-25 |
| 4 | 更新版本号,触发流水线推包 | 7 | 更新版本号,触发流水线推包 |
| 5 | 8 | ||
| @@ -0,0 +1,13 @@ | |||
| 1 | +# PLDM 取时业务适配器 | ||
| 2 | + | ||
| 3 | +`pldm_time.register(responder, bus)` 注册 BIOS GetDateTime(Type 3、Command 12)。适配器接受提供 `register` 方法的命令注册表;`bus` 必须是完整生产 `SDBus`,同时提供 `timeout_call` 和原始连接 `bus.bus:timeout_call`,不能只传原始D-Bus对象;本 PR 提供可独立测试的业务模块,尚未接入生产收包入口,不宣告 BMC 已支持该命令。传输解码、回复令牌、能力发布与入口接线另行提交。 | ||
| 4 | + | ||
| 5 | +通过 MDB 发现唯一 Time 对象及服务,发现接口保留共享内存快路径,远程回退受剩余预算保护;通过单次有期限属性 RPC 读取 `SyncStatus` 和 `DateTime`,校验日历、时区并编码 UTC BCD。未授时/无效时间返回 `0x8A`;服务缺失、IPC 失败及业务超预算返回 `NOT_READY`。20ms 业务预算先于 50ms 命令期限,单调时钟防止 tick 提前结束或迟到结果被记为成功;最多保留 16 个未返回的 IPC,协议超时不冒充底层调用已取消。 | ||
| 6 | + | ||
| 7 | +单测隔离命令注册接口,覆盖真实接口名称、板级对象路径、单次 RPC、时区/闰年、迟到回调、服务退出和有界等待。此前完整集成候选 `4620c09` 在专用 43000 四张 SP888 上完成 44 次真实 Base/时间/错误码请求,P95 12.38ms、最大 12.94ms;该结果属于集成候选,不冒充本独立模块已接线。真实未授时切换尚未执行。 | ||
| 8 | + | ||
| 9 | +检视补充:单测先核实真实单调时钟 API、timespec 字段和 `SDBus.timeout_call` 存在,再替换时钟。IPC超时返回后释放调用槽位;DEBUG 日志区分对象/owner缺失或歧义、预算耗尽、IPC失败、属性缺失、无效时间及过载,带本地请求编号和阶段,不输出远程错误正文。该业务模块没有传输路由上下文,不伪造Network/EID/Tag等字段。 | ||
| 10 | + | ||
| 11 | +偏移语法按 [RFC3339 §5.6](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6):数值偏移复用00~23的小时字段;不能把现实民用时区范围误当成协议语法的±14:00上限。新增±23:59的UTC跨日验证及24:00拒绝测试;未知偏移-00:00仍不作为有效绝对时间。 | ||
| 12 | + | ||
| 13 | +评审回归:接受RFC3339的小写t/z;16个实际挂起调用到业务期限后仅完成回复,继续保留IPC槽位,防止把未取消的工作误计为已释放而突破上限。底层有期限调用返回后槽位归还,迟到完成不重复应答;若底层违约永久不返回,则保持有界拒绝而不继续堆积协程。该故障需要修复底层,不能靠提前释放计数掩盖。 | ||
| @@ -2,7 +2,7 @@ | |||
| 2 | "name": "mctpd", | 2 | "name": "mctpd", |
| 3 | "type": "application", | 3 | "type": "application", |
| 4 | "deployConfig": "framework.service", | 4 | "deployConfig": "framework.service", |
| 5 | - "version": "1.130.15", | 5 | + "version": "1.130.17", |
| 6 | "author": "Huawei", | 6 | "author": "Huawei", |
| 7 | "license": "Mulan PSL v2", | 7 | "license": "Mulan PSL v2", |
| 8 | "options": { | 8 | "options": { |
| @@ -31,5 +31,6 @@ require('test_routing_table') | |||
| 31 | require('test_eid_table') | 31 | require('test_eid_table') |
| 32 | require('test_mctp_dump') | 32 | require('test_mctp_dump') |
| 33 | require('test_discovery') | 33 | require('test_discovery') |
| 34 | +require('test_pldm_time') | ||
| 34 | 35 | ||
| 35 | os.exit(lu.LuaUnit.run()) | 36 | os.exit(lu.LuaUnit.run()) |
| @@ -114,6 +114,7 @@ function TestMctpDiscovery:test_mctp_discovery_success() | |||
| 114 | end) | 114 | end) |
| 115 | 115 | ||
| 116 | self.mctp_discovery:smbus_set_eid_to_endpoint( | 116 | self.mctp_discovery:smbus_set_eid_to_endpoint( |
| 117 | + 'discovery-success', | ||
| 117 | target_eid, | 118 | target_eid, |
| 118 | ref_chip, | 119 | ref_chip, |
| 119 | medium_type, | 120 | medium_type, |
| @@ -158,6 +159,7 @@ function TestMctpDiscovery:test_mctp_discovery_failed() | |||
| 158 | local ref_chip = { path = path } | 159 | local ref_chip = { path = path } |
| 159 | 160 | ||
| 160 | self.mctp_discovery:smbus_set_eid_to_endpoint( | 161 | self.mctp_discovery:smbus_set_eid_to_endpoint( |
| 162 | + 'discovery-failed', | ||
| 161 | target_eid, | 163 | target_eid, |
| 162 | ref_chip, | 164 | ref_chip, |
| 163 | medium_type, | 165 | medium_type, |
| @@ -0,0 +1,487 @@ | |||||||||
| 1 | +-- Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||||||||
| 2 | +-- openUBMC is licensed under Mulan PSL v2. | ||||||||
| 3 | +-- You can use this software according to the terms and conditions of the Mulan PSL v2. | ||||||||
| 4 | +-- You may obtain a copy of Mulan PSL v2 at: | ||||||||
| 5 | +-- http://license.coscl.org.cn/MulanPSL2 | ||||||||
| 6 | +-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||||||||
| 7 | +-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||||||||
| 8 | +-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||||||||
| 9 | +-- See the Mulan PSL v2 for more details. | ||||||||
| 10 | + | ||||||||
| 11 | +-- SPDX-License-Identifier: MulanPSL-2.0 | ||||||||
| 12 | +local lu = require('luaunit') | ||||||||
| 13 | +local skynet = require('skynet') | ||||||||
| 14 | +local clock = require('utils.core') | ||||||||
| 15 | +local log = require('mc.logging') | ||||||||
| 16 | +-- 本组隔离测试业务适配器;传输令牌/回复生命周期由 responder 集成测试覆盖。 | ||||||||
| 17 | +local Responder = {} | ||||||||
| 18 | +function Responder.new() | ||||||||
| 19 | + return { | ||||||||
| 20 | + handlers = {}, | ||||||||
| 21 | + register = function(self, handler) | ||||||||
| 22 | + lu.assertEquals({ handler.pldm_type, handler.command, handler.version }, { 3, 12, 0xF1F0F000 }) | ||||||||
| 23 | + lu.assertEquals({ handler.min_req, handler.max_req, handler.max_rsp, handler.timeout_ms }, { 0, 0, 7, 50 }) | ||||||||
| 24 | + self.handlers[handler.pldm_type * 256 + handler.command] = handler | ||||||||
| 25 | + return true | ||||||||
| 26 | + end, | ||||||||
| 27 | + } | ||||||||
| 28 | +end | ||||||||
| 29 | + | ||||||||
| 30 | +local function attach_raw_bus(bus) | ||||||||
| 31 | + bus.timeout_call = function(self, budget, ...) | ||||||||
| 32 | + lu.assertTrue(budget > 0 and budget <= 20) | ||||||||
| 33 | + return self:call(...) | ||||||||
| 34 | + end | ||||||||
| 35 | + if not bus.bus then | ||||||||
| 36 | + bus.bus = { | ||||||||
| 37 | + timeout_call = function(_, _, ...) | ||||||||
| 38 | + local err, values = bus.pcall(bus, ...) | ||||||||
| 39 | + if err then | ||||||||
| 40 | + error(err) | ||||||||
| 41 | + end | ||||||||
| 42 | + return { | ||||||||
| 43 | + read = function() | ||||||||
| 44 | + return values | ||||||||
| 45 | + end, | ||||||||
| 46 | + } | ||||||||
| 47 | + end, | ||||||||
| 48 | + } | ||||||||
| 49 | + end | ||||||||
| 50 | +end | ||||||||
| 51 | + | ||||||||
| 52 | +TestPldmTime = {} | ||||||||
| 53 | + | ||||||||
| 54 | +function TestPldmTime:setup() | ||||||||
| 55 | + self.module_ok, self.time = pcall(require, 'pldm_time') | ||||||||
| 56 | + self.original_debug, self.messages = log.debug, {} | ||||||||
| 57 | + log.debug = function(_, fmt, ...) | ||||||||
| 58 | + self.messages[#self.messages + 1] = string.format(fmt, ...) | ||||||||
| 59 | + end | ||||||||
| 60 | + self.original_fork = skynet.fork | ||||||||
| 61 | + self.original_timeout = skynet.timeout | ||||||||
| 62 | + self.original_clock = clock.monotonic_time | ||||||||
🔵 Low Priority 变更行:test/unit/test_pldm_time.lua:53 与 55-57: 影响的行为/契约与被掩盖的风险:src/lualib/pldm_time.lua:77 的 失败模式:本组隔离测试因为 setup 里凭空造出 建议:在 setup 中对生产时钟 API 增加存在性断言(type(self.original_clock)=='function'),或改用 skynet.now() 等确定的单调时钟源,避免桩函数掩盖生产时钟 API 缺失。 Suggestions
![]() ![]() Incorrect? | |||||||||
| 63 | + lu.assertEquals(type(self.original_clock), 'function', '真实单调时钟 API 缺失') | ||||||||
| 64 | + local sample = self.original_clock() | ||||||||
| 65 | + lu.assertEquals(type(sample.tv_sec), 'number') | ||||||||
| 66 | + lu.assertEquals(type(sample.tv_nsec), 'number') | ||||||||
| 67 | + lu.assertTrue(sample.tv_nsec >= 0 and sample.tv_nsec < 1000000000) | ||||||||
| 68 | + lu.assertEquals(type(require('sd_bus').SDBus.timeout_call), 'function') | ||||||||
| 69 | + self.now_ms = 100 | ||||||||
| 70 | + clock.monotonic_time = function() | ||||||||
| 71 | + return { tv_sec = 0, tv_nsec = self.now_ms * 1000000 } | ||||||||
| 72 | + end | ||||||||
| 73 | + self.queue = {} | ||||||||
| 74 | + self.timers = {} | ||||||||
| 75 | + skynet.timeout = function(ticks, fn) | ||||||||
| 76 | + self.timers[#self.timers + 1] = { ticks = ticks, run = fn } | ||||||||
| 77 | + end | ||||||||
| 78 | + skynet.fork = function(fn) | ||||||||
| 79 | + self.queue[#self.queue + 1] = fn | ||||||||
| 80 | + end | ||||||||
| 81 | +end | ||||||||
| 82 | + | ||||||||
| 83 | +function TestPldmTime:teardown() | ||||||||
| 84 | + log.debug = self.original_debug | ||||||||
| 85 | + skynet.fork = self.original_fork | ||||||||
| 86 | + skynet.timeout = self.original_timeout | ||||||||
| 87 | + clock.monotonic_time = self.original_clock | ||||||||
| 88 | +end | ||||||||
| 89 | + | ||||||||
| 90 | +function TestPldmTime:test_business_deadline_replies_once_before_late_ipc_returns() | ||||||||
| 91 | + local responder, codes = Responder.new({}), {} | ||||||||
| 92 | + self.time.register(responder, {}) | ||||||||
| 93 | + responder.handlers[3 * 256 + 12].dispatch('', function(cc) | ||||||||
| 94 | + codes[#codes + 1] = cc | ||||||||
| 95 | + end) | ||||||||
| 96 | + lu.assertEquals(#self.timers, 1, '缺少独立业务等待期限') | ||||||||
| 97 | + lu.assertEquals(self.timers[1].ticks, 2) | ||||||||
| 98 | + self.now_ms = 120 | ||||||||
| 99 | + self.timers[1].run() | ||||||||
| 100 | + lu.assertEquals(codes, { 4 }) | ||||||||
| 101 | + self.queue[1]() | ||||||||
| 102 | + lu.assertEquals(codes, { 4 }, '迟到 IPC 不得再次完成') | ||||||||
| 103 | +end | ||||||||
| 104 | + | ||||||||
| 105 | +function TestPldmTime:test_early_scheduler_tick_does_not_shorten_business_budget() | ||||||||
| 106 | + local responder, codes = Responder.new({}), {} | ||||||||
| 107 | + self.time.register(responder, {}) | ||||||||
| 108 | + responder.handlers[3 * 256 + 12].dispatch('', function(cc) | ||||||||
| 109 | + codes[#codes + 1] = cc | ||||||||
| 110 | + end) | ||||||||
| 111 | + self.now_ms = 110 | ||||||||
| 112 | + self.timers[1].run() | ||||||||
| 113 | + lu.assertEquals(codes, {}) | ||||||||
| 114 | + lu.assertEquals(#self.timers, 2) | ||||||||
| 115 | + lu.assertEquals(self.timers[2].ticks, 1) | ||||||||
| 116 | + self.now_ms = 120 | ||||||||
| 117 | + self.timers[2].run() | ||||||||
| 118 | + lu.assertEquals(codes, { 4 }) | ||||||||
| 119 | +end | ||||||||
| 120 | + | ||||||||
| 121 | +function TestPldmTime:test_single_bounded_rpc_reads_time_snapshot() | ||||||||
| 122 | + local path, intf = '/bmc/kepler/Managers/Time_Bmc_010101', 'bmc.kepler.Managers.Time' | ||||||||
| 123 | + local result, bus = nil, { bus = {} } | ||||||||
| 124 | + bus.call = function() | ||||||||
| 125 | + self.now_ms = 103 | ||||||||
| 126 | + return { [path] = { ['bmc.kepler.bmc_time'] = { intf } } } | ||||||||
| 127 | + end | ||||||||
| 128 | + bus.bus.timeout_call = function(_, timeout, service, actual_path, iface, method, signature, _, target) | ||||||||
| 129 | + lu.assertEquals({ timeout, service, actual_path, iface, method, signature, target }, { | ||||||||
| 130 | + 17, | ||||||||
| 131 | + 'bmc.kepler.bmc_time', | ||||||||
| 132 | + path, | ||||||||
| 133 | + 'bmc.kepler.Object.Properties', | ||||||||
| 134 | + 'GetAllWithContext', | ||||||||
| 135 | + 'a{ss}s', | ||||||||
| 136 | + intf, | ||||||||
| 137 | + }) | ||||||||
| 138 | + return { | ||||||||
| 139 | + read = function() | ||||||||
| 140 | + return { | ||||||||
| 141 | + SyncStatus = { | ||||||||
| 142 | + value = function() | ||||||||
| 143 | + return true | ||||||||
| 144 | + end, | ||||||||
| 145 | + }, | ||||||||
| 146 | + DateTime = { | ||||||||
| 147 | + value = function() | ||||||||
| 148 | + return '2026-10-26T01:02:03Z' | ||||||||
| 149 | + end, | ||||||||
| 150 | + }, | ||||||||
| 151 | + } | ||||||||
| 152 | + end, | ||||||||
| 153 | + } | ||||||||
| 154 | + end | ||||||||
| 155 | + local responder = Responder.new({}) | ||||||||
| 156 | + attach_raw_bus(bus) | ||||||||
| 157 | + self.time.register(responder, bus) | ||||||||
| 158 | + responder.handlers[3 * 256 + 12].dispatch('', function(cc, data) | ||||||||
| 159 | + result = { cc, data } | ||||||||
| 160 | + end) | ||||||||
| 161 | + self.queue[1]() | ||||||||
| 162 | + lu.assertEquals(result, { 0, '\x03\x02\x01\x26\x10\x26\x20' }) | ||||||||
| 163 | +end | ||||||||
| 164 | + | ||||||||
| 165 | +function TestPldmTime:test_late_ipc_result_cannot_beat_queued_timeout_callback() | ||||||||
| 166 | + local result, responder = nil, Responder.new({}) | ||||||||
| 167 | + local bus = { | ||||||||
| 168 | + call = function() | ||||||||
| 169 | + return { ['/bmc/kepler/Managers/Time_Bmc'] = { ['bmc.kepler.bmc_time'] = {} } } | ||||||||
| 170 | + end, | ||||||||
| 171 | + pcall = function() | ||||||||
| 172 | + self.now_ms = 121 | ||||||||
| 173 | + return nil, | ||||||||
| 174 | + { | ||||||||
| 175 | + SyncStatus = { | ||||||||
| 176 | + value = function() | ||||||||
| 177 | + return true | ||||||||
| 178 | + end, | ||||||||
| 179 | + }, | ||||||||
| 180 | + DateTime = { | ||||||||
| 181 | + value = function() | ||||||||
| 182 | + return '2026-10-26T01:02:03Z' | ||||||||
| 183 | + end, | ||||||||
| 184 | + }, | ||||||||
| 185 | + } | ||||||||
| 186 | + end, | ||||||||
| 187 | + } | ||||||||
| 188 | + attach_raw_bus(bus) | ||||||||
| 189 | + self.time.register(responder, bus) | ||||||||
| 190 | + responder.handlers[3 * 256 + 12].dispatch('', function(cc) | ||||||||
| 191 | + result = cc | ||||||||
| 192 | + end) | ||||||||
| 193 | + self.queue[1]() | ||||||||
| 194 | + lu.assertEquals(result, 4) | ||||||||
| 195 | +end | ||||||||
| 196 | + | ||||||||
| 197 | +function TestPldmTime:test_utc_bcd_calendar_and_timezone() | ||||||||
| 198 | + lu.assertTrue(self.module_ok, '缺少真实时间适配器') | ||||||||
| 199 | + for _, case in ipairs({ | ||||||||
| 200 | + { '2024-02-29T00:00:01+08:00', '\x01\x00\x16\x28\x02\x24\x20' }, | ||||||||
| 201 | + { '2025-01-01T00:30:00+01:00', '\x00\x30\x23\x31\x12\x24\x20' }, | ||||||||
| 202 | + { '2024-12-31T23:30:59-01:00', '\x59\x30\x00\x01\x01\x25\x20' }, | ||||||||
| 203 | + { '2000-02-29T23:59:59.125Z', '\x59\x59\x23\x29\x02\x00\x20' }, | ||||||||
| 204 | + { '2026-01-01T00:00:00.125+08:00', '\x00\x00\x16\x31\x12\x25\x20' }, | ||||||||
| 205 | + { '2025-12-31T23:59:59.999-01:00', '\x59\x59\x00\x01\x01\x26\x20' }, | ||||||||
| 206 | + { '2000-03-01T00:00:00.001+00:30', '\x00\x30\x23\x29\x02\x00\x20' }, | ||||||||
| 207 | + }) do | ||||||||
| 208 | + lu.assertEquals(self.time.encode({ SyncStatus = true, DateTime = case[1] }), case[2]) | ||||||||
| 209 | + end | ||||||||
| 210 | +end | ||||||||
| 211 | + | ||||||||
| 212 | +function TestPldmTime:test_invalid_time_never_uses_host_clock() | ||||||||
| 213 | + lu.assertTrue(self.module_ok) | ||||||||
| 214 | + lu.assertIsNil(self.time.encode({ SyncStatus = false, DateTime = '2026-01-01T00:00:00Z' })) | ||||||||
| 215 | + for _, stamp in ipairs({ | ||||||||
| 216 | + '2023-02-29T00:00:00Z', | ||||||||
| 217 | + '1900-02-29T00:00:00Z', | ||||||||
| 218 | + '2026-04-31T00:00:00Z', | ||||||||
| 219 | + '2026-01-01T24:00:00Z', | ||||||||
| 220 | + '2026-01-01T00:00:60Z', | ||||||||
| 221 | + '2026-01-01T00:00:00', | ||||||||
| 222 | + '2026-01-01T00:00:00-00:00', | ||||||||
| 223 | + '2026-01-01T00:00:00+24:00', | ||||||||
| 224 | + '0000-01-01T00:00:00Z', | ||||||||
| 225 | + '9999-12-31T23:00:00-02:00', | ||||||||
| 226 | + '2026-01-01T00:00:00Zjunk', | ||||||||
| 227 | + }) do | ||||||||
| 228 | + lu.assertIsNil(self.time.encode({ SyncStatus = true, DateTime = stamp }), stamp) | ||||||||
| 229 | + end | ||||||||
| 230 | +end | ||||||||
| 231 | + | ||||||||
| 232 | +function TestPldmTime:test_service_read_is_deferred_and_reports_invalid_or_unavailable() | ||||||||
| 233 | + lu.assertTrue(self.module_ok) | ||||||||
| 234 | + local responder, bus = Responder.new({}), {} | ||||||||
| 235 | + local snapshot = { SyncStatus = true, DateTime = '2026-10-26T08:09:10Z' } | ||||||||
| 236 | + bus.pcall = function(_, service, path, interface, method, signature, _, target) | ||||||||
| 237 | + lu.assertEquals({ service, path, interface, method, signature, target }, { | ||||||||
| 238 | + 'bmc.kepler.bmc_time', | ||||||||
| 239 | + '/bmc/kepler/Managers/Time_Bmc', | ||||||||
| 240 | + 'bmc.kepler.Object.Properties', | ||||||||
| 241 | + 'GetAllWithContext', | ||||||||
| 242 | + 'a{ss}s', | ||||||||
| 243 | + 'bmc.kepler.Managers.Time', | ||||||||
| 244 | + }) | ||||||||
| 245 | + local values = {} | ||||||||
| 246 | + for k, v in pairs(snapshot) do | ||||||||
| 247 | + values[k] = { | ||||||||
| 248 | + value = function() | ||||||||
| 249 | + return v | ||||||||
| 250 | + end, | ||||||||
| 251 | + } | ||||||||
| 252 | + end | ||||||||
| 253 | + return nil, values | ||||||||
| 254 | + end | ||||||||
| 255 | + attach_raw_bus(bus) | ||||||||
| 256 | + self.time.register(responder, bus) | ||||||||
| 257 | + local handler = responder.handlers[3 * 256 + 12] | ||||||||
| 258 | + local result, reads | ||||||||
| 259 | + reads = 0 | ||||||||
| 260 | + bus.call = function(actual_bus, _, _, _, _, _, _, path, depth, interfaces) | ||||||||
| 261 | + lu.assertEquals(actual_bus, bus) | ||||||||
| 262 | + lu.assertEquals({ path, depth }, { '/bmc/kepler/Managers', 1 }) | ||||||||
| 263 | + lu.assertEquals(interfaces, { 'bmc.kepler.Managers.Time' }) | ||||||||
| 264 | + reads = reads + 1 | ||||||||
| 265 | + return { ['/bmc/kepler/Managers/Time_Bmc'] = { ['bmc.kepler.bmc_time'] = { 'bmc.kepler.Managers.Time' } } } | ||||||||
| 266 | + end | ||||||||
| 267 | + handler.dispatch('', function(cc, data) | ||||||||
| 268 | + result = { cc, data } | ||||||||
| 269 | + end) | ||||||||
| 270 | + lu.assertEquals(reads, 0) | ||||||||
| 271 | + lu.assertIsNil(result) | ||||||||
| 272 | + self.queue[1]() | ||||||||
| 273 | + lu.assertEquals(result, { 0, '\x10\x09\x08\x26\x10\x26\x20' }) | ||||||||
| 274 | + snapshot.SyncStatus = false | ||||||||
| 275 | + handler.dispatch('', function(cc, data) | ||||||||
| 276 | + result = { cc, data } | ||||||||
| 277 | + end) | ||||||||
| 278 | + self.queue[2]() | ||||||||
| 279 | + lu.assertEquals(result, { 0x8a, '' }) | ||||||||
| 280 | + bus.call = function() | ||||||||
| 281 | + error('service unavailable') | ||||||||
| 282 | + end | ||||||||
| 283 | + handler.dispatch('', function(cc, data) | ||||||||
| 284 | + result = { cc, data } | ||||||||
| 285 | + end) | ||||||||
| 286 | + self.queue[3]() | ||||||||
| 287 | + lu.assertEquals(result, { 4, '' }) | ||||||||
| 288 | +end | ||||||||
| 289 | + | ||||||||
| 290 | +function TestPldmTime:test_slow_service_work_is_bounded_after_tokens_expire() | ||||||||
| 291 | + lu.assertTrue(self.module_ok) | ||||||||
| 292 | + local responder = Responder.new({}) | ||||||||
| 293 | + self.time.register(responder, {}) | ||||||||
| 294 | + local handler, codes = responder.handlers[3 * 256 + 12], {} | ||||||||
| 295 | + for _ = 1, 20 do | ||||||||
| 296 | + handler.dispatch('', function(cc) | ||||||||
| 297 | + codes[#codes + 1] = cc | ||||||||
| 298 | + end) | ||||||||
| 299 | + end | ||||||||
| 300 | + lu.assertEquals(#self.queue, 16) | ||||||||
| 301 | + lu.assertEquals(codes, { 4, 4, 4, 4 }) | ||||||||
| 302 | + self.queue[1]() | ||||||||
| 303 | + handler.dispatch('', function() end) | ||||||||
| 304 | + lu.assertEquals(#self.queue, 17) | ||||||||
| 305 | +end | ||||||||
| 306 | + | ||||||||
| 307 | +function TestPldmTime:test_real_interface_discovery_does_not_require_generated_time_client() | ||||||||
| 308 | + lu.assertTrue(self.module_ok) | ||||||||
| 309 | + local bus, responder, result = {}, Responder.new({}), nil | ||||||||
| 310 | + bus.call = function(_, service, path, intf, method, signature, _, object_path, depth, interfaces) | ||||||||
| 311 | + lu.assertEquals({ service, path, intf, method, signature, object_path, depth, interfaces }, { | ||||||||
| 312 | + 'bmc.kepler.maca', | ||||||||
| 313 | + '/bmc/kepler/MdbService', | ||||||||
| 314 | + 'bmc.kepler.Mdb', | ||||||||
| 315 | + 'GetSubObjects', | ||||||||
| 316 | + 'a{ss}sias', | ||||||||
| 317 | + '/bmc/kepler/Managers', | ||||||||
| 318 | + 1, | ||||||||
| 319 | + { 'bmc.kepler.Managers.Time' }, | ||||||||
| 320 | + }) | ||||||||
| 321 | + return { ['/bmc/kepler/Managers/Time_Bmc'] = { ['bmc.kepler.actual_time'] = { 'bmc.kepler.Managers.Time' } } } | ||||||||
| 322 | + end | ||||||||
| 323 | + bus.pcall = function(_, service) | ||||||||
| 324 | + lu.assertEquals(service, 'bmc.kepler.actual_time') | ||||||||
| 325 | + return nil, | ||||||||
| 326 | + { | ||||||||
| 327 | + SyncStatus = { | ||||||||
| 328 | + value = function() | ||||||||
| 329 | + return true | ||||||||
| 330 | + end, | ||||||||
| 331 | + }, | ||||||||
| 332 | + DateTime = { | ||||||||
| 333 | + value = function() | ||||||||
| 334 | + return '2026-10-26T01:02:03Z' | ||||||||
| 335 | + end, | ||||||||
| 336 | + }, | ||||||||
| 337 | + } | ||||||||
| 338 | + end | ||||||||
| 339 | + attach_raw_bus(bus) | ||||||||
| 340 | + self.time.register(responder, bus) | ||||||||
| 341 | + responder.handlers[3 * 256 + 12].dispatch('', function(cc, data) | ||||||||
| 342 | + result = { cc, data } | ||||||||
| 343 | + end) | ||||||||
| 344 | + self.queue[1]() | ||||||||
| 345 | + lu.assertEquals(result, { 0, '\x03\x02\x01\x26\x10\x26\x20' }) | ||||||||
| 346 | +end | ||||||||
| 347 | + | ||||||||
| 348 | +function TestPldmTime:test_discovers_board_suffix_and_rejects_ambiguous_time_sources() | ||||||||
| 349 | + local bus, responder, result = {}, Responder.new({}), nil | ||||||||
| 350 | + local path = '/bmc/kepler/Managers/Time_Bmc_010101' | ||||||||
| 351 | + local objects = { [path] = { ['bmc.kepler.bmc_time'] = { 'bmc.kepler.Managers.Time' } } } | ||||||||
| 352 | + bus.call = function(_, _, _, _, method, signature, _, root, depth, interfaces) | ||||||||
| 353 | + if method ~= 'GetSubObjects' then | ||||||||
| 354 | + return {} | ||||||||
| 355 | + end | ||||||||
| 356 | + lu.assertEquals({ signature, root, depth, interfaces }, { | ||||||||
| 357 | + 'a{ss}sias', | ||||||||
| 358 | + '/bmc/kepler/Managers', | ||||||||
| 359 | + 1, | ||||||||
| 360 | + { 'bmc.kepler.Managers.Time' }, | ||||||||
| 361 | + }) | ||||||||
| 362 | + return objects | ||||||||
| 363 | + end | ||||||||
| 364 | + bus.pcall = function(_, service, actual_path) | ||||||||
| 365 | + lu.assertEquals({ service, actual_path }, { 'bmc.kepler.bmc_time', path }) | ||||||||
| 366 | + return nil, | ||||||||
| 367 | + { | ||||||||
| 368 | + SyncStatus = { | ||||||||
| 369 | + value = function() | ||||||||
| 370 | + return true | ||||||||
| 371 | + end, | ||||||||
| 372 | + }, | ||||||||
| 373 | + DateTime = { | ||||||||
| 374 | + value = function() | ||||||||
| 375 | + return '2026-10-26T01:02:03Z' | ||||||||
| 376 | + end, | ||||||||
| 377 | + }, | ||||||||
| 378 | + } | ||||||||
| 379 | + end | ||||||||
| 380 | + attach_raw_bus(bus) | ||||||||
| 381 | + self.time.register(responder, bus) | ||||||||
| 382 | + local function query() | ||||||||
| 383 | + responder.handlers[3 * 256 + 12].dispatch('', function(cc, data) | ||||||||
| 384 | + result = { cc, data } | ||||||||
| 385 | + end) | ||||||||
| 386 | + self.queue[#self.queue]() | ||||||||
| 387 | + return result | ||||||||
| 388 | + end | ||||||||
| 389 | + lu.assertEquals(query(), { 0, '\x03\x02\x01\x26\x10\x26\x20' }) | ||||||||
| 390 | + objects['/bmc/kepler/Managers/Time_Bmc_020202'] = objects[path] | ||||||||
| 391 | + lu.assertEquals(query(), { 4, '' }) | ||||||||
| 392 | + objects = {} | ||||||||
| 393 | + lu.assertEquals(query(), { 4, '' }) | ||||||||
| 394 | +end | ||||||||
| 395 | + | ||||||||
| 396 | +function TestPldmTime:test_discovery_uses_remaining_budget_and_recovers_after_timeout() | ||||||||
| 397 | + local responder, results, budgets = Responder.new(), {}, {} | ||||||||
| 398 | + local bus = { | ||||||||
| 399 | + timeout_call = function(_, budget) | ||||||||
| 400 | + budgets[#budgets + 1] = budget | ||||||||
| 401 | + error('discovery timeout') | ||||||||
| 402 | + end, | ||||||||
| 403 | + } | ||||||||
| 404 | + self.time.register(responder, bus) | ||||||||
| 405 | + local handler = responder.handlers[3 * 256 + 12] | ||||||||
| 406 | + for i = 1, 20 do | ||||||||
| 407 | + handler.dispatch('', function(code) | ||||||||
| 408 | + results[#results + 1] = code | ||||||||
| 409 | + end) | ||||||||
| 410 | + self.queue[i]() | ||||||||
| 411 | + end | ||||||||
| 412 | + lu.assertEquals(#budgets, 20, '发现调用未受预算保护或槽位未归还') | ||||||||
| 413 | + lu.assertEquals(budgets, { 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20 }) | ||||||||
| 414 | + lu.assertEquals(#results, 20) | ||||||||
| 415 | + for _, code in ipairs(results) do | ||||||||
| 416 | + lu.assertEquals(code, 4) | ||||||||
| 417 | + end | ||||||||
| 418 | +end | ||||||||
| 419 | + | ||||||||
| 420 | +function TestPldmTime:test_rfc3339_numeric_offset_bounds() | ||||||||
| 421 | + lu.assertEquals( | ||||||||
| 422 | + self.time.encode({ SyncStatus = true, DateTime = '2026-01-01T00:00:00+23:59' }), | ||||||||
| 423 | + '\x00\x01\x00\x31\x12\x25\x20' | ||||||||
| 424 | + ) | ||||||||
| 425 | + lu.assertEquals( | ||||||||
| 426 | + self.time.encode({ SyncStatus = true, DateTime = '2026-01-01T00:00:00-23:59' }), | ||||||||
| 427 | + '\x00\x59\x23\x01\x01\x26\x20' | ||||||||
| 428 | + ) | ||||||||
| 429 | + lu.assertIsNil(self.time.encode({ SyncStatus = true, DateTime = '2026-01-01T00:00:00+24:00' })) | ||||||||
| 430 | +end | ||||||||
| 431 | + | ||||||||
| 432 | +function TestPldmTime:test_diagnostic_reason_does_not_expose_remote_error() | ||||||||
| 433 | + local responder = Responder.new() | ||||||||
| 434 | + self.time.register(responder, { | ||||||||
| 435 | + timeout_call = function() | ||||||||
| 436 | + error('private service payload') | ||||||||
| 437 | + end, | ||||||||
| 438 | + }) | ||||||||
| 439 | + responder.handlers[3 * 256 + 12].dispatch('', function() end) | ||||||||
| 440 | + self.queue[1]() | ||||||||
| 441 | + local text = table.concat(self.messages, '\n') | ||||||||
| 442 | + lu.assertStrContains(text, 'reason=ipc_error') | ||||||||
| 443 | + lu.assertStrContains(text, 'stage=discovery') | ||||||||
| 444 | + lu.assertStrContains(text, 'request=1') | ||||||||
| 445 | + lu.assertIsNil(text:find('private service payload', 1, true)) | ||||||||
| 446 | +end | ||||||||
| 447 | + | ||||||||
| 448 | +function TestPldmTime:test_lowercase_rfc3339_separators() | ||||||||
| 449 | + for _, stamp in ipairs({ '2026-01-01t00:00:00z', '2026-01-01T00:00:00z', '2026-01-01t01:00:00+01:00' }) do | ||||||||
| 450 | + lu.assertEquals(self.time.encode({ SyncStatus = true, DateTime = stamp }), '\x00\x00\x00\x01\x01\x26\x20') | ||||||||
| 451 | + end | ||||||||
| 452 | +end | ||||||||
| 453 | + | ||||||||
| 454 | +function TestPldmTime:test_expired_tokens_do_not_release_still_running_ipc() | ||||||||
| 455 | + local responder, codes, workers = Responder.new(), {}, {} | ||||||||
| 456 | + self.time.register(responder, { | ||||||||
| 457 | + timeout_call = function() | ||||||||
| 458 | + coroutine.yield() | ||||||||
| 459 | + error('bounded IPC returned timeout') | ||||||||
| 460 | + end, | ||||||||
| 461 | + }) | ||||||||
| 462 | + local function request() | ||||||||
| 463 | + responder.handlers[3 * 256 + 12].dispatch('', function(code) | ||||||||
| 464 | + codes[#codes + 1] = code | ||||||||
| 465 | + end) | ||||||||
| 466 | + end | ||||||||
| 467 | + for i = 1, 16 do | ||||||||
| 468 | + request() | ||||||||
| 469 | + workers[i] = coroutine.create(self.queue[i]) | ||||||||
| 470 | + lu.assertTrue(coroutine.resume(workers[i])) | ||||||||
| 471 | + lu.assertEquals(coroutine.status(workers[i]), 'suspended') | ||||||||
| 472 | + end | ||||||||
| 473 | + self.now_ms = 120 | ||||||||
| 474 | + for _, timeout in ipairs(self.timers) do | ||||||||
| 475 | + timeout.run() | ||||||||
| 476 | + end | ||||||||
| 477 | + lu.assertEquals(#codes, 16) | ||||||||
| 478 | + request() | ||||||||
| 479 | + lu.assertEquals({ #codes, #self.queue }, { 17, 16 }, '仍在运行的IPC不能突破16个') | ||||||||
| 480 | + lu.assertTrue(coroutine.resume(workers[1])) | ||||||||
| 481 | + request() | ||||||||
| 482 | + lu.assertEquals({ #codes, #self.queue }, { 17, 17 }, 'IPC返回后可接收新请求,迟到结果不重复回复') | ||||||||
| 483 | + for i = 2, 16 do | ||||||||
| 484 | + lu.assertTrue(coroutine.resume(workers[i])) | ||||||||
| 485 | + end | ||||||||
| 486 | + self.queue[17]() | ||||||||
| 487 | +end | ||||||||


💡 建议:补充"小数秒 + 数值时区偏移"组合的单测
M.encode会先剥离小数秒再匹配数值偏移,但当前单测只分别覆盖了两者:2000-02-29T23:59:59.125Z(小数秒+Z)和2026-01-01T00:00:00+23:59(无小数秒、数值偏移),没有把两者组合起来验证(如2024-02-29T23:59:59.125+08:00)。该组合正是生产场景常见格式(本地时间带小数秒)。一旦未来调整
zone:gsub('^%.%d+','')的正则顺序或剥离逻辑,该路径回归不易被测试发现。修复建议:在
TestPldmTime:test_utc_bcd_calendar_and_timezone增加一条断言,例如{ '2024-02-29T23:59:59.125+08:00', '\x59\x59\x15\x29\x02\x24\x20' }, 覆盖"小数秒 + 数值偏移"的组合路径。