package com.battery.device.registry; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNotSame; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; import com.battery.device.api.DeviceAdapter; import com.battery.device.model.AdapterCapabilities; import com.battery.device.model.ChannelIdentity; import com.battery.device.model.DeviceControlCommand; class DeviceAdapterRegistryTest { @Test void synchronizesEnabledConfigurationWithoutRecreatingUnchangedAdapter() { RecordingFactory factory = new RecordingFactory(); try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of(factory))) { AdapterInstanceConfig config = config("127.0.0.1", true); registry.synchronize(config); DeviceAdapter first = registry.execute("lab-bts", adapter -> adapter); registry.synchronize(config); assertSame(first, registry.execute("lab-bts", adapter -> adapter)); assertFalse(factory.adapters.get(0).closed.get()); } } @Test void replacesChangedConfigurationAndClosesAdapterWhenDisabled() { RecordingFactory factory = new RecordingFactory(); try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of(factory))) { registry.synchronize(config("127.0.0.1", true)); StubAdapter first = factory.adapters.get(0); registry.synchronize(config("127.0.0.2", true)); assertTrue(first.closed.get()); assertNotSame(first, registry.execute("lab-bts", adapter -> adapter)); registry.synchronize(config("127.0.0.2", false)); assertTrue(factory.adapters.get(1).closed.get()); assertFalse(registry.contains("lab-bts")); assertThrows(AdapterRegistryException.class, () -> registry.execute("lab-bts", adapter -> adapter)); } } @Test void serializesOperationsForTheSameAdapterInstance() throws Exception { RecordingFactory factory = new RecordingFactory(); try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of(factory))) { registry.synchronize(config("127.0.0.1", true)); CountDownLatch firstEntered = new CountDownLatch(1); CountDownLatch releaseFirst = new CountDownLatch(1); CountDownLatch secondEntered = new CountDownLatch(1); ExecutorService executor = Executors.newFixedThreadPool(2); try { Future first = executor.submit(() -> registry.execute("lab-bts", adapter -> { firstEntered.countDown(); await(releaseFirst); return null; })); assertTrue(firstEntered.await(1, TimeUnit.SECONDS)); Future second = executor.submit(() -> registry.execute("lab-bts", adapter -> { secondEntered.countDown(); return null; })); assertFalse(secondEntered.await(100, TimeUnit.MILLISECONDS)); releaseFirst.countDown(); first.get(1, TimeUnit.SECONDS); second.get(1, TimeUnit.SECONDS); assertTrue(secondEntered.await(1, TimeUnit.SECONDS)); } finally { executor.shutdownNow(); } } } @Test void configurationReplacementWaitsForRunningOperationBeforeClosingAdapter() throws Exception { RecordingFactory factory = new RecordingFactory(); try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of(factory))) { registry.synchronize(config("127.0.0.1", true)); StubAdapter firstAdapter = factory.adapters.get(0); CountDownLatch operationEntered = new CountDownLatch(1); CountDownLatch releaseOperation = new CountDownLatch(1); CountDownLatch replacementStarted = new CountDownLatch(1); ExecutorService executor = Executors.newFixedThreadPool(2); try { Future operation = executor.submit(() -> registry.execute("lab-bts", adapter -> { operationEntered.countDown(); await(releaseOperation); return null; })); assertTrue(operationEntered.await(1, TimeUnit.SECONDS)); Future replacement = executor.submit(() -> { replacementStarted.countDown(); registry.synchronize(config("127.0.0.2", true)); }); assertTrue(replacementStarted.await(1, TimeUnit.SECONDS)); assertFalse(replacement.isDone()); assertFalse(firstAdapter.closed.get()); releaseOperation.countDown(); operation.get(1, TimeUnit.SECONDS); replacement.get(1, TimeUnit.SECONDS); assertTrue(firstAdapter.closed.get()); } finally { executor.shutdownNow(); } } } private static void await(CountDownLatch latch) { try { if (!latch.await(1, TimeUnit.SECONDS)) throw new AssertionError("等待适配器测试信号超时"); } catch (InterruptedException exception) { Thread.currentThread().interrupt(); throw new AssertionError("等待适配器测试信号被中断", exception); } } private static AdapterInstanceConfig config(String host, boolean enabled) { return new AdapterInstanceConfig("lab-bts", "BTS", host, 502, 5000, "Asia/Hong_Kong", enabled, AdapterCapabilities.observeOnly()); } private static final class RecordingFactory implements DeviceAdapterFactory { private final List adapters = new ArrayList<>(); @Override public String adapterType() { return "BTS"; } @Override public DeviceAdapter create(AdapterInstanceConfig config) { StubAdapter adapter = new StubAdapter(config.adapterCode()); adapters.add(adapter); return adapter; } } private static final class StubAdapter implements DeviceAdapter { private final String code; private final AtomicBoolean closed = new AtomicBoolean(); private StubAdapter(String code) { this.code = code; } @Override public String adapterCode() { return code; } @Override public AdapterCapabilities capabilities() { return AdapterCapabilities.observeOnly(); } @Override public List discoverChannels() { return List.of(); } @Override public java.util.Map inquire( java.util.Set channels) { return java.util.Map.of(); } @Override public List inquireHistory( java.util.Set channels) { return List.of(); } @Override public com.battery.device.model.HistoryPage downloadHistory(ChannelIdentity channel, String btsTestId, long startPosition, int limit) { throw new UnsupportedOperationException(); } @Override public void control(ChannelIdentity channel, DeviceControlCommand command) { throw new UnsupportedOperationException(); } @Override public void close() { closed.set(true); } } }