Skip to content

Commit 480d6b4

Browse files
committed
Prioritize executions with identical trace to minimize stateBefore
1 parent 6eb5418 commit 480d6b4

File tree

3 files changed

+92
-69
lines changed

3 files changed

+92
-69
lines changed

utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.utbot.framework.UtSettings.useDebugVisualization
3636
import org.utbot.framework.plugin.api.*
3737
import org.utbot.framework.plugin.api.Step
3838
import org.utbot.framework.plugin.api.util.*
39+
import org.utbot.framework.util.calculateSize
3940
import org.utbot.framework.util.convertToAssemble
4041
import org.utbot.framework.util.graph
4142
import org.utbot.framework.util.sootMethod
@@ -423,6 +424,7 @@ class UtBotSymbolicEngine(
423424
.with(ValueProvider.of(relevantRepositories.map { SavedEntityValueProvider(defaultIdGenerator, it) }))
424425
.with(ValueProvider.of(generatedValueFieldIds.map { FieldValueProvider(defaultIdGenerator, it) }))
425426
}.let(transform)
427+
val coverageToMinStateBeforeSize = mutableMapOf<Coverage, Int>()
426428
runJavaFuzzing(
427429
defaultIdGenerator,
428430
methodUnderTest,
@@ -449,15 +451,15 @@ class UtBotSymbolicEngine(
449451
return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP)
450452
}
451453

452-
val initialEnvironmentModels = EnvironmentModels(thisInstance?.model, values.map { it.model }, mapOf())
454+
val stateBefore = EnvironmentModels(thisInstance?.model, values.map { it.model }, mapOf())
453455

