Skip to content

Instantly share code, notes, and snippets.

@elihart
Created November 27, 2019 16:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elihart/019bd116d3fa0d6214b7396eedc4b206 to your computer and use it in GitHub Desktop.
Save elihart/019bd116d3fa0d6214b7396eedc4b206 to your computer and use it in GitHub Desktop.
Kotlin script that runs `gradle clean test` on other scripts to easily build and test them (using kscript)
import java.io.BufferedReader
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
/**
* This script takes the name of another kts script as an argument, builds that script with gradle, and runs
* its tests via "gradle clean test".
*/
val scriptPath = args.firstOrNull() ?: error("First argument must be a path to a script")
// Checks for a global path or a relative path
val scriptFile = listOf(File(scriptPath), File("", scriptPath))
.firstOrNull { it.exists() }
?: error("Could not find kts file at $scriptPath")
// This leverages the kscript --idea command to create a project containing all the dependencies for the script,
// which we can then build and test.
// We don't want kscript to actually open Intellij, so we create a local "idea" file that does nothing.
// It is added to the PATH immediately before executing the kscript command since the PATH change
// is only applicable for the life of that process.
// It is prepended to take precedence over the real idea executable.
val mockIdeaFile = File("idea").apply {
check(createNewFile()) { "idea file already exists in this directory" }
setExecutable(true)
deleteOnExit()
}
val ideaPath = mockIdeaFile.canonicalPath.substringBeforeLast("/")
val projectLocation = "export PATH=\"$ideaPath:\$PATH\"; kscript --idea ${scriptFile.canonicalPath}"
.execute()
// We capture the output of the --idea command since it contains the location of the generated project.
// kscript prints this info to err instead of stdout
.stdErr
.lineSequence()
.map {
// Output is like "[kscript] Project set up at /Users/your_name/.kscript/kscript_tmp_project__MyScript.kts_1574748399761"
it.substringAfter("Project set up at ", "")
}
.firstOrNull { it.isNotEmpty() }
?: error("Project output not found")
// The generated project does not automatically include a ./gradlew wrapper so we have to test using a global gradle installation
assertInPath("gradle")
println("\nTesting ${scriptFile.canonicalPath}...\n")
"""
cd $projectLocation
gradle clean test
""".trimIndent()
.execute(
// Inherit is used so that gradle test output is shown in console to the user
stdoutRedirectBehavior = ProcessBuilder.Redirect.INHERIT,
stderrRedirectBehavior = ProcessBuilder.Redirect.INHERIT
).let { result ->
exitProcess(result.exitCode)
}
fun assertInPath(executableName: String) {
"which $executableName"
.execute()
.stdOut
.readLine()
.let {
checkOrExit("not found" !in it) {
"$executableName was not found in PATH. Make sure it is installed globally."
}
}
}
fun checkOrExit(condition: Boolean, msg: () -> String) {
if (!condition) {
println("Error: ${msg()}")
exitProcess(1)
}
}
fun String.execute(
workingDir: File = File("."),
timeoutAmount: Long = 60,
timeoutUnit: TimeUnit = TimeUnit.SECONDS,
stdoutRedirectBehavior: ProcessBuilder.Redirect = ProcessBuilder.Redirect.PIPE,
stderrRedirectBehavior: ProcessBuilder.Redirect = ProcessBuilder.Redirect.PIPE
): ProcessResult {
val processBuilder = ProcessBuilder("/bin/sh", "-c", this)
.directory(workingDir)
.redirectOutput(stdoutRedirectBehavior)
.redirectError(stderrRedirectBehavior)
return processBuilder.start()
.apply {
waitFor(timeoutAmount, timeoutUnit)
if (isAlive) {
destroyForcibly()
println("Command timed out after ${timeoutUnit.toSeconds(timeoutAmount)} seconds: '$this'")
exitProcess(1)
}
}
.let { process ->
val stdOut = processBuilder.redirectOutput()?.file()?.bufferedReader() ?: process.inputStream.bufferedReader()
val stdErr = processBuilder.redirectError()?.file()?.bufferedReader() ?: process.errorStream.bufferedReader()
ProcessResult(process.exitValue(), stdOut, stdErr)
}
}
data class ProcessResult(val exitCode: Int, val stdOut: BufferedReader, val stdErr: BufferedReader) {
val succeeded: Boolean = exitCode == 0
val failed: Boolean = !succeeded
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment