Skip to content

Commit c1d1ca7

Browse files
authored
Minimize UtExecution number produced by fuzzing and collect coverage statistics (#465)
1 parent 233ed6e commit c1d1ca7

File tree

3 files changed

+311
-3
lines changed

3 files changed

+311
-3
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ import org.utbot.framework.plugin.api.util.utContext
8181
import org.utbot.framework.plugin.api.util.description
8282
import org.utbot.fuzzer.FallbackModelProvider
8383
import org.utbot.fuzzer.FuzzedMethodDescription
84+
import org.utbot.fuzzer.FuzzedValue
8485
import org.utbot.fuzzer.ModelProvider
86+
import org.utbot.fuzzer.Trie
8587
import org.utbot.fuzzer.collectConstantsForFuzzer
8688
import org.utbot.fuzzer.defaultModelProviders
8789
import org.utbot.fuzzer.fuzz
@@ -408,7 +410,8 @@ class UtBotSymbolicEngine(
408410
parameterNameMap = { index -> names?.getOrNull(index) }
409411
}
410412
val modelProviderWithFallback = modelProvider(defaultModelProviders { nextDefaultModelId++ }).withFallback(fallbackModelProvider::toModel)
411-
val coveredInstructionTracker = mutableSetOf<Instruction>()
413+
val coveredInstructionTracker = Trie(Instruction::id)
414+
val coveredInstructionValues = mutableMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
412415
var attempts = UtSettings.fuzzingMaxAttempts
413416
fuzz(methodUnderTestDescription, modelProviderWithFallback).forEach { values ->
414417
if (System.currentTimeMillis() >= until) {
@@ -431,12 +434,14 @@ class UtBotSymbolicEngine(
431434
}
432435
}
433436

434-
if (!coveredInstructionTracker.addAll(concreteExecutionResult.coverage.coveredInstructions)) {
437+
val count = coveredInstructionTracker.add(concreteExecutionResult.coverage.coveredInstructions)
438+
if (count.count > 1) {
435439
if (--attempts < 0) {
436440
return@flow
437441
}
442+
return@forEach
438443
}
439-
444+
coveredInstructionValues[count] = values
440445
val nameSuggester = sequenceOf(ModelBasedNameSuggester(), MethodBasedNameSuggester())
441446
val testMethodName = try {
442447
nameSuggester.flatMap { it.suggest(methodUnderTestDescription, values, concreteExecutionResult.result) }.firstOrNull()
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package org.utbot.fuzzer
2+
3+
fun <T> trieOf(vararg values: Iterable<T>): Trie<T, T> = IdentityTrie<T>().apply {
4+
values.forEach(this::add)
5+
}
6+
7+
fun stringTrieOf(vararg values: String): StringTrie = StringTrie().apply {
8+
values.forEach(this::add)
9+
}
10+
11+
class StringTrie : IdentityTrie<Char>() {
12+
fun add(string: String) = super.add(string.toCharArray().asIterable())
13+
fun removeCompletely(string: String) = super.removeCompletely(string.toCharArray().asIterable())
14+
fun remove(string: String) = super.remove(string.toCharArray().asIterable())
15+
operator fun get(string: String) = super.get(string.toCharArray().asIterable())
16+
fun collect() = asSequence().map { String(it.toCharArray()) }.toSet()
17+
}
18+
19+
open class IdentityTrie<T> : Trie<T, T>({ it })
20+
21+
/**
22+
* Implementation of a trie for any iterable values.
23+
*/
24+
open class Trie<T, K>(
25+
private val keyExtractor: (T) -> K
26+
) : Iterable<List<T>> {
27+
28+
private val roots = HashMap<K, NodeImpl<T, K>>()
29+
private val implementations = HashMap<Node<T>, NodeImpl<T, K>>()
30+
31+
/**
32+
* Adds value into a trie.
33+
*
34+
* If value already exists then do nothing except increasing internal counter of added values.
35+
* The counter can be returned by [Node.count].
36+
*
37+
* @return corresponding [Node] of the last element in the `values`
38+
*/
39+
fun add(values: Iterable<T>): Node<T> {
40+
val root = try { values.first() } catch (e: NoSuchElementException) { error("Empty list are not allowed") }
41+
var key = keyExtractor(root)
42+
var node = roots.computeIfAbsent(key) { NodeImpl(root, null) }
43+
values.asSequence().drop(1).forEach { value ->
44+
key = keyExtractor(value)
45+
node = node.children.computeIfAbsent(key) { NodeImpl(value, node) }
46+
}
47+
node.count++
48+
implementations[node] = node
49+
return node
50+
}
51+
52+
/**
53+
* Decreases node counter value or removes the value completely if `counter == 1`.
54+
*
55+
* Use [removeCompletely] to remove the value from the trie regardless of counter value.
56+
*
57+
* @return removed node if value exists.
58+
*/
59+
fun remove(values: Iterable<T>): Node<T>? {
60+
val node = findImpl(values) ?: return null
61+
return when {
62+
node.count == 1 -> removeCompletely(values)
63+
node.count > 1 -> node.apply { count-- }
64+
else -> throw IllegalStateException("count should be 1 or greater")
65+
}
66+
}
67+
68+
/**
69+
* Removes value from a trie.
70+
*
71+
* The value is removed completely from the trie. Thus, the next code is true:
72+
*
73+
* ```
74+
* trie.remove(someValue)
75+
* trie.get(someValue) == null
76+
* ```
77+
*
78+
* Use [remove] to decrease counter value instead of removal.
79+
*
80+
* @return removed node if value exists
81+
*/
82+
fun removeCompletely(values: Iterable<T>): Node<T>? {
83+
val node = findImpl(values) ?: return null
84+
if (node.count > 0 && node.children.isEmpty()) {
85+
var n: NodeImpl<T, K>? = node
86+
while (n != null) {
87+
val key = keyExtractor(n.data)
88+
n = n.parent
89+
if (n == null) {
90+
val removed = roots.remove(key)
91+
check(removed != null)
92+
} else {
93+
val removed = n.children.remove(key)
94+
check(removed != null)
95+
if (n.count != 0) {
96+
break
97+
}
98+
}
99+
}
100+
}
101+
return if (node.count > 0) {
102+
node.count = 0
103+
implementations.remove(node)
104+
node
105+
} else {
106+
null
107+
}
108+
}
109+
110+
operator fun get(values: Iterable<T>): Node<T>? {
111+
return findImpl(values)
112+
}
113+
114+
operator fun get(node: Node<T>): List<T>? {
115+
return implementations[node]?.let(this::buildValue)
116+
}
117+
118+
private fun findImpl(values: Iterable<T>): NodeImpl<T, K>? {
119+
val root = try { values.first() } catch (e: NoSuchElementException) { return null }
120+
var key = keyExtractor(root)
121+
var node = roots[key] ?: return null
122+
values.asSequence().drop(1).forEach { value ->
123+
key = keyExtractor(value)
124+
node = node.children[key] ?: return null
125+
}
126+
return node.takeIf { it.count > 0 }
127+
}
128+
129+
override fun iterator(): Iterator<List<T>> {
130+
return iterator {
131+
roots.values.forEach { node ->
132+
traverseImpl(node)
133+
}
134+
}
135+
}
136+
137+
private suspend fun SequenceScope<List<T>>.traverseImpl(node: NodeImpl<T, K>) {
138+
val stack = ArrayDeque<NodeImpl<T, K>>()
139+
stack.addLast(node)
140+
while (stack.isNotEmpty()) {
141+
val n = stack.removeLast()
142+
if (n.count > 0) {
143+
yield(buildValue(n))
144+
}
145+
n.children.values.forEach(stack::addLast)
146+
}
147+
}
148+
149+
private fun buildValue(node: NodeImpl<T, K>): List<T> {
150+
return generateSequence(node) { it.parent }.map { it.data }.toList().asReversed()
151+
}
152+
153+
interface Node<T> {
154+
val data: T
155+
val count: Int
156+
}
157+
158+
/**
159+
* Trie node
160+
*
161+
* @param data data to be stored
162+
* @param parent reference to the previous element of the value
163+
* @param count number of value insertions
164+
* @param children list of children mapped by their key
165+
*/
166+
private class NodeImpl<T, K>(
167+
override val data: T,
168+
val parent: NodeImpl<T, K>?,
169+
override var count: Int = 0,
170+
val children: MutableMap<K, NodeImpl<T, K>> = HashMap(),
171+
) : Node<T>
172+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package org.utbot.framework.plugin.api
2+
3+
import org.junit.jupiter.api.Assertions.*
4+
import org.junit.jupiter.api.Test
5+
import org.utbot.fuzzer.Trie
6+
import org.utbot.fuzzer.stringTrieOf
7+
import org.utbot.fuzzer.trieOf
8+
9+
class TrieTest {
10+
11+
@Test
12+
fun simpleTest() {
13+
val trie = stringTrieOf()
14+
assertThrows(java.lang.IllegalStateException::class.java) {
15+
trie.add(emptyList())
16+
}
17+
assertEquals(1, trie.add("Tree").count)
18+
assertEquals(2, trie.add("Tree").count)
19+
assertEquals(1, trie.add("Trees").count)
20+
assertEquals(1, trie.add("Treespss").count)
21+
assertEquals(1, trie.add("Game").count)
22+
assertEquals(1, trie.add("Gamer").count)
23+
assertEquals(1, trie.add("Games").count)
24+
assertEquals(2, trie["Tree"]?.count)
25+
assertEquals(1, trie["Trees"]?.count)
26+
assertEquals(1, trie["Gamer"]?.count)
27+
assertNull(trie["Treesp"])
28+
assertNull(trie["Treessss"])
29+
30+
assertEquals(setOf("Tree", "Trees", "Treespss", "Game", "Gamer", "Games"), trie.collect())
31+
}
32+
33+
@Test
34+
fun testSingleElement() {
35+
val trie = trieOf(listOf(1))
36+
assertEquals(1, trie.toList().size)
37+
}
38+
39+
@Test
40+
fun testRemoval() {
41+
val trie = stringTrieOf()
42+
trie.add("abc")
43+
assertEquals(1, trie.toList().size)
44+
trie.add("abcd")
45+
assertEquals(2, trie.toList().size)
46+
trie.add("abcd")
47+
assertEquals(2, trie.toList().size)
48+
trie.add("abcde")
49+
assertEquals(3, trie.toList().size)
50+
51+
assertNotNull(trie.removeCompletely("abcd"))
52+
assertEquals(2, trie.toList().size)
53+
54+
assertNull(trie.removeCompletely("ffff"))
55+
assertEquals(2, trie.toList().size)
56+
57+
assertNotNull(trie.removeCompletely("abcde"))
58+
assertEquals(1, trie.toList().size)
59+
60+
assertNotNull(trie.removeCompletely("abc"))
61+
assertEquals(0, trie.toList().size)
62+
}
63+
64+
@Test
65+
fun testSearchingAfterDeletion() {
66+
val trie = stringTrieOf("abc", "abc", "abcde")
67+
assertEquals(2, trie.toList().size)
68+
assertEquals(2, trie["abc"]?.count)
69+
70+
val removed1 = trie.remove("abc")
71+
assertNotNull(removed1)
72+
73+
val find = trie["abc"]
74+
assertNotNull(find)
75+
assertEquals(1, find!!.count)
76+
77+
val removed2 = trie.remove("abc")
78+
assertNotNull(removed2)
79+
}
80+
81+
@Test
82+
fun testTraverse() {
83+
val trie = Trie(Data::id).apply {
84+
add((1..10).map { Data(it.toLong(), it) })
85+
add((1..10).mapIndexed { index, it -> if (index == 5) Data(3L, it) else Data(it.toLong(), it) })
86+
}
87+
88+
val paths = trie.toList()
89+
assertEquals(2, paths.size)
90+
assertNotEquals(paths[0], paths[1])
91+
}
92+
93+
@Test
94+
fun testNoDuplications() {
95+
val trie = trieOf(
96+
(1..10),
97+
(1..10),
98+
(1..10),
99+
(1..10),
100+
(1..10),
101+
)
102+
103+
assertEquals(1, trie.toList().size)
104+
assertEquals(5, trie[(1..10)]!!.count)
105+
}
106+
107+
@Test
108+
fun testAcceptsNulls() {
109+
val trie = trieOf(
110+
listOf(null),
111+
listOf(null, null),
112+
listOf(null, null, null),
113+
)
114+
115+
assertEquals(3, trie.toList().size)
116+
for (i in 1 .. 3) {
117+
assertEquals(1, trie[(1..i).map { null }]!!.count)
118+
}
119+
}
120+
121+
@Test
122+
fun testAddPrefixAfterWord() {
123+
val trie = stringTrieOf()
124+
trie.add("Hello, world!")
125+
trie.add("Hello")
126+
127+
assertEquals(setOf("Hello, world!", "Hello"), trie.collect())
128+
}
129+
130+
data class Data(val id: Long, val number: Int)
131+
}

0 commit comments

Comments
 (0)