THRIFT-5548: add kotlin code gen
Client: kotlin
Patch: Jiayu Liu
This closes #2556
diff --git a/lib/kotlin/.gitignore b/lib/kotlin/.gitignore
new file mode 100644
index 0000000..7d5c789
--- /dev/null
+++ b/lib/kotlin/.gitignore
@@ -0,0 +1,78 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+# Created by https://www.toptal.com/developers/gitignore/api/gradle,kotlin
+# Edit at https://www.toptal.com/developers/gitignore?templates=gradle,kotlin
+
+### Kotlin ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Gradle ###
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+# !gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+# !gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin
+out/
+
+gradlew
+gradlew.bat
+gradle/wrapper/gradle-wrapper.jar
+gradle/wrapper/gradle-wrapper.properties
diff --git a/lib/kotlin/CMakeLists.txt b/lib/kotlin/CMakeLists.txt
new file mode 100644
index 0000000..d81287c
--- /dev/null
+++ b/lib/kotlin/CMakeLists.txt
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+file(GLOB_RECURSE THRIFTKOTLIN_SOURCES LIST_DIRECTORIES false
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/*")
diff --git a/lib/kotlin/Makefile.am b/lib/kotlin/Makefile.am
new file mode 100644
index 0000000..6a99628
--- /dev/null
+++ b/lib/kotlin/Makefile.am
@@ -0,0 +1,44 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+export CLASSPATH
+
+all-local:
+ $(GRADLE) $(GRADLE_OPTS) assemble \
+ -Pthrift.version=$(PACKAGE_VERSION) \
+ -Pthrift.compiler=$(THRIFT) \
+ --console=plain
+
+clean-local:
+ $(GRADLE) $(GRADLE_OPTS) clean --console=plain
+
+precross: $(THRIFT)
+ $(GRADLE) $(GRADLE_OPTS) installDist \
+ -Pthrift.version=$(PACKAGE_VERSION) \
+ -Pthrift.compiler=$(THRIFT) \
+ --console=plain
+
+check-local: $(THRIFT)
+ $(GRADLE) $(GRADLE_OPTS) test \
+ -Pthrift.version=$(PACKAGE_VERSION) \
+ -Pthrift.compiler=$(THRIFT) \
+ --console=plain
+
+EXTRA_DIST = \
+ README.md
diff --git a/lib/kotlin/README.md b/lib/kotlin/README.md
new file mode 100644
index 0000000..2f73dea
--- /dev/null
+++ b/lib/kotlin/README.md
@@ -0,0 +1,29 @@
+# Test Only Library for Kotlin
+
+This directory (`/lib/kotlin`) contains test only library code for Kotlin code gen. Because Kotlin code gen produces code that works on top of libthrift (i.e. Java library), the purpose of this library is to encode the cross test server and client to make sure it conforms to the thrift specifications.
+
+The output artifact in this library is *not* published to Maven central.
+
+## How to compile
+
+This library is managed using Gradle 7+, so the easiest way is to use gradle wrapper (`./gradlew`).
+
+Run the following command (requires C++ compiler):
+
+```bash
+gradle build
+```
+
+```bash
+gradle installDist
+```
+
+## How to run cross test server / client
+
+```bash
+gradle :cross-test-server:run
+```
+
+```bash
+gradle :cross-test-client:run
+```
diff --git a/lib/kotlin/cross-test-client/build.gradle.kts b/lib/kotlin/cross-test-client/build.gradle.kts
new file mode 100644
index 0000000..3e67abf
--- /dev/null
+++ b/lib/kotlin/cross-test-client/build.gradle.kts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+plugins {
+ kotlin("jvm") version "1.5.31"
+ id("com.ncorti.ktfmt.gradle") version "0.4.0"
+ java
+ application
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-jdk8
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.1")
+ // https://mvnrepository.com/artifact/org.apache.thrift/libthrift
+ implementation("org.apache.thrift:libthrift:INCLUDED")
+ // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
+ implementation("ch.qos.logback:logback-classic:1.3.0-alpha14")
+ testImplementation("org.jetbrains.kotlin:kotlin-test")
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
+}
+
+tasks {
+ application {
+ applicationName = "TestClient"
+ mainClass.set("org.apache.thrift.test.TestClientKt")
+ }
+
+ ktfmt {
+ kotlinLangStyle()
+ }
+
+ task<Exec>("compileThrift") {
+ val thriftBin = if (hasProperty("thrift.compiler")) {
+ file(property("thrift.compiler"))
+ } else {
+ project.rootDir.resolve("../../compiler/cpp/thrift")
+ }
+ val outputDir = layout.buildDirectory.dir("generated-sources")
+ doFirst {
+ mkdir(outputDir)
+ }
+ commandLine = listOf(
+ thriftBin.absolutePath,
+ "-gen",
+ "kotlin",
+ "-out",
+ outputDir.get().toString(),
+ project.rootDir.resolve("../../test/ThriftTest.thrift").absolutePath
+ )
+ group = LifecycleBasePlugin.BUILD_GROUP
+ }
+
+ compileKotlin {
+ dependsOn("compileThrift")
+ }
+}
+
+sourceSets["main"].java {
+ srcDir(layout.buildDirectory.dir("generated-sources"))
+}
diff --git a/lib/kotlin/cross-test-client/src/main/kotlin/org/apache/thrift/test/TestClient.kt b/lib/kotlin/cross-test-client/src/main/kotlin/org/apache/thrift/test/TestClient.kt
new file mode 100644
index 0000000..10ca829
--- /dev/null
+++ b/lib/kotlin/cross-test-client/src/main/kotlin/org/apache/thrift/test/TestClient.kt
@@ -0,0 +1,1045 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.thrift.test
+
+import org.apache.http.impl.client.HttpClients
+import org.apache.thrift.TApplicationException
+import org.apache.thrift.TException
+import org.apache.thrift.TSerializer
+import org.apache.thrift.async.TAsyncClientManager
+import org.apache.thrift.protocol.TBinaryProtocol
+import org.apache.thrift.protocol.TCompactProtocol
+import org.apache.thrift.protocol.TJSONProtocol
+import org.apache.thrift.protocol.TMultiplexedProtocol
+import org.apache.thrift.protocol.TProtocol
+import org.apache.thrift.protocol.TSimpleJSONProtocol
+import org.apache.thrift.transport.THttpClient
+import org.apache.thrift.transport.TNonblockingSocket
+import org.apache.thrift.transport.TSSLTransportFactory
+import org.apache.thrift.transport.TTransport
+import org.apache.thrift.transport.TZlibTransport
+import org.apache.thrift.transport.layered.TFastFramedTransport
+import org.apache.thrift.transport.layered.TFramedTransport
+import thrift.test.Insanity
+import thrift.test.Numberz
+import thrift.test.ThriftTestClient
+import thrift.test.Xception
+import thrift.test.Xception2
+import thrift.test.Xtruct
+import thrift.test.Xtruct2
+import java.nio.ByteBuffer
+import kotlin.math.abs
+import kotlin.system.exitProcess
+
+/**
+ * Test Java client for thrift. Essentially just a copy of the C++ version, this makes a variety of
+ * requests to enable testing for both performance and correctness of the output.
+ */
+const val ERR_BASETYPES = 1
+const val ERR_STRUCTS = 2
+const val ERR_CONTAINERS = 4
+const val ERR_EXCEPTIONS = 8
+const val ERR_PROTOCOLS = 16
+const val ERR_UNKNOWN = 64
+
+suspend fun main(args: Array<String>) {
+ var host = "localhost"
+ var port = 9090
+ var numTests = 1
+ var protocolType = "binary"
+ var transportType = "buffered"
+ var ssl = false
+ var zlib = false
+ var httpClient = false
+ var socketTimeout = 1000
+ try {
+ for (i in args.indices) {
+ if (args[i].startsWith("--host")) {
+ host = args[i].split("=").toTypedArray()[1]
+ host.trim { it <= ' ' }
+ } else if (args[i].startsWith("--port")) {
+ port = Integer.valueOf(args[i].split("=").toTypedArray()[1])
+ } else if (args[i].startsWith("--n") || args[i].startsWith("--testloops")) {
+ numTests = Integer.valueOf(args[i].split("=").toTypedArray()[1])
+ } else if (args[i] == "--timeout") {
+ socketTimeout = Integer.valueOf(args[i].split("=").toTypedArray()[1])
+ } else if (args[i].startsWith("--protocol")) {
+ protocolType = args[i].split("=").toTypedArray()[1]
+ protocolType.trim { it <= ' ' }
+ } else if (args[i].startsWith("--transport")) {
+ transportType = args[i].split("=").toTypedArray()[1]
+ transportType.trim { it <= ' ' }
+ } else if (args[i] == "--ssl") {
+ ssl = true
+ } else if (args[i] == "--zlib") {
+ zlib = true
+ } else if (args[i] == "--client") {
+ httpClient = true
+ } else if (args[i] == "--help") {
+ println("Allowed options:")
+ println(" --help\t\t\tProduce help message")
+ println(" --host=arg (=$host)\tHost to connect")
+ println(" --port=arg (=$port)\tPort number to connect")
+ println(
+ " --transport=arg (=$transportType)\n\t\t\t\tTransport: buffered, framed, fastframed, http, zlib"
+ )
+ println(
+ " --protocol=arg (=$protocolType)\tProtocol: binary, compact, json, multi, multic, multij"
+ )
+ println(" --ssl\t\t\tEncrypted Transport using SSL")
+ println(" --zlib\t\t\tCompressed Transport using Zlib")
+ println(" --testloops[--n]=arg (=$numTests)\tNumber of Tests")
+ exitProcess(0)
+ }
+ }
+ } catch (x: Exception) {
+ System.err.println("Can not parse arguments! See --help")
+ exitProcess(ERR_UNKNOWN)
+ }
+ try {
+ checkProtocolType(protocolType)
+ checkTransportType(transportType)
+ if (transportType == "http" && ssl) {
+ throw Exception("SSL is not supported over http.")
+ }
+ } catch (e: Exception) {
+ System.err.println("Error: " + e.message)
+ exitProcess(ERR_UNKNOWN)
+ }
+ val transport: TTransport
+ try {
+ transport = getTTransport(transportType, host, port, httpClient, ssl, socketTimeout, zlib)
+ } catch (x: Exception) {
+ x.printStackTrace()
+ exitProcess(ERR_UNKNOWN)
+ }
+ var tProtocol = getTProtocol(protocolType, transport)
+ var tProtocol2: TProtocol? = null
+ if (protocolType.startsWith("multi")) {
+ tProtocol2 = TMultiplexedProtocol(tProtocol, "SecondService")
+ tProtocol = TMultiplexedProtocol(tProtocol, "ThriftTest")
+ }
+ println("$tProtocol, $transport")
+
+ val clientFactory = {
+ ThriftTestClient(
+ { TBinaryProtocol(it) },
+ TAsyncClientManager(),
+ TNonblockingSocket(host, port, socketTimeout)
+ )
+ }
+
+ var testClient = clientFactory()
+ val insane = Insanity()
+ var timeMin: Long = 0
+ var timeMax: Long = 0
+ var timeTot: Long = 0
+ var returnCode = 0
+ for (test in 0 until numTests) {
+ // transport.startConnect()
+ try {
+ /** CONNECT TEST */
+ println("Test #${test + 1}, connect $host:$port")
+ // if (!transport.isOpen) {
+ // try {
+ // transport.open()
+ // } catch (ttx: TTransportException) {
+ // ttx.printStackTrace()
+ // println("Connect failed: " + ttx.message)
+ // exitProcess(ERR_UNKNOWN)
+ // }
+ // }
+ val start = System.nanoTime()
+ /** VOID TEST */
+ try {
+ print("testVoid()")
+ testClient.testVoid()
+ print(" = void\n")
+ } catch (tax: TApplicationException) {
+ tax.printStackTrace()
+ returnCode = returnCode or ERR_BASETYPES
+ }
+ /** STRING TEST */
+ print("testString(\"Test\")")
+ val s: String = testClient.testString("Test")
+ print(" = \"$s\"\n")
+ if (s != "Test") {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ /** Multiplexed test */
+ if (protocolType.startsWith("multi")) {
+ throw UnsupportedOperationException("multi protocol is not yet supported")
+ // val secondClient: SecondServiceClient =
+ // SecondServiceClient(tProtocol2)
+ // print("secondtestString(\"Test2\")")
+ // s = secondClient.secondtestString("Test2")
+ // print(" = \"$s\"\n")
+ // if (s != "testString(\"Test2\")") {
+ // returnCode = returnCode or ERR_PROTOCOLS
+ // println("*** FAILURE ***\n")
+ // }
+ }
+ /** BYTE TEST */
+ print("testByte(1)")
+ val i8: Byte = testClient.testByte(1.toByte())
+ print(" = $i8\n")
+ if (i8.toInt() != 1) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ /** I32 TEST */
+ print("testI32(-1)")
+ val i32: Int = testClient.testI32(-1)
+ print(" = $i32\n")
+ if (i32 != -1) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ /** I64 TEST */
+ print("testI64(-34359738368)")
+ val i64: Long = testClient.testI64(-34359738368L)
+ print(" = $i64\n")
+ if (i64 != -34359738368L) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ /** DOUBLE TEST */
+ print("testDouble(-5.325098235)")
+ val dub: Double = testClient.testDouble(-5.325098235)
+ print(" = $dub\n")
+ if (abs(dub - -5.325098235) > 0.001) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ /** BINARY TEST */
+ try {
+ print("testBinary(-128...127) = ")
+ val data =
+ byteArrayOf(
+ -128,
+ -127,
+ -126,
+ -125,
+ -124,
+ -123,
+ -122,
+ -121,
+ -120,
+ -119,
+ -118,
+ -117,
+ -116,
+ -115,
+ -114,
+ -113,
+ -112,
+ -111,
+ -110,
+ -109,
+ -108,
+ -107,
+ -106,
+ -105,
+ -104,
+ -103,
+ -102,
+ -101,
+ -100,
+ -99,
+ -98,
+ -97,
+ -96,
+ -95,
+ -94,
+ -93,
+ -92,
+ -91,
+ -90,
+ -89,
+ -88,
+ -87,
+ -86,
+ -85,
+ -84,
+ -83,
+ -82,
+ -81,
+ -80,
+ -79,
+ -78,
+ -77,
+ -76,
+ -75,
+ -74,
+ -73,
+ -72,
+ -71,
+ -70,
+ -69,
+ -68,
+ -67,
+ -66,
+ -65,
+ -64,
+ -63,
+ -62,
+ -61,
+ -60,
+ -59,
+ -58,
+ -57,
+ -56,
+ -55,
+ -54,
+ -53,
+ -52,
+ -51,
+ -50,
+ -49,
+ -48,
+ -47,
+ -46,
+ -45,
+ -44,
+ -43,
+ -42,
+ -41,
+ -40,
+ -39,
+ -38,
+ -37,
+ -36,
+ -35,
+ -34,
+ -33,
+ -32,
+ -31,
+ -30,
+ -29,
+ -28,
+ -27,
+ -26,
+ -25,
+ -24,
+ -23,
+ -22,
+ -21,
+ -20,
+ -19,
+ -18,
+ -17,
+ -16,
+ -15,
+ -14,
+ -13,
+ -12,
+ -11,
+ -10,
+ -9,
+ -8,
+ -7,
+ -6,
+ -5,
+ -4,
+ -3,
+ -2,
+ -1,
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 42,
+ 43,
+ 44,
+ 45,
+ 46,
+ 47,
+ 48,
+ 49,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 62,
+ 63,
+ 64,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ 87,
+ 88,
+ 89,
+ 90,
+ 91,
+ 92,
+ 93,
+ 94,
+ 95,
+ 96,
+ 97,
+ 98,
+ 99,
+ 100,
+ 101,
+ 102,
+ 103,
+ 104,
+ 105,
+ 106,
+ 107,
+ 108,
+ 109,
+ 110,
+ 111,
+ 112,
+ 113,
+ 114,
+ 115,
+ 116,
+ 117,
+ 118,
+ 119,
+ 120,
+ 121,
+ 122,
+ 123,
+ 124,
+ 125,
+ 126,
+ 127
+ )
+ val bin: ByteBuffer = ByteBuffer.wrap(testClient.testBinary(data))
+ bin.mark()
+ val bytes = ByteArray(bin.limit() - bin.position())
+ bin[bytes]
+ bin.reset()
+ print("{")
+ var first = true
+ for (i in bytes.indices) {
+ if (first) first = false else print(", ")
+ print(bytes[i])
+ }
+ println("}")
+ if (ByteBuffer.wrap(data) != bin) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ } catch (ex: Exception) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("\n*** FAILURE ***\n")
+ ex.printStackTrace(System.out)
+ }
+ /** STRUCT TEST */
+ print("testStruct({\"Zero\", 1, -3, -5})")
+ val out = Xtruct()
+ out.string_thing = "Zero"
+ out.byte_thing = 1.toByte()
+ out.i32_thing = -3
+ out.i64_thing = -5
+ var `in`: Xtruct = testClient.testStruct(out)
+ print(
+ """ = {"${`in`.string_thing}",${`in`.byte_thing}, ${`in`.i32_thing}, ${`in`.i64_thing}}
+"""
+ )
+ if (`in` != out) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ /** NESTED STRUCT TEST */
+ print("testNest({1, {\"Zero\", 1, -3, -5}), 5}")
+ val out2 = Xtruct2()
+ out2.byte_thing = 1.toShort().toByte()
+ out2.struct_thing = out
+ out2.i32_thing = 5
+ val in2: Xtruct2 = testClient.testNest(out2)
+ `in` = in2.struct_thing!!
+ print(
+ """ = {${in2.byte_thing}, {"${`in`.string_thing}", ${`in`.byte_thing}, ${`in`.i32_thing}, ${`in`.i64_thing}}, ${in2.i32_thing}}
+"""
+ )
+ if (in2 != out2) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ /** MAP TEST */
+ val mapout: MutableMap<Int, Int> = HashMap()
+ for (i in 0..4) {
+ mapout[i] = i - 10
+ }
+ print("testMap({")
+ var first = true
+ for (key in mapout.keys) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(key.toString() + " => " + mapout[key])
+ }
+ print("})")
+ val mapin: Map<Int, Int> = testClient.testMap(mapout)
+ print(" = {")
+ first = true
+ for (key in mapin.keys) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(key.toString() + " => " + mapout[key])
+ }
+ print("}\n")
+ if (mapout != mapin) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ }
+ /** STRING MAP TEST */
+ try {
+ val smapout: MutableMap<String, String> = HashMap()
+ smapout["a"] = "2"
+ smapout["b"] = "blah"
+ smapout["some"] = "thing"
+ for (key in smapout.keys) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(key + " => " + smapout[key])
+ }
+ print("})")
+ val smapin: Map<String, String> = testClient.testStringMap(smapout)
+ print(" = {")
+ first = true
+ for (key in smapin.keys) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(key + " => " + smapout[key])
+ }
+ print("}\n")
+ if (smapout != smapin) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ }
+ } catch (ex: Exception) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ ex.printStackTrace(System.out)
+ }
+ /** SET TEST */
+ val setout: MutableSet<Int> = HashSet()
+ for (i in -2..2) {
+ setout.add(i)
+ }
+ print("testSet({")
+ first = true
+ for (elem in setout) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(elem)
+ }
+ print("})")
+ val setin: Set<Int> = testClient.testSet(setout)
+ print(" = {")
+ first = true
+ for (elem in setin) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(elem)
+ }
+ print("}\n")
+ if (setout != setin) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ }
+ /** LIST TEST */
+ val listout: MutableList<Int> = ArrayList()
+ for (i in -2..2) {
+ listout.add(i)
+ }
+ print("testList({")
+ first = true
+ for (elem in listout) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(elem)
+ }
+ print("})")
+ val listin: List<Int> = testClient.testList(listout)
+ print(" = {")
+ first = true
+ for (elem in listin) {
+ if (first) {
+ first = false
+ } else {
+ print(", ")
+ }
+ print(elem)
+ }
+ print("}\n")
+ if (listout != listin) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ }
+ /** ENUM TEST */
+ print("testEnum(ONE)")
+ var ret: Numberz = testClient.testEnum(Numberz.ONE)
+ print(" = $ret\n")
+ if (ret !== Numberz.ONE) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ print("testEnum(TWO)")
+ ret = testClient.testEnum(Numberz.TWO)
+ print(" = $ret\n")
+ if (ret !== Numberz.TWO) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ print("testEnum(THREE)")
+ ret = testClient.testEnum(Numberz.THREE)
+ print(" = $ret\n")
+ if (ret !== Numberz.THREE) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ print("testEnum(FIVE)")
+ ret = testClient.testEnum(Numberz.FIVE)
+ print(" = $ret\n")
+ if (ret !== Numberz.FIVE) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ print("testEnum(EIGHT)")
+ ret = testClient.testEnum(Numberz.EIGHT)
+ print(" = $ret\n")
+ if (ret !== Numberz.EIGHT) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ /** TYPEDEF TEST */
+ print("testTypedef(309858235082523)")
+ val uid: Long = testClient.testTypedef(309858235082523L)
+ print(" = $uid\n")
+ if (uid != 309858235082523L) {
+ returnCode = returnCode or ERR_BASETYPES
+ println("*** FAILURE ***\n")
+ }
+ /** NESTED MAP TEST */
+ print("testMapMap(1)")
+ val mm: Map<Int, Map<Int, Int>> = testClient.testMapMap(1)
+ print(" = {")
+ for (key in mm.keys) {
+ print("$key => {")
+ val m2 = mm[key]!!
+ for (k2 in m2.keys) {
+ print(k2.toString() + " => " + m2[k2] + ", ")
+ }
+ print("}, ")
+ }
+ print("}\n")
+ if (mm.size != 2 || !mm.containsKey(4) || !mm.containsKey(-4)) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ } else {
+ val m1 = mm[4]!!
+ val m2 = mm[-4]!!
+ if (m1[1] != 1 ||
+ m1[2] != 2 ||
+ m1[3] != 3 ||
+ m1[4] != 4 ||
+ m2[-1] != -1 ||
+ m2[-2] != -2 ||
+ m2[-3] != -3 ||
+ m2[-4] != -4
+ ) {
+ returnCode = returnCode or ERR_CONTAINERS
+ println("*** FAILURE ***\n")
+ }
+ }
+ /** INSANITY TEST */
+ var insanityFailed = true
+ try {
+ val hello = Xtruct()
+ hello.string_thing = "Hello2"
+ hello.byte_thing = 2
+ hello.i32_thing = 2
+ hello.i64_thing = 2
+ val goodbye = Xtruct()
+ goodbye.string_thing = "Goodbye4"
+ goodbye.byte_thing = 4.toByte()
+ goodbye.i32_thing = 4
+ goodbye.i64_thing = 4L
+ insane.userMap =
+ HashMap<Numberz, Long>().apply {
+ put(Numberz.EIGHT, 8L)
+ put(Numberz.FIVE, 5L)
+ }
+
+ insane.xtructs =
+ ArrayList<Xtruct>().apply {
+ add(goodbye)
+ add(hello)
+ }
+
+ print("testInsanity()")
+ val whoa: Map<Long, Map<Numberz, Insanity>> = testClient.testInsanity(insane)
+ print(" = {")
+ for (key in whoa.keys) {
+ val `val` = whoa[key]!!
+ print("$key => {")
+ for (k2 in `val`.keys) {
+ val v2 = `val`[k2]
+ print("$k2 => {")
+ val userMap = v2!!.userMap
+ print("{")
+ if (userMap != null) {
+ for (k3 in userMap.keys) {
+ print(k3.toString() + " => " + userMap[k3] + ", ")
+ }
+ }
+ print("}, ")
+ val xtructs = v2.xtructs
+ print("{")
+ if (xtructs != null) {
+ for ((string_thing, byte_thing, i32_thing, i64_thing) in xtructs) {
+ print("{\"$string_thing\", $byte_thing, $i32_thing, $i64_thing}, ")
+ }
+ }
+ print("}")
+ print("}, ")
+ }
+ print("}, ")
+ }
+ print("}\n")
+ if (whoa.size == 2 && whoa.containsKey(1L) && whoa.containsKey(2L)) {
+ val first_map = whoa[1L]!!
+ val second_map = whoa[2L]!!
+ if (first_map.size == 2 &&
+ first_map.containsKey(Numberz.TWO) &&
+ first_map.containsKey(Numberz.THREE) &&
+ second_map.size == 1 &&
+ second_map.containsKey(Numberz.SIX) &&
+ insane == first_map[Numberz.TWO] &&
+ insane == first_map[Numberz.THREE]
+ ) {
+ val six = second_map[Numberz.SIX]!!
+ // Cannot use "new Insanity().equals(six)" because as of now,
+ // struct/container
+ // fields with default requiredness have isset=false for local instances and
+ // yet
+ // received empty values from other languages like C++ have isset=true .
+ if ((six.userMap?.size ?: 0) == 0 && (six.xtructs?.size ?: 0) == 0) {
+ // OK
+ insanityFailed = false
+ }
+ }
+ }
+ } catch (ex: Exception) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ ex.printStackTrace(System.out)
+ insanityFailed = false
+ }
+ if (insanityFailed) {
+ returnCode = returnCode or ERR_STRUCTS
+ println("*** FAILURE ***\n")
+ }
+ /** EXECPTION TEST */
+ try {
+ print("testClient.testException(\"Xception\") =>")
+ testClient.testException("Xception")
+ print(" void\n*** FAILURE ***\n")
+ returnCode = returnCode or ERR_EXCEPTIONS
+ } catch (e: Xception) {
+ System.out.printf(" {%d, \"%s\"}\n", e.errorCode, e.message)
+ testClient = clientFactory()
+ }
+ try {
+ print("testClient.testException(\"TException\") =>")
+ testClient.testException("TException")
+ print(" void\n*** FAILURE ***\n")
+ returnCode = returnCode or ERR_EXCEPTIONS
+ } catch (e: TException) {
+ System.out.printf(" {\"%s\"}\n", e.message)
+ testClient = clientFactory()
+ }
+ try {
+ print("testClient.testException(\"success\") =>")
+ testClient.testException("success")
+ print(" void\n")
+ } catch (e: Exception) {
+ System.out.printf(" exception\n*** FAILURE ***\n")
+ returnCode = returnCode or ERR_EXCEPTIONS
+ }
+ /** MULTI EXCEPTION TEST */
+ try {
+ System.out.printf("testClient.testMultiException(\"Xception\", \"test 1\") =>")
+ testClient.testMultiException("Xception", "test 1")
+ print(" result\n*** FAILURE ***\n")
+ returnCode = returnCode or ERR_EXCEPTIONS
+ } catch (e: Xception) {
+ System.out.printf(" {%d, \"%s\"}\n", e.errorCode, e.message)
+ testClient = clientFactory()
+ }
+ try {
+ System.out.printf("testClient.testMultiException(\"Xception2\", \"test 2\") =>")
+ testClient.testMultiException("Xception2", "test 2")
+ print(" result\n*** FAILURE ***\n")
+ returnCode = returnCode or ERR_EXCEPTIONS
+ } catch (e: Xception2) {
+ System.out.printf(" {%d, {\"%s\"}}\n", e.errorCode, e.struct_thing!!.string_thing)
+ testClient = clientFactory()
+ }
+ try {
+ print("testClient.testMultiException(\"success\", \"test 3\") =>")
+ val result: Xtruct = testClient.testMultiException("success", "test 3")
+ System.out.printf(" {{\"%s\"}}\n", result.string_thing)
+ } catch (e: Exception) {
+ System.out.printf(" exception\n*** FAILURE ***\n")
+ returnCode = returnCode or ERR_EXCEPTIONS
+ }
+ /** ONEWAY TEST */
+ print("testOneway(3)...")
+ val startOneway = System.nanoTime()
+ testClient.testOneway(3)
+ val onewayElapsedMillis = (System.nanoTime() - startOneway) / 1000000
+ if (onewayElapsedMillis > 200) {
+ println(
+ "Oneway test took too long to execute failed: took " +
+ onewayElapsedMillis +
+ "ms"
+ )
+ println(
+ "oneway calls are 'fire and forget' and therefore should not cause blocking."
+ )
+ println(
+ "Some transports (HTTP) have a required response, and typically this failure"
+ )
+ println("means the transport response was delayed until after the execution")
+ println(
+ "of the RPC. The server should post the transport response immediately and"
+ )
+ println("before executing the RPC.")
+ println("*** FAILURE ***")
+ returnCode = returnCode or ERR_BASETYPES
+ } else {
+ println("Success - fire and forget only took " + onewayElapsedMillis + "ms")
+ }
+ val stop = System.nanoTime()
+ val tot = stop - start
+ println("Total time: " + tot / 1000 + "us")
+ if (timeMin == 0L || tot < timeMin) {
+ timeMin = tot
+ }
+ if (tot > timeMax) {
+ timeMax = tot
+ }
+ timeTot += tot
+ // transport.close()
+ } catch (x: Exception) {
+ System.out.printf("*** FAILURE ***\n")
+ x.printStackTrace()
+ returnCode = returnCode or ERR_UNKNOWN
+ }
+ }
+ val timeAvg = timeTot / numTests
+ println("Min time: " + timeMin / 1000 + "us")
+ println("Max time: " + timeMax / 1000 + "us")
+ println("Avg time: " + timeAvg / 1000 + "us")
+ try {
+ val json = TSerializer(TSimpleJSONProtocol.Factory()).toString(insane)
+ println("\nSample TSimpleJSONProtocol output:\n$json")
+ } catch (x: TException) {
+ println("*** FAILURE ***")
+ x.printStackTrace()
+ returnCode = returnCode or ERR_BASETYPES
+ }
+ exitProcess(returnCode)
+}
+
+private fun getTProtocol(protocol_type: String, transport: TTransport) =
+ when (protocol_type) {
+ "json", "multij" -> {
+ TJSONProtocol(transport)
+ }
+ "compact", "multic" -> {
+ TCompactProtocol(transport)
+ }
+ else -> {
+ TBinaryProtocol(transport)
+ }
+ }
+
+private fun checkTransportType(transport_type: String) {
+ when (transport_type) {
+ "buffered" -> {}
+ "framed" -> {}
+ "fastframed" -> {}
+ "http" -> {}
+ "zlib" -> {}
+ else -> {
+ throw Exception("Unknown transport type! $transport_type")
+ }
+ }
+}
+
+private fun checkProtocolType(protocol_type: String) {
+ when (protocol_type) {
+ "binary" -> {}
+ "compact" -> {}
+ "json" -> {}
+ "multi" -> {}
+ "multic" -> {}
+ "multij" -> {}
+ else -> {
+ throw Exception("Unknown protocol type! $protocol_type")
+ }
+ }
+}
+
+private fun getTTransport(
+ transport_type: String,
+ host: String,
+ port: Int,
+ http_client: Boolean,
+ ssl: Boolean,
+ socketTimeout: Int,
+ zlib: Boolean
+): TTransport {
+ when (transport_type) {
+ "http" -> {
+ val url = "http://$host:$port/test/service"
+ return if (http_client) {
+ THttpClient(url, HttpClients.createDefault())
+ } else {
+ THttpClient(url)
+ }
+ }
+ else -> {
+ val socket = if (ssl) {
+ TSSLTransportFactory.getClientSocket(host, port, socketTimeout)
+ } else {
+ println("using non-blocking socket $host:$port")
+ TNonblockingSocket(host, port, socketTimeout)
+ }
+ if (transport_type == "zlib") {
+ return TZlibTransport(socket)
+ } else {
+ val wrapped = when (transport_type) {
+ "buffered" -> {
+ socket
+ }
+ "framed" -> {
+ TFramedTransport(socket)
+ }
+ "fastframed" -> {
+ TFastFramedTransport(socket)
+ }
+ else -> {
+ socket
+ }
+ }
+ return if (zlib) {
+ TZlibTransport(wrapped)
+ } else {
+ wrapped
+ }
+ }
+ }
+ }
+}
diff --git a/lib/kotlin/cross-test-client/src/main/resources/logback.xml b/lib/kotlin/cross-test-client/src/main/resources/logback.xml
new file mode 100644
index 0000000..2a2fc0a
--- /dev/null
+++ b/lib/kotlin/cross-test-client/src/main/resources/logback.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <!-- encoders are assigned the type
+ ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="debug">
+ <appender-ref ref="STDOUT"/>
+ </root>
+</configuration>
+
diff --git a/lib/kotlin/cross-test-server/build.gradle.kts b/lib/kotlin/cross-test-server/build.gradle.kts
new file mode 100644
index 0000000..6b20de1
--- /dev/null
+++ b/lib/kotlin/cross-test-server/build.gradle.kts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+plugins {
+ kotlin("jvm") version "1.5.31"
+ id("com.ncorti.ktfmt.gradle") version "0.4.0"
+ java
+ application
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-jdk8
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.1")
+ // https://mvnrepository.com/artifact/org.apache.thrift/libthrift
+ implementation("org.apache.thrift:libthrift:INCLUDED")
+ // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
+ implementation("ch.qos.logback:logback-classic:1.3.0-alpha14")
+ testImplementation("org.jetbrains.kotlin:kotlin-test")
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
+}
+
+tasks {
+ application {
+ applicationName = "TestServer"
+ mainClass.set("org.apache.thrift.test.TestServerKt")
+ }
+
+ ktfmt {
+ kotlinLangStyle()
+ }
+
+ task<Exec>("compileThrift") {
+ val thriftBin = if (hasProperty("thrift.compiler")) {
+ file(property("thrift.compiler"))
+ } else {
+ project.rootDir.resolve("../../compiler/cpp/thrift")
+ }
+ val outputDir = layout.buildDirectory.dir("generated-sources")
+ doFirst {
+ mkdir(outputDir)
+ }
+ commandLine = listOf(
+ thriftBin.absolutePath,
+ "-gen",
+ "kotlin",
+ "-out",
+ outputDir.get().toString(),
+ project.rootDir.resolve("../../test/ThriftTest.thrift").absolutePath
+ )
+ group = LifecycleBasePlugin.BUILD_GROUP
+ }
+
+ compileKotlin {
+ dependsOn("compileThrift")
+ }
+}
+
+sourceSets["main"].java {
+ srcDir(layout.buildDirectory.dir("generated-sources"))
+}
diff --git a/lib/kotlin/cross-test-server/src/main/kotlin/org/apache/thrift/test/TestHandler.kt b/lib/kotlin/cross-test-server/src/main/kotlin/org/apache/thrift/test/TestHandler.kt
new file mode 100644
index 0000000..4bbdb6a
--- /dev/null
+++ b/lib/kotlin/cross-test-server/src/main/kotlin/org/apache/thrift/test/TestHandler.kt
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.thrift.test
+
+import java.nio.ByteBuffer
+import kotlinx.coroutines.delay
+import org.apache.thrift.TException
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import thrift.test.Insanity
+import thrift.test.Numberz
+import thrift.test.ThriftTest
+import thrift.test.Xception
+import thrift.test.Xception2
+import thrift.test.Xtruct
+import thrift.test.Xtruct2
+
+class TestHandler : ThriftTest {
+
+ companion object {
+ private val logger: Logger = LoggerFactory.getLogger(TestHandler::class.java)
+ }
+
+ override suspend fun testVoid() {
+ logger.info("testVoid()\n")
+ }
+
+ override suspend fun testString(thing: String): String {
+ logger.info("testString(\"$thing\")\n")
+ return thing
+ }
+
+ override suspend fun testBool(thing: Boolean): Boolean {
+ logger.info("testBool($thing)\n")
+ return thing
+ }
+
+ override suspend fun testByte(thing: Byte): Byte {
+ logger.info("testByte($thing)\n")
+ return thing
+ }
+
+ override suspend fun testI32(thing: Int): Int {
+ logger.info("testI32($thing)\n")
+ return thing
+ }
+
+ override suspend fun testI64(thing: Long): Long {
+ logger.info("testI64($thing)\n")
+ return thing
+ }
+
+ override suspend fun testDouble(thing: Double): Double {
+ logger.info("testDouble($thing)\n")
+ return thing
+ }
+
+ override suspend fun testBinary(thing: ByteArray): ByteArray {
+ val buffer = ByteBuffer.wrap(thing)
+ val sb = StringBuilder(buffer.remaining() * 3)
+ buffer.mark()
+ var limit = 0 // limit output to keep the log size sane
+ while (buffer.remaining() > 0 && ++limit < 1024) {
+ sb.append(String.format("%02X ", buffer.get()))
+ }
+ if (buffer.remaining() > 0) {
+ sb.append("...") // indicate we have more date
+ }
+ logger.info("testBinary($sb)\n")
+ buffer.reset()
+ return buffer.array()
+ }
+
+ override suspend fun testStruct(thing: Xtruct): Xtruct {
+ logger.info(
+ """
+testStruct({"${thing.string_thing}", ${thing.byte_thing}, ${thing.i32_thing}, ${thing.i64_thing}})
+
+""".trimIndent()
+ )
+ return thing
+ }
+
+ override suspend fun testNest(thing: Xtruct2): Xtruct2 {
+ val thing2: Xtruct = thing.struct_thing!!
+ logger.info(
+ """
+testNest({${thing.byte_thing}, {"${thing2.string_thing}", ${thing2.byte_thing}, ${thing2.i32_thing}, ${thing2.i64_thing}}, ${thing.i32_thing}})
+
+""".trimIndent()
+ )
+ return thing
+ }
+
+ override suspend fun testMap(thing: Map<Int, Int>): Map<Int, Int> {
+ logger.info("testMap({")
+ logger.info("{}", thing)
+ logger.info("})\n")
+ return thing
+ }
+
+ override suspend fun testStringMap(thing: Map<String, String>): Map<String, String> {
+ logger.info("testStringMap({")
+ logger.info("{}", thing)
+ logger.info("})\n")
+ return thing
+ }
+
+ override suspend fun testSet(thing: Set<Int>): Set<Int> {
+ logger.info("testSet({")
+ var first = true
+ for (elem in thing) {
+ if (first) {
+ first = false
+ } else {
+ logger.info(", ")
+ }
+ logger.info("{}", elem)
+ }
+ logger.info("})\n")
+ return thing
+ }
+
+ override suspend fun testList(thing: List<Int>): List<Int> {
+ logger.info("testList({")
+ var first = true
+ for (elem in thing) {
+ if (first) {
+ first = false
+ } else {
+ logger.info(", ")
+ }
+ logger.info("{}", elem)
+ }
+ logger.info("})\n")
+ return thing
+ }
+
+ override suspend fun testEnum(thing: Numberz): Numberz {
+ logger.info("testEnum($thing)\n")
+ return thing
+ }
+
+ override suspend fun testTypedef(thing: Long): Long {
+ logger.info("testTypedef($thing)\n")
+ return thing
+ }
+
+ override suspend fun testMapMap(hello: Int): Map<Int, Map<Int, Int>> {
+ logger.info("testMapMap($hello)\n")
+ val mapmap: MutableMap<Int, Map<Int, Int>> = HashMap()
+ val pos = HashMap<Int, Int>()
+ val neg = HashMap<Int, Int>()
+ for (i in 1..4) {
+ pos[i] = i
+ neg[-i] = -i
+ }
+ mapmap[4] = pos
+ mapmap[-4] = neg
+ return mapmap
+ }
+
+ override suspend fun testInsanity(argument: Insanity): Map<Long, Map<Numberz, Insanity>> {
+ logger.info("testInsanity()\n")
+ val firstMap = mutableMapOf<Numberz, Insanity>()
+ val secondMap = mutableMapOf<Numberz, Insanity>()
+ firstMap[Numberz.TWO] = argument
+ firstMap[Numberz.THREE] = argument
+ val looney = Insanity()
+ secondMap[Numberz.SIX] = looney
+ val insane: MutableMap<Long, Map<Numberz, Insanity>> = HashMap()
+ insane[1L] = firstMap
+ insane[2L] = secondMap
+ return insane
+ }
+
+ override suspend fun testMulti(
+ arg0: Byte,
+ arg1: Int,
+ arg2: Long,
+ arg3: Map<Short, String>,
+ arg4: Numberz,
+ arg5: Long
+ ): Xtruct {
+ logger.info("testMulti()\n")
+ val hello = Xtruct()
+ hello.string_thing = "Hello2"
+ hello.byte_thing = arg0
+ hello.i32_thing = arg1
+ hello.i64_thing = arg2
+ return hello
+ }
+
+ @Throws(Xception::class, TException::class)
+ override suspend fun testException(arg: String) {
+ logger.info("testException($arg)\n")
+ when (arg) {
+ "Xception" -> {
+ val x = Xception()
+ x.errorCode = 1001
+ x.setFieldValue(Xception._Fields.MESSAGE, arg)
+ throw x
+ }
+ "TException" -> {
+ // Unspecified exception should yield a TApplicationException on client side
+ throw RuntimeException(arg)
+ }
+ else -> {
+ val result = Xtruct()
+ result.string_thing = arg
+ }
+ }
+ return
+ }
+
+ @Throws(Xception::class, Xception2::class)
+ override suspend fun testMultiException(arg0: String, arg1: String): Xtruct {
+ logger.info("testMultiException($arg0, $arg1)\n")
+ if (arg0 == "Xception") {
+ val x = Xception()
+ x.errorCode = 1001
+ x.setFieldValue(Xception._Fields.MESSAGE, "This is an Xception")
+ throw x
+ } else if (arg0 == "Xception2") {
+ val x = Xception2()
+ x.errorCode = 2002
+ x.struct_thing = Xtruct().apply { string_thing = "This is an Xception2" }
+ throw x
+ }
+ val result = Xtruct()
+ result.string_thing = arg1
+ return result
+ }
+
+ override suspend fun testOneway(secondsToSleep: Int) {
+ logger.info("testOneway($secondsToSleep) => sleeping...")
+ try {
+ delay(secondsToSleep * 1000L)
+ logger.info("Done sleeping!")
+ } catch (ie: InterruptedException) {
+ throw RuntimeException(ie)
+ }
+ }
+}
diff --git a/lib/kotlin/cross-test-server/src/main/kotlin/org/apache/thrift/test/TestServer.kt b/lib/kotlin/cross-test-server/src/main/kotlin/org/apache/thrift/test/TestServer.kt
new file mode 100644
index 0000000..b04548d
--- /dev/null
+++ b/lib/kotlin/cross-test-server/src/main/kotlin/org/apache/thrift/test/TestServer.kt
@@ -0,0 +1,358 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.thrift.test
+
+import kotlin.system.exitProcess
+import kotlinx.coroutines.GlobalScope
+import org.apache.thrift.TException
+import org.apache.thrift.TMultiplexedProcessor
+import org.apache.thrift.protocol.TBinaryProtocol
+import org.apache.thrift.protocol.TCompactProtocol
+import org.apache.thrift.protocol.TJSONProtocol
+import org.apache.thrift.protocol.TProtocol
+import org.apache.thrift.protocol.TProtocolFactory
+import org.apache.thrift.server.ServerContext
+import org.apache.thrift.server.TNonblockingServer
+import org.apache.thrift.server.TServer
+import org.apache.thrift.server.TServerEventHandler
+import org.apache.thrift.server.TSimpleServer
+import org.apache.thrift.server.TThreadPoolServer
+import org.apache.thrift.server.TThreadedSelectorServer
+import org.apache.thrift.transport.TNonblockingServerSocket
+import org.apache.thrift.transport.TNonblockingServerSocket.NonblockingAbstractServerSocketArgs
+import org.apache.thrift.transport.TSSLTransportFactory
+import org.apache.thrift.transport.TServerSocket
+import org.apache.thrift.transport.TServerSocket.ServerSocketTransportArgs
+import org.apache.thrift.transport.TTransport
+import org.apache.thrift.transport.TTransportFactory
+import org.apache.thrift.transport.TZlibTransport
+import org.apache.thrift.transport.layered.TFastFramedTransport
+import org.apache.thrift.transport.layered.TFramedTransport
+import thrift.test.SecondService
+import thrift.test.SecondServiceProcessor
+import thrift.test.ThriftTestProcessor
+
+object TestServer {
+
+ // Multiplexed Protocol Support Details:
+ //
+ // For multiplexed testing we always use binary protocol underneath.
+ //
+ // "ThriftTest" named service implements "ThriftTest" from ThriftTest.thrift
+ // "SecondService" named service implements "SecondService" from ThriftTest.thrift
+ // In addition, to support older non-multiplexed clients using the same concrete protocol
+ // the multiplexed processor is taught to use "ThriftTest" if the incoming request has no
+ // multiplexed call name decoration.
+ internal class SecondHandler : SecondService {
+ @Throws(TException::class)
+ override suspend fun secondtestString(thing: String): String {
+ return "testString(\"$thing\")"
+ }
+ }
+
+ internal class TestServerContext(var connectionId: Int) : ServerContext {
+
+ override fun <T> unwrap(iface: Class<T>): T {
+ try {
+ return if (isWrapperFor(iface)) {
+ iface.cast(this)
+ } else {
+ throw RuntimeException("The context is not a wrapper for " + iface.name)
+ }
+ } catch (e: Exception) {
+ throw RuntimeException(
+ "The context is not a wrapper and does not implement the interface"
+ )
+ }
+ }
+
+ override fun isWrapperFor(iface: Class<*>): Boolean {
+ return iface.isInstance(this)
+ }
+ }
+
+ internal class TestServerEventHandler() : TServerEventHandler {
+ private var nextConnectionId = 1
+ override fun preServe() {
+ println(
+ "TServerEventHandler.preServe - called only once before server starts accepting connections"
+ )
+ }
+
+ override fun createContext(input: TProtocol, output: TProtocol): ServerContext {
+ // we can create some connection level data which is stored while connection is alive &
+ // served
+ val ctx = TestServerContext(nextConnectionId++)
+ println(
+ "TServerEventHandler.createContext - connection #" +
+ ctx.connectionId +
+ " established"
+ )
+ return ctx
+ }
+
+ override fun deleteContext(
+ serverContext: ServerContext,
+ input: TProtocol,
+ output: TProtocol
+ ) {
+ val ctx = serverContext.unwrap(TestServerContext::class.java)
+ println(
+ "TServerEventHandler.deleteContext - connection #" +
+ ctx.connectionId +
+ " terminated"
+ )
+ }
+
+ override fun processContext(
+ serverContext: ServerContext,
+ inputTransport: TTransport,
+ outputTransport: TTransport
+ ) {
+ val ctx = serverContext.unwrap(TestServerContext::class.java)
+ println(
+ "TServerEventHandler.processContext - connection #" +
+ ctx.connectionId +
+ " is ready to process next request"
+ )
+ }
+ }
+}
+
+fun main(args: Array<String>) {
+ try {
+ var port = 9090
+ var ssl = false
+ var zlib = false
+ var transportType = "buffered"
+ var protocolType = "binary"
+// var serverType = "thread-pool"
+ var serverType = "nonblocking"
+ val domainSocket = ""
+ var stringLimit: Long = -1
+ var containerLimit: Long = -1
+ try {
+ for (i in args.indices) {
+ if (args[i].startsWith("--port")) {
+ port = Integer.valueOf(args[i].split("=").toTypedArray()[1])
+ } else if (args[i].startsWith("--server-type")) {
+ serverType = args[i].split("=").toTypedArray()[1]
+ serverType.trim { it <= ' ' }
+ } else if (args[i].startsWith("--port")) {
+ port = args[i].split("=").toTypedArray()[1].toInt()
+ } else if (args[i].startsWith("--protocol")) {
+ protocolType = args[i].split("=").toTypedArray()[1]
+ protocolType.trim { it <= ' ' }
+ } else if (args[i].startsWith("--transport")) {
+ transportType = args[i].split("=").toTypedArray()[1]
+ transportType.trim { it <= ' ' }
+ } else if (args[i] == "--ssl") {
+ ssl = true
+ } else if (args[i] == "--zlib") {
+ zlib = true
+ } else if (args[i].startsWith("--string-limit")) {
+ stringLimit = args[i].split("=").toTypedArray()[1].toLong()
+ } else if (args[i].startsWith("--container-limit")) {
+ containerLimit = args[i].split("=").toTypedArray()[1].toLong()
+ } else if (args[i] == "--help") {
+ println("Allowed options:")
+ println(" --help\t\t\tProduce help message")
+ println(" --port=arg (=$port)\tPort number to connect")
+ println(
+ " --transport=arg (=$transportType)\n\t\t\t\tTransport: buffered, framed, fastframed, zlib"
+ )
+ println(
+ " --protocol=arg (=$protocolType)\tProtocol: binary, compact, json, multi, multic, multij"
+ )
+ println(" --ssl\t\t\tEncrypted Transport using SSL")
+ println(" --zlib\t\t\tCompressed Transport using Zlib")
+ println(
+ " --server-type=arg (=$serverType)\n\t\t\t\tType of server: simple, thread-pool, nonblocking, threaded-selector"
+ )
+ println(" --string-limit=arg (=$stringLimit)\tString read length limit")
+ println(
+ " --container-limit=arg (=$containerLimit)\tContainer read length limit"
+ )
+ exitProcess(0)
+ }
+ }
+ } catch (e: Exception) {
+ System.err.println("Can not parse arguments! See --help")
+ exitProcess(1)
+ }
+ try {
+ when (serverType) {
+ "simple" -> {}
+ "thread-pool" -> {}
+ "nonblocking" -> {
+ if (ssl) {
+ throw Exception("SSL is not supported over nonblocking servers!")
+ }
+ }
+ "threaded-selector" -> {
+ if (ssl) {
+ throw Exception("SSL is not supported over nonblocking servers!")
+ }
+ }
+ else -> {
+ throw Exception("Unknown server type! $serverType")
+ }
+ }
+ when (protocolType) {
+ "binary" -> {}
+ "compact" -> {}
+ "json" -> {}
+ "multi" -> {}
+ "multic" -> {}
+ "multij" -> {}
+ else -> {
+ throw Exception("Unknown protocol type! $protocolType")
+ }
+ }
+ when (transportType) {
+ "buffered" -> {}
+ "framed" -> {}
+ "fastframed" -> {}
+ "zlib" -> {}
+ else -> {
+ throw Exception("Unknown transport type! $transportType")
+ }
+ }
+ } catch (e: Exception) {
+ System.err.println("Error: " + e.message)
+ exitProcess(1)
+ }
+
+ // Processors
+ val testHandler = TestHandler()
+ val testProcessor = ThriftTestProcessor(testHandler, scope = GlobalScope)
+ val secondHandler = TestServer.SecondHandler()
+ val secondProcessor = SecondServiceProcessor(secondHandler, scope = GlobalScope)
+
+ // Protocol factory
+ val tProtocolFactory: TProtocolFactory =
+ when (protocolType) {
+ "json", "multij" -> {
+ TJSONProtocol.Factory()
+ }
+ "compact", "multic" -> {
+ TCompactProtocol.Factory(stringLimit, containerLimit)
+ }
+ else -> { // also covers multi
+ TBinaryProtocol.Factory(stringLimit, containerLimit)
+ }
+ }
+ val tTransportFactory: TTransportFactory =
+ when (transportType) {
+ "framed" -> {
+ TFramedTransport.Factory()
+ }
+ "fastframed" -> {
+ TFastFramedTransport.Factory()
+ }
+ "zlib" -> {
+ TZlibTransport.Factory()
+ }
+ else -> { // .equals("buffered") => default value
+ TTransportFactory()
+ }
+ }
+ val serverEngine: TServer
+ // If we are multiplexing services in one server...
+ val multiplexedProcessor = TMultiplexedProcessor()
+ multiplexedProcessor.registerDefault(testProcessor)
+ multiplexedProcessor.registerProcessor("ThriftTest", testProcessor)
+ multiplexedProcessor.registerProcessor("SecondService", secondProcessor)
+ if (serverType == "nonblocking" || serverType == "threaded-selector") {
+ // Nonblocking servers
+ val tNonblockingServerSocket =
+ TNonblockingServerSocket(NonblockingAbstractServerSocketArgs().port(port))
+ if (serverType.contains("nonblocking")) {
+ // Nonblocking Server
+ val tNonblockingServerArgs = TNonblockingServer.Args(tNonblockingServerSocket)
+ tNonblockingServerArgs.processor(
+ if (protocolType.startsWith("multi")) multiplexedProcessor else testProcessor
+ )
+ tNonblockingServerArgs.protocolFactory(tProtocolFactory)
+ tNonblockingServerArgs.transportFactory(tTransportFactory)
+ serverEngine = TNonblockingServer(tNonblockingServerArgs)
+ } else { // server_type.equals("threaded-selector")
+ // ThreadedSelector Server
+ val tThreadedSelectorServerArgs =
+ TThreadedSelectorServer.Args(tNonblockingServerSocket)
+ tThreadedSelectorServerArgs.processor(
+ if (protocolType.startsWith("multi")) multiplexedProcessor else testProcessor
+ )
+ tThreadedSelectorServerArgs.protocolFactory(tProtocolFactory)
+ tThreadedSelectorServerArgs.transportFactory(tTransportFactory)
+ serverEngine = TThreadedSelectorServer(tThreadedSelectorServerArgs)
+ }
+ } else {
+ // Blocking servers
+
+ // SSL socket
+ val tServerSocket: TServerSocket = if (ssl) {
+ TSSLTransportFactory.getServerSocket(port, 0)
+ } else {
+ TServerSocket(ServerSocketTransportArgs().port(port))
+ }
+ if (serverType == "simple") {
+ // Simple Server
+ val tServerArgs = TServer.Args(tServerSocket)
+ tServerArgs.processor(
+ if (protocolType.startsWith("multi")) multiplexedProcessor else testProcessor
+ )
+ tServerArgs.protocolFactory(tProtocolFactory)
+ tServerArgs.transportFactory(tTransportFactory)
+ serverEngine = TSimpleServer(tServerArgs)
+ } else { // server_type.equals("threadpool")
+ // ThreadPool Server
+ val tThreadPoolServerArgs = TThreadPoolServer.Args(tServerSocket)
+ tThreadPoolServerArgs.processor(
+ if (protocolType.startsWith("multi")) multiplexedProcessor else testProcessor
+ )
+ tThreadPoolServerArgs.protocolFactory(tProtocolFactory)
+ tThreadPoolServerArgs.transportFactory(tTransportFactory)
+ serverEngine = TThreadPoolServer(tThreadPoolServerArgs)
+ }
+ }
+
+ // Set server event handler
+ serverEngine.setServerEventHandler(TestServer.TestServerEventHandler())
+
+ // Run it
+ println(
+ "Starting the " +
+ (if (ssl) "ssl server" else "server") +
+ " [" +
+ protocolType +
+ "/" +
+ transportType +
+ "/" +
+ serverType +
+ "] on " +
+ if (domainSocket === "") "port $port" else "unix socket $domainSocket"
+ )
+ serverEngine.serve()
+ } catch (x: Exception) {
+ x.printStackTrace()
+ }
+ println("done.")
+}
diff --git a/lib/kotlin/cross-test-server/src/main/resources/logback.xml b/lib/kotlin/cross-test-server/src/main/resources/logback.xml
new file mode 100644
index 0000000..98309d7
--- /dev/null
+++ b/lib/kotlin/cross-test-server/src/main/resources/logback.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <!-- encoders are assigned the type
+ ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="debug">
+ <appender-ref ref="STDOUT"/>
+ </root>
+</configuration>
diff --git a/lib/kotlin/settings.gradle.kts b/lib/kotlin/settings.gradle.kts
new file mode 100644
index 0000000..f412707
--- /dev/null
+++ b/lib/kotlin/settings.gradle.kts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+rootProject.name = "libthrift-kotlin"
+
+include("cross-test-client", "cross-test-server")
+
+includeBuild("../java")