/* * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.boot.devtools.livereload; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.apache.tomcat.websocket.WsWebSocketContainer; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.util.SocketUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.PingMessage; import org.springframework.web.socket.PongMessage; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.WebSocketClient; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.handler.TextWebSocketHandler; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link LiveReloadServer}. * * @author Phillip Webb * @author Andy Wilkinson */ public class LiveReloadServerTests { private static final String HANDSHAKE = "{command: 'hello', " + "protocols: ['http://livereload.com/protocols/official-7']}"; private int port = SocketUtils.findAvailableTcpPort(); private MonitoredLiveReloadServer server; @Before public void setUp() throws Exception { this.server = new MonitoredLiveReloadServer(this.port); this.server.start(); } @After public void tearDown() throws Exception { this.server.stop(); } @Test @Ignore public void servesLivereloadJs() throws Exception { RestTemplate template = new RestTemplate(); URI uri = new URI("http://localhost:" + this.port + "/livereload.js"); String script = template.getForObject(uri, String.class); assertThat(script).contains("livereload.com/protocols/official-7"); } @Test public void triggerReload() throws Exception { LiveReloadWebSocketHandler handler = connect(); this.server.triggerReload(); Thread.sleep(200); this.server.stop(); assertThat(handler.getMessages().get(0)) .contains("http://livereload.com/protocols/official-7"); assertThat(handler.getMessages().get(1)).contains("command\":\"reload\""); } @Test public void pingPong() throws Exception { LiveReloadWebSocketHandler handler = connect(); handler.sendMessage(new PingMessage()); Thread.sleep(200); assertThat(handler.getPongCount()).isEqualTo(1); this.server.stop(); } @Test public void clientClose() throws Exception { LiveReloadWebSocketHandler handler = connect(); handler.close(); awaitClosedException(); assertThat(this.server.getClosedExceptions().size()).isGreaterThan(0); } private void awaitClosedException() throws InterruptedException { long startTime = System.currentTimeMillis(); while (this.server.getClosedExceptions().isEmpty() && System.currentTimeMillis() - startTime < 10000) { Thread.sleep(100); } } @Test public void serverClose() throws Exception { LiveReloadWebSocketHandler handler = connect(); this.server.stop(); Thread.sleep(200); assertThat(handler.getCloseStatus().getCode()).isEqualTo(1006); } private LiveReloadWebSocketHandler connect() throws Exception { WebSocketClient client = new StandardWebSocketClient(new WsWebSocketContainer()); LiveReloadWebSocketHandler handler = new LiveReloadWebSocketHandler(); client.doHandshake(handler, "ws://localhost:" + this.port + "/livereload"); handler.awaitHello(); return handler; } /** * Useful main method for manual testing against a real browser. * @param args main args * @throws IOException in case of I/O errors */ public static void main(String[] args) throws IOException { LiveReloadServer server = new LiveReloadServer(); server.start(); while (true) { try { Thread.sleep(1000); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } server.triggerReload(); } } /** * {@link LiveReloadServer} with additional monitoring. */ private static class MonitoredLiveReloadServer extends LiveReloadServer { private final List<ConnectionClosedException> closedExceptions = new ArrayList<>(); private final Object monitor = new Object(); MonitoredLiveReloadServer(int port) { super(port); } @Override protected Connection createConnection(java.net.Socket socket, InputStream inputStream, OutputStream outputStream) throws IOException { return new MonitoredConnection(socket, inputStream, outputStream); } public List<ConnectionClosedException> getClosedExceptions() { synchronized (this.monitor) { return new ArrayList<>(this.closedExceptions); } } private class MonitoredConnection extends Connection { MonitoredConnection(java.net.Socket socket, InputStream inputStream, OutputStream outputStream) throws IOException { super(socket, inputStream, outputStream); } @Override public void run() throws Exception { try { super.run(); } catch (ConnectionClosedException ex) { ex.printStackTrace(); synchronized (MonitoredLiveReloadServer.this.monitor) { MonitoredLiveReloadServer.this.closedExceptions.add(ex); } throw ex; } } } } private static class LiveReloadWebSocketHandler extends TextWebSocketHandler { private WebSocketSession session; private final CountDownLatch helloLatch = new CountDownLatch(2); private final List<String> messages = new ArrayList<>(); private int pongCount; private CloseStatus closeStatus; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { this.session = session; session.sendMessage(new TextMessage(HANDSHAKE)); this.helloLatch.countDown(); } public void awaitHello() throws InterruptedException { this.helloLatch.await(1, TimeUnit.MINUTES); Thread.sleep(200); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { if (message.getPayload().contains("hello")) { this.helloLatch.countDown(); } this.messages.add(message.getPayload()); } @Override protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception { this.pongCount++; } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { this.closeStatus = status; } public void sendMessage(WebSocketMessage<?> message) throws IOException { this.session.sendMessage(message); } public void close() throws IOException { this.session.close(); } public List<String> getMessages() { return this.messages; } public int getPongCount() { return this.pongCount; } public CloseStatus getCloseStatus() { return this.closeStatus; } } }