Skip to content

Minimize UtExecution number produced by fuzzing and collect coverage statistics #465

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
merged 2 commits into from
Jul 11, 2022
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
Expand Up @@ -81,7 +81,9 @@ import org.utbot.framework.plugin.api.util.utContext
import org.utbot.framework.plugin.api.util.description
import org.utbot.fuzzer.FallbackModelProvider
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.Trie
import org.utbot.fuzzer.collectConstantsForFuzzer
import org.utbot.fuzzer.defaultModelProviders
import org.utbot.fuzzer.fuzz
Expand Down Expand Up @@ -408,7 +410,8 @@ class UtBotSymbolicEngine(
parameterNameMap = { index -> names?.getOrNull(index) }
}
val modelProviderWithFallback = modelProvider(defaultModelProviders { nextDefaultModelId++ }).withFallback(fallbackModelProvider::toModel)
val coveredInstructionTracker = mutableSetOf<Instruction>()
val coveredInstructionTracker = Trie(Instruction::id)
val coveredInstructionValues = mutableMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
var attempts = UtSettings.fuzzingMaxAttempts
fuzz(methodUnderTestDescription, modelProviderWithFallback).forEach { values ->
if (System.currentTimeMillis() >= until) {
Expand All @@ -431,12 +434,14 @@ class UtBotSymbolicEngine(
}
}

if (!coveredInstructionTracker.addAll(concreteExecutionResult.coverage.coveredInstructions)) {
val count = coveredInstructionTracker.add(concreteExecutionResult.coverage.coveredInstructions)
if (count.count > 1) {
if (--attempts < 0) {
return@flow
}
return@forEach
}

coveredInstructionValues[count] = values
val nameSuggester = sequenceOf(ModelBasedNameSuggester(), MethodBasedNameSuggester())
val testMethodName = try {
nameSuggester.flatMap { it.suggest(methodUnderTestDescription, values, concreteExecutionResult.result) }.firstOrNull()
Expand Down
172 changes: 172 additions & 0 deletions utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package org.utbot.fuzzer

fun <T> trieOf(vararg values: Iterable<T>): Trie<T, T> = IdentityTrie<T>().apply {
values.forEach(this::add)
}

fun stringTrieOf(vararg values: String): StringTrie = StringTrie().apply {
values.forEach(this::add)
}

class StringTrie : IdentityTrie<Char>() {
fun add(string: String) = super.add(string.toCharArray().asIterable())
fun removeCompletely(string: String) = super.removeCompletely(string.toCharArray().asIterable())
fun remove(string: String) = super.remove(string.toCharArray().asIterable())
operator fun get(string: String) = super.get(string.toCharArray().asIterable())
fun collect() = asSequence().map { String(it.toCharArray()) }.toSet()
}

open class IdentityTrie<T> : Trie<T, T>({ it })

/**
* Implementation of a trie for any iterable values.
*/
open class Trie<T, K>(
private val keyExtractor: (T) -> K
) : Iterable<List<T>> {

private val roots = HashMap<K, NodeImpl<T, K>>()
private val implementations = HashMap<Node<T>, NodeImpl<T, K>>()

/**
* Adds value into a trie.
*
* If value already exists then do nothing except increasing internal counter of added values.
* The counter can be returned by [Node.count].
*
* @return corresponding [Node] of the last element in the `values`
*/
fun add(values: Iterable<T>): Node<T> {
val root = try { values.first() } catch (e: NoSuchElementException) { error("Empty list are not allowed") }
var key = keyExtractor(root)
var node = roots.computeIfAbsent(key) { NodeImpl(root, null) }
values.asSequence().drop(1).forEach { value ->
key = keyExtractor(value)
node = node.children.computeIfAbsent(key) { NodeImpl(value, node) }
}
node.count++
implementations[node] = node
return node
}

/**
* Decreases node counter value or removes the value completely if `counter == 1`.
*
* Use [removeCompletely] to remove the value from the trie regardless of counter value.
*
* @return removed node if value exists.
*/
fun remove(values: Iterable<T>): Node<T>? {
val node = findImpl(values) ?: return null
return when {
node.count == 1 -> removeCompletely(values)
node.count > 1 -> node.apply { count-- }
else -> throw IllegalStateException("count should be 1 or greater")
}
}

/**
* Removes value from a trie.
*
* The value is removed completely from the trie. Thus, the next code is true:
*
* ```
* trie.remove(someValue)
* trie.get(someValue) == null
* ```
*
* Use [remove] to decrease counter value instead of removal.
*
* @return removed node if value exists
*/
fun removeCompletely(values: Iterable<T>): Node<T>? {
val node = findImpl(values) ?: return null
if (node.count > 0 && node.children.isEmpty()) {
var n: NodeImpl<T, K>? = node
while (n != null) {
val key = keyExtractor(n.data)
n = n.parent
if (n == null) {
val removed = roots.remove(key)
check(removed != null)
} else {
val removed = n.children.remove(key)
check(removed != null)
if (n.count != 0) {
break
}
}
}
}
return if (node.count > 0) {
node.count = 0
implementations.remove(node)
node
} else {
null
}
}

operator fun get(values: Iterable<T>): Node<T>? {
return findImpl(values)
}

operator fun get(node: Node<T>): List<T>? {
return implementations[node]?.let(this::buildValue)
}

private fun findImpl(values: Iterable<T>): NodeImpl<T, K>? {
val root = try { values.first() } catch (e: NoSuchElementException) { return null }
var key = keyExtractor(root)
var node = roots[key] ?: return null
values.asSequence().drop(1).forEach { value ->
key = keyExtractor(value)
node = node.children[key] ?: return null
}
return node.takeIf { it.count > 0 }
}

override fun iterator(): Iterator<List<T>> {
return iterator {
roots.values.forEach { node ->
traverseImpl(node)
}
}
}

private suspend fun SequenceScope<List<T>>.traverseImpl(node: NodeImpl<T, K>) {
val stack = ArrayDeque<NodeImpl<T, K>>()
stack.addLast(node)
while (stack.isNotEmpty()) {
val n = stack.removeLast()
if (n.count > 0) {
yield(buildValue(n))
}
n.children.values.forEach(stack::addLast)
}
}

private fun buildValue(node: NodeImpl<T, K>): List<T> {
return generateSequence(node) { it.parent }.map { it.data }.toList().asReversed()
}

interface Node<T> {
val data: T
val count: Int
}

/**
* Trie node
*
* @param data data to be stored
* @param parent reference to the previous element of the value
* @param count number of value insertions
* @param children list of children mapped by their key
*/
private class NodeImpl<T, K>(
override val data: T,
val parent: NodeImpl<T, K>?,
override var count: Int = 0,
val children: MutableMap<K, NodeImpl<T, K>> = HashMap(),
) : Node<T>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.utbot.framework.plugin.api

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.utbot.fuzzer.Trie
import org.utbot.fuzzer.stringTrieOf
import org.utbot.fuzzer.trieOf

class TrieTest {

@Test
fun simpleTest() {
val trie = stringTrieOf()
assertThrows(java.lang.IllegalStateException::class.java) {
trie.add(emptyList())
}
assertEquals(1, trie.add("Tree").count)
assertEquals(2, trie.add("Tree").count)
assertEquals(1, trie.add("Trees").count)
assertEquals(1, trie.add("Treespss").count)
assertEquals(1, trie.add("Game").count)
assertEquals(1, trie.add("Gamer").count)
assertEquals(1, trie.add("Games").count)
assertEquals(2, trie["Tree"]?.count)
assertEquals(1, trie["Trees"]?.count)
assertEquals(1, trie["Gamer"]?.count)
assertNull(trie["Treesp"])
assertNull(trie["Treessss"])

assertEquals(setOf("Tree", "Trees", "Treespss", "Game", "Gamer", "Games"), trie.collect())
}

@Test
fun testSingleElement() {
val trie = trieOf(listOf(1))
assertEquals(1, trie.toList().size)
}

@Test
fun testRemoval() {
val trie = stringTrieOf()
trie.add("abc")
assertEquals(1, trie.toList().size)
trie.add("abcd")
assertEquals(2, trie.toList().size)
trie.add("abcd")
assertEquals(2, trie.toList().size)
trie.add("abcde")
assertEquals(3, trie.toList().size)

assertNotNull(trie.removeCompletely("abcd"))
assertEquals(2, trie.toList().size)

assertNull(trie.removeCompletely("ffff"))
assertEquals(2, trie.toList().size)

assertNotNull(trie.removeCompletely("abcde"))
assertEquals(1, trie.toList().size)

assertNotNull(trie.removeCompletely("abc"))
assertEquals(0, trie.toList().size)
}

@Test
fun testSearchingAfterDeletion() {
val trie = stringTrieOf("abc", "abc", "abcde")
assertEquals(2, trie.toList().size)
assertEquals(2, trie["abc"]?.count)

val removed1 = trie.remove("abc")
assertNotNull(removed1)

val find = trie["abc"]
assertNotNull(find)
assertEquals(1, find!!.count)

val removed2 = trie.remove("abc")
assertNotNull(removed2)
}

@Test
fun testTraverse() {
val trie = Trie(Data::id).apply {
add((1..10).map { Data(it.toLong(), it) })
add((1..10).mapIndexed { index, it -> if (index == 5) Data(3L, it) else Data(it.toLong(), it) })
}

val paths = trie.toList()
assertEquals(2, paths.size)
assertNotEquals(paths[0], paths[1])
}

@Test
fun testNoDuplications() {
val trie = trieOf(
(1..10),
(1..10),
(1..10),
(1..10),
(1..10),
)

assertEquals(1, trie.toList().size)
assertEquals(5, trie[(1..10)]!!.count)
}

@Test
fun testAcceptsNulls() {
val trie = trieOf(
listOf(null),
listOf(null, null),
listOf(null, null, null),
)

assertEquals(3, trie.toList().size)
for (i in 1 .. 3) {
assertEquals(1, trie[(1..i).map { null }]!!.count)
}
}

@Test
fun testAddPrefixAfterWord() {
val trie = stringTrieOf()
trie.add("Hello, world!")
trie.add("Hello")

assertEquals(setOf("Hello, world!", "Hello"), trie.collect())
}

data class Data(val id: Long, val number: Int)
}