Skip to content

Commit

Permalink
Fixup coverage computations
Browse files Browse the repository at this point in the history
- fix a number of coverage bugs
- added test suite stuff to double check
  • Loading branch information
softwaresale committed Jul 29, 2024
1 parent f7d4b2f commit 247dadc
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 32 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
.settings*
.classpath
.project
.idea/
.gradle/
65 changes: 58 additions & 7 deletions src/main/java/dk/brics/automaton/AutomatonCoverage.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package dk.brics.automaton;

import java.util.*;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class AutomatonCoverage {

Expand All @@ -9,23 +13,25 @@ public class AutomatonCoverage {
public static final class Edge {

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

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

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

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

@Override
Expand All @@ -41,6 +47,17 @@ public int hashCode() {
return Objects.hash(leftStateId, rightStateId, edgeMin, edgeMax);
}

@Override
public String toString() {
return "Edge{" +
"leftStateId=" + leftStateId +
", rightStateId=" + rightStateId +
", transId=" + transId +
", edgeMin=" + edgeMin +
", edgeMax=" + edgeMax +
'}';
}

public int getLeftStateId() {
return leftStateId;
}
Expand Down Expand Up @@ -71,6 +88,14 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(leftEdge, rightEdge);
}

@Override
public String toString() {
return "EdgePair{" +
"leftEdge=" + leftEdge +
", rightEdge=" + rightEdge +
'}';
}
}

public static final class VisitationInfo {
Expand Down Expand Up @@ -200,6 +225,15 @@ public AutomatonCoverage(Automaton automaton) {
partialMatchVisitationInfo = new VisitationInfo();
}

protected AutomatonCoverage(Automaton automaton, TransitionTable transitionTable) {
this.originalAutomaton = automaton;
this.runAutomaton = new RunAutomaton(automaton);
this.transitionTable = transitionTable;

fullMatchVisitationInfo = new VisitationInfo();
partialMatchVisitationInfo = new VisitationInfo();
}

public void evaluate(String subject) {
fullMatchVisitationInfo.foldIn(evaluateString(subject, true));
partialMatchVisitationInfo.foldIn(evaluateString(subject, false));
Expand Down Expand Up @@ -253,6 +287,14 @@ private VisitationInfo evaluateString(String input, boolean fullMatch) {

// there's no outgoing state, then there are two things we can try:
if (fullMatch) {

// if there is any remaining input, then we should add an edge for the covered self loop
if (currentPos + 1 < input.length()) {
Edge failSelfLoop = Edge.failEdge(FAILURE_STATE_ID);
visitationInfo.addVisitedEdge(failSelfLoop);
visitationInfo.addVisitedEdgePair(new EdgePair(failEdge, failSelfLoop));
}

// if we're in full match mode, then we're done
break;
} else {
Expand Down Expand Up @@ -285,20 +327,29 @@ private VisitationInfo evaluateString(String input, boolean fullMatch) {

private int computeNumberOfStates() {
// add one for the error state
return originalAutomaton.getLiveStates().size() + 1;
return originalAutomaton.getLiveStates().size() // all states already in the automaton
+ 1; // plus the error state
}

private int computeNumberOfEdges() {
// all edges + an edge from each state to the failure state
return originalAutomaton.getNumberOfTransitions() + originalAutomaton.getNumberOfStates();
return originalAutomaton.getNumberOfTransitions() // all edges from normal node to normal node
+ originalAutomaton.getNumberOfStates() // + an edge from each node to the fail state
+ 1; // plus an edge from the fail state to the fail state
}

/**
* TODO figure out how to compute this without using the transition table
* @return Number of possible edge pairs
*/
private int computeNumberOfEdgePairs() {
return transitionTable.countPossibleEdgePairs() + originalAutomaton.getNumberOfTransitions();
return transitionTable.possibleEdgePairs().size();
}

Set<EdgePair> missingFullMatchEdgePairs() {
Set<EdgePair> possibleEdges = transitionTable.possibleEdgePairs();
possibleEdges.removeAll(fullMatchVisitationInfo.getVisitedEdgePairs());
return possibleEdges;
}

/**
Expand Down
13 changes: 5 additions & 8 deletions src/main/java/dk/brics/automaton/RunAutomaton.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
Expand All @@ -50,7 +51,7 @@ public class RunAutomaton implements Serializable {

int size;
boolean[] accept;
int initial;
final int initial;
int[] transitions; // delta(state,c) = transitions[state*points.length + getCharClass(c)]
char[] points; // char interval start points
int[] classmap; // map from char number to class class
Expand Down Expand Up @@ -139,9 +140,6 @@ int getCharClass(char c) {
return SpecialOperations.findIndex(c, points);
}

@SuppressWarnings("unused")
private RunAutomaton() {}

/**
* Constructs a new <code>RunAutomaton</code> from a deterministic
* <code>Automaton</code>. Same as <code>RunAutomaton(a, true)</code>.
Expand Down Expand Up @@ -202,9 +200,8 @@ public RunAutomaton(Automaton a, boolean tableize) {
size = states.size();
accept = new boolean[size];
transitions = new int[size * points.length];
for (int n = 0; n < size * points.length; n++)
transitions[n] = -1;
for (State s : states) {
Arrays.fill(transitions, -1);
for (State s : states.stream().sorted(Comparator.comparingInt(state -> state.number)).collect(Collectors.toList())) {
int n = s.number;
accept[n] = s.accept;
for (int c = 0; c < points.length; c++) {
Expand Down
92 changes: 80 additions & 12 deletions src/main/java/dk/brics/automaton/TransitionTable.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dk.brics.automaton;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TransitionTable {

Expand All @@ -22,40 +24,45 @@ private static Map<Integer, Set<Transition>> createDestTableFromState(State stat
return destinationMap;
}

private static Set<State> getAdjacentStates(State state) {
private static List<State> getAdjacentStates(State state) {
Set<State> neighbors = new HashSet<>();
for (Transition dest : state.transitions) {
neighbors.add(dest.to);
}

return neighbors;
return neighbors.stream().sorted().collect(Collectors.toList());
}

// Sparse matrix transition table. For each map, the key is the destination state, and the value is a set of
// transitions that can be used to transition between the two
private final Map<Integer, Map<Integer, Set<Transition>>> table;
private final Set<Integer> acceptStates;

public TransitionTable(Automaton auto) {

// Initialize table
table = new HashMap<>();
this.table = new HashMap<>();
this.acceptStates = new HashSet<>();

// Populate table
Set<Integer> visitedStates = new HashSet<>();
Queue<State> traversalQueue = new ArrayDeque<>();
traversalQueue.add(auto.getInitialState());
while (!traversalQueue.isEmpty()) {
State state = traversalQueue.remove();
if (state.isAccept()) {
acceptStates.add(state.number);
}
visitedStates.add(state.id);
// Create the entry for this state
Map<Integer, Set<Transition>> destinationMap = createDestTableFromState(state);
table.put(state.number, destinationMap);
// Get the next states to check. Only enqueue states that we haven't visited yet
getAdjacentStates(state).stream()
.filter(neighbor -> !visitedStates.contains(neighbor.id))
.forEach(item -> {
if (!traversalQueue.contains(item)) {
traversalQueue.add(item);
.forEach(neighbor -> {
if (!traversalQueue.contains(neighbor)) {
traversalQueue.add(neighbor);
}
});
}
Expand Down Expand Up @@ -112,27 +119,88 @@ public Optional<Transition> findEdgeBetweenStates(int leftStateNumber, int right
.findAny();
}

public int countPossibleEdgePairs() {
public Set<AutomatonCoverage.EdgePair> possibleEdgePairs() {
Set<AutomatonCoverage.EdgePair> edgePairs = new HashSet<>();
for (int leftState : table.keySet()) {
Set<AutomatonCoverage.Edge> leftEdges = getSuccessors(leftState).stream()
Stream<Integer> leftSuccessorStates = Stream.concat(
Stream.of(-1),
getSuccessors(leftState).stream()
);
Set<AutomatonCoverage.Edge> leftEdges = leftSuccessorStates
.flatMap(leftSuccessor -> {
if (leftSuccessor == -1) {
return Stream.of(AutomatonCoverage.Edge.failEdge(leftState));
}

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()
int middleState = leftEdge.getRightStateId();
Stream<Integer> middleSuccessorStates = Stream.concat(
Stream.of(-1),
getSuccessors(middleState).stream()
);

middleSuccessorStates
.flatMap(middleSuccessor -> {
return getTransitionsBetweenStates(leftEdge.getRightStateId(), middleSuccessor).stream()
.map(transition -> new AutomatonCoverage.Edge(leftEdge.getRightStateId(), middleSuccessor, transition));
if (middleSuccessor == -1) {
return Stream.of(AutomatonCoverage.Edge.failEdge(middleState));
}

return getTransitionsBetweenStates(middleState, middleSuccessor).stream()
.map(transition -> new AutomatonCoverage.Edge(middleState, middleSuccessor, transition));
})
.forEach(destEdge -> edgePairs.add(new AutomatonCoverage.EdgePair(leftEdge, destEdge)));
}
}

return edgePairs.size();
return edgePairs;
}

public String toDot() {

Function<Character, String> printableCharacter = (ch) -> {
if (Character.isAlphabetic(ch) || Character.isDigit(ch)) {
return String.valueOf(ch);
} else {
return String.format("u%d", Character.getNumericValue(ch));
}
};

StringBuilder builder = new StringBuilder();
builder.append("digraph automaton {\n");
builder.append("\trankdir = LR;\n");
for (int state : this.table.keySet()) {
String shape = acceptStates.contains(state) ? "doublecircle" : "circle";
builder.append(String.format("\t%d [shape=%s, label=%d]\n", state, shape, state));
}

for (Map.Entry<Integer, Map<Integer, Set<Transition>>> entry : table.entrySet()) {
int originState = entry.getKey();
Map<Integer, Set<Transition>> destMap = entry.getValue();
for (Map.Entry<Integer, Set<Transition>> destEntry : destMap.entrySet()) {
int dest = destEntry.getKey();
Set<Transition> transitions = destEntry.getValue();

for (Transition trans : transitions) {
String label;
if (trans.getMin() == trans.getMax()) {
label = printableCharacter.apply(trans.getMin());
} else {
String lowerPrintableBound = printableCharacter.apply(trans.getMin());
String upperPrintableBound = printableCharacter.apply(trans.getMax());
label = String.format("[%s,%s]", lowerPrintableBound, upperPrintableBound);
}
builder.append(String.format("\t%d -> %d [label=\"%s,%d\"]\n", originState, dest, label, trans.getId()));
}
}
}

builder.append("}\n");
return builder.toString();
}

private Set<Integer> getSuccessors(int state) {
Expand Down
Loading

0 comments on commit 247dadc

Please sign in to comment.