Add kotlin-script-util lib with tests

- collection of standard templates and resolvers
This commit is contained in:
Ilya Chernikov
2016-09-09 13:41:24 +02:00
parent 42a4f0ed70
commit f992f91686
14 changed files with 598 additions and 0 deletions
+1
View File
@@ -104,6 +104,7 @@
<module>tools/kotlin-gradle-plugin-api</module>
<module>tools/kotlin-android-extensions</module>
<module>tools/kotlin-maven-plugin-test</module>
<module>tools/kotlin-script-util</module>
<module>examples/kotlin-java-example</module>
<module>examples/js-example</module>
+114
View File
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<properties>
<maven-plugin-anno.version>1.4.1</maven-plugin-anno.version>
<maven.version>3.0.4</maven.version>
</properties>
<parent>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-project</artifactId>
<version>1.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>kotlin-script-util</artifactId>
<packaging>jar</packaging>
<description>Kotlin scripting support utilities</description>
<repositories>
<repository>
<id>jetbrains-utils</id>
<url>http://repository.jetbrains.com/utils</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-compiler</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-aether</artifactId>
<version>0.10.1</version>
</dependency>
<dependency>
<groupId>org.sonatype.aether</groupId>
<artifactId>aether-api</artifactId>
<version>1.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${project.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals> <goal>compile</goal> </goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals> <goal>test-compile</goal> </goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.6</version>
<configuration>
<systemPropertyVariables>
<runtimeJar>${org.jetbrains.kotlin:kotlin-stdlib:jar}</runtimeJar>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.script.util
@Target(AnnotationTarget.FILE, AnnotationTarget.EXPRESSION, AnnotationTarget.LOCAL_VARIABLE, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class DependsOn(val value: String)
@Target(AnnotationTarget.FILE, AnnotationTarget.EXPRESSION, AnnotationTarget.LOCAL_VARIABLE, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Repository(val value: String)
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.script.util
import org.jetbrains.kotlin.script.*
import org.jetbrains.kotlin.script.util.resolvers.DirectResolver
import org.jetbrains.kotlin.script.util.resolvers.FlatLibDirectoryResolver
import org.jetbrains.kotlin.script.util.resolvers.MavenResolver
import org.jetbrains.kotlin.script.util.resolvers.Resolver
import java.io.File
import java.lang.Exception
import java.lang.IllegalArgumentException
import java.net.URL
import java.net.URLClassLoader
import java.util.concurrent.Future
import kotlin.reflect.KClass
open class KotlinAnnotatedScriptDependenciesResolver(val baseClassPath: List<File>, resolvers: Iterable<Resolver>)
: ScriptDependenciesResolver
{
private val resolvers: MutableList<Resolver> = resolvers.toMutableList()
@AcceptedAnnotations(DependsOn::class, Repository::class)
override fun resolve(script: ScriptContents,
environment: Map<String, Any?>?,
report: (ScriptDependenciesResolver.ReportSeverity, String, ScriptContents.Position?) -> Unit,
previousDependencies: KotlinScriptExternalDependencies?
): Future<KotlinScriptExternalDependencies?>
= (if (previousDependencies != null && resolveFromAnnotations(script).isEmpty()) previousDependencies
else
object : KotlinScriptExternalDependencies {
override val classpath: Iterable<File> = if (resolvers.isEmpty()) baseClassPath else baseClassPath + resolveFromAnnotations(script)
override val imports: Iterable<String> =
previousDependencies?.let { emptyList<String>() } ?: listOf(DependsOn::class.java.`package`.name + ".*")
}
).asFuture()
private fun resolveFromAnnotations(script: ScriptContents): List<File> {
script.annotations.forEach {
when (it) {
is Repository ->
when {
File(it.value).run { exists() && isDirectory } -> resolvers.add(FlatLibDirectoryResolver(File(it.value)))
else -> throw IllegalArgumentException("Illegal argument for Repository annotation: ${it.value}")
}
is DependsOn -> {}
is InvalidScriptResolverAnnotation -> throw Exception("Invalid annotation ${it.name}", it.error)
else -> throw Exception("Unknown annotation ${it.javaClass}")
}
}
return script.annotations.filterIsInstance(DependsOn::class.java).flatMap { dep ->
resolvers.asSequence().mapNotNull { it.tryResolve(dep) }.firstOrNull() ?:
throw Exception("Unable to resolve dependency $dep")
}
}
}
private fun URL.toFile() =
try {
File(toURI().schemeSpecificPart)
}
catch (e: java.net.URISyntaxException) {
if (protocol != "file") null
else File(file)
}
private fun classpathFromClassloader(classLoader: ClassLoader): List<File>? =
generateSequence(classLoader) { it.parent }.toList().flatMap { (it as? URLClassLoader)?.urLs?.mapNotNull { it.toFile() } ?: emptyList() }
private fun classpathFromClasspathProperty(): List<File>? =
System.getProperty("java.class.path")?.let {
it.split(String.format("\\%s", File.pathSeparatorChar).toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
.map(::File)
}
private fun classpathFromClass(classLoader: ClassLoader, klass: KClass<out Any>): List<File>? {
val clp = "${klass.qualifiedName?.replace('.', '/')}.class"
val url = classLoader.getResource(clp)
return url?.toURI()?.path?.removeSuffix(clp)?.let {
listOf(File(it))
}
}
val defaultScriptBaseClasspath: List<File> by lazy {
classpathFromClass(Thread.currentThread().contextClassLoader, KotlinAnnotatedScriptDependenciesResolver::class)
?: classpathFromClasspathProperty()
?: classpathFromClassloader(Thread.currentThread().contextClassLoader)
?: emptyList()
}
class DefaultKotlinResolver() :
KotlinAnnotatedScriptDependenciesResolver(defaultScriptBaseClasspath, arrayListOf())
class DefaultKotlinAnnotatedScriptDependenciesResolver :
KotlinAnnotatedScriptDependenciesResolver(defaultScriptBaseClasspath, arrayListOf(DirectResolver(), MavenResolver()))
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.script.util.resolvers
import org.jetbrains.kotlin.script.util.DependsOn
import java.io.File
import java.lang.IllegalArgumentException
interface Resolver {
fun tryResolve(dependsOn: DependsOn): Iterable<File>?
}
class DirectResolver : Resolver {
override fun tryResolve(dependsOn: DependsOn): Iterable<File>? =
if (dependsOn.value.isNotBlank() && File(dependsOn.value).exists()) listOf(File(dependsOn.value)) else null
}
class FlatLibDirectoryResolver(val path: File) : Resolver {
init {
if (!path.exists() || !path.isDirectory) throw IllegalArgumentException("Invalid flat lib directory repository path '$path'")
}
override fun tryResolve(dependsOn: DependsOn): Iterable<File>? =
when {
dependsOn.value.isNotBlank() && File(path, dependsOn.value).exists() -> listOf(File(path, dependsOn.value))
// TODO: add coordinates and wildcard matching
else -> null
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.script.util.resolvers
import com.jcabi.aether.Aether
import org.jetbrains.kotlin.script.util.DependsOn
import java.io.File
import java.util.*
import org.sonatype.aether.repository.RemoteRepository
import org.sonatype.aether.resolution.DependencyResolutionException
import org.sonatype.aether.util.artifact.DefaultArtifact
import org.sonatype.aether.util.artifact.JavaScopes
val mavenCentral = RemoteRepository("maven-central", "default", "http://repo1.maven.org/maven2/")
class MavenResolver(val reportError: ((String) -> Unit) = {}): Resolver {
// TODO: make robust
val localRepo = File(File(System.getProperty("user.home")!!, ".m2"), "repository")
val repos: ArrayList<RemoteRepository> = arrayListOf()
private fun currentRepos() = if (repos.isEmpty()) arrayListOf(mavenCentral) else repos
override fun tryResolve(dependsOn: DependsOn): Iterable<File>? {
if (dependsOn.value.count { it == ':' } == 2) {
try {
val deps = Aether(currentRepos(), localRepo).resolve(
DefaultArtifact(dependsOn.value),
JavaScopes.RUNTIME)
if (deps != null)
return deps.map { it.file }
else {
reportError("resolving [${dependsOn.value}] failed: no results")
}
}
catch (e: DependencyResolutionException) {
reportError("resolving [${dependsOn.value}] failed: $e")
}
return listOf()
}
return null
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("unused") // an API
package org.jetbrains.kotlin.script.util
import org.jetbrains.kotlin.script.ScriptTemplateDefinition
@ScriptTemplateDefinition(resolver = DefaultKotlinResolver::class, scriptFilePattern = ".*\\.kts")
abstract class StandardScript(val args: Array<String>)
@ScriptTemplateDefinition(resolver = DefaultKotlinAnnotatedScriptDependenciesResolver::class, scriptFilePattern = ".*\\.kts")
abstract class StandardScriptWithAnnotatedResolving(val args: Array<String>)
@ScriptTemplateDefinition(resolver = DefaultKotlinResolver::class, scriptFilePattern = ".*\\.kts")
abstract class ScriptWithBindings(val bindings: Map<String, Any?>)
@ScriptTemplateDefinition(resolver = DefaultKotlinAnnotatedScriptDependenciesResolver::class, scriptFilePattern = ".*\\.kts")
abstract class ScriptWithBindingsAndAnnotatedResolving(val bindings: Map<String, Any?>)
@@ -0,0 +1,161 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.script.util
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoot
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
import org.jetbrains.kotlin.codegen.CompilationException
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.config.addKotlinSourceRoot
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromTemplate
import org.jetbrains.kotlin.utils.PathUtil
import org.junit.Assert
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import kotlin.reflect.KClass
class ScriptUtilIT {
companion object {
private val argsHelloWorldOutput =
"""Hello, world!
a1
done
"""
private val bindingsHelloWorldOutput =
"""Hello, world!
a1 = 42
done
"""
}
@Test
fun testArgsHelloWorld() {
val scriptClass = compileScript("args-hello-world.kts", StandardScript::class)
Assert.assertNotNull(scriptClass)
val ctor = scriptClass?.getConstructor(Array<String>::class.java)
Assert.assertNotNull(ctor)
captureOut {
ctor!!.newInstance(arrayOf("a1"))
}.let {
Assert.assertEquals(argsHelloWorldOutput, it)
}
}
@Test
fun testBndHelloWorld() {
val scriptClass = compileScript("bindings-hello-world.kts", ScriptWithBindings::class)
Assert.assertNotNull(scriptClass)
val ctor = scriptClass?.getConstructor(Map::class.java)
Assert.assertNotNull(ctor)
captureOut {
ctor!!.newInstance(hashMapOf("a1" to 42))
}.let {
Assert.assertEquals(bindingsHelloWorldOutput, it)
}
}
@Test
fun testResolveStdHelloWorld() {
Assert.assertNull(compileScript("args-junit-hello-world.kts", StandardScript::class))
val scriptClass = compileScript("args-junit-hello-world.kts", StandardScriptWithAnnotatedResolving::class)
Assert.assertNotNull(scriptClass)
captureOut {
scriptClass!!.getConstructor(Array<String>::class.java)!!.newInstance(arrayOf("a1"))
}.let {
Assert.assertEquals(argsHelloWorldOutput, it)
}
}
private fun compileScript(
scriptFileName: String,
scriptTemplate: KClass<out Any>,
environment: Map<String, Any?>? = null,
runIsolated: Boolean = true,
suppressOutput: Boolean = false): Class<*>? =
compileScriptImpl("src/test/resources/scripts/" + scriptFileName, KotlinScriptDefinitionFromTemplate(scriptTemplate, null, null, environment), runIsolated, suppressOutput)
private fun compileScriptImpl(
scriptPath: String,
scriptDefinition: KotlinScriptDefinition,
runIsolated: Boolean,
suppressOutput: Boolean): Class<*>?
{
val paths = PathUtil.getKotlinPathsForDistDirectory()
val messageCollector =
if (suppressOutput) MessageCollector.NONE
else PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false)
val rootDisposable = Disposer.newDisposable()
try {
val configuration = CompilerConfiguration().apply {
addJvmClasspathRoots(PathUtil.getJdkClassesRoots())
val rtJar = System.getProperty("runtimeJar")
Assert.assertNotNull(rtJar)
addJvmClasspathRoot(File(rtJar))
put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector)
addKotlinSourceRoot(scriptPath)
put(CommonConfigurationKeys.MODULE_NAME, "kotlin-script-util-test")
add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, scriptDefinition)
put(JVMConfigurationKeys.RETAIN_OUTPUT_IN_MEMORY, true)
put(JVMConfigurationKeys.INCLUDE_RUNTIME, true)
}
val environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES)
try {
return if (runIsolated) KotlinToJVMBytecodeCompiler.compileScript(environment, paths)
else KotlinToJVMBytecodeCompiler.compileScript(environment, this.javaClass.classLoader)
}
catch (e: CompilationException) {
messageCollector.report(CompilerMessageSeverity.EXCEPTION, OutputMessageUtil.renderException(e),
MessageUtil.psiElementToMessageLocation(e.element))
return null
}
catch (t: Throwable) {
MessageCollectorUtil.reportException(messageCollector, t)
throw t
}
}
finally {
Disposer.dispose(rootDisposable)
}
}
private fun captureOut(body: () -> Unit): String {
val outStream = ByteArrayOutputStream()
val prevOut = System.out
System.setOut(PrintStream(outStream))
body()
System.out.flush()
System.setOut(prevOut)
return outStream.toString()
}
}
@@ -0,0 +1,8 @@
println("Hello, world!")
if (args.isNotEmpty()) {
println(args.joinToString())
}
println("done")
@@ -0,0 +1,12 @@
@file:DependsOn("junit:junit:4.11")
org.junit.Assert.assertTrue(true)
println("Hello, world!")
if (args.isNotEmpty()) {
println(args.joinToString())
}
println("done")
@@ -0,0 +1,8 @@
println("Hello, world!")
if (bindings.isNotEmpty()) {
println(bindings.entries.joinToString { "${it.key} = ${it.value}" })
}
println("done")
@@ -0,0 +1,8 @@
println("Hello, world!")
if (bindings.isNotEmpty()) {
println(bindings.joinToString { "${it.key} = ${it.value}" })
}
println("done")
@@ -0,0 +1,8 @@
println("Hello, world!")
if (args.isNotEmpty()) {
println(args.joinToString())
}
println("done")
@@ -0,0 +1,8 @@
println("Hello, world!")
if (args.isNotEmpty()) {
println(args.joinToString())
}
println("done")