diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 32617e49e2..52f7d756cc 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -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 diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 2828c7485e..257e19c8a5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -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 @@ -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) } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 567b90d0a8..abd009f0dc 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -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 @@ -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, @@ -196,7 +204,7 @@ 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 @@ -204,7 +212,7 @@ class DisjunctionRxGene( * @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 @@ -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 } /** @@ -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 } /** @@ -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? { + val nestedGroupRequirements = mutableListOf() + 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): 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, 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 = + 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 = + 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): 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, 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 + ) } } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index ebb010daa2..fd7ca0eef7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -248,7 +248,7 @@ class QuantifierRxGene( * @see [AssertionRepairWalk.absorbableCount] */ override fun absorbableCount(value: String): Int = - AssertionRepairWalk.absorbableCount(atoms, value) + AssertionRepairWalk.absorbableCount(atoms, value).consumed /** * Delegates to a backward walk over [atoms], mirroring [absorbableCount] in the @@ -256,7 +256,7 @@ class QuantifierRxGene( * @see [RxAbsorbable.absorbableSuffixCount] */ override fun absorbableSuffixCount(value: String): Int = - AssertionRepairWalk.absorbableSuffixCount(atoms, value) + AssertionRepairWalk.absorbableSuffixCount(atoms, value).consumed /** * True if zero repetitions are allowed ([min] == 0), or if [template] can itself render "". @@ -272,7 +272,7 @@ class QuantifierRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForce(atoms, value) + return AssertionRepairWalk.tryForce(atoms, value).consumed } /** @@ -281,7 +281,7 @@ class QuantifierRxGene( */ override fun tryForceSuffix(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForceSuffix(atoms, value) + return AssertionRepairWalk.tryForceSuffix(atoms, value).consumed } /** diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt new file mode 100644 index 0000000000..279ca34f4c --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -0,0 +1,60 @@ +package org.evomaster.core.search.gene.utils + +import org.evomaster.core.search.gene.regex.DisjunctionListRxGene +import org.evomaster.core.search.gene.regex.DisjunctionRxGene + +/** + * Result of [DisjunctionRxGene.attemptAssertionRepair] / [DisjunctionListRxGene.attemptAssertionRepair]. + * + * Assertion repair for one scope (a [DisjunctionRxGene]'s own direct terms) can be successful, + * fail or be conditionally successful, depending on either an external prefix/postfix (or both). + * + * [neededPrefix] is a requirement on whatever precedes this scope (from a lookbehind-direction); + * [neededPostfix] is a requirement on whatever follows it (lookahead-direction). Each is + * either `null` (no requirement in that direction), `""` (everything further out on that side must + * also collapse to zero width), or a non-empty literal that must be absorbed there. + */ +data class AssertionRepairResult( + val success: Boolean, + val neededPrefix: String? = null, + val neededPostfix: String? = null +) { + companion object { + val SUCCESS = AssertionRepairResult(success = true) + val FAILURE = AssertionRepairResult(success = false) + + /** + * A successful result whose only outcome is [remainder] still being needed by whatever + * lies further out, depending on [backward]. + */ + fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = + if (backward) { + AssertionRepairResult(success = true, neededPrefix = remainder) + } else { + AssertionRepairResult(success = true, neededPostfix = remainder) + } + } + + init { + require(success || (neededPrefix == null && neededPostfix == null)) { + "A failed AssertionRepairResult cannot carry an outward requirement" + } + } + + /** + * Combines this result with [next], the outcome of resolving one more requirement in the + * same scope. Failure on either side wins outright, otherwise [next]'s own + * [neededPrefix]/[neededPostfix] each take priority over this result's, falling back to + * this result's own value when [next] did not set one of them. + */ + fun mergedWith(next: AssertionRepairResult): AssertionRepairResult { + if (!success || !next.success) { + return FAILURE + } + return AssertionRepairResult( + success = true, + neededPrefix = next.neededPrefix ?: neededPrefix, + neededPostfix = next.neededPostfix ?: neededPostfix + ) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 5a1dbb3322..2429e61496 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -3,6 +3,15 @@ package org.evomaster.core.search.gene.utils import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.regex.RxAbsorbable +/** + * Outcome of one greedy walk over a gene list: [consumed] is how much of the candidate string + * can be placed. If less than the full string was consumed, [hardMismatch] says why. `true` means + * a gene genuinely refused to match ([consumed] resets to 0 in that case) and the candidate is + * simply wrong here; `false` means the gene list just ran out of room with no mismatch, + * so the leftover is still worth trying to escape outwards. + */ +data class WalkOutcome(val consumed: Int, val hardMismatch: Boolean) + /** * Greedy-walk utilities for assertion repair. * @@ -16,6 +25,7 @@ object AssertionRepairWalk { * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] * - [reversed]: whether to walk [genes] right-to-left for lookbehind or left-to-right for lookahead + * @see WalkOutcome */ private fun walk( genes: List, @@ -23,9 +33,9 @@ object AssertionRepairWalk { absorb: (RxAbsorbable, String) -> Int, onZeroWidth: (RxAbsorbable) -> Unit, reversed: Boolean - ): Int { + ): WalkOutcome { if (value.isEmpty()) { - return 0 + return WalkOutcome(0, hardMismatch = false) } var consumed = 0 val walkTarget = if (reversed) { @@ -41,7 +51,7 @@ object AssertionRepairWalk { val remaining = if (reversed) { value.dropLast(consumed) } else { - value.substring(consumed) + value.drop(consumed) } val amount = absorb(absorbable, remaining) if (amount == 0) { @@ -49,18 +59,19 @@ object AssertionRepairWalk { onZeroWidth(absorbable) continue } - return 0 + return WalkOutcome(0, hardMismatch = true) } consumed += amount } - return consumed + return WalkOutcome(consumed, hardMismatch = false) } /** * Maximum leading characters of [value] that can be absorbed across [genes] * left-to-right, without mutating anything. + * @see WalkOutcome */ - fun absorbableCount(genes: List, value: String): Int = + fun absorbableCount(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = false, absorb = { gene, value -> gene.absorbableCount(value) }, onZeroWidth = {} @@ -68,9 +79,10 @@ object AssertionRepairWalk { /** * Forces as much of [value] as possible into [genes] left-to-right, mutating each - * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. + * gene in place using each gene's [RxAbsorbable.tryForce]. + * @see WalkOutcome */ - fun tryForce(genes: List, value: String): Int = + fun tryForce(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = false, absorb = { gene, value -> gene.tryForce(value) }, onZeroWidth = { it.forceZeroWidth() } @@ -81,8 +93,9 @@ object AssertionRepairWalk { * trailing characters of [value] that can be absorbed across [genes] right-to-left * (walking [genes] in reverse, since the gene closest to the assertion's position is the * last one in [genes]), without mutating anything. + * @see WalkOutcome */ - fun absorbableSuffixCount(genes: List, value: String): Int = + fun absorbableSuffixCount(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = true, absorb = { gene, value -> gene.absorbableSuffixCount(value) }, onZeroWidth = {} @@ -91,9 +104,10 @@ object AssertionRepairWalk { /** * Suffix-anchored counterpart of [tryForce], used by lookbehind repair: forces as much * of [value] as possible into [genes] right-to-left (mirroring [tryForce]), mutating each - * gene in place using [RxAbsorbable.tryForceSuffix]. Returns total characters placed. + * gene in place using [RxAbsorbable.tryForceSuffix]. + * @see WalkOutcome */ - fun tryForceSuffix(genes: List, value: String): Int = + fun tryForceSuffix(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = true, absorb = { gene, value -> gene.tryForceSuffix(value) }, onZeroWidth = { it.forceZeroWidth() } diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index 19d7ba5216..14ad457648 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -508,4 +508,20 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { assertThrows { checkSameAsJava("(?<=X)a") } assertThrows { checkSameAsJava("a(?<=[a&&b])a") } } + + @Test + fun testNestedAssertionInGroupLocallySatisfied() { + checkSameAsJava("^(a(?=bc)bc)d$") + checkSameAsJava("^(a(?<=a)b)c$") + checkSameAsJava("^((?x)(?=y)y)z$") + checkSameAsJava("^(a(?=ok)(o|k|a|y)*)$") + } + + @Test + fun testNestedAssertionOutwardEscape() { + checkSameAsJava("""^a((?=b\d)b)\d$""") + checkSameAsJava("""^\d(x(?<=\dx))y$""") + assertThrows { checkSameAsJava("""^a((?=b\d)b)y$""") } + assertThrows { checkSameAsJava("^(a(?=bc)d)e$") } + } } \ No newline at end of file