/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.devtools.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.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.event.ContextClosedEvent;
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.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link Restarter}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
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")).isGreaterThan(1);
assertThat(StringUtils.countOccurrencesOf(output, "Tick 1")).isGreaterThan(1);
assertThat(CloseCountingApplicationListener.closed).isGreaterThan(0);
}
@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).isEqualTo("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]).isEqualTo(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")))
.isEqualTo("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).isEqualTo("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()).isEqualTo(contextClassLoader);
// Factory threads should inherit from the initial thread
assertThat(viaFactory.getContextClassLoader()).isEqualTo(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()).isEqualTo(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(), true);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SampleApplication.class);
context.addApplicationListener(new CloseCountingApplicationListener());
Restarter.getInstance().prepare(context);
System.out.println("Sleep " + Thread.currentThread());
sleep();
quit = true;
}
private static void sleep() {
try {
Thread.sleep(1200);
}
catch (InterruptedException ex) {
// Ignore
}
}
}
private static class CloseCountingApplicationListener
implements ApplicationListener<ContextClosedEvent> {
static int closed = 0;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
closed++;
}
}
private static class TestableRestarter extends Restarter {
private ClassLoader relaunchClassLoader;
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(FailureHandler failureHandler) {
try {
stop();
start(failureHandler);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@Override
protected Throwable relaunch(ClassLoader classLoader) throws Exception {
this.relaunchClassLoader = classLoader;
return null;
}
@Override
protected void stop() throws Exception {
}
public ClassLoader getRelaunchClassLoader() {
return this.relaunchClassLoader;
}
}
}