diff --git a/README.md b/README.md index 4a8093e..44c6c8a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ import liquidjava.fsm.StateMachine; import liquidjava.fsm.StateMachineParser; StateMachine stateMachine = StateMachineParser.parse(path.toUri().toString()); +// in a file with several classes, or for a verifier error's StateErrorContext#receiverClass +StateMachine named = StateMachineParser.parse(path.toUri().toString(), "example.File"); ``` ### JSON Output diff --git a/pom.xml b/pom.xml index 5fd5be2..1c3befd 100644 --- a/pom.xml +++ b/pom.xml @@ -44,12 +44,12 @@ io.github.liquid-java liquidjava-verifier - 0.0.35 + 0.0.36 fr.inria.gforge.spoon spoon-core - 10.4.2 + 11.5.0 org.junit.jupiter diff --git a/src/main/java/liquidjava/fsm/StateMachine.java b/src/main/java/liquidjava/fsm/StateMachine.java index e2a7f60..055344e 100644 --- a/src/main/java/liquidjava/fsm/StateMachine.java +++ b/src/main/java/liquidjava/fsm/StateMachine.java @@ -9,6 +9,5 @@ public record StateMachine( String className, List states, List transitions, - List initialTransitions, - StateMachineErrorContext errorContext + List initialTransitions ) {} diff --git a/src/main/java/liquidjava/fsm/StateMachineErrorContext.java b/src/main/java/liquidjava/fsm/StateMachineErrorContext.java deleted file mode 100644 index 6a76209..0000000 --- a/src/main/java/liquidjava/fsm/StateMachineErrorContext.java +++ /dev/null @@ -1,25 +0,0 @@ -package liquidjava.fsm; - -import java.util.List; - -/** - * Describes the verification error overlaid on a state machine diagram. - * @param calledMethod the method, constructor or static factory declared at the error's declaration position - * @param actualStates the states mentioned by the found condition for the receiver - * @param receiver the receiver of the failing call (e.g. root) - * @param expectedStates the states accepted by the expected condition, in state set order - * @param expectedCondition the expected condition as text - * @param foundCondition the found condition for the receiver as text - * @param definiteFoundState the single state the found condition establishes, or null if there is none - * @param possibleStates the states the found condition allows for the receiver, in state set order - */ -public record StateMachineErrorContext( - String calledMethod, - List actualStates, - String receiver, - List expectedStates, - String expectedCondition, - String foundCondition, - String definiteFoundState, - List possibleStates -) {} diff --git a/src/main/java/liquidjava/fsm/StateMachineParser.java b/src/main/java/liquidjava/fsm/StateMachineParser.java index 3b21f35..a9a77ad 100644 --- a/src/main/java/liquidjava/fsm/StateMachineParser.java +++ b/src/main/java/liquidjava/fsm/StateMachineParser.java @@ -4,25 +4,18 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.List; import liquidjava.rj_language.ast.*; import liquidjava.rj_language.parsing.RefinementsParser; -import liquidjava.processor.VCImplication; -import liquidjava.rj_language.ast.formatter.ExpressionFormatter; -import liquidjava.rj_language.ast.formatter.VariableFormatter; import liquidjava.utils.Utils; import spoon.Launcher; import spoon.reflect.CtModel; import spoon.reflect.declaration.CtAnnotation; import spoon.reflect.declaration.CtClass; -import spoon.reflect.declaration.CtConstructor; -import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtType; -import spoon.reflect.cu.SourcePosition; public class StateMachineParser { @@ -39,16 +32,16 @@ private record TransitionSource(String from, String cond) {} * @return StateMachine or null if none found */ public static StateMachine parse(String uri) { - return parse(uri, null, null, null); + return parse(uri, null); } - public static StateMachine parseWithErrorContext(String uri, SourcePosition declarationPosition, - Expression expectedState, VCImplication foundState) { - return parse(uri, declarationPosition, expectedState, foundState); - } - - private static StateMachine parse(String uri, SourcePosition declarationPosition, - Expression expectedState, VCImplication foundState) { + /** + * Parses the class or interface with the given name in the given uri, for files that declare several + * @param uri the document URI + * @param className the qualified class name, or the name given by ExternalRefinementsFor + * @return StateMachine or null if none found + */ + public static StateMachine parse(String uri, String className) { try { String filePath = Paths.get(new URI(uri)).toString(); Launcher launcher = new Launcher(); @@ -59,7 +52,7 @@ private static StateMachine parse(String uri, SourcePosition declarationPosition CtModel model = launcher.getModel(); // get class or interface - CtType ctType = getType(model); + CtType ctType = getType(model, className); if (ctType == null) return null; // no class or interface found @@ -67,8 +60,8 @@ private static StateMachine parse(String uri, SourcePosition declarationPosition List states = getStates(ctType); if (states == null || states.isEmpty()) return null; // no states found - String className = getClassName(ctType); - String simpleClassName = Utils.getSimpleName(className); + String qualifiedName = getClassName(ctType); + String simpleClassName = Utils.getSimpleName(qualifiedName); // get initial transitions and method transitions List factoryTransitions = getFactoryTransitions(ctType, simpleClassName, states); @@ -83,10 +76,7 @@ private static StateMachine parse(String uri, SourcePosition declarationPosition if (transitions.isEmpty()) return null; // no transitions found - StateMachineErrorContext errorContext = declarationPosition == null && expectedState == null - && foundState == null ? null : getErrorContext(ctType, simpleClassName, states, - declarationPosition, expectedState, foundState); - return new StateMachine(className, states, transitions, initialTransitions, errorContext); + return new StateMachine(qualifiedName, states, transitions, initialTransitions); } catch (Exception e) { e.printStackTrace(); @@ -94,194 +84,15 @@ private static StateMachine parse(String uri, SourcePosition declarationPosition } } - private static StateMachineErrorContext getErrorContext(CtType ctType, String className, List states, - SourcePosition declarationPosition, Expression expectedState, VCImplication foundState) { - String receiver = getStateReceiver(expectedState, states); - String receiverName = receiver == null ? null : VariableFormatter.withoutInstance(receiver); - List foundPremises = getReceiverPremises(foundState, receiver); - return new StateMachineErrorContext( - getCalledMethod(ctType, className, declarationPosition), - getActualStates(expectedState, foundState, states), - receiverName, - getExpectedStates(expectedState, states, receiver), - expectedState == null ? null : formatCondition(expectedState, receiver, receiverName), - getFoundCondition(foundState, foundPremises, receiver, receiverName), - getDefiniteState(foundPremises, states, receiver), - getPossibleStates(foundPremises, states, receiver)); - } - - /** - * Gets the name of the method, constructor or static factory whose declaration contains the given position - * The verifier reports the position of the state refinement annotation value, so the position is matched by range - * Constructors are named after the class - */ - private static String getCalledMethod(CtType ctType, String className, SourcePosition declarationPosition) { - if (declarationPosition == null || !declarationPosition.isValidPosition()) return null; - List> executables = new ArrayList<>(ctType.getMethods()); - if (ctType instanceof CtClass ctClass) { - executables.addAll(ctClass.getConstructors()); - } - return executables.stream() - .filter(executable -> contains(executable.getPosition(), declarationPosition)) - .min(Comparator.comparingInt(executable -> executable.getPosition().getSourceEnd() - - executable.getPosition().getSourceStart())) - .map(executable -> executable instanceof CtConstructor ? className : executable.getSimpleName()) - .orElse(null); - } - - private static boolean contains(SourcePosition outer, SourcePosition inner) { - return outer.isValidPosition() - && outer.getFile() != null && inner.getFile() != null - && outer.getFile().toPath().toAbsolutePath().normalize() - .equals(inner.getFile().toPath().toAbsolutePath().normalize()) - && outer.getSourceStart() <= inner.getSourceStart() - && inner.getSourceEnd() <= outer.getSourceEnd(); - } - - private static List getActualStates(Expression expectedState, VCImplication foundState, - List states) { - String receiver = getStateReceiver(expectedState, states); - if (receiver == null || foundState == null) return List.of(); - - List foundStates = new ArrayList<>(); - for (VCImplication premise = foundState; premise != null; premise = premise.getNext()) { - if (premise.hasBinder() && receiver.equals(premise.getName()) && premise.getRefinement() != null) { - foundStates.addAll(getStateExpressions(premise.getRefinement().getExpression(), states, receiver)); - } - } - return states.stream().filter(foundStates::contains).toList(); - } - - /** - * Gets the states accepted by the expected condition for the receiver, in state set order - */ - private static List getExpectedStates(Expression expectedState, List states, String receiver) { - if (expectedState == null || receiver == null) return List.of(); - List expectedStates = getStateExpressions(expectedState, states, receiver); - return states.stream().filter(expectedStates::contains).toList(); - } - - /** - * Gets the found premises about the receiver: those bound to it and unbound ones that mention it - */ - private static List getReceiverPremises(VCImplication foundState, String receiver) { - List premises = new ArrayList<>(); - if (receiver == null) return premises; - for (VCImplication premise = foundState; premise != null; premise = premise.getNext()) { - if (premise.getRefinement() == null) continue; - Expression expr = premise.getRefinement().getExpression(); - boolean bound = premise.hasBinder() && receiver.equals(premise.getName()); - boolean mentioned = !premise.hasBinder() && premise.getRefinement().getVariableNames().contains(receiver); - if ((bound || mentioned) && !expr.isBooleanTrue()) { - premises.add(expr); - } - } - return premises; - } - - private static String getFoundCondition(VCImplication foundState, List foundPremises, - String receiver, String receiverName) { - if (foundState == null) return null; - if (receiver == null) return ExpressionFormatter.format(foundState.toPredicate().getExpression()); - return formatCondition(joinConjunction(foundPremises), receiver, receiverName); - } - - private static Expression joinConjunction(List exprs) { - Expression conjunction = null; - for (Expression expr : exprs) { - conjunction = conjunction == null ? expr : new BinaryExpression(conjunction, "&&", expr); - } - return conjunction == null ? new LiteralBoolean(true) : conjunction; - } - /** - * Formats a condition for display, showing the receiver by its source name (e.g. root instead of root¹⁷) - */ - private static String formatCondition(Expression expr, String receiver, String receiverName) { - if (receiver != null && receiverName != null) { - expr = expr.substitute(new Var(receiver), new Var(receiverName)); - } - return ExpressionFormatter.format(expr); - } - - /** - * Gets the single state the found premises establish for the receiver, or null if there is none - * A state is established only when it is a top-level conjunct; any other conjunct about the receiver's - * states (disjunction, conditional, ...) makes the result null, except negations of other states. - * Negations alone never establish a state since the verifier does not bound the state set - */ - private static String getDefiniteState(List premises, List states, String receiver) { - if (receiver == null) return null; - List found = new ArrayList<>(); - List excluded = new ArrayList<>(); - for (Expression premise : premises) { - for (Expression conjunct : premise.getConjuncts()) { - String state = getReceiverState(conjunct, states, receiver); - String negated = conjunct instanceof UnaryExpression unary && unary.getOp().equals("!") - ? getReceiverState(unary.getExpression(), states, receiver) : null; - if (state != null) { - if (!found.contains(state)) found.add(state); - } else if (negated != null) { - excluded.add(negated); - } else if (mentionsReceiverState(conjunct, states, receiver)) { - return null; - } - } - } - if (found.size() != 1 || excluded.contains(found.get(0))) return null; - return found.get(0); - } - - /** - * Gets the states the found premises allow for the receiver, in state set order - * Each conjunct about the receiver's states is evaluated like the expected condition and the results are - * intersected; conjuncts that do not mention the receiver's states do not restrict it - */ - private static List getPossibleStates(List premises, List states, String receiver) { - if (receiver == null) return List.of(); - List possible = new ArrayList<>(states); - for (Expression premise : premises) { - for (Expression conjunct : premise.getConjuncts()) { - if (mentionsReceiverState(conjunct, states, receiver)) { - possible.retainAll(getStateExpressions(conjunct, states, receiver)); - } - } - } - return List.copyOf(possible); - } - - private static String getReceiverState(Expression expr, List states, String receiver) { - return expr instanceof FunctionInvocation ? getStateName(expr, states, receiver) : null; - } - - private static boolean mentionsReceiverState(Expression expr, List states, String receiver) { - if (getReceiverState(expr, states, receiver) != null) return true; - return expr.getChildren().stream().anyMatch(child -> mentionsReceiverState(child, states, receiver)); - } - - private static String getStateReceiver(Expression expr, List states) { - if (expr == null) return null; - if (expr instanceof FunctionInvocation invocation - && findState(invocation.getName(), states) != null - && !invocation.getArgs().isEmpty() - && invocation.getArgs().get(0) instanceof Var receiver) { - return receiver.getName(); - } - for (Expression child : expr.getChildren()) { - String receiver = getStateReceiver(child, states); - if (receiver != null) return receiver; - } - return null; - } - - /** - * Finds the first class or interface in the model (we assume only one per file) + * Finds the class or interface in the model with the given name, or the first one if the name is null * @param model the CtModel + * @param className the qualified class name, or null * @return CtType or null if none found */ - private static CtType getType(CtModel model) { + private static CtType getType(CtModel model, String className) { for (CtType type : model.getAllTypes()) { - if (type.isClass() || type.isInterface()) + if ((type.isClass() || type.isInterface()) && (className == null || className.equals(getClassName(type)))) return type; } return null; @@ -625,21 +436,17 @@ private static List getStateSources(Expression expr, List getStateExpressions(Expression expr, List states) { - return getStateExpressions(expr, states, null); - } - - private static List getStateExpressions(Expression expr, List states, String receiver) { List stateExpressions = new ArrayList<>(); - String state = getStateName(expr, states, receiver); + String state = getStateName(expr, states); if (state != null) { stateExpressions.add(state); } else if (expr instanceof BinaryExpression bin) { - stateExpressions.addAll(getStateExpressions(bin.getFirstOperand(), states, receiver)); - stateExpressions.addAll(getStateExpressions(bin.getSecondOperand(), states, receiver)); + stateExpressions.addAll(getStateExpressions(bin.getFirstOperand(), states)); + stateExpressions.addAll(getStateExpressions(bin.getSecondOperand(), states)); } else if (expr instanceof UnaryExpression unary) { if (unary.getOp().equals("!")) { // all except those in the expression - List negatedStates = getStateExpressions(unary.getExpression(), states, receiver); + List negatedStates = getStateExpressions(unary.getExpression(), states); if (!negatedStates.isEmpty()) { for (String possibleState : states) { if (!negatedStates.contains(possibleState)) { @@ -650,27 +457,17 @@ private static List getStateExpressions(Expression expr, List st } } else if (expr instanceof Ite ite) { // combine states from then and else branches - stateExpressions.addAll(getStateExpressions(ite.getThen(), states, receiver)); - stateExpressions.addAll(getStateExpressions(ite.getElse(), states, receiver)); + stateExpressions.addAll(getStateExpressions(ite.getThen(), states)); + stateExpressions.addAll(getStateExpressions(ite.getElse(), states)); } return stateExpressions; } private static String getStateName(Expression expr, List states) { - return getStateName(expr, states, null); - } - - private static String getStateName(Expression expr, List states, String receiver) { if (expr instanceof Var var) { return findState(var.getName(), states); } else if (expr instanceof FunctionInvocation func) { - String state = findState(func.getName(), states); - if (state == null || receiver == null) return state; - if (!func.getArgs().isEmpty() - && func.getArgs().get(0) instanceof Var target - && receiver.equals(target.getName())) { - return state; - } + return findState(func.getName(), states); } return null; } diff --git a/src/test/java/liquidjava/fsm/StateMachineParserTests.java b/src/test/java/liquidjava/fsm/StateMachineParserTests.java index 631eda8..c346dd1 100644 --- a/src/test/java/liquidjava/fsm/StateMachineParserTests.java +++ b/src/test/java/liquidjava/fsm/StateMachineParserTests.java @@ -1,27 +1,21 @@ package liquidjava.fsm; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.File; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import liquidjava.api.CommandLineLauncher; import liquidjava.diagnostics.Diagnostics; +import liquidjava.diagnostics.errors.StateErrorContext; import liquidjava.diagnostics.errors.StateRefinementError; -import liquidjava.processor.VCImplication; -import liquidjava.rj_language.Predicate; -import liquidjava.rj_language.parsing.RefinementsParser; -import spoon.Launcher; -import spoon.reflect.cu.SourcePosition; -import spoon.reflect.declaration.CtClass; -import spoon.reflect.declaration.CtType; -import spoon.reflect.reference.CtTypeReference; - -import static org.junit.jupiter.api.Assertions.assertNotNull; public class StateMachineParserTests { @@ -39,17 +33,6 @@ public void testSimpleStateMachine() { new StateMachineTransition("open", "open", "read")), List.of(new StateMachineInitialTransition("open"))); assertStateMachineEquals(expectedSm, sm); - assertNull(sm.errorContext(), "Plain state machines should not contain diagnostic context"); - } - - @Test - public void testStateMachineGroupsErrorContext() { - StateMachineErrorContext errorContext = new StateMachineErrorContext( - "read", List.of("closed"), "file", List.of("open"), "open(file)", "closed(file)", "closed", - List.of("closed")); - StateMachine sm = new StateMachine("Simple", List.of("open", "closed"), List.of(), List.of(), errorContext); - - assertEquals(errorContext, sm.errorContext()); } @Test @@ -282,158 +265,36 @@ public void testExternalFactoryWithoutConstructorHasNoDefaultInitialState() { } @Test - public void testErrorContextForRejectedCall() { - // root.getFirstChild() after add and removeAllChildren - StateRefinementError error = verifyStateError("errors/tree", "root"); - StateMachine sm = parseWithErrorContext(error); - assertNotNull(sm); - assertEquals(new StateMachineErrorContext("getFirstChild", List.of("childrenAllowed"), "root", - List.of("maybeHasChildren"), "maybeHasChildren(root)", "childrenAllowed(root)", "childrenAllowed", - List.of("childrenAllowed")), - sm.errorContext()); - } - - @Test - public void testErrorContextForPreconditionWithMultipleStates() { - StateRefinementError error = verifyStateError("errors/door", "twice"); - assertEquals(new StateMachineErrorContext("lock", List.of("locked"), "twice", List.of("open", "closed"), - "open(twice) || closed(twice)", "locked(twice)", "locked", List.of("locked")), - parseWithErrorContext(error).errorContext()); - } - - @Test - public void testErrorContextForNegatedPrecondition() { - StateRefinementError error = verifyStateError("errors/door", "pushed"); - assertEquals(new StateMachineErrorContext("push", List.of("locked"), "pushed", List.of("open", "closed"), - "!locked(pushed)", "locked(pushed)", "locked", List.of("locked")), - parseWithErrorContext(error).errorContext()); - } - - @Test - public void testErrorContextWithoutDefiniteFoundState() { - // found flag ? open(unknown) : closed(unknown) does not establish one state - StateRefinementError error = verifyStateError("errors/door", "unknown"); - assertEquals(new StateMachineErrorContext("unlock", List.of("open", "closed"), "unknown", - List.of("locked"), "locked(unknown)", "flag ? open(unknown) : closed(unknown)", null, - List.of("open", "closed")), - parseWithErrorContext(error).errorContext()); - } - - @Test - public void testErrorContextForStateFromBranches() { - // socket.receive after closing the socket on one branch and sending on the other - StateRefinementError error = verifyStateError("errors/udp", "socket"); - StateMachineErrorContext errorContext = parseWithErrorContext(error).errorContext(); - assertEquals("receive", errorContext.calledMethod()); - assertEquals("socket", errorContext.receiver()); - assertEquals(List.of("unbound", "bound", "connected"), errorContext.expectedStates()); - assertEquals(List.of("bound", "closed"), errorContext.possibleStates()); - assertNull(errorContext.definiteFoundState()); - } - - @Test - public void testPossibleStates() { - String doorUri = uri("errors/door/Door.java"); - assertEquals(List.of("open"), possibleStates(doorUri, "open(d)")); - assertEquals(List.of("open"), possibleStates(doorUri, "open(d) && !locked(d)")); - assertEquals(List.of("open"), possibleStates(doorUri, "open(d) && (closed(e) || locked(e))")); - assertEquals(List.of("open", "closed"), possibleStates(doorUri, "closed(d) || open(d)")); - assertEquals(List.of("open", "closed"), possibleStates(doorUri, "flag ? open(d) : closed(d)")); - assertEquals(List.of("open", "locked"), possibleStates(doorUri, "!closed(d)")); - assertEquals(List.of("closed"), possibleStates(doorUri, "!open(d) && !locked(d)")); - assertEquals(List.of("closed"), possibleStates(doorUri, "!(open(d) || locked(d))")); - assertEquals(List.of("open"), possibleStates(doorUri, "open(d) && (open(d) || closed(d))")); - assertEquals(List.of(), possibleStates(doorUri, "open(d) && closed(d)")); - assertEquals(List.of(), possibleStates(doorUri, "open(d) && !open(d)")); - assertEquals(List.of("open", "closed", "locked"), possibleStates(doorUri, "open(e)")); - assertEquals(List.of("open", "closed", "locked"), possibleStates(doorUri, "x > 0")); - assertEquals(List.of("open", "closed", "locked"), possibleStates(doorUri, "true")); - } - - @Test - public void testNegatedFoundExcludesState() { - CtTypeReference doorType = new Launcher().getFactory().Type().createReference("fsm.errors.door.Door"); - VCImplication found = new VCImplication("#s_4", doorType, predicate("!closed(#s_4)")); - StateMachineErrorContext errorContext = StateMachineParser.parseWithErrorContext( - uri("errors/door/Door.java"), null, RefinementsParser.createAST("closed(#s_4)", ""), found) - .errorContext(); - assertEquals(List.of("open", "locked"), errorContext.possibleStates()); - } - - @Test - public void testPossibleStatesWithoutReceiver() { - CtTypeReference doorType = new Launcher().getFactory().Type().createReference("fsm.errors.door.Door"); - VCImplication found = new VCImplication("d", doorType, predicate("open(d)")); - assertEquals(List.of(), StateMachineParser.parseWithErrorContext(uri("errors/door/Door.java"), null, - RefinementsParser.createAST("x > 0", ""), found).errorContext().possibleStates()); - } - - @Test - public void testPossibleStatesMatchDefiniteFoundState() { - String doorUri = uri("errors/door/Door.java"); - for (String found : List.of("open(d)", "open(d) && !locked(d)", "open(d) && open(e)", - "open(d) && (closed(e) || locked(e))", "open(d) || closed(d)", "flag ? open(d) : closed(d)", - "!locked(d)", "!open(d) && !locked(d)", "!(open(d) || locked(d))", "open(d) && closed(d)", - "open(d) && !open(d)", "open(d) && (open(d) || closed(d))", "open(e)", "true")) { - StateMachineErrorContext errorContext = errorContext(doorUri, found); - if (errorContext.definiteFoundState() != null) { - assertEquals(List.of(errorContext.definiteFoundState()), errorContext.possibleStates(), found); + public void testClassSelectedByName() { + StateMachine sm = StateMachineParser.parse(uri("TwoStateClasses.java"), "fsm.Second"); + assertStateMachineEquals(stateMachine("Second", List.of("idle", "busy"), + List.of(new StateMachineTransition("idle", "busy", "start")), + List.of(new StateMachineInitialTransition("idle"))), sm); + assertEquals("fsm.TwoStateClasses", StateMachineParser.parse(uri("TwoStateClasses.java")).className()); + assertNull(StateMachineParser.parse(uri("TwoStateClasses.java"), "fsm.Missing")); + } + + @Test + public void testVerifierStatesAreStatesOfTheDiagram() { + for (String directory : List.of("errors/door", "errors/tree", "errors/udp")) { + List errors = stateErrors(directory); + assertFalse(errors.isEmpty(), "Expected state refinement errors in " + directory); + for (StateRefinementError error : errors) { + StateErrorContext context = error.getContext(); + assertNotNull(context, "Expected the verifier's states for " + error.getMessage()); + StateMachine sm = StateMachineParser.parse(error.getDeclarationPosition().getFile().toURI().toString(), + context.receiverClass()); + assertNotNull(sm, "Expected a diagram of " + context.receiverClass()); + List> verifierStates = new ArrayList<>(context.possibleStates()); + if (context.acceptedStates() != null) + verifierStates.addAll(context.acceptedStates()); + // the diagram shows the first state set + for (List state : verifierStates) + assertTrue(sm.states().contains(state.get(0)), state.get(0) + " is not in " + sm.states()); } } } - @Test - public void testDefiniteFoundState() { - String doorUri = uri("errors/door/Door.java"); - assertEquals("open", definiteFoundState(doorUri, "open(d)")); - assertEquals("open", definiteFoundState(doorUri, "open(d) && !locked(d)")); - assertEquals("open", definiteFoundState(doorUri, "open(d) && open(e)")); - assertEquals("open", definiteFoundState(doorUri, "open(d) && (closed(e) || locked(e))")); - assertNull(definiteFoundState(doorUri, "open(d) || closed(d)")); - assertNull(definiteFoundState(doorUri, "flag ? open(d) : closed(d)")); - assertNull(definiteFoundState(doorUri, "!locked(d)")); - assertNull(definiteFoundState(doorUri, "!open(d) && !locked(d)")); - assertNull(definiteFoundState(doorUri, "!(open(d) || locked(d))")); - assertNull(definiteFoundState(doorUri, "open(d) && closed(d)")); - assertNull(definiteFoundState(doorUri, "open(d) && !open(d)")); - assertNull(definiteFoundState(doorUri, "open(d) && (open(d) || closed(d))")); - assertNull(definiteFoundState(doorUri, "open(e)")); - assertNull(definiteFoundState(doorUri, "true")); - } - - @Test - public void testFoundConditionIsRestrictedToReceiver() { - CtTypeReference doorType = new Launcher().getFactory().Type().createReference("fsm.errors.door.Door"); - VCImplication other = new VCImplication("#e_2", doorType, predicate("closed(#e_2)")); - VCImplication receiver = new VCImplication("#d_3", doorType, predicate("open(#d_3) && x > 0")); - other.setNext(receiver); - StateMachineErrorContext errorContext = StateMachineParser.parseWithErrorContext( - uri("errors/door/Door.java"), null, RefinementsParser.createAST("locked(#d_3)", ""), other) - .errorContext(); - assertEquals(new StateMachineErrorContext(null, List.of("open"), "d", List.of("locked"), "locked(d)", - "open(d) && x > 0", "open", List.of("open")), errorContext); - } - - @Test - public void testCalledConstructorAndStaticFactory() { - CtClass staticFactory = (CtClass) buildType("StaticFactory.java"); - assertEquals("StaticFactory", calledMethod("StaticFactory.java", - staticFactory.getConstructors().iterator().next().getPosition())); - assertEquals("of", calledMethod("StaticFactory.java", - staticFactory.getMethodsByName("of").get(0).getPosition())); - assertEquals("close", calledMethod("StaticFactory.java", - staticFactory.getMethodsByName("close").get(0).getPosition())); - - // interfaces declare constructors as methods named after the class - CtType refinements = buildType("errors/tree/DefaultMutableTreeNodeRefinements.java"); - assertEquals("DefaultMutableTreeNode", calledMethod("errors/tree/DefaultMutableTreeNodeRefinements.java", - refinements.getMethodsByName("DefaultMutableTreeNode").get(0).getPosition())); - - // positions outside the state machine's type resolve to no method - assertNull(calledMethod("errors/tree/DefaultMutableTreeNodeRefinements.java", - staticFactory.getMethodsByName("close").get(0).getPosition())); - } - private static void assertStateMachineEquals(StateMachine expected, StateMachine actual) { assertNotNull(actual, "State machine should not be null"); assertEquals(expected.className(), actual.className(), "Class names should match"); @@ -442,61 +303,19 @@ private static void assertStateMachineEquals(StateMachine expected, StateMachine .toList(), "Initial transitions should match"); assertEquals(expected.states(), actual.states(), "States should match"); assertEquals(expected.transitions(), actual.transitions(), "State transitions should match"); - assertEquals(expected.errorContext(), actual.errorContext(), "Error contexts should match"); } private static StateMachine stateMachine(String className, List states, List transitions, List initialTransitions) { String qualifiedName = className.contains(".") ? className : "fsm." + className; - return new StateMachine(qualifiedName, states, transitions, initialTransitions, null); + return new StateMachine(qualifiedName, states, transitions, initialTransitions); } - private static StateRefinementError verifyStateError(String directory, String receiver) { + private static List stateErrors(String directory) { CommandLineLauncher.launch(BASE_PATH.resolve(directory).toAbsolutePath().toString()); return Diagnostics.getInstance().getErrors().stream() .filter(StateRefinementError.class::isInstance) .map(StateRefinementError.class::cast) - .filter(error -> error.getFound().toString().contains("#" + receiver + "_")) - .findFirst() - .orElseThrow(() -> new AssertionError("No state refinement error found for " + receiver)); - } - - // same arguments as the vscode-liquidjava server - private static StateMachine parseWithErrorContext(StateRefinementError error) { - return StateMachineParser.parseWithErrorContext( - new File(error.getDeclarationPosition().getFile().getPath()).toURI().toString(), - error.getDeclarationPosition(), error.getExpected().getExpression(), error.getFound()); - } - - private static StateMachineErrorContext errorContext(String uri, String found) { - CtTypeReference doorType = new Launcher().getFactory().Type().createReference("fsm.errors.door.Door"); - VCImplication foundState = new VCImplication("d", doorType, predicate(found)); - return StateMachineParser.parseWithErrorContext(uri, null, RefinementsParser.createAST("locked(d)", ""), - foundState).errorContext(); - } - - private static String definiteFoundState(String uri, String found) { - return errorContext(uri, found).definiteFoundState(); - } - - private static List possibleStates(String uri, String found) { - return errorContext(uri, found).possibleStates(); - } - - private static Predicate predicate(String expr) { - return new Predicate(RefinementsParser.createAST(expr, "")); - } - - private static String calledMethod(String file, SourcePosition declarationPosition) { - return StateMachineParser.parseWithErrorContext(uri(file), declarationPosition, null, null) - .errorContext().calledMethod(); - } - - private static CtType buildType(String file) { - Launcher launcher = new Launcher(); - launcher.getEnvironment().setNoClasspath(true); - launcher.addInputResource(BASE_PATH.resolve(file).toAbsolutePath().toString()); - launcher.buildModel(); - return launcher.getModel().getAllTypes().iterator().next(); + .toList(); } } diff --git a/src/test/resources/fsm/TwoStateClasses.java b/src/test/resources/fsm/TwoStateClasses.java new file mode 100644 index 0000000..c83e93a --- /dev/null +++ b/src/test/resources/fsm/TwoStateClasses.java @@ -0,0 +1,18 @@ +package fsm; + +import liquidjava.specification.StateRefinement; +import liquidjava.specification.StateSet; + +@StateSet({"open", "closed"}) +public class TwoStateClasses { + + @StateRefinement(from="open(this)", to="closed(this)") + public void close() {} +} + +@StateSet({"idle", "busy"}) +class Second { + + @StateRefinement(from="idle(this)", to="busy(this)") + public void start() {} +} diff --git a/src/test/resources/fsm/errors/udp/DatagramSocketRefinementsExpert.java b/src/test/resources/fsm/errors/udp/DatagramSocketRefinementsExpert.java index 64acf97..b9ed2ca 100644 --- a/src/test/resources/fsm/errors/udp/DatagramSocketRefinementsExpert.java +++ b/src/test/resources/fsm/errors/udp/DatagramSocketRefinementsExpert.java @@ -47,16 +47,13 @@ public interface DatagramSocketRefinementsExpert { void disconnect(); @StateRefinement(from = "connected(this)") - @StateRefinement(from = "closed(this) && connected(old(this))") InetAddress getInetAddress(); @StateRefinement(from = "connected(this)") - @StateRefinement(from = "closed(this) && connected(old(this))") @Refinement("Port(_) || _ == -1") int getPort(); @StateRefinement(from = "connected(this)") - @StateRefinement(from = "closed(this) && connected(old(this))") SocketAddress getRemoteSocketAddress(); @StateRefinement(from = "bound(this)")