Rename spring-boot-developer-tools -> devtools

Fixes gh-3099
This commit is contained in:
Phillip Webb
2015-06-04 12:31:49 -07:00
parent 683c19ee8b
commit 983484f429
165 changed files with 507 additions and 437 deletions

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2015 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;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.RemoteUrlPropertyExtractor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RemoteUrlPropertyExtractor}.
*
* @author Phillip Webb
*/
public class RemoteUrlPropertyExtractorTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void missingUrl() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("No remote URL specified");
doTest();
}
@Test
public void malformedUrl() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Malformed URL '::://wibble'");
doTest("::://wibble");
}
@Test
public void multipleUrls() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Multiple URLs specified");
doTest("http://localhost:8080", "http://localhost:9090");
}
@Test
public void validUrl() throws Exception {
ApplicationContext context = doTest("http://localhost:8080");
assertThat(context.getEnvironment().getProperty("remoteUrl"),
equalTo("http://localhost:8080"));
}
private ApplicationContext doTest(String... args) {
SpringApplication application = new SpringApplication(Config.class);
application.setWebEnvironment(false);
application.addListeners(new RemoteUrlPropertyExtractor());
return application.run(args);
}
@Configuration
static class Config {
}
}

View File

@@ -0,0 +1,209 @@
/*
* Copyright 2012-2015 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.autoconfigure;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import org.springframework.boot.devtools.restart.MockRestartInitializer;
import org.springframework.boot.devtools.restart.MockRestarter;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.util.SocketUtils;
import org.thymeleaf.templateresolver.TemplateResolver;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link LocalDevToolsAutoConfiguration}.
*
* @author Phillip Webb
*/
public class LocalDevToolsAutoConfigurationTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public MockRestarter mockRestarter = new MockRestarter();
private int liveReloadPort = SocketUtils.findAvailableTcpPort();
private ConfigurableApplicationContext context;
@After
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void thymeleafCacheIsFalse() throws Exception {
this.context = initializeAndRun(Config.class);
TemplateResolver resolver = this.context.getBean(TemplateResolver.class);
resolver.initialize();
assertThat(resolver.isCacheable(), equalTo(false));
}
@Test
public void liveReloadServer() throws Exception {
this.context = initializeAndRun(Config.class);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
assertThat(server.isStarted(), equalTo(true));
}
@Test
public void liveReloadTriggeredOnContextRefresh() throws Exception {
this.context = initializeAndRun(ConfigWithMockLiveReload.class);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
reset(server);
this.context.publishEvent(new ContextRefreshedEvent(this.context));
verify(server).triggerReload();
}
@Test
public void liveReloadTriggerdOnClassPathChangeWithoutRestart() throws Exception {
this.context = initializeAndRun(ConfigWithMockLiveReload.class);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
reset(server);
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
Collections.<ChangedFiles> emptySet(), false);
this.context.publishEvent(event);
verify(server).triggerReload();
}
@Test
public void liveReloadNotTriggerdOnClassPathChangeWithRestart() throws Exception {
this.context = initializeAndRun(ConfigWithMockLiveReload.class);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
reset(server);
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
Collections.<ChangedFiles> emptySet(), true);
this.context.publishEvent(event);
verify(server, never()).triggerReload();
}
@Test
public void liveReloadDisabled() throws Exception {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("spring.devtools.livereload.enabled", false);
this.context = initializeAndRun(Config.class, properties);
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(OptionalLiveReloadServer.class);
}
@Test
public void restartTriggerdOnClassPathChangeWithRestart() throws Exception {
this.context = initializeAndRun(Config.class);
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
Collections.<ChangedFiles> emptySet(), true);
this.context.publishEvent(event);
verify(this.mockRestarter.getMock()).restart();
}
@Test
public void restartNotTriggerdOnClassPathChangeWithRestart() throws Exception {
this.context = initializeAndRun(Config.class);
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
Collections.<ChangedFiles> emptySet(), false);
this.context.publishEvent(event);
verify(this.mockRestarter.getMock(), never()).restart();
}
@Test
public void restartWatchingClassPath() throws Exception {
this.context = initializeAndRun(Config.class);
ClassPathFileSystemWatcher watcher = this.context
.getBean(ClassPathFileSystemWatcher.class);
assertThat(watcher, notNullValue());
}
@Test
public void restartDisabled() throws Exception {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("spring.devtools.restart.enabled", false);
this.context = initializeAndRun(Config.class, properties);
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(ClassPathFileSystemWatcher.class);
}
private ConfigurableApplicationContext initializeAndRun(Class<?> config) {
return initializeAndRun(config, Collections.<String, Object> emptyMap());
}
private ConfigurableApplicationContext initializeAndRun(Class<?> config,
Map<String, Object> properties) {
Restarter.initialize(new String[0], false, new MockRestartInitializer(), false);
SpringApplication application = new SpringApplication(config);
application.setDefaultProperties(getDefaultProperties(properties));
application.setWebEnvironment(false);
ConfigurableApplicationContext context = application.run();
return context;
}
private Map<String, Object> getDefaultProperties(
Map<String, Object> specifiedProperties) {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("spring.thymeleaf.check-template-location", false);
properties.put("spring.devtools.livereload.port", this.liveReloadPort);
properties.putAll(specifiedProperties);
return properties;
}
@Configuration
@Import({ LocalDevToolsAutoConfiguration.class,
ThymeleafAutoConfiguration.class })
public static class Config {
}
@Configuration
@Import({ LocalDevToolsAutoConfiguration.class,
ThymeleafAutoConfiguration.class })
public static class ConfigWithMockLiveReload {
@Bean
public LiveReloadServer liveReloadServer() {
return mock(LiveReloadServer.class);
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2015 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.autoconfigure;
import org.junit.Test;
import org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OptionalLiveReloadServer}.
*
* @author Phillip Webb
*/
public class OptionalLiveReloadServerTests {
@Test
public void nullServer() throws Exception {
OptionalLiveReloadServer server = new OptionalLiveReloadServer(null);
server.startServer();
server.triggerReload();
}
@Test
public void serverWontStart() throws Exception {
LiveReloadServer delegate = mock(LiveReloadServer.class);
OptionalLiveReloadServer server = new OptionalLiveReloadServer(delegate);
willThrow(new RuntimeException("Error")).given(delegate).start();
server.startServer();
server.triggerReload();
verify(delegate, never()).triggerReload();
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2012-2015 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.autoconfigure;
import java.io.IOException;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
import org.springframework.boot.devtools.remote.server.DispatcherFilter;
import org.springframework.boot.devtools.restart.MockRestarter;
import org.springframework.boot.devtools.restart.server.HttpRestartServer;
import org.springframework.boot.devtools.restart.server.SourceFolderUrlFilter;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer;
import org.springframework.boot.devtools.tunnel.server.RemoteDebugPortProvider;
import org.springframework.boot.devtools.tunnel.server.SocketTargetServerConnection;
import org.springframework.boot.devtools.tunnel.server.TargetServerConnection;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RemoteDevToolsAutoConfiguration}.
*
* @author Rob Winch
* @author Phillip Webb
*/
public class RemoteDevToolsAutoConfigurationTests {
private static final String DEFAULT_CONTEXT_PATH = RemoteDevToolsProperties.DEFAULT_CONTEXT_PATH;
private static final String DEFAULT_SECRET_HEADER_NAME = RemoteDevToolsProperties.DEFAULT_SECRET_HEADER_NAME;
@Rule
public MockRestarter mockRestarter = new MockRestarter();
@Rule
public ExpectedException thrown = ExpectedException.none();
private AnnotationConfigWebApplicationContext context;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private MockFilterChain chain;
@Before
public void setup() {
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.chain = new MockFilterChain();
}
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void disabledIfRemoteSecretIsMissing() throws Exception {
loadContext("a:b");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(DispatcherFilter.class);
}
@Test
public void ignoresUnmappedUrl() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI("/restart");
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
filter.doFilter(this.request, this.response, this.chain);
assertRestartInvoked(false);
}
@Test
public void ignoresIfMissingSecretFromRequest() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
filter.doFilter(this.request, this.response, this.chain);
assertRestartInvoked(false);
}
@Test
public void ignoresInvalidSecretInRequest() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "invalid");
filter.doFilter(this.request, this.response, this.chain);
assertRestartInvoked(false);
}
@Test
public void invokeRestartWithDefaultSetup() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
filter.doFilter(this.request, this.response, this.chain);
assertRestartInvoked(true);
}
@Test
public void disableRestart() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret",
"spring.devtools.remote.restart.enabled:false");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean("remoteRestartHanderMapper");
}
@Test
public void invokeTunnelWithDefaultSetup() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug");
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
filter.doFilter(this.request, this.response, this.chain);
assertTunnelInvoked(true);
}
@Test
public void invokeTunnelWithCustomHeaderName() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret",
"spring.devtools.remote.secretHeaderName:customheader");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug");
this.request.addHeader("customheader", "supersecret");
filter.doFilter(this.request, this.response, this.chain);
assertTunnelInvoked(true);
}
@Test
public void disableRemoteDebug() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret",
"spring.devtools.remote.debug.enabled:false");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean("remoteDebugHanderMapper");
}
@Test
public void devToolsHealthReturns200() throws Exception {
loadContext("spring.devtools.remote.secret:supersecret");
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
this.request.setRequestURI(DEFAULT_CONTEXT_PATH);
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
this.response.setStatus(500);
filter.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus(), equalTo(200));
}
private void assertTunnelInvoked(boolean value) {
assertThat(this.context.getBean(MockHttpTunnelServer.class).invoked,
equalTo(value));
}
private void assertRestartInvoked(boolean value) {
assertThat(this.context.getBean(MockHttpRestartServer.class).invoked,
equalTo(value));
}
private void loadContext(String... properties) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext());
this.context.register(Config.class, ServerPropertiesAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context, properties);
this.context.refresh();
}
@Configuration
@Import(RemoteDevToolsAutoConfiguration.class)
static class Config {
@Bean
public HttpTunnelServer remoteDebugHttpTunnelServer() {
return new MockHttpTunnelServer(new SocketTargetServerConnection(
new RemoteDebugPortProvider()));
}
@Bean
public HttpRestartServer remoteRestartHttpRestartServer() {
SourceFolderUrlFilter sourceFolderUrlFilter = mock(SourceFolderUrlFilter.class);
return new MockHttpRestartServer(sourceFolderUrlFilter);
}
}
/**
* Mock {@link HttpTunnelServer} implementation.
*/
static class MockHttpTunnelServer extends HttpTunnelServer {
private boolean invoked;
public MockHttpTunnelServer(TargetServerConnection serverConnection) {
super(serverConnection);
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
this.invoked = true;
}
}
/**
* Mock {@link HttpRestartServer} implementation.
*/
static class MockHttpRestartServer extends HttpRestartServer {
private boolean invoked;
public MockHttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
super(sourceFolderUrlFilter);
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
this.invoked = true;
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2015 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.classpath;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ClassPathChangedEvent}.
*
* @author Phillip Webb
*/
public class ClassPathChangedEventTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private Object source = new Object();
@Test
public void changeSetMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ChangeSet must not be null");
new ClassPathChangedEvent(this.source, null, false);
}
@Test
public void getChangeSet() throws Exception {
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
ClassPathChangedEvent event = new ClassPathChangedEvent(this.source, changeSet,
false);
assertThat(event.getChangeSet(), sameInstance(changeSet));
}
@Test
public void getRestartRequired() throws Exception {
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
ClassPathChangedEvent event;
event = new ClassPathChangedEvent(this.source, changeSet, false);
assertThat(event.isRestartRequired(), equalTo(false));
event = new ClassPathChangedEvent(this.source, changeSet, true);
assertThat(event.isRestartRequired(), equalTo(true));
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2012-2015 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.classpath;
import java.io.File;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileChangeListener;
import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link ClassPathFileChangeListener}.
*
* @author Phillip Webb
*/
public class ClassPathFileChangeListenerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Captor
private ArgumentCaptor<ApplicationEvent> eventCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void eventPublisherMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("EventPublisher must not be null");
new ClassPathFileChangeListener(null, mock(ClassPathRestartStrategy.class));
}
@Test
public void restartStrategyMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RestartStrategy must not be null");
new ClassPathFileChangeListener(mock(ApplicationEventPublisher.class), null);
}
@Test
public void sendsEventWithoutRestart() throws Exception {
testSendsEvent(false);
}
@Test
public void sendsEventWithRestart() throws Exception {
testSendsEvent(true);
}
private void testSendsEvent(boolean restart) {
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
ClassPathRestartStrategy restartStrategy = mock(ClassPathRestartStrategy.class);
ClassPathFileChangeListener listener = new ClassPathFileChangeListener(
eventPublisher, restartStrategy);
File folder = new File("s1");
File file = new File("f1");
ChangedFile file1 = new ChangedFile(folder, file, ChangedFile.Type.ADD);
ChangedFile file2 = new ChangedFile(folder, file, ChangedFile.Type.ADD);
Set<ChangedFile> files = new LinkedHashSet<ChangedFile>();
files.add(file1);
files.add(file2);
ChangedFiles changedFiles = new ChangedFiles(new File("source"), files);
Set<ChangedFiles> changeSet = Collections.singleton(changedFiles);
if (restart) {
given(restartStrategy.isRestartRequired(file2)).willReturn(true);
}
listener.onChange(changeSet);
verify(eventPublisher).publishEvent(this.eventCaptor.capture());
ClassPathChangedEvent actualEvent = (ClassPathChangedEvent) this.eventCaptor
.getValue();
assertThat(actualEvent.getChangeSet(), equalTo(changeSet));
assertThat(actualEvent.isRestartRequired(), equalTo(restart));
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2015 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.classpath;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ClassPathFileSystemWatcher}.
*
* @author Phillip Webb
*/
public class ClassPathFileSystemWatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void urlsMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Urls must not be null");
URL[] urls = null;
new ClassPathFileSystemWatcher(urls);
}
@Test
public void configuredWithRestartStrategy() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
Map<String, Object> properties = new HashMap<String, Object>();
File folder = this.temp.newFolder();
List<URL> urls = new ArrayList<URL>();
urls.add(new URL("http://spring.io"));
urls.add(folder.toURI().toURL());
properties.put("urls", urls);
MapPropertySource propertySource = new MapPropertySource("test", properties);
context.getEnvironment().getPropertySources().addLast(propertySource);
context.register(Config.class);
context.refresh();
Thread.sleep(100);
File classFile = new File(folder, "Example.class");
FileCopyUtils.copy("file".getBytes(), classFile);
Thread.sleep(1100);
List<ClassPathChangedEvent> events = context.getBean(Listener.class).getEvents();
assertThat(events.size(), equalTo(1));
assertThat(events.get(0).getChangeSet().iterator().next().getFiles().iterator()
.next().getFile(), equalTo(classFile));
context.close();
}
@Configuration
public static class Config {
@Autowired
public Environment environemnt;
@Bean
public ClassPathFileSystemWatcher watcher() {
FileSystemWatcher watcher = new FileSystemWatcher(false, 100, 10);
URL[] urls = this.environemnt.getProperty("urls", URL[].class);
return new ClassPathFileSystemWatcher(watcher, restartStrategy(), urls);
}
@Bean
public ClassPathRestartStrategy restartStrategy() {
return new ClassPathRestartStrategy() {
@Override
public boolean isRestartRequired(ChangedFile file) {
return false;
}
};
}
@Bean
public Listener listener() {
return new Listener();
}
}
public static class Listener implements ApplicationListener<ClassPathChangedEvent> {
private List<ClassPathChangedEvent> events = new ArrayList<ClassPathChangedEvent>();
@Override
public void onApplicationEvent(ClassPathChangedEvent event) {
this.events.add(event);
}
public List<ClassPathChangedEvent> getEvents() {
return this.events;
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2015 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.classpath;
import java.io.File;
import org.junit.Test;
import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.devtools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link PatternClassPathRestartStrategy}.
*
* @author Phillip Webb
*/
public class PatternClassPathRestartStrategyTests {
@Test
public void nullPattern() throws Exception {
ClassPathRestartStrategy strategy = createStrategy(null);
assertRestartRequired(strategy, "a/b.txt", true);
}
@Test
public void emptyPattern() throws Exception {
ClassPathRestartStrategy strategy = createStrategy("");
assertRestartRequired(strategy, "a/b.txt", true);
}
@Test
public void singlePattern() throws Exception {
ClassPathRestartStrategy strategy = createStrategy("static/**");
assertRestartRequired(strategy, "static/file.txt", false);
assertRestartRequired(strategy, "static/folder/file.txt", false);
assertRestartRequired(strategy, "public/file.txt", true);
assertRestartRequired(strategy, "public/folder/file.txt", true);
}
@Test
public void multiplePatterns() throws Exception {
ClassPathRestartStrategy strategy = createStrategy("static/**,public/**");
assertRestartRequired(strategy, "static/file.txt", false);
assertRestartRequired(strategy, "static/folder/file.txt", false);
assertRestartRequired(strategy, "public/file.txt", false);
assertRestartRequired(strategy, "public/folder/file.txt", false);
assertRestartRequired(strategy, "src/file.txt", true);
assertRestartRequired(strategy, "src/folder/file.txt", true);
}
private ClassPathRestartStrategy createStrategy(String pattern) {
return new PatternClassPathRestartStrategy(pattern);
}
private void assertRestartRequired(ClassPathRestartStrategy strategy,
String relativeName, boolean expected) {
assertThat(strategy.isRestartRequired(mockFile(relativeName)), equalTo(expected));
}
private ChangedFile mockFile(String relativeName) {
return new ChangedFile(new File("."), new File("./" + relativeName), Type.ADD);
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2015 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.filewatch;
import java.io.File;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ChangedFile}.
*
* @author Phillip Webb
*/
public class ChangedFileTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void sourceFolderMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SourceFolder must not be null");
new ChangedFile(null, this.temp.newFile(), Type.ADD);
}
@Test
public void fileMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new ChangedFile(this.temp.newFolder(), null, Type.ADD);
}
@Test
public void typeMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Type must not be null");
new ChangedFile(this.temp.newFile(), this.temp.newFolder(), null);
}
@Test
public void getFile() throws Exception {
File file = this.temp.newFile();
ChangedFile changedFile = new ChangedFile(this.temp.newFolder(), file, Type.ADD);
assertThat(changedFile.getFile(), equalTo(file));
}
@Test
public void getType() throws Exception {
ChangedFile changedFile = new ChangedFile(this.temp.newFolder(),
this.temp.newFile(), Type.DELETE);
assertThat(changedFile.getType(), equalTo(Type.DELETE));
}
@Test
public void getRelativeName() throws Exception {
File folder = this.temp.newFolder();
File subFolder = new File(folder, "A");
File file = new File(subFolder, "B.txt");
ChangedFile changedFile = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFile.getRelativeName(), equalTo("A/B.txt"));
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2012-2015 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.filewatch;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.filewatch.FileSnapshot;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link FileSnapshot}.
*
* @author Phillip Webb
*/
public class FileSnapshotTests {
private static final long TWO_MINS = TimeUnit.MINUTES.toMillis(2);
private static final long MODIFIED = new Date().getTime()
- TimeUnit.DAYS.toMillis(10);
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void fileMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new FileSnapshot(null);
}
@Test
public void fileMustNotBeAFolder() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be a folder");
new FileSnapshot(this.temporaryFolder.newFolder());
}
@Test
public void equalsIfTheSame() throws Exception {
File file = createNewFile("abc", MODIFIED);
File fileCopy = new File(file, "x").getParentFile();
FileSnapshot snapshot1 = new FileSnapshot(file);
FileSnapshot snapshot2 = new FileSnapshot(fileCopy);
assertThat(snapshot1, equalTo(snapshot2));
assertThat(snapshot1.hashCode(), equalTo(snapshot2.hashCode()));
}
@Test
public void notEqualsIfDeleted() throws Exception {
File file = createNewFile("abc", MODIFIED);
FileSnapshot snapshot1 = new FileSnapshot(file);
file.delete();
assertThat(snapshot1, not(equalTo(new FileSnapshot(file))));
}
@Test
public void notEqualsIfLengthChanges() throws Exception {
File file = createNewFile("abc", MODIFIED);
FileSnapshot snapshot1 = new FileSnapshot(file);
setupFile(file, "abcd", MODIFIED);
assertThat(snapshot1, not(equalTo(new FileSnapshot(file))));
}
@Test
public void notEqualsIfLastModifiedChanges() throws Exception {
File file = createNewFile("abc", MODIFIED);
FileSnapshot snapshot1 = new FileSnapshot(file);
setupFile(file, "abc", MODIFIED + TWO_MINS);
assertThat(snapshot1, not(equalTo(new FileSnapshot(file))));
}
private File createNewFile(String content, long lastModified) throws IOException {
File file = this.temporaryFolder.newFile();
setupFile(file, content, lastModified);
return file;
}
private void setupFile(File file, String content, long lastModified)
throws IOException {
FileCopyUtils.copy(content.getBytes(), file);
file.setLastModified(lastModified);
}
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2012-2015 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.filewatch;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.filewatch.FileChangeListener;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link FileSystemWatcher}.
*
* @author Phillip Webb
*/
public class FileSystemWatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private FileSystemWatcher watcher;
private List<Set<ChangedFiles>> changes = new ArrayList<Set<ChangedFiles>>();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Before
public void setup() throws Exception {
setupWatcher(20, 10);
}
@Test
public void listenerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("FileChangeListener must not be null");
this.watcher.addListener(null);
}
@Test
public void cannotAddListenerToStartedListener() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("FileSystemWatcher already started");
this.watcher.start();
this.watcher.addListener(mock(FileChangeListener.class));
}
@Test
public void sourceFolderMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be null");
this.watcher.addSourceFolder(null);
}
@Test
public void cannotAddSourceFolderToStartedListener() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("FileSystemWatcher already started");
this.watcher.start();
this.watcher.addSourceFolder(this.temp.newFolder());
}
@Test
public void addFile() throws Exception {
File folder = startWithNewFolder();
File file = touch(new File(folder, "test.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
}
@Test
public void addNestedFile() throws Exception {
File folder = startWithNewFolder();
File file = touch(new File(new File(folder, "sub"), "text.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
}
@Test
public void waitsForIdleTime() throws Exception {
this.changes.clear();
setupWatcher(100, 0);
File folder = startWithNewFolder();
touch(new File(folder, "test1.txt"));
Thread.sleep(200);
touch(new File(folder, "test2.txt"));
this.watcher.stopAfter(1);
assertThat(this.changes.size(), equalTo(2));
}
@Test
public void waitsForQuietTime() throws Exception {
setupWatcher(300, 200);
File folder = startWithNewFolder();
for (int i = 0; i < 10; i++) {
touch(new File(folder, i + "test.txt"));
Thread.sleep(100);
}
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
assertThat(changedFiles.getFiles().size(), equalTo(10));
}
@Test
public void withExistingFiles() throws Exception {
File folder = this.temp.newFolder();
touch(new File(folder, "test.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.start();
File file = touch(new File(folder, "test2.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
}
@Test
public void multipleSources() throws Exception {
File folder1 = this.temp.newFolder();
File folder2 = this.temp.newFolder();
this.watcher.addSourceFolder(folder1);
this.watcher.addSourceFolder(folder2);
this.watcher.start();
File file1 = touch(new File(folder1, "test.txt"));
File file2 = touch(new File(folder2, "test.txt"));
this.watcher.stopAfter(1);
Set<ChangedFiles> change = getSingleOnChange();
assertThat(change.size(), equalTo(2));
for (ChangedFiles changedFiles : change) {
if (changedFiles.getSourceFolder().equals(folder1)) {
ChangedFile file = new ChangedFile(folder1, file1, Type.ADD);
assertEquals(new HashSet<ChangedFile>(Arrays.asList(file)),
changedFiles.getFiles());
}
else {
ChangedFile file = new ChangedFile(folder2, file2, Type.ADD);
assertEquals(new HashSet<ChangedFile>(Arrays.asList(file)),
changedFiles.getFiles());
}
}
}
@Test
public void multipleListeners() throws Exception {
File folder = this.temp.newFolder();
final Set<ChangedFiles> listener2Changes = new LinkedHashSet<ChangedFiles>();
this.watcher.addSourceFolder(folder);
this.watcher.addListener(new FileChangeListener() {
@Override
public void onChange(Set<ChangedFiles> changeSet) {
listener2Changes.addAll(changeSet);
}
});
this.watcher.start();
File file = touch(new File(folder, "test.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
assertEquals(this.changes.get(0), listener2Changes);
}
@Test
public void modifyDeleteAndAdd() throws Exception {
File folder = this.temp.newFolder();
File modify = touch(new File(folder, "modify.txt"));
File delete = touch(new File(folder, "delete.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.start();
FileCopyUtils.copy("abc".getBytes(), modify);
delete.delete();
File add = touch(new File(folder, "add.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
Set<ChangedFile> actual = changedFiles.getFiles();
Set<ChangedFile> expected = new HashSet<ChangedFile>();
expected.add(new ChangedFile(folder, modify, Type.MODIFY));
expected.add(new ChangedFile(folder, delete, Type.DELETE));
expected.add(new ChangedFile(folder, add, Type.ADD));
assertEquals(expected, actual);
}
private void setupWatcher(long idleTime, long quietTime) {
this.watcher = new FileSystemWatcher(false, idleTime, quietTime);
this.watcher.addListener(new FileChangeListener() {
@Override
public void onChange(Set<ChangedFiles> changeSet) {
FileSystemWatcherTests.this.changes.add(changeSet);
}
});
}
private File startWithNewFolder() throws IOException {
File folder = this.temp.newFolder();
this.watcher.addSourceFolder(folder);
this.watcher.start();
return folder;
}
private ChangedFiles getSingleChangedFiles() {
Set<ChangedFiles> singleChange = getSingleOnChange();
assertThat(singleChange.size(), equalTo(1));
return singleChange.iterator().next();
}
private Set<ChangedFiles> getSingleOnChange() {
assertThat(this.changes.size(), equalTo(1));
return this.changes.get(0);
}
private File touch(File file) throws FileNotFoundException, IOException {
file.getParentFile().mkdirs();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.close();
return file;
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2012-2015 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.filewatch;
import java.io.File;
import java.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.filewatch.FolderSnapshot;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link FolderSnapshot}.
*
* @author Phillip Webb
*/
public class FolderSnapshotTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File folder;
private FolderSnapshot initialSnapshot;
@Before
public void setup() throws Exception {
this.folder = createTestFolderStructure();
this.initialSnapshot = new FolderSnapshot(this.folder);
}
@Test
public void folderMustNotBeNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be null");
new FolderSnapshot(null);
}
@Test
public void folderMustNotBeFile() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be a file");
new FolderSnapshot(this.temporaryFolder.newFile());
}
@Test
public void equalsWhenNothingHasChanged() throws Exception {
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, equalTo(updatedSnapshot));
assertThat(this.initialSnapshot.hashCode(), equalTo(updatedSnapshot.hashCode()));
}
@Test
public void notEqualsWhenAFileIsAdded() throws Exception {
new File(new File(this.folder, "folder1"), "newfile").createNewFile();
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, not(equalTo(updatedSnapshot)));
}
@Test
public void notEqualsWhenAFileIsDeleted() throws Exception {
new File(new File(this.folder, "folder1"), "file1").delete();
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, not(equalTo(updatedSnapshot)));
}
@Test
public void notEqualsWhenAFileIsModified() throws Exception {
File file1 = new File(new File(this.folder, "folder1"), "file1");
FileCopyUtils.copy("updatedcontent".getBytes(), file1);
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, not(equalTo(updatedSnapshot)));
}
@Test
public void getChangedFilesSnapshotMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Snapshot must not be null");
this.initialSnapshot.getChangedFiles(null);
}
@Test
public void getChangedFilesSnapshotMustBeTheSameSourceFolder() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Snapshot source folder must be '" + this.folder + "'");
this.initialSnapshot.getChangedFiles(new FolderSnapshot(
createTestFolderStructure()));
}
@Test
public void getChangedFilesWhenNothingHasChanged() throws Exception {
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
this.initialSnapshot.getChangedFiles(updatedSnapshot);
}
@Test
public void getChangedFilesWhenAFileIsAddedAndDeletedAndChanged() throws Exception {
File folder1 = new File(this.folder, "folder1");
File file1 = new File(folder1, "file1");
File file2 = new File(folder1, "file2");
File newFile = new File(folder1, "newfile");
FileCopyUtils.copy("updatedcontent".getBytes(), file1);
file2.delete();
newFile.createNewFile();
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
ChangedFiles changedFiles = this.initialSnapshot.getChangedFiles(updatedSnapshot);
assertThat(changedFiles.getSourceFolder(), equalTo(this.folder));
assertThat(getChangedFile(changedFiles, file1).getType(), equalTo(Type.MODIFY));
assertThat(getChangedFile(changedFiles, file2).getType(), equalTo(Type.DELETE));
assertThat(getChangedFile(changedFiles, newFile).getType(), equalTo(Type.ADD));
}
private ChangedFile getChangedFile(ChangedFiles changedFiles, File file) {
for (ChangedFile changedFile : changedFiles) {
if (changedFile.getFile().equals(file)) {
return changedFile;
}
}
return null;
}
private File createTestFolderStructure() throws IOException {
File root = this.temporaryFolder.newFolder();
File folder1 = new File(root, "folder1");
folder1.mkdirs();
FileCopyUtils.copy("abc".getBytes(), new File(folder1, "file1"));
FileCopyUtils.copy("abc".getBytes(), new File(folder1, "file2"));
return root;
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2012-2015 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.integrationtest;
import java.util.Collection;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.devtools.remote.server.AccessManager;
import org.springframework.boot.devtools.remote.server.Dispatcher;
import org.springframework.boot.devtools.remote.server.DispatcherFilter;
import org.springframework.boot.devtools.remote.server.HandlerMapper;
import org.springframework.boot.devtools.remote.server.UrlHandlerMapper;
import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection;
import org.springframework.boot.devtools.tunnel.client.TunnelClient;
import org.springframework.boot.devtools.tunnel.client.TunnelConnection;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServerHandler;
import org.springframework.boot.devtools.tunnel.server.PortProvider;
import org.springframework.boot.devtools.tunnel.server.SocketTargetServerConnection;
import org.springframework.boot.devtools.tunnel.server.StaticPortProvider;
import org.springframework.boot.devtools.tunnel.server.TargetServerConnection;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.SocketUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.junit.Assert.assertEquals;
/**
* Simple integration tests for HTTP tunneling.
*
* @author Phillip Webb
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = HttpTunnelIntegrationTest.Config.class)
@WebIntegrationTest
public class HttpTunnelIntegrationTest {
@Autowired
private Config config;
@Test
public void httpServerDirect() throws Exception {
String url = "http://localhost:" + this.config.httpServerPort + "/hello";
ResponseEntity<String> entity = new TestRestTemplate().getForEntity(url,
String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertEquals("Hello World", entity.getBody());
}
@Test
public void viaTunnel() throws Exception {
String url = "http://localhost:" + this.config.clientPort + "/hello";
ResponseEntity<String> entity = new TestRestTemplate().getForEntity(url,
String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertEquals("Hello World", entity.getBody());
}
@Configuration
@EnableWebMvc
static class Config {
private int clientPort = SocketUtils.findAvailableTcpPort();
private int httpServerPort = SocketUtils.findAvailableTcpPort();
@Bean
public EmbeddedServletContainerFactory container() {
return new TomcatEmbeddedServletContainerFactory(this.httpServerPort);
}
@Bean
public DispatcherFilter filter() {
PortProvider port = new StaticPortProvider(this.httpServerPort);
TargetServerConnection connection = new SocketTargetServerConnection(port);
HttpTunnelServer server = new HttpTunnelServer(connection);
HandlerMapper mapper = new UrlHandlerMapper("/httptunnel",
new HttpTunnelServerHandler(server));
Collection<HandlerMapper> mappers = Collections.singleton(mapper);
Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers);
return new DispatcherFilter(dispatcher);
}
@Bean
public TunnelClient tunnelClient() {
String url = "http://localhost:" + this.httpServerPort + "/httptunnel";
TunnelConnection connection = new HttpTunnelConnection(url,
new SimpleClientHttpRequestFactory());
return new TunnelClient(this.clientPort, connection);
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public MyController myController() {
return new MyController();
}
}
@RestController
static class MyController {
@RequestMapping("/hello")
public String hello() {
return "Hello World";
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2015 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 org.junit.Test;
import org.springframework.boot.devtools.livereload.Base64Encoder;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link Base64Encoder}.
*
* @author Phillip Webb
*/
public class Base64EncoderTests {
private static final String TEXT = "Man is distinguished, not only by his reason, "
+ "but by this singular passion from other animals, which is a lust of the "
+ "mind, that by a perseverance of delight in the continued and indefatigable "
+ "generation of knowledge, exceeds the short vehemence of any carnal pleasure.";
private static final String ENCODED = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5I"
+ "GhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbm"
+ "ltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmF"
+ "uY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVy"
+ "YXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55I"
+ "GNhcm5hbCBwbGVhc3VyZS4=";
@Test
public void encodeText() {
assertThat(Base64Encoder.encode(TEXT), equalTo(ENCODED));
assertThat(Base64Encoder.encode("pleasure."), equalTo("cGxlYXN1cmUu"));
assertThat(Base64Encoder.encode("leasure."), equalTo("bGVhc3VyZS4="));
assertThat(Base64Encoder.encode("easure."), equalTo("ZWFzdXJlLg=="));
assertThat(Base64Encoder.encode("asure."), equalTo("YXN1cmUu"));
assertThat(Base64Encoder.encode("sure."), equalTo("c3VyZS4="));
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2012-2015 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.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.livereload.ConnectionInputStream;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ConnectionInputStream}.
*
* @author Phillip Webb
*/
@SuppressWarnings("resource")
public class ConnectionInputStreamTests {
private static final byte[] NO_BYTES = {};
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void readHeader() throws Exception {
String header = "";
for (int i = 0; i < 100; i++) {
header += "x-something-" + i
+ ": xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
}
String data = header + "\r\n\r\n" + "content\r\n";
ConnectionInputStream inputStream = new ConnectionInputStream(
new ByteArrayInputStream(data.getBytes()));
assertThat(inputStream.readHeader(), equalTo(header));
}
@Test
public void readFully() throws Exception {
byte[] bytes = "the data that we want to read fully".getBytes();
LimitedInputStream source = new LimitedInputStream(
new ByteArrayInputStream(bytes), 2);
ConnectionInputStream inputStream = new ConnectionInputStream(source);
byte[] buffer = new byte[bytes.length];
inputStream.readFully(buffer, 0, buffer.length);
assertThat(buffer, equalTo(bytes));
}
@Test
public void checkedRead() throws Exception {
ConnectionInputStream inputStream = new ConnectionInputStream(
new ByteArrayInputStream(NO_BYTES));
this.thrown.expect(IOException.class);
this.thrown.expectMessage("End of stream");
inputStream.checkedRead();
}
@Test
public void checkedReadArray() throws Exception {
ConnectionInputStream inputStream = new ConnectionInputStream(
new ByteArrayInputStream(NO_BYTES));
this.thrown.expect(IOException.class);
this.thrown.expectMessage("End of stream");
byte[] buffer = new byte[100];
inputStream.checkedRead(buffer, 0, buffer.length);
}
private static class LimitedInputStream extends FilterInputStream {
private final int max;
protected LimitedInputStream(InputStream in, int max) {
super(in);
this.max = max;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return super.read(b, off, Math.min(len, this.max));
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2015 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import org.junit.Test;
import org.springframework.boot.devtools.livereload.ConnectionOutputStream;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link ConnectionOutputStream}.
*
* @author Phillip Webb
*/
@SuppressWarnings("resource")
public class ConnectionOutputStreamTests {
@Test
public void write() throws Exception {
OutputStream out = mock(OutputStream.class);
ConnectionOutputStream outputStream = new ConnectionOutputStream(out);
byte[] b = new byte[100];
outputStream.write(b, 1, 2);
verify(out).write(b, 1, 2);
}
@Test
public void writeHttp() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ConnectionOutputStream outputStream = new ConnectionOutputStream(out);
outputStream.writeHttp(new ByteArrayInputStream("hi".getBytes()), "x-type");
String expected = "";
expected += "HTTP/1.1 200 OK\r\n";
expected += "Content-Type: x-type\r\n";
expected += "Content-Length: 2\r\n";
expected += "Connection: close\r\n\r\n";
expected += "hi";
assertThat(out.toString(), equalTo(expected));
}
@Test
public void writeHeaders() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ConnectionOutputStream outputStream = new ConnectionOutputStream(out);
outputStream.writeHeaders("A: a", "B: b");
outputStream.flush();
String expected = "";
expected += "A: a\r\n";
expected += "B: b\r\n\r\n";
assertThat(out.toString(), equalTo(expected));
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2012-2015 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.livereload.ConnectionInputStream;
import org.springframework.boot.devtools.livereload.Frame;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link Frame}.
*
* @author Phillip Webb
*/
public class FrameTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void payloadMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Payload must not be null");
new Frame((String) null);
}
@Test
public void typeMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Type must not be null");
new Frame((Frame.Type) null);
}
@Test
public void textPayload() throws Exception {
Frame frame = new Frame("abc");
assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
assertThat(frame.getPayload(), equalTo("abc".getBytes()));
}
@Test
public void typedPayload() throws Exception {
Frame frame = new Frame(Frame.Type.CLOSE);
assertThat(frame.getType(), equalTo(Frame.Type.CLOSE));
assertThat(frame.getPayload(), equalTo(new byte[] {}));
}
@Test
public void writeSmallPayload() throws Exception {
String payload = createString(1);
Frame frame = new Frame(payload);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
frame.write(bos);
assertThat(bos.toByteArray(), equalTo(new byte[] { (byte) 0x81, 0x01, 0x41 }));
}
@Test
public void writeLargePayload() throws Exception {
String payload = createString(126);
Frame frame = new Frame(payload);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
frame.write(bos);
byte[] bytes = bos.toByteArray();
assertThat(bytes.length, equalTo(130));
assertThat(bytes[0], equalTo((byte) 0x81));
assertThat(bytes[1], equalTo((byte) 0x7E));
assertThat(bytes[2], equalTo((byte) 0x00));
assertThat(bytes[3], equalTo((byte) 126));
assertThat(bytes[4], equalTo((byte) 0x41));
assertThat(bytes[5], equalTo((byte) 0x41));
}
@Test
public void readFragmentedNotSupported() throws Exception {
byte[] bytes = new byte[] { 0x0F };
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Fragmented frames are not supported");
Frame.read(newConnectionInputStream(bytes));
}
@Test
public void readLargeFramesNotSupported() throws Exception {
byte[] bytes = new byte[] { (byte) 0x80, (byte) 0xFF };
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Large frames are not supported");
Frame.read(newConnectionInputStream(bytes));
}
@Test
public void readSmallTextFrame() throws Exception {
byte[] bytes = new byte[] { (byte) 0x81, (byte) 0x02, 0x41, 0x41 };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
assertThat(frame.getPayload(), equalTo(new byte[] { 0x41, 0x41 }));
}
@Test
public void readMaskedTextFrame() throws Exception {
byte[] bytes = new byte[] { (byte) 0x81, (byte) 0x82, 0x0F, 0x0F, 0x0F, 0x0F,
0x4E, 0x4E };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
assertThat(frame.getPayload(), equalTo(new byte[] { 0x41, 0x41 }));
}
@Test
public void readLargeTextFrame() throws Exception {
byte[] bytes = new byte[134];
Arrays.fill(bytes, (byte) 0x4E);
bytes[0] = (byte) 0x81;
bytes[1] = (byte) 0xFE;
bytes[2] = 0x00;
bytes[3] = 126;
bytes[4] = 0x0F;
bytes[5] = 0x0F;
bytes[6] = 0x0F;
bytes[7] = 0x0F;
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
assertThat(frame.getPayload(), equalTo(createString(126).getBytes()));
}
@Test
public void readContinuation() throws Exception {
byte[] bytes = new byte[] { (byte) 0x80, (byte) 0x00 };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.CONTINUATION));
}
@Test
public void readBinary() throws Exception {
byte[] bytes = new byte[] { (byte) 0x82, (byte) 0x00 };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.BINARY));
}
@Test
public void readClose() throws Exception {
byte[] bytes = new byte[] { (byte) 0x88, (byte) 0x00 };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.CLOSE));
}
@Test
public void readPing() throws Exception {
byte[] bytes = new byte[] { (byte) 0x89, (byte) 0x00 };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.PING));
}
@Test
public void readPong() throws Exception {
byte[] bytes = new byte[] { (byte) 0x8A, (byte) 0x00 };
Frame frame = Frame.read(newConnectionInputStream(bytes));
assertThat(frame.getType(), equalTo(Frame.Type.PONG));
}
private ConnectionInputStream newConnectionInputStream(byte[] bytes) {
return new ConnectionInputStream(new ByteArrayInputStream(bytes));
}
private String createString(int length) {
char[] chars = new char[length];
Arrays.fill(chars, 'A');
return new String(chars);
}
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright 2012-2015 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.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.events.JettyListenerEventDriver;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.devtools.livereload.Connection;
import org.springframework.boot.devtools.livereload.ConnectionClosedException;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import org.springframework.util.SocketUtils;
import org.springframework.web.client.RestTemplate;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link LiveReloadServer}.
*
* @author Phillip Webb
*/
public class LiveReloadServerTests {
private static final String HANDSHAKE = "{command: 'hello', "
+ "protocols: ['http://livereload.com/protocols/official-7']}";
private static final ByteBuffer NO_DATA = ByteBuffer.allocate(0);
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
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, containsString("livereload.com/protocols/official-7"));
}
@Test
public void triggerReload() throws Exception {
WebSocketClient client = new WebSocketClient();
try {
Socket socket = openSocket(client, new Socket());
this.server.triggerReload();
Thread.sleep(500);
this.server.stop();
assertThat(socket.getMessages(0),
containsString("http://livereload.com/protocols/official-7"));
assertThat(socket.getMessages(1), containsString("command\":\"reload\""));
}
finally {
client.stop();
}
}
@Test
public void pingPong() throws Exception {
WebSocketClient client = new WebSocketClient();
try {
Socket socket = new Socket();
Driver driver = openSocket(client, new Driver(socket));
socket.getRemote().sendPing(NO_DATA);
Thread.sleep(200);
this.server.stop();
assertThat(driver.getPongCount(), equalTo(1));
}
finally {
client.stop();
}
}
@Test
public void clientClose() throws Exception {
WebSocketClient client = new WebSocketClient();
try {
Socket socket = openSocket(client, new Socket());
socket.getSession().close();
}
finally {
client.stop();
}
assertThat(this.server.getClosedExceptions().size(), greaterThan(0));
}
@Test
public void serverClose() throws Exception {
WebSocketClient client = new WebSocketClient();
try {
Socket socket = openSocket(client, new Socket());
Thread.sleep(200);
this.server.stop();
Thread.sleep(200);
assertThat(socket.getCloseStatus(), equalTo(1006));
}
finally {
client.stop();
}
}
private <T> T openSocket(WebSocketClient client, T socket) throws Exception,
URISyntaxException, InterruptedException, ExecutionException, IOException {
client.start();
ClientUpgradeRequest request = new ClientUpgradeRequest();
URI uri = new URI("ws://localhost:" + this.port + "/livereload");
Session session = client.connect(socket, uri, request).get();
session.getRemote().sendString(HANDSHAKE);
Thread.sleep(200);
return socket;
}
private static class Driver extends JettyListenerEventDriver {
private int pongCount;
public Driver(WebSocketListener listener) {
super(WebSocketPolicy.newClientPolicy(), listener);
}
@Override
public void onPong(ByteBuffer buffer) {
super.onPong(buffer);
this.pongCount++;
}
public int getPongCount() {
return this.pongCount;
}
}
private static class Socket extends WebSocketAdapter {
private List<String> messages = new ArrayList<String>();
private Integer closeStatus;
@Override
public void onWebSocketText(String message) {
this.messages.add(message);
}
public String getMessages(int index) {
return this.messages.get(index);
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
this.closeStatus = statusCode;
}
public Integer getCloseStatus() {
return this.closeStatus;
}
}
/**
* Useful main method for manual testing against a real browser.
* @param args main args
* @throws IOException
*/
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 List<ConnectionClosedException> closedExceptions = new ArrayList<ConnectionClosedException>();
public 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() {
return this.closedExceptions;
}
private class MonitoredConnection extends Connection {
public 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) {
MonitoredLiveReloadServer.this.closedExceptions.add(ex);
throw ex;
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2012-2015 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.remote.client;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import org.springframework.boot.devtools.remote.client.ClassPathChangeUploader;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder;
import org.springframework.boot.devtools.test.MockClientHttpRequestFactory;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ClassPathChangeUploader}.
*
* @author Phillip Webb
*/
public class ClassPathChangeUploaderTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
private MockClientHttpRequestFactory requestFactory;
private ClassPathChangeUploader uploader;
@Before
public void setup() {
this.requestFactory = new MockClientHttpRequestFactory();
this.uploader = new ClassPathChangeUploader("http://localhost/upload",
this.requestFactory);
}
@Test
public void urlMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new ClassPathChangeUploader(null, this.requestFactory);
}
@Test
public void urlMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new ClassPathChangeUploader("", this.requestFactory);
}
@Test
public void requestFactoryMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RequestFactory must not be null");
new ClassPathChangeUploader("http://localhost:8080", null);
}
@Test
public void urlMustNotBeMalformed() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Malformed URL 'htttttp:///ttest'");
new ClassPathChangeUploader("htttttp:///ttest", this.requestFactory);
}
@Test
public void sendsClassLoaderFiles() throws Exception {
File sourceFolder = this.temp.newFolder();
Set<ChangedFile> files = new LinkedHashSet<ChangedFile>();
File file1 = createFile(sourceFolder, "File1");
File file2 = createFile(sourceFolder, "File2");
File file3 = createFile(sourceFolder, "File3");
files.add(new ChangedFile(sourceFolder, file1, Type.ADD));
files.add(new ChangedFile(sourceFolder, file2, Type.MODIFY));
files.add(new ChangedFile(sourceFolder, file3, Type.DELETE));
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
changeSet.add(new ChangedFiles(sourceFolder, files));
ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false);
this.requestFactory.willRespond(HttpStatus.OK);
this.uploader.onApplicationEvent(event);
MockClientHttpRequest request = this.requestFactory.getExecutedRequests().get(0);
ClassLoaderFiles classLoaderFiles = deserialize(request.getBodyAsBytes());
Collection<SourceFolder> sourceFolders = classLoaderFiles.getSourceFolders();
assertThat(sourceFolders.size(), equalTo(1));
SourceFolder classSourceFolder = sourceFolders.iterator().next();
assertThat(classSourceFolder.getName(), equalTo(sourceFolder.getAbsolutePath()));
Iterator<ClassLoaderFile> classFiles = classSourceFolder.getFiles().iterator();
assertClassFile(classFiles.next(), "File1", ClassLoaderFile.Kind.ADDED);
assertClassFile(classFiles.next(), "File2", ClassLoaderFile.Kind.MODIFIED);
assertClassFile(classFiles.next(), null, ClassLoaderFile.Kind.DELETED);
assertThat(classFiles.hasNext(), equalTo(false));
}
private void assertClassFile(ClassLoaderFile file, String content, Kind kind) {
assertThat(file.getContents(),
equalTo(content == null ? null : content.getBytes()));
assertThat(file.getKind(), equalTo(kind));
}
private File createFile(File sourceFolder, String name) throws IOException {
File file = new File(sourceFolder, name);
FileCopyUtils.copy(name.getBytes(), file);
return file;
}
private ClassLoaderFiles deserialize(byte[] bytes) throws IOException,
ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(
new ByteArrayInputStream(bytes));
return (ClassLoaderFiles) objectInputStream.readObject();
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2012-2015 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.remote.client;
import java.io.IOException;
import java.net.URI;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.devtools.remote.client.DelayedLiveReloadTrigger;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link DelayedLiveReloadTrigger}.
*
* @author Phillip Webb
*/
public class DelayedLiveReloadTriggerTests {
private static final String URL = "http://localhost:8080";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private OptionalLiveReloadServer liveReloadServer;
@Mock
private ClientHttpRequestFactory requestFactory;
@Mock
private ClientHttpRequest errorRequest;
@Mock
private ClientHttpRequest okRequest;
@Mock
private ClientHttpResponse errorResponse;
@Mock
private ClientHttpResponse okResponse;
private DelayedLiveReloadTrigger trigger;
@Before
public void setup() throws IOException {
MockitoAnnotations.initMocks(this);
given(this.errorRequest.execute()).willReturn(this.errorResponse);
given(this.okRequest.execute()).willReturn(this.okResponse);
given(this.errorResponse.getStatusCode()).willReturn(
HttpStatus.INTERNAL_SERVER_ERROR);
given(this.okResponse.getStatusCode()).willReturn(HttpStatus.OK);
this.trigger = new DelayedLiveReloadTrigger(this.liveReloadServer,
this.requestFactory, URL);
}
@Test
public void liveReloadServerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("LiveReloadServer must not be null");
new DelayedLiveReloadTrigger(null, this.requestFactory, URL);
}
@Test
public void requestFactoryMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RequestFactory must not be null");
new DelayedLiveReloadTrigger(this.liveReloadServer, null, URL);
}
@Test
public void urlMostNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new DelayedLiveReloadTrigger(this.liveReloadServer, this.requestFactory, null);
}
@Test
public void urlMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new DelayedLiveReloadTrigger(this.liveReloadServer, this.requestFactory, "");
}
@Test
public void triggerReloadOnStatus() throws Exception {
given(this.requestFactory.createRequest(new URI(URL), HttpMethod.GET)).willThrow(
new IOException()).willReturn(this.errorRequest, this.okRequest);
long startTime = System.currentTimeMillis();
this.trigger.setTimings(10, 200, 30000);
this.trigger.run();
assertThat(System.currentTimeMillis() - startTime, greaterThan(300L));
verify(this.liveReloadServer).triggerReload();
}
@Test
public void timeout() throws Exception {
given(this.requestFactory.createRequest(new URI(URL), HttpMethod.GET)).willThrow(
new IOException());
this.trigger.setTimings(10, 0, 10);
this.trigger.run();
verify(this.liveReloadServer, never()).triggerReload();
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2012-2015 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.remote.client;
import java.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.boot.devtools.remote.client.HttpHeaderInterceptor;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link HttpHeaderInterceptor}.
*
* @author Rob Winch
* @since 1.3.0
*/
@RunWith(MockitoJUnitRunner.class)
public class HttpHeaderInterceptorTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private String name;
private String value;
private HttpHeaderInterceptor interceptor;
private HttpRequest request;
private byte[] body;
@Mock
private ClientHttpRequestExecution execution;
@Mock
private ClientHttpResponse response;
private MockHttpServletRequest httpRequest;
@Before
public void setup() throws IOException {
this.body = new byte[] {};
this.httpRequest = new MockHttpServletRequest();
this.request = new ServletServerHttpRequest(this.httpRequest);
this.name = "X-AUTH-TOKEN";
this.value = "secret";
given(this.execution.execute(this.request, this.body)).willReturn(this.response);
this.interceptor = new HttpHeaderInterceptor(this.name, this.value);
}
@Test
public void constructorNullHeaderName() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Name must not be empty");
new HttpHeaderInterceptor(null, this.value);
}
@Test
public void constructorEmptyHeaderName() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Name must not be empty");
new HttpHeaderInterceptor("", this.value);
}
@Test
public void constructorNullHeaderValue() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Value must not be empty");
new HttpHeaderInterceptor(this.name, null);
}
@Test
public void constructorEmptyHeaderValue() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Value must not be empty");
new HttpHeaderInterceptor(this.name, "");
}
@Test
public void intercept() throws IOException {
ClientHttpResponse result = this.interceptor.intercept(this.request, this.body,
this.execution);
assertThat(this.request.getHeaders().getFirst(this.name), equalTo(this.value));
assertThat(result, equalTo(this.response));
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2015 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.remote.client;
import java.net.ServerSocket;
import javax.net.ServerSocketFactory;
import org.junit.Test;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.SocketUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LocalDebugPortAvailableCondition}.
*
* @author Phillip Webb
*/
public class LocalDebugPortAvailableConditionTests {
private int port = SocketUtils.findAvailableTcpPort();
private LocalDebugPortAvailableCondition condition = new LocalDebugPortAvailableCondition();
@Test
public void portAvailable() throws Exception {
ConditionOutcome outcome = getOutcome();
assertThat(outcome.isMatch(), equalTo(true));
assertThat(outcome.getMessage(), equalTo("Local debug port availble"));
}
@Test
public void portInUse() throws Exception {
final ServerSocket serverSocket = ServerSocketFactory.getDefault()
.createServerSocket(this.port);
ConditionOutcome outcome = getOutcome();
serverSocket.close();
assertThat(outcome.isMatch(), equalTo(false));
assertThat(outcome.getMessage(), equalTo("Local debug port unavailble"));
}
private ConditionOutcome getOutcome() {
MockEnvironment environment = new MockEnvironment();
EnvironmentTestUtils.addEnvironment(environment,
"spring.devtools.remote.debug.local-port:" + this.port);
ConditionContext context = mock(ConditionContext.class);
given(context.getEnvironment()).willReturn(environment);
ConditionOutcome outcome = this.condition.getMatchOutcome(context, null);
return outcome;
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2012-2015 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.remote.client;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import org.springframework.boot.devtools.remote.client.RemoteClientConfiguration.LiveReloadConfiguration;
import org.springframework.boot.devtools.remote.server.Dispatcher;
import org.springframework.boot.devtools.remote.server.DispatcherFilter;
import org.springframework.boot.devtools.restart.MockRestarter;
import org.springframework.boot.devtools.restart.RestartScopeInitializer;
import org.springframework.boot.devtools.tunnel.client.TunnelClient;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.boot.test.OutputCapture;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.SocketUtils;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link RemoteClientConfiguration}.
*
* @author Phillip Webb
*/
public class RemoteClientConfigurationTests {
@Rule
public MockRestarter restarter = new MockRestarter();
@Rule
public OutputCapture output = new OutputCapture();
@Rule
public ExpectedException thrown = ExpectedException.none();
private AnnotationConfigEmbeddedWebApplicationContext context;
private static int remotePort = SocketUtils.findAvailableTcpPort();
@After
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void warnIfDebugAndRestartDisabled() throws Exception {
configure("spring.devtools.remote.debug.enabled:false",
"spring.devtools.remote.restart.enabled:false");
assertThat(this.output.toString(),
containsString("Remote restart and debug are both disabled"));
}
@Test
public void warnIfNotHttps() throws Exception {
configure("http://localhost", true);
assertThat(this.output.toString(), containsString("is insecure"));
}
@Test
public void doesntWarnIfUsingHttps() throws Exception {
configure("https://localhost", true);
assertThat(this.output.toString(), not(containsString("is insecure")));
}
@Test
public void failIfNoSecret() throws Exception {
this.thrown.expect(BeanCreationException.class);
this.thrown.expectMessage("required to secure your connection");
configure("http://localhost", false);
}
@Test
public void liveReloadOnClassPathChanged() throws Exception {
configure();
Set<ChangedFiles> changeSet = new HashSet<ChangedFiles>();
ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false);
this.context.publishEvent(event);
LiveReloadConfiguration configuration = this.context
.getBean(LiveReloadConfiguration.class);
configuration.getExecutor().shutdown();
configuration.getExecutor().awaitTermination(2, TimeUnit.SECONDS);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
verify(server).triggerReload();
}
@Test
public void liveReloadDisabled() throws Exception {
configure("spring.devtools.livereload.enabled:false");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(OptionalLiveReloadServer.class);
}
@Test
public void remoteRestartDisabled() throws Exception {
configure("spring.devtools.remote.restart.enabled:false");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(ClassPathFileSystemWatcher.class);
}
@Test
public void remoteDebugDisabled() throws Exception {
configure("spring.devtools.remote.debug.enabled:false");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(TunnelClient.class);
}
private void configure(String... pairs) {
configure("http://localhost", true, pairs);
}
private void configure(String remoteUrl, boolean setSecret, String... pairs) {
this.context = new AnnotationConfigEmbeddedWebApplicationContext();
new RestartScopeInitializer().initialize(this.context);
this.context.register(Config.class, RemoteClientConfiguration.class);
String remoteUrlProperty = "remoteUrl:" + remoteUrl + ":"
+ RemoteClientConfigurationTests.remotePort;
EnvironmentTestUtils.addEnvironment(this.context, remoteUrlProperty);
EnvironmentTestUtils.addEnvironment(this.context, pairs);
if (setSecret) {
EnvironmentTestUtils.addEnvironment(this.context,
"spring.devtools.remote.secret:secret");
}
this.context.refresh();
}
@Configuration
static class Config {
@Bean
public TomcatEmbeddedServletContainerFactory tomcat() {
return new TomcatEmbeddedServletContainerFactory(remotePort);
}
@Bean
public LiveReloadServer liveReloadServer() {
return mock(LiveReloadServer.class);
}
@Bean
public DispatcherFilter dispatcherFilter() throws IOException {
return new DispatcherFilter(dispatcher());
}
public Dispatcher dispatcher() throws IOException {
Dispatcher dispatcher = mock(Dispatcher.class);
ServerHttpRequest anyRequest = (ServerHttpRequest) any();
ServerHttpResponse anyResponse = (ServerHttpResponse) any();
given(dispatcher.handle(anyRequest, anyResponse)).willReturn(true);
return dispatcher;
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2015 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.remote.server;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.remote.server.Dispatcher;
import org.springframework.boot.devtools.remote.server.DispatcherFilter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link DispatcherFilter}.
*
* @author Phillip Webb
*/
public class DispatcherFilterTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private Dispatcher dispatcher;
@Mock
private FilterChain chain;
@Captor
private ArgumentCaptor<ServerHttpResponse> serverResponseCaptor;
@Captor
private ArgumentCaptor<ServerHttpRequest> serverRequestCaptor;
private DispatcherFilter filter;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
this.filter = new DispatcherFilter(this.dispatcher);
}
@Test
public void dispatcherMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Dispatcher must not be null");
new DispatcherFilter(null);
}
@Test
public void ignoresNotServletRequests() throws Exception {
ServletRequest request = mock(ServletRequest.class);
ServletResponse response = mock(ServletResponse.class);
this.filter.doFilter(request, response, this.chain);
verifyZeroInteractions(this.dispatcher);
verify(this.chain).doFilter(request, response);
}
@Test
public void ignoredByDispatcher() throws Exception {
HttpServletRequest request = new MockHttpServletRequest("GET", "/hello");
HttpServletResponse response = new MockHttpServletResponse();
this.filter.doFilter(request, response, this.chain);
verify(this.chain).doFilter(request, response);
}
@Test
public void handledByDispatcher() throws Exception {
HttpServletRequest request = new MockHttpServletRequest("GET", "/hello");
HttpServletResponse response = new MockHttpServletResponse();
willReturn(true).given(this.dispatcher).handle(any(ServerHttpRequest.class),
any(ServerHttpResponse.class));
this.filter.doFilter(request, response, this.chain);
verifyZeroInteractions(this.chain);
verify(this.dispatcher).handle(this.serverRequestCaptor.capture(),
this.serverResponseCaptor.capture());
ServerHttpRequest dispatcherRequest = this.serverRequestCaptor.getValue();
ServletServerHttpRequest actualRequest = (ServletServerHttpRequest) dispatcherRequest;
ServerHttpResponse dispatcherResponse = this.serverResponseCaptor.getValue();
ServletServerHttpResponse actualResponse = (ServletServerHttpResponse) dispatcherResponse;
assertThat(actualRequest.getServletRequest(), equalTo(request));
assertThat(actualResponse.getServletResponse(), equalTo(response));
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2015 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.remote.server;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.remote.server.AccessManager;
import org.springframework.boot.devtools.remote.server.Dispatcher;
import org.springframework.boot.devtools.remote.server.Handler;
import org.springframework.boot.devtools.remote.server.HandlerMapper;
import org.springframework.core.Ordered;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link Dispatcher}.
*
* @author Phillip Webb
*/
public class DispatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private AccessManager accessManager;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private ServerHttpRequest serverRequest;
private ServerHttpResponse serverResponse;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.serverRequest = new ServletServerHttpRequest(this.request);
this.serverResponse = new ServletServerHttpResponse(this.response);
}
@Test
public void accessManagerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("AccessManager must not be null");
new Dispatcher(null, Collections.<HandlerMapper> emptyList());
}
@Test
public void mappersMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Mappers must not be null");
new Dispatcher(this.accessManager, null);
}
@Test
public void accessManagerVetoRequest() throws Exception {
given(this.accessManager.isAllowed(any(ServerHttpRequest.class))).willReturn(
false);
HandlerMapper mapper = mock(HandlerMapper.class);
Handler handler = mock(Handler.class);
given(mapper.getHandler(any(ServerHttpRequest.class))).willReturn(handler);
Dispatcher dispatcher = new Dispatcher(this.accessManager,
Collections.singleton(mapper));
dispatcher.handle(this.serverRequest, this.serverResponse);
verifyZeroInteractions(handler);
assertThat(this.response.getStatus(), equalTo(403));
}
@Test
public void accessManagerAllowRequest() throws Exception {
given(this.accessManager.isAllowed(any(ServerHttpRequest.class)))
.willReturn(true);
HandlerMapper mapper = mock(HandlerMapper.class);
Handler handler = mock(Handler.class);
given(mapper.getHandler(any(ServerHttpRequest.class))).willReturn(handler);
Dispatcher dispatcher = new Dispatcher(this.accessManager,
Collections.singleton(mapper));
dispatcher.handle(this.serverRequest, this.serverResponse);
verify(handler).handle(this.serverRequest, this.serverResponse);
}
@Test
public void ordersMappers() throws Exception {
HandlerMapper mapper1 = mock(HandlerMapper.class,
withSettings().extraInterfaces(Ordered.class));
HandlerMapper mapper2 = mock(HandlerMapper.class,
withSettings().extraInterfaces(Ordered.class));
given(((Ordered) mapper1).getOrder()).willReturn(1);
given(((Ordered) mapper2).getOrder()).willReturn(2);
List<HandlerMapper> mappers = Arrays.asList(mapper2, mapper1);
Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers);
dispatcher.handle(this.serverRequest, this.serverResponse);
InOrder inOrder = inOrder(mapper1, mapper2);
inOrder.verify(mapper1).getHandler(this.serverRequest);
inOrder.verify(mapper2).getHandler(this.serverRequest);
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2012-2015 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.remote.server;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.remote.server.HttpHeaderAccessManager;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link HttpHeaderAccessManager}.
*
* @author Rob Winch
* @author Phillip Webb
*/
public class HttpHeaderAccessManagerTests {
private static final String HEADER = "X-AUTH_TOKEN";
private static final String SECRET = "password";
@Rule
public ExpectedException thrown = ExpectedException.none();
private MockHttpServletRequest request;
private ServerHttpRequest serverRequest;
private HttpHeaderAccessManager manager;
@Before
public void setup() {
this.request = new MockHttpServletRequest("GET", "/");
this.serverRequest = new ServletServerHttpRequest(this.request);
this.manager = new HttpHeaderAccessManager(HEADER, SECRET);
}
@Test
public void headerNameMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("HeaderName must not be empty");
new HttpHeaderAccessManager(null, SECRET);
}
@Test
public void headerNameMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("HeaderName must not be empty");
new HttpHeaderAccessManager("", SECRET);
}
@Test
public void expectedSecretMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ExpectedSecret must not be empty");
new HttpHeaderAccessManager(HEADER, null);
}
@Test
public void expectedSecretMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ExpectedSecret must not be empty");
new HttpHeaderAccessManager(HEADER, "");
}
@Test
public void allowsMatching() throws Exception {
this.request.addHeader(HEADER, SECRET);
assertThat(this.manager.isAllowed(this.serverRequest), equalTo(true));
}
@Test
public void disallowsWrongSecret() throws Exception {
this.request.addHeader(HEADER, "wrong");
assertThat(this.manager.isAllowed(this.serverRequest), equalTo(false));
}
@Test
public void disallowsNoSecret() throws Exception {
assertThat(this.manager.isAllowed(this.serverRequest), equalTo(false));
}
@Test
public void disallowsWrongHeader() throws Exception {
this.request.addHeader("X-WRONG", SECRET);
assertThat(this.manager.isAllowed(this.serverRequest), equalTo(false));
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2015 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.remote.server;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.remote.server.HttpStatusHandler;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link HttpStatusHandler}.
*
* @author Phillip Webb
*/
public class HttpStatusHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private MockHttpServletRequest servletRequest;
private MockHttpServletResponse servletResponse;
private ServerHttpResponse response;
private ServerHttpRequest request;
@Before
public void setup() {
this.servletRequest = new MockHttpServletRequest();
this.servletResponse = new MockHttpServletResponse();
this.request = new ServletServerHttpRequest(this.servletRequest);
this.response = new ServletServerHttpResponse(this.servletResponse);
}
@Test
public void statusMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Status must not be null");
new HttpStatusHandler(null);
}
@Test
public void respondsOk() throws Exception {
HttpStatusHandler handler = new HttpStatusHandler();
handler.handle(this.request, this.response);
assertThat(this.servletResponse.getStatus(), equalTo(200));
}
@Test
public void respondsWithStatus() throws Exception {
HttpStatusHandler handler = new HttpStatusHandler(HttpStatus.I_AM_A_TEAPOT);
handler.handle(this.request, this.response);
assertThat(this.servletResponse.getStatus(), equalTo(418));
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2012-2015 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.remote.server;
import javax.servlet.http.HttpServletRequest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.remote.server.Handler;
import org.springframework.boot.devtools.remote.server.UrlHandlerMapper;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link UrlHandlerMapper}.
*
* @author Rob Winch
* @author Phillip Webb
*/
public class UrlHandlerMapperTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private Handler handler = mock(Handler.class);
@Test
public void requestUriMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new UrlHandlerMapper(null, this.handler);
}
@Test
public void requestUriMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new UrlHandlerMapper("", this.handler);
}
@Test
public void requestUrlMustStartWithSlash() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must start with '/'");
new UrlHandlerMapper("tunnel", this.handler);
}
@Test
public void handlesMatchedUrl() throws Exception {
UrlHandlerMapper mapper = new UrlHandlerMapper("/tunnel", this.handler);
HttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/tunnel");
ServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
assertThat(mapper.getHandler(request), equalTo(this.handler));
}
@Test
public void ignoresDifferentUrl() throws Exception {
UrlHandlerMapper mapper = new UrlHandlerMapper("/tunnel", this.handler);
HttpServletRequest servletRequest = new MockHttpServletRequest("GET",
"/tunnel/other");
ServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
assertThat(mapper.getHandler(request), nullValue());
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2015 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.restart;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.restart.ChangeableUrls;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ChangeableUrls}.
*
* @author Phillip Webb
*/
public class ChangeableUrlsTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void folderUrl() throws Exception {
URL url = makeUrl("myproject");
assertThat(ChangeableUrls.fromUrls(url).size(), equalTo(1));
}
@Test
public void fileUrl() throws Exception {
URL url = this.temporaryFolder.newFile().toURI().toURL();
assertThat(ChangeableUrls.fromUrls(url).size(), equalTo(0));
}
@Test
public void httpUrl() throws Exception {
URL url = new URL("http://spring.io");
assertThat(ChangeableUrls.fromUrls(url).size(), equalTo(0));
}
@Test
public void skipsUrls() throws Exception {
ChangeableUrls urls = ChangeableUrls
.fromUrls(makeUrl("spring-boot"), makeUrl("spring-boot-autoconfigure"),
makeUrl("spring-boot-actuator"), makeUrl("spring-boot-starter"),
makeUrl("spring-boot-starter-some-thing"));
assertThat(urls.size(), equalTo(0));
}
private URL makeUrl(String name) throws IOException {
File file = this.temporaryFolder.newFolder();
file = new File(file, name);
file = new File(file, "target");
file = new File(file, "classes");
file.mkdirs();
return file.toURI().toURL();
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2012-2015 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.restart;
import java.net.URL;
import org.junit.Test;
import org.springframework.boot.devtools.restart.DefaultRestartInitializer;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DefaultRestartInitializer}.
*
* @author Phillip Webb
*/
public class DefaultRestartInitializerTests {
@Test
public void nullForTests() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(true);
assertThat(initializer.getInitialUrls(Thread.currentThread()), nullValue());
}
@Test
public void validMainThread() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(false);
ClassLoader classLoader = new MockAppClassLoader(getClass().getClassLoader());
Thread thread = new Thread();
thread.setName("main");
thread.setContextClassLoader(classLoader);
assertThat(initializer.isMain(thread), equalTo(true));
assertThat(initializer.getInitialUrls(thread), not(nullValue()));
}
@Test
public void threadNotNamedMain() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(false);
ClassLoader classLoader = new MockAppClassLoader(getClass().getClassLoader());
Thread thread = new Thread();
thread.setName("buscuit");
thread.setContextClassLoader(classLoader);
assertThat(initializer.isMain(thread), equalTo(false));
assertThat(initializer.getInitialUrls(thread), nullValue());
}
@Test
public void threadNotUsingAppClassLoader() throws Exception {
MockRestartInitializer initializer = new MockRestartInitializer(false);
ClassLoader classLoader = new MockLauncherClassLoader(getClass().getClassLoader());
Thread thread = new Thread();
thread.setName("main");
thread.setContextClassLoader(classLoader);
assertThat(initializer.isMain(thread), equalTo(false));
assertThat(initializer.getInitialUrls(thread), nullValue());
}
@Test
public void skipsDueToJUnitStacks() throws Exception {
testSkipStack("org.junit.runners.Something", true);
}
@Test
public void skipsDueToSpringTest() throws Exception {
testSkipStack("org.springframework.boot.test.Something", true);
}
private void testSkipStack(String className, boolean expected) {
MockRestartInitializer initializer = new MockRestartInitializer(true);
StackTraceElement element = new StackTraceElement(className, "someMethod",
"someFile", 123);
assertThat(initializer.isSkippedStackElement(element), equalTo(expected));
}
private static class MockAppClassLoader extends ClassLoader {
public MockAppClassLoader(ClassLoader parent) {
super(parent);
}
}
private static class MockLauncherClassLoader extends ClassLoader {
public MockLauncherClassLoader(ClassLoader parent) {
super(parent);
}
}
private static class MockRestartInitializer extends DefaultRestartInitializer {
private final boolean considerStackElements;
public MockRestartInitializer(boolean considerStackElements) {
this.considerStackElements = considerStackElements;
}
@Override
protected boolean isSkippedStackElement(StackTraceElement element) {
if (!this.considerStackElements) {
return false;
}
return true;
}
@Override
protected URL[] getUrls(Thread thread) {
return new URL[0];
}
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2012-2015 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.restart;
import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.restart.MainMethod;
import org.springframework.util.ReflectionUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link MainMethod}.
*
* @author Phillip Webb
*/
public class MainMethodTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private static ThreadLocal<MainMethod> mainMethod = new ThreadLocal<MainMethod>();
private Method actualMain;
@Before
public void setup() throws Exception {
this.actualMain = Valid.class.getMethod("main", String[].class);
}
@Test
public void threadMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Thread must not be null");
new MainMethod(null);
}
@Test
public void validMainMethod() throws Exception {
MainMethod method = new TestThread(new Runnable() {
@Override
public void run() {
Valid.main();
}
}).test();
assertThat(method.getMethod(), equalTo(this.actualMain));
assertThat(method.getDeclaringClassName(), equalTo(this.actualMain
.getDeclaringClass().getName()));
}
@Test
public void missingArgsMainMethod() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Unable to find main method");
new TestThread(new Runnable() {
@Override
public void run() {
MissingArgs.main();
}
}).test();
}
@Test
public void nonStatic() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Unable to find main method");
new TestThread(new Runnable() {
@Override
public void run() {
new NonStaticMain().main();
}
}).test();
}
private static class TestThread extends Thread {
private final Runnable runnable;
private Exception exception;
private MainMethod mainMethod;
public TestThread(Runnable runnable) {
this.runnable = runnable;
}
public MainMethod test() throws InterruptedException {
start();
join();
if (this.exception != null) {
ReflectionUtils.rethrowRuntimeException(this.exception);
}
return this.mainMethod;
}
@Override
public void run() {
try {
this.runnable.run();
this.mainMethod = MainMethodTests.mainMethod.get();
}
catch (Exception ex) {
this.exception = ex;
}
}
}
public static class Valid {
public static void main(String... args) {
someOtherMethod();
}
private static void someOtherMethod() {
mainMethod.set(new MainMethod());
}
}
public static class MissingArgs {
public static void main() {
mainMethod.set(new MainMethod());
}
}
private static class NonStaticMain {
public void main(String... args) {
mainMethod.set(new MainMethod());
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2015 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.restart;
import java.net.URL;
import org.springframework.boot.devtools.restart.RestartInitializer;
/**
* Simple mock {@link RestartInitializer} that returns an empty array of URLs.
*
* @author Phillip Webb
*/
public class MockRestartInitializer implements RestartInitializer {
@Override
public URL[] getInitialUrls(Thread thread) {
return new URL[] {};
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2015 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.restart;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.devtools.restart.Restarter;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
/**
* Mocked version of {@link Restarter}.
*
* @author Phillip Webb
*/
public class MockRestarter implements TestRule {
private Map<String, Object> attributes = new HashMap<String, Object>();
private Restarter mock = mock(Restarter.class);
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
setup();
base.evaluate();
cleanup();
}
};
}
@SuppressWarnings("rawtypes")
private void setup() {
Restarter.setInstance(this.mock);
given(this.mock.getInitialUrls()).willReturn(new URL[] {});
given(this.mock.getOrAddAttribute(anyString(), (ObjectFactory) any()))
.willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
String name = (String) invocation.getArguments()[0];
ObjectFactory factory = (ObjectFactory) invocation.getArguments()[1];
Object attribute = MockRestarter.this.attributes.get(name);
if (attribute == null) {
attribute = factory.getObject();
MockRestarter.this.attributes.put(name, attribute);
}
return attribute;
}
});
given(this.mock.getThreadFactory()).willReturn(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
});
}
private void cleanup() {
this.attributes.clear();
Restarter.clearInstance();
}
public Restarter getMock() {
return this.mock;
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2012-2015 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.restart;
import java.net.URL;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.devtools.restart.ConditionalOnInitializedRestarter;
import org.springframework.boot.devtools.restart.OnInitializedRestarterCondition;
import org.springframework.boot.devtools.restart.RestartInitializer;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OnInitializedRestarterCondition}.
*
* @author Phillip Webb
*/
public class OnInitializedRestarterConditionTests {
private static Object wait = new Object();
@Before
@After
public void cleanup() {
Restarter.clearInstance();
}
@Test
public void noInstance() throws Exception {
Restarter.clearInstance();
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
Config.class);
assertThat(context.containsBean("bean"), equalTo(false));
context.close();
}
@Test
public void noInitialization() throws Exception {
Restarter.initialize(new String[0], false, RestartInitializer.NONE);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
Config.class);
assertThat(context.containsBean("bean"), equalTo(false));
context.close();
}
@Test
public void initialized() throws Exception {
Thread thread = new Thread() {
@Override
public void run() {
TestInitialized.main();
};
};
thread.start();
synchronized (wait) {
wait.wait();
}
}
public static class TestInitialized {
public static void main(String... args) {
RestartInitializer initializer = mock(RestartInitializer.class);
given(initializer.getInitialUrls((Thread) any())).willReturn(new URL[0]);
Restarter.initialize(new String[0], false, initializer);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
Config.class);
assertThat(context.containsBean("bean"), equalTo(true));
context.close();
synchronized (wait) {
wait.notify();
}
}
}
@Configuration
public static class Config {
@Bean
@ConditionalOnInitializedRestarter
public String bean() {
return "bean";
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2012-2015 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.restart;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.devtools.restart.RestartApplicationListener;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.test.util.ReflectionTestUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RestartApplicationListener}.
*
* @author Phillip Webb
*/
public class RestartApplicationListenerTests {
@Before
@After
public void cleanup() {
Restarter.clearInstance();
}
@Test
public void isHighestPriority() throws Exception {
assertThat(new RestartApplicationListener().getOrder(),
equalTo(Ordered.HIGHEST_PRECEDENCE));
}
@Test
public void initializeWithReady() throws Exception {
testInitialize(false);
}
@Test
public void initializeWithFail() throws Exception {
testInitialize(true);
}
private void testInitialize(boolean failed) {
Restarter.clearInstance();
RestartApplicationListener listener = new RestartApplicationListener();
SpringApplication application = new SpringApplication();
ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
String[] args = new String[] { "a", "b", "c" };
listener.onApplicationEvent(new ApplicationStartedEvent(application, args));
assertThat(Restarter.getInstance(), not(nullValue()));
assertThat(Restarter.getInstance().isFinished(), equalTo(false));
assertThat(ReflectionTestUtils.getField(Restarter.getInstance(), "args"),
equalTo((Object) args));
if (failed) {
listener.onApplicationEvent(new ApplicationFailedEvent(application, args,
context, new RuntimeException()));
}
else {
listener.onApplicationEvent(new ApplicationReadyEvent(application, args,
context));
}
assertThat(Restarter.getInstance().isFinished(), equalTo(true));
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2015 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.restart;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.devtools.restart.RestartScopeInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RestartScopeInitializer}.
*
* @author Phillip Webb
*/
public class RestartScopeInitializerTests {
private static AtomicInteger createCount;
private static AtomicInteger refreshCount;
@Test
public void restartScope() throws Exception {
createCount = new AtomicInteger();
refreshCount = new AtomicInteger();
ConfigurableApplicationContext context = runApplication();
context.close();
context = runApplication();
context.close();
assertThat(createCount.get(), equalTo(1));
assertThat(refreshCount.get(), equalTo(2));
}
private ConfigurableApplicationContext runApplication() {
SpringApplication application = new SpringApplication(Config.class);
application.setWebEnvironment(false);
return application.run();
}
@Configuration
public static class Config {
@Bean
@RestartScope
public ScopeTestBean scopeTestBean() {
return new ScopeTestBean();
}
}
public static class ScopeTestBean implements
ApplicationListener<ContextRefreshedEvent> {
public ScopeTestBean() {
createCount.incrementAndGet();
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
refreshCount.incrementAndGet();
}
}
}

View File

@@ -0,0 +1,278 @@
/*
* Copyright 2012-2015 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.restart;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ThreadFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.devtools.restart.RestartInitializer;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.test.OutputCapture;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link Restarter}.
*
* @author Phillip Webb
*/
public class RestarterTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public OutputCapture out = new OutputCapture();
@Before
public void setup() {
Restarter.setInstance(new TestableRestarter());
}
@After
public void cleanup() {
Restarter.clearInstance();
}
@Test
public void cantGetInstanceBeforeInitialize() throws Exception {
Restarter.clearInstance();
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Restarter has not been initialized");
Restarter.getInstance();
}
@Test
public void testRestart() throws Exception {
Restarter.clearInstance();
Thread thread = new Thread() {
@Override
public void run() {
SampleApplication.main();
};
};
thread.start();
Thread.sleep(2600);
String output = this.out.toString();
assertThat(StringUtils.countOccurrencesOf(output, "Tick 0"), greaterThan(1));
assertThat(StringUtils.countOccurrencesOf(output, "Tick 1"), greaterThan(1));
}
@Test
@SuppressWarnings("rawtypes")
public void getOrAddAttributeWithNewAttribute() throws Exception {
ObjectFactory objectFactory = mock(ObjectFactory.class);
given(objectFactory.getObject()).willReturn("abc");
Object attribute = Restarter.getInstance().getOrAddAttribute("x", objectFactory);
assertThat(attribute, equalTo((Object) "abc"));
}
public void addUrlsMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Urls must not be null");
Restarter.getInstance().addUrls(null);
}
@Test
public void addUrls() throws Exception {
URL url = new URL("file:/proj/module-a.jar!/");
Collection<URL> urls = Collections.singleton(url);
Restarter restarter = Restarter.getInstance();
restarter.addUrls(urls);
restarter.restart();
ClassLoader classLoader = ((TestableRestarter) restarter)
.getRelaunchClassLoader();
assertThat(((URLClassLoader) classLoader).getURLs()[0], equalTo(url));
}
@Test
public void addClassLoaderFilesMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ClassLoaderFiles must not be null");
Restarter.getInstance().addClassLoaderFiles(null);
}
@Test
public void addClassLoaderFiles() throws Exception {
ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles();
classLoaderFiles.addFile("f", new ClassLoaderFile(Kind.ADDED, "abc".getBytes()));
Restarter restarter = Restarter.getInstance();
restarter.addClassLoaderFiles(classLoaderFiles);
restarter.restart();
ClassLoader classLoader = ((TestableRestarter) restarter)
.getRelaunchClassLoader();
assertThat(FileCopyUtils.copyToByteArray(classLoader.getResourceAsStream("f")),
equalTo("abc".getBytes()));
}
@Test
@SuppressWarnings("rawtypes")
public void getOrAddAttributeWithExistingAttribute() throws Exception {
Restarter.getInstance().getOrAddAttribute("x", new ObjectFactory<String>() {
@Override
public String getObject() throws BeansException {
return "abc";
}
});
ObjectFactory objectFactory = mock(ObjectFactory.class);
Object attribute = Restarter.getInstance().getOrAddAttribute("x", objectFactory);
assertThat(attribute, equalTo((Object) "abc"));
verifyZeroInteractions(objectFactory);
}
@Test
public void getThreadFactory() throws Exception {
final ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader contextClassLoader = new URLClassLoader(new URL[0]);
Thread thread = new Thread() {
@Override
public void run() {
Runnable runnable = mock(Runnable.class);
Thread regular = new Thread();
ThreadFactory factory = Restarter.getInstance().getThreadFactory();
Thread viaFactory = factory.newThread(runnable);
// Regular threads will inherit the current thread
assertThat(regular.getContextClassLoader(), equalTo(contextClassLoader));
// Factory threads should should inherit from the initial thread
assertThat(viaFactory.getContextClassLoader(), equalTo(parentLoader));
};
};
thread.setContextClassLoader(contextClassLoader);
thread.start();
thread.join();
}
@Test
public void getInitialUrls() throws Exception {
Restarter.clearInstance();
RestartInitializer initializer = mock(RestartInitializer.class);
URL[] urls = new URL[] { new URL("file:/proj/module-a.jar!/") };
given(initializer.getInitialUrls(any(Thread.class))).willReturn(urls);
Restarter.initialize(new String[0], false, initializer, false);
assertThat(Restarter.getInstance().getInitialUrls(), equalTo(urls));
}
@Component
@EnableScheduling
public static class SampleApplication {
private int count = 0;
private static volatile boolean quit = false;
@Scheduled(fixedDelay = 200)
public void tickBean() {
System.out.println("Tick " + this.count++ + " " + Thread.currentThread());
}
@Scheduled(initialDelay = 500, fixedDelay = 500)
public void restart() {
System.out.println("Restart " + Thread.currentThread());
if (!SampleApplication.quit) {
Restarter.getInstance().restart();
}
}
public static void main(String... args) {
Restarter.initialize(args, false, new MockRestartInitializer());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SampleApplication.class);
context.registerShutdownHook();
System.out.println("Sleep " + Thread.currentThread());
sleep();
quit = true;
context.close();
}
private static void sleep() {
try {
Thread.sleep(1200);
}
catch (InterruptedException ex) {
}
}
}
private static class TestableRestarter extends Restarter {
private ClassLoader relaunchClassLoader;
public TestableRestarter() {
this(Thread.currentThread(), new String[] {}, false,
new MockRestartInitializer());
}
protected TestableRestarter(Thread thread, String[] args,
boolean forceReferenceCleanup, RestartInitializer initializer) {
super(thread, args, forceReferenceCleanup, initializer);
}
@Override
public void restart() {
try {
stop();
start();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@Override
protected void relaunch(ClassLoader classLoader) throws Exception {
this.relaunchClassLoader = classLoader;
}
@Override
protected void stop() throws Exception {
}
public ClassLoader getRelaunchClassLoader() {
return this.relaunchClassLoader;
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2012-2015 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.restart;
import org.junit.Test;
import org.springframework.boot.devtools.restart.SilentExitExceptionHandler;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
/**
* Tests for {@link SilentExitExceptionHandler}.
*
* @author Phillip Webb
*/
public class SilentExitExceptionHandlerTests {
@Test
public void setupAndExit() throws Exception {
TestThread testThread = new TestThread() {
@Override
public void run() {
SilentExitExceptionHandler.exitCurrentThread();
fail("Didn't exit");
}
};
SilentExitExceptionHandler.setup(testThread);
testThread.startAndJoin();
assertThat(testThread.getThrown(), nullValue());
}
@Test
public void doesntInterferWithOtherExceptions() throws Exception {
TestThread testThread = new TestThread() {
@Override
public void run() {
throw new IllegalStateException("Expected");
}
};
SilentExitExceptionHandler.setup(testThread);
testThread.startAndJoin();
assertThat(testThread.getThrown().getMessage(), equalTo("Expected"));
}
private static abstract class TestThread extends Thread {
private Throwable thrown;
public TestThread() {
setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
TestThread.this.thrown = e;
}
});
}
public Throwable getThrown() {
return this.thrown;
}
public void startAndJoin() throws InterruptedException {
start();
join();
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2015 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.restart.classloader;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ClassLoaderFile}.
*
* @author Phillip Webb
*/
public class ClassLoaderFileTests {
public static final byte[] BYTES = "ABC".getBytes();
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void kindMustNotBeNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Kind must not be null");
new ClassLoaderFile(null, null);
}
@Test
public void addedContentsMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Contents must not be null");
new ClassLoaderFile(Kind.ADDED, null);
}
@Test
public void modifiedContentsMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Contents must not be null");
new ClassLoaderFile(Kind.MODIFIED, null);
}
@Test
public void deletedContentsMustBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Contents must be null");
new ClassLoaderFile(Kind.DELETED, new byte[10]);
}
@Test
public void added() throws Exception {
ClassLoaderFile file = new ClassLoaderFile(Kind.ADDED, BYTES);
assertThat(file.getKind(), equalTo(ClassLoaderFile.Kind.ADDED));
assertThat(file.getContents(), equalTo(BYTES));
}
@Test
public void modified() throws Exception {
ClassLoaderFile file = new ClassLoaderFile(Kind.MODIFIED, BYTES);
assertThat(file.getKind(), equalTo(ClassLoaderFile.Kind.MODIFIED));
assertThat(file.getContents(), equalTo(BYTES));
}
@Test
public void deleted() throws Exception {
ClassLoaderFile file = new ClassLoaderFile(Kind.DELETED, null);
assertThat(file.getKind(), equalTo(ClassLoaderFile.Kind.DELETED));
assertThat(file.getContents(), nullValue());
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2012-2015 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.restart.classloader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ClassLoaderFiles}.
*
* @author Phillip Webb
*/
public class ClassLoaderFilesTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private ClassLoaderFiles files = new ClassLoaderFiles();
@Test
public void addFileNameMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Name must not be null");
this.files.addFile(null, mock(ClassLoaderFile.class));
}
@Test
public void addFileFileMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
this.files.addFile("test", null);
}
@Test
public void getFileWithNullName() throws Exception {
assertThat(this.files.getFile(null), nullValue());
}
@Test
public void addAndGet() throws Exception {
ClassLoaderFile file = new ClassLoaderFile(Kind.ADDED, new byte[10]);
this.files.addFile("myfile", file);
assertThat(this.files.getFile("myfile"), equalTo(file));
}
@Test
public void getMissing() throws Exception {
assertThat(this.files.getFile("missing"), nullValue());
}
@Test
public void addTwice() throws Exception {
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
this.files.addFile("myfile", file1);
this.files.addFile("myfile", file2);
assertThat(this.files.getFile("myfile"), equalTo(file2));
}
@Test
public void addTwiceInDifferentSourceFolders() throws Exception {
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
this.files.addFile("a", "myfile", file1);
this.files.addFile("b", "myfile", file2);
assertThat(this.files.getFile("myfile"), equalTo(file2));
assertThat(this.files.getOrCreateSourceFolder("a").getFiles().size(), equalTo(0));
assertThat(this.files.getOrCreateSourceFolder("b").getFiles().size(), equalTo(1));
}
@Test
public void getSourceFolders() throws Exception {
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
ClassLoaderFile file3 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
ClassLoaderFile file4 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
this.files.addFile("a", "myfile1", file1);
this.files.addFile("a", "myfile2", file2);
this.files.addFile("b", "myfile3", file3);
this.files.addFile("b", "myfile4", file4);
Iterator<SourceFolder> sourceFolders = this.files.getSourceFolders().iterator();
SourceFolder sourceFolder1 = sourceFolders.next();
SourceFolder sourceFolder2 = sourceFolders.next();
assertThat(sourceFolders.hasNext(), equalTo(false));
assertThat(sourceFolder1.getName(), equalTo("a"));
assertThat(sourceFolder2.getName(), equalTo("b"));
assertThat(new ArrayList<ClassLoaderFile>(sourceFolder1.getFiles()),
equalTo(Arrays.asList(file1, file2)));
assertThat(new ArrayList<ClassLoaderFile>(sourceFolder2.getFiles()),
equalTo(Arrays.asList(file3, file4)));
}
@Test
public void serialzie() throws Exception {
ClassLoaderFile file = new ClassLoaderFile(Kind.ADDED, new byte[10]);
this.files.addFile("myfile", file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this.files);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(
bos.toByteArray()));
ClassLoaderFiles readObject = (ClassLoaderFiles) ois.readObject();
assertThat(readObject.getFile("myfile"), notNullValue());
}
@Test
public void addAll() throws Exception {
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
this.files.addFile("a", "myfile1", file1);
ClassLoaderFiles toAdd = new ClassLoaderFiles();
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
ClassLoaderFile file3 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
toAdd.addFile("a", "myfile2", file2);
toAdd.addFile("b", "myfile3", file3);
this.files.addAll(toAdd);
Iterator<SourceFolder> sourceFolders = this.files.getSourceFolders().iterator();
SourceFolder sourceFolder1 = sourceFolders.next();
SourceFolder sourceFolder2 = sourceFolders.next();
assertThat(sourceFolders.hasNext(), equalTo(false));
assertThat(sourceFolder1.getName(), equalTo("a"));
assertThat(sourceFolder2.getName(), equalTo("b"));
assertThat(new ArrayList<ClassLoaderFile>(sourceFolder1.getFiles()),
equalTo(Arrays.asList(file1, file2)));
}
@Test
public void getSize() throws Exception {
this.files.addFile("s1", "n1", mock(ClassLoaderFile.class));
this.files.addFile("s1", "n2", mock(ClassLoaderFile.class));
this.files.addFile("s2", "n3", mock(ClassLoaderFile.class));
this.files.addFile("s2", "n1", mock(ClassLoaderFile.class));
assertThat(this.files.size(), equalTo(3));
}
@Test
public void classLoaderFilesMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ClassLoaderFiles must not be null");
new ClassLoaderFiles(null);
}
@Test
public void constructFromExistingSet() throws Exception {
this.files.addFile("s1", "n1", mock(ClassLoaderFile.class));
this.files.addFile("s1", "n2", mock(ClassLoaderFile.class));
ClassLoaderFiles copy = new ClassLoaderFiles(this.files);
this.files.addFile("s2", "n3", mock(ClassLoaderFile.class));
assertThat(this.files.size(), equalTo(3));
assertThat(copy.size(), equalTo(2));
}
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2012-2015 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.restart.classloader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.RestartClassLoader;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RestartClassLoader}.
*
* @author Phillip Webb
*/
@SuppressWarnings("resource")
public class RestartClassLoaderTests {
private static final String PACKAGE = RestartClassLoaderTests.class.getPackage()
.getName();
private static final String PACKAGE_PATH = PACKAGE.replace(".", "/");
private static final Charset UTF_8 = Charset.forName("UTF-8");
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
private File sampleJarFile;
private URLClassLoader parentClassLoader;
private ClassLoaderFiles updatedFiles;
private RestartClassLoader reloadClassLoader;
@Before
public void setup() throws Exception {
this.sampleJarFile = createSampleJarFile();
URL url = this.sampleJarFile.toURI().toURL();
ClassLoader classLoader = getClass().getClassLoader();
URL[] urls = new URL[] { url };
this.parentClassLoader = new URLClassLoader(urls, classLoader);
this.updatedFiles = new ClassLoaderFiles();
this.reloadClassLoader = new RestartClassLoader(this.parentClassLoader, urls,
this.updatedFiles);
}
private File createSampleJarFile() throws IOException {
File file = this.temp.newFile("sample.jar");
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(file));
jarOutputStream.putNextEntry(new ZipEntry(PACKAGE_PATH + "/Sample.class"));
StreamUtils.copy(getClass().getResourceAsStream("Sample.class"), jarOutputStream);
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(PACKAGE_PATH + "/Sample.txt"));
StreamUtils.copy("fromchild", UTF_8, jarOutputStream);
jarOutputStream.closeEntry();
jarOutputStream.close();
return file;
}
@Test
public void parentMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Parent must not be null");
new RestartClassLoader(null, new URL[] {});
}
@Test
public void updatedFilesMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("UpdatedFiles must not be null");
new RestartClassLoader(this.parentClassLoader, new URL[] {}, null);
}
@Test
public void getResourceFromReloadableUrl() throws Exception {
String content = readString(this.reloadClassLoader
.getResourceAsStream(PACKAGE_PATH + "/Sample.txt"));
assertThat(content, startsWith("fromchild"));
}
@Test
public void getResourceFromParent() throws Exception {
String content = readString(this.reloadClassLoader
.getResourceAsStream(PACKAGE_PATH + "/Parent.txt"));
assertThat(content, startsWith("fromparent"));
}
@Test
public void getResourcesFiltersDuplicates() throws Exception {
List<URL> resources = toList(this.reloadClassLoader.getResources(PACKAGE_PATH
+ "/Sample.txt"));
assertThat(resources.size(), equalTo(1));
}
@Test
public void loadClassFromReloadableUrl() throws Exception {
Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
assertThat(loaded.getClassLoader(), equalTo((ClassLoader) this.reloadClassLoader));
}
@Test
public void loadClassFromParent() throws Exception {
Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".SampleParent");
assertThat(loaded.getClassLoader(), equalTo(getClass().getClassLoader()));
}
@Test
public void getDeletedResource() throws Exception {
String name = PACKAGE_PATH + "/Sample.txt";
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
assertThat(this.reloadClassLoader.getResource(name), equalTo(null));
}
@Test
public void getDeletedResourceAsStream() throws Exception {
String name = PACKAGE_PATH + "/Sample.txt";
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
assertThat(this.reloadClassLoader.getResourceAsStream(name), equalTo(null));
}
@Test
public void getUpdatedResource() throws Exception {
String name = PACKAGE_PATH + "/Sample.txt";
byte[] bytes = "abc".getBytes();
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, bytes));
URL resource = this.reloadClassLoader.getResource(name);
assertThat(FileCopyUtils.copyToByteArray(resource.openStream()), equalTo(bytes));
}
@Test
public void getResourcesWithDeleted() throws Exception {
String name = PACKAGE_PATH + "/Sample.txt";
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
List<URL> resources = toList(this.reloadClassLoader.getResources(name));
assertThat(resources.size(), equalTo(0));
}
@Test
public void getResourcesWithUpdated() throws Exception {
String name = PACKAGE_PATH + "/Sample.txt";
byte[] bytes = "abc".getBytes();
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, bytes));
List<URL> resources = toList(this.reloadClassLoader.getResources(name));
assertThat(FileCopyUtils.copyToByteArray(resources.get(0).openStream()),
equalTo(bytes));
}
@Test
public void getDeletedClass() throws Exception {
String name = PACKAGE_PATH + "/Sample.class";
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
this.thrown.expect(ClassNotFoundException.class);
this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
}
@Test
public void getUpdatedClass() throws Exception {
String name = PACKAGE_PATH + "/Sample.class";
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, new byte[10]));
this.thrown.expect(ClassFormatError.class);
this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
}
@Test
public void getAddedClass() throws Exception {
String name = PACKAGE_PATH + "/SampleParent.class";
byte[] bytes = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream(
"SampleParent.class"));
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.ADDED, bytes));
Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".SampleParent");
assertThat(loaded.getClassLoader(), equalTo((ClassLoader) this.reloadClassLoader));
}
private String readString(InputStream in) throws IOException {
return new String(FileCopyUtils.copyToByteArray(in));
}
private <T> List<T> toList(Enumeration<T> enumeration) {
List<T> list = new ArrayList<T>();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
list.add(enumeration.nextElement());
}
}
return list;
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2012-2015 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.restart.classloader;
/**
* A sample class used to test reloading.
*
* @author Phillip Webb
*/
public class Sample {
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2012-2015 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.restart.classloader;
/**
* A sample class used to test reloading.
*
* @author Phillip Webb
*/
public class SampleParent {
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2012-2015 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.restart.server;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.springframework.boot.devtools.restart.server.DefaultSourceFolderUrlFilter;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DefaultSourceFolderUrlFilter}.
*
* @author Phillip Webb
*/
public class DefaultSourceFolderUrlFilterTests {
private static final String SOURCE_ROOT = "/Users/me/code/some-root/";
private static final List<String> COMMON_POSTFIXES;
static {
List<String> postfixes = new ArrayList<String>();
postfixes.add(".jar");
postfixes.add("-1.3.0.jar");
postfixes.add("-1.3.0-SNAPSHOT.jar");
postfixes.add("-1.3.0.BUILD-SNAPSHOT.jar");
postfixes.add("-1.3.0.M1.jar");
postfixes.add("-1.3.0.RC1.jar");
postfixes.add("-1.3.0.RELEASE.jar");
postfixes.add("-1.3.0.Final.jar");
postfixes.add("-1.3.0.GA.jar");
postfixes.add("-1.3.0.0.0.0.jar");
COMMON_POSTFIXES = Collections.unmodifiableList(postfixes);
}
private DefaultSourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
@Test
public void mavenSourceFolder() throws Exception {
doTest("my-module/target/classes/");
}
@Test
public void gradleEclipseSourceFolder() throws Exception {
doTest("my-module/bin/");
}
@Test
public void unusualSourceFolder() throws Exception {
doTest("my-module/something/quite/quite/mad/");
}
private void doTest(String sourcePostfix) throws MalformedURLException {
doTest(sourcePostfix, "my-module", true);
doTest(sourcePostfix, "my-module-other", false);
doTest(sourcePostfix, "my-module-other-again", false);
doTest(sourcePostfix, "my-module.other", false);
}
private void doTest(String sourcePostfix, String moduleRoot, boolean expected)
throws MalformedURLException {
String sourceFolder = SOURCE_ROOT + sourcePostfix;
for (String postfix : COMMON_POSTFIXES) {
for (URL url : getUrls(moduleRoot + postfix)) {
boolean match = this.filter.isMatch(sourceFolder, url);
assertThat(url + " against " + sourceFolder, match, equalTo(expected));
}
}
}
private List<URL> getUrls(String name) throws MalformedURLException {
List<URL> urls = new ArrayList<URL>();
urls.add(new URL("file:/some/path/" + name));
urls.add(new URL("file:/some/path/" + name + "!/"));
for (String postfix : COMMON_POSTFIXES) {
urls.add(new URL("jar:file:/some/path/lib-module" + postfix + "!/lib/" + name));
urls.add(new URL("jar:file:/some/path/lib-module" + postfix + "!/lib/" + name
+ "!/"));
}
return urls;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2015 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.restart.server;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.restart.server.HttpRestartServer;
import org.springframework.boot.devtools.restart.server.HttpRestartServerHandler;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link HttpRestartServerHandler}.
*
* @author Phillip Webb
*/
public class HttpRestartServerHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void serverMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Server must not be null");
new HttpRestartServerHandler(null);
}
@Test
public void handleDelegatesToServer() throws Exception {
HttpRestartServer server = mock(HttpRestartServer.class);
HttpRestartServerHandler handler = new HttpRestartServerHandler(server);
ServerHttpRequest request = mock(ServerHttpRequest.class);
ServerHttpResponse response = mock(ServerHttpResponse.class);
handler.handle(request, response);
verify(server).handle(request, response);
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2012-2015 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.restart.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.server.HttpRestartServer;
import org.springframework.boot.devtools.restart.server.RestartServer;
import org.springframework.boot.devtools.restart.server.SourceFolderUrlFilter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link HttpRestartServer}.
*
* @author Phillip Webb
*/
public class HttpRestartServerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private RestartServer delegate;
private HttpRestartServer server;
@Captor
private ArgumentCaptor<ClassLoaderFiles> filesCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.server = new HttpRestartServer(this.delegate);
}
@Test
public void sourceFolderUrlFilterMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SourceFolderUrlFilter must not be null");
new HttpRestartServer((SourceFolderUrlFilter) null);
}
@Test
public void restartServerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RestartServer must not be null");
new HttpRestartServer((RestartServer) null);
}
@Test
public void sendClassLoaderFiles() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ClassLoaderFiles files = new ClassLoaderFiles();
files.addFile("name", new ClassLoaderFile(Kind.ADDED, new byte[0]));
byte[] bytes = serialize(files);
request.setContent(bytes);
this.server.handle(new ServletServerHttpRequest(request),
new ServletServerHttpResponse(response));
verify(this.delegate).updateAndRestart(this.filesCaptor.capture());
assertThat(this.filesCaptor.getValue().getFile("name"), notNullValue());
assertThat(response.getStatus(), equalTo(200));
}
@Test
public void sendNoContent() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
this.server.handle(new ServletServerHttpRequest(request),
new ServletServerHttpResponse(response));
verifyZeroInteractions(this.delegate);
assertThat(response.getStatus(), equalTo(500));
}
@Test
public void sendBadData() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.setContent(new byte[] { 0, 0, 0 });
this.server.handle(new ServletServerHttpRequest(request),
new ServletServerHttpResponse(response));
verifyZeroInteractions(this.delegate);
assertThat(response.getStatus(), equalTo(500));
}
private byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.close();
return bos.toByteArray();
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2012-2015 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.restart.server;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.server.DefaultSourceFolderUrlFilter;
import org.springframework.boot.devtools.restart.server.RestartServer;
import org.springframework.boot.devtools.restart.server.SourceFolderUrlFilter;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RestartServer}.
*
* @author Phillip Webb
*/
public class RestartServerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void sourceFolderUrlFilterMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SourceFolderUrlFilter must not be null");
new RestartServer((SourceFolderUrlFilter) null);
}
@Test
public void updateAndRestart() throws Exception {
URL url1 = new URL("file:/proj/module-a.jar!/");
URL url2 = new URL("file:/proj/module-b.jar!/");
URL url3 = new URL("file:/proj/module-c.jar!/");
URL url4 = new URL("file:/proj/module-d.jar!/");
URLClassLoader classLoaderA = new URLClassLoader(new URL[] { url1, url2 });
URLClassLoader classLoaderB = new URLClassLoader(new URL[] { url3, url4 },
classLoaderA);
SourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
MockRestartServer server = new MockRestartServer(filter, classLoaderB);
ClassLoaderFiles files = new ClassLoaderFiles();
ClassLoaderFile fileA = new ClassLoaderFile(Kind.ADDED, new byte[0]);
ClassLoaderFile fileB = new ClassLoaderFile(Kind.ADDED, new byte[0]);
files.addFile("my/module-a", "ClassA.class", fileA);
files.addFile("my/module-c", "ClassB.class", fileB);
server.updateAndRestart(files);
Set<URL> expectedUrls = new LinkedHashSet<URL>(Arrays.asList(url1, url3));
assertThat(server.restartUrls, equalTo(expectedUrls));
assertThat(server.restartFiles, equalTo(files));
}
@Test
public void updateSetsJarLastModified() throws Exception {
long startTime = System.currentTimeMillis();
File folder = this.temp.newFolder();
File jarFile = new File(folder, "module-a.jar");
new FileOutputStream(jarFile).close();
jarFile.setLastModified(0);
URL url = jarFile.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[] { url });
SourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
MockRestartServer server = new MockRestartServer(filter, classLoader);
ClassLoaderFiles files = new ClassLoaderFiles();
ClassLoaderFile fileA = new ClassLoaderFile(Kind.ADDED, new byte[0]);
files.addFile("my/module-a", "ClassA.class", fileA);
server.updateAndRestart(files);
assertThat(jarFile.lastModified(), greaterThan(startTime - 1000));
}
@Test
public void updateReplacesLocalFilesWhenPossible() throws Exception {
// This is critical for Cloud Foundry support where the application is
// run exploded and resources can be found from the servlet root (outside of the
// classloader)
File folder = this.temp.newFolder();
File classFile = new File(folder, "ClassA.class");
FileCopyUtils.copy("abc".getBytes(), classFile);
URL url = folder.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[] { url });
SourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
MockRestartServer server = new MockRestartServer(filter, classLoader);
ClassLoaderFiles files = new ClassLoaderFiles();
ClassLoaderFile fileA = new ClassLoaderFile(Kind.ADDED, "def".getBytes());
files.addFile("my/module-a", "ClassA.class", fileA);
server.updateAndRestart(files);
assertThat(FileCopyUtils.copyToByteArray(classFile), equalTo("def".getBytes()));
}
private static class MockRestartServer extends RestartServer {
public MockRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter,
ClassLoader classLoader) {
super(sourceFolderUrlFilter, classLoader);
}
private Set<URL> restartUrls;
private ClassLoaderFiles restartFiles;
@Override
protected void restart(Set<URL> urls, ClassLoaderFiles files) {
this.restartUrls = urls;
this.restartFiles = files;
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2012-2015 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.test;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.mock.http.client.MockClientHttpResponse;
/**
* Mock {@link ClientHttpRequestFactory}.
*
* @author Phillip Webb
*/
public class MockClientHttpRequestFactory implements ClientHttpRequestFactory {
private AtomicLong seq = new AtomicLong();
private Deque<Response> responses = new ArrayDeque<Response>();
private List<MockClientHttpRequest> executedRequests = new ArrayList<MockClientHttpRequest>();
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod)
throws IOException {
return new MockRequest(uri, httpMethod);
}
public void willRespond(HttpStatus... response) {
for (HttpStatus status : response) {
this.responses.add(new Response(0, null, status));
}
}
public void willRespond(String... response) {
for (String payload : response) {
this.responses.add(new Response(0, payload.getBytes(), HttpStatus.OK));
}
}
public void willRespondAfterDelay(int delay, HttpStatus status) {
this.responses.add(new Response(delay, null, status));
}
public List<MockClientHttpRequest> getExecutedRequests() {
return this.executedRequests;
}
private class MockRequest extends MockClientHttpRequest {
public MockRequest(URI uri, HttpMethod httpMethod) {
super(httpMethod, uri);
}
@Override
protected ClientHttpResponse executeInternal() throws IOException {
MockClientHttpRequestFactory.this.executedRequests.add(this);
Response response = MockClientHttpRequestFactory.this.responses.pollFirst();
if (response == null) {
response = new Response(0, null, HttpStatus.GONE);
}
return response.asHttpResponse(MockClientHttpRequestFactory.this.seq);
}
}
static class Response {
private final int delay;
private final byte[] payload;
private final HttpStatus status;
public Response(int delay, byte[] payload, HttpStatus status) {
this.delay = delay;
this.payload = payload;
this.status = status;
}
public ClientHttpResponse asHttpResponse(AtomicLong seq) {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
this.payload, this.status);
waitForDelay();
if (this.payload != null) {
httpResponse.getHeaders().setContentLength(this.payload.length);
httpResponse.getHeaders().setContentType(
MediaType.APPLICATION_OCTET_STREAM);
httpResponse.getHeaders().add("x-seq",
Long.toString(seq.incrementAndGet()));
}
return httpResponse;
}
private void waitForDelay() {
if (this.delay > 0) {
try {
Thread.sleep(this.delay);
}
catch (InterruptedException ex) {
}
}
}
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2012-2015 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.tunnel.client;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.Executor;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.test.MockClientHttpRequestFactory;
import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection;
import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection.TunnelChannel;
import org.springframework.http.HttpStatus;
import org.springframework.util.SocketUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link HttpTunnelConnection}.
*
* @author Phillip Webb
* @author Rob Winch
*/
public class HttpTunnelConnectionTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private int port = SocketUtils.findAvailableTcpPort();
private String url;
private ByteArrayOutputStream incommingData;
private WritableByteChannel incomingChannel;
@Mock
private Closeable closeable;
private MockClientHttpRequestFactory requestFactory = new MockClientHttpRequestFactory();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.url = "http://localhost:" + this.port;
this.incommingData = new ByteArrayOutputStream();
this.incomingChannel = Channels.newChannel(this.incommingData);
}
@Test
public void urlMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new HttpTunnelConnection(null, this.requestFactory);
}
@Test
public void urlMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new HttpTunnelConnection("", this.requestFactory);
}
@Test
public void urlMustNotBeMalformed() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Malformed URL 'htttttp:///ttest'");
new HttpTunnelConnection("htttttp:///ttest", this.requestFactory);
}
@Test
public void requestFactoryMustNotBeNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RequestFactory must not be null");
new HttpTunnelConnection(this.url, null);
}
@Test
public void closeTunnelChangesIsOpen() throws Exception {
this.requestFactory.willRespondAfterDelay(1000, HttpStatus.GONE);
WritableByteChannel channel = openTunnel(false);
assertThat(channel.isOpen(), equalTo(true));
channel.close();
assertThat(channel.isOpen(), equalTo(false));
}
@Test
public void closeTunnelCallsCloseableOnce() throws Exception {
this.requestFactory.willRespondAfterDelay(1000, HttpStatus.GONE);
WritableByteChannel channel = openTunnel(false);
verify(this.closeable, never()).close();
channel.close();
channel.close();
verify(this.closeable, times(1)).close();
}
@Test
public void typicalTraffic() throws Exception {
this.requestFactory.willRespond("hi", "=2", "=3");
TunnelChannel channel = openTunnel(true);
write(channel, "hello");
write(channel, "1+1");
write(channel, "1+2");
assertThat(this.incommingData.toString(), equalTo("hi=2=3"));
}
@Test
public void trafficWithLongPollTimeouts() throws Exception {
for (int i = 0; i < 10; i++) {
this.requestFactory.willRespond(HttpStatus.NO_CONTENT);
}
this.requestFactory.willRespond("hi");
TunnelChannel channel = openTunnel(true);
write(channel, "hello");
assertThat(this.incommingData.toString(), equalTo("hi"));
assertThat(this.requestFactory.getExecutedRequests().size(), greaterThan(10));
}
private void write(TunnelChannel channel, String string) throws IOException {
channel.write(ByteBuffer.wrap(string.getBytes()));
}
private TunnelChannel openTunnel(boolean singleThreaded) throws Exception {
HttpTunnelConnection connection = new HttpTunnelConnection(this.url,
this.requestFactory,
(singleThreaded ? new CurrentThreadExecutor() : null));
return connection.open(this.incomingChannel, this.closeable);
}
private static class CurrentThreadExecutor implements Executor {
@Override
public void execute(Runnable command) {
command.run();
}
}
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2012-2015 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.tunnel.client;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.tunnel.client.TunnelClient;
import org.springframework.boot.devtools.tunnel.client.TunnelClientListener;
import org.springframework.boot.devtools.tunnel.client.TunnelConnection;
import org.springframework.util.SocketUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link TunnelClient}.
*
* @author Phillip Webb
*/
public class TunnelClientTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private int listenPort = SocketUtils.findAvailableTcpPort();
private MockTunnelConnection tunnelConnection = new MockTunnelConnection();
@Test
public void listenPortMustBePositive() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ListenPort must be positive");
new TunnelClient(0, this.tunnelConnection);
}
@Test
public void tunnelConnectionMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TunnelConnection must not be null");
new TunnelClient(1, null);
}
@Test
public void typicalTraffic() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
channel.write(ByteBuffer.wrap("hello".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(5);
channel.read(buffer);
channel.close();
this.tunnelConnection.verifyWritten("hello");
assertThat(new String(buffer.array()), equalTo("olleh"));
}
@Test
public void socketChannelClosedTriggersTunnelClose() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
Thread.sleep(200);
channel.close();
client.getServerThread().stopAcceptingConnections();
client.getServerThread().join(2000);
assertThat(this.tunnelConnection.getOpenedTimes(), equalTo(1));
assertThat(this.tunnelConnection.isOpen(), equalTo(false));
}
@Test
public void stopTriggersTunnelClose() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
Thread.sleep(200);
client.stop();
assertThat(this.tunnelConnection.getOpenedTimes(), equalTo(1));
assertThat(this.tunnelConnection.isOpen(), equalTo(false));
assertThat(channel.read(ByteBuffer.allocate(1)), equalTo(-1));
}
@Test
public void addListener() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
TunnelClientListener listener = mock(TunnelClientListener.class);
client.addListener(listener);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
Thread.sleep(200);
channel.close();
client.getServerThread().stopAcceptingConnections();
client.getServerThread().join(2000);
verify(listener).onOpen(any(SocketChannel.class));
verify(listener).onClose(any(SocketChannel.class));
}
private static class MockTunnelConnection implements TunnelConnection {
private final ByteArrayOutputStream written = new ByteArrayOutputStream();
private boolean open;
private int openedTimes;
@Override
public WritableByteChannel open(WritableByteChannel incomingChannel,
Closeable closeable) throws Exception {
this.openedTimes++;
this.open = true;
return new TunnelChannel(incomingChannel, closeable);
}
public void verifyWritten(String expected) {
verifyWritten(expected.getBytes());
}
public void verifyWritten(byte[] expected) {
synchronized (this.written) {
assertThat(this.written.toByteArray(), equalTo(expected));
this.written.reset();
}
}
public boolean isOpen() {
return this.open;
}
public int getOpenedTimes() {
return this.openedTimes;
}
private class TunnelChannel implements WritableByteChannel {
private final WritableByteChannel incomingChannel;
private final Closeable closeable;
public TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) {
this.incomingChannel = incomingChannel;
this.closeable = closeable;
}
@Override
public boolean isOpen() {
return MockTunnelConnection.this.open;
}
@Override
public void close() throws IOException {
MockTunnelConnection.this.open = false;
this.closeable.close();
}
@Override
public int write(ByteBuffer src) throws IOException {
int remaining = src.remaining();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Channels.newChannel(stream).write(src);
byte[] bytes = stream.toByteArray();
synchronized (MockTunnelConnection.this.written) {
MockTunnelConnection.this.written.write(bytes);
}
byte[] reversed = new byte[bytes.length];
for (int i = 0; i < reversed.length; i++) {
reversed[i] = bytes[bytes.length - 1 - i];
}
this.incomingChannel.write(ByteBuffer.wrap(reversed));
return remaining;
}
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2015 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.tunnel.payload;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayloadForwarder;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link HttpTunnelPayloadForwarder}.
*
* @author Phillip Webb
*/
public class HttpTunnelPayloadForwarderTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void targetChannelMustNoBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TargetChannel must not be null");
new HttpTunnelPayloadForwarder(null);
}
@Test
public void forwardInSequence() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
WritableByteChannel channel = Channels.newChannel(out);
HttpTunnelPayloadForwarder forwarder = new HttpTunnelPayloadForwarder(channel);
forwarder.forward(payload(1, "he"));
forwarder.forward(payload(2, "ll"));
forwarder.forward(payload(3, "o"));
assertThat(out.toByteArray(), equalTo("hello".getBytes()));
}
@Test
public void forwardOutOfSequence() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
WritableByteChannel channel = Channels.newChannel(out);
HttpTunnelPayloadForwarder forwarder = new HttpTunnelPayloadForwarder(channel);
forwarder.forward(payload(3, "o"));
forwarder.forward(payload(2, "ll"));
forwarder.forward(payload(1, "he"));
assertThat(out.toByteArray(), equalTo("hello".getBytes()));
}
@Test
public void overflow() throws Exception {
WritableByteChannel channel = Channels.newChannel(new ByteArrayOutputStream());
HttpTunnelPayloadForwarder forwarder = new HttpTunnelPayloadForwarder(channel);
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Too many messages queued");
for (int i = 2; i < 130; i++) {
forwarder.forward(payload(i, "data" + i));
}
}
private HttpTunnelPayload payload(long sequence, String data) {
return new HttpTunnelPayload(sequence, ByteBuffer.wrap(data.getBytes()));
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2012-2015 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.tunnel.payload;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpTunnelPayload}.
*
* @author Phillip Webb
*/
public class HttpTunnelPayloadTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void sequenceMustBePositive() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Sequence must be positive");
new HttpTunnelPayload(0, ByteBuffer.allocate(1));
}
@Test
public void dataMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Data must not be null");
new HttpTunnelPayload(1, null);
}
@Test
public void getSequence() throws Exception {
HttpTunnelPayload payload = new HttpTunnelPayload(1, ByteBuffer.allocate(1));
assertThat(payload.getSequence(), equalTo(1L));
}
@Test
public void getData() throws Exception {
ByteBuffer data = ByteBuffer.wrap("hello".getBytes());
HttpTunnelPayload payload = new HttpTunnelPayload(1, data);
assertThat(getData(payload), equalTo(data.array()));
}
@Test
public void assignTo() throws Exception {
ByteBuffer data = ByteBuffer.wrap("hello".getBytes());
HttpTunnelPayload payload = new HttpTunnelPayload(2, data);
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
HttpOutputMessage response = new ServletServerHttpResponse(servletResponse);
payload.assignTo(response);
assertThat(servletResponse.getHeader("x-seq"), equalTo("2"));
assertThat(servletResponse.getContentAsString(), equalTo("hello"));
}
@Test
public void getNoData() throws Exception {
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
HttpInputMessage request = new ServletServerHttpRequest(servletRequest);
HttpTunnelPayload payload = HttpTunnelPayload.get(request);
assertThat(payload, nullValue());
}
@Test
public void getWithMissingHeader() throws Exception {
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
servletRequest.setContent("hello".getBytes());
HttpInputMessage request = new ServletServerHttpRequest(servletRequest);
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Missing sequence header");
HttpTunnelPayload.get(request);
}
@Test
public void getWithData() throws Exception {
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
servletRequest.setContent("hello".getBytes());
servletRequest.addHeader("x-seq", 123);
HttpInputMessage request = new ServletServerHttpRequest(servletRequest);
HttpTunnelPayload payload = HttpTunnelPayload.get(request);
assertThat(payload.getSequence(), equalTo(123L));
assertThat(getData(payload), equalTo("hello".getBytes()));
}
@Test
public void getPayloadData() throws Exception {
ReadableByteChannel channel = Channels.newChannel(new ByteArrayInputStream(
"hello".getBytes()));
ByteBuffer payloadData = HttpTunnelPayload.getPayloadData(channel);
ByteArrayOutputStream out = new ByteArrayOutputStream();
WritableByteChannel writeChannel = Channels.newChannel(out);
while (payloadData.hasRemaining()) {
writeChannel.write(payloadData);
}
assertThat(out.toByteArray(), equalTo("hello".getBytes()));
}
@Test
public void getPayloadDataWithTimeout() throws Exception {
ReadableByteChannel channel = mock(ReadableByteChannel.class);
given(channel.read(any(ByteBuffer.class)))
.willThrow(new SocketTimeoutException());
ByteBuffer payload = HttpTunnelPayload.getPayloadData(channel);
assertThat(payload, nullValue());
}
private byte[] getData(HttpTunnelPayload payload) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
WritableByteChannel channel = Channels.newChannel(out);
payload.writeTo(channel);
return out.toByteArray();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2015 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.tunnel.server;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServerHandler;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link HttpTunnelServerHandler}.
*
* @author Phillip Webb
*/
public class HttpTunnelServerHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void serverMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Server must not be null");
new HttpTunnelServerHandler(null);
}
@Test
public void handleDelegatesToServer() throws Exception {
HttpTunnelServer server = mock(HttpTunnelServer.class);
HttpTunnelServerHandler handler = new HttpTunnelServerHandler(server);
ServerHttpRequest request = mock(ServerHttpRequest.class);
ServerHttpResponse response = mock(ServerHttpResponse.class);
handler.handle(request, response);
verify(server).handle(request, response);
}
}

