package com.battery.device.contract;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import java.math.BigDecimal;
|
import java.time.Duration;
|
import java.time.Instant;
|
import java.time.ZoneId;
|
import java.util.List;
|
import java.util.Set;
|
import org.junit.jupiter.api.Test;
|
import com.alibaba.fastjson2.JSON;
|
import com.battery.device.api.DeviceAdapter;
|
import com.battery.device.simulator.BtsDevelopmentSimulator;
|
import com.battery.device.bts.BtsDeviceAdapter;
|
import com.battery.device.bts.BtsXmlCodec;
|
import com.battery.device.bts.BtsTcpTransport;
|
import com.battery.device.simulator.ScriptedBtsTcpSimulator;
|
import com.battery.device.api.model.DetectedChannel;
|
import com.battery.device.api.model.DeviceWorkStatus;
|
import com.battery.device.simulator.BtsSimulatorStateStore;
|
import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorHistoryBatch;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.DeviceState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.RecentRequest;
|
import com.battery.device.simulator.InMemoryBtsSimulatorStateStore;
|
|
class SimulatorPublicContractTest
|
{
|
@Test
|
void collectorGeneratesIndependentRealtimeAndHistoricalMeasurements() throws Exception
|
{
|
Instant startedAt = Instant.parse("2026-08-29T01:00:00Z");
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
|
{
|
simulator.configureCollectorSampling(1, 200, CollectorHistoryFaults.none());
|
simulator.configureCollectorChannel(1, "S-01", List.of(
|
new CollectorMetricState("temperature", new BigDecimal("26.5"), "℃"),
|
new CollectorMetricState("positive_potential", new BigDecimal("3.801"), "V"),
|
new CollectorMetricState("negative_potential", new BigDecimal("-0.120"), "V"),
|
new CollectorMetricState("pressure", new BigDecimal("101.2"), "kPa")));
|
|
simulator.advanceCollectors(startedAt);
|
simulator.advanceCollectors(startedAt.plusMillis(400));
|
|
var realtime = simulator.snapshot().collectors().get(0).latestMeasurements();
|
var history = simulator.collectorHistory(1, 0, 1000);
|
assertEquals(13, realtime.size());
|
assertTrue(realtime.stream().allMatch(item -> item.sequenceId() == 3));
|
assertTrue(realtime.stream().anyMatch(item -> item.channelCode().equals("S-01")
|
&& item.metricCode().equals("pressure") && item.unit().equals("kPa")));
|
assertEquals(39, history.total());
|
assertEquals("dev-sim", history.records().get(0).adapterCode());
|
assertEquals("dev-sim-collector-1", history.records().get(0).collectorCode());
|
assertEquals(startedAt, history.records().get(0).deviceTime());
|
assertTrue(history.records().get(0).deviceTimeRaw().startsWith("2026-08-29 09:00:00"));
|
assertEquals(startedAt, history.records().get(0).serverReceivedAt());
|
}
|
}
|
|
@Test
|
void collectorHistorySupportsPagingAndRepeatableFaultScenarios() throws Exception
|
{
|
Instant startedAt = Instant.parse("2026-08-29T02:00:00Z");
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
|
{
|
simulator.configureCollectorSampling(1, 100,
|
new CollectorHistoryFaults(List.of(2L), 1L, 3L, true));
|
simulator.advanceCollectors(startedAt);
|
simulator.advanceCollectors(startedAt.plusMillis(200));
|
|
var first = simulator.collectorHistory(1, 0, 17);
|
var second = simulator.collectorHistory(1, first.nextOffset(), 1000);
|
List<com.battery.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord> records =
|
new java.util.ArrayList<>(first.records());
|
records.addAll(second.records());
|
|
assertEquals(48, first.total());
|
assertTrue(first.hasMore());
|
assertFalse(second.hasMore());
|
assertTrue(records.stream().noneMatch(item -> item.sequenceId() == 2));
|
assertEquals(24, records.stream().filter(item -> item.sequenceId() == 1).count());
|
assertEquals(2, records.stream().filter(item -> item.sequenceId() == 3
|
&& item.channelCode().equals("S-01") && item.metricCode().equals("temperature")).count());
|
assertTrue(records.get(0).sequenceId() >= records.get(records.size() - 1).sequenceId());
|
}
|
}
|
|
@Test
|
void collectorStopsWhileOfflineAndResumesFromPersistedSequence() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
Instant now = Instant.now();
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
|
{
|
simulator.advanceCollectors(now);
|
simulator.updateCollector(1, "127.0.1.2", 4, false);
|
simulator.advanceCollectors(now.plusSeconds(10));
|
assertEquals(1, simulator.snapshot().collectors().get(0).latestMeasurements().get(0).sequenceId());
|
}
|
|
try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
|
{
|
restored.updateCollector(1, "127.0.1.2", 4, true);
|
restored.advanceCollectors(Instant.now().plusSeconds(2));
|
assertTrue(restored.snapshot().collectors().get(0).latestMeasurements().get(0).sequenceId() >= 2);
|
assertEquals(List.of("S-01", "S-02", "S-03", "S-04"),
|
restored.snapshot().collectors().get(0).channels().stream()
|
.map(channel -> channel.channelCode()).toList());
|
}
|
}
|
|
@Test
|
void simulatorPowerSwitchStopsSamplingAndRestoresWithoutDeletingHistory() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
long historyCount;
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
|
{
|
simulator.advanceCollectors(Instant.now());
|
historyCount = simulator.snapshot().collectors().get(0).historyRecordCount();
|
|
simulator.setAllDevicesPoweredOn(false);
|
simulator.advanceCollectors(Instant.now().plusSeconds(10));
|
|
var poweredOff = simulator.snapshot();
|
assertTrue(poweredOff.devices().stream().noneMatch(device -> device.online()
|
|| device.channels().stream().anyMatch(channel -> channel.online()
|
|| channel.workStatus() != DeviceWorkStatus.STOPPED)));
|
assertTrue(poweredOff.collectors().stream().noneMatch(collector -> collector.online()));
|
assertEquals(historyCount, poweredOff.collectors().get(0).historyRecordCount());
|
}
|
|
try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
|
{
|
assertFalse(restored.snapshot().collectors().get(0).online());
|
assertEquals(historyCount, restored.collectorHistory(1, 0, 1000).total());
|
|
restored.setAllDevicesPoweredOn(true);
|
var poweredOn = restored.snapshot();
|
assertTrue(poweredOn.devices().stream().allMatch(device -> device.online()
|
&& device.channels().stream().allMatch(channel -> channel.online()
|
&& channel.workStatus() == DeviceWorkStatus.STOPPED)));
|
assertTrue(poweredOn.collectors().stream().allMatch(collector -> collector.online()));
|
restored.advanceCollectors(Instant.now().plusSeconds(2));
|
assertTrue(restored.snapshot().collectors().get(0).historyRecordCount() > historyCount);
|
}
|
}
|
|
@Test
|
void collectorKeepsOnlyBoundedDemoHistory() throws Exception
|
{
|
Instant startedAt = Instant.parse("2026-08-29T03:00:00Z");
|
TrackingHistoryStore store = new TrackingHistoryStore();
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
|
{
|
simulator.configureCollectorSampling(1, 100, CollectorHistoryFaults.none());
|
simulator.advanceCollectors(startedAt);
|
for (int batch = 1; batch <= 5; batch++)
|
simulator.advanceCollectors(startedAt.plusMillis(batch * 100_000L));
|
|
store.resetReadStats();
|
var page = simulator.collectorHistory(1, 0, 1);
|
assertEquals(BtsSimulatorStateStore.MAX_HISTORY_RECORDS, page.total());
|
assertTrue(page.records().get(0).sequenceId() > 1);
|
assertEquals(1, store.historyReads);
|
assertEquals(1, store.recordsRead);
|
}
|
|
store.resetReadStats();
|
try (BtsDevelopmentSimulator ignored = new BtsDevelopmentSimulator(0, store))
|
{
|
assertTrue(store.historyReads > 1);
|
assertEquals(1000, store.maximumRequestedLimit);
|
assertEquals(BtsSimulatorStateStore.MAX_HISTORY_RECORDS, store.recordsRead);
|
}
|
}
|
|
@Test
|
void collectorRawHistoryIsNotEmbeddedInMainSimulatorState() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
|
{
|
simulator.advanceCollectors(Instant.parse("2026-08-29T03:30:00Z"));
|
|
String mainState = JSON.toJSONString(store.loadOrInitialize(null));
|
assertFalse(mainState.contains("\"history\":[{"));
|
assertEquals(12, simulator.collectorHistory(1, 0, 100).total());
|
}
|
}
|
|
@Test
|
void collectorRealtimeDropsMeasurementsRemovedFromCurrentConfiguration() throws Exception
|
{
|
Instant startedAt = Instant.parse("2026-08-29T04:00:00Z");
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
|
{
|
simulator.advanceCollectors(startedAt);
|
assertEquals(12, simulator.snapshot().collectors().get(0).latestMeasurements().size());
|
|
simulator.configureCollectorChannel(1, "S-01", List.of(
|
new CollectorMetricState("temperature", new BigDecimal("27.0"), "℃")));
|
simulator.updateCollector(1, "127.0.1.2", 2, true);
|
|
var snapshot = simulator.snapshot().collectors().get(0);
|
assertEquals(4, snapshot.latestMeasurements().size());
|
assertTrue(snapshot.latestMeasurements().stream()
|
.noneMatch(item -> item.channelCode().equals("S-03")
|
|| item.channelCode().equals("S-04")
|
|| item.channelCode().equals("S-01")
|
&& item.metricCode().equals("positive_potential")));
|
assertEquals(12, simulator.collectorHistory(1, 0, 100).total());
|
}
|
|
try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
|
{
|
assertEquals(4, restored.snapshot().collectors().get(0).latestMeasurements().size());
|
assertEquals(12, restored.collectorHistory(1, 0, 100).total());
|
}
|
}
|
|
@Test
|
void collectorRejectsMetricWhitespaceThatWouldBreakRestore() throws Exception
|
{
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
|
{
|
assertThrows(IllegalArgumentException.class, () -> simulator.configureCollectorChannel(1, "S-01",
|
List.of(new CollectorMetricState("temperature ", new BigDecimal("25.0"), "℃"))));
|
assertThrows(IllegalArgumentException.class, () -> simulator.configureCollectorChannel(1, "S-01",
|
List.of(new CollectorMetricState("temperature", new BigDecimal("25.0"), " ℃"))));
|
}
|
}
|
|
@Test
|
void simulatorIsReusableOutsideTheBtsPackage() throws Exception
|
{
|
try (ScriptedBtsTcpSimulator simulator = new ScriptedBtsTcpSimulator();
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1)))
|
{
|
simulator.enqueue("<bts><cmd>getdevinfo_resp</cmd><middle count=\"0\"/></bts>");
|
|
assertEquals("<bts><cmd>getdevinfo_resp</cmd><middle count=\"0\"/></bts>",
|
transport.exchange("<bts><cmd>getdevinfo</cmd></bts>"));
|
assertEquals(1, simulator.requests().size());
|
}
|
}
|
|
@Test
|
void simulatorPreservesRequestEndingWithDelimiterPrefix() throws Exception
|
{
|
try (ScriptedBtsTcpSimulator simulator = new ScriptedBtsTcpSimulator();
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1)))
|
{
|
simulator.enqueue("ok");
|
|
assertEquals("ok", transport.exchange("payload#"));
|
assertEquals("payload#", simulator.requests().get(0));
|
}
|
}
|
|
@Test
|
void developmentSimulatorSupportsRepeatedDiscoveryAndObservation() throws Exception
|
{
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("development-bts", transport,
|
new BtsXmlCodec("development-bts", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
var firstDiscovery = adapter.discoverChannels();
|
var secondDiscovery = adapter.discoverChannels();
|
|
assertEquals(4, firstDiscovery.size());
|
assertEquals(firstDiscovery, secondDiscovery);
|
assertEquals("开发模拟器", firstDiscovery.get(0).deviceMetadata().manufacturer());
|
assertEquals("BTS Virtual Cabinet", firstDiscovery.get(0).deviceMetadata().model());
|
assertEquals(1, firstDiscovery.stream().map(channel -> channel.identity().devid()).distinct().count());
|
assertEquals(4, firstDiscovery.stream().filter(channel -> channel.available()).count());
|
|
Set<com.battery.device.api.model.ChannelIdentity> channels = firstDiscovery.stream()
|
.map(channel -> channel.identity())
|
.collect(java.util.stream.Collectors.toSet());
|
var firstObservation = adapter.inquire(channels);
|
var secondObservation = adapter.inquire(channels);
|
|
assertEquals(4, firstObservation.size());
|
assertEquals(DeviceWorkStatus.STOPPED, firstObservation.values().iterator().next().workStatus());
|
assertEquals(firstObservation.keySet(), secondObservation.keySet());
|
}
|
}
|
|
@Test
|
void developmentSimulatorSupportsNewareProfilesAndDynamicObservation() throws Exception
|
{
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
int tDevice = simulator.addNewareDevice("127.0.0.8", "CT-4008T-5V6A-S1-F");
|
int tnDevice = simulator.addNewareDevice("127.0.0.9", "CT-4008Tn-5V12A-S1-F");
|
simulator.updateChannel(tDevice, 1, true, DeviceWorkStatus.WORKING);
|
|
var discovered = adapter.discoverChannels();
|
var tChannels = discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.8")).toList();
|
var tnChannels = discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.9")).toList();
|
|
assertEquals(4, discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.2")).count());
|
assertEquals(8, tChannels.size());
|
assertEquals(8, tnChannels.size());
|
assertEquals("新威", tChannels.get(0).deviceMetadata().manufacturer());
|
assertEquals("CT-4008T-5V6A-S1-F", tChannels.get(0).deviceMetadata().model());
|
assertEquals("CT-4008Tn-5V12A-S1-F", tnChannels.get(0).deviceMetadata().model());
|
assertEquals(8, simulator.snapshot().devices().stream()
|
.filter(device -> device.deviceId() == tDevice || device.deviceId() == tnDevice)
|
.mapToInt(device -> device.channels().size()).sum() / 2);
|
|
Set<com.battery.device.api.model.ChannelIdentity> channel = tChannels.stream()
|
.map(DetectedChannel::identity).filter(identity -> identity.chlid() == 1)
|
.collect(java.util.stream.Collectors.toSet());
|
var first = adapter.inquire(channel).values().iterator().next();
|
var second = adapter.inquire(channel).values().iterator().next();
|
assertEquals(DeviceWorkStatus.WORKING, first.workStatus());
|
assertNotEquals(first.currentA(), second.currentA());
|
assertTrue(first.voltageV().doubleValue() >= 0.0 && first.voltageV().doubleValue() <= 5.0);
|
}
|
}
|
|
@Test
|
void developmentSimulatorPersistsNewareProfileMetadata() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
|
{
|
simulator.addNewareDevice("127.0.0.8", "CT-4008Tn-5V12A-S1-F");
|
}
|
|
try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", restored.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
var profileChannels = adapter.discoverChannels().stream()
|
.filter(channel -> channel.serverIp().equals("127.0.0.8")).toList();
|
assertEquals(8, profileChannels.size());
|
assertEquals("CT-4008Tn-5V12A-S1-F", profileChannels.get(0).deviceMetadata().model());
|
assertEquals("新威", profileChannels.get(0).deviceMetadata().manufacturer());
|
}
|
}
|
|
@Test
|
void developmentSimulatorPublishesOperatorControlledChannelFacts() throws Exception
|
{
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING);
|
simulator.updateChannel(1, 2, false, DeviceWorkStatus.PAUSED);
|
|
var discovered = adapter.discoverChannels();
|
Set<com.battery.device.api.model.ChannelIdentity> available = discovered.stream()
|
.filter(channel -> channel.available())
|
.map(channel -> channel.identity())
|
.collect(java.util.stream.Collectors.toSet());
|
var observed = adapter.inquire(available);
|
|
assertEquals(4, discovered.size());
|
assertEquals(3, available.size());
|
assertEquals(DeviceWorkStatus.WORKING, observed.entrySet().stream()
|
.filter(entry -> entry.getKey().chlid() == 1)
|
.findFirst().orElseThrow().getValue().workStatus());
|
|
simulator.updateDevice(1, "127.0.0.2", 4, false);
|
assertEquals(0, adapter.discoverChannels().stream().filter(channel -> channel.available()).count());
|
assertEquals(4, simulator.snapshot().devices().get(0).channels().size());
|
assertEquals(3, simulator.snapshot().recentRequests().size());
|
}
|
}
|
|
@Test
|
void developmentSimulatorSupportsMultipleConfigurableDevices() throws Exception
|
{
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
int secondDeviceId = simulator.addDevice("127.0.0.3", 2);
|
simulator.updateDevice(1, "127.0.0.2", 4, true);
|
|
var discovered = adapter.discoverChannels();
|
|
assertEquals(6, discovered.size());
|
assertEquals(2, discovered.stream().map(channel -> channel.identity().devid()).distinct().count());
|
assertEquals(4, discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.2")).count());
|
assertEquals(2, discovered.stream().filter(channel -> channel.serverIp().equals("127.0.0.3")).count());
|
|
simulator.deleteDevice(secondDeviceId);
|
for (int index = 0; index < 3; index++)
|
{
|
var withTombstone = adapter.discoverChannels();
|
assertEquals(6, withTombstone.size());
|
assertEquals(4, withTombstone.stream().filter(DetectedChannel::available).count());
|
}
|
assertEquals(4, adapter.discoverChannels().size());
|
}
|
}
|
|
@Test
|
void developmentCollectorsNeverEnterBtsCabinetDiscovery() throws Exception
|
{
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", simulator.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
assertEquals(4, adapter.discoverChannels().size());
|
|
simulator.addCollector("127.0.1.8", 12);
|
|
assertEquals(4, adapter.discoverChannels().size());
|
assertEquals(2, simulator.snapshot().collectors().size());
|
}
|
}
|
|
@Test
|
void developmentSimulatorRestoresConfigurationNumberingAndStatus() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
try (BtsDevelopmentSimulator first = new BtsDevelopmentSimulator(0, store))
|
{
|
first.updateDevice(1, "127.0.0.9", 2, false);
|
first.addDevice("127.0.0.10", 3);
|
first.updateCollector(1, "127.0.1.9", 7, false);
|
}
|
|
try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store))
|
{
|
assertEquals(2, restored.snapshot().devices().size());
|
assertEquals("127.0.0.9", restored.snapshot().devices().get(0).ip());
|
assertEquals(false, restored.snapshot().devices().get(0).online());
|
assertEquals(7, restored.snapshot().collectors().get(0).channelCount());
|
assertEquals(3, restored.addDevice("127.0.0.11", 1));
|
}
|
}
|
|
@Test
|
void developmentSimulatorDoesNotExposeWriteThroughFailureInMemory() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore delegate = new InMemoryBtsSimulatorStateStore();
|
BtsSimulatorStateStore failing = new BtsSimulatorStateStore()
|
{
|
@Override public SimulatorState loadOrInitialize(SimulatorState value)
|
{ return delegate.loadOrInitialize(value); }
|
@Override public void save(SimulatorState value) { throw new IllegalStateException("redis unavailable"); }
|
@Override public java.util.List<RecentRequest> loadRecentRequests()
|
{ return delegate.loadRecentRequests(); }
|
@Override public void saveRecentRequests(java.util.List<RecentRequest> requests)
|
{ delegate.saveRecentRequests(requests); }
|
@Override public void reset(SimulatorState value) { delegate.reset(value); }
|
};
|
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, failing))
|
{
|
assertThrows(IllegalStateException.class, () -> simulator.addDevice("127.0.0.3", 2));
|
assertEquals(1, simulator.snapshot().devices().size());
|
}
|
}
|
|
@Test
|
void collectorDeletionDoesNotReplaceMemoryWhenAtomicPersistenceFails() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore delegate = new InMemoryBtsSimulatorStateStore();
|
BtsSimulatorStateStore failing = new BtsSimulatorStateStore()
|
{
|
@Override public SimulatorState loadOrInitialize(SimulatorState value)
|
{ return delegate.loadOrInitialize(value); }
|
@Override public void save(SimulatorState value) { delegate.save(value); }
|
@Override public void saveAndDeleteCollectorHistory(SimulatorState value, String collectorCode)
|
{ throw new IllegalStateException("redis unavailable"); }
|
@Override public java.util.List<RecentRequest> loadRecentRequests()
|
{ return delegate.loadRecentRequests(); }
|
@Override public void saveRecentRequests(java.util.List<RecentRequest> requests)
|
{ delegate.saveRecentRequests(requests); }
|
@Override public void reset(SimulatorState value) { delegate.reset(value); }
|
};
|
|
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, failing))
|
{
|
assertThrows(IllegalStateException.class, () -> simulator.deleteCollector(1));
|
assertEquals(1, simulator.snapshot().collectors().size());
|
}
|
}
|
|
@Test
|
void developmentSimulatorRestoresUnfinishedTombstones() throws Exception
|
{
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
|
try (BtsDevelopmentSimulator first = new BtsDevelopmentSimulator(0, store);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", first.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
first.deleteDevice(1);
|
assertEquals(4, adapter.discoverChannels().size());
|
}
|
|
try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store);
|
BtsTcpTransport transport = new BtsTcpTransport("127.0.0.1", restored.port(),
|
Duration.ofSeconds(1), Duration.ofSeconds(1));
|
DeviceAdapter adapter = new BtsDeviceAdapter("dev-sim", transport,
|
new BtsXmlCodec("dev-sim", ZoneId.of("Asia/Hong_Kong"))))
|
{
|
assertEquals(4, adapter.discoverChannels().size());
|
assertEquals(4, adapter.discoverChannels().size());
|
assertEquals(0, adapter.discoverChannels().size());
|
}
|
}
|
|
@Test
|
void developmentSimulatorRejectsNonContiguousRestoredChannelTopology()
|
{
|
SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
|
java.util.List.of(new DeviceState(1, "127.0.0.2", true,
|
java.util.List.of(new ChannelState(2, true, DeviceWorkStatus.STOPPED)))),
|
java.util.List.of(), 2, 1, java.util.List.of());
|
|
assertThrows(IllegalStateException.class,
|
() -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged)));
|
}
|
|
@Test
|
void developmentSimulatorRejectsDamagedCollectorHistoryIdentity()
|
{
|
Instant observedAt = Instant.parse("2026-08-29T05:00:00Z");
|
List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of(
|
new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃"))));
|
CollectorMeasurementRecord record = new CollectorMeasurementRecord(
|
"dev-sim", "wrong-collector", "S-01", 1L, observedAt,
|
"2026-08-29 13:00:00.000", observedAt, "temperature",
|
new BigDecimal("25.0"), "℃");
|
CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2",
|
"02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true,
|
1000, 2L, observedAt, channels, List.of(), 1L, CollectorHistoryFaults.none());
|
SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
|
List.of(), List.of(collector), 1, 2, List.of());
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(damaged);
|
store.save(damaged, List.of(new CollectorHistoryAppend(collector.deviceCode(), List.of(record))));
|
|
assertThrows(IllegalStateException.class,
|
() -> new BtsDevelopmentSimulator(0, store));
|
}
|
|
@Test
|
void developmentSimulatorRejectsCollectorSequenceBehindPersistedHistory()
|
{
|
Instant observedAt = Instant.parse("2026-08-29T06:00:00Z");
|
List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of(
|
new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃"))));
|
CollectorMeasurementRecord record = new CollectorMeasurementRecord(
|
"dev-sim", "dev-sim-collector-1", "S-01", 1L, observedAt,
|
"2026-08-29 14:00:00.000", observedAt, "temperature",
|
new BigDecimal("25.0"), "℃");
|
CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2",
|
"02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true,
|
1000, 1L, observedAt, channels, List.of(), 1L, CollectorHistoryFaults.none());
|
SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
|
List.of(), List.of(collector), 1, 2, List.of());
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(damaged);
|
store.save(damaged, List.of(new CollectorHistoryAppend(collector.deviceCode(), List.of(record))));
|
|
assertThrows(IllegalStateException.class,
|
() -> new BtsDevelopmentSimulator(0, store));
|
}
|
|
@Test
|
void developmentSimulatorRejectsHistorySequenceRegressionAcrossPages()
|
{
|
Instant observedAt = Instant.parse("2026-08-29T06:30:00Z");
|
List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of(
|
new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃"))));
|
List<CollectorMeasurementRecord> records = new java.util.ArrayList<>();
|
for (long sequence = 1; sequence <= 1000; sequence++)
|
records.add(new CollectorMeasurementRecord("dev-sim", "dev-sim-collector-1", "S-01", sequence,
|
observedAt, "2026-08-29 14:30:00.000", observedAt, "temperature",
|
new BigDecimal("25.0"), "℃"));
|
records.add(new CollectorMeasurementRecord("dev-sim", "dev-sim-collector-1", "S-01", 999,
|
observedAt, "2026-08-29 14:30:00.000", observedAt, "temperature",
|
new BigDecimal("25.0"), "℃"));
|
CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2",
|
"02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true,
|
1000, 1001L, observedAt, channels, List.of(), records.size(), CollectorHistoryFaults.none());
|
SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
|
List.of(), List.of(collector), 1, 2, List.of());
|
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(damaged);
|
store.save(damaged, List.of(new CollectorHistoryAppend(collector.deviceCode(), records)));
|
|
assertThrows(IllegalStateException.class,
|
() -> new BtsDevelopmentSimulator(0, store));
|
}
|
|
private static final class TrackingHistoryStore implements BtsSimulatorStateStore
|
{
|
private final InMemoryBtsSimulatorStateStore delegate = new InMemoryBtsSimulatorStateStore();
|
private int historyReads;
|
private int maximumRequestedLimit;
|
private long recordsRead;
|
|
@Override
|
public SimulatorState loadOrInitialize(SimulatorState defaultState)
|
{
|
return delegate.loadOrInitialize(defaultState);
|
}
|
|
@Override
|
public void save(SimulatorState state)
|
{
|
delegate.save(state);
|
}
|
|
@Override
|
public void save(SimulatorState state, List<CollectorHistoryAppend> historyAppends)
|
{
|
delegate.save(state, historyAppends);
|
}
|
|
@Override
|
public CollectorHistoryBatch loadCollectorHistoryPage(String collectorCode, long offset, int limit,
|
boolean reverse)
|
{
|
CollectorHistoryBatch batch = delegate.loadCollectorHistoryPage(collectorCode, offset, limit, reverse);
|
historyReads++;
|
maximumRequestedLimit = Math.max(maximumRequestedLimit, limit);
|
recordsRead += batch.records().size();
|
return batch;
|
}
|
|
@Override
|
public void saveAndDeleteCollectorHistory(SimulatorState state, String collectorCode)
|
{
|
delegate.saveAndDeleteCollectorHistory(state, collectorCode);
|
}
|
|
@Override
|
public List<RecentRequest> loadRecentRequests()
|
{
|
return delegate.loadRecentRequests();
|
}
|
|
@Override
|
public void saveRecentRequests(List<RecentRequest> requests)
|
{
|
delegate.saveRecentRequests(requests);
|
}
|
|
@Override
|
public void reset(SimulatorState defaultState)
|
{
|
delegate.reset(defaultState);
|
}
|
|
private void resetReadStats()
|
{
|
historyReads = 0;
|
maximumRequestedLimit = 0;
|
recordsRead = 0;
|
}
|
}
|
}
|