Build incremental testing utils to kotlin-build-common-test.jar

KT-8487
This commit is contained in:
Alexey Tsvetkov
2016-02-16 16:13:42 +03:00
parent 239e217d19
commit 7a7562d685
7 changed files with 45 additions and 13 deletions
@@ -0,0 +1,178 @@
/*
* Copyright 2010-2016 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.incremental.testingUtils
import com.google.common.collect.Sets
import com.google.common.hash.Hashing
import com.google.common.io.Files
import com.google.protobuf.ExtensionRegistry
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.incremental.LocalFileKotlinClass
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.serialization.DebugProtoBuf
import org.jetbrains.kotlin.serialization.jvm.BitEncoding
import org.jetbrains.kotlin.serialization.jvm.DebugJvmProtoBuf
import org.jetbrains.kotlin.utils.Printer
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import java.io.ByteArrayInputStream
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.*
import kotlin.comparisons.compareBy
// Set this to true if you want to dump all bytecode (test will fail in this case)
val DUMP_ALL = System.getProperty("comparison.dump.all") == "true"
fun File.hash() = Files.hash(this, Hashing.crc32())
fun getDirectoryString(dir: File, interestingPaths: List<String>): String {
val buf = StringBuilder()
val p = Printer(buf)
fun addDirContent(dir: File) {
p.pushIndent()
val listFiles = dir.listFiles()
assertNotNull(listFiles)
val children = listFiles!!.sortedWith(compareBy({ it.isDirectory }, { it.name }))
for (child in children) {
if (child.isDirectory) {
p.println(child.name)
addDirContent(child)
}
else {
p.println(child.name, " ", child.hash())
}
}
p.popIndent()
}
p.println(".")
addDirContent(dir)
for (path in interestingPaths) {
p.println("================", path, "================")
p.println(fileToStringRepresentation(File(dir, path)))
p.println()
p.println()
}
return buf.toString()
}
fun getAllRelativePaths(dir: File): Set<String> {
val result = HashSet<String>()
FileUtil.processFilesRecursively(dir) {
if (it!!.isFile) {
result.add(FileUtil.getRelativePath(dir, it)!!)
}
true
}
return result
}
fun assertEqualDirectories(expected: File, actual: File, forgiveExtraFiles: Boolean) {
val pathsInExpected = getAllRelativePaths(expected)
val pathsInActual = getAllRelativePaths(actual)
val commonPaths = Sets.intersection(pathsInExpected, pathsInActual)
val changedPaths = commonPaths
.filter { DUMP_ALL || !Arrays.equals(File(expected, it).readBytes(), File(actual, it).readBytes()) }
.sorted()
val expectedString = getDirectoryString(expected, changedPaths)
val actualString = getDirectoryString(actual, changedPaths)
if (DUMP_ALL) {
assertEquals(expectedString, actualString + " ")
}
if (forgiveExtraFiles) {
// If compilation fails, output may be different for full rebuild and partial make. Parsing output (directory string) for simplicity.
if (changedPaths.isEmpty()) {
val expectedListingLines = expectedString.split('\n').toList()
val actualListingLines = actualString.split('\n').toList()
if (actualListingLines.containsAll(expectedListingLines)) {
return
}
}
}
assertEquals(expectedString, actualString)
}
fun classFileToString(classFile: File): String {
val out = StringWriter()
val traceVisitor = TraceClassVisitor(PrintWriter(out))
ClassReader(classFile.readBytes()).accept(traceVisitor, 0)
val classHeader = LocalFileKotlinClass.create(classFile)?.classHeader
val annotationDataEncoded = classHeader?.data
if (annotationDataEncoded != null) {
ByteArrayInputStream(BitEncoding.decodeBytes(annotationDataEncoded)).use {
input ->
out.write("\n------ string table types proto -----\n${DebugJvmProtoBuf.StringTableTypes.parseDelimitedFrom(input)}")
if (!classHeader!!.metadataVersion.isCompatible()) {
error("Incompatible class ($classHeader): $classFile")
}
when (classHeader.kind) {
KotlinClassHeader.Kind.FILE_FACADE ->
out.write("\n------ file facade proto -----\n${DebugProtoBuf.Package.parseFrom(input, getExtensionRegistry())}")
KotlinClassHeader.Kind.CLASS ->
out.write("\n------ class proto -----\n${DebugProtoBuf.Class.parseFrom(input, getExtensionRegistry())}")
KotlinClassHeader.Kind.MULTIFILE_CLASS_PART ->
out.write("\n------ multi-file part proto -----\n${DebugProtoBuf.Package.parseFrom(input, getExtensionRegistry())}")
else -> throw IllegalStateException()
}
}
}
return out.toString()
}
fun getExtensionRegistry(): ExtensionRegistry {
val registry = ExtensionRegistry.newInstance()!!
DebugJvmProtoBuf.registerAllExtensions(registry)
return registry
}
fun fileToStringRepresentation(file: File): String {
return when {
file.name.endsWith(".class") -> {
classFileToString(file)
}
else -> {
file.readText()
}
}
}
@@ -0,0 +1,160 @@
/*
* Copyright 2010-2016 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.incremental.testingUtils
import com.intellij.openapi.util.io.FileUtil
import java.io.File
import java.util.*
private val COMMANDS = listOf("new", "touch", "delete")
private val COMMANDS_AS_REGEX_PART = COMMANDS.joinToString("|")
private val COMMANDS_AS_MESSAGE_PART = COMMANDS.joinToString("/") { "\".$it\"" }
enum class TouchPolicy {
TIMESTAMP,
CHECKSUM
}
fun copyTestSources(testDataDir: File, sourceDestinationDir: File, filePrefix: String): Map<File, File> {
val mapping = hashMapOf<File, File>()
FileUtil.copyDir(testDataDir, sourceDestinationDir) {
it.isDirectory || it.name.startsWith(filePrefix) && (it.name.endsWith(".kt") || it.name.endsWith(".java"))
}
sourceDestinationDir.walk().forEach { mapping[it] = File(testDataDir, filePrefix + it.name) }
return mapping
}
fun getModificationsToPerform(
testDataDir: File,
moduleNames: Collection<String>?,
allowNoFilesWithSuffixInTestData: Boolean,
touchPolicy: TouchPolicy
): List<List<Modification>> {
fun getModificationsForIteration(newSuffix: String, touchSuffix: String, deleteSuffix: String): List<Modification> {
fun getDirPrefix(fileName: String): String {
val underscore = fileName.indexOf("_")
if (underscore != -1) {
val module = fileName.substring(0, underscore)
assert(moduleNames != null) { "File name has module prefix, but multi-module environment is absent" }
assert(module in moduleNames!!) { "Module not found for file with prefix: $fileName" }
return "$module/src"
}
assert(moduleNames == null) { "Test is multi-module, but file has no module prefix: $fileName" }
return "src"
}
val modifications = ArrayList<Modification>()
for (file in testDataDir.listFiles()!!) {
val fileName = file.name
if (fileName.endsWith(newSuffix)) {
modifications.add(ModifyContent(getDirPrefix(fileName) + "/" + fileName.removeSuffix(newSuffix), file))
}
if (fileName.endsWith(touchSuffix)) {
modifications.add(TouchFile(getDirPrefix(fileName) + "/" + fileName.removeSuffix(touchSuffix), touchPolicy))
}
if (fileName.endsWith(deleteSuffix)) {
modifications.add(DeleteFile(getDirPrefix(fileName) + "/" + fileName.removeSuffix(deleteSuffix)))
}
}
return modifications
}
val haveFilesWithoutNumbers = testDataDir.listFiles { it -> it.name.matches(".+\\.(${COMMANDS_AS_REGEX_PART})$".toRegex()) }?.isNotEmpty() ?: false
val haveFilesWithNumbers = testDataDir.listFiles { it -> it.name.matches(".+\\.(${COMMANDS_AS_REGEX_PART})\\.\\d+$".toRegex()) }?.isNotEmpty() ?: false
if (haveFilesWithoutNumbers && haveFilesWithNumbers) {
throw IllegalStateException("Bad test data format: files ending with both unnumbered and numbered ${COMMANDS_AS_MESSAGE_PART} were found")
}
if (!haveFilesWithoutNumbers && !haveFilesWithNumbers) {
if (allowNoFilesWithSuffixInTestData) {
return listOf(listOf())
}
else {
throw IllegalStateException("Bad test data format: no files ending with ${COMMANDS_AS_MESSAGE_PART} found")
}
}
if (haveFilesWithoutNumbers) {
return listOf(getModificationsForIteration(".new", ".touch", ".delete"))
}
else {
return (1..10)
.map { getModificationsForIteration(".new.$it", ".touch.$it", ".delete.$it") }
.filter { it.isNotEmpty() }
}
}
abstract class Modification(val path: String) {
abstract fun perform(workDir: File, mapping: MutableMap<File, File>)
override fun toString(): String = "${javaClass.simpleName} $path"
}
class ModifyContent(path: String, val dataFile: File) : Modification(path) {
override fun perform(workDir: File, mapping: MutableMap<File, File>) {
val file = File(workDir, path)
val oldLastModified = file.lastModified()
file.delete()
dataFile.copyTo(file)
val newLastModified = file.lastModified()
if (newLastModified <= oldLastModified) {
//Mac OS and some versions of Linux truncate timestamp to nearest second
file.setLastModified(oldLastModified + 1000)
}
mapping[file] = dataFile
}
}
class TouchFile(path: String, private val touchPolicy: TouchPolicy) : Modification(path) {
override fun perform(workDir: File, mapping: MutableMap<File, File>) {
val file = File(workDir, path)
when (touchPolicy) {
TouchPolicy.TIMESTAMP -> {
val oldLastModified = file.lastModified()
//Mac OS and some versions of Linux truncate timestamp to nearest second
file.setLastModified(Math.max(System.currentTimeMillis(), oldLastModified + 1000))
}
TouchPolicy.CHECKSUM -> {
file.appendText(" ")
}
}
}
}
class DeleteFile(path: String) : Modification(path) {
override fun perform(workDir: File, mapping: MutableMap<File, File>) {
val fileToDelete = File(workDir, path)
if (!fileToDelete.delete()) {
throw AssertionError("Couldn't delete $fileToDelete")
}
mapping.remove(fileToDelete)
}
}