Skip to content

Vassiliy kudryashov/919 simultaneous test generation crashes one of them #1164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.utbot.framework.plugin.api.util

import java.io.OutputStream
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.text.DateFormat
import kotlin.io.path.deleteIfExists
import kotlin.io.path.outputStream
import mu.KotlinLogging
import org.utbot.framework.utbotHomePath

private val lockFilePath = "$utbotHomePath/utbot.lock"
private var currentLock : OutputStream? = null
private val logger = KotlinLogging.logger {}

object Lock {
@Synchronized
fun isLocked() = currentLock != null

@Synchronized
fun lock(): Boolean {
if (currentLock != null) return false
return try {
currentLock = Paths.get(lockFilePath).outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.DELETE_ON_CLOSE).also {
it.write(DateFormat.getDateTimeInstance().format(System.currentTimeMillis()).toByteArray())
}
logger.debug("Locked")
true
} catch (e: Exception) {
logger.error("Failed to lock")
false
}
}

@Synchronized
fun unlock(): Boolean {
try {
val tmp = currentLock
if (tmp != null) {
tmp.close()
Paths.get(lockFilePath).deleteIfExists()
logger.debug("Unlocked")
currentLock = null
return true
}
} catch (ignored: Exception) {
logger.error("Failed to unlock")
}
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ private fun EngineProcessModel.setup(
val testPackageName: String? = params.testPackageName
var hasWarnings = false
val reports = testGenerationReports
if (reports.isEmpty()) return@measureExecutionForTermination GenerateTestReportResult("No tests were generated", null, true)
val isMultiPackage = params.isMultiPackage
val (notifyMessage, statistics) = if (reports.size == 1) {
val report = reports.first()
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import com.intellij.refactoring.util.classMembers.MemberInfo
import com.intellij.task.ProjectTaskManager
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.containers.nullize
import com.intellij.util.io.exists
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
Expand Down Expand Up @@ -49,11 +48,26 @@ import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import kotlin.io.path.pathString
import org.utbot.framework.plugin.api.util.Lock

object UtTestsDialogProcessor {

private val logger = KotlinLogging.logger {}

enum class ProgressRange(val from : Double, val to: Double) {
SOLVING(from = 0.0, to = 0.7),
CODEGEN(from = 0.7, to = 0.9),
SARIF(from = 0.9, to = 1.0)
}

fun updateIndicator(indicator: ProgressIndicator, range : ProgressRange, text: String? = null, fraction: Double? = null) {
invokeLater {
text?.let { indicator.text = it }
fraction?.let { indicator.fraction = indicator.fraction.coerceAtLeast(range.from + (range.to - range.from) * fraction.coerceIn(0.0, 1.0)) }
logger.debug("Phase ${indicator.text} with progress ${String.format("%.2f",indicator.fraction)}")
}
}


fun createDialogAndGenerateTests(
project: Project,
srcClasses: Set<PsiClass>,
Expand Down Expand Up @@ -108,24 +122,20 @@ object UtTestsDialogProcessor {
promise.onSuccess {
if (it.hasErrors() || it.isAborted)
return@onSuccess
if (!Lock.lock()) {
return@onSuccess
}

(object : Task.Backgroundable(project, "Generate tests") {

override fun run(indicator: ProgressIndicator) {
val ldef = LifetimeDefinition()
ldef.onTermination { Lock.unlock() }
ldef.terminateOnException { lifetime ->
val startTime = System.currentTimeMillis()
val secondsTimeout = TimeUnit.MILLISECONDS.toSeconds(model.timeout)
val totalTimeout = model.timeout * model.srcClasses.size

indicator.isIndeterminate = false
indicator.text = "Generate tests: read classes"

val timerHandler =
AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({
indicator.fraction =
(System.currentTimeMillis() - startTime).toDouble() / totalTimeout
}, 0, 500, TimeUnit.MILLISECONDS)
updateIndicator(indicator, ProgressRange.SOLVING, "Generate tests: read classes", 0.0)

val buildPaths = ReadAction
.nonBlocking<BuildPaths?> { findPaths(model.srcClasses) }
Expand Down Expand Up @@ -178,10 +188,13 @@ object UtTestsDialogProcessor {
continue
}

indicator.text = "Generate test cases for class $className"
if (totalClasses > 1) {
indicator.fraction =
indicator.fraction.coerceAtLeast(0.9 * processedClasses / totalClasses)
updateIndicator(
indicator,
ProgressRange.SOLVING,
"Generate test cases for class $className",
processedClasses.toDouble() / totalClasses
)
}

// set timeout for concrete execution and for generated tests
Expand Down Expand Up @@ -231,8 +244,6 @@ object UtTestsDialogProcessor {
} else {
testSetsByClass[srcClass] = rdGenerateResult
}

timerHandler.cancel(true)
}
processedClasses++
}
Expand All @@ -247,14 +258,13 @@ object UtTestsDialogProcessor {
}
return
}

indicator.fraction = indicator.fraction.coerceAtLeast(0.9)
indicator.text = "Generate code for tests"
updateIndicator(indicator, ProgressRange.CODEGEN, "Generate code for tests", 0.0)
// Commented out to generate tests for collected executions even if action was canceled.
// indicator.checkCanceled()

invokeLater {
generateTests(model, testSetsByClass, psi2KClass, proc)
generateTests(model, testSetsByClass, psi2KClass, proc, indicator)
logger.info { "Generation complete" }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ package org.utbot.intellij.plugin.sarif

import com.intellij.openapi.application.WriteAction
import com.intellij.psi.PsiClass
import com.intellij.openapi.progress.ProgressIndicator
import org.utbot.common.PathUtil.classFqnToPath
import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
import com.intellij.openapi.vfs.VfsUtil
import java.util.concurrent.CountDownLatch
import mu.KotlinLogging
import org.utbot.framework.plugin.api.ClassId
import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor
import org.utbot.intellij.plugin.models.GenerateTestsModel
import org.utbot.intellij.plugin.process.EngineProcess
import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath
import org.utbot.intellij.plugin.util.IntelliJApiHelper
import java.nio.file.Path

object SarifReportIdea {

private val logger = KotlinLogging.logger {}
/**
* Creates the SARIF report by calling the SarifReport.createReport(),
* saves it to test resources directory and notifies the user about the creation.
Expand All @@ -21,14 +27,25 @@ object SarifReportIdea {
classId: ClassId,
model: GenerateTestsModel,
generatedTestsCode: String,
psiClass: PsiClass
psiClass: PsiClass,
reportsCountDown: CountDownLatch,
indicator: ProgressIndicator
) {
UtTestsDialogProcessor.updateIndicator(indicator, UtTestsDialogProcessor.ProgressRange.SARIF, "Generate SARIF report for ${classId.name}", .5)
// building the path to the report file
val classFqn = classId.name
val (sarifReportsPath, sourceFinding) = WriteAction.computeAndWait<Pair<Path, SourceFindingStrategyIdea>, Exception> { model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) to SourceFindingStrategyIdea(psiClass) }
val reportFilePath = sarifReportsPath.resolve("${classFqnToPath(classFqn)}Report.sarif")

proc.writeSarif(reportFilePath, testSetsId, generatedTestsCode, sourceFinding)
IntelliJApiHelper.run(IntelliJApiHelper.Target.THREAD_POOL, indicator) {
try {
proc.writeSarif(reportFilePath, testSetsId, generatedTestsCode, sourceFinding)
} catch (e: Exception) {
logger.error { e }
} finally {
reportsCountDown.countDown()
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.utbot.intellij.plugin.util.isVisible
import java.util.*
import org.jetbrains.kotlin.j2k.getContainingClass
import org.jetbrains.kotlin.utils.addIfNotNull
import org.utbot.framework.plugin.api.util.Lock
import org.utbot.intellij.plugin.models.packageName
import org.utbot.intellij.plugin.ui.InvalidClassNotifier
import org.utbot.intellij.plugin.util.isAbstract
Expand All @@ -41,6 +42,10 @@ class GenerateTestsAction : AnAction(), UpdateInBackground {
}

override fun update(e: AnActionEvent) {
if (Lock.isLocked()) {
e.presentation.isEnabled = false
return
}
if (e.place == ActionPlaces.POPUP) {
e.presentation.text = "Tests with UnitTestBot..."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,36 @@ import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.util.PlatformUtils
import com.intellij.util.ReflectionUtil
import com.intellij.util.concurrency.AppExecutorUtil
import mu.KotlinLogging
import org.jetbrains.kotlin.idea.util.application.invokeLater

/**
* This object is required to encapsulate Android API usage and grant safe access to it.
*/
object IntelliJApiHelper {

private val logger = KotlinLogging.logger {}
enum class Target { THREAD_POOL, READ_ACTION, WRITE_ACTION, EDT_LATER }

fun run(target: Target, runnable: Runnable) {
when (target) {
Target.THREAD_POOL -> AppExecutorUtil.getAppExecutorService().submit {
fun run(target: Target, indicator: ProgressIndicator? = null, runnable: Runnable) {
if (indicator != null && indicator.isCanceled) return
val wrapper = Runnable {
try {
runnable.run()
} catch (e: Exception) {
logger.error(e) { target.toString() }
throw e
}

Target.READ_ACTION -> runReadAction { runnable.run() }
Target.WRITE_ACTION -> runWriteAction { runnable.run() }
Target.EDT_LATER -> invokeLater { runnable.run() }
}
when (target) {
Target.THREAD_POOL -> AppExecutorUtil.getAppExecutorService().submit { wrapper.run() }
Target.READ_ACTION -> runReadAction { wrapper.run() }
Target.WRITE_ACTION -> runWriteAction { wrapper.run() }
Target.EDT_LATER -> invokeLater { wrapper.run() }
}
}

Expand Down