Extended version of AbstractWriteSignatureTest:
- support multiple expectations in file - support field signature expectations - report failures in a more usable format
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
public val list: List<String> = throw Exception()
|
||||
public val mutList: MutableList<String> = throw Exception()
|
||||
|
||||
// field: _DefaultPackage::list
|
||||
// jvm signature: Ljava/util/List;
|
||||
// generic signature: Ljava/util/List<+Ljava/lang/String;>;
|
||||
|
||||
// field: _DefaultPackage::mutList
|
||||
// jvm signature: Ljava/util/List;
|
||||
// generic signature: Ljava/util/List<Ljava/lang/String;>;
|
||||
@@ -1,209 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2015 JetBrains s.r.o.
|
||||
*
|
||||
* 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.jetbrains.kotlin.jvm.compiler;
|
||||
|
||||
import com.google.common.io.Closeables;
|
||||
import com.google.common.io.Files;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.backend.common.output.OutputFileCollection;
|
||||
import org.jetbrains.kotlin.cli.common.output.outputUtils.OutputUtilsPackage;
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment;
|
||||
import org.jetbrains.kotlin.codegen.GenerationUtils;
|
||||
import org.jetbrains.kotlin.psi.JetFile;
|
||||
import org.jetbrains.kotlin.test.JetTestUtils;
|
||||
import org.jetbrains.kotlin.test.TestCaseWithTmpdir;
|
||||
import org.jetbrains.org.objectweb.asm.ClassReader;
|
||||
import org.jetbrains.org.objectweb.asm.ClassVisitor;
|
||||
import org.jetbrains.org.objectweb.asm.MethodVisitor;
|
||||
import org.jetbrains.org.objectweb.asm.Opcodes;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Test correctness of written JVM signature
|
||||
*
|
||||
* @see CompileJavaAgainstKotlinTestGenerated
|
||||
*/
|
||||
public abstract class AbstractWriteSignatureTest extends TestCaseWithTmpdir {
|
||||
|
||||
private KotlinCoreEnvironment jetCoreEnvironment;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
jetCoreEnvironment = JetTestUtils.createEnvironmentWithMockJdkAndIdeaAnnotations(myTestRootDisposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
jetCoreEnvironment = null;
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
protected void doTest(String ktFileName) throws Exception {
|
||||
File ktFile = new File(ktFileName);
|
||||
String text = FileUtil.loadFile(ktFile, true);
|
||||
|
||||
JetFile psiFile = JetTestUtils.createFile(ktFile.getName(), text, jetCoreEnvironment.getProject());
|
||||
|
||||
OutputFileCollection outputFiles = GenerationUtils.compileFileGetClassFileFactoryForTest(psiFile);
|
||||
|
||||
OutputUtilsPackage.writeAllTo(outputFiles, tmpdir);
|
||||
|
||||
Disposer.dispose(myTestRootDisposable);
|
||||
|
||||
Expectation expectation = parseExpectations(ktFile);
|
||||
|
||||
ActualSignature actualSignature = readSignature(expectation.className, expectation.methodName);
|
||||
|
||||
String template =
|
||||
"jvm signature: %s\n" +
|
||||
"generic signature: %s\n" +
|
||||
"";
|
||||
|
||||
String expected = String.format(template, expectation.jvmSignature, expectation.genericSignature);
|
||||
String actual = String.format(template, actualSignature.jvmSignature, actualSignature.genericSignature);
|
||||
|
||||
Assert.assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
private static class ActualSignature {
|
||||
private final String jvmSignature;
|
||||
private final String genericSignature;
|
||||
|
||||
private ActualSignature(@NotNull String jvmSignature, @Nullable String genericSignature) {
|
||||
this.jvmSignature = jvmSignature;
|
||||
this.genericSignature = genericSignature;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ActualSignature readSignature(@NotNull String className, @Nullable final String methodName) throws Exception {
|
||||
// ugly unreadable code begin
|
||||
FileInputStream classInputStream = new FileInputStream(tmpdir + "/" + className.replace('.', '/') + ".class");
|
||||
try {
|
||||
class Visitor extends ClassVisitor {
|
||||
ActualSignature readSignature;
|
||||
|
||||
public Visitor() {
|
||||
super(Opcodes.ASM5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||
if (methodName == null) {
|
||||
Assert.assertNull(readSignature);
|
||||
readSignature = new ActualSignature(name, signature);
|
||||
}
|
||||
super.visit(version, access, name, signature, superName, interfaces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, final String desc, final String signature, String[] exceptions) {
|
||||
if (name.equals(methodName)) {
|
||||
|
||||
return new MethodVisitor(Opcodes.ASM5) {
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
Assert.assertNull(readSignature);
|
||||
readSignature = new ActualSignature(desc, signature);
|
||||
}
|
||||
};
|
||||
}
|
||||
return super.visitMethod(access, name, desc, signature, exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
Visitor visitor = new Visitor();
|
||||
|
||||
new ClassReader(classInputStream).accept(visitor,
|
||||
ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
|
||||
|
||||
Assert.assertNotNull("method not found: " + className + "::" + methodName, visitor.readSignature);
|
||||
|
||||
return visitor.readSignature;
|
||||
}
|
||||
finally {
|
||||
Closeables.closeQuietly(classInputStream);
|
||||
}
|
||||
// ugly unreadable code end
|
||||
}
|
||||
|
||||
private static class Expectation {
|
||||
private final String className;
|
||||
@Nullable
|
||||
private final String methodName;
|
||||
private final String jvmSignature;
|
||||
private final String genericSignature;
|
||||
|
||||
private Expectation(
|
||||
@NotNull String className, @Nullable String methodName,
|
||||
@NotNull String jvmSignature, @Nullable String genericSignature
|
||||
) {
|
||||
this.className = className;
|
||||
this.methodName = methodName;
|
||||
this.jvmSignature = jvmSignature;
|
||||
this.genericSignature = genericSignature;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static final Pattern classPattern = Pattern.compile("^// class: *(.*)");
|
||||
private static final Pattern methodPattern = Pattern.compile("^// method: *(.*)::(.*?) *(//.*)?");
|
||||
private static final Pattern jvmSignaturePattern = Pattern.compile("^// jvm signature: *(.+?) *(//.*)?");
|
||||
private static final Pattern genericSignaturePattern = Pattern.compile("^// generic signature: *(.+?) *(//.*)?");
|
||||
|
||||
private Expectation parseExpectations(File ktFile) throws IOException {
|
||||
List<String> lines = Files.readLines(ktFile, Charset.forName("utf-8"));
|
||||
for (int i = 0; i < lines.size() - 2; ++i) {
|
||||
String line = lines.get(i);
|
||||
Matcher methodMatcher = methodPattern.matcher(line);
|
||||
Matcher classMatcher = classPattern.matcher(line);
|
||||
boolean isMethod = methodMatcher.matches();
|
||||
boolean isClass = classMatcher.matches();
|
||||
Matcher matcher = isMethod ? methodMatcher : classMatcher;
|
||||
if (isMethod || isClass) {
|
||||
Matcher jvmSignatureMatcher = jvmSignaturePattern.matcher(lines.get(i + 1));
|
||||
Matcher genericSignatureMatcher = genericSignaturePattern.matcher(lines.get(i + 2));
|
||||
if (!jvmSignatureMatcher.matches() || !genericSignatureMatcher.matches()) {
|
||||
throw new AssertionError("'method:' must be followed ... bla bla ... use the source luke");
|
||||
}
|
||||
|
||||
String className = matcher.group(1);
|
||||
String methodName = isMethod ? matcher.group(2) : null;
|
||||
|
||||
String jvmSignature = jvmSignatureMatcher.group(1);
|
||||
String genericSignature = genericSignatureMatcher.group(1);
|
||||
if (genericSignature.equals("null")) {
|
||||
genericSignature = null;
|
||||
}
|
||||
return new Expectation(className, methodName, jvmSignature, genericSignature);
|
||||
}
|
||||
}
|
||||
throw new AssertionError("test instructions not found in " + ktFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2010-2015 JetBrains s.r.o.
|
||||
*
|
||||
* 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.jetbrains.kotlin.jvm.compiler
|
||||
|
||||
import com.google.common.io.Closeables
|
||||
import com.google.common.io.Files
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import org.jetbrains.kotlin.cli.common.output.outputUtils.writeAllTo
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.codegen.GenerationUtils
|
||||
import org.jetbrains.kotlin.test.JetTestUtils
|
||||
import org.jetbrains.kotlin.test.TestCaseWithTmpdir
|
||||
import org.jetbrains.kotlin.utils.join
|
||||
import org.jetbrains.org.objectweb.asm.*
|
||||
import org.junit.Assert
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
import java.util.regex.MatchResult
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.text.Regex
|
||||
|
||||
|
||||
public abstract class AbstractWriteSignatureTest : TestCaseWithTmpdir() {
|
||||
private var jetCoreEnvironment: KotlinCoreEnvironment? = null
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
jetCoreEnvironment = JetTestUtils.createEnvironmentWithMockJdkAndIdeaAnnotations(myTestRootDisposable)
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
jetCoreEnvironment = null
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
protected fun doTest(ktFileName: String) {
|
||||
val ktFile = File(ktFileName)
|
||||
val text = FileUtil.loadFile(ktFile, true)
|
||||
|
||||
val psiFile = JetTestUtils.createFile(ktFile.getName(), text, jetCoreEnvironment!!.project)
|
||||
|
||||
val outputFiles = GenerationUtils.compileFileGetClassFileFactoryForTest(psiFile)
|
||||
|
||||
outputFiles.writeAllTo(tmpdir)
|
||||
|
||||
Disposer.dispose(myTestRootDisposable)
|
||||
|
||||
val expectations = parseExpectations(ktFile)
|
||||
expectations.check()
|
||||
}
|
||||
|
||||
private class SignatureExpectation(val header: String, val name: String, expectedJvmSignature: String, expectedGenericSignature: String) {
|
||||
private var checked = false
|
||||
private val expectedSignature = formatSignature(header, expectedJvmSignature, expectedGenericSignature)
|
||||
|
||||
fun isChecked(): Boolean = checked
|
||||
|
||||
fun check(name: String, actualJvmSignature: String, actualGenericSignature: String) {
|
||||
if (this.name == name) {
|
||||
checked = true
|
||||
val actualSignature = formatSignature(header, actualJvmSignature, actualGenericSignature)
|
||||
Assert.assertEquals(expectedSignature, actualSignature)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class PackageExpectationsSuite() {
|
||||
private val classSuitesByClassName = LinkedHashMap<String, ClassExpectationsSuite>()
|
||||
|
||||
fun getOrCreateClassSuite(className: String): ClassExpectationsSuite =
|
||||
classSuitesByClassName.getOrPut(className) { ClassExpectationsSuite(className) }
|
||||
|
||||
fun check() {
|
||||
classSuitesByClassName.values().forEach { it.check() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class ClassExpectationsSuite(val className: String) {
|
||||
val classExpectations = ArrayList<SignatureExpectation>()
|
||||
val methodExpectations = ArrayList<SignatureExpectation>()
|
||||
val fieldExpectations = ArrayList<SignatureExpectation>()
|
||||
|
||||
fun check() {
|
||||
val checker = Checker()
|
||||
val classFileName = "$tmpdir/${className.replace('.', '/')}.class"
|
||||
val classFile = File(classFileName)
|
||||
|
||||
checkClassFile(checker, classFile)
|
||||
|
||||
if (className.endsWith("Package")) {
|
||||
// This class is a package facade. We should also check package parts.
|
||||
checkPackageParts(checker, classFile)
|
||||
}
|
||||
|
||||
assertAllChecked()
|
||||
}
|
||||
|
||||
private fun checkPackageParts(checker: Checker, classFile: File) {
|
||||
// Look for package parts in the same directory.
|
||||
// Package part file names for package SomePackage look like SomePackage$<hash>.class.
|
||||
val classDir = classFile.getParentFile()
|
||||
val classLastName = classFile.getName()
|
||||
val packageFacadePrefix = classLastName.replace(".class", "\$")
|
||||
classDir.listFiles { dir, lastName ->
|
||||
lastName.startsWith(packageFacadePrefix) && lastName.endsWith(".class")
|
||||
} forEach { packageFacadeFile ->
|
||||
checkClassFile(checker, packageFacadeFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertAllChecked() {
|
||||
val uncheckedExpectations = ArrayList<SignatureExpectation>()
|
||||
classExpectations.filterNotTo(uncheckedExpectations) { it.isChecked() }
|
||||
methodExpectations.filterNotTo(uncheckedExpectations) { it.isChecked() }
|
||||
fieldExpectations.filterNotTo(uncheckedExpectations) { it.isChecked() }
|
||||
Assert.assertTrue(
|
||||
"Unchecked expectations (${uncheckedExpectations.size()} total):\n " + uncheckedExpectations.join("\n "),
|
||||
uncheckedExpectations.isEmpty())
|
||||
}
|
||||
|
||||
private fun checkClassFile(checker: Checker, classFile: File) {
|
||||
val classInputStream = FileInputStream(classFile)
|
||||
try {
|
||||
ClassReader(classInputStream).accept(checker,
|
||||
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
|
||||
}
|
||||
finally {
|
||||
Closeables.closeQuietly(classInputStream)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Checker : ClassVisitor(Opcodes.ASM5) {
|
||||
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
|
||||
classExpectations forEach { it.check(name, name, signature?:"null") }
|
||||
super.visit(version, access, name, signature, superName, interfaces)
|
||||
}
|
||||
|
||||
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
|
||||
methodExpectations forEach { it.check(name, desc, signature?:"null") }
|
||||
return super.visitMethod(access, name, desc, signature, exceptions)
|
||||
}
|
||||
|
||||
override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? {
|
||||
fieldExpectations forEach { it.check(name, desc, signature?:"null") }
|
||||
return super.visitField(access, name, desc, signature, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun addClassExpectation(name: String, jvmSignature: String, genericSignature: String) {
|
||||
classExpectations.add(SignatureExpectation("class: $name", name, jvmSignature, genericSignature))
|
||||
}
|
||||
|
||||
fun addFieldExpectation(className: String, memberName: String, jvmSignature: String, genericSignature: String) {
|
||||
fieldExpectations.add(SignatureExpectation("field: $className::$memberName", memberName, jvmSignature, genericSignature))
|
||||
}
|
||||
|
||||
fun addMethodExpectation(className: String, memberName: String, jvmSignature: String, genericSignature: String) {
|
||||
methodExpectations.add(SignatureExpectation("method: $className::$memberName", memberName, jvmSignature, genericSignature))
|
||||
}
|
||||
}
|
||||
|
||||
fun parseExpectations(ktFile: File): PackageExpectationsSuite {
|
||||
val expectations = PackageExpectationsSuite()
|
||||
|
||||
val lines = Files.readLines(ktFile, Charset.forName("utf-8"))
|
||||
var lineNo = 0
|
||||
while (lineNo < lines.size()) {
|
||||
val line = lines[lineNo]
|
||||
val expectationMatch = expectationRegex.matchExact(line)
|
||||
|
||||
if (expectationMatch != null) {
|
||||
val kind = expectationMatch.group(1)!!
|
||||
val className = expectationMatch.group(2)!!
|
||||
val memberName = expectationMatch.group(4)
|
||||
|
||||
if (kind == "class" && memberName != null) {
|
||||
throw AssertionError("$ktFile:${lineNo+1}: use $className\$$memberName to denote inner class")
|
||||
}
|
||||
else if (memberName == null) {
|
||||
throw AssertionError("$ktFile:${lineNo+1}: '$kind' requires member name (after $className::)")
|
||||
}
|
||||
|
||||
val jvmSignatureMatch = jvmSignatureRegex.matchExact(lines[lineNo+1])
|
||||
val genericSignatureMatch = genericSignatureRegex.matchExact(lines[lineNo+2])
|
||||
|
||||
if (jvmSignatureMatch != null && genericSignatureMatch != null) {
|
||||
val jvmSignature = jvmSignatureMatch.group(1)
|
||||
val genericSignature = genericSignatureMatch.group(1)
|
||||
|
||||
val classSuite = expectations.getOrCreateClassSuite(className)
|
||||
|
||||
when (kind) {
|
||||
"class" -> classSuite.addClassExpectation(className, jvmSignature, genericSignature)
|
||||
"field" -> classSuite.addFieldExpectation(className, memberName, jvmSignature, genericSignature)
|
||||
"method" -> classSuite.addMethodExpectation(className, memberName, jvmSignature, genericSignature)
|
||||
else -> throw AssertionError("$ktFile:${lineNo+1}: unsupported expectation kind: $kind")
|
||||
}
|
||||
|
||||
// Expectation, skip the following 'jvm signature' and 'generic signature' lines
|
||||
lineNo += 3
|
||||
}
|
||||
else {
|
||||
throw AssertionError("$ktFile:${lineNo+1}: '$kind' should be followed by 'jvm signature' and 'generic signature'")
|
||||
}
|
||||
}
|
||||
else {
|
||||
++lineNo
|
||||
}
|
||||
}
|
||||
|
||||
return expectations
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun formatSignature(header: String, jvmSignature: String, genericSignature: String): String =
|
||||
"""// $header
|
||||
// jvm signature: $jvmSignature
|
||||
// generic signature: $genericSignature"""
|
||||
|
||||
val expectationRegex = Regex("^// (class|method|field): *([^:]+)(::(.+)) *(//.*)?")
|
||||
val jvmSignatureRegex = Regex("^// jvm signature: *(.+) *(//.*)?")
|
||||
val genericSignatureRegex = Regex("^// generic signature: *(.+) *(//.*)?")
|
||||
|
||||
fun Regex.matchExact(input: String): MatchResult? {
|
||||
val matcher = this.toPattern().matcher(input)
|
||||
if (matcher.matches()) {
|
||||
return matcher.toMatchResult()
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,12 @@ public class WriteSignatureTestGenerated extends AbstractWriteSignatureTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("OutInField.kt")
|
||||
public void testOutInField() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("compiler/testData/writeSignature/declarationSiteVariance/OutInField.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("OutInInPosition.kt")
|
||||
public void testOutInInPosition() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("compiler/testData/writeSignature/declarationSiteVariance/OutInInPosition.kt");
|
||||
|
||||
Reference in New Issue
Block a user