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:
dnpetrov
2015-06-15 12:10:35 +03:00
parent b068a1dd5f
commit a2b2eba1a1
4 changed files with 270 additions and 209 deletions
@@ -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");