How do I run tests compiling a kotlin file in memory and check the result?

1.9k views Asked by At

So far I have

import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler

  MyProjectCompiler.initialize("SampleKtFileOutput")
    .packageName("com.test.sample")
    .compile(File(someFile.path))
    .result { ktSource: String -> K2JVMCompiler()
       .exec(System.out, /** arguments here?*/) }

This manually starts the compiler, but I would like to compile the resulting String from the first compiler (MyProjectCompiler which generates kotlin source) in-memory and check the result without writing to a file.

I would like to include everything on the current classpath if possible.

2

There are 2 answers

1
Preston Garno On BEST ANSWER

I found the easiest way to do it is to use something like the code in the original question and use java.io.tmpdir. Here's a re-usable solution:

Add the kotlin compiler as a test dependency:

testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler', version: "$kotlin_version"

Wrapper for the compiler:

object JvmCompile {

  fun exe(input: File, output: File): Boolean = K2JVMCompiler().run {
    val args = K2JVMCompilerArguments().apply {
      freeArgs = listOf(input.absolutePath)
      loadBuiltInsFromDependencies = true
      destination = output.absolutePath
      classpath = System.getProperty("java.class.path")
          .split(System.getProperty("path.separator"))
          .filter {
            it.asFile().exists() && it.asFile().canRead()
          }.joinToString(":")
      noStdlib = true
      noReflect = true
      skipRuntimeVersionCheck = true
      reportPerf = true
    }
    output.deleteOnExit()
    execImpl(
        PrintingMessageCollector(
            System.out,
            MessageRenderer.WITHOUT_PATHS, true),
        Services.EMPTY,
        args)
  }.code == 0

}

Classloader for creating objects from the compiled classes:

class Initializer(private val root: File) {

  val loader = URLClassLoader(
      listOf(root.toURI().toURL()).toTypedArray(),
      this::class.java.classLoader)

  @Suppress("UNCHECKED_CAST") 
  inline fun <reified T> loadCompiledObject(clazzName: String): T? 
      = loader.loadClass(clazzName).kotlin.objectInstance as T

  @Suppress("UNCHECKED_CAST") 
  inline fun <reified T> createInstance(clazzName: String): T? 
      = loader.loadClass(clazzName).kotlin.createInstance() as T

}

Example test case:

First make a kotlin source file

MockClasswriter("""
    |
    |package com.test
    |
    |class Example : Consumer<String> {
    |  override fun accept(value: String) {
    |    println("found: '$\value'")
    |  }
    |}
    """.trimMargin("|"))
    .writeToFile(codegenOutputFile)

Make sure it compiles:

assertTrue(JvmCompile.exe(codegenOutputFile, compileOutputDir))

Load the class as interface instance

Initializer(compileOutputDir)
      .createInstance<Consumer<String>>("com.test.Example")
      ?.accept("Hello, world!")

The output will be as expected: found: 'Hello, world!'

5
Xvolks On

Reading the source of the K2JVMCompiler class, it seems that the compiler only supports compilation for files. Digging deeper, it seems overcomplicated to fake the entries of org.jetbrains.kotlin.codegen.KotlinCodegenFacade static method compileCorrectFiles.

Your best guess it to use a file system to do this. A temporary RAM disk may suit your needs. (This is built-in macOS for example)