diff --git a/.travis.yml b/.travis.yml index d7ab9fa7..740b8661 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,7 @@ install: # - bash -c 'source "/home/travis/.sdkman/bin/sdkman-init.sh"; yes | sdk install java 8u144-zulu' # - sudo apt-get install -y openjdk-8-jdk - sdk install kotlin - - sdk install gradle + - sdk install gradle 4.10.2 - export KSCRIPT_HOME=${TRAVIS_BUILD_DIR} - export PATH=${KSCRIPT_HOME}/build/libs:$PATH diff --git a/build.gradle.kts b/build.gradle.kts index d741f34e..06a0393d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { testCompile("junit:junit:4.12") testCompile( "io.kotlintest:kotlintest:2.0.7") + testImplementation("io.mockk:mockk:1.8.13") testCompile(kotlin("script-runtime")) } diff --git a/src/main/kotlin/kscript/app/AppHelpers.kt b/src/main/kotlin/kscript/app/AppHelpers.kt index 50b036fe..745bc810 100644 --- a/src/main/kotlin/kscript/app/AppHelpers.kt +++ b/src/main/kotlin/kscript/app/AppHelpers.kt @@ -20,6 +20,25 @@ data class ProcessResult(val command: String, val exitCode: Int, val stdout: Str } } +fun isWindows (): Boolean { + return System.getProperty("os.name").toLowerCase().startsWith("windows") +} + +fun isCygwinOrMingw() : Boolean { + val result = evalCommand("uname") + // `uname` will fail on Windows cmd returning 1 as exitCode + return result.exitCode == 0 && (result.stdout.startsWith("CYGWIN") || result.stdout.startsWith("MINGW")) +} + +fun evalCommand(cmd: String, wd: File? = null, + stdoutConsumer: Consumer = StringBuilderConsumer(), + stderrConsumer: Consumer = StringBuilderConsumer()): ProcessResult { + + val (shell, shellArg0) = if (isWindows()) Pair("cmd", "/C") else Pair("bash", "-c") + return runProcess(shell, shellArg0, cmd, + wd = wd, stderrConsumer = stderrConsumer, stdoutConsumer = stdoutConsumer) +} + fun evalBash(cmd: String, wd: File? = null, stdoutConsumer: Consumer = StringBuilderConsumer(), stderrConsumer: Consumer = StringBuilderConsumer()): ProcessResult { @@ -89,7 +108,7 @@ internal open class StringBuilderConsumer : Consumer { object ShellUtils { - fun isInPath(tool: String) = evalBash("which $tool").stdout.trim().isNotBlank() + fun isInPath(tool: String) = evalCommand( (if (isWindows()) "where" else "which") + " $tool").stdout.trim().isNotBlank() fun requireInPath(tool: String, msg: String = "$tool is not in PATH") = errorIf(!isInPath(tool)) { msg } @@ -122,9 +141,19 @@ fun quit(status: Int): Nothing { /** see discussion on https://github.com/holgerbrandl/kscript/issues/15*/ fun guessKotlinHome(): String? { - return evalBash("KOTLIN_RUNNER=1 JAVACMD=echo kotlinc").stdout.run { - "kotlin.home=([^\\s]*)".toRegex() - .find(this)?.groups?.get(1)?.value + if (isWindows()) { + return evalCommand("where kotlinc").run { + if (exitCode == 1) + null + else + File(stdout.split("\n")[0].trim()) + .parentFile.parent + } + } else { + return evalBash("KOTLIN_RUNNER=1 JAVACMD=echo kotlinc").stdout.run { + "kotlin.home=([^\\s]*)".toRegex() + .find(this)?.groups?.get(1)?.value + } } } @@ -352,7 +381,8 @@ exec java -jar ${'$'}0 "${'$'}@" File(tmpProjectDir, "build.gradle").writeText(gradleScript) - val pckgResult = evalBash("cd '${tmpProjectDir}' && gradle simpleCapsule") + // wrap chained commands (&&) with additional " for Windows + val pckgResult = evalCommand("gradle --no-daemon -p \"${tmpProjectDir}\" simpleCapsule") with(pckgResult) { kscript.app.errorIf(exitCode != 0) { "packaging of '$appName' failed:\n$pckgResult" } diff --git a/src/main/kotlin/kscript/app/DependencyUtil.kt b/src/main/kotlin/kscript/app/DependencyUtil.kt index 94a24c50..428c296d 100644 --- a/src/main/kotlin/kscript/app/DependencyUtil.kt +++ b/src/main/kotlin/kscript/app/DependencyUtil.kt @@ -11,7 +11,7 @@ import java.io.File val DEP_LOOKUP_CACHE_FILE = File(KSCRIPT_CACHE_DIR, "dependency_cache.txt") -val CP_SEPARATOR_CHAR = if (System.getProperty("os.name").toLowerCase().contains("windows")) ";" else ":" +val CP_SEPARATOR_CHAR = if (isWindows()) ";" else ":" fun resolveDependencies(depIds: List, customRepos: List = emptyList(), loggingEnabled: Boolean): String? { @@ -34,12 +34,11 @@ fun resolveDependencies(depIds: List, customRepos: List = emp if (cache.containsKey(depsHash)) { val cachedCP = cache.get(depsHash)!! - // Make sure that local dependencies have not been wiped since resolving them (like by deleting .m2) (see #146) - if (cachedCP.split(":").all { File(it).exists() }) { + if (cachedCP.split(CP_SEPARATOR_CHAR).all { File(it).exists() }) { return cachedCP } else { - System.err.println("[kscript] Detected missing dependencies in cache.") + System.err.println("[kscript] Detected missing dependencies in cache") } } } diff --git a/src/main/kotlin/kscript/app/Kscript.kt b/src/main/kotlin/kscript/app/Kscript.kt index e1d67900..6fab2cd6 100644 --- a/src/main/kotlin/kscript/app/Kscript.kt +++ b/src/main/kotlin/kscript/app/Kscript.kt @@ -54,7 +54,7 @@ Website : https://github.com/holgerbrandl/kscript // see https://stackoverflow.com/questions/585534/what-is-the-best-way-to-find-the-users-home-directory-in-java // See #146 "allow kscript cache dir to be configurable" for details val KSCRIPT_CACHE_DIR = System.getenv("KSCRIPT_CACHE_DIR")?.let { File(it) } - ?: File(System.getProperty("user.home")!!, ".kscript") + ?: File(System.getProperty("user.home")!!, ".kscript") // use lazy here prevent empty dirs for regular scripts https://github.com/holgerbrandl/kscript/issues/130 val SCRIPT_TEMP_DIR by lazy { createTempDir() } @@ -83,9 +83,7 @@ fun main(args: Array) { // optionally clear up the jar cache if (docopt.getBoolean("clear-cache")) { - info("Cleaning up cache...") - KSCRIPT_CACHE_DIR.listFiles().forEach { it.delete() } - // evalBash("rm -f ${KSCRIPT_CACHE_DIR}/*") + clearCache() quit(0) } @@ -146,7 +144,7 @@ fun main(args: Array) { val classpath = resolveDependencies(dependencies, customRepos, loggingEnabled) ?: "" - val optionalCpArg = if (classpath.isNotEmpty()) "-classpath '${classpath}'" else "" + val optionalCpArg = if (classpath.isNotEmpty()) """-classpath "${classpath}"""" else "" // Extract kotlin arguments val kotlinOpts = script.collectRuntimeOptions() @@ -242,12 +240,19 @@ fun main(args: Array) { } """.trimIndent()) - "'${mainKotlin.absolutePath}'" + "${mainKotlin.absolutePath}" } else { "" } - val scriptCompileResult = evalBash("kotlinc ${compilerOpts} ${optionalCpArg} -d '${jarFile.absolutePath}' '${scriptFile.absolutePath}' ${wrapperSrcArg}") + val cmd= StringBuilder() + .append("kotlinc ${compilerOpts} ${optionalCpArg} -d \"${jarFile.absolutePath}\" \"${scriptFile.absolutePath}\"") + .apply { + if (wrapperSrcArg.isNotEmpty()) + this.append(" \"${wrapperSrcArg}\"") + } + .toString() + val scriptCompileResult = evalCommand(cmd) with(scriptCompileResult) { errorIf(exitCode != 0) { "compilation of '$scriptResource' failed\n$stderr" } } @@ -274,9 +279,14 @@ fun main(args: Array) { if (classpath.isNotEmpty()) extClassPath += kscript.app.CP_SEPARATOR_CHAR + classpath - println("kotlin ${kotlinOpts} -classpath \"${extClassPath}\" ${execClassName} ${joinedUserArgs} ") + println("kotlin ${kotlinOpts} -classpath \"${extClassPath}\" ${execClassName} ${joinedUserArgs}") } +fun clearCache() { + info("Cleaning up cache...") + KSCRIPT_CACHE_DIR.listFiles().forEach { it.delete() } + // evalBash("rm -f ${KSCRIPT_CACHE_DIR}/*") +} /** Determine the latest version by checking github repo and print info if newer version is available. */ private fun versionCheck() { diff --git a/src/main/kotlin/kscript/app/ResolveIncludes.kt b/src/main/kotlin/kscript/app/ResolveIncludes.kt index 2013bd4f..610c9230 100644 --- a/src/main/kotlin/kscript/app/ResolveIncludes.kt +++ b/src/main/kotlin/kscript/app/ResolveIncludes.kt @@ -36,7 +36,11 @@ fun resolveIncludes(template: File, includeContext: URI = template.parentFile.to isUrl(include) -> URL(include) include.startsWith("/") -> File(include).toURI().toURL() include.startsWith("~/") -> File(System.getenv("HOME")!! + include.substring(1)).toURI().toURL() - else -> includeContext.resolve(URI(include.removePrefix("./"))).toURL() + else -> { + val includeFile = if (File(include).isAbsolute) { File(include) } else File(template.parent, include) + // TODO is `resolve` really necessary? + includeContext.resolve(includeFile.toURI()).toURL() + } } // test if include was processed already (aka include duplication, see #151) diff --git a/src/test/kotlin/KscriptTest.kt b/src/test/kotlin/KscriptTest.kt new file mode 100644 index 00000000..b5593c4a --- /dev/null +++ b/src/test/kotlin/KscriptTest.kt @@ -0,0 +1,191 @@ +import io.kotlintest.matchers.shouldBe +import io.kotlintest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockkStatic +import kscript.app.* +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +// We can assert messages thrown during pre-processing but not the script result, which is currently in a separate process +class KscriptTest { + + @Before + fun before() { + clearCache() + } + + @Test + fun `should run a piped script`() { + assumeTrue(kotlinIsAvailableInPath()) + + main(arrayOf("""println("kotlin rocks")""")) + } + + @Test + fun `should run a script with only println`() { + assumeTrue(kotlinIsAvailableInPath()) + // setup + val (originalErr, newErr) = captureStdErr() + // when + main(arrayOf("test/resources/dot.Test.kts")) + // then + newErr.toString().lines() shouldNotBe listOf( + "[kscript] Resolving dependencies...", + "" + ) + // clean up + restoreStdErr(originalErr) + } + + @Test + fun `should package a piped script`() { + assumeTrue(kotlinIsAvailableInPath()) + assumeTrue(gradleIsAvailableInPath()) + + val result: Int = exitStatus { + main(arrayOf("--package", """println("kotlin rocks")""")) + } + + result shouldBe 0 + } + + @Test + fun `should run a script with multiple dependencies as comments`() { + assumeTrue(kotlinIsAvailableInPath()) + // setup + val (originalErr, newErr) = captureStdErr() + // when + main(arrayOf("test/resources/multi_line_deps.kts")) + // then + newErr.toString().lines() shouldBe listOf( + "[kscript] Resolving dependencies...", + "[kscript] Resolving com.offbytwo:docopt:0.6.0.20150202...Done", + "[kscript] Resolving log4j:log4j:1.2.14...Done", + "[kscript] Dependencies resolved", + "" + ) + // clean up + restoreStdErr(originalErr) + } + + @Test + fun `should run a script with dependencies as annotations`() { + assumeTrue(kotlinIsAvailableInPath()) + // setup + val (originalErr, newErr) = captureStdErr() + // when + main(arrayOf("test/resources/depends_on_with_type.kts")) + // then + println(newErr) + newErr.toString().lines() shouldBe listOf( + "[kscript] Resolving dependencies...", + "[kscript] Resolving org.javamoney:moneta:1.3@pom...Done", + "[kscript] Resolving com.github.holgerbrandl:kscript-annotations:1.2...Done", + "[kscript] Dependencies resolved", + "" + ) + // clean up + restoreStdErr(originalErr) + } + + @Test + fun `should run a script with support API`() { + assumeTrue(kotlinIsAvailableInPath()) + // setup + val (originalErr, newErr) = captureStdErr() + // when + main(arrayOf("-t", """println("hello")""")) + // then + newErr.toString().lines() shouldBe listOf( + "[kscript] Resolving dependencies...", + "[kscript] Resolving com.github.holgerbrandl:kscript-support:1.2.5...Done", + "[kscript] Dependencies resolved", + "" + ) + // clean up + restoreStdErr(originalErr) + } + + @Test + fun `should run a script from an url`() { + assumeTrue(kotlinIsAvailableInPath()) + // setup + val (originalErr, newErr) = captureStdErr() + // when + main(arrayOf("https://raw.githubusercontent.com/holgerbrandl/kscript/master/test/resources/url_test.kts")) + // then + newErr.toString().trim().length shouldBe 0 + // clean up + restoreStdErr(originalErr) + } + + @Test + fun `should resolve dependencies from script from an url`() { + assumeTrue(kotlinIsAvailableInPath()) + // setup + val (originalErr, newErr) = captureStdErr() + // when + main(arrayOf("https://git.io/fxHBv")) + // then + newErr.toString().lines() shouldBe listOf( + "[kscript] Resolving dependencies...", + "[kscript] Resolving log4j:log4j:1.2.14...Done", + "[kscript] Dependencies resolved", + "" + ) + // clean up + restoreStdErr(originalErr) + } + + @Test + fun `should exit if kotlinc is not in PATH`() { + assumeFalse(kotlinIsAvailableInPath()) + + val result = exitStatus { + main(arrayOf("println(\"kotlin rocks\")")) + } + + result shouldBe 1 + } + + private fun kotlinIsAvailableInPath() = guessKotlinHome() != null + + private fun gradleIsAvailableInPath() = evalCommand("where gradle").exitCode != 1 + + private fun captureStdErr(): Pair { + val originalErr = System.err + val newErr = ByteArrayOutputStream() + restoreStdErr(PrintStream(newErr)) + return Pair(originalErr, newErr) + } + + private fun restoreStdErr(originalErr: PrintStream) { + System.setErr(originalErr) + } + + private fun exitStatus(arg: () -> Unit): Int { + mockkStatic("kscript.app.AppHelpersKt") + // workaround for `Nothing` methods: https://github.com/mockk/mockk/issues/187 + every { + quit(1) + } answers { + throw RuntimeException("1") + } + every { + quit(0) + } answers { + throw RuntimeException("0") + } + + try { + arg.invoke() + } catch (e: RuntimeException) { + return e.message?.toInt() ?: 0 + } + return 0 + } +} diff --git a/src/test/kotlin/Tests.kt b/src/test/kotlin/Tests.kt index 6c12ae1c..aa5d53c5 100644 --- a/src/test/kotlin/Tests.kt +++ b/src/test/kotlin/Tests.kt @@ -1,9 +1,13 @@ +import io.kotlintest.matchers.beGreaterThan +import io.kotlintest.matchers.should import io.kotlintest.matchers.shouldBe import kscript.app.* import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue import org.junit.Test import java.io.File +import java.util.function.Consumer /** * @author Holger Brandl @@ -153,7 +157,7 @@ class Tests { @Test fun test_include_annotations() { val file = File("test/resources/includes/include_variations.kts") - val expected = File("test/resources/includes/expexcted_variations.kts") + val expected = File("test/resources/includes/expected_variations.kts") val result = resolveIncludes(file) @@ -177,4 +181,37 @@ class Tests { "dup_include_2.kt" ) } + + @Test + fun `should run process in Windows shell`() { + // run only in windows + assumeTrue(isWindows()) + + val sout = StringBuilder() + val runProcess = runProcess( + cmd = *arrayOf("cmd", "/C", "dir", "."), + wd = File(".").absoluteFile, + stdoutConsumer = Consumer { + sout.append("$it\n") + }) + + runProcess.exitCode shouldBe 0 + sout.split("\n").size should beGreaterThan(1) + } + + @Test + fun `should resolve if command is in path`() { + val cmd = if (isWindows()) "cmd" else "ls" + val isInPath = ShellUtils.isInPath(cmd) + + isInPath shouldBe true + } + + @Test + fun `should resolve dependencies`() { + val resolvedDependencies = resolveDependencies(depIds = listOf("junit:junit:4.12", "org.jetbrains.kotlin:kotlin-stdlib-common:1.2.71"), customRepos = emptyList(), loggingEnabled = true) + + resolvedDependencies!!.split(File.pathSeparator).size shouldBe 3 + } + } diff --git a/test/prepare_test_suite.sh b/test/prepare_test_suite.sh new file mode 100644 index 00000000..44de1e3b --- /dev/null +++ b/test/prepare_test_suite.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Requires kotlin & wget in PATH +function clean_generate_test_files () { + rm -f idea + rm -f 123foo.kts + rm -f compiler_opts_with_includes_dep.kts + rm -f compiler_opts_with_includes_master.kt + rm -f home_dir_master.kts + rm -f test_script.kts + rm -f kscriptlet.* + rm -f package_example +} + +export KSCRIPT_HOME=$(pwd) +export PATH=${KSCRIPT_HOME}/build/libs:$PATH +IT_PATH="$KSCRIPT_HOME" + +clean_generate_test_files + +cd $KSCRIPT_HOME && ./gradlew clean assemble + +export PATH=$(pwd):$PATH + +if [[ ! -f "$IT_PATH/assert.sh" ]]; then + wget --no-check-certificate https://raw.github.com/lehmannro/assert.sh/v1.1/assert.sh && chmod +x assert.sh +fi + +if [[ ! -f idea ]]; then + touch idea && chmod +x idea +fi + +which kscript +source $KSCRIPT_HOME/test/test_suite.sh + +# clean +clean_generate_test_files diff --git a/test/resources/includes/expexcted_variations.kts b/test/resources/includes/expected_variations.kts similarity index 100% rename from test/resources/includes/expexcted_variations.kts rename to test/resources/includes/expected_variations.kts diff --git a/test/test_suite.sh b/test/test_suite.sh index c72a4d2b..0f8759c0 100755 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -230,7 +230,7 @@ export PATH=${PATH}:${KSCRIPT_HOME}/test/resources/custom_dsl assert 'mydsl "println(foo)"' "bar" -assert '${KSCRIPT_HOME}/test/resources/custom_dsl/mydsl_test_with_deps.kts' "foobar" +assert "${KSCRIPT_HOME}/test/resources/custom_dsl/mydsl_test_with_deps.kts" "foobar" assert_end custom_interpreters