Skip to content

Commit

Permalink
fix bugs in automaton coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
softwaresale committed Jul 29, 2024
1 parent 93464c1 commit f7d4b2f
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 72 deletions.
110 changes: 53 additions & 57 deletions src/main/java/dk/brics/automaton/AutomatonCoverage.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,38 @@ public class AutomatonCoverage {
public static final int FAILURE_STATE_ID = -1;

public static final class Edge {

public static Edge failEdge(int originState) {
return new Edge(originState, FAILURE_STATE_ID, (char) 0, (char) 0);
}

private final int leftStateId;
private final int rightStateId;
private final char edgeMin;
private final char edgeMax;

public Edge(int leftStateId, int rightStateId) {
public Edge(int leftStateId, int rightStateId, Transition transition) {
this(leftStateId, rightStateId, transition.getMin(), transition.getMax());
}

public Edge(int leftStateId, int rightStateId, char edgeMin, char edgeMax) {
this.leftStateId = leftStateId;
this.rightStateId = rightStateId;
this.edgeMin = edgeMin;
this.edgeMax = edgeMax;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Edge edge = (Edge) o;
return leftStateId == edge.leftStateId && rightStateId == edge.rightStateId;
return leftStateId == edge.leftStateId && rightStateId == edge.rightStateId && edgeMin == edge.edgeMin && edgeMax == edge.edgeMax;
}

@Override
public int hashCode() {
return Objects.hash(leftStateId, rightStateId);
return Objects.hash(leftStateId, rightStateId, edgeMin, edgeMax);
}

public int getLeftStateId() {
Expand All @@ -38,39 +51,25 @@ public int getRightStateId() {
}

public static final class EdgePair {
private final int left;
private final int middle;
private final int right;

public EdgePair(int left, int middle, int right) {
this.left = left;
this.middle = middle;
this.right = right;
}
private final Edge leftEdge;
private final Edge rightEdge;

public int getLeft() {
return left;
}

public int getMiddle() {
return middle;
}

public int getRight() {
return right;
public EdgePair(Edge leftEdge, Edge rightEdge) {
this.leftEdge = leftEdge;
this.rightEdge = rightEdge;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EdgePair edgePair = (EdgePair) o;
return left == edgePair.left && middle == edgePair.middle && right == edgePair.right;
return Objects.equals(leftEdge, edgePair.leftEdge) && Objects.equals(rightEdge, edgePair.rightEdge);
}

@Override
public int hashCode() {
return Objects.hash(left, middle, right);
return Objects.hash(leftEdge, rightEdge);
}
}

Expand Down Expand Up @@ -134,28 +133,6 @@ public VisitationInfo combineWith(VisitationInfo other) {
);
}

/**
* Visit everything at once.
* @param previousState The state we were at
* @param currentState The state we are current on
* @param nextState The state we are about to go to
*/
public void visitConfiguration(Integer previousState, int currentState, Integer nextState) {
// visit the center state
addVisitedNode(currentState);

// add an edge if needed
if (previousState == null) {
return;
}

addVisitedEdge(new Edge(previousState, currentState));

if (nextState != null) {
addVisitedEdgePair(new EdgePair(previousState, currentState, nextState));
}
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down Expand Up @@ -254,8 +231,7 @@ public VisitationInfo getPartialMatchVisitationInfo() {

private VisitationInfo evaluateString(String input, boolean fullMatch) {
VisitationInfo visitationInfo = new VisitationInfo();

OptionalInt previousState = OptionalInt.empty(); // used for edge pair
Optional<Edge> previousEdge = Optional.empty();
int stateCursor = this.runAutomaton.getInitialState();
visitationInfo.addVisitedNode(stateCursor); // first state is always visited

Expand All @@ -265,12 +241,15 @@ private VisitationInfo evaluateString(String input, boolean fullMatch) {
int nextState = this.runAutomaton.step(stateCursor, transitionCharacter);
if (nextState == -1) {

// add visited node
visitationInfo.addVisitedNode(FAILURE_STATE_ID);

// we have encountered a failure state/edge
visitationInfo.addVisitedEdge(new Edge(stateCursor, FAILURE_STATE_ID));
Edge failEdge = Edge.failEdge(stateCursor);
visitationInfo.addVisitedEdge(failEdge);

// add an edge pair as well
int finalStateCursor2 = stateCursor;
previousState.ifPresent(prevState -> visitationInfo.addVisitedEdgePair(new EdgePair(prevState, finalStateCursor2, FAILURE_STATE_ID)));
previousEdge.ifPresent(prevEdge -> visitationInfo.addVisitedEdgePair(new EdgePair(prevEdge, failEdge)));

// there's no outgoing state, then there are two things we can try:
if (fullMatch) {
Expand All @@ -279,22 +258,22 @@ private VisitationInfo evaluateString(String input, boolean fullMatch) {
} else {
// otherwise, we should restart the automaton
stateCursor = runAutomaton.getInitialState();
previousState = OptionalInt.empty();
previousEdge = Optional.empty();
}
} else {
Transition joiningTransition = findTransitionForState(stateCursor, nextState, transitionCharacter).orElseThrow();
// We moved to another state, so that state should be marked as visited
visitationInfo.addVisitedNode(nextState);

// construct an edge with our current state info
int finalStateCursor1 = stateCursor;
previousState.ifPresent(prevState -> visitationInfo.addVisitedEdge(new Edge(prevState, finalStateCursor1)));
Edge takenEdge = new Edge(stateCursor, nextState, joiningTransition);
visitationInfo.addVisitedEdge(takenEdge);

// record edge pair if possible
int finalStateCursor = stateCursor;
previousState.ifPresent(prevState -> visitationInfo.addVisitedEdgePair(new EdgePair(prevState, finalStateCursor, nextState)));
previousEdge.ifPresent(prevEdge -> visitationInfo.addVisitedEdgePair(new EdgePair(prevEdge, takenEdge)));

// move states along
previousState = OptionalInt.of(stateCursor);
previousEdge = Optional.of(takenEdge);
stateCursor = nextState;
}
// Update the cursor
Expand All @@ -321,4 +300,21 @@ private int computeNumberOfEdges() {
private int computeNumberOfEdgePairs() {
return transitionTable.countPossibleEdgePairs() + originalAutomaton.getNumberOfTransitions();
}

/**
* Find a transition between the two given states that accepts the given transition character
* @param startState left state
* @param endState destination state
* @param transitionChar The character
* @return Transition between the two with the given character, or empty if doesn't exist
*/
private Optional<Transition> findTransitionForState(int startState, int endState, char transitionChar) {
try {
return transitionTable.getTransitionsBetweenStates(startState, endState).stream()
.filter(transition -> transition.getMin() <= transitionChar && transitionChar <= transition.getMax())
.findFirst();
} catch (NoSuchElementException notFound) {
return Optional.empty();
}
}
}
20 changes: 14 additions & 6 deletions src/main/java/dk/brics/automaton/TransitionTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,20 @@ public Optional<Transition> findEdgeBetweenStates(int leftStateNumber, int right
public int countPossibleEdgePairs() {
Set<AutomatonCoverage.EdgePair> edgePairs = new HashSet<>();
for (int leftState : table.keySet()) {
Set<Integer> possibleMiddleStates = getSuccessors(leftState);
for (int middleState : possibleMiddleStates) {
Set<Integer> rightStates = getSuccessors(middleState);
for (int rightState : rightStates) {
edgePairs.add(new AutomatonCoverage.EdgePair(leftState, middleState, rightState));
}
Set<AutomatonCoverage.Edge> leftEdges = getSuccessors(leftState).stream()
.flatMap(leftSuccessor -> {
return getTransitionsBetweenStates(leftState, leftSuccessor).stream()
.map(transition -> new AutomatonCoverage.Edge(leftState, leftSuccessor, transition));
})
.collect(Collectors.toSet());

for (AutomatonCoverage.Edge leftEdge : leftEdges) {
getSuccessors(leftEdge.getRightStateId()).stream()
.flatMap(middleSuccessor -> {
return getTransitionsBetweenStates(leftEdge.getRightStateId(), middleSuccessor).stream()
.map(transition -> new AutomatonCoverage.Edge(leftEdge.getRightStateId(), middleSuccessor, transition));
})
.forEach(destEdge -> edgePairs.add(new AutomatonCoverage.EdgePair(leftEdge, destEdge)));
}
}

Expand Down
127 changes: 118 additions & 9 deletions src/test/java/dk/brics/automaton/AutomatonCoverageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.Collection;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -17,12 +18,17 @@ void pattern1_coverage_shouldHaveFullCoverage() throws DfaTooLargeException {
coverage.evaluate("a");
coverage.evaluate("b");
coverage.evaluate("c");
AutomatonCoverage.VisitationInfo info = coverage.getFullMatchVisitationInfo();

Set<Integer> liveStates = statesToStateNums(auto.getLiveStates());

assertThat(info.getVisitedNodes()).containsExactlyElementsOf(liveStates);
assertThat(info.getVisitedEdges().size()).isEqualTo(auto.getNumberOfTransitions());
coverage.evaluate("aa");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrder(-1, 0, 1);
},
summary -> {
assertThat(summary.getNodeCoverage()).isEqualTo(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(1.0);
}
);
}

@Test
Expand All @@ -32,10 +38,30 @@ void pattern2_coverage_shouldHaveFullCoverage() throws DfaTooLargeException {
AutomatonCoverage coverage = new AutomatonCoverage(auto);

coverage.evaluate("abd");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrderElementsOf(statesToStateNums(auto.getLiveStates()));
assertThat(info.getVisitedEdges().size()).isEqualTo(auto.getNumberOfTransitions());
},
summary -> {
assertThat(summary.getNodeCoverage()).isLessThan(1.0);
assertThat(summary.getEdgeCoverage()).isLessThan(1.0);
}
);

AutomatonCoverage.VisitationInfo info = coverage.getFullMatchVisitationInfo();
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrderElementsOf(statesToStateNums(auto.getLiveStates()));
assertThat(info.getVisitedEdges().size()).isEqualTo(auto.getNumberOfTransitions());
coverage.evaluate("b");
coverage.evaluate("ae");
coverage.evaluate("abe");
coverage.evaluate("abde");
assertFullMatchCoverage(
coverage,
info -> {},
summary -> {
assertThat(summary.getNodeCoverage()).isEqualTo(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(1.0);
}
);
}

@Test
Expand Down Expand Up @@ -83,6 +109,73 @@ void pattern3_coverage_hasCompleteCoverage() throws DfaTooLargeException {
assertThat(info.getVisitedEdges().size()).isEqualTo(auto.getNumberOfTransitions());
}

@Test
void pattern4_coverage_hasCompleteCoverage() throws DfaTooLargeException {
Automaton auto = prepareRegex("[a-z0-9]*A");
System.out.println(auto.toDot());
AutomatonCoverage coverage = new AutomatonCoverage(auto);

coverage.evaluate("A");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactly(0, 1);
},
summary -> {
assertThat(summary.getNodeCoverage()).isLessThan(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(1.0 / 5.0);
}
);

coverage.evaluate("*");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrder(0, 1, -1);
},
summary -> {
assertThat(summary.getNodeCoverage()).isEqualTo(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(2.0 / 5.0);
}
);

coverage.evaluate("Ab");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrder(0, 1, -1);
},
summary -> {
assertThat(summary.getNodeCoverage()).isEqualTo(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(3.0 / 5.0);
}
);

coverage.evaluate("aA");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrder(0, 1, -1);
},
summary -> {
assertThat(summary.getNodeCoverage()).isEqualTo(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(4.0 / 5.0);
}
);

coverage.evaluate("0A");
assertFullMatchCoverage(
coverage,
info -> {
assertThat(info.getVisitedNodes()).containsExactlyInAnyOrder(0, 1, -1);
},
summary -> {
assertThat(summary.getNodeCoverage()).isEqualTo(1.0);
assertThat(summary.getEdgeCoverage()).isEqualTo(1.0);
}
);
}

private static Automaton prepareRegex(String pattern) {
RegExp regex = new RegExp(pattern, RegExp.NONE);
Automaton auto = regex.toAutomaton();
Expand All @@ -94,4 +187,20 @@ private static Automaton prepareRegex(String pattern) {
private static Set<Integer> statesToStateNums(Collection<State> states) {
return states.stream().map(state -> state.number).collect(Collectors.toSet());
}

private static void assertFullMatchCoverage(AutomatonCoverage coverage,
Consumer<AutomatonCoverage.VisitationInfo> onVisitationInfo,
Consumer<AutomatonCoverage.VisitationInfoSummary> onVisitationInfoSummary) {

onVisitationInfo.accept(coverage.getFullMatchVisitationInfo());
onVisitationInfoSummary.accept(coverage.getFullMatchVisitationInfoSummary());
}

private static void assertPartialMatchCoverage(AutomatonCoverage coverage,
Consumer<AutomatonCoverage.VisitationInfo> onVisitationInfo,
Consumer<AutomatonCoverage.VisitationInfoSummary> onVisitationInfoSummary) {

onVisitationInfo.accept(coverage.getPartialMatchVisitationInfo());
onVisitationInfoSummary.accept(coverage.getPartialMatchVisitationInfoSummary());
}
}

0 comments on commit f7d4b2f

Please sign in to comment.