gson-comments/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java

244 lines
8.0 KiB
Java

/*
* Copyright (C) 2023 Google Inc.
*
* 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 com.google.gson.it;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/**
* Integration test verifying behavior of shrunken and obfuscated JARs.
*/
@RunWith(Parameterized.class)
public class ShrinkingIT {
// These JAR files are prepared by the Maven build
public static final Path PROGUARD_RESULT_PATH = Paths.get("target/proguard-output.jar");
public static final Path R8_RESULT_PATH = Paths.get("target/r8-output.jar");
@Parameters(name = "{index}: {0}")
public static List<Path> jarsToTest() {
return Arrays.asList(PROGUARD_RESULT_PATH, R8_RESULT_PATH);
}
@Parameter
public Path jarToTest;
@Before
public void verifyJarExists() {
if (!Files.isRegularFile(jarToTest)) {
fail("JAR file " + jarToTest + " does not exist; run this test with `mvn clean verify`");
}
}
@FunctionalInterface
interface TestAction {
void run(Class<?> c) throws Exception;
}
private void runTest(String className, TestAction testAction) throws Exception {
// Use bootstrap class loader; load all custom classes from JAR and not
// from dependencies of this test
ClassLoader classLoader = null;
// Load the shrunken and obfuscated JARs with a separate class loader, then load
// the main test class from it and let the test action invoke its test methods
try (URLClassLoader loader = new URLClassLoader(new URL[] {jarToTest.toUri().toURL()}, classLoader)) {
Class<?> c = loader.loadClass(className);
testAction.run(c);
}
}
@Test
public void test() throws Exception {
StringBuilder output = new StringBuilder();
runTest("com.example.Main", c -> {
Method m = c.getMethod("runTests", BiConsumer.class);
m.invoke(null, (BiConsumer<String, String>) (name, content) -> output.append(name + "\n" + content + "\n===\n"));
});
assertThat(output.toString()).isEqualTo(String.join("\n",
"Write: TypeToken anonymous",
"[",
" {",
" \"custom\": 1",
" }",
"]",
"===",
"Read: TypeToken anonymous",
"[ClassWithAdapter[3]]",
"===",
"Write: TypeToken manual",
"[",
" {",
" \"custom\": 1",
" }",
"]",
"===",
"Read: TypeToken manual",
"[ClassWithAdapter[3]]",
"===",
"Write: Named fields",
"{",
" \"myField\": 2,",
" \"notAccessedField\": -1",
"}",
"===",
"Read: Named fields",
"3",
"===",
"Write: SerializedName",
"{",
" \"myField\": 2,",
" \"notAccessed\": -1",
"}",
"===",
"Read: SerializedName",
"3",
"===",
"Read: No JDK Unsafe; initial constructor value",
"-3",
"===",
"Read: No JDK Unsafe; custom value",
"3",
"===",
"Write: Enum",
"\"FIRST\"",
"===",
"Read: Enum",
"SECOND",
"===",
"Write: Enum SerializedName",
"\"one\"",
"===",
"Read: Enum SerializedName",
"SECOND",
"===",
"Write: @Expose",
"{\"i\":0}",
"===",
"Write: Version annotations",
"{\"i1\":0,\"i4\":0}",
"===",
"Write: JsonAdapter on fields",
"{",
" \"f\": \"adapter-null\",",
" \"f1\": \"adapter-1\",",
" \"f2\": \"factory-2\",",
" \"f3\": \"serializer-3\",",
// For f4 only a JsonDeserializer is registered, so serialization falls back to reflection
" \"f4\": {",
" \"s\": \"4\"",
" }",
"}",
"===",
"Read: JsonAdapter on fields",
// For f3 only a JsonSerializer is registered, so for deserialization value is read as is using reflection
"ClassWithJsonAdapterAnnotation[f1=adapter-1, f2=factory-2, f3=3, f4=deserializer-4]",
"===",
"Read: Generic TypeToken",
"{t=read-1}",
"===",
"Read: Using Generic",
"{g={t=read-1}}",
"===",
"Read: Using Generic TypeToken",
"{g={t=read-1}}",
"===",
""
));
}
@Test
public void testDefaultConstructor() throws Exception {
runTest("com.example.DefaultConstructorMain", c -> {
Method m = c.getMethod("runTest");
if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
Object result = m.invoke(null);
assertThat(result).isEqualTo("value");
} else {
// R8 performs more aggressive optimizations
Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
"Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: com.example.DefaultConstructorMain$TestClass"
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"
);
}
});
}
@Test
public void testDefaultConstructorNoJdkUnsafe() throws Exception {
runTest("com.example.DefaultConstructorMain", c -> {
Method m = c.getMethod("runTestNoJdkUnsafe");
if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
Object result = m.invoke(null);
assertThat(result).isEqualTo("value");
} else {
// R8 performs more aggressive optimizations
Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
"Unable to create instance of class com.example.DefaultConstructorMain$TestClassNotAbstract;"
+ " usage of JDK Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for this type,"
+ " adding a no-args constructor, or enabling usage of JDK Unsafe may fix this problem. Or adjust"
+ " your R8 configuration to keep the no-args constructor of the class."
);
}
});
}
@Test
public void testNoDefaultConstructor() throws Exception {
runTest("com.example.DefaultConstructorMain", c -> {
Method m = c.getMethod("runTestNoDefaultConstructor");
if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
Object result = m.invoke(null);
assertThat(result).isEqualTo("value");
} else {
// R8 performs more aggressive optimizations
Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
"Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: com.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor"
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"
);
}
});
}
}