View File

@@ -0,0 +1,482 @@
/*
* Copyright 2012-2015 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.tunnel.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.Channels;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer;
import org.springframework.boot.devtools.tunnel.server.TargetServerConnection;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer.HttpConnection;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpAsyncRequestControl;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link HttpTunnelServer}.
*
* @author Phillip Webb
*/
public class HttpTunnelServerTests {
private static final int DEFAULT_LONG_POLL_TIMEOUT = 10000;
private static final byte[] NO_DATA = {};
private static final String SEQ_HEADER = "x-seq";
@Rule
public ExpectedException thrown = ExpectedException.none();
private HttpTunnelServer server;
@Mock
private TargetServerConnection serverConnection;
private MockHttpServletRequest servletRequest;
private MockHttpServletResponse servletResponse;
private ServerHttpRequest request;
private ServerHttpResponse response;
private MockServerChannel serverChannel;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
this.server = new HttpTunnelServer(this.serverConnection);
given(this.serverConnection.open(anyInt())).willAnswer(new Answer<ByteChannel>() {
@Override
public ByteChannel answer(InvocationOnMock invocation) throws Throwable {
MockServerChannel channel = HttpTunnelServerTests.this.serverChannel;
channel.setTimeout((Integer) invocation.getArguments()[0]);
return channel;
}
});
this.servletRequest = new MockHttpServletRequest();
this.servletRequest.setAsyncSupported(true);
this.servletResponse = new MockHttpServletResponse();
this.request = new ServletServerHttpRequest(this.servletRequest);
this.response = new ServletServerHttpResponse(this.servletResponse);
this.serverChannel = new MockServerChannel();
}
@Test
public void serverConnectionIsRequired() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ServerConnection must not be null");
new HttpTunnelServer(null);
}
@Test
public void serverConnectedOnFirstRequest() throws Exception {
verify(this.serverConnection, never()).open(anyInt());
this.server.handle(this.request, this.response);
verify(this.serverConnection, times(1)).open(DEFAULT_LONG_POLL_TIMEOUT);
}
@Test
public void longPollTimeout() throws Exception {
this.server.setLongPollTimeout(800);
this.server.handle(this.request, this.response);
verify(this.serverConnection, times(1)).open(800);
}
@Test
public void longPollTimeoutMustBePositiveValue() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("LongPollTimeout must be a positive value");
this.server.setLongPollTimeout(0);
}
@Test
public void initialRequestIsSentToServer() throws Exception {
this.servletRequest.addHeader(SEQ_HEADER, "1");
this.servletRequest.setContent("hello".getBytes());
this.server.handle(this.request, this.response);
this.serverChannel.disconnect();
this.server.getServerThread().join();
this.serverChannel.verifyReceived("hello");
}
@Test
public void intialRequestIsUsedForFirstServerResponse() throws Exception {
this.servletRequest.addHeader(SEQ_HEADER, "1");
this.servletRequest.setContent("hello".getBytes());
this.server.handle(this.request, this.response);
System.out.println("sending");
this.serverChannel.send("hello");
this.serverChannel.disconnect();
this.server.getServerThread().join();
assertThat(this.servletResponse.getContentAsString(), equalTo("hello"));
this.serverChannel.verifyReceived("hello");
}
@Test
public void initialRequestHasNoPayload() throws Exception {
this.server.handle(this.request, this.response);
this.serverChannel.disconnect();
this.server.getServerThread().join();
this.serverChannel.verifyReceived(NO_DATA);
}
@Test
public void typicalReqestResponseTraffic() throws Exception {
MockHttpConnection h1 = new MockHttpConnection();
this.server.handle(h1);
MockHttpConnection h2 = new MockHttpConnection("hello server", 1);
this.server.handle(h2);
this.serverChannel.verifyReceived("hello server");
this.serverChannel.send("hello client");
h1.verifyReceived("hello client", 1);
MockHttpConnection h3 = new MockHttpConnection("1+1", 2);
this.server.handle(h3);
this.serverChannel.send("=2");
h2.verifyReceived("=2", 2);
MockHttpConnection h4 = new MockHttpConnection("1+2", 3);
this.server.handle(h4);
this.serverChannel.send("=3");
h3.verifyReceived("=3", 3);
this.serverChannel.disconnect();
this.server.getServerThread().join();
}
@Test
public void clientIsAwareOfServerClose() throws Exception {
MockHttpConnection h1 = new MockHttpConnection("1", 1);
this.server.handle(h1);
this.serverChannel.disconnect();
this.server.getServerThread().join();
assertThat(h1.getServletResponse().getStatus(), equalTo(410));
}
@Test
public void clientCanCloseServer() throws Exception {
MockHttpConnection h1 = new MockHttpConnection();
this.server.handle(h1);
MockHttpConnection h2 = new MockHttpConnection("DISCONNECT", 1);
h2.getServletRequest().addHeader("Content-Type", "application/x-disconnect");
this.server.handle(h2);
this.server.getServerThread().join();
assertThat(h1.getServletResponse().getStatus(), equalTo(410));
assertThat(this.serverChannel.isOpen(), equalTo(false));
}
@Test
public void neverMoreThanTwoHttpConnections() throws Exception {
MockHttpConnection h1 = new MockHttpConnection();
this.server.handle(h1);
MockHttpConnection h2 = new MockHttpConnection("1", 2);
this.server.handle(h2);
MockHttpConnection h3 = new MockHttpConnection("2", 3);
this.server.handle(h3);
h1.waitForResponse();
assertThat(h1.getServletResponse().getStatus(), equalTo(429));
this.serverChannel.disconnect();
this.server.getServerThread().join();
}
@Test
public void requestRecievedOutOfOrder() throws Exception {
MockHttpConnection h1 = new MockHttpConnection();
MockHttpConnection h2 = new MockHttpConnection("1+2", 1);
MockHttpConnection h3 = new MockHttpConnection("+3", 2);
this.server.handle(h1);
this.server.handle(h3);
this.server.handle(h2);
this.serverChannel.verifyReceived("1+2+3");
this.serverChannel.disconnect();
this.server.getServerThread().join();
}
@Test
public void httpConnectionsAreClosedAfterLongPollTimeout() throws Exception {
this.server.setDisconnectTimeout(1000);
this.server.setLongPollTimeout(100);
MockHttpConnection h1 = new MockHttpConnection();
this.server.handle(h1);
MockHttpConnection h2 = new MockHttpConnection();
this.server.handle(h2);
Thread.sleep(400);
this.serverChannel.disconnect();
this.server.getServerThread().join();
assertThat(h1.getServletResponse().getStatus(), equalTo(204));
assertThat(h2.getServletResponse().getStatus(), equalTo(204));
}
@Test
public void disconnectTimeout() throws Exception {
this.server.setDisconnectTimeout(100);
this.server.setLongPollTimeout(100);
MockHttpConnection h1 = new MockHttpConnection();
this.server.handle(h1);
this.serverChannel.send("hello");
this.server.getServerThread().join();
assertThat(this.serverChannel.isOpen(), equalTo(false));
}
@Test
public void disconnectTimeoutMustBePositive() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("DisconnectTimeout must be a positive value");
this.server.setDisconnectTimeout(0);
}
@Test
public void httpConnectionRespondWithPayload() throws Exception {
HttpConnection connection = new HttpConnection(this.request, this.response);
connection.waitForResponse();
connection.respond(new HttpTunnelPayload(1, ByteBuffer.wrap("hello".getBytes())));
assertThat(this.servletResponse.getStatus(), equalTo(200));
assertThat(this.servletResponse.getContentAsString(), equalTo("hello"));
assertThat(this.servletResponse.getHeader(SEQ_HEADER), equalTo("1"));
}
@Test
public void httpConnectionRespondWithStatus() throws Exception {
HttpConnection connection = new HttpConnection(this.request, this.response);
connection.waitForResponse();
connection.respond(HttpStatus.I_AM_A_TEAPOT);
assertThat(this.servletResponse.getStatus(), equalTo(418));
assertThat(this.servletResponse.getContentLength(), equalTo(0));
}
@Test
public void httpConnectionAsync() throws Exception {
ServerHttpAsyncRequestControl async = mock(ServerHttpAsyncRequestControl.class);
ServerHttpRequest request = mock(ServerHttpRequest.class);
given(request.getAsyncRequestControl(this.response)).willReturn(async);
HttpConnection connection = new HttpConnection(request, this.response);
connection.waitForResponse();
verify(async).start();
connection.respond(HttpStatus.NO_CONTENT);
verify(async).complete();
}
@Test
public void httpConnectionNonAsync() throws Exception {
testHttpConnectionNonAsync(0);
testHttpConnectionNonAsync(100);
}
private void testHttpConnectionNonAsync(long sleepBeforeResponse) throws IOException,
InterruptedException {
ServerHttpRequest request = mock(ServerHttpRequest.class);
given(request.getAsyncRequestControl(this.response)).willThrow(
new IllegalArgumentException());
final HttpConnection connection = new HttpConnection(request, this.response);
final AtomicBoolean responded = new AtomicBoolean();
Thread connectionThread = new Thread() {
@Override
public void run() {
connection.waitForResponse();
responded.set(true);
}
};
connectionThread.start();
assertThat(responded.get(), equalTo(false));
Thread.sleep(sleepBeforeResponse);
connection.respond(HttpStatus.NO_CONTENT);
connectionThread.join();
assertThat(responded.get(), equalTo(true));
}
@Test
public void httpConnectionRunning() throws Exception {
HttpConnection connection = new HttpConnection(this.request, this.response);
assertThat(connection.isOlderThan(100), equalTo(false));
Thread.sleep(200);
assertThat(connection.isOlderThan(100), equalTo(true));
}
/**
* Mock {@link ByteChannel} used to simulate the server connection.
*/
private static class MockServerChannel implements ByteChannel {
private static final ByteBuffer DISCONNECT = ByteBuffer.wrap(NO_DATA);
private int timeout;
private BlockingDeque<ByteBuffer> outgoing = new LinkedBlockingDeque<ByteBuffer>();
private ByteArrayOutputStream written = new ByteArrayOutputStream();
private AtomicBoolean open = new AtomicBoolean(true);
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public void send(String content) {
send(content.getBytes());
}
public void send(byte[] bytes) {
this.outgoing.addLast(ByteBuffer.wrap(bytes));
}
public void disconnect() {
this.outgoing.addLast(DISCONNECT);
}
public void verifyReceived(String expected) {
verifyReceived(expected.getBytes());
}
public void verifyReceived(byte[] expected) {
synchronized (this.written) {
assertThat(this.written.toByteArray(), equalTo(expected));
this.written.reset();
}
}
@Override
public int read(ByteBuffer dst) throws IOException {
try {
ByteBuffer bytes = this.outgoing.pollFirst(this.timeout,
TimeUnit.MILLISECONDS);
if (bytes == null) {
throw new SocketTimeoutException();
}
if (bytes == DISCONNECT) {
this.open.set(false);
return -1;
}
int initialRemaining = dst.remaining();
bytes.limit(Math.min(bytes.limit(), initialRemaining));
dst.put(bytes);
bytes.limit(bytes.capacity());
return initialRemaining - dst.remaining();
}
catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public int write(ByteBuffer src) throws IOException {
int remaining = src.remaining();
synchronized (this.written) {
Channels.newChannel(this.written).write(src);
}
return remaining;
}
@Override
public boolean isOpen() {
return this.open.get();
}
@Override
public void close() throws IOException {
this.open.set(false);
}
}
/**
* Mock {@link HttpConnection}.
*/
private static class MockHttpConnection extends HttpConnection {
public MockHttpConnection() {
super(new ServletServerHttpRequest(new MockHttpServletRequest()),
new ServletServerHttpResponse(new MockHttpServletResponse()));
}
public MockHttpConnection(String content, int seq) {
this();
MockHttpServletRequest request = getServletRequest();
request.setContent(content.getBytes());
request.addHeader(SEQ_HEADER, String.valueOf(seq));
}
@Override
protected ServerHttpAsyncRequestControl startAsync() {
getServletRequest().setAsyncSupported(true);
return super.startAsync();
}
@Override
protected void complete() {
super.complete();
getServletResponse().setCommitted(true);
}
public MockHttpServletRequest getServletRequest() {
return (MockHttpServletRequest) ((ServletServerHttpRequest) getRequest())
.getServletRequest();
}
public MockHttpServletResponse getServletResponse() {
return (MockHttpServletResponse) ((ServletServerHttpResponse) getResponse())
.getServletResponse();
}
public void verifyReceived(String expectedContent, int expectedSeq)
throws Exception {
waitForServletResponse();
MockHttpServletResponse resp = getServletResponse();
assertThat(resp.getContentAsString(), equalTo(expectedContent));
assertThat(resp.getHeader(SEQ_HEADER), equalTo(String.valueOf(expectedSeq)));
}
public void waitForServletResponse() throws InterruptedException {
while (!getServletResponse().isCommitted()) {
Thread.sleep(10);
}
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2012-2015 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.tunnel.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.devtools.tunnel.server.SocketTargetServerConnection;
import org.springframework.boot.devtools.tunnel.server.StaticPortProvider;
import org.springframework.util.SocketUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
/**
* Tests for {@link SocketTargetServerConnection}.
*
* @author Phillip Webb
*/
public class SocketTargetServerConnectionTests {
private static final int DEFAULT_TIMEOUT = 1000;
private int port;
private MockServer server;
private SocketTargetServerConnection connection;
@Before
public void setup() throws IOException {
this.port = SocketUtils.findAvailableTcpPort();
this.server = new MockServer(this.port);
StaticPortProvider portProvider = new StaticPortProvider(this.port);
this.connection = new SocketTargetServerConnection(portProvider);
}
@Test
public void readData() throws Exception {
this.server.willSend("hello".getBytes());
this.server.start();
ByteChannel channel = this.connection.open(DEFAULT_TIMEOUT);
ByteBuffer buffer = ByteBuffer.allocate(5);
channel.read(buffer);
assertThat(buffer.array(), equalTo("hello".getBytes()));
}
@Test
public void writeData() throws Exception {
this.server.expect("hello".getBytes());
this.server.start();
ByteChannel channel = this.connection.open(DEFAULT_TIMEOUT);
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
channel.write(buffer);
this.server.closeAndVerify();
}
@Test
public void timeout() throws Exception {
this.server.delay(1000);
this.server.start();
ByteChannel channel = this.connection.open(10);
long startTime = System.currentTimeMillis();
try {
channel.read(ByteBuffer.allocate(5));
fail("No socket timeout thrown");
}
catch (SocketTimeoutException ex) {
// Expected
long runTime = System.currentTimeMillis() - startTime;
assertThat(runTime, greaterThanOrEqualTo(10L));
assertThat(runTime, lessThan(10000L));
}
}
private static class MockServer {
private ServerSocketChannel serverSocket;
private byte[] send;
private byte[] expect;
private int delay;
private ByteBuffer actualRead;
private ServerThread thread;
public MockServer(int port) throws IOException {
this.serverSocket = ServerSocketChannel.open();
this.serverSocket.bind(new InetSocketAddress(port));
}
public void delay(int delay) {
this.delay = delay;
}
public void willSend(byte[] send) {
this.send = send;
}
public void expect(byte[] expect) {
this.expect = expect;
}
public void start() {
this.thread = new ServerThread();
this.thread.start();
}
public void closeAndVerify() throws InterruptedException {
close();
assertThat(this.actualRead.array(), equalTo(this.expect));
}
public void close() throws InterruptedException {
while (this.thread.isAlive()) {
Thread.sleep(10);
}
}
private class ServerThread extends Thread {
@Override
public void run() {
try {
SocketChannel channel = MockServer.this.serverSocket.accept();
Thread.sleep(MockServer.this.delay);
if (MockServer.this.send != null) {
ByteBuffer buffer = ByteBuffer.wrap(MockServer.this.send);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
if (MockServer.this.expect != null) {
ByteBuffer buffer = ByteBuffer
.allocate(MockServer.this.expect.length);
while (buffer.hasRemaining()) {
channel.read(buffer);
}
MockServer.this.actualRead = buffer;
}
channel.close();
}
catch (Exception ex) {
ex.printStackTrace();
fail();
}
}
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2015 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.tunnel.server;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.devtools.tunnel.server.StaticPortProvider;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link StaticPortProvider}.
*
* @author Phillip Webb
*/
public class StaticPortProviderTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void portMustBePostive() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Port must be positive");
new StaticPortProvider(0);
}
@Test
public void getPort() throws Exception {
StaticPortProvider provider = new StaticPortProvider(123);
assertThat(provider.getPort(), equalTo(123));
}
}