test framework: add DSL to generate class-files in 'directoryContent'

So we can produce simple *.class files dynamically instead of placing them in testData directories.
This commit is contained in:
nik
2017-09-18 16:17:12 +03:00
parent f7495235c7
commit 291bb2a609
7 changed files with 260 additions and 18 deletions
@@ -0,0 +1,94 @@
/*
* Copyright 2000-2017 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 com.intellij.util.io.java
import com.intellij.testFramework.UsefulTestCase.assertEmpty
import com.intellij.testFramework.UsefulTestCase.assertOneElement
import com.intellij.util.io.directoryContent
import junit.framework.TestCase.assertSame
import org.junit.Test
import java.io.File
import java.io.Serializable
import java.lang.reflect.Modifier
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* @author nik
*/
class ClassFileBuilderTest {
@Test
fun `empty class`() {
val dir = directoryContent {
classFile("A") {}
}.generateInTempDir()
val aClass = loadClass("A", File(dir, "A.class"))
assertSame(Object::class.java, aClass.superclass)
assertEmpty(aClass.interfaces)
assertEmpty(aClass.declaredFields)
}
@Test
fun `class with extends and implements`() {
val dir = directoryContent {
classFile("A") {
superclass = ArrayList::class.java.name
interfaces = listOf(Serializable::class.java.name)
}
}.generateInTempDir()
val aClass = loadClass("A", File(dir, "A.class"))
assertSame(ArrayList::class.java, aClass.superclass)
assertSame(Serializable::class.java, assertOneElement(aClass.interfaces))
assertEmpty(aClass.declaredFields)
}
@Test
fun `class with fields`() {
val dir = directoryContent {
classFile("A") {
field("foo", Int::class, AccessModifier.PUBLIC)
field("bar", Object::class.java.name)
}
}.generateInTempDir()
val aClass = loadClass("A", File(dir, "A.class"))
val foo = aClass.getDeclaredField("foo")
assertSame(Int::class.java, foo.type)
assertTrue(Modifier.isPublic(foo.modifiers))
val bar = aClass.getDeclaredField("bar")
assertSame(Object::class.java, bar.type)
assertTrue(Modifier.isPrivate(bar.modifiers))
}
@Test
fun `class in non-default package`() {
val dir = directoryContent {
classFile("p.A") {}
}.generateInTempDir()
val aClass = loadClass("p.A", File(dir, "p/A.class"))
assertEquals("p.A", aClass.name)
}
}
private fun loadClass(name: String, file: File): Class<*> {
return MyClassLoader(ClassFileBuilderTest::class.java.classLoader).doDefineClass(name, file.readBytes())
}
private class MyClassLoader(parent: ClassLoader) : ClassLoader(parent) {
fun doDefineClass(name: String, data: ByteArray): Class<*> {
return defineClass(name, data, 0, data.size)
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2000-2017 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 com.intellij.util.io.java
import com.intellij.util.io.DirectoryContentBuilder
import com.intellij.util.io.java.impl.ClassFileBuilderImpl
import org.jetbrains.jps.model.java.LanguageLevel
import kotlin.reflect.KClass
/**
* Produces class-file with qualified name [name] in a place specified by [content]. If the class is not from the default package,
* the produced file will be placed in a sub-directory according to its package.
*
* @author nik
*/
inline fun DirectoryContentBuilder.classFile(name: String, content: ClassFileBuilder.() -> Unit) {
val builder = ClassFileBuilderImpl(name)
builder.content()
builder.generate(this)
}
abstract class ClassFileBuilder {
var javaVersion: LanguageLevel = LanguageLevel.JDK_1_8
var superclass: String = "java.lang.Object"
var interfaces: List<String> = emptyList()
var access: AccessModifier = AccessModifier.PUBLIC
abstract fun field(name: String, type: KClass<*>, access: AccessModifier = AccessModifier.PRIVATE)
/**
* Adds a field which type is a class with qualified name [type]
*/
abstract fun field(name: String, type: String, access: AccessModifier = AccessModifier.PRIVATE)
}
enum class AccessModifier { PRIVATE, PUBLIC, PROTECTED, PACKAGE_LOCAL }
@@ -0,0 +1,90 @@
/*
* Copyright 2000-2017 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 com.intellij.util.io.java.impl
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.PathUtil
import com.intellij.util.io.DirectoryContentBuilder
import com.intellij.util.io.java.AccessModifier
import com.intellij.util.io.java.ClassFileBuilder
import org.jetbrains.jps.model.java.LanguageLevel
import org.jetbrains.org.objectweb.asm.ClassWriter
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import kotlin.reflect.KClass
/**
* @author nik
*/
class ClassFileBuilderImpl(private val name: String) : ClassFileBuilder() {
private val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)
override fun field(name: String, type: String, access: AccessModifier) {
addField(name, "L" + toJvmName(type) + ";", access)
}
override fun field(name: String, type: KClass<*>, access: AccessModifier) {
addField(name, Type.getDescriptor(type.java), access)
}
private fun addField(name: String, typeDescriptor: String, access: AccessModifier) {
writer.visitField(access.toAsmCode(), name, typeDescriptor, null, null).visitEnd()
}
fun generate(targetRoot: DirectoryContentBuilder) {
writer.visit(javaVersion.toAsmCode(), access.toAsmCode(), toJvmName(name), null,
superclass.replace('.', '/'),
interfaces.map(::toJvmName).toTypedArray())
writer.visitEnd()
targetRoot.directories(StringUtil.getPackageName(name).replace('.', '/')) {
file("${StringUtil.getShortName(name)}.class", writer.toByteArray())
}
}
private fun DirectoryContentBuilder.directories(relativePath: String, content: DirectoryContentBuilder.() -> Unit) {
if (relativePath.isEmpty()) {
content()
}
else {
directories(PathUtil.getParentPath(relativePath)) {
dir(PathUtil.getFileName(relativePath)) {
content()
}
}
}
}
}
private fun toJvmName(className: String) = className.replace('.', '/')
private fun LanguageLevel.toAsmCode() = when (this) {
LanguageLevel.JDK_1_3 -> Opcodes.V1_3
LanguageLevel.JDK_1_4 -> Opcodes.V1_4
LanguageLevel.JDK_1_5 -> Opcodes.V1_5
LanguageLevel.JDK_1_6 -> Opcodes.V1_6
LanguageLevel.JDK_1_7 -> Opcodes.V1_7
LanguageLevel.JDK_1_8 -> Opcodes.V1_8
LanguageLevel.JDK_1_9 -> Opcodes.V1_9
LanguageLevel.JDK_X -> throw UnsupportedOperationException("Java 10 isn't supported yet")
}
private fun AccessModifier.toAsmCode() = when (this) {
AccessModifier.PROTECTED -> Opcodes.ACC_PROTECTED
AccessModifier.PRIVATE -> Opcodes.ACC_PRIVATE
AccessModifier.PUBLIC -> Opcodes.ACC_PUBLIC
AccessModifier.PACKAGE_LOCAL -> 0
}
+1 -1
View File
@@ -24,7 +24,7 @@
<orderEntry type="module" module-name="java-runtime" scope="RUNTIME" />
<orderEntry type="library" name="Log4J" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="module" module-name="testFramework" scope="TEST" />
<orderEntry type="module" module-name="testFramework-java" scope="TEST" />
<orderEntry type="library" scope="TEST" name="Eclipse" level="project" />
<orderEntry type="module" module-name="jps-model-api" />
<orderEntry type="module" module-name="jps-model-serialization" />
@@ -20,6 +20,10 @@ import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.containers.MultiMap
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.directoryContent
import com.intellij.util.io.java.AccessModifier
import com.intellij.util.io.java.ClassFileBuilder
import com.intellij.util.io.java.classFile
import gnu.trove.THashSet
import org.jetbrains.jps.ModuleChunk
import org.jetbrains.jps.builders.DirtyFilesHolder
@@ -30,9 +34,8 @@ import org.jetbrains.jps.incremental.ModuleBuildTarget
import org.jetbrains.jps.incremental.ModuleLevelBuilder
import org.jetbrains.jps.incremental.storage.AbstractStateStorage
import org.jetbrains.jps.incremental.storage.PathStringDescriptor
import org.jetbrains.jps.model.java.LanguageLevel
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassWriter
import org.jetbrains.org.objectweb.asm.Opcodes
import java.io.File
import java.util.*
import java.util.regex.Pattern
@@ -75,19 +78,19 @@ class MockPackageFacadeGenerator : ModuleLevelBuilder(BuilderCategory.SOURCE_PRO
val callback = JavaBuilderUtil.getDependenciesRegistrar(context)
fun generateClass(packageName: String, className: String, target: ModuleBuildTarget, sources: Collection<String>,
allSources: Collection<String>, generate: (ClassWriter.() -> Unit)? = null) {
val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)
val fullClassName = StringUtil.getQualifiedName(packageName, className).replace('.', '/')
writer.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, fullClassName, null, "java/lang/Object", null)
if (generate != null) {
writer.generate()
}
writer.visitEnd()
val outputFile = File(target.outputDir, "$fullClassName.class")
val classBytes = writer.toByteArray()
FileUtil.writeToFile(outputFile, classBytes)
allSources: Collection<String>, content: (ClassFileBuilder.() -> Unit)? = null) {
val fullClassName = StringUtil.getQualifiedName(packageName, className)
directoryContent {
classFile(fullClassName) {
javaVersion = LanguageLevel.JDK_1_6
if (content != null) {
content()
}
}
}.generate(target.outputDir!!)
val outputFile = File(target.outputDir, "${fullClassName.replace('.', '/')}.class")
outputConsumer.registerOutputFile(target, outputFile, sources)
callback.associate(fullClassName.replace('/', '.'), allSources, ClassReader(classBytes))
callback.associate(fullClassName, allSources, ClassReader(outputFile.readBytes()))
}
for (target in chunk.targets) {
@@ -130,8 +133,8 @@ class MockPackageFacadeGenerator : ModuleLevelBuilder(BuilderCategory.SOURCE_PRO
generateClass(packageName, "PackageFacade", target, dirtySource, allSources) {
for (fileName in classNames) {
val fieldClass = StringUtil.getQualifiedName(packageName, fileName).replace('.', '/')
visitField(Opcodes.ACC_PUBLIC, StringUtil.decapitalize(fileName), "L$fieldClass;", null, null).visitEnd()
val fieldClass = StringUtil.getQualifiedName(packageName, fileName)
field(StringUtil.decapitalize(fileName), fieldClass, AccessModifier.PUBLIC)
}
}
for (source in dirtySource) {
@@ -42,6 +42,8 @@ abstract class DirectoryContentBuilder {
abstract fun file(name: String, text: String)
abstract fun file(name: String, content: ByteArray)
inline fun dir(name: String, content: DirectoryContentBuilder.() -> Unit) {
val dirDefinition = DirectorySpec()
DirectoryContentBuilderImpl(dirDefinition).content()
@@ -104,7 +104,11 @@ class DirectoryContentBuilderImpl(val result: DirectorySpecBase) : DirectoryCont
}
override fun file(name: String, text: String) {
addChild(name, FileSpec(text.toByteArray()))
file(name, text.toByteArray())
}
override fun file(name: String, content: ByteArray) {
addChild(name, FileSpec(content))
}
}