Skip to content
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 @@ -97,10 +97,17 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege
private fun isAssertionNested(ctx: RegexJavaParser.AssertionContext): Boolean {
var current = ctx.parent
while (current != null && current !is RegexJavaParser.PatternContext) {
if (current is RegexJavaParser.AssertionContext
|| (current is RegexJavaParser.AtomContext && current.disjunction() != null)) {
if (current is RegexJavaParser.AssertionContext) {
// assertion within assertion
return true
}
if (current is RegexJavaParser.AtomContext && current.disjunction() != null) {
val enclosingTerm = current.parent as? RegexJavaParser.TermContext
if (enclosingTerm?.quantifier() != null) {
// assertion within quantified group
return true
}
}
current = current.parent
}
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.evomaster.core.output.OutputFormat
import org.evomaster.core.search.gene.root.CompositeFixedGene
import org.evomaster.core.search.gene.Gene
import org.evomaster.core.search.gene.interfaces.PhenotypeDormantGene
import org.evomaster.core.search.gene.utils.AssertionRepairResult
import org.evomaster.core.search.gene.utils.GeneUtils
import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionListRxGeneImpact
import org.evomaster.core.search.service.AdaptiveParameterControl
Expand Down Expand Up @@ -326,11 +327,11 @@ class DisjunctionListRxGene(
}

/**
* Delegates assertion repair to whichever branch is currently active, as only that
* branch's rendered value is ever observed, so only it needs repairing. See
* [DisjunctionRxGene.attemptAssertionRepair] for the actual repair logic.
* Delegates to whichever branch is currently active, as only that branch's rendered
* value is ever observed, so only it needs repairing. See [DisjunctionRxGene.attemptAssertionRepair] for
* the actual repair logic and what its return value means.
*/
fun attemptAssertionRepair(randomness: Randomness) {
disjunctions.getOrNull(activeDisjunction)?.attemptAssertionRepair(randomness)
fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult {
return disjunctions[activeDisjunction].attemptAssertionRepair(randomness)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.evomaster.core.logging.LoggingUtil
import org.evomaster.core.output.OutputFormat
import org.evomaster.core.search.gene.root.CompositeFixedGene
import org.evomaster.core.search.gene.Gene
import org.evomaster.core.search.gene.utils.AssertionRepairResult
import org.evomaster.core.search.gene.utils.AssertionRepairWalk
import org.evomaster.core.search.gene.utils.GeneUtils
import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionRxGeneImpact
Expand All @@ -21,6 +22,13 @@ import org.slf4j.LoggerFactory
*/
const val MAX_LOCAL_ASSERTION_ATTEMPTS = 20

/**
* One nested group's own unresolved requirement, as settled by [DisjunctionRxGene.settleNestedGroups]
* and resolved by [DisjunctionRxGene.resolveNestedGroupRequirements]. [termIndex] is that group's
* own index in [DisjunctionRxGene.terms].
*/
private data class NestedGroupRequirement(val termIndex: Int, val result: AssertionRepairResult)

class DisjunctionRxGene(
name: String,
val terms: List<Gene>,
Expand Down Expand Up @@ -196,15 +204,15 @@ class DisjunctionRxGene(
* @see [AssertionRepairWalk.absorbableCount]
*/
override fun absorbableCount(value: String): Int =
AssertionRepairWalk.absorbableCount(terms, value)
AssertionRepairWalk.absorbableCount(terms, value).consumed

/**
* Delegates to a backward walk over [terms]. Mirrors [absorbableCount], walking
* right-to-left since lookbehind's target sits before the assertion.
* @see [RxAbsorbable.absorbableSuffixCount]
*/
override fun absorbableSuffixCount(value: String): Int =
AssertionRepairWalk.absorbableSuffixCount(terms, value)
AssertionRepairWalk.absorbableSuffixCount(terms, value).consumed

/**
* True only if every term can independently render "", as this disjunction's own value is
Expand All @@ -221,7 +229,7 @@ class DisjunctionRxGene(
*/
override fun tryForce(value: String): Int {
require(value.isNotEmpty())
return AssertionRepairWalk.tryForce(terms, value)
return AssertionRepairWalk.tryForce(terms, value).consumed
}

/**
Expand All @@ -231,7 +239,7 @@ class DisjunctionRxGene(
*/
override fun tryForceSuffix(value: String): Int {
require(value.isNotEmpty())
return AssertionRepairWalk.tryForceSuffix(terms, value)
return AssertionRepairWalk.tryForceSuffix(terms, value).consumed
}

/**
Expand All @@ -248,54 +256,234 @@ class DisjunctionRxGene(
* [AssertionRxGene]s is actually satisfied, by forcing the assertion's sampled inner
* value onto the genes on the appropriate side of it within [terms]:
* - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD]
* - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND].
* - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]
*
* Also, recurses into any direct term that is itself a nested group repairing
* it first, and resolving whatever it couldn't satisfy locally against this scope's own
* neighboring terms. Runs as three passes, in order: [settleNestedGroups],
* [resolveNestedGroupRequirements], [repairDirectAssertions].
*
* Note: forcing here is sequential and uncoordinated, so a later force can overwrite an
* earlier one. Still sound as the top-level pattern check catches any resulting mismatch.
*
* @return whether repair succeeded, with possible outside requirements.
*/
fun attemptAssertionRepair(randomness: Randomness) {
if (terms.none { it is AssertionRxGene }) {
return
fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult {
if (terms.none { it is AssertionRxGene || it is DisjunctionListRxGene }) {
return AssertionRepairResult.SUCCESS
}

val nestedGroupRequirements = settleNestedGroups(randomness)
?: return AssertionRepairResult.FAILURE

val nestedResult = resolveNestedGroupRequirements(nestedGroupRequirements)
if (!nestedResult.success) {
return AssertionRepairResult.FAILURE
}

val directResult = repairDirectAssertions(randomness)
if (!directResult.success) {
return AssertionRepairResult.FAILURE
}

return AssertionRepairResult(
success = true,
neededPrefix = directResult.neededPrefix ?: nestedResult.neededPrefix,
neededPostfix = directResult.neededPostfix ?: nestedResult.neededPostfix
)
}

/**
* Pass 1 of [attemptAssertionRepair]: settles every nested group's own internal repair,
* left to right, before anything in this scope uses it as a forcing target.
*
* @return the outward requirements for each nested group's own term index, in ascending index order
* (left to right, matching [terms] itself). `null` if any nested group's own repair failed outright.
*/
private fun settleNestedGroups(randomness: Randomness): List<NestedGroupRequirement>? {
val nestedGroupRequirements = mutableListOf<NestedGroupRequirement>()
for (idx in terms.indices) {
val term = terms[idx] as? DisjunctionListRxGene ?: continue
val result = term.attemptAssertionRepair(randomness)
if (!result.success) {
return null
}
if (result.neededPrefix != null || result.neededPostfix != null) {
nestedGroupRequirements.add(NestedGroupRequirement(idx, result))
}
}
return nestedGroupRequirements
}

/**
* Pass 2 of [attemptAssertionRepair]: resolves each nested group's own outward requirement
* (as settled by [settleNestedGroups]) against this scope's own neighboring terms.
*
* @return [AssertionRepairResult.FAILURE] if resolving any of them failed outright; otherwise
* a successful result carrying whatever this scope itself must still propagate outward.
*/
private fun resolveNestedGroupRequirements(nestedGroupRequirements: List<NestedGroupRequirement>): AssertionRepairResult {
var pending = AssertionRepairResult.SUCCESS
for ((idx, requirement) in nestedGroupRequirements) {
requirement.neededPrefix?.let { requirement ->
pending = pending.mergedWith(resolveOutwardRequirement(requirement, genesBefore(idx), backward = true))
}
if (!pending.success) {
return AssertionRepairResult.FAILURE
}
requirement.neededPostfix?.let { requirement ->
pending = pending.mergedWith(resolveOutwardRequirement(requirement, genesAfter(idx), backward = false))
}
if (!pending.success) {
return AssertionRepairResult.FAILURE
}
}
return pending
}

/**
* Pass 3 of [attemptAssertionRepair]: repairs every direct-term assertion in [terms].
*
* @return [AssertionRepairResult.FAILURE] if repairing any of them failed outright; otherwise
* a successful result carrying whatever this scope's own assertions still need propagated
* outward.
*/
private fun repairDirectAssertions(randomness: Randomness): AssertionRepairResult {
var pending = AssertionRepairResult.SUCCESS
for (idx in terms.indices) {
val assertion = terms[idx] as? AssertionRxGene ?: continue
val innerGene = assertion.innerGene ?: continue
val backward = assertion.assertionType == AssertionType.LOOKBEHIND
val target = if (backward) genesBefore(idx) else genesAfter(idx)

val target = if (assertion.assertionType == AssertionType.LOOKBEHIND) {
terms.subList(0, idx).filter { it !is AssertionRxGene }
val resolution = if (target.isEmpty()) {
repairAssertionWithNoTarget(assertion, backward, randomness)
} else {
terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene }
repairAssertionAgainstTarget(assertion, target, backward, randomness)
}

// we may not be able to force genes as target is empty but if the lookaround can be zero-width it is fine.
if (target.isEmpty()) {
if (innerGene.canBeZeroWidth) {
innerGene.forceZeroWidth()
continue
}
return
pending = pending.mergedWith(resolution)
if (!pending.success) {
return AssertionRepairResult.FAILURE
}
}
return pending
}

val (countFunction, forceFunction) =
if (assertion.assertionType == AssertionType.LOOKBEHIND) {
AssertionRepairWalk::absorbableSuffixCount to AssertionRepairWalk::tryForceSuffix
} else {
AssertionRepairWalk::absorbableCount to AssertionRepairWalk::tryForce
}
/**
* Handles an assertion with nothing local to force onto: escapes zero-width if the assertion
* itself allows it, otherwise samples once and escapes the whole candidate outward.
*/
private fun repairAssertionWithNoTarget(assertion: AssertionRxGene, backward: Boolean, randomness: Randomness): AssertionRepairResult {
val innerGene = assertion.innerGene!!
if (innerGene.canBeZeroWidth) {
innerGene.forceZeroWidth()
return AssertionRepairResult.SUCCESS
}
assertion.randomize(randomness, false)
val candidate = assertion.sampledInnerValue()!!
return AssertionRepairResult.stillNeeded(candidate, backward)
}

var satisfied = false
for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) {
assertion.randomize(randomness, false)
val candidate = assertion.sampledInnerValue() ?: break
if (candidate.isEmpty() || countFunction(target, candidate) == candidate.length) {
if (candidate.isNotEmpty()) {
forceFunction(target, candidate)
}
satisfied = true
break
}
/**
* Resamples [assertion] up to [MAX_LOCAL_ASSERTION_ATTEMPTS] times looking for a candidate
* [target] fully absorbs; if none does, escapes the last candidate tried outwards. This mirrors
* the same outcome [resolveOutwardRequirement] produces.
*/
private fun repairAssertionAgainstTarget(assertion: AssertionRxGene, target: List<Gene>, backward: Boolean, randomness: Randomness): AssertionRepairResult {
val countFunction = countWalkFunction(backward)
val forceFunction = forceWalkFunction(backward)

var lastCandidate: String? = null
for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) {
assertion.randomize(randomness, false)
val candidate = assertion.sampledInnerValue()!!
if (candidate.isEmpty()) {
return AssertionRepairResult.SUCCESS
}
if (!satisfied) {
return
if (countFunction(target, candidate).consumed == candidate.length) {
forceFunction(target, candidate)
return AssertionRepairResult.SUCCESS
}
// Read-only for now, try to force full match before escaping partial match.
lastCandidate = candidate
}

val candidate = lastCandidate ?: return AssertionRepairResult.FAILURE
return resolveOutwardRequirement(candidate, target, backward)
}

/**
* The genes in [terms] lying before index [idx], excluding other assertions. This is the forcing
* target for a [AssertionType.LOOKBEHIND] assertion (or an outward requirement) sitting at [idx].
*/
private fun genesBefore(idx: Int): List<Gene> =
terms.subList(0, idx).filter { it !is AssertionRxGene }

/**
* The genes in [terms] lying after index [idx], excluding other assertions. This is the forcing
* target for a [AssertionType.LOOKAHEAD] assertion (or an outward requirement) sitting at [idx].
*/
private fun genesAfter(idx: Int): List<Gene> =
terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene }

/**
* The read-only walk function for [backward].
*/
private fun countWalkFunction(backward: Boolean) = if (backward) {
AssertionRepairWalk::absorbableSuffixCount
} else {
AssertionRepairWalk::absorbableCount
}

/**
* The mutating counterpart of [countWalkFunction], for the same [backward] direction.
*/
private fun forceWalkFunction(backward: Boolean) = if (backward) {
AssertionRepairWalk::tryForceSuffix
} else {
AssertionRepairWalk::tryForce
}

/**
* Resolves a "" (empty) requirement: every gene in [target] must collapse to zero width.
*/
private fun resolveEmptyRequirement(target: List<Gene>): AssertionRepairResult {
if (target.any { !(it as RxAbsorbable).canBeZeroWidth }) {
return AssertionRepairResult.FAILURE
}
target.forEach { (it as RxAbsorbable).forceZeroWidth() }
return AssertionRepairResult.SUCCESS
}

/**
* Resolves an outward [requirement] against [target], a list of this scope's own genes lying to one
* side of wherever the requirement originated. [backward] selects which direction [target] is walked.
*/
private fun resolveOutwardRequirement(requirement: String, target: List<Gene>, backward: Boolean): AssertionRepairResult {
if (requirement.isEmpty()) {
return resolveEmptyRequirement(target)
}
if (target.isEmpty()) {
return AssertionRepairResult.stillNeeded(requirement, backward)
}

val countFunction = countWalkFunction(backward)
val outcome = countFunction(target, requirement)
if (outcome.hardMismatch) {
return AssertionRepairResult.FAILURE
}

val forceFunction = forceWalkFunction(backward)
forceFunction(target, requirement)

val consumed = outcome.consumed
return if (consumed == requirement.length) {
AssertionRepairResult.SUCCESS
} else {
val remainder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed)
AssertionRepairResult.stillNeeded(
remainder,
backward
)
}
}
}
}
Loading