454456
val concreteExecutionResult: UtConcreteExecutionResult? = try {
455457
val timeoutMillis = min(UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, diff)
456-
concreteExecutor.executeConcretely(methodUnderTest, initialEnvironmentModels, listOf(), timeoutMillis)
458+
concreteExecutor.executeConcretely(methodUnderTest, stateBefore, listOf(), timeoutMillis)
457459
} catch (e: CancellationException) {
458460
logger.debug { "Cancelled by timeout" }; null
459461
} catch (e: InstrumentedProcessDeathException) {
460-
emitFailedConcreteExecutionResult(initialEnvironmentModels, e); null
462+
emitFailedConcreteExecutionResult(stateBefore, e); null
461463
} catch (e: Throwable) {
462464
emit(UtError("Default concrete execution failed", e)); null
463465
}
@@ -479,9 +481,16 @@ class UtBotSymbolicEngine(
479481
val result = concreteExecutionResult.result
480482
val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions
481483
var trieNode: Trie.Node<Instruction>? = null
484+
482485
if (coveredInstructions.isNotEmpty()) {
483486
trieNode = descr.tracer.add(coveredInstructions)
484-
if (trieNode.count > 1) {
487+
488+
val earlierStateBeforeSize = coverageToMinStateBeforeSize[concreteExecutionResult.coverage]
489+
val curStateBeforeSize = stateBefore.calculateSize()
490+
491+
if (earlierStateBeforeSize == null || curStateBeforeSize < earlierStateBeforeSize)
492+
coverageToMinStateBeforeSize[concreteExecutionResult.coverage] = curStateBeforeSize
493+
else {
485494
if (++attempts >= attemptsLimit) {
486495
return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP)
487496
}
@@ -499,7 +508,7 @@ class UtBotSymbolicEngine(
499508

500509
emit(
501510
UtFuzzedExecution(
502-
stateBefore = initialEnvironmentModels,
511+
stateBefore = stateBefore,
503512
stateAfter = concreteExecutionResult.stateAfter,
504513
result = concreteExecutionResult.result,
505514
coverage = concreteExecutionResult.coverage,

utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt

Lines changed: 23 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,13 @@
11
package org.utbot.framework.minimization
22

33
import org.utbot.framework.UtSettings
4-
import org.utbot.framework.plugin.api.EnvironmentModels
5-
import org.utbot.framework.plugin.api.UtArrayModel
6-
import org.utbot.framework.plugin.api.UtAssembleModel
7-
import org.utbot.framework.plugin.api.UtClassRefModel
8-
import org.utbot.framework.plugin.api.UtCompositeModel
94
import org.utbot.framework.plugin.api.UtConcreteExecutionFailure
10-
import org.utbot.framework.plugin.api.UtDirectSetFieldModel
11-
import org.utbot.framework.plugin.api.UtEnumConstantModel
12-
import org.utbot.framework.plugin.api.UtExecutableCallModel
135
import org.utbot.framework.plugin.api.UtExecution
146
import org.utbot.framework.plugin.api.UtExecutionFailure
157
import org.utbot.framework.plugin.api.UtExecutionResult
16-
import org.utbot.framework.plugin.api.UtLambdaModel
17-
import org.utbot.framework.plugin.api.UtModel
18-
import org.utbot.framework.plugin.api.UtNullModel
19-
import org.utbot.framework.plugin.api.UtPrimitiveModel
20-
import org.utbot.framework.plugin.api.UtStatementCallModel
21-
import org.utbot.framework.plugin.api.UtStatementModel
228
import org.utbot.framework.plugin.api.UtSymbolicExecution
23-
import org.utbot.framework.plugin.api.UtVoidModel
9+
import org.utbot.framework.util.calculateSize
10+
import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor
2411

2512

2613
/**
@@ -53,16 +40,21 @@ fun <T : Any> minimizeTestCase(
5340

5441
fun minimizeExecutions(executions: List<UtExecution>): List<UtExecution> {
5542
val unknownCoverageExecutions =
56-
executions.indices.filter { executions[it].coverage?.coveredInstructions?.isEmpty() ?: true }.toSet()
43+
executions.filter { it.coverage?.coveredInstructions.isNullOrEmpty() }.toSet()
5744
// ^^^ here we add executions with empty or null coverage, because it happens only if a concrete execution failed,
5845
// so we don't know the actual coverage for such executions
5946

60-
val filteredExecutions = executions.filterIndexed { idx, _ -> idx !in unknownCoverageExecutions }
47+
val filteredExecutions = executions
48+
.subtract(unknownCoverageExecutions)
49+
.groupBy { it.coverage }.values
50+
.map { executionsWithEqualCoverage -> executionsWithEqualCoverage.chooseOneExecution() }
6151
val (mapping, executionToPriorityMapping) = buildMapping(filteredExecutions)
6252

63-
val usedExecutionIndexes = (GreedyEssential.minimize(mapping, executionToPriorityMapping) + unknownCoverageExecutions).toSet()
53+
val usedExecutionIndexes = GreedyEssential.minimize(mapping, executionToPriorityMapping)
6454

65-
val usedMinimizedExecutions = executions.filterIndexed { idx, _ -> idx in usedExecutionIndexes }
55+
val usedMinimizedExecutions =
56+
filteredExecutions.filterIndexed { idx, _ -> idx in usedExecutionIndexes } +
57+
unknownCoverageExecutions
6658

6759
return if (UtSettings.minimizeCrashExecutions) {
6860
usedMinimizedExecutions.filteredCrashExecutions()
@@ -192,55 +184,20 @@ private fun List<UtExecution>.filteredCrashExecutions(): List<UtExecution> {
192184

193185
val notCrashExecutions = filterNot { it.result is UtConcreteExecutionFailure }
194186

195-
return notCrashExecutions + crashExecutions.chooseMinimalCrashExecution()
187+
return notCrashExecutions + crashExecutions.chooseOneExecution()
196188
}
197189

198190
/**
199-
* As for now crash execution can only be produced by Concrete Executor, it does not have [UtExecution.stateAfter] and
200-
* [UtExecution.result] is [UtExecutionFailure], so we check only [UtExecution.stateBefore].
191+
* Chooses one execution with the highest [execution priority][getExecutionPriority]. If multiple executions
192+
* have the same priority, then the one with the [smallest][calculateSize] [UtExecution.stateBefore] is chosen.
193+
*
194+
* Only [UtExecution.stateBefore] is considered, because [UtExecution.result] and [UtExecution.stateAfter]
195+
* don't represent true picture as they are limited by [construction depth][UtModelConstructor.maxDepth] and their
196+
* sizes can't be calculated for crushed executions.
201197
*/
202-
private fun List<UtExecution>.chooseMinimalCrashExecution(): UtExecution = minByOrNull {
203-
it.stateBefore.calculateSize()
204-
} ?: error("Cannot find minimal crash execution within empty executions")
205-
206-
private fun EnvironmentModels.calculateSize(): Int {
207-
val thisInstanceSize = thisInstance?.calculateSize() ?: 0
208-
val parametersSize = parameters.sumOf { it.calculateSize() }
209-
val staticsSize = statics.values.sumOf { it.calculateSize() }
210-
211-
return thisInstanceSize + parametersSize + staticsSize
212-
}
213-
214-
/**
215-
* We assume that "size" for "common" models is 1, 0 for [UtVoidModel] (as they do not return anything) and
216-
* [UtPrimitiveModel] and [UtNullModel] (we use them as literals in codegen), summarising for all statements for [UtAssembleModel] and
217-
* summarising for all fields and mocks for [UtCompositeModel]. As [UtCompositeModel] could be recursive, we need to
218-
* store it in [used]. Moreover, if we already calculate size for [this], it means that we will use already created
219-
* variable by this model and do not need to create it again, so size should be equal to 0.
220-
*/
221-
private fun UtModel.calculateSize(used: MutableSet<UtModel> = mutableSetOf()): Int {
222-
if (this in used) return 0
223-
224-
used += this
225-
226-
return when (this) {
227-
is UtNullModel, is UtPrimitiveModel, UtVoidModel -> 0
228-
is UtClassRefModel, is UtEnumConstantModel, is UtArrayModel -> 1
229-
is UtAssembleModel -> {
230-
1 + instantiationCall.calculateSize(used) + modificationsChain.sumOf { it.calculateSize(used) }
231-
}
232-
is UtCompositeModel -> 1 + fields.values.sumOf { it.calculateSize(used) }
233-
is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) }
234-
// PythonModel, JsUtModel, UtSpringContextModel may be here
235-
else -> 0
236-
}
237-
}
238-
239-
private fun UtStatementModel.calculateSize(used: MutableSet<UtModel> = mutableSetOf()): Int =
240-
when (this) {
241-
is UtDirectSetFieldModel -> 1 + fieldModel.calculateSize(used) + instance.calculateSize(used)
242-
is UtStatementCallModel -> 1 + params.sumOf { it.calculateSize(used) } + (instance?.calculateSize(used) ?: 0)
243-
}
198+
private fun List<UtExecution>.chooseOneExecution(): UtExecution = minWithOrNull(
199+
compareBy({ it.getExecutionPriority() }, { it.stateBefore.calculateSize() })
200+
) ?: error("Cannot find minimal execution within empty executions")
244201

245202
/**
246203
* Extends the [instructionsWithoutExtra] with one extra instruction if the [result] is
@@ -274,6 +231,8 @@ private fun Throwable.exceptionToInfo(): String =
274231
* Returns an execution priority. [UtSymbolicExecution] has the highest priority
275232
* over other executions like [UtFuzzedExecution], [UtFailedExecution], etc.
276233
*
234+
* NOTE! Smaller number represents higher priority.
235+
*
277236
* See [https://github.com/UnitTestBot/UTBotJava/issues/1504] for more details.
278237
*/
279238
private fun UtExecution.getExecutionPriority(): Int = when (this) {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.utbot.framework.util
2+
3+
import org.utbot.framework.plugin.api.EnvironmentModels
4+
import org.utbot.framework.plugin.api.UtArrayModel
5+
import org.utbot.framework.plugin.api.UtAssembleModel
6+
import org.utbot.framework.plugin.api.UtClassRefModel
7+
import org.utbot.framework.plugin.api.UtCompositeModel
8+
import org.utbot.framework.plugin.api.UtDirectSetFieldModel
9+
import org.utbot.framework.plugin.api.UtEnumConstantModel
10+
import org.utbot.framework.plugin.api.UtLambdaModel
11+
import org.utbot.framework.plugin.api.UtModel
12+
import org.utbot.framework.plugin.api.UtNullModel
13+
import org.utbot.framework.plugin.api.UtPrimitiveModel
14+
import org.utbot.framework.plugin.api.UtStatementCallModel
15+
import org.utbot.framework.plugin.api.UtStatementModel
16+
import org.utbot.framework.plugin.api.UtVoidModel
17+
18+
fun EnvironmentModels.calculateSize(): Int {
19+
val thisInstanceSize = thisInstance?.calculateSize() ?: 0
20+
val parametersSize = parameters.sumOf { it.calculateSize() }
21+
val staticsSize = statics.values.sumOf { it.calculateSize() }
22+
23+
return thisInstanceSize + parametersSize + staticsSize
24+
}
25+
26+
/**
27+
* We assume that "size" for "common" models is 1, 0 for [UtVoidModel] (as they do not return anything) and
28+
* [UtPrimitiveModel] and [UtNullModel] (we use them as literals in codegen), summarising for all statements for [UtAssembleModel] and
29+
* summarising for all fields and mocks for [UtCompositeModel]. As [UtCompositeModel] could be recursive, we need to
30+
* store it in [used]. Moreover, if we already calculate size for [this], it means that we will use already created
31+
* variable by this model and do not need to create it again, so size should be equal to 0.
32+
*/
33+
private fun UtModel.calculateSize(used: MutableSet<UtModel> = mutableSetOf()): Int {
34+
if (this in used) return 0
35+
36+
used += this
37+
38+
return when (this) {
39+
is UtNullModel, is UtPrimitiveModel, UtVoidModel -> 0
40+
is UtClassRefModel, is UtEnumConstantModel, is UtArrayModel -> 1
41+
is UtAssembleModel -> {
42+
1 + instantiationCall.calculateSize(used) + modificationsChain.sumOf { it.calculateSize(used) }
43+
}
44+
is UtCompositeModel -> 1 + fields.values.sumOf { it.calculateSize(used) }
45+
is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) }
46+
// PythonModel, JsUtModel, UtSpringContextModel may be here
47+
else -> 0
48+
}
49+
}
50+
51+
private fun UtStatementModel.calculateSize(used: MutableSet<UtModel> = mutableSetOf()): Int =
52+
when (this) {
53+
is UtDirectSetFieldModel -> 1 + fieldModel.calculateSize(used) + instance.calculateSize(used)
54+
is UtStatementCallModel -> 1 + params.sumOf { it.calculateSize(used) } + (instance?.calculateSize(used) ?: 0)
55+
}

0 commit comments

Comments
 (0)