Kotlin annotations defined in tests not present in reflection info in integration tests - kotlin

We have a fairly standard Kotlin DSL Gradle build. We've added an integrationTest sourceSet and task:
plugins {
kotlin("jvm") version "1.3.72"
application
}
sourceSets {
create("integrationTest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
compileClasspath += sourceSets.test.get().output
compileClasspath += sourceSets.main.get().runtimeClasspath
runtimeClasspath += sourceSets.test.get().output
resources.srcDir(sourceSets.test.get().resources.srcDirs)
}
}
val integrationTest = task<Test>("integrationTest") {
description = "Runs the integration tests."
group = "verification"
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
classpath = sourceSets["integrationTest"].runtimeClasspath
mustRunAfter(tasks.test)
useJUnitPlatform()
}
Classes in src/integrationTest/kotlin can use classes from src/test/kotlin just fine, but annotations defined in src/test/kotlin do not show up in reflection data for classes in src/integrationTest/kotlin. When used on classes in src/test/kotlin, the annotations are present in reflection data as expected.
The annotations are very simple:
#Target(FUNCTION, CLASS)
// NB: Default Retention is RUNTIME (Annotation is stored in binary output and visible for reflection)
annotation class SystemProperty(val key: String, val value: String)
// Kotlin does not yet support repeatable annotations https://youtrack.jetbrains.com/issue/KT-12794
#Target(FUNCTION, CLASS)
annotation class SystemProperties(vararg val systemProperties: SystemProperty)
This is how the annotations are used, in a JUnit 5 Extension:
class SystemPropertyExtension : BeforeAllCallback {
override fun beforeAll(extensionContext: ExtensionContext) {
val clazz = extensionContext.requiredTestClass
clazz.getAnnotation(SystemProperty::class.java)?.let {
System.setProperty(it.key, it.value)
}
clazz.getAnnotation(SystemProperties::class.java)?.let {
it.systemProperties.forEach { prop -> System.setProperty(prop.key, prop.value) }
}
}
}
And typical use on the test itself:
#SystemProperty(key = "aws.s3.endpoint", value = "http://localstack:4566")
#ExtendWith(SystemPropertyExtension::class)
class SomeIntegrationTest {
//
}
Setting breakpoints while running tests shows System.setProperty(it.key, it.value) getting called. However while debugging integration tests, the breakpoint is not hit.
Any ideas on what might be wrong/missing here?
We could add a "testing" module to the project and export the test-jar, but would like to avoid that if possible.

The annotations were simply missing #Inherited. They were found on classes, but without #Inherited, they weren't found via superclass.

Related

Gradle + Kotest + KMongo Coroutines - Could not create instance of class

I'm writing a small application in Kotlin that uses KMongo coroutines and I want to use Kotest as the testing framework.
I wrote a simple test to access a database and retrieve a document:
class KabotMultiDBClientTest : StringSpec({
val client = KabotMultiDBClient(
mapOf(
System.getenv("TEST_DB_ID")!! to MongoCredentials(
System.getenv("DB_TEST_USER")!!,
System.getenv("DB_TEST_PWD")!!,
System.getenv("TEST_DB")!!,
System.getenv("DB_TEST_IP")!!,
System.getenv("DB_TEST_PORT")!!.toInt(),
)
)
)
"dummy test" { true shouldBe true }
})
When I test it using the IntelliJ Kotest plugin it works but If I use the
./gradlew kotest
command I receive this error.
Could not create instance of class org.wagham.db.KabotMultiDBClientTest
If I remove the client instantiation the the gradle task works without problems.
This is the code of the class:
class KabotMultiDBClient(
credentials: Map<String, MongoCredentials>
) {
private val databaseCache = credentials.keys.fold(mapOf<String, CoroutineDatabase>()) { acc, guildId ->
credentials[guildId]?.let {
acc + (guildId to
KMongo.createClient("mongodb://${it.username}:${it.password}#${it.ip}:${it.port}/${it.database}").coroutine.getDatabase(it.database))
} ?: throw InvalidCredentialsExceptions(guildId)
}
suspend fun getActiveCharacter(guildId: String, playerId: String): org.wagham.db.models.Character {
return databaseCache[guildId]?.let {
val col = it.getCollection<org.wagham.db.models.Character>("characters")
col.findOne(Document(mapOf("status" to "active", "player" to playerId)))
} ?: throw InvalidGuildException(guildId)
}
}
What could be the origin of the error?
I found the error, and it was far more trivial than I imagined: apparently, gradlew launched from the Windows terminal couldn't read the environment variables.
I modified the build.gradle.kts file in this way and everything worked fine:
tasks.withType<Test> {
useJUnitPlatform()
environment("VAR1", "value 1")
environment("VAR2", "value 2")
environment("VARN", "value N")
}

