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

2

10

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.

Polemics answered 25/8, 2017 at 19:12 Comment(0)
P
5

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

Polemics answered 17/9, 2017 at 17:0 Comment(1)
I tried the code but 3 things I am missing: MockClasswriter,codegenOutputFile and compileOutputDirVernacularism
C
0

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)

Clam answered 2/9, 2017 at 20:46 Comment(5)
Is there any way to use the JSR 223 API to do this? I'm not too familiar with itPolemics
JSR 223 will just let you add scripting ability to your application, it will be easier to tune your application, but it won't help you with RAM drives or compiling in RAM.Clam
Thats's what the REPL runs with, no? Possible to use that to define each type that I compile one by one and then just test that way? I've been through the kotlin source but I can't really figure out how to run the REPL programaticallyPolemics
IMHO: The REPL is the command line utility. One the other hands the JSR 233 allows you to run pre-written scripts inside your application without the REPL. I've already used it to run Beanshell and Jython scripts inside applications, this helps creating user defined behaviors for example.Clam
regardless of 223 I thing that figuring out the REPL could be much easier than manual compile & classloading. I'll be testing with strings at the very least but that's not hard since I still have the classes in IR. Couldn't find anything about it anywhere though so it might take some workPolemics

© 2022 - 2024 — McMap. All rights reserved.