Skip to content

Commit f41d891

Browse files
authored
Prioritize executions with identical trace to minimize stateBefore #2219 (#2371)
* Prioritize executions with identical trace to minimize `stateBefore` * Use `Trie.Node<Instruction>` as key instead of `Coverage`
1 parent e87b5b9 commit f41d891

File tree

3 files changed

+101
-69
lines changed

3 files changed

+101
-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
@@ -424,6 +425,7 @@ class UtBotSymbolicEngine(
424425
.with(ValueProvider.of(relevantRepositories.map { SavedEntityValueProvider(defaultIdGenerator, it) }))
425426
.with(ValueProvider.of(generatedValueFieldIds.map { FieldValueProvider(defaultIdGenerator, it) }))
426427
}.let(transform)
428+
val coverageToMinStateBeforeSize = mutableMapOf<Trie.Node<Instruction>, Int>()
427429
runJavaFuzzing(
428430
defaultIdGenerator,
429431
methodUnderTest,
@@ -450,15 +452,15 @@ class UtBotSymbolicEngine(
450452
return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP)
451453
}
452454

453-
val initialEnvironmentModels = EnvironmentModels(thisInstance?.model, values.map { it.model }, mapOf())
455+
val stateBefore = EnvironmentModels(thisInstance?.model, values.map { it.model }, mapOf())
454456

455457
val concreteExecutionResult: UtConcreteExecutionResult? = try {
456458
val timeoutMillis = min(UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, diff)
457-
concreteExecutor.executeConcretely(methodUnderTest, initialEnvironmentModels, listOf(), timeoutMillis)
459+
concreteExecutor.executeConcretely(methodUnderTest, stateBefore, listOf(), timeoutMillis)
458460
} catch (e: CancellationException) {
459461
logger.debug { "Cancelled by timeout" }; null
460462
} catch (e: InstrumentedProcessDeathException) {
461-
emitFailedConcreteExecutionResult(initialEnvironmentModels, e); null
463+
emitFailedConcreteExecutionResult(stateBefore, e); null
462464
} catch (e: Throwable) {
463465
emit(UtError("Default concrete execution failed", e)); null
464466
}
@@ -480,9 +482,16 @@ class UtBotSymbolicEngine(
480482
val result = concreteExecutionResult.result
481483
val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions
482484
var trieNode: Trie.Node<Instruction>? = null
485+
483486
if (coveredInstructions.isNotEmpty()) {
484487
trieNode = descr.tracer.add(coveredInstructions)
485-
if (trieNode.count > 1) {
488+
489+
val earlierStateBeforeSize = coverageToMinStateBeforeSize[trieNode]
490+
val curStateBeforeSize = stateBefore.calculateSize()
491+
492+
if (earlierStateBeforeSize == null || curStateBeforeSize < earlierStateBeforeSize)
493+
coverageToMinStateBeforeSize[trieNode] = curStateBeforeSize
494+
else {
486495
if (++attempts >= attemptsLimit) {
487496
return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP)
488497
}
@@ -500,7 +509,7 @@ class UtBotSymbolicEngine(
500509

501510
emit(
502511
UtFuzzedExecution(
503-
stateBefore = initialEnvironmentModels,
512+
stateBefore = stateBefore,
504513
stateAfter = concreteExecutionResult.stateAfter,
505514
result = concreteExecutionResult.result,
506515
coverage = concreteExecutionResult.coverage,

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

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
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.fuzzer.UtFuzzedExecution
11+
import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor
2412

2513

2614
/**
@@ -53,16 +41,17 @@ fun <T : Any> minimizeTestCase(
5341

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

60-
val filteredExecutions = executions.filterIndexed { idx, _ -> idx !in unknownCoverageExecutions }
48+
val filteredExecutions = filterOutDuplicateCoverages(executions - unknownCoverageExecutions)
6149
val (mapping, executionToPriorityMapping) = buildMapping(filteredExecutions)
6250

63-
val usedExecutionIndexes = (GreedyEssential.minimize(mapping, executionToPriorityMapping) + unknownCoverageExecutions).toSet()
51+
val usedFilteredExecutionIndexes = GreedyEssential.minimize(mapping, executionToPriorityMapping).toSet()
52+
val usedFilteredExecutions = filteredExecutions.filterIndexed { idx, _ -> idx in usedFilteredExecutionIndexes }
6453

65-
val usedMinimizedExecutions = executions.filterIndexed { idx, _ -> idx in usedExecutionIndexes }
54+
val usedMinimizedExecutions = usedFilteredExecutions + unknownCoverageExecutions
6655

6756
return if (UtSettings.minimizeCrashExecutions) {
6857
usedMinimizedExecutions.filteredCrashExecutions()
@@ -71,6 +60,18 @@ fun minimizeExecutions(executions: List<UtExecution>): List<UtExecution> {
7160
}
7261
}
7362

63+
private fun filterOutDuplicateCoverages(executions: List<UtExecution>): List<UtExecution> {
64+
val (executionIdxToCoveredEdgesMap, _) = buildMapping(executions)
65+
return executions
66+
.withIndex()
67+
// we need to group by coveredEdges and not just Coverage to not miss exceptional edges that buildMapping() function adds
68+
.groupBy(
69+
keySelector = { indexedExecution -> executionIdxToCoveredEdgesMap[indexedExecution.index] },
70+
valueTransform = { indexedExecution -> indexedExecution.value }
71+
).values
72+
.map { executionsWithEqualCoverage -> executionsWithEqualCoverage.chooseOneExecution() }
73+
}
74+
7475
/**
7576
* Groups the [executions] by their `paths` on `first` [branchInstructionsNumber] `branch` instructions.
7677
*
@@ -192,55 +193,20 @@ private fun List<UtExecution>.filteredCrashExecutions(): List<UtExecution> {
192193

193194
val notCrashExecutions = filterNot { it.result is UtConcreteExecutionFailure }
194195

195-
return notCrashExecutions + crashExecutions.chooseMinimalCrashExecution()
196+
return notCrashExecutions + crashExecutions.chooseOneExecution()
196197
}
197198

198199
/**
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].
200+
* Chooses one execution with the highest [execution priority][getExecutionPriority]. If multiple executions
201+
* have the same priority, then the one with the [smallest][calculateSize] [UtExecution.stateBefore] is chosen.
202+
*
203+
* Only [UtExecution.stateBefore] is considered, because [UtExecution.result] and [UtExecution.stateAfter]
204+
* don't represent true picture as they are limited by [construction depth][UtModelConstructor.maxDepth] and their
205+
* sizes can't be calculated for crushed executions.
201206
*/
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-
}
207+
private fun List<UtExecution>.chooseOneExecution(): UtExecution = minWithOrNull(
208+
compareBy({ it.getExecutionPriority() }, { it.stateBefore.calculateSize() })
209+
) ?: error("Cannot find minimal execution within empty executions")
244210

245211
/**
246212
* Extends the [instructionsWithoutExtra] with one extra instruction if the [result] is
@@ -274,6 +240,8 @@ private fun Throwable.exceptionToInfo(): String =
274240
* Returns an execution priority. [UtSymbolicExecution] has the highest priority
275241
* over other executions like [UtFuzzedExecution], [UtFailedExecution], etc.
276242
*
243+
* NOTE! Smaller number represents higher priority.
244+
*
277245
* See [https://github.com/UnitTestBot/UTBotJava/issues/1504] for more details.
278246
*/
279247
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)