Creating a test simulating idea-jps-daemon processes, that should now fail on windows, some refactorings in the daemon client to simplify test writing
This commit is contained in:
@@ -17,7 +17,6 @@
|
|||||||
package org.jetbrains.kotlin.rmi.kotlinr
|
package org.jetbrains.kotlin.rmi.kotlinr
|
||||||
|
|
||||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||||
import org.jetbrains.kotlin.modules.TargetId
|
|
||||||
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
||||||
import org.jetbrains.kotlin.rmi.*
|
import org.jetbrains.kotlin.rmi.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
@@ -150,6 +149,7 @@ public object KotlinCompilerClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public val COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY: String = "kotlin.daemon.client.options"
|
||||||
data class ClientOptions(
|
data class ClientOptions(
|
||||||
public var stop: Boolean = false
|
public var stop: Boolean = false
|
||||||
) : OptionsGroup {
|
) : OptionsGroup {
|
||||||
@@ -157,30 +157,34 @@ public object KotlinCompilerClient {
|
|||||||
get() = listOf(BoolPropMapper(this, ClientOptions::stop))
|
get() = listOf(BoolPropMapper(this, ClientOptions::stop))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun configureClientOptions(opts: ClientOptions): ClientOptions {
|
||||||
|
System.getProperty(COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY)?.let {
|
||||||
|
val unrecognized = it.trimQuotes().split(",").filterExtractProps(opts.mappers, "")
|
||||||
|
if (unrecognized.any())
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unrecognized client options passed via property $COMPILE_DAEMON_OPTIONS_PROPERTY: " + unrecognized.joinToString(" ") +
|
||||||
|
"\nSupported options: " + opts.mappers.joinToString(", ", transform = { it.names.first() }))
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureClientOptions(): ClientOptions = configureClientOptions(ClientOptions())
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun main(vararg args: String) {
|
public fun main(vararg args: String) {
|
||||||
val compilerId = CompilerId()
|
val compilerId = CompilerId()
|
||||||
val daemonOptions = DaemonOptions()
|
val daemonOptions = configureDaemonOptions()
|
||||||
val daemonLaunchingOptions = DaemonJVMOptions()
|
val daemonLaunchingOptions = configureDaemonJVMOptions(true)
|
||||||
val clientOptions = ClientOptions()
|
val clientOptions = configureClientOptions()
|
||||||
val filteredArgs = args.asIterable().filterExtractProps(compilerId, daemonOptions, daemonLaunchingOptions, clientOptions, prefix = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX)
|
val filteredArgs = args.asIterable().filterExtractProps(compilerId, daemonOptions, daemonLaunchingOptions, clientOptions, prefix = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX)
|
||||||
|
|
||||||
if (!clientOptions.stop) {
|
if (!clientOptions.stop) {
|
||||||
if (compilerId.compilerClasspath.none()) {
|
if (compilerId.compilerClasspath.none()) {
|
||||||
// attempt to find compiler to use
|
// attempt to find compiler to use
|
||||||
System.err.println("compiler wasn't explicitly specified, attempt to find appropriate jar")
|
System.err.println("compiler wasn't explicitly specified, attempt to find appropriate jar")
|
||||||
System.getProperty("java.class.path")
|
detectCompilerClasspath()
|
||||||
?.split(File.pathSeparator)
|
?.let { compilerId.compilerClasspath = it }
|
||||||
?.map { File(it).parentFile }
|
|
||||||
?.distinct()
|
|
||||||
?.map {
|
|
||||||
it?.walk()
|
|
||||||
?.firstOrNull { it.name.equals(COMPILER_JAR_NAME, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
?.filterNotNull()
|
|
||||||
?.firstOrNull()
|
|
||||||
?.let { compilerId.compilerClasspath = listOf(it.absolutePath) }
|
|
||||||
}
|
}
|
||||||
if (compilerId.compilerClasspath.none())
|
if (compilerId.compilerClasspath.none())
|
||||||
throw IllegalArgumentException("Cannot find compiler jar")
|
throw IllegalArgumentException("Cannot find compiler jar")
|
||||||
@@ -204,6 +208,10 @@ public object KotlinCompilerClient {
|
|||||||
daemon.shutdown()
|
daemon.shutdown()
|
||||||
println("Daemon shut down successfully")
|
println("Daemon shut down successfully")
|
||||||
}
|
}
|
||||||
|
filteredArgs.none() -> {
|
||||||
|
// so far used only in tests
|
||||||
|
println("Warning: empty arguments list, only daemon check is performed: checkCompilerId() returns ${checkCompilerId(daemon, compilerId)}")
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
println("Executing daemon compilation with args: " + filteredArgs.joinToString(" "))
|
println("Executing daemon compilation with args: " + filteredArgs.joinToString(" "))
|
||||||
val outStrm = RemoteOutputStreamServer(System.out)
|
val outStrm = RemoteOutputStreamServer(System.out)
|
||||||
@@ -228,6 +236,19 @@ public object KotlinCompilerClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun detectCompilerClasspath(): List<String>? =
|
||||||
|
System.getProperty("java.class.path")
|
||||||
|
?.split(File.pathSeparator)
|
||||||
|
?.map { File(it).parentFile }
|
||||||
|
?.distinct()
|
||||||
|
?.map {
|
||||||
|
it?.walk()
|
||||||
|
?.firstOrNull { it.name.equals(COMPILER_JAR_NAME, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
?.filterNotNull()
|
||||||
|
?.firstOrNull()
|
||||||
|
?.let { listOf(it.absolutePath) }
|
||||||
|
|
||||||
// --- Implementation ---------------------------------------
|
// --- Implementation ---------------------------------------
|
||||||
|
|
||||||
fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String = "daemon client") {
|
fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String = "daemon client") {
|
||||||
@@ -335,7 +356,7 @@ public object KotlinCompilerClient {
|
|||||||
} ?: DAEMON_DEFAULT_STARTUP_TIMEOUT_MS
|
} ?: DAEMON_DEFAULT_STARTUP_TIMEOUT_MS
|
||||||
if (daemonOptions.runFilesPath.isNotEmpty()) {
|
if (daemonOptions.runFilesPath.isNotEmpty()) {
|
||||||
val succeeded = isEchoRead.tryAcquire(daemonStartupTimeout, TimeUnit.MILLISECONDS)
|
val succeeded = isEchoRead.tryAcquire(daemonStartupTimeout, TimeUnit.MILLISECONDS)
|
||||||
if (!daemon.isAlive())
|
if (!isProcessAlive(daemon))
|
||||||
throw Exception("Daemon terminated unexpectedly")
|
throw Exception("Daemon terminated unexpectedly")
|
||||||
if (!succeeded)
|
if (!succeeded)
|
||||||
throw Exception("Unable to get response from daemon in $daemonStartupTimeout ms")
|
throw Exception("Unable to get response from daemon in $daemonStartupTimeout ms")
|
||||||
@@ -413,11 +434,12 @@ public data class DaemonReportMessage(public val category: DaemonReportCategory,
|
|||||||
public class DaemonReportingTargets(public val out: PrintStream? = null, public val messages: MutableCollection<DaemonReportMessage>? = null)
|
public class DaemonReportingTargets(public val out: PrintStream? = null, public val messages: MutableCollection<DaemonReportMessage>? = null)
|
||||||
|
|
||||||
|
|
||||||
internal fun Process.isAlive() =
|
internal fun isProcessAlive(process: Process) =
|
||||||
try {
|
try {
|
||||||
this.exitValue()
|
process.exitValue()
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
catch (e: IllegalThreadStateException) {
|
catch (e: IllegalThreadStateException) {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import org.jetbrains.kotlin.rmi.kotlinr.KotlinCompilerClient
|
|||||||
import org.jetbrains.kotlin.test.JetTestUtils
|
import org.jetbrains.kotlin.test.JetTestUtils
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
|
||||||
|
val TIMEOUT_DAEMON_RUNNER_EXIT_MS = 10000L
|
||||||
|
|
||||||
public class CompilerDaemonTest : KotlinIntegrationTestBase() {
|
public class CompilerDaemonTest : KotlinIntegrationTestBase() {
|
||||||
|
|
||||||
@@ -32,6 +36,8 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() {
|
|||||||
|
|
||||||
val compilerClassPath = listOf(
|
val compilerClassPath = listOf(
|
||||||
File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler.jar"))
|
File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler.jar"))
|
||||||
|
val daemonClientClassPath = listOf( File(KotlinIntegrationTestBase.getCompilerLib(), "kotlinr.jar"),
|
||||||
|
File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler.jar"))
|
||||||
val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) }
|
val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) }
|
||||||
|
|
||||||
private fun compileOnDaemon(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults {
|
private fun compileOnDaemon(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults {
|
||||||
@@ -178,6 +184,53 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() {
|
|||||||
logFile2.assertLogContainsSequence("Shutdown complete")
|
logFile2.assertLogContainsSequence("Shutdown complete")
|
||||||
logFile2.delete()
|
logFile2.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Testing that running daemon in the child process doesn't block on s child process.waitFor()
|
||||||
|
* that may happen on windows if simple processBuilder.start is used due to handles inheritance:
|
||||||
|
* - process A starts process B using ProcessBuilder and waits for it using process.waitFor()
|
||||||
|
* - process B starts daemon and exits
|
||||||
|
* - due to default behavior of CreateProcess on windows, the handles of process B are inherited by the daemon
|
||||||
|
* (in particular handles of stdin/out/err) and therefore these handles remain open while daemon is running
|
||||||
|
* - (seems) due to the way how waiting for process is implemented, waitFor() hangs until daemon is killed
|
||||||
|
* This seems a known problem, e.g. gradle uses a library with native code that prevents io handles inheritance when launching it's daemon
|
||||||
|
* (the same solution is used in kotlin daemon client - see next commit)
|
||||||
|
*/
|
||||||
|
public fun testDaemonExecutionViaIntermediateProcess() {
|
||||||
|
val clientAliveFile = createTempFile("kotlin-daemon-transitive-run-test", ".run")
|
||||||
|
val runFilesPath = File(tmpdir, getTestName(true)).absolutePath
|
||||||
|
val daemonOptions = DaemonOptions(runFilesPath = runFilesPath, clientAliveFlagPath = clientAliveFile.absolutePath)
|
||||||
|
val args = listOf(
|
||||||
|
File(File(System.getProperty("java.home"), "bin"), "java").absolutePath,
|
||||||
|
"-D$COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY",
|
||||||
|
"-cp",
|
||||||
|
daemonClientClassPath.joinToString(File.pathSeparator) { it.absolutePath },
|
||||||
|
KotlinCompilerClient::class.qualifiedName!!) +
|
||||||
|
daemonOptions.mappers.flatMap { it.toArgs(COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) } +
|
||||||
|
compilerId.mappers.flatMap { it.toArgs(COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) } +
|
||||||
|
File(getHelloAppBaseDir(), "hello.kt").absolutePath
|
||||||
|
try {
|
||||||
|
KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions)
|
||||||
|
var resOutput: String? = null
|
||||||
|
var resCode: Int? = null
|
||||||
|
// running intermediate process (daemon command line controller) that executes the daemon
|
||||||
|
val runnerProcess = ProcessBuilder(args).redirectErrorStream(true).start()
|
||||||
|
thread {
|
||||||
|
resOutput = runnerProcess.inputStream.reader().readText()
|
||||||
|
}
|
||||||
|
val waitThread = thread {
|
||||||
|
resCode = runnerProcess.waitFor()
|
||||||
|
}
|
||||||
|
waitThread.join(TIMEOUT_DAEMON_RUNNER_EXIT_MS)
|
||||||
|
|
||||||
|
TestCase.assertFalse("process.waitFor() hangs:\n$resOutput", waitThread.isAlive)
|
||||||
|
TestCase.assertEquals("Compilation failed:\n$resOutput", 0, resCode)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (clientAliveFile.exists())
|
||||||
|
clientAliveFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user