Fix unit tests on Java 11 (#1483)

The unit test I18nServlet and MultiParentClassLoaderTest are failing on Java 11.
This is because they mock ClassLoaders which cause a jvm error.

The following tickets describe the problem in more detail:

- https://bugs.openjdk.java.net/browse/JDK-8254969
- https://github.com/mockito/mockito/issues/2043
- https://github.com/mockito/mockito/issues/1696
This commit is contained in:
Sebastian Sdorra
2020-12-18 14:23:36 +01:00
committed by GitHub
parent 43e6bffeca
commit dda761ffc2
7 changed files with 282 additions and 230 deletions

View File

@@ -21,114 +21,125 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import com.google.common.io.Resources;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.FixedValue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import static org.hamcrest.Matchers.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class MultiParentClassLoaderTest
{
@Mock
private ClassLoader parent1;
@Mock
private ClassLoader parent2;
class MultiParentClassLoaderTest {
private MultiParentClassLoader classLoader;
@Before
public void setUp(){
classLoader = new MultiParentClassLoader(parent1, parent2);
}
@SuppressWarnings("unchecked")
@Test(expected = ClassNotFoundException.class)
public void testClassNotFoundException() throws ClassNotFoundException
{
when(parent1.loadClass("string")).thenThrow(ClassNotFoundException.class);
when(parent2.loadClass("string")).thenThrow(ClassNotFoundException.class);
classLoader.loadClass("string");
}
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void testGetResource() throws IOException
{
URL url1 = tempFolder.newFile().toURI().toURL();
URL url2 = tempFolder.newFile().toURI().toURL();
when(parent1.getResource("resource1")).thenReturn(url1);
when(parent2.getResource("resource2")).thenReturn(url2);
assertEquals(url1, classLoader.getResource("resource1"));
assertEquals(url2, classLoader.getResource("resource2"));
assertNull(classLoader.getResource("resource3"));
}
@Test
public void testGetResources() throws IOException{
URL url1 = tempFolder.newFile().toURI().toURL();
URL url2 = tempFolder.newFile().toURI().toURL();
URL url3 = tempFolder.newFile().toURI().toURL();
when(parent1.getResources("resources")).thenReturn(res(url1, url2));
when(parent2.getResources("resources")).thenReturn(res(url3));
List<URL> enm = Collections.list(classLoader.getResources("resources"));
assertThat(enm, containsInAnyOrder(url1, url2, url3));
}
@Test
public void testLoadClass() throws ClassNotFoundException{
when(parent1.loadClass("string")).then(new ClassAnswer(String.class));
when(parent1.loadClass("int")).then(new ClassAnswer(Integer.class));
assertEquals(String.class, classLoader.loadClass("string"));
assertEquals(Integer.class, classLoader.loadClass("int"));
}
private static class ClassAnswer implements Answer<Object> {
private final Class<?> clazz;
void shouldLoadClass(@TempDir Path directory) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
byte[] classOneRaw = createDynamicClass("One", "Value of one");
ClassLoader one = createClassLoader(directory, "One.class", classOneRaw);
byte[] classTwoRaw = createDynamicClass("Two", "Value of two");
ClassLoader two = createClassLoader(directory, "Two.class", classTwoRaw);
public ClassAnswer(
Class<?> clazz)
{
this.clazz = clazz;
MultiParentClassLoader classLoader = new MultiParentClassLoader(one, two);
assertClassToString(classLoader, "One", "Value of one");
assertClassToString(classLoader, "Two", "Value of two");
}
private void assertClassToString(ClassLoader classLoader, String className, String toStringValue) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> aClass = classLoader.loadClass(className);
assertThat(aClass.newInstance()).hasToString(toStringValue);
}
private byte[] createDynamicClass(String name, String toStringValue) {
return new ByteBuddy()
.subclass(Object.class)
.name(name)
.method(named("toString"))
.intercept(FixedValue.value(toStringValue))
.make()
.getBytes();
}
@Test
void shouldThrowClassNotFoundException(@TempDir Path directory) throws MalformedURLException {
URLClassLoader one = createClassLoader(directory.resolve("one"));
URLClassLoader two = createClassLoader(directory.resolve("two"));
MultiParentClassLoader classLoader = new MultiParentClassLoader(one, two);
assertThrows(ClassNotFoundException.class, () -> classLoader.loadClass("UnknownClass"));
}
@Test
void shouldReturnResource(@TempDir Path directory) throws IOException {
URLClassLoader one = createClassLoader(directory.resolve("one"), "one", "one");
URLClassLoader two = createClassLoader(directory.resolve("two"), "two", "two");
MultiParentClassLoader classLoader = new MultiParentClassLoader(one, two);
assertResource(classLoader, "one", "one");
assertResource(classLoader, "two", "two");
}
@Test
void shouldReturnResources(@TempDir Path directory) throws IOException {
URLClassLoader one = createClassLoader(directory.resolve("one"), "both", "one");
URLClassLoader two = createClassLoader(directory.resolve("two"), "both", "two");
MultiParentClassLoader classLoader = new MultiParentClassLoader(one, two);
Enumeration<URL> both = classLoader.getResources("both");
List<String> content = toStrings(both);
assertThat(content).containsOnly("one", "two");
}
@SuppressWarnings("UnstableApiUsage")
private List<String> toStrings(Enumeration<URL> urlEnumeration) throws IOException {
List<String> content = new ArrayList<>();
while (urlEnumeration.hasMoreElements()) {
URL url = urlEnumeration.nextElement();
content.add(Resources.toString(url, StandardCharsets.UTF_8));
}
return content;
}
@Override
public Object answer(InvocationOnMock invocation) throws Throwable
{
return clazz;
}
@SuppressWarnings("UnstableApiUsage")
private void assertResource(ClassLoader classLoader, String resource, String content) throws IOException {
URL url = classLoader.getResource(resource);
assertThat(url).isNotNull();
String urlContent = Resources.toString(url, StandardCharsets.UTF_8);
assertThat(urlContent).isEqualTo(content);
}
private Enumeration<URL> res(URL... urls){
return Collections.enumeration(Arrays.asList(urls));
private URLClassLoader createClassLoader(Path directory, String resource, String content) throws IOException {
return createClassLoader(directory, resource, content.getBytes(StandardCharsets.UTF_8));
}
private URLClassLoader createClassLoader(Path directory, String resource, byte[] content) throws IOException {
Path file = directory.resolve(resource);
Files.createDirectories(file.getParent());
Files.write(file, content);
return createClassLoader(directory);
}
private URLClassLoader createClassLoader(Path directory) throws MalformedURLException {
ClassLoader parent = ClassLoader.getSystemClassLoader().getParent();
return new URLClassLoader(new URL[]{directory.toUri().toURL()}, parent);
}
}

View File

@@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.legman.EventBus;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -41,19 +42,23 @@ import sonia.scm.cache.CacheManager;
import sonia.scm.lifecycle.RestartEventFactory;
import sonia.scm.plugin.PluginLoader;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Collection;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
@@ -96,8 +101,12 @@ class I18nServletTest {
"}"
);
private static final String[] ALL_PLUGIN_JSON = new String[]{
GIT_PLUGIN_JSON, HG_PLUGIN_JSON, SVN_PLUGIN_JSON
};
private static String json(String... parts) {
return String.join("\n", parts ).replaceAll("'", "\"");
return String.join("\n", parts).replaceAll("'", "\"");
}
@Mock
@@ -106,142 +115,12 @@ class I18nServletTest {
@Mock
private PluginLoader pluginLoader;
@Mock
private ClassLoader classLoader;
@Mock
private CacheManager cacheManager;
@Mock
private Cache<String, JsonNode> cache;
private I18nServlet servlet;
@BeforeEach
void init() {
when(pluginLoader.getUberClassLoader()).thenReturn(classLoader);
when(cacheManager.<String, JsonNode>getCache(I18nServlet.CACHE_NAME)).thenReturn(cache);
servlet = new I18nServlet(context, pluginLoader, cacheManager);
}
@Test
void shouldCleanCacheOnRestartEvent() {
EventBus eventBus = new EventBus("forTestingOnly");
eventBus.register(servlet);
eventBus.post(RestartEventFactory.create(I18nServlet.class, "Restart to reload the plugin resources"));
verify(cache).clear();
}
@Test
void shouldFailWith404OnMissingResources() throws IOException {
String path = "/locales/de/plugins.json";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
when(request.getServletPath()).thenReturn(path);
when(classLoader.getResources("locales/de/plugins.json")).thenReturn(
I18nServlet.class.getClassLoader().getResources("something/not/available")
);
servlet.doGet(request, response);
verify(response).setStatus(404);
}
@Test
void shouldFailWith500OnIOException() throws IOException {
stage(Stage.DEVELOPMENT);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn("/locales/de/plugins.json");
HttpServletResponse response = mock(HttpServletResponse.class);
when(classLoader.getResources("locales/de/plugins.json")).thenThrow(new IOException("failed"));
servlet.doGet(request, response);
verify(response).setStatus(500);
}
private void stage(Stage stage) {
when(context.getStage()).thenReturn(stage);
}
@Test
void inDevelopmentStageShouldNotUseCache(@TempDir Path temp) throws IOException {
stage(Stage.DEVELOPMENT);
mockResources(temp, "locales/de/plugins.json");
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn("/locales/de/plugins.json");
HttpServletResponse response = mock(HttpServletResponse.class);
String json = doGetString(request, response);
assertJson(json);
verify(cache, never()).get(any());
}
private String doGetString(HttpServletRequest request, HttpServletResponse response) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(baos);
when(response.getWriter()).thenReturn(writer);
servlet.doGet(request, response);
writer.flush();
return baos.toString(StandardCharsets.UTF_8.name());
}
private void mockResources(Path directory, String resourcePath) throws IOException {
Enumeration<URL> resources = Collections.enumeration(
Arrays.asList(
toURL(directory, "git.json", GIT_PLUGIN_JSON),
toURL(directory, "hg.json", HG_PLUGIN_JSON),
toURL(directory, "svn.json", SVN_PLUGIN_JSON)
)
);
when(classLoader.getResources(resourcePath)).thenReturn(resources);
}
private URL toURL(Path directory, String name, String content) throws IOException {
Path file = directory.resolve(name);
java.nio.file.Files.write(file, content.getBytes(StandardCharsets.UTF_8));
return file.toUri().toURL();
}
@Test
void shouldGetFromCacheInProductionStage() throws IOException {
String path = "/locales/de/plugins.json";
stage(Stage.PRODUCTION);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn(path);
HttpServletResponse response = mock(HttpServletResponse.class);
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(GIT_PLUGIN_JSON);
when(cache.get(path)).thenReturn(jsonNode);
String json = doGetString(request, response);
assertThat(json).contains("scm-git-plugin").doesNotContain("scm-hg-plugin");
verifyHeaders(response);
}
@Test
void shouldStoreToCacheInProductionStage(@TempDir Path temp) throws IOException {
String path = "/locales/de/plugins.json";
mockResources(temp, "locales/de/plugins.json");
stage(Stage.PRODUCTION);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn(path);
HttpServletResponse response = mock(HttpServletResponse.class);
String json = doGetString(request, response);
verify(cache).put(any(String.class), any(JsonNode.class));
verifyHeaders(response);
assertJson(json);
}
@Test
void shouldNotHaveInvalidPluginsJsonFiles() throws IOException {
String path = getClass().getClassLoader().getResource("locales/en/plugins.json").getPath();
@@ -266,6 +145,169 @@ class I18nServletTest {
}
}
@Nested
class WithCacheManager {
@BeforeEach
void init() {
when(cacheManager.<String, JsonNode>getCache(I18nServlet.CACHE_NAME)).thenReturn(cache);
}
@Test
void shouldFailWith404OnMissingResources(@TempDir Path directory) throws IOException {
String path = "/locales/de/plugins.json";
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn(path);
HttpServletResponse response = mock(HttpServletResponse.class);
mockUberClassLoader(directory);
createServlet().doGet(request, response);
verify(response).setStatus(404);
}
private void mockUberClassLoader(Path... directories) throws MalformedURLException {
mockUberClassLoader(Arrays.asList(directories));
}
private void mockUberClassLoader(Collection<Path> directories) throws MalformedURLException {
List<URL> urls = new ArrayList<>();
for (Path directory : directories) {
urls.add(directory.toUri().toURL());
}
ClassLoader bootstrapLoader = ClassLoader.getSystemClassLoader().getParent();
URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), bootstrapLoader);
when(pluginLoader.getUberClassLoader()).thenReturn(classLoader);
}
@Test
void shouldFailWith500OnIOException(@TempDir Path directory) throws IOException {
stage(Stage.DEVELOPMENT);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn("/locales/de/plugins.json");
HttpServletResponse response = mock(HttpServletResponse.class);
mockResource(directory, "locales/de/plugins.json", "invalid json");
createServlet().doGet(request, response);
verify(response).setStatus(500);
}
private void mockResource(Path directory, String resourcePath, String content) throws IOException {
Path file = directory.resolve(resourcePath);
Files.createDirectories(file.getParent());
Files.write(file, content.getBytes(StandardCharsets.UTF_8));
mockUberClassLoader(directory);
}
private void stage(Stage stage) {
when(context.getStage()).thenReturn(stage);
}
@Test
void inDevelopmentStageShouldNotUseCache(@TempDir Path temp) throws IOException {
stage(Stage.DEVELOPMENT);
mockResources(temp, "locales/de/plugins.json");
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn("/locales/de/plugins.json");
HttpServletResponse response = mock(HttpServletResponse.class);
I18nServlet servlet = createServlet();
String json = doGetString(servlet, request, response);
assertJson(json);
verify(cache, never()).get(any());
}
@Nonnull
private I18nServlet createServlet() {
return new I18nServlet(context, pluginLoader, cacheManager);
}
private String doGetString(I18nServlet servlet, HttpServletRequest request, HttpServletResponse response) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(baos);
when(response.getWriter()).thenReturn(writer);
servlet.doGet(request, response);
writer.flush();
return baos.toString(StandardCharsets.UTF_8.name());
}
private void mockResources(Path directory, String resourcePath) throws IOException {
List<Path> directories = new ArrayList<>();
for (int i = 0; i < ALL_PLUGIN_JSON.length; i++) {
Path pluginDirectory = directory.resolve("plugin-" + i);
Path file = pluginDirectory.resolve(resourcePath);
Files.createDirectories(file.getParent());
Files.write(file, ALL_PLUGIN_JSON[i].getBytes(StandardCharsets.UTF_8));
directories.add(pluginDirectory);
}
mockUberClassLoader(directories);
}
@Test
void shouldGetFromCacheInProductionStage() throws IOException {
String path = "/locales/de/plugins.json";
stage(Stage.PRODUCTION);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn(path);
HttpServletResponse response = mock(HttpServletResponse.class);
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(GIT_PLUGIN_JSON);
when(cache.get(path)).thenReturn(jsonNode);
I18nServlet servlet = createServlet();
String json = doGetString(servlet, request, response);
assertThat(json).contains("scm-git-plugin").doesNotContain("scm-hg-plugin");
verifyHeaders(response);
}
@Test
void shouldStoreToCacheInProductionStage(@TempDir Path temp) throws IOException {
String path = "/locales/de/plugins.json";
mockResources(temp, "locales/de/plugins.json");
stage(Stage.PRODUCTION);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getServletPath()).thenReturn(path);
HttpServletResponse response = mock(HttpServletResponse.class);
I18nServlet servlet = createServlet();
String json = doGetString(servlet, request, response);
verify(cache).put(any(String.class), any(JsonNode.class));
verifyHeaders(response);
assertJson(json);
}
@Nested
class WithDefaultClassLoader {
@BeforeEach
void init() {
when(pluginLoader.getUberClassLoader()).thenReturn(I18nServletTest.class.getClassLoader());
}
@Test
void shouldCleanCacheOnRestartEvent() {
I18nServlet servlet = createServlet();
EventBus eventBus = new EventBus("forTestingOnly");
eventBus.register(servlet);
eventBus.post(RestartEventFactory.create(I18nServlet.class, "Restart to reload the plugin resources"));
verify(cache).clear();
}
}
}
private void verifyHeaders(HttpServletResponse response) {
verify(response).setCharacterEncoding("UTF-8");
verify(response).setContentType("application/json");