diff --git a/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/processings/ConvertGettersAndSettersToPropertyProcessing.kt b/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/processings/ConvertGettersAndSettersToPropertyProcessing.kt index a9c3fb0194c..53ccbf12183 100644 --- a/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/processings/ConvertGettersAndSettersToPropertyProcessing.kt +++ b/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/processings/ConvertGettersAndSettersToPropertyProcessing.kt @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.idea.references.KtSimpleNameReference import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.idea.references.readWriteAccess import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor import org.jetbrains.kotlin.idea.util.CommentSaver import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.idea.util.application.runReadAction @@ -42,6 +43,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered import org.jetbrains.kotlin.types.KotlinType @@ -383,15 +385,18 @@ private class ConvertGettersAndSettersToPropertyStatefulProcessing( val realProperty = group.firstIsInstanceOrNull() val name = realGetter?.name ?: realSetter?.name!! - val superDeclarationOwner = (realGetter?.function ?: realSetter?.function) + val functionDescriptor = (realGetter?.function ?: realSetter?.function) ?.resolveToDescriptorIfAny(resolutionFacade) + + if (functionDescriptor != null && isSamDescriptor(functionDescriptor)) return@mapNotNull null + + val superDeclarationOwner = functionDescriptor ?.overriddenDescriptors ?.firstOrNull() ?.findPsi() ?.safeAs() ?.containingClassOrObject - collectingState.propertyNameToSuperType[superDeclarationOwner to name] val type = collectingState.propertyNameToSuperType[superDeclarationOwner to name] ?: calculatePropertyType( realGetter?.function, @@ -408,6 +413,11 @@ private class ConvertGettersAndSettersToPropertyStatefulProcessing( } } + private fun KtClassOrObject.isSamDescriptor(functionDescriptor: FunctionDescriptor): Boolean { + val classDescriptor = descriptor as? ClassDescriptor ?: return false + return getSingleAbstractMethodOrNull(classDescriptor) == functionDescriptor + } + private fun List.filterGettersAndSetters( klass: KtClassOrObject, factory: KtPsiFactory diff --git a/nj2k/src/org/jetbrains/kotlin/nj2k/ConversionsRunner.kt b/nj2k/src/org/jetbrains/kotlin/nj2k/ConversionsRunner.kt index 2e732e60074..b9b9dd2ef9c 100644 --- a/nj2k/src/org/jetbrains/kotlin/nj2k/ConversionsRunner.kt +++ b/nj2k/src/org/jetbrains/kotlin/nj2k/ConversionsRunner.kt @@ -78,6 +78,8 @@ object ConversionsRunner { LiteralConversion(context), StaticMemberAccessConversion(context), RemoveRedundantQualifiersForCallsConversion(context), + FunctionalInterfacesConverter(context), + FilterImportsConversion(context), MoveInitBlocksToTheEndConversion(context), AddElementsInfoConversion(context) diff --git a/nj2k/src/org/jetbrains/kotlin/nj2k/NewJ2kConverterContext.kt b/nj2k/src/org/jetbrains/kotlin/nj2k/NewJ2kConverterContext.kt index f9f3f80431c..12be9b409a4 100644 --- a/nj2k/src/org/jetbrains/kotlin/nj2k/NewJ2kConverterContext.kt +++ b/nj2k/src/org/jetbrains/kotlin/nj2k/NewJ2kConverterContext.kt @@ -18,7 +18,8 @@ data class NewJ2kConverterContext( val inConversionContext: (PsiElement) -> Boolean, val importStorage: JKImportStorage, val elementsInfoStorage: JKElementInfoStorage, - val externalCodeProcessor: NewExternalCodeProcessing + val externalCodeProcessor: NewExternalCodeProcessing, + val functionalInterfaceConversionEnabled: Boolean ) : ConverterContext { val project: Project get() = converter.project diff --git a/nj2k/src/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverter.kt b/nj2k/src/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverter.kt index e274c2ea36c..62d976a0054 100644 --- a/nj2k/src/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverter.kt +++ b/nj2k/src/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverter.kt @@ -24,6 +24,7 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Computable import com.intellij.psi.* +import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl import org.jetbrains.kotlin.idea.project.languageVersionSettings import org.jetbrains.kotlin.j2k.* @@ -161,7 +162,8 @@ class NewJavaToKotlinConverter( inConversionContext, importStorage, JKElementInfoStorage(), - externalCodeProcessing + externalCodeProcessing, + languageVersion.supportsFeature(LanguageFeature.FunctionalInterfaceConversion) ) ConversionsRunner.doApply(asts.withIndex().mapNotNull { (i, ast) -> processor.updateState(i, 1, phaseDescription) diff --git a/nj2k/src/org/jetbrains/kotlin/nj2k/conversions/FunctionalInterfacesConverter.kt b/nj2k/src/org/jetbrains/kotlin/nj2k/conversions/FunctionalInterfacesConverter.kt new file mode 100644 index 00000000000..92fe62c007e --- /dev/null +++ b/nj2k/src/org/jetbrains/kotlin/nj2k/conversions/FunctionalInterfacesConverter.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.nj2k.conversions + +import org.jetbrains.kotlin.nj2k.NewJ2kConverterContext +import org.jetbrains.kotlin.nj2k.annotationByFqName +import org.jetbrains.kotlin.nj2k.tree.* + +private const val FUNCTIONAL_INTERFACE = "java.lang.FunctionalInterface" + +internal class FunctionalInterfacesConverter(context: NewJ2kConverterContext) : RecursiveApplicableConversionBase(context) { + override fun applyToElement(element: JKTreeElement): JKTreeElement { + if (!context.functionalInterfaceConversionEnabled) return recurse(element) + if (element !is JKClass) return recurse(element) + if (element.classKind != JKClass.ClassKind.INTERFACE) return recurse(element) + if (element.inheritance.extends.isNotEmpty()) return recurse(element) + + val functionalInterfaceMarker = element.annotationList.annotationByFqName(FUNCTIONAL_INTERFACE) ?: return recurse(element) + + val samMethod = element.classBody.declarations.filterIsInstance().singleOrNull { it.block is JKBodyStub } + if (samMethod == null || samMethod.typeParameterList.typeParameters.isNotEmpty()) return recurse(element) + + element.otherModifierElements += JKOtherModifierElement(OtherModifier.FUN) + element.annotationList.annotations -= functionalInterfaceMarker + + return recurse(element) + } +} diff --git a/nj2k/src/org/jetbrains/kotlin/nj2k/tree/modifiers.kt b/nj2k/src/org/jetbrains/kotlin/nj2k/tree/modifiers.kt index 6d05ea5157a..26d26bd3a36 100644 --- a/nj2k/src/org/jetbrains/kotlin/nj2k/tree/modifiers.kt +++ b/nj2k/src/org/jetbrains/kotlin/nj2k/tree/modifiers.kt @@ -34,6 +34,7 @@ enum class OtherModifier(override val text: String) : Modifier { SUSPEND("suspend"), TAILREC("tailrec"), VARARG("vararg"), + FUN("fun"), NATIVE("native"), STATIC("static"), @@ -136,8 +137,8 @@ val JKModifierElement.modifier: Modifier inline fun JKModifiersListOwner.forEachModifier(action: (JKModifierElement) -> Unit) { safeAs()?.visibilityElement?.let(action) - safeAs()?.otherModifierElements?.forEach(action) safeAs()?.modalityElement?.let(action) + safeAs()?.otherModifierElements?.forEach(action) safeAs()?.mutabilityElement?.let(action) } diff --git a/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.java b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.java new file mode 100644 index 00000000000..96cd146e613 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.java @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK + +@FunctionalInterface +public interface MyRunnable { + int getResult(); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.kt b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.kt new file mode 100644 index 00000000000..e1312ab1b96 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.kt @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +fun interface MyRunnable { + fun getResult(): Int +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.java b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.java new file mode 100644 index 00000000000..f8474eef778 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.java @@ -0,0 +1,10 @@ +// RUNTIME_WITH_FULL_JDK + +@FunctionalInterface +public interface MyRunnable { + int getResult(); + + default int getDoubleResult() { + return getResult() * 2; + } +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.kt b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.kt new file mode 100644 index 00000000000..128ec4fff96 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.kt @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK +fun interface MyRunnable { + fun getResult(): Int + val doubleResult: Int + get() = getResult() * 2 +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.java b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.java new file mode 100644 index 00000000000..f88dfa5edbf --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.java @@ -0,0 +1,5 @@ +// RUNTIME_WITH_FULL_JDK + +public interface MyRunnable { + int getResult(); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.kt b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.kt new file mode 100644 index 00000000000..c98892438bc --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.kt @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +interface MyRunnable { + val result: Int +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.java b/nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.java new file mode 100644 index 00000000000..c003a099c46 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.java @@ -0,0 +1,18 @@ +// RUNTIME_WITH_FULL_JDK + +// we intentionally do not convert interface to Kotlin fun interface +// if it inherits from some other interface, because it is hard to deal +// with default methods which were already converted to properties +// (and in kotlin fun interface cannot have abstract property) + +public interface MyRunnableBase { + default int getValue() { + return 0; + } +} + +@FunctionalInterface +public interface MyRunnable extends MyRunnableBase { + @Override + int getValue(); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.kt b/nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.kt new file mode 100644 index 00000000000..702e22e868b --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.kt @@ -0,0 +1,14 @@ +// RUNTIME_WITH_FULL_JDK +// we intentionally do not convert interface to Kotlin fun interface +// if it inherits from some other interface, because it is hard to deal +// with default methods which were already converted to properties +// (and in kotlin fun interface cannot have abstract property) +interface MyRunnableBase { + val value: Int + get() = 0 +} + +@FunctionalInterface +interface MyRunnable : MyRunnableBase { + abstract override val value: Int +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.java b/nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.java new file mode 100644 index 00000000000..20785113353 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.java @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK + +@FunctionalInterface +public interface MyRunnable { + void run(); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.kt b/nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.kt new file mode 100644 index 00000000000..9fd5cc3cf0f --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.kt @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +fun interface MyRunnable { + fun run() +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.java b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.java new file mode 100644 index 00000000000..0123febf0a2 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.java @@ -0,0 +1,3 @@ +public interface MyRunnable { + void run(); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.kt b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.kt new file mode 100644 index 00000000000..2970a8ba2e8 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.kt @@ -0,0 +1,3 @@ +interface MyRunnable { + fun run() +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.java b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.java new file mode 100644 index 00000000000..7b93ae47268 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.java @@ -0,0 +1,7 @@ +// RUNTIME_WITH_FULL_JDK +// COMPILER_ARGUMENTS: -XXLanguage:-FunctionalInterfaceConversion + +@FunctionalInterface +public interface MyRunnable { + void run(); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.kt b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.kt new file mode 100644 index 00000000000..ec2013324d5 --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.kt @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK +// COMPILER_ARGUMENTS: -XXLanguage:-FunctionalInterfaceConversion +@FunctionalInterface +interface MyRunnable { + fun run() +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.java b/nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.java new file mode 100644 index 00000000000..df406c7745b --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.java @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK + +@FunctionalInterface +public interface MyRunnable { + void process(T t); +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.kt b/nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.kt new file mode 100644 index 00000000000..51b231de24a --- /dev/null +++ b/nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.kt @@ -0,0 +1,5 @@ +// RUNTIME_WITH_FULL_JDK +@FunctionalInterface +interface MyRunnable { + fun process(t: T) +} \ No newline at end of file diff --git a/nj2k/tests/org/jetbrains/kotlin/nj2k/AbstractNewJavaToKotlinConverterSingleFileTest.kt b/nj2k/tests/org/jetbrains/kotlin/nj2k/AbstractNewJavaToKotlinConverterSingleFileTest.kt index 470f39368a1..ab96fe46ed7 100644 --- a/nj2k/tests/org/jetbrains/kotlin/nj2k/AbstractNewJavaToKotlinConverterSingleFileTest.kt +++ b/nj2k/tests/org/jetbrains/kotlin/nj2k/AbstractNewJavaToKotlinConverterSingleFileTest.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.nj2k import com.intellij.openapi.project.Project import com.intellij.psi.codeStyle.JavaCodeStyleSettings import org.jetbrains.kotlin.idea.j2k.IdeaJavaToKotlinServices +import org.jetbrains.kotlin.idea.test.withCustomCompilerOptions import org.jetbrains.kotlin.j2k.AbstractJavaToKotlinConverterSingleFileTest import org.jetbrains.kotlin.j2k.ConverterSettings import org.jetbrains.kotlin.nj2k.postProcessing.NewJ2kPostProcessor @@ -16,15 +17,18 @@ import java.io.File abstract class AbstractNewJavaToKotlinConverterSingleFileTest : AbstractJavaToKotlinConverterSingleFileTest() { override fun doTest(javaPath: String) { - val directory = File(javaPath).parentFile - val expectedFileName = "${File(javaPath).nameWithoutExtension}.external" - val expectedFiles = directory.listFiles { _, name -> - name == "$expectedFileName.kt" || name == "$expectedFileName.java" - }!!.filterNotNull() - for (expectedFile in expectedFiles) { - addFile(expectedFile, dirName = null) + val javaFile = File(javaPath) + withCustomCompilerOptions(javaFile.readText(), project, module) { + val directory = javaFile.parentFile + val expectedFileName = "${javaFile.nameWithoutExtension}.external" + val expectedFiles = directory.listFiles { _, name -> + name == "$expectedFileName.kt" || name == "$expectedFileName.java" + }!!.filterNotNull() + for (expectedFile in expectedFiles) { + addFile(expectedFile, dirName = null) + } + super.doTest(javaPath) } - super.doTest(javaPath) } override fun compareResults(expectedFile: File, actual: String) { diff --git a/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java b/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java index 359335b9f30..03ef1cb7730 100644 --- a/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java +++ b/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java @@ -2583,6 +2583,59 @@ public class NewJavaToKotlinConverterSingleFileTestGenerated extends AbstractNew } } + @TestMetadata("nj2k/testData/newJ2k/functionalInterfaces") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class FunctionalInterfaces extends AbstractNewJavaToKotlinConverterSingleFileTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("AbstractGetter1.java") + public void testAbstractGetter1() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter1.java"); + } + + @TestMetadata("AbstractGetter2.java") + public void testAbstractGetter2() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/AbstractGetter2.java"); + } + + @TestMetadata("AbstractGetterNoFunctionalInterfaceAnnotation.java") + public void testAbstractGetterNoFunctionalInterfaceAnnotation() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/AbstractGetterNoFunctionalInterfaceAnnotation.java"); + } + + public void testAllFilesPresentInFunctionalInterfaces() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("nj2k/testData/newJ2k/functionalInterfaces"), Pattern.compile("^([^\\.]+)\\.java$"), null, true); + } + + @TestMetadata("InterfacesHierarchy.java") + public void testInterfacesHierarchy() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/InterfacesHierarchy.java"); + } + + @TestMetadata("MyRunnable.java") + public void testMyRunnable() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/MyRunnable.java"); + } + + @TestMetadata("NoFunctionalInterfaceAnnotation.java") + public void testNoFunctionalInterfaceAnnotation() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceAnnotation.java"); + } + + @TestMetadata("NoFunctionalInterfaceConversionEnabled.java") + public void testNoFunctionalInterfaceConversionEnabled() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/NoFunctionalInterfaceConversionEnabled.java"); + } + + @TestMetadata("SamMethodWithGenerics.java") + public void testSamMethodWithGenerics() throws Exception { + runTest("nj2k/testData/newJ2k/functionalInterfaces/SamMethodWithGenerics.java"); + } + } + @TestMetadata("nj2k/testData/newJ2k/identifier") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)