| | |
| | | import org.junit.jupiter.api.Test; |
| | | import com.battery.device.api.DeviceAdapter; |
| | | import com.battery.device.api.TestDataObservationAdapter; |
| | | import com.battery.device.api.TestDataObservationAdapter.CycleRecord; |
| | | import com.battery.device.api.TestDataObservationAdapter.SampleWatermark; |
| | | import com.battery.device.api.model.ChannelIdentity; |
| | | import com.battery.device.api.model.DeviceControlCommand; |
| | |
| | | } |
| | | |
| | | @Test |
| | | void doesNotInventStepCyclesCollectorOrPlanFacts() |
| | | void doesNotInventCollectorOrPlanFacts() |
| | | { |
| | | ScriptedPipe pipe = new ScriptedPipe(); |
| | | try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe)) |
| | | { |
| | | // 工步层要未验证的 downloadStepLayer;采集器测量 ADR-0068 已复核不可得;协议里没有读计划的命令。 |
| | | assertTrue(adapter.stepRecords(CHANNEL).isEmpty()); |
| | | assertTrue(adapter.cycleRecords(CHANNEL).isEmpty()); |
| | | // 采集器测量 ADR-0068 已复核不可得;协议里没有读计划的命令。 |
| | | assertNull(adapter.latestCollector(ADAPTER, "collector-1")); |
| | | assertNull(adapter.testPlan(CHANNEL)); |
| | | assertEquals(NewareBtsAdapter.SAMPLE_INTERVAL_MS, adapter.persistenceIntervalMillis(CHANNEL)); |
| | |
| | | assertThrows(UnsupportedOperationException.class, () -> adapter.control(CHANNEL, command)); |
| | | assertTrue(pipe.commands().isEmpty()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void handsOutEveryStepAndCycleWhenTheChannelHasEnded() |
| | | { |
| | | ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.FINISHED); |
| | | try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe)) |
| | | { |
| | | var steps = adapter.stepRecords(CHANNEL); |
| | | var cycles = adapter.cycleRecords(CHANNEL); |
| | | |
| | | assertEquals(4, steps.size()); |
| | | assertEquals("充电", steps.get(1).stepName()); |
| | | // 循环号已按 0 起归一到与厂商库、逐点同口径(应答里的 cycleid 是 1 起)。 |
| | | assertEquals(0, steps.get(0).cycleNo()); |
| | | |
| | | assertEquals(1, cycles.size()); |
| | | var cycle = cycles.get(0); |
| | | assertEquals(0, cycle.cycleNo()); |
| | | assertEquals(2.81630277777778, cycle.chargeCapacityAh(), 1e-9); |
| | | assertEquals(0.00431017035590278, cycle.dischargeCapacityAh(), 1e-12); |
| | | assertEquals(5.58447055555556, cycle.chargeEnergyWh(), 1e-9); |
| | | assertEquals(0.00426197048611111, cycle.dischargeEnergyWh(), 1e-12); |
| | | // 效率 = 放电容量 / 充电容量 × 100;搁置工步不进任何一侧。 |
| | | assertEquals(0.153, cycle.efficiency(), 0.0005); |
| | | assertEquals("neware:316:cycle:0", cycle.sourceRecordId()); |
| | | assertEquals(CycleRecord.SOURCE_AGGREGATED, cycle.sourceKind()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void holdsBackTheRunningStepAndItsCycleWhileTheTestIsStillRunning() |
| | | { |
| | | // 实测这条命令会把正在进行的那一步也交出来,而它的 endseqid 恰好等于已落盘行数 |
| | | // (四个测试都相等),所以只能靠通道工作状态判断它有没有结束。 |
| | | ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.WORKING); |
| | | try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe)) |
| | | { |
| | | var steps = adapter.stepRecords(CHANNEL); |
| | | var cycles = adapter.cycleRecords(CHANNEL); |
| | | |
| | | // 最后一步(stepindex=4)与它所在的循环都不交出,等它结束再交。 |
| | | assertEquals(3, steps.size()); |
| | | assertEquals("neware:316:step:3", steps.get(2).sourceRecordId()); |
| | | assertTrue(cycles.isEmpty()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void readsTheStepLayerOnceForBothStepsAndCycles() |
| | | { |
| | | // 工步与循环必须来自同一次读取:否则一个工步边界落在两次读取之间时, |
| | | // 循环合计会包含工步列表里没有的那一步,使循环记录每轮内容都不同。 |
| | | ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.FINISHED); |
| | | try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe)) |
| | | { |
| | | adapter.stepRecords(CHANNEL); |
| | | adapter.cycleRecords(CHANNEL); |
| | | |
| | | assertEquals(1, pipe.commands().stream().filter("downloadsteplayer"::equals).count()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void aChannelWithNoTestInTheBufferHasNoStepsOrCycles() |
| | | { |
| | | ScriptedPipe pipe = new ScriptedPipe() |
| | | .on("downloadStepLayer", "<bts><cmd>downloadStepLayer_resp</cmd><list count=\"0\" /></bts>"); |
| | | try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe)) |
| | | { |
| | | assertTrue(adapter.stepRecords(CHANNEL).isEmpty()); |
| | | assertTrue(adapter.cycleRecords(CHANNEL).isEmpty()); |
| | | // 缓冲里没有测试就直接是空,不必再去问工作状态。 |
| | | assertEquals(List.of("downloadsteplayer"), pipe.commands()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void aFailedStateReadSkipsTheRoundInsteadOfHandingOutTheRunningStep() |
| | | { |
| | | // 读取失败不吞:调用方本来就有"来源读不到就本轮放弃、下一轮重试"的约定, |
| | | // 所以一次传输抖动不会变成"把一个还在跑的工步交出去",也不会被当成结论缓存下来。 |
| | | ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.WORKING).failing("inquire"); |
| | | try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe)) |
| | | { |
| | | assertThrows(RuntimeException.class, () -> adapter.stepRecords(CHANNEL)); |
| | | |
| | | pipe.recover(); |
| | | // 恢复后重新读,拿到的是完整结论(在跑 → 交出 3 步、按住最后一步),不是上一次的空结果。 |
| | | assertEquals(3, adapter.stepRecords(CHANNEL).size()); |
| | | } |
| | | } |
| | | |
| | | /** 一个已结束测试的工步层(现场实测原文,测试 316):4 步都在循环 1(归一后为 0)。 */ |
| | | private static ScriptedPipe finishedTestPipe(DeviceWorkStatus workStatus) |
| | | { |
| | | return new ScriptedPipe() |
| | | .on("inquire", "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire dev=\"25-81-1-2-0\"" |
| | | + " cycle_id=\"1\" step_id=\"4\" step_type=\"dc\" workstatus=\"" |
| | | + workStatus.name().toLowerCase(java.util.Locale.ROOT) |
| | | + "\" current=\"0\" voltage=\"1.2\" /></list></bts>") |
| | | .on("downloadStepLayer", "<bts><cmd>downloadStepLayer_resp</cmd>" |
| | | + "<downloadStepLayer devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"2\" testid=\"316\" />" |
| | | + "<list count=\"4\">" |
| | | + "<data startseqid=\"1\" endseqid=\"2\" stepindex=\"1\" stepid=\"1\" cycleid=\"1\"" |
| | | + " steptype=\"rest\" steptime=\"23100\" endatime=\"2026-10-10 09:09:14\"" |
| | | + " startvolt=\"0.806611083984375\" endvolt=\"0.80638037109375\" startcurr=\"0\"" |
| | | + " endcurr=\"0\" cap=\"0\" eng=\"0\" dcir=\"0\" />" |
| | | + "<data startseqid=\"3\" endseqid=\"304\" stepindex=\"2\" stepid=\"2\" cycleid=\"1\"" |
| | | + " steptype=\"cc\" steptime=\"18000000\" endatime=\"2026-10-10 14:09:14\"" |
| | | + " startvolt=\"1.07542861328125\" endvolt=\"1.972108203125\"" |
| | | + " startcurr=\"0.563903076171875\" endcurr=\"0.563332702636719\"" |
| | | + " cap=\"2.81630277777778\" eng=\"5.58447055555556\" dcir=\"0\" />" |
| | | + "<data startseqid=\"305\" endseqid=\"307\" stepindex=\"3\" stepid=\"3\" cycleid=\"1\"" |
| | | + " steptype=\"rest\" steptime=\"83600\" endatime=\"2026-10-10 14:10:38\"" |
| | | + " startvolt=\"1.7850955078125\" endvolt=\"1.42794287109375\" startcurr=\"0\"" |
| | | + " endcurr=\"0\" cap=\"0\" eng=\"0\" dcir=\"0\" />" |
| | | + "<data startseqid=\"308\" endseqid=\"309\" stepindex=\"4\" stepid=\"4\" cycleid=\"1\"" |
| | | + " steptype=\"dc\" steptime=\"27600\" endatime=\"2026-10-10 14:11:05\"" |
| | | + " startvolt=\"1.27452001953125\" endvolt=\"0.49604326171875\"" |
| | | + " startcurr=\"-0.562971618652344\" endcurr=\"-0.5622275390625\"" |
| | | + " cap=\"0.00431017035590278\" eng=\"0.00426197048611111\" dcir=\"0\" /></list></bts>"); |
| | | } |
| | | |
| | | private static DeviceAdapter adapter(ScriptedPipe pipe) |
| | |
| | | |
| | | private final Map<String, String> responses = new java.util.LinkedHashMap<>(); |
| | | private final List<String> commands = new ArrayList<>(); |
| | | /** 让指定命令暂时失败,用来验"读取失败不吞、不当结论缓存"。 */ |
| | | private final java.util.Set<String> failing = new java.util.LinkedHashSet<>(); |
| | | private int firstRow = 0; |
| | | private int lastRow = 0; |
| | | |
| | | ScriptedPipe on(String command, String response) |
| | | { |
| | | responses.put(command, response); |
| | | // 请求里的命令名按小写匹配(与生产传输的比对口径一致)。 |
| | | responses.put(command.toLowerCase(java.util.Locale.ROOT), response); |
| | | return this; |
| | | } |
| | | |
| | | ScriptedPipe failing(String command) |
| | | { |
| | | failing.add(command.toLowerCase(java.util.Locale.ROOT)); |
| | | return this; |
| | | } |
| | | |
| | | void recover() |
| | | { |
| | | failing.clear(); |
| | | } |
| | | |
| | | /** 生成 {@code [first, last]} 的逐点行集,供 download/inquiredf 按窗口回答。 */ |
| | |
| | | Matcher matcher = COMMAND.matcher(request); |
| | | if (!matcher.find()) throw new IllegalStateException("请求缺少命令:" + request); |
| | | String command = matcher.group(1).toLowerCase(java.util.Locale.ROOT); |
| | | if (failing.contains(command)) |
| | | throw new BtsTransportException(BtsTransportException.Reason.TIMEOUT, "测试脚本让 " + command + " 失败", null); |
| | | if ("inquiredf".equals(command)) |
| | | { |
| | | commands.add("inquiredf"); |