Co-routines in Kotlin multiplatform project

I'm trying to use "runTest()" in Kotlin multiplatform. I'm using Jetbrains's "Getting started"-project as an example. (https://kotlinlang.org/docs/multiplatform-library.html)
The problem is that runTest() does not find a coroutine context. It gives me the following build error:
Cannot access class 'kotlin.coroutines.CoroutineContext'. Check your module classpath for missing or conflicting dependencies
Here is my test:
class Base64JvmTest {
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun testNonAsciiString() {
runTest {
val utf8String = "Gödel"
val actual = Base64Factory.createEncoder().encodeToString(utf8String.toByteArray())
assertEquals("R8O2ZGVs", actual)
}
}
}
In build.gradle.kts, I set the following in kotlin.sourceSets:
val jvmTest by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
}
Please help me out - what am I missing?
As it turns out, there was an issue with Idea. I added the following dependency to get rid of the error:
dependencies {
commonTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
It shouldn't really be needed, as the common tests are not dependent on coroutines, but an acceptable work-around.
you can run runTest following way as documentation is suggested
#Test
fun exampleTest() = runTest {
val deferred = async {
delay(1_000)
async {
delay(1_000)
}.await()
}
deferred.await() // result available immediately
}
documentation code link

Custom Lint annotation detector is not triggered

I am trying to write my first Lint rule. For now I just want to detect the
use of the annotation #AnyThread.
I have created a module to implement my custom rule. The gradle file for this module is (I use the gradle plugin version 3.6.1):
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
compileOnly 'com.android.tools.lint:lint-api:26.6.1'
compileOnly 'com.android.tools.lint:lint-checks:26.6.1'
testImplementation "com.android.tools.lint:lint:26.6.1"
testImplementation "com.android.tools.lint:lint-tests:26.6.1"
testImplementation "com.android.tools:testutils:26.6.1"
testImplementation "junit:junit:4.12"
}
jar {
manifest {
attributes("Lint-Registry-v2": "com.test.lint.MyIssueRegistry")
}
}
My detector is:
package com.test.lint
//...
class AnyThreadAnnotationDetector: AbstractAnnotationDetector(), Detector.UastScanner {
companion object {
private const val AnyThreadId = "AnyThreadId"
const val AnyThreadDescription = "This is an attempt to find AnyThread annotation in code"
const val AnyThreadExplanation = "AnyThread annotation found!"
val ANYTHREAD_ANNOTATION_ISSUE = Issue.create(
id = AnyThreadId,
briefDescription = AnyThreadDescription,
explanation = AnyThreadExplanation,
category = Category.CORRECTNESS,
priority = 4,
severity = Severity.INFORMATIONAL,
implementation = Implementation(
AnyThreadAnnotationDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
override fun applicableAnnotations(): List<String>? = listOf("androidx.annotation.AnyThread")
override fun visitAnnotationUsage(
context: JavaContext,
usage: UElement,
type: AnnotationUsageType,
annotation: UAnnotation,
qualifiedName: String,
method: PsiMethod?,
annotations: List<UAnnotation>,
allMemberAnnotations: List<UAnnotation>,
allClassAnnotations: List<UAnnotation>,
allPackageAnnotations: List<UAnnotation>
) {
context.report(
issue = ANYTHREAD_ANNOTATION_ISSUE,
scope = usage,
location = context.getNameLocation(usage),
message = "A message"
)
}
}
My IssueRegistry is:
class MyIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = listOf(
AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
override val api: Int = CURRENT_API
}
I wrote some tests:
class AnyThreadAnnotationDetectorTest {
#Test
fun noAnnotatedFileKotlin() {
TestLintTask.lint()
.files(
LintDetectorTest.kotlin(
"""
|package foo;
|
|class XmlHttpRequest {
|}""".trimMargin()
)
)
.allowMissingSdk()
.issues(AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
.run()
.expectClean()
}
#Test
fun annotatedKotlinMethod() {
TestLintTask.lint()
.files(
LintDetectorTest.kotlin(
"""
|package foo;
|
|import androidx.annotation.AnyThread
|
|class XmlHttpRequest {
|#AnyThread
|fun test(){}
|}""".trimMargin()
)
)
.allowMissingSdk()
.issues(AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
.run()
.expect(
"""
Just a test to find annotations
0 errors, 0 warnings
""".trimIndent()
)
}
#Test
fun testNoisyDetector() {
TestLintTask.lint().files(Stubs.ANYTHREAD_EXPERIMENT)
.allowMissingSdk()
.issues(AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
.run()
.expect(
"""
Just a test to find annotations
0 errors, 0 warnings
""".trimIndent()
)
}
}
Where the Stubs.ANYTHREAD_EXPERIMENT is:
object Stubs {
val ANYTHREAD_EXPERIMENT = kotlin(
"com/test/applicationlintdemoapp/AnythreadAnnotationStubs.kt",
"""
package com.test.applicationlintdemoapp
import androidx.annotation.AnyThread
class AnythreadClassExperiment {
#AnyThread
fun setTimeToNow() {
TimeTravelProvider().setTime(System.currentTimeMillis())
}
#AnyThread
fun setTimeToEpoch() {
TimeTravelProvider().setTime(0)
}
fun violateTimeTravelAccords() {
TimeTravelProvider().setTime(-1)
}
}
"""
).indented().within("src")
}
All my test fail (except noAnnotatedFileKotlin), actually if I put a breakpoint on
the call to context.report the test made in debug mode is never paused, meaning
that the annotation androidx.annotation.AnyThread is never detected.
What could go wrong ? what did I miss?
I have seen and read a some docs:
Writing custom lint rules
KotlinConf 2017 - Kotlin Static Analysis with Android Lint
Writing your first Lint check
Making Custom Lint for Kotlin Code
Getting the Most Out of Android Lint
Coding in Style: Static Analysis with Custom Lint Rules
And I controlled the configuration by implementing the NoisyDetector given
in the talk Coding in Style: Static Analysis with Custom Lint Rules, the result
of the test are fine.
I might be a little late to answer this, but it might be useful for other people who run into this question
I'm having the same problem, I need to find usages of an Annotation and report them. But for some reason the Kotlin UAST (Java works fine) doesn't record/report annotations. I'm using a sort of workaround to get through this
Workaround
Instead of visiting annotations, I'm visiting UMethod or UClass depending on what you need. Then I'm doing a manual String.contains() check on the node.sourcePsi.text to see if the annotation is there
override fun getApplicableUastTypes() = listOf(UMethod::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
override fun visitMethod(node: UMethod) {
if (!shouldSkip(node.sourcePsi) && node.isAnnotatedWith("AnyThread")) {
context.report(
issue = ANYTHREAD_ANNOTATION_ISSUE,
scope = usage,
location = context.getNameLocation(usage),
message = "A message"
)
}
}
}
// Skip KtClass, because it calls the `visitMethod` method since the class has the constructor method in it
private fun shouldSkip(node: PsiElement?): Boolean = node is KtClass
}
fun UAnnotated.isAnnotatedWith(annotation: String) = sourcePsi?.text?.contains("#$annotation") == true
Drawbacks
The problem I see with this is that it will be called for every method instead of only when the annotation is found, and the shouldSkip() method seems like a hack to me. But other than that it works correctly and should report problems
Note: Calling node.hasAnnotation(), node.findAnnotation() or context.evaluator.hasAnnotation() will not find annotations in Kotlin
You can add stubs for the #AnyThread annotation by adding SUPPORT_ANNOTATIONS_JAR to the lint().files(...) call or manually declaring the #AnyThread annotation class in a separate test source file.
An example of using SUPPORT_ANNOTATIONS_JAR inside of CheckResultDetectorTest can be found here.

Kotlin script eval and Java 11 with inline function in the DSL

Our project decided to make the "jump" and move to open implementations, and grabbing the opportunity to move the Kotlin Java compatibility to version 11.
It introduced several 3rd-party library compatibility challenges, but I was able to solve them all.
Now I use Kotlin 1.3.50 (with the new JSR223 engine) with Java 11 target and builds and runs correctly.
plugins {
kotlin("jvm") version "1.3.50"
}
dependencies {
compile(kotlin("stdlib"))
implementation(kotlin("scripting-jsr223-embeddable"))
}
configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}
The application uses complex DSL scripts in the form of kts. They worked perfectly prior upgrading to 11, but fails to evaluate since switching to version 11.
When the code calls the eval:
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
val building = engine.eval(src) as Building
it throws class version incompatibility exception:
javax.script.ScriptException: Cannot inline bytecode built
with JVM target 11 into bytecode that is being built with
JVM target 1.8. Please specify proper '-jvm-target' option
...
From this error, I can't determine whether the script engine compiles the script into Java 8 class and then tries to add the result to the core code; or the script tries to use a class which is not in Java 11 format.
How can fix this incompatibility or how could I debug all the project (including 3rd party) classes of their class file version?
Note: The DSL is too complex to give a clear and simple example, however I was able to eval the script of "2+2" without problem.
EDIT 1: A simplified example
After a long lets-comment-out session, I was able to locate the problem: it fails when I use a reified inline function in the script. This discovery made it possible to create a simple example:
package hu.app
import javax.script.ScriptEngineManager
import kotlin.reflect.KClass
/**
* A class representing a type-value pair
*/
class TypedProperty<T : Any>(val type: KClass<T>, initialValue: T? = null) {
var value: T? = initialValue
companion object {
inline fun <reified T : Any> create(initialValue: T? = null) = TypedProperty(T::class, initialValue)
}
}
/**
* The root object the DSL creates. Body omitted for simplicity.
*/
class Building {
// Class body is omitted for simplicity
}
/**
* The simple DSL
*/
#DslMarker
annotation class MyDsl
#MyDsl
class PropertyBuilder constructor() {
#MyDsl
val properties: Map<String, TypedProperty<*>> = mutableMapOf()
/**
* An extension function for String allowing the creation of a freely typed, but typed properties.
* This function causes the problem!
*/
#MyDsl
inline infix fun <reified T : Any> String.set(value: T?) {
val p = (properties as MutableMap).getOrPut(this, { TypedProperty.create(value) })
if (T::class == p.type)
(p as TypedProperty<T>).apply { this.value = value }
else
throw RuntimeException("Property '$this' is reassigned with different type. Original: ${p.type.simpleName}, new: ${T::class.simpleName}")
}
}
// Root of the DSL
#MyDsl
fun building(id: String = "Unnamed", op: BuildingBuilder.() -> Unit) = BuildingBuilder(id).apply(op).build()
// An interface for DSL classes with property support
#MyDsl
interface HasPropertiesBuilder {
#MyDsl
val properties: PropertyBuilder
#MyDsl
fun properties(op: PropertyBuilder.() -> Unit) = properties.op()
}
// The builder of the root object: it has properties
class BuildingBuilder(val id: String) : HasPropertiesBuilder {
override val properties = PropertyBuilder()
// This function body is omitted for simplification
fun build() : Building = Building()
}
fun main() {
// Initialize the engine (new 1.3.50 engine is used)
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
// Evaluation
val res = engine.eval("""
import hu.pmi.autoparking.app.*
building {
properties {
"X" set 42
}
}
""".trimIndent()) as Building
}
In the above example, there is an extension function on String letting to create typed properties. It has reified type parameter, so it should be inline. This method cause the exception: if the "X" set 42 line is removed, the script compiles.
Digging deeper, now it is clear, that the script engine compiles into Java 8 and therefor, when it tries to inject the inline function which is in Java 11 class format, it fails.
So the new question: how could I tell the JSR223 kotlin engine to compile to version 11?

Using Akka Route TestKit with Kotlin Spek

I am trying to test my AkkaHTTP routes (written in Kotlin) using akka-http-testkit. The tests in our project use Spek and I would like to keep it this way.
The Route TestKit tutorial gives a Java example:
public class TestkitExampleTest extends JUnitRouteTest {
TestRoute appRoute = testRoute(new MyAppService().createRoute())
#Test
public void testCalculatorAdd() {
// test happy path
appRoute.run(HttpRequest.GET("/calculator/add?x=4.2&y=2.3"))
.assertStatusCode(200)
.assertEntity("x + y = 6.5")
// test responses to potential errors
appRoute.run(HttpRequest.GET("/calculator/add?x=3.2"))
.assertStatusCode(StatusCodes.NOT_FOUND) // 404
.assertEntity("Request is missing required query parameter 'y'")
// test responses to potential errors
appRoute.run(HttpRequest.GET("/calculator/add?x=3.2&y=three"))
.assertStatusCode(StatusCodes.BAD_REQUEST)
.assertEntity("The query parameter 'y' was malformed:\n" +
"'three' is not a valid 64-bit floating point value")
}
}
The setup uses the testRoute function, which means the test class must extend JUnitRouteTest.
Attempting to translate to a Kotlin Spek test I got this:
class TestKitExampleTest : JUnitRouteTest(), Spek({
describe("My routes") {
val appRoute = testRoute(MyAppService().createRoute())
it("calculator add") {
// test happy path
appRoute.run(HttpRequest.GET("/calculator/add?x=4.2&y=2.3"))
.assertStatusCode(200)
.assertEntity("x + y = 6.5")
//...rest omitted
}
}
})
which does not compile as the class is attempting to inherit two classes. I converted it to the following instead:
class TestKitExampleTest : Spek({
describe("My routes") {
val appRoute = testRoute(MyAppService().createRoute())
it("calculator add") {
// test happy path
appRoute.run(HttpRequest.GET("/calculator/add?x=4.2&y=2.3"))
.assertStatusCode(200)
.assertEntity("x + y = 6.5")
//...rest omitted
}
}
}) {
companion object : JUnitRouteTest()
}
which encouters the runtime error java.lang.IllegalStateException: Unknown factory null
at akka.http.impl.util.package$.actorSystem(package.scala:34).
Is there a way to use Akka's route testkit with Spek? Or is there another way to test these routes?
As #raniejade mentioned above, answered on Github. JUnitRouteTest bootstraps Akka with a rule, but Spek's LifeCycleListener can do the same thing.
Adding the code:
class SpekRouteBootstrapper: LifecycleListener, JUnitRouteTest() {
override fun beforeExecuteTest(test: TestScope) {
systemResource().before()
}
override fun afterExecuteTest(test: TestScope) {
systemResource().after()
}
}
allowed me to do this on the test class:
class TestKitExampleTest: Spek({
val bootstrapper = SpekRouteBootstrapper()
registerListener(bootstrapper)
describe("My routes") {
val appRoute by memoized {
bootstrapper.testRoute(MyAppService().createRoute())
}
it("calculator add") {
// test happy path
appRoute.run(HttpRequest.GET("/calculator/add?x=4.2&y=2.3"))
.assertStatusCode(200)
.assertEntity("x + y = 6.5")
}
}
})