| | |
| | | import java.util.concurrent.BlockingQueue; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.ExecutorService; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | | import java.util.concurrent.ThreadFactory; |
| | | import java.util.concurrent.ThreadPoolExecutor; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.function.Function; |
| | | |
| | | /** |
| | |
| | | |
| | | private final ServerSocket serverSocket; |
| | | private final int port; |
| | | private final ExecutorService executor = Executors.newSingleThreadExecutor(); |
| | | /** 连接服务线程显式创建并命名。该池只提交一个常驻服务任务,队列容量 1 只是声明不再接受更多任务。 */ |
| | | private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, |
| | | new LinkedBlockingQueue<>(1), simulatorThreadFactory()); |
| | | private final BlockingQueue<ScriptedAction> actions = new LinkedBlockingQueue<>(); |
| | | private final List<String> requests = new CopyOnWriteArrayList<>(); |
| | | private final Function<String, String> responseProvider; |
| | |
| | | serverSocket.close(); |
| | | } |
| | | |
| | | /** 创建一启动就关闭监听、任何连接都会被拒绝的模拟器。 */ |
| | | public static ScriptedBtsTcpSimulator refusingConnections() throws IOException |
| | | { |
| | | return new ScriptedBtsTcpSimulator(0, false, null); |
| | |
| | | return new ScriptedBtsTcpSimulator(port, true, responseProvider); |
| | | } |
| | | |
| | | /** 返回该模拟器绑定的本机端口;构造时传入 0 时返回的是系统分配端口而非 0。 */ |
| | | public int port() |
| | | { |
| | | return port; |
| | | } |
| | | |
| | | /** 向脚本队列追加一条零延迟应答,按 UTF-8 编码,由下一次请求按顺序取用。 */ |
| | | public void enqueue(String response) |
| | | { |
| | | actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false)); |
| | | } |
| | | |
| | | /** 追加一条延迟应答:连接服务线程收到请求后先等待 delay 再写出;delay 不得为负。 */ |
| | | public void enqueueDelayed(Duration delay, String response) |
| | | { |
| | | if (delay.isNegative()) throw new IllegalArgumentException("delay must not be negative"); |
| | | actions.add(new ScriptedAction(delay, response.getBytes(StandardCharsets.UTF_8), false)); |
| | | } |
| | | |
| | | /** 追加一条零延迟的原始字节应答:字节原样写出后追加帧结束符,不做字符编码转换。 */ |
| | | public void enqueueRaw(byte[] response) |
| | | { |
| | | actions.add(new ScriptedAction(Duration.ZERO, response.clone(), false)); |
| | | } |
| | | |
| | | /** 追加一条断开动作:处理到该条时结束当前连接,且不写出任何应答。 */ |
| | | public void enqueueDisconnect() |
| | | { |
| | | actions.add(new ScriptedAction(Duration.ZERO, null, true)); |
| | | } |
| | | |
| | | /** 返回已接收请求帧的只读快照;只有脚本模式(未配置 responseProvider)才记录。 */ |
| | | public List<String> requests() |
| | | { |
| | | return List.copyOf(requests); |
| | |
| | | } |
| | | } |
| | | |
| | | /** 关闭监听并终止连接服务线程,最多等待 2 秒,避免测试退出时残留线程。 */ |
| | | @Override |
| | | public void close() throws Exception |
| | | { |
| | |
| | | executor.awaitTermination(2, TimeUnit.SECONDS); |
| | | } |
| | | |
| | | /** 模拟器连接服务线程统一命名并设为守护线程,避免测试退出时残留。 */ |
| | | private static ThreadFactory simulatorThreadFactory() |
| | | { |
| | | AtomicInteger sequence = new AtomicInteger(); |
| | | return runnable -> { |
| | | Thread thread = new Thread(runnable, "bts-simulator-pool-" + sequence.incrementAndGet()); |
| | | thread.setDaemon(true); |
| | | return thread; |
| | | }; |
| | | } |
| | | |
| | | private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect) |
| | | { |
| | | } |