Preston Garno
Preston Garno

Reputation: 1225

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

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.

Upvotes: 10

Views: 2114

Answers (2)

Preston Garno
Preston Garno

Reputation: 1225

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!'

Upvotes: 5

Xvolks
Xvolks

Reputation: 2155

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)

Upvotes: 0

Related Questions