javaandroidjvmkotlinkotlinc

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.


Solution

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