From 07cfc173f9e225e106102ac85546b23cab4aa353 Mon Sep 17 00:00:00 2001 From: Alexander Udalov Date: Mon, 1 Dec 2014 17:55:55 +0300 Subject: [PATCH] Add ProtoBufConsistencyTest This test checks that indices of all extensions on the same message type are unique across different extension points (JVM, built-ins, ...) --- .../generators/protobuf/GenerateProtoBuf.kt | 54 ++++++++++------ .../protobuf/ProtoBufConsistencyTest.kt | 62 +++++++++++++++++++ 2 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 generators/tests/org/jetbrains/jet/generators/protobuf/ProtoBufConsistencyTest.kt diff --git a/generators/src/org/jetbrains/jet/generators/protobuf/GenerateProtoBuf.kt b/generators/src/org/jetbrains/jet/generators/protobuf/GenerateProtoBuf.kt index 570f1eeccc2..fc014c53726 100644 --- a/generators/src/org/jetbrains/jet/generators/protobuf/GenerateProtoBuf.kt +++ b/generators/src/org/jetbrains/jet/generators/protobuf/GenerateProtoBuf.kt @@ -18,6 +18,7 @@ package org.jetbrains.jet.generators.protobuf import com.intellij.execution.util.ExecUtil import java.io.File +import java.util.regex.Pattern // This file generates protobuf classes from formal description. // To run it, you'll need protoc (protobuf compiler) 2.5.0 installed. @@ -30,17 +31,36 @@ import java.io.File // You may need to provide custom path to protoc executable, just modify this constant: val PROTOC_EXE = "protoc" +public data class ProtoPath( + public val file: String, + public val outPath: String +) { + public val packageName: String = findFirst(Pattern.compile("package (.+);")) + public val className: String = findFirst(Pattern.compile("option java_outer_classname = \"(.+)\";")) + public val debugClassName: String = "Debug$className" + + private fun findFirst(pattern: Pattern): String { + for (line in File(file).readLines()) { + val m = pattern.matcher(line) + if (m.find()) return m.group(1) + } + error("Pattern not found in $file: $pattern") + } +} + +public val PROTO_PATHS: List = listOf( + ProtoPath("core/serialization/src/descriptors.proto", "core/serialization/src"), + ProtoPath("core/serialization/src/builtins.proto", "core/serialization/src"), + ProtoPath("core/serialization.java/src/java_descriptors.proto", "core/serialization.java/src") +) + fun main(args: Array) { try { checkVersion() - for ((protoPath, outPath) in listOf( - Pair("core/serialization/src/descriptors.proto", "core/serialization/src"), - Pair("core/serialization/src/builtins.proto", "core/serialization/src"), - Pair("core/serialization.java/src/java_descriptors.proto", "core/serialization.java/src") - )) { - execProtoc(protoPath, outPath) - modifyAndExecProtoc(protoPath, "compiler/tests") + for (protoPath in PROTO_PATHS) { + execProtoc(protoPath.file, protoPath.outPath) + modifyAndExecProtoc(protoPath) } } catch (e: Throwable) { @@ -56,7 +76,7 @@ fun main(args: Array) { fun checkVersion() { val processOutput = ExecUtil.execAndGetOutput(listOf(PROTOC_EXE, "--version"), null) - val version = processOutput.getStdout()!!.trim() + val version = processOutput.getStdout().trim() if (version.isEmpty()) { throw AssertionError("Output is empty, stderr: " + processOutput.getStderr()) } @@ -73,20 +93,18 @@ fun execProtoc(protoPath: String, outPath: String) { } } -fun modifyAndExecProtoc(protoPath: String, outPath: String) { - val originalText = File(protoPath).readText() - - val debugProtoFile = File(protoPath.replace(".proto", ".debug.proto")) - debugProtoFile.writeText(modifyForDebug(originalText)) +fun modifyAndExecProtoc(protoPath: ProtoPath) { + val debugProtoFile = File(protoPath.file.replace(".proto", ".debug.proto")) + debugProtoFile.writeText(modifyForDebug(protoPath)) debugProtoFile.deleteOnExit() - execProtoc(debugProtoFile.getPath(), outPath) + execProtoc(debugProtoFile.getPath(), "compiler/tests") } -fun modifyForDebug(originalProto: String): String { - return originalProto - .replace("java_outer_classname = \"", "java_outer_classname = \"Debug") // give different name for class +fun modifyForDebug(protoPath: ProtoPath): String { + return File(protoPath.file).readText() + .replace("option java_outer_classname = \"${protoPath.className}\"", + "option java_outer_classname = \"${protoPath.debugClassName}\"") // give different name for class .replace("option optimize_for = LITE_RUNTIME;", "") // using default instead .replace(".proto\"", ".debug.proto\"") // for "import" statement in proto } - diff --git a/generators/tests/org/jetbrains/jet/generators/protobuf/ProtoBufConsistencyTest.kt b/generators/tests/org/jetbrains/jet/generators/protobuf/ProtoBufConsistencyTest.kt new file mode 100644 index 00000000000..ea73b8ae223 --- /dev/null +++ b/generators/tests/org/jetbrains/jet/generators/protobuf/ProtoBufConsistencyTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2014 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.jet.generators.protobuf + +import junit.framework.TestCase +import com.google.protobuf.GeneratedMessage.GeneratedExtension +import java.lang.reflect.ParameterizedType +import com.google.protobuf.Descriptors +import kotlin.test.fail +import com.google.common.collect.LinkedHashMultimap +import java.lang.reflect.Modifier + +public class ProtoBufConsistencyTest : TestCase() { + public fun testExtensionNumbersDoNotIntersect() { + [data] class Key(val messageType: Class<*>, val index: Int) + + val extensions = LinkedHashMultimap.create() + + for (protoPath in PROTO_PATHS) { + val classFqName = protoPath.packageName + "." + protoPath.debugClassName + val klass = javaClass.getClassLoader().loadClass(classFqName) ?: error("Class not found: $classFqName") + for (field in klass.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) && field.getType() == javaClass>()) { + // The only place where type information for an extension is stored is the field's declared generic type. + // The message type which this extension extends is the first argument to GeneratedExtension<*, *> + val containingType = (field.getGenericType() as ParameterizedType).getActualTypeArguments().first() as Class<*> + val value = field.get(null) as GeneratedExtension<*, *> + val desc = value.getDescriptor() + extensions.put(Key(containingType, desc.getNumber()), desc) + } + } + } + + for ((key, descriptors) in extensions.asMap().entrySet()) { + if (descriptors.size() > 1) { + fail(""" +Several extensions to the same message type with the same index were found. +This will cause different hard-to-debug problems if these extensions are used at the same time during (de-)serialization of the message. +Consider changing the indices in the corresponding .proto definition files. +Message type: ${key.messageType.getSimpleName()} +Index: ${key.index} +Extensions found: ${descriptors.map { it.getName() }} +""" + ) + } + } + } +}