diff --git a/utbot-core/build.gradle b/utbot-core/build.gradle index 271a7a5651..ef4c5020e3 100644 --- a/utbot-core/build.gradle +++ b/utbot-core/build.gradle @@ -7,6 +7,8 @@ apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" dependencies { implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '5.5.0' + + testImplementation group: 'junit', name: 'junit', version: junit4_version } shadowJar { diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt index c692c54fdb..13ab683dca 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt @@ -5,6 +5,7 @@ import java.lang.reflect.AccessibleObject import java.lang.reflect.Field import java.lang.reflect.Modifier import sun.misc.Unsafe +import java.lang.reflect.Method object Reflection { val unsafe: Unsafe @@ -15,15 +16,18 @@ object Reflection { unsafe = f.get(null) as Unsafe } + private val getDeclaredFields0Method: Method = + Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.java).apply { + isAccessible = true + } + + @Suppress("UNCHECKED_CAST") + private val fields: Array = + getDeclaredFields0Method.invoke(Field::class.java, false) as Array // TODO: works on JDK 8-17. Doesn't work on JDK 18 - private val modifiersField: Field = run { - val getDeclaredFields0 = Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.java) - getDeclaredFields0.isAccessible = true - @Suppress("UNCHECKED_CAST") - val fields = getDeclaredFields0.invoke(Field::class.java, false) as Array + private val modifiersField: Field = fields.first { it.name == "modifiers" } - } init { modifiersField.isAccessible = true @@ -36,8 +40,9 @@ object Reflection { inline fun AccessibleObject.withAccessibility(block: () -> R): R { val prevAccessibility = isAccessible + isAccessible = true + try { - isAccessible = true return block() } finally { isAccessible = prevAccessibility @@ -47,15 +52,24 @@ inline fun AccessibleObject.withAccessibility(block: () -> R): R { /** * Executes given [block] with removed final modifier and restores it back after execution. * Also sets `isAccessible` to `true` and restores it back. + * + * Please note, that this function doesn't guarantee that reflection calls in the [block] always succeed. The problem is + * that prior calls to reflection may result in caching internal FieldAccessor field which is not suitable for setting + * [this]. But if you didn't call reflection previously, this function should help. + * + * Also note, that primitive static final fields may be inlined, so may not be possible to change. */ -inline fun Field.withRemovedFinalModifier(block: () -> R): R { +inline fun Field.withAccessibility(block: () -> R): R { val prevModifiers = modifiers + val prevAccessibility = isAccessible + + isAccessible = true setModifiers(this, modifiers and Modifier.FINAL.inv()) - this.withAccessibility { - try { - return block() - } finally { - setModifiers(this, prevModifiers) - } + + try { + return block() + } finally { + isAccessible = prevAccessibility + setModifiers(this, prevModifiers) } } \ No newline at end of file diff --git a/utbot-core/src/test/java/org/utbot/examples/reflection/ClassWithDifferentModifiers.java b/utbot-core/src/test/java/org/utbot/examples/reflection/ClassWithDifferentModifiers.java new file mode 100644 index 0000000000..1520c271b7 --- /dev/null +++ b/utbot-core/src/test/java/org/utbot/examples/reflection/ClassWithDifferentModifiers.java @@ -0,0 +1,27 @@ +package org.utbot.examples.reflection; + +public class ClassWithDifferentModifiers { + @SuppressWarnings({"All"}) + private int privateField; + + private static final Wrapper privateStaticFinalField = new Wrapper(1); + + public ClassWithDifferentModifiers() { + privateField = 0; + } + + int packagePrivateMethod() { + return 1; + } + + private int privateMethod() { + return 1; + } + + public static class Wrapper { + public int x; + public Wrapper(int x) { + this.x = x; + } + } +} diff --git a/utbot-core/src/test/kotlin/org/utbot/common/ReflectionUtilTest.kt b/utbot-core/src/test/kotlin/org/utbot/common/ReflectionUtilTest.kt new file mode 100644 index 0000000000..3452ced216 --- /dev/null +++ b/utbot-core/src/test/kotlin/org/utbot/common/ReflectionUtilTest.kt @@ -0,0 +1,117 @@ +package org.utbot.common + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.utbot.examples.reflection.ClassWithDifferentModifiers +import org.utbot.examples.reflection.ClassWithDifferentModifiers.Wrapper + +class ReflectionUtilTest { + private val testedClass = ClassWithDifferentModifiers::class.java + + @Test + fun testPackagePrivateInvoke() { + val method = testedClass.declaredMethods.first { it.name == "packagePrivateMethod"} + val instance = ClassWithDifferentModifiers() + + method.apply { + withAccessibility { + assertEquals(1, invoke(instance)) + } + } + } + + @Test + fun testPrivateInvoke() { + val method = testedClass.declaredMethods.first { it.name == "privateMethod"} + val instance = ClassWithDifferentModifiers() + + method.apply { + withAccessibility { + assertEquals(1, invoke(instance)) + } + } + + } + + @Test + fun testPrivateFieldSetting() { + val field = testedClass.declaredFields.first { it.name == "privateField" } + val instance = ClassWithDifferentModifiers() + + field.apply { + withAccessibility { + set(instance, 0) + } + } + } + + @Test + fun testPrivateFieldGetting() { + val field = testedClass.declaredFields.first { it.name == "privateField" } + val instance = ClassWithDifferentModifiers() + + field.apply { + withAccessibility { + assertEquals(0, get(instance)) + } + } + + } + + @Test + fun testPrivateFieldGettingAfterSetting() { + val field = testedClass.declaredFields.first { it.name == "privateField" } + val instance = ClassWithDifferentModifiers() + + + field.apply { + withAccessibility { + set(instance, 1) + } + + withAccessibility { + assertEquals(1, get(instance)) + } + } + } + + @Test + fun testPrivateStaticFinalFieldSetting() { + val field = testedClass.declaredFields.first { it.name == "privateStaticFinalField" } + + field.apply { + withAccessibility { + set(null, Wrapper(2)) + } + } + } + + @Test + fun testPrivateStaticFinalFieldGetting() { + val field = testedClass.declaredFields.first { it.name == "privateStaticFinalField" } + + field.apply { + withAccessibility { + val value = get(null) as? Wrapper + assertNotNull(value) + } + } + } + + @Test + fun testPrivateStaticFinalFieldGettingAfterSetting() { + val field = testedClass.declaredFields.first { it.name == "privateStaticFinalField" } + + field.apply { + withAccessibility { + set(null, Wrapper(3)) + } + + withAccessibility { + val value = (get(null) as? Wrapper)?.x + assertEquals(3, value) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt index 48e1a2d564..342c867a15 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt @@ -1,6 +1,6 @@ package org.utbot.framework.concrete -import org.utbot.common.withRemovedFinalModifier +import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.util.signature import org.utbot.instrumentation.instrumentation.mock.MockConfig import java.lang.reflect.Field @@ -36,7 +36,7 @@ class MethodMockController( isMockField = clazz.declaredFields.firstOrNull { it.name == MockConfig.IS_MOCK_FIELD + id } ?: error("No field ${MockConfig.IS_MOCK_FIELD + id} in $clazz") - isMockField.withRemovedFinalModifier { + isMockField.withAccessibility { isMockField.set(instance, true) } @@ -46,7 +46,7 @@ class MethodMockController( } override fun close() { - isMockField.withRemovedFinalModifier { + isMockField.withAccessibility { isMockField.set(instance, false) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt index 29d4d51139..5c587a48ee 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt @@ -3,7 +3,6 @@ package org.utbot.framework.concrete import org.utbot.common.StopWatch import org.utbot.common.ThreadBasedExecutor import org.utbot.common.withAccessibility -import org.utbot.common.withRemovedFinalModifier import org.utbot.framework.UtSettings import org.utbot.framework.assemble.AssembleModelGenerator import org.utbot.framework.plugin.api.Coverage @@ -260,7 +259,7 @@ object UtExecutionInstrumentation : Instrumentation { try { staticFields.forEach { (fieldId, value) -> fieldId.field.run { - withRemovedFinalModifier { + withAccessibility { savedFields[fieldId] = get(null) set(null, value) } @@ -270,7 +269,7 @@ object UtExecutionInstrumentation : Instrumentation { } finally { savedFields.forEach { (fieldId, value) -> fieldId.field.run { - withRemovedFinalModifier { + withAccessibility { set(null, value) } } diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt index 0f5944c3b7..0d7c785153 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt @@ -1,6 +1,6 @@ package org.utbot.instrumentation.instrumentation -import org.utbot.common.withRemovedFinalModifier +import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.util.field import org.utbot.instrumentation.util.StaticEnvironment import java.lang.reflect.Field @@ -47,7 +47,7 @@ class InvokeWithStaticsInstrumentation : Instrumentation> { staticEnvironment?.run { listOfFields.forEach { (fieldId, value) -> fieldId.field.run { - withRemovedFinalModifier { + withAccessibility { set(null, value) } } @@ -75,14 +75,14 @@ class InvokeWithStaticsInstrumentation : Instrumentation> { init { val staticFields = clazz.declaredFields .filter { checkField(it) } // TODO: think on this - .associate { it.name to it.withRemovedFinalModifier { it.get(null) } } + .associate { it.name to it.withAccessibility { it.get(null) } } savedFields = staticFields } fun restore() { clazz.declaredFields .filter { checkField(it) } - .forEach { it.withRemovedFinalModifier { it.set(null, savedFields[it.name]) } } + .forEach { it.withAccessibility { it.set(null, savedFields[it.name]) } } } } } diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt index 5af671fba5..afb04d9f05 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt @@ -1,6 +1,6 @@ package org.utbot.instrumentation.instrumentation.coverage -import org.utbot.common.withRemovedFinalModifier +import org.utbot.common.withAccessibility import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.Settings import org.utbot.instrumentation.instrumentation.ArgumentList @@ -43,7 +43,7 @@ object CoverageInstrumentation : Instrumentation> { val visitedLinesField = clazz.fields.firstOrNull { it.name == probesFieldName } ?: throw NoProbesArrayException(clazz, Settings.PROBES_ARRAY_NAME) - return visitedLinesField.withRemovedFinalModifier { + return visitedLinesField.withAccessibility { invokeWithStatics.invoke(clazz, methodSignature, arguments, parameters) } } @@ -57,7 +57,7 @@ object CoverageInstrumentation : Instrumentation> { val visitedLinesField = clazz.fields.firstOrNull { it.name == probesFieldName } ?: throw NoProbesArrayException(clazz, Settings.PROBES_ARRAY_NAME) - return visitedLinesField.withRemovedFinalModifier { + return visitedLinesField.withAccessibility { val visitedLines = visitedLinesField.get(null) as? BooleanArray ?: throw CastProbesArrayException() @@ -131,5 +131,5 @@ fun ConcreteExecutor, CoverageInstrumentation>.collectCoverage(clazz: is Protocol.ExceptionInChildProcess -> throw ChildProcessError(it.exception) else -> throw UnexpectedCommand(it) } - }!! + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt index b6f1472771..e7b10fc982 100644 --- a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt @@ -1,6 +1,6 @@ package org.utbot.instrumentation.examples.mock -import org.utbot.common.withRemovedFinalModifier +import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.util.signature import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor @@ -58,7 +58,7 @@ class MockHelper( val isMockField = instrumentedClazz.getDeclaredField(MockConfig.IS_MOCK_FIELD + methodId) MockGetter.updateMocks(instance, method, mockedValues) - return isMockField.withRemovedFinalModifier { + return isMockField.withAccessibility { isMockField.set(instance, true) val res = block(instance) isMockField.set(instance, false) diff --git a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/TestConstructorMock.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/TestConstructorMock.kt index bb39ea4a41..22c3f3e255 100644 --- a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/TestConstructorMock.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/TestConstructorMock.kt @@ -1,6 +1,6 @@ package org.utbot.instrumentation.examples.mock -import org.utbot.common.withRemovedFinalModifier +import org.utbot.common.withAccessibility import org.utbot.instrumentation.samples.mock.ClassForMockConstructor import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals @@ -11,11 +11,11 @@ import org.junit.jupiter.api.Test class TestConstructorMock { private fun checkFields(instance: Any, x: Int, s: String?) { val xField = instance::class.java.getDeclaredField("x") - xField.withRemovedFinalModifier { + xField.withAccessibility { assertEquals(x, xField.getInt(instance)) } val sField = instance::class.java.getDeclaredField("s") - sField.withRemovedFinalModifier { + sField.withAccessibility { assertEquals(s, sField.get(instance)) } }