[evaluation-plugin] LME-66 Init conflict-resolution evaluation

GitOrigin-RevId: 4c19dedb0b0b0333bbf2a0613cd009874b2564e1
This commit is contained in:
Roman Vasiliev
2024-10-17 16:10:04 +00:00
committed by intellij-monorepo-bot
parent a83afce5dc
commit 8503c208a2
28 changed files with 705 additions and 39 deletions
@@ -292,7 +292,8 @@ function addSuggestions(sessionDiv, popup, lookup) {
p.setAttribute("style", "font-weight: bold;")
}
const presentationText = suggestions[i].presentationText.replace(/</g, '&lt;').replace(/>/g, '&gt;')
p.innerHTML = removeCommonIndentFromCodeSnippet(presentationText)
p.innerHTML = presentationText
// p.innerHTML = removeCommonIndentFromCodeSnippet(presentationText) // FIXME doesn't work anyway?
suggestionDiv.appendChild(p)
popup.appendChild(suggestionDiv)
}
@@ -69,6 +69,7 @@ button:focus {
.session {
position: relative;
display: block;
font-weight: bold;
}
@@ -126,7 +127,7 @@ button:focus {
border-bottom: none;
border-top: none;
z-index: 99;
top: 1.5em;
top: 100%;
left: 0;
}
@@ -16,7 +16,8 @@ import java.nio.file.Paths
* @property outputDir The output directory for the evaluation results.
* @property strategy The evaluation strategy used.
* @property actions The configuration for actions generation step.
* @property fileDataset Dataset configuration for standalone setups
* @property csvDataset Dataset configuration for CSV file.
* @property conflictDataset Dataset configuration for conflict resolution evaluations.
* @property interpret The configuration for actions interpretation step.
* @property reorder The configuration for element reordering step.
* @property reports The configuration for report generation step.
@@ -25,7 +26,8 @@ data class Config private constructor(
val outputDir: String,
val strategy: EvaluationStrategy,
val actions: ActionsGeneration?,
val fileDataset: FileDataset?,
val csvDataset: FileDataset?,
val conflictDataset: ConflictDataset?,
val interpret: ActionsInterpretation,
val reorder: ReorderElements,
val reports: ReportGeneration
@@ -76,6 +78,10 @@ data class Config private constructor(
val chunkSize: Int,
)
data class ConflictDataset internal constructor(
val url: String
)
/**
* Represents the configuration for the interpretation of actions.
*
@@ -135,6 +141,7 @@ data class Config private constructor(
class Builder internal constructor() {
var actions: ActionsGeneration? = null
var fileDataset: FileDataset? = null
var conflictDataset: ConflictDataset? = null
var outputDir: String? = null
var strategy: EvaluationStrategy = EvaluationStrategy.defaultStrategy
@@ -161,7 +168,8 @@ data class Config private constructor(
constructor(config: Config) : this() {
actions = config.actions
fileDataset = config.fileDataset
fileDataset = config.csvDataset
conflictDataset = config.conflictDataset
outputDir = config.outputDir
strategy = config.strategy
saveLogs = config.interpret.saveLogs
@@ -206,6 +214,7 @@ data class Config private constructor(
strategy,
actions,
fileDataset,
conflictDataset,
ActionsInterpretation(
experimentGroup,
sessionsLimit,
@@ -63,7 +63,8 @@ object ConfigFactory {
map.getIfExists<String>("language"),
this
)
deserializeFileDataset(map.getIfExists("fileDataset"), this)
deserializeFileDataset(map.getIfExists("csvDataset"), this)
deserializeConflictDataset(map.getIfExists("conflictDataset"), this)
deserializeStrategy(map.getIfExists("strategy"), strategySerializer, actions?.language, this)
deserializeActionsInterpretation(map.getIfExists("interpret"), this)
@@ -123,6 +124,19 @@ object ConfigFactory {
)
}
private fun deserializeConflictDataset(
map: Map<String, Any>?,
builder: Config.Builder
) {
if (map == null) {
return
}
builder.conflictDataset = Config.ConflictDataset(
map.getAs("url")
)
}
private fun deserializeActionsInterpretation(map: Map<String, Any>?, builder: Config.Builder) {
if (map == null) return
if (map.containsKey("experimentGroup")) {
@@ -0,0 +1,23 @@
{
"outputDir": "ml-eval-conflict-resolution-output",
"strategy": {},
"conflictDataset": {
"url": "existing:conflicts"
},
"interpret": {
"saveLogs": false,
"saveFeatures": false,
"sessionProbability": 1.0,
"sessionSeed": null,
"sessionsLimit": 30,
"filesLimit": 3,
"order": "LINEAR",
"trainTestSplit": 70
},
"reports": {
"evaluationTitle": "Default",
"defaultMetrics": null,
"sessionsFilters": [],
"comparisonFilters": []
}
}
@@ -1,7 +1,7 @@
{
"outputDir": "ml-eval-standalone-example-output",
"strategy": {},
"fileDataset": {
"csvDataset": {
"url": "./latin_letters.csv",
"chunkSize": 10
},
@@ -55,6 +55,7 @@
<evaluableFeature implementation="com.intellij.cce.evaluable.completion.TokenCompletionFeature"/>
<evaluableFeature implementation="com.intellij.cce.actions.ContextCollectionFeature"/>
<evaluableFeature implementation="com.intellij.cce.evaluable.standaloneExample.StandaloneExampleFeature"/>
<evaluableFeature implementation="com.intellij.cce.evaluable.conflictResolution.ConflictResolutionFeature"/>
<suggestionsProvider implementation="com.intellij.cce.evaluable.completion.DefaultCompletionProvider"/>
</extensions>
@@ -72,6 +72,7 @@ internal class ContextCollectionEvaluationCommand : CompletionEvaluationStarter.
dataset.project,
dataset.processor,
feature.name,
dataset.featureInvoker
) {
override fun prepare(datasetContext: DatasetContext, progress: Progress) {
val files = runReadAction {
@@ -14,7 +14,8 @@ import kotlin.io.path.readLines
class CsvDataset(
private val datasetRef: DatasetRef,
private val chunkSize: Int,
private val targetField: String
private val targetField: String,
private val featureInvoker: FeatureInvoker,
) : EvaluationDataset {
override val setupSdk: EvaluationStep? = null
override val checkSdk: EvaluationStep? = null
@@ -70,7 +71,6 @@ class CsvDataset(
override val presentationText: String = presentationText.toString()
override fun evaluate(
featureInvoker: FeatureInvoker,
handler: InterpretationHandler,
filter: InterpretFilter,
order: InterpretationOrder,
@@ -11,7 +11,7 @@ import com.intellij.cce.util.Progress
/**
* Represents data which will be used for evaluation.
*/
interface EvaluationDataset {
interface EvaluationDataset { // FIXME with implicit invoker inside it may make sense to merge this interface into EvaluationEnvironment
val setupSdk: EvaluationStep?
val checkSdk: EvaluationStep?
@@ -32,7 +32,6 @@ interface EvaluationDatasetChunk {
val presentationText: String
fun evaluate(
featureInvoker: FeatureInvoker,
handler: InterpretationHandler,
filter: InterpretFilter,
order: InterpretationOrder,
@@ -36,6 +36,7 @@ open class ProjectActionsDataset(
val project: Project,
val processor: GenerateActionsProcessor,
private val featureName: String,
val featureInvoker: FeatureInvoker,
) : EvaluationDataset {
private val datasetRef = config.sourceFile.run {
val sf = this ?: ""
@@ -237,7 +238,6 @@ open class ProjectActionsDataset(
override val sessionCount: Int = fileActions.sessionsCount
override fun evaluate(
featureInvoker: FeatureInvoker,
handler: InterpretationHandler,
filter: InterpretFilter,
order: InterpretationOrder,
@@ -54,9 +54,9 @@ abstract class EvaluableFeatureBase<T : EvaluationStrategy>(override val name: S
EvaluationRootInfo(true),
project,
getGenerateActionsProcessor(strategy, project),
name
),
featureInvoker = getFeatureInvoker(project, Language.resolve(actions.language), strategy)
name,
featureInvoker = getFeatureInvoker(project, Language.resolve(actions.language), strategy)
)
)
}
}
@@ -18,8 +18,6 @@ abstract class StandaloneFeature<T : EvaluationStrategy>(
abstract fun getDataset(config: Config): EvaluationDataset
abstract fun getFeatureInvoker(strategy: T): FeatureInvoker
override fun getPreliminaryEvaluationSteps(): List<EvaluationStep> = emptyList()
override fun getEvaluationSteps(config: Config): List<EvaluationStep> = emptyList()
@@ -34,8 +32,7 @@ abstract class StandaloneFeature<T : EvaluationStrategy>(
override fun prepareEnvironment(config: Config): EvaluationEnvironment {
return StandaloneEnvironment(
getDataset(config),
getFeatureInvoker(config.strategy())
getDataset(config)
)
}
}
@@ -0,0 +1,124 @@
package com.intellij.cce.evaluable.conflictResolution
import com.intellij.cce.actions.*
import com.intellij.cce.core.*
import com.intellij.cce.evaluation.EvaluationStep
import com.intellij.cce.interpreter.InterpretFilter
import com.intellij.cce.interpreter.InterpretationHandler
import com.intellij.cce.interpreter.InterpretationOrder
import com.intellij.cce.util.Progress
import java.nio.file.Path
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.readText
class ConflictDataset(
private val datasetRef: DatasetRef,
private val conflictResolver: ConflictResolver<*>
) : EvaluationDataset {
override val setupSdk: EvaluationStep? = null
override val checkSdk: EvaluationStep? = null
override val preparationDescription: String = "Checking that conflict dataset exists"
override fun prepare(datasetContext: DatasetContext, progress: Progress) {
datasetRef.prepare(datasetContext)
}
override fun sessionCount(datasetContext: DatasetContext): Int =
datasetContext.path(datasetRef).listDirectoryEntries().size // FIXME should be pre-calculated
override fun chunks(datasetContext: DatasetContext): Iterator<EvaluationDatasetChunk> {
return datasetContext.path(datasetRef).listDirectoryEntries().map { conflictPath ->
ConflictChunk(conflictPath)
}.iterator()
}
private inner class ConflictChunk(
private val conflictPath: Path,
override val sessionCount: Int = 1 // FIXME we should pre-calculate it?
) : EvaluationDatasetChunk {
override val datasetName: String = datasetRef.name
override val name: String = conflictPath.fileName.toString()
override val presentationText: String = readText("result")
override fun evaluate(
handler: InterpretationHandler, // FIXME use this as well as filter and order
filter: InterpretFilter,
order: InterpretationOrder,
sessionHandler: (Session) -> Unit
): List<Session> {
val props = ConflictResolver.Props(
base = readText("base"),
parent1 = readText("parent1"),
parent2 = readText("parent2"),
target = readText("result"),
)
val resolvedConflicts = conflictResolver.resolveConflicts(props)
require(resolvedConflicts.keys.containsAll(TextLabel.entries))
return (0 until resolvedConflicts.size).map { conflictIndex ->
val session = conflictSession(resolvedConflicts, conflictIndex)
sessionHandler(session)
session
}
}
private fun <T> conflictSession(
conflicts: MatchSequence<T>,
conflictIndex: Int,
): Session {
val expected = conflicts.text(TextLabel.TARGET, conflictIndex)
val session = Session(
conflicts.charOffset(TextLabel.TARGET, conflictIndex),
expected,
expected.length,
TokenProperties.UNKNOWN
)
val result = conflicts.text(TextLabel.RESULT, conflictIndex)
val suggestions = listOf(
Suggestion(
result,
result,
SuggestionSource.INTELLIJ,
emptyMap(),
isRelevant = expected == result
)
)
val lookup = Lookup(
"",
0,
suggestions,
0,
null,
selectedPosition = suggestions.indexOfFirst { it.isRelevant },
isNew = false,
mapOf(
"aia_context" to contextPresentation(conflicts, conflictIndex)
)
)
session.addLookup(lookup)
return session
}
private fun readText(namePrefix: String): String =
conflictPath.listDirectoryEntries().first { it.fileName.toString().startsWith(namePrefix) }.readText()
private fun contextPresentation(conflicts: MatchSequence<*>, conflictIndex: Int) = """
<<<<<<< BASE
${conflicts.text(TextLabel.BASE, conflictIndex).trimEnd()}
======= PARENT1
${conflicts.text(TextLabel.PARENT_1, conflictIndex).trimEnd()}
======= PARENT2
${conflicts.text(TextLabel.PARENT_2, conflictIndex).trimEnd()}
>>>>>>>
""".trim()
}
}
@@ -0,0 +1,24 @@
package com.intellij.cce.evaluable.conflictResolution
import com.intellij.cce.actions.DatasetRef
import com.intellij.cce.actions.EvaluationDataset
import com.intellij.cce.evaluable.StandaloneFeature
import com.intellij.cce.evaluable.StrategySerializer
import com.intellij.cce.metric.Metric
import com.intellij.cce.metric.PrecisionMetric
import com.intellij.cce.metric.SessionsCountMetric
import com.intellij.cce.workspace.Config
class ConflictResolutionFeature : StandaloneFeature<ConflictResolutionStrategy>("conflict-resolution") {
override fun getStrategySerializer(): StrategySerializer<ConflictResolutionStrategy> = ConflictResolutionStrategy.Serializer()
override fun getDataset(config: Config): EvaluationDataset = ConflictDataset(
DatasetRef.parse(config.conflictDataset!!.url),
TheirConflictResolver()
)
override fun getMetrics(): List<Metric> = listOf(
SessionsCountMetric(),
PrecisionMetric()
)
}
@@ -0,0 +1,17 @@
package com.intellij.cce.evaluable.conflictResolution
import com.google.gson.JsonObject
import com.google.gson.JsonSerializationContext
import com.intellij.cce.evaluable.EvaluationStrategy
import com.intellij.cce.evaluable.StrategySerializer
import com.intellij.cce.filter.EvaluationFilter
import java.lang.reflect.Type
class ConflictResolutionStrategy : EvaluationStrategy {
override val filters: Map<String, EvaluationFilter> = mapOf()
class Serializer : StrategySerializer<ConflictResolutionStrategy> {
override fun serialize(src: ConflictResolutionStrategy, typeOfSrc: Type, context: JsonSerializationContext): JsonObject = JsonObject()
override fun deserialize(map: Map<String, Any>, language: String): ConflictResolutionStrategy = ConflictResolutionStrategy()
}
}
@@ -0,0 +1,40 @@
package com.intellij.cce.evaluable.conflictResolution
interface ConflictResolver<T> {
fun resolveConflicts(props: Props): MatchSequence<T>
class Props(
val base: String,
val parent1: String,
val parent2: String,
val target: String
)
}
open class TheirConflictResolver : ConflictResolver<String> {
override fun resolveConflicts(props: ConflictResolver.Props): MatchSequence<String> {
val resultText = props.parent2
val conflicts = calculateAlignment(TextLabel.BASE, props.base, TextLabel.RESULT, resultText)
.intersectAlignments(calculateAlignment(TextLabel.BASE, props.base, TextLabel.PARENT_1, props.parent1))
.intersectAlignments(calculateAlignment(TextLabel.BASE, props.base, TextLabel.PARENT_2, props.parent2))
.intersectAlignments(calculateAlignment(TextLabel.BASE, props.base, TextLabel.TARGET, props.target))
.complement()
val nonObviousConflicts = (0 until conflicts.size).filterNot { conflictIndex ->
conflicts.text(TextLabel.BASE, conflictIndex) == conflicts.text(TextLabel.PARENT_1, conflictIndex) ||
conflicts.text(TextLabel.BASE, conflictIndex) == conflicts.text(TextLabel.PARENT_2, conflictIndex) ||
conflicts.text(TextLabel.PARENT_1, conflictIndex) == conflicts.text(TextLabel.PARENT_2, conflictIndex)
}
return conflicts.filter(nonObviousConflicts)
}
@Suppress("SameParameterValue", "MemberVisibilityCanBePrivate")
protected fun calculateAlignment(label1: TextLabel, text1: String, label2: TextLabel, text2: String): MatchSequence<String> {
val splitText1 = LineSplitText(text1)
val splitText2 = LineSplitText(text2)
val distances = calculateEditDistance(splitText1, splitText2)
val (ranges1, ranges2) = retrieveAlignment(distances)
return MatchSequence(mapOf(label1 to ranges1, label2 to ranges2), mapOf(label1 to splitText1, label2 to splitText2))
}
}
@@ -0,0 +1,185 @@
package com.intellij.cce.evaluable.conflictResolution
/**
* Represents a sequence of matched ranges within texts.
* Can be used to store texts alignment or conflict sequence.
*
* @constructor For each text range, there should be corresponding ranges in other texts (maybe empty ones).
* Ranges for each text should be sorted.
* Check the init section for range restrictions.
*/
class MatchSequence<T>(
private val ranges: Map<TextLabel, List<IntRange>>,
private val originalTexts: Map<TextLabel, SplitText<T>>,
) {
init {
require(ranges.keys == originalTexts.keys) {
"Keys mismatch: ${ranges.keys} != ${originalTexts.keys}"
}
require(ranges.map { it.value.size }.distinct().size <= 1) {
"For all texts there should be the same amount of ranges."
}
for ((label, labelRanges) in ranges) {
require((labelRanges.firstOrNull()?.first ?: 0) >= 0) {
"First range should be non-negative in $label: $labelRanges"
}
for (i in 0 until labelRanges.size - 1) {
require(labelRanges[i].last < labelRanges[i + 1].first) {
"Ranges should be sorted in $label: $labelRanges"
}
}
require((labelRanges.lastOrNull()?.last ?: -1) < originalTexts.getValue(label).size) {
"Last range shouldn't be greater then text size in $label: $labelRanges"
}
}
}
val keys get() = ranges.keys
val size: Int = keys.firstOrNull()?.let { ranges[it]!!.size } ?: 0
fun range(label: TextLabel, index: Int) = ranges.getValue(label)[index]
fun text(label: TextLabel) = originalTexts.getValue(label).text
fun text(label: TextLabel, index: Int): String = originalTexts.getValue(label).substring(range(label, index))
fun charOffset(label: TextLabel, index: Int): Int = originalTexts.getValue(label).charOffset(range(label, index).first)
fun filter(indexes: Iterable<Int>): MatchSequence<T> {
return MatchSequence(
ranges.mapValues { it.value.filterIndexed { index, _ -> indexes.contains(index) } },
originalTexts
)
}
fun requireAlignment() {
for (index in 0 until size) {
val indexRanges = ranges.map { it.key to it.value[index] }.toMap()
require(indexRanges.map { it.value.last - it.value.first }.distinct().size == 1) {
"There are ranges of different sizes in mapping $index: $indexRanges\n$ranges"
}
}
}
/**
* Generates a sequence of text ranges that are not covered by the original match ranges.
*
* This method computes the complement of the original match ranges, effectively identifying all
* text ranges that do not match.
*/
fun complement(): MatchSequence<T> {
if (ranges.isEmpty() || size == 0) {
return MatchSequence(originalTexts.map { (key, text) -> key to listOf(0 until text.size) }.toMap(), originalTexts)
}
val result = ranges.map { it.key to mutableListOf<IntRange>() }.toMap()
if (ranges.any { it.value.first().first > 0 }) {
ranges.forEach { (key, keyRanges) ->
result.getValue(key) += (0 until keyRanges.first().first)
}
}
for (i in 0 until size - 1) {
ranges.forEach { (key, keyRanges) ->
val prev = keyRanges[i]
val next = keyRanges[i + 1]
result.getValue(key) += (prev.last + 1 until next.first)
}
}
if (ranges.any { (key, ranges) -> ranges.last().last + 1 < originalTexts.getValue(key).size }) {
ranges.forEach { (key, keyRange) ->
result.getValue(key) += (keyRange.last().last + 1 until originalTexts.getValue(key).size)
}
}
return MatchSequence(result, originalTexts)
}
/**
* Intersects the current MatchSequence with another provided MatchSequence.
*
* This method requires that both sequences align properly. It computes the intersection
* of the sequences using only one common key and ensures that the original texts for the
* common key are identical.
*/
fun intersectAlignments(other: MatchSequence<T>): MatchSequence<T> {
requireAlignment()
other.requireAlignment()
val intersectionKeys = keys intersect other.keys
require(intersectionKeys.size == 1) {
"Only intersection of two sequences with one same key is supported: ${keys}, ${other.keys}"
}
val commonKey = intersectionKeys.first()
require(originalTexts.getValue(commonKey) == other.originalTexts.getValue(commonKey)) {
"Texts are different for key $commonKey"
}
val result = (keys union other.keys).associateWith { mutableListOf<IntRange>() }
var pointer1 = 0
var pointer2 = 0
while (pointer1 < size && pointer2 < other.size) {
val range1 = range(commonKey, pointer1)
val range2 = other.range(commonKey, pointer2)
val first = maxOf(range1.first, range2.first)
if (range1.last < first) {
pointer1 += 1
continue
}
if (range2.last < first) {
pointer2 += 1
continue
}
val last = minOf(range1.last, range2.last)
val length = last - first + 1
val delta1 = first - range1.first
val delta2 = first - range2.first
ranges.forEach { (key, keyRanges) ->
val range = keyRanges[pointer1]
result.getValue(key) += range.first + delta1 until range.first + delta1 + length
}
other.ranges.filter { it.key != commonKey }.forEach { (key, keyRanges) ->
val range = keyRanges[pointer2]
result.getValue(key) += range.first + delta2 until range.first + delta2 + length
}
if (range1.last == last) {
pointer1 += 1
}
if (range2.last == last) {
pointer2 += 1
}
}
return MatchSequence(result, originalTexts + other.originalTexts)
}
}
enum class TextLabel {
BASE,
PARENT_1,
PARENT_2,
TARGET,
RESULT;
}
@@ -0,0 +1,95 @@
package com.intellij.cce.evaluable.conflictResolution
interface SplitText<T> {
val text: String
val size: Int
operator fun get(index: Int): T
fun substring(range: IntRange): String
fun charOffset(index: Int): Int
}
data class LineSplitText(override val text: String) : SplitText<String> {
private val lines = text.lines()
private val offsets = lines.scanIndexed(0) { i, acc, line -> acc + line.length + (if (i == lines.lastIndex) 0 else 1) }
override val size: Int get() = lines.size
override fun get(index: Int): String = lines[index]
override fun substring(range: IntRange): String = lines.subList(range.first, range.last + 1).joinToString("\n")
override fun charOffset(index: Int): Int = offsets[index]
}
data class CharacterSplitText(override val text: String) : SplitText<Char> {
override val size: Int = text.length
override fun get(index: Int): Char = text[index]
override fun substring(range: IntRange): String = text.substring(range)
override fun charOffset(index: Int): Int = index
}
fun <T> calculateEditDistance(left: SplitText<T>, right: SplitText<T>): Array<IntArray> {
val dp = Array(left.size + 1) { IntArray(right.size + 1) }
for (i in 0..left.size) {
for (j in 0..right.size) {
dp[i][j] = when {
i == 0 -> j
j == 0 -> i
else -> minOf(
dp[i - 1][j] + 1,
dp[i][j - 1] + 1,
dp[i - 1][j - 1] + (if (left[i - 1] == right[j - 1]) 0 else 2)
)
}
}
}
return dp
}
fun retrieveAlignment(distances: Array<IntArray>): Pair<List<IntRange>, List<IntRange>> {
val leftRanges = mutableListOf<IntRange>()
val rightRanges = mutableListOf<IntRange>()
var i = distances.size - 1
var j = distances.first().size - 1
var matchedLength = 0
fun commitSection() {
if (matchedLength > 0) {
leftRanges.add(i until i + matchedLength)
rightRanges.add(j until j + matchedLength)
matchedLength = 0
}
}
while (i > 0 && j > 0) {
when {
distances[i - 1][j - 1] <= distances[i - 1][j] && distances[i - 1][j - 1] <= distances[i][j - 1] -> {
if (distances[i - 1][j - 1] == distances[i][j]) {
matchedLength += 1
}
else {
commitSection()
}
i -= 1
j -= 1
}
distances[i - 1][j] <= distances[i][j - 1] -> {
commitSection()
i -= 1
}
else -> {
commitSection()
j -= 1
}
}
}
commitSection()
return Pair(leftRanges.reversed(), rightRanges.reversed())
}
@@ -9,7 +9,6 @@ import com.intellij.cce.evaluable.EvaluationStrategy
import com.intellij.cce.evaluable.StandaloneFeature
import com.intellij.cce.evaluable.StrategySerializer
import com.intellij.cce.filter.EvaluationFilter
import com.intellij.cce.interpreter.FeatureInvoker
import com.intellij.cce.metric.Metric
import com.intellij.cce.metric.PrecisionMetric
import com.intellij.cce.metric.SessionsCountMetric
@@ -24,16 +23,15 @@ class StandaloneExampleFeature : StandaloneFeature<DatasetStrategy>("standalone-
}
override fun getDataset(config: Config): EvaluationDataset {
val fileDataset = config.fileDataset ?: throw IllegalStateException("Required dataset config")
val fileDataset = config.csvDataset ?: throw IllegalStateException("Required dataset config")
return CsvDataset(
datasetRef = DatasetRef.parse(fileDataset.url),
chunkSize = fileDataset.chunkSize,
targetField = "Type"
targetField = "Type",
featureInvoker = StandaloneExampleInvoker(),
)
}
override fun getFeatureInvoker(strategy: DatasetStrategy): FeatureInvoker = StandaloneExampleInvoker()
override fun getMetrics(): List<Metric> = listOf(
SessionsCountMetric(),
PrecisionMetric()
@@ -23,8 +23,7 @@ import kotlin.system.measureTimeMillis
class ActionsInterpretationHandler(
private val config: Config,
private val datasetContext: DatasetContext,
private val featureInvoker: FeatureInvoker,
private val datasetContext: DatasetContext
) {
companion object {
@@ -64,7 +63,7 @@ class ActionsInterpretationHandler(
workspace.fullLineLogsStorage.enableLogging(chunk.name)
try {
val sessions = logsSaver.invokeRememberingLogs {
chunk.evaluate(featureInvoker, handler, filter, interpretationConfig.order) { session ->
chunk.evaluate(handler, filter, interpretationConfig.order) { session ->
featuresStorage.saveSession(session, chunk.name)
}
}
@@ -20,14 +20,14 @@ class BackgroundStepFactory(
override fun generateActionsStep(): EvaluationStep = DatasetPreparationStep(environment.dataset, datasetContext)
override fun interpretActionsStep(): EvaluationStep =
ActionsInterpretationStep(config, environment.dataset, datasetContext, environment.featureInvoker, newWorkspace = false)
ActionsInterpretationStep(config, environment.dataset, datasetContext, newWorkspace = false)
override fun generateReportStep(): EvaluationStep =
ReportGenerationStep(inputWorkspacePaths?.map { EvaluationWorkspace.open(it, SetupStatsCollectorStep.statsCollectorLogsDirectory) },
config.reports.sessionsFilters, config.reports.comparisonFilters, feature)
override fun interpretActionsOnNewWorkspaceStep(): EvaluationStep =
ActionsInterpretationStep(config, environment.dataset, datasetContext, environment.featureInvoker, newWorkspace = true)
ActionsInterpretationStep(config, environment.dataset, datasetContext, newWorkspace = true)
override fun reorderElements(): EvaluationStep =
ReorderElementsStep(config)
@@ -12,7 +12,6 @@ import com.intellij.cce.workspace.EvaluationWorkspace
*/
interface EvaluationEnvironment : AutoCloseable {
val dataset: EvaluationDataset
val featureInvoker: FeatureInvoker
fun execute(step: EvaluationStep, workspace: EvaluationWorkspace): EvaluationWorkspace?
}
@@ -22,7 +21,6 @@ interface EvaluationEnvironment : AutoCloseable {
*/
class StandaloneEnvironment(
override val dataset: EvaluationDataset,
override val featureInvoker: FeatureInvoker
) : EvaluationEnvironment {
override fun execute(step: EvaluationStep, workspace: EvaluationWorkspace): EvaluationWorkspace? =
step.runInIntellij(null, workspace)
@@ -14,7 +14,6 @@ import java.nio.file.FileSystems
class ProjectEnvironment(
val project: Project,
override val dataset: EvaluationDataset,
override val featureInvoker: FeatureInvoker,
) : EvaluationEnvironment {
override fun execute(step: EvaluationStep, workspace: EvaluationWorkspace): EvaluationWorkspace? =
@@ -43,8 +42,7 @@ class ProjectEnvironment(
return ProjectEnvironment(
project,
environment.dataset,
environment.featureInvoker
environment.dataset
)
}
}
@@ -13,7 +13,6 @@ class ActionsInterpretationStep(
private val config: Config,
private val dataset: EvaluationDataset,
private val datasetContext: DatasetContext,
private val featureInvoker: FeatureInvoker,
private val newWorkspace: Boolean
) : BackgroundEvaluationStep {
override val name: String = "Actions interpreting"
@@ -24,7 +23,7 @@ class ActionsInterpretationStep(
val resultWorkspace =
if (newWorkspace) EvaluationWorkspace.create(config, SetupStatsCollectorStep.statsCollectorLogsDirectory)
else workspace
ActionsInterpretationHandler(config, datasetContext, featureInvoker).invoke(dataset, resultWorkspace, progress)
ActionsInterpretationHandler(config, datasetContext).invoke(dataset, resultWorkspace, progress)
return resultWorkspace
}
}
@@ -0,0 +1,104 @@
package com.intellij.cce.evaluable.conflictResolution
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import kotlin.math.absoluteValue
import kotlin.math.sign
class MatchSequenceTest {
@Test
fun `test complement`() {
assertEquals(listOf(0..9), complement(10))
assertEquals(listOf<IntRange>(), complement(10, 0..9))
assertEquals(listOf(5..9), complement(10, 0..4))
assertEquals(listOf(0..4), complement(10, 5..9))
assertEquals(listOf(2..3, 6..7), complement(10, 0..1, 4..5, 8..9))
assertEquals(listOf(0..1, 4..5, 8..9), complement(10, 2..3, 6..7))
}
@Test
fun `test intersectAlignments`() {
assertEquals(listOf<IntRange>(), intersect(listOf(), listOf()))
assertEquals(listOf(0..9), intersect(listOf(0..9), listOf(0..9)))
assertEquals(listOf<IntRange>(), intersect(listOf(0..9), listOf()))
assertEquals(listOf(0..2), intersect(listOf(0..9), listOf(0..2)))
assertEquals(listOf(3..6), intersect(listOf(0..9), listOf(3..6)))
assertEquals(listOf(7..9), intersect(listOf(0..9), listOf(7..9)))
assertEquals(listOf(1..2, 7..8), intersect(listOf(0..2, 7..9), listOf(1..8)))
assertEquals(listOf(1..2, 7..8), intersect(listOf(0..2, 7..9), listOf(1..3, 6..8)))
}
private fun complement(size: Int, vararg ranges: IntRange): List<IntRange> {
val offset = 10
val scale = 2
val sequence = MatchSequence(
mapOf(
TextLabel.BASE to ranges.toList(),
TextLabel.PARENT_1 to ranges.map { (it.first + offset)..(it.last + offset) },
TextLabel.PARENT_2 to ranges.map { (it.first * scale)..(it.last * scale) }
),
mapOf(
TextLabel.BASE to CharacterSplitText("a".repeat(size)),
TextLabel.PARENT_1 to CharacterSplitText("b".repeat(size + offset)),
TextLabel.PARENT_2 to CharacterSplitText("c".repeat(size * scale))
)
)
val complement = sequence.complement()
val baseRanges = complement.ranges(TextLabel.BASE)
assertEquals(baseRanges, complement.ranges(TextLabel.PARENT_1).map { maxOf(0, it.first - offset)..(it.last - offset) })
assertEquals(baseRanges, complement.ranges(TextLabel.PARENT_2).map {
val start = it.first.floorDiv(scale) + it.first.rem(scale).sign.absoluteValue // round up division
start..(if (it.last < it.first) start - 1 else it.last / scale)
})
return baseRanges
.filterNot { it.isEmpty() } // because of offset, there can appear unexpected empty intervals
}
private fun intersect(ranges1: List<IntRange>, ranges2: List<IntRange>): List<IntRange> {
val offset1 = 10
val offset2 = 100
fun size(ranges: List<IntRange>) = ranges.lastOrNull()?.last?.plus(1) ?: 0
val baseSize = maxOf(size(ranges1), size(ranges2))
val sequence1 = MatchSequence(
mapOf(
TextLabel.BASE to ranges1,
TextLabel.PARENT_1 to ranges1.map { (it.first + offset1)..(it.last + offset1) },
),
mapOf(
TextLabel.BASE to CharacterSplitText("a".repeat(baseSize)),
TextLabel.PARENT_1 to CharacterSplitText("b".repeat(baseSize + offset1)),
)
)
val sequence2 = MatchSequence(
mapOf(
TextLabel.BASE to ranges2,
TextLabel.PARENT_2 to ranges2.map { (it.first + offset2)..(it.last + offset2) },
),
mapOf(
TextLabel.BASE to CharacterSplitText("a".repeat(baseSize)),
TextLabel.PARENT_2 to CharacterSplitText("b".repeat(baseSize + offset2)),
)
)
val intersection = sequence1.intersectAlignments(sequence2)
intersection.requireAlignment()
val baseRanges = intersection.ranges(TextLabel.BASE)
assertEquals(baseRanges, intersection.ranges(TextLabel.PARENT_1).map { maxOf(0, it.first - offset1)..(it.last - offset1) })
assertEquals(baseRanges, intersection.ranges(TextLabel.PARENT_2).map { maxOf(0, it.first - offset2)..(it.last - offset2) })
return baseRanges
}
private fun <T> MatchSequence<T>.ranges(label: TextLabel) = (0 until size).map { range(label, it) }
}
@@ -0,0 +1,39 @@
package com.intellij.cce.evaluable.conflictResolution
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class SplitTextTest {
private val matrix1 = listOf(
listOf(0, 1, 2, 3),
listOf(1, 0, 1, 2),
listOf(2, 1, 0, 1),
listOf(3, 2, 1, 0),
)
private val matrix2 = listOf(
listOf(0, 1, 2, 3, 4, 5),
listOf(1, 2, 1, 2, 3, 4),
listOf(2, 3, 2, 3, 4, 5),
listOf(3, 4, 3, 4, 3, 4),
)
@Test
fun `test editDistanceMatrix`() {
assertEquals(matrix1, editDistance("abc", "abc"))
assertEquals(matrix2, editDistance("abc", "_a_c_"))
}
@Test
fun `test retrieveAlignment`() {
assertEquals(Pair(listOf(0..2), listOf(0..2)), alignment(matrix1))
assertEquals(Pair(listOf(0..0, 2..2), listOf(1..1, 3..3)), alignment(matrix2))
}
private fun editDistance(s1: String, s2: String) =
calculateEditDistance(CharacterSplitText(s1), CharacterSplitText(s2)).map { it.toList() }
private fun alignment(matrix: List<List<Int>>) =
retrieveAlignment(matrix.map { it.toIntArray() }.toTypedArray())
}
@@ -91,15 +91,15 @@ class ConfigFactoryTest {
{
"outputDir": "test_outputDir",
"strategy": {},
"fileDataset": {
"csvDataset": {
"url": "test_url",
"chunkSize": 300
}
}
""".trimIndent()
).also {
assertEquals("test_url", it.fileDataset?.url)
assertEquals(300, it.fileDataset?.chunkSize)
assertEquals("test_url", it.csvDataset?.url)
assertEquals(300, it.csvDataset?.chunkSize)
}
}