From d228e1aa81f50b88a0caa62c4c5e0f0932fb94e8 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Thu, 11 Dec 2025 17:24:49 +0100 Subject: [PATCH 01/15] Add core classes for statemodel changedetection --- .../changedetection/DeltaAction.java | 86 ++++++++++++ .../changedetection/DeltaState.java | 118 ++++++++++++++++ .../statemodel/changedetection/DiffType.java | 37 +++++ .../changedetection/PropertyDiff.java | 85 ++++++++++++ .../VertexPropertyComparator.java | 96 +++++++++++++ .../changedetection/VertexPropertyDiff.java | 76 +++++++++++ .../changedetection/DeltaActionTest.java | 60 +++++++++ .../changedetection/DeltaStateTest.java | 64 +++++++++ .../VertexPropertyComparatorTest.java | 126 ++++++++++++++++++ 9 files changed, 748 insertions(+) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/DeltaState.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/DiffType.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java b/statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java new file mode 100644 index 000000000..c59afea41 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java @@ -0,0 +1,86 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Objects; + +public class DeltaAction { + + public enum Direction { + INCOMING, + OUTGOING + } + + private final String actionId; + private final String description; + private final Direction direction; + + public DeltaAction(String actionId, String description, Direction direction) { + this.actionId = Objects.requireNonNull(actionId, "actionId cannot be null"); + if (actionId.trim().isEmpty()) { + throw new IllegalArgumentException("actionId cannot be empty or blank"); + } + this.description = Objects.requireNonNull(description, "description cannot be null"); + this.direction = Objects.requireNonNull(direction, "direction cannot be null"); + } + + public String getActionId() { + return actionId; + } + + public String getDescription() { + return description; + } + + public Direction getDirection() { + return direction; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DeltaAction)) { + return false; + } + DeltaAction other = (DeltaAction) obj; + return actionId.equals(other.actionId) && + description.equals(other.description) && + direction == other.direction; + } + + @Override + public int hashCode() { + return Objects.hash(actionId, description, direction); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/DeltaState.java b/statemodel/src/org/testar/statemodel/changedetection/DeltaState.java new file mode 100644 index 000000000..ee881ab2f --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/DeltaState.java @@ -0,0 +1,118 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class DeltaState { + + private final String stateId; + private final List concreteStateIds; + private final List incomingDeltaActions; + private final List outgoingDeltaActions; + + public DeltaState(String stateId, + List concreteStateIds, + List incomingDeltaActions, + List outgoingDeltaActions) { + this.stateId = Objects.requireNonNull(stateId, "stateId cannot be null"); + if (stateId.trim().isEmpty()) { + throw new IllegalArgumentException("stateId cannot be empty or blank"); + } + this.concreteStateIds = copyConcreteStateIdList(concreteStateIds); + this.incomingDeltaActions = copyDeltaActionList(incomingDeltaActions); + this.outgoingDeltaActions = copyDeltaActionList(outgoingDeltaActions); + } + + private static List copyConcreteStateIdList(List source) { + List copy = new ArrayList<>(); + if (source != null) { + for (String item : source) { + Objects.requireNonNull(item, "ConcreteStateID cannot be null"); + if (item.trim().isEmpty()) { + throw new IllegalArgumentException("ConcreteStateID cannot be empty or blank"); + } + copy.add(item); + } + } + return copy; + } + + private static List copyDeltaActionList(List source) { + List copy = new ArrayList<>(); + if (source != null) { + for (DeltaAction item : source) { + copy.add(Objects.requireNonNull(item, "DeltaAction cannot be null")); + } + } + return copy; + } + + public String getStateId() { + return stateId; + } + + public List getConcreteStateIds() { + return Collections.unmodifiableList(concreteStateIds); + } + + public List getIncomingDeltaActions() { + return Collections.unmodifiableList(incomingDeltaActions); + } + + public List getOutgoingDeltaActions() { + return Collections.unmodifiableList(outgoingDeltaActions); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DeltaState)) { + return false; + } + DeltaState other = (DeltaState) obj; + return stateId.equals(other.stateId) && + concreteStateIds.equals(other.concreteStateIds) && + incomingDeltaActions.equals(other.incomingDeltaActions) && + outgoingDeltaActions.equals(other.outgoingDeltaActions); + } + + @Override + public int hashCode() { + return Objects.hash(stateId, concreteStateIds, incomingDeltaActions, outgoingDeltaActions); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/DiffType.java b/statemodel/src/org/testar/statemodel/changedetection/DiffType.java new file mode 100644 index 000000000..f2a6cf57f --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/DiffType.java @@ -0,0 +1,37 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +public enum DiffType { + ADDED, + REMOVED, + CHANGED +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java b/statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java new file mode 100644 index 000000000..09e69aa49 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java @@ -0,0 +1,85 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Objects; + +public class PropertyDiff { + + private final String propertyName; + private final String oldValue; + private final String newValue; + private final DiffType diffType; + + public PropertyDiff(String propertyName, String oldValue, String newValue, DiffType diffType) { + this.propertyName = Objects.requireNonNull(propertyName, "propertyName cannot be null"); + this.diffType = Objects.requireNonNull(diffType, "diffType cannot be null"); + this.oldValue = oldValue; + this.newValue = newValue; + } + + public String getPropertyName() { + return propertyName; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public DiffType getDiffType() { + return diffType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PropertyDiff)) { + return false; + } + PropertyDiff other = (PropertyDiff) obj; + return diffType == other.diffType && + propertyName.equals(other.propertyName) && + Objects.equals(oldValue, other.oldValue) && + Objects.equals(newValue, other.newValue); + } + + @Override + public int hashCode() { + return Objects.hash(propertyName, oldValue, newValue, diffType); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java b/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java new file mode 100644 index 000000000..52e25c5b9 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java @@ -0,0 +1,96 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Compares two sets of vertex properties and classifies their differences. + * - properties starting with "CD_" or custom predicate are ignored + * - added/removed/changed properties are reported with their values + */ +public class VertexPropertyComparator { + + private static final Predicate DEFAULT_FILTER = name -> !name.startsWith("CD_"); + + private VertexPropertyComparator() { } + + public static VertexPropertyDiff compare(Map oldProps, Map newProps) { + return compare(oldProps, newProps, DEFAULT_FILTER); + } + + public static VertexPropertyDiff compare(Map oldProps, + Map newProps, + Predicate includePredicate) { + Objects.requireNonNull(oldProps, "oldProps cannot be null"); + Objects.requireNonNull(newProps, "newProps cannot be null"); + Objects.requireNonNull(includePredicate, "includePredicate cannot be null"); + + List added = new ArrayList<>(); + List removed = new ArrayList<>(); + List changed = new ArrayList<>(); + + // evaluate new properties first (added/changed) + for (Map.Entry entry : newProps.entrySet()) { + String key = entry.getKey(); + if (!includePredicate.test(key)) { + continue; + } + String newValue = entry.getValue(); + if (oldProps.containsKey(key)) { + String oldValue = oldProps.get(key); + if (!Objects.equals(newValue, oldValue)) { + changed.add(new PropertyDiff(key, oldValue, newValue, DiffType.CHANGED)); + } + } else { + added.add(new PropertyDiff(key, null, newValue, DiffType.ADDED)); + } + } + + // evaluate removed properties + for (Map.Entry entry : oldProps.entrySet()) { + String key = entry.getKey(); + if (!includePredicate.test(key)) { + continue; + } + if (!newProps.containsKey(key)) { + removed.add(new PropertyDiff(key, entry.getValue(), null, DiffType.REMOVED)); + } + } + + return new VertexPropertyDiff(added, removed, changed); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java b/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java new file mode 100644 index 000000000..838051d70 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java @@ -0,0 +1,76 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class VertexPropertyDiff { + + private final List added; + private final List removed; + private final List changed; + + public VertexPropertyDiff(List added, List removed, List changed) { + this.added = safeCopy(added); + this.removed = safeCopy(removed); + this.changed = safeCopy(changed); + } + + private static List safeCopy(List source) { + List copy = new ArrayList<>(); + if (source != null) { + for (PropertyDiff diff : source) { + copy.add(Objects.requireNonNull(diff, "diff item cannot be null")); + } + } + return copy; + } + + public List getAdded() { + return Collections.unmodifiableList(added); + } + + public List getRemoved() { + return Collections.unmodifiableList(removed); + } + + public List getChanged() { + return Collections.unmodifiableList(changed); + } + + public boolean isEmpty() { + return added.isEmpty() && removed.isEmpty() && changed.isEmpty(); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java b/statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java new file mode 100644 index 000000000..f8b950bbb --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java @@ -0,0 +1,60 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; +import org.testar.statemodel.changedetection.DeltaAction.Direction; + +public class DeltaActionTest { + + @Test + public void testOutgoingDeltaAction() { + DeltaAction action = new DeltaAction("aa1", "click", Direction.OUTGOING); + assertEquals("aa1", action.getActionId()); + assertEquals("click", action.getDescription()); + assertEquals(Direction.OUTGOING, action.getDirection()); + } + + @Test + public void testIncomingDeltaAction() { + DeltaAction action = new DeltaAction("aa2", "type", Direction.INCOMING); + assertEquals("aa2", action.getActionId()); + assertEquals("type", action.getDescription()); + assertEquals(Direction.INCOMING, action.getDirection()); + } + + @Test + public void testEqualsDeltaAction() { + DeltaAction a = new DeltaAction("aa1", "click", Direction.INCOMING); + DeltaAction b = new DeltaAction("aa1", "click", Direction.INCOMING); + DeltaAction c = new DeltaAction("aa2", "click", Direction.INCOMING); + DeltaAction d = new DeltaAction("aa1", "click", Direction.OUTGOING); + DeltaAction e = new DeltaAction("aa1", "tap", Direction.INCOMING); + assertEquals(a, b); + assertNotEquals(a, c); + assertNotEquals(a, d); + assertNotEquals(a, e); + } + + @Test(expected = NullPointerException.class) + public void testNullActionId() { + new DeltaAction(null, "desc", Direction.INCOMING); + } + + @Test(expected = NullPointerException.class) + public void testNullDescription() { + new DeltaAction("aa1", null, Direction.INCOMING); + } + + @Test(expected = NullPointerException.class) + public void testNullDirection() { + new DeltaAction("aa1", "desc", null); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyActionId() { + new DeltaAction(" ", "desc", Direction.INCOMING); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java b/statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java new file mode 100644 index 000000000..e4d19fb55 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java @@ -0,0 +1,64 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.testar.statemodel.changedetection.DeltaAction.Direction; + +public class DeltaStateTest { + + @Test + public void testDeltaStateConstructor() { + DeltaAction incomingOne = new DeltaAction("aa11", "click", Direction.INCOMING); + DeltaAction incomingTwo = new DeltaAction("aa12", "type", Direction.INCOMING); + DeltaAction outgoing = new DeltaAction("aa2", "click", Direction.OUTGOING); + + DeltaState deltaState = new DeltaState("as1", + Arrays.asList("cs1", "cs2", "cs3"), + Arrays.asList(incomingOne, incomingTwo), + Arrays.asList(outgoing)); + + assertEquals(3, deltaState.getConcreteStateIds().size()); + assertEquals(2, deltaState.getIncomingDeltaActions().size()); + assertEquals(1, deltaState.getOutgoingDeltaActions().size()); + + assertTrue(deltaState.getIncomingDeltaActions().contains(incomingOne)); + assertTrue(deltaState.getIncomingDeltaActions().contains(incomingTwo)); + assertTrue(deltaState.getOutgoingDeltaActions().contains(outgoing)); + + assertNotSame(deltaState.getIncomingDeltaActions(), deltaState.getOutgoingDeltaActions()); + assertNotSame(deltaState.getConcreteStateIds(), deltaState.getOutgoingDeltaActions()); + } + + @Test(expected = NullPointerException.class) + public void testNullStateId() { + new DeltaState(null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + + @Test + public void testNullActionList() { + DeltaState deltaState = new DeltaState("as1", null, null, null); + assertTrue(deltaState.getConcreteStateIds().isEmpty()); + assertTrue(deltaState.getIncomingDeltaActions().isEmpty()); + assertTrue(deltaState.getOutgoingDeltaActions().isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyStateId() { + new DeltaState(" ", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + + @Test + public void testEmptyActionList() { + DeltaState deltaState = new DeltaState("as1", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + assertTrue(deltaState.getConcreteStateIds().isEmpty()); + assertTrue(deltaState.getIncomingDeltaActions().isEmpty()); + assertTrue(deltaState.getOutgoingDeltaActions().isEmpty()); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java new file mode 100644 index 000000000..db9169a7a --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java @@ -0,0 +1,126 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +public class VertexPropertyComparatorTest { + + @Test + public void testUnchangedProperties() { + Map oldProps = new HashMap<>(); + oldProps.put("name", "Home"); + oldProps.put("id", "same"); + + Map newProps = new HashMap<>(); + newProps.put("name", "Home"); + newProps.put("id", "same"); + + VertexPropertyDiff vertexDiff = VertexPropertyComparator.compare(oldProps, newProps); + + assertEquals(0, vertexDiff.getAdded().size()); + assertEquals(0, vertexDiff.getRemoved().size()); + assertEquals(0, vertexDiff.getChanged().size()); + } + + @Test + public void testChangedProperties() { + Map oldProps = new HashMap<>(); + oldProps.put("name", "Home"); + oldProps.put("id", "same"); + + Map newProps = new HashMap<>(); + newProps.put("name", "Start"); + newProps.put("id", "same"); + + VertexPropertyDiff vertexDiff = VertexPropertyComparator.compare(oldProps, newProps); + + assertEquals(0, vertexDiff.getAdded().size()); + assertEquals(0, vertexDiff.getRemoved().size()); + + assertEquals(1, vertexDiff.getChanged().size()); + assertEquals("name", vertexDiff.getChanged().get(0).getPropertyName()); + assertEquals("Home", vertexDiff.getChanged().get(0).getOldValue()); + assertEquals("Start", vertexDiff.getChanged().get(0).getNewValue()); + assertEquals(DiffType.CHANGED, vertexDiff.getChanged().get(0).getDiffType()); + } + + @Test + public void testAddedProperties() { + Map oldProps = new HashMap<>(); + oldProps.put("name", "Home"); + oldProps.put("id", "same"); + + Map newProps = new HashMap<>(); + newProps.put("name", "Home"); + newProps.put("id", "same"); + newProps.put("title", "Bank"); + + VertexPropertyDiff vertexDiff = VertexPropertyComparator.compare(oldProps, newProps); + + assertEquals(0, vertexDiff.getChanged().size()); + assertEquals(0, vertexDiff.getRemoved().size()); + + assertEquals(1, vertexDiff.getAdded().size()); + assertEquals("title", vertexDiff.getAdded().get(0).getPropertyName()); + assertEquals(null, vertexDiff.getAdded().get(0).getOldValue()); + assertEquals("Bank", vertexDiff.getAdded().get(0).getNewValue()); + assertEquals(DiffType.ADDED, vertexDiff.getAdded().get(0).getDiffType()); + } + + @Test + public void testRemovedProperties() { + Map oldProps = new HashMap<>(); + oldProps.put("name", "Home"); + oldProps.put("id", "same"); + + Map newProps = new HashMap<>(); + newProps.put("name", "Home"); + + VertexPropertyDiff vertexDiff = VertexPropertyComparator.compare(oldProps, newProps); + + assertEquals(0, vertexDiff.getChanged().size()); + assertEquals(0, vertexDiff.getAdded().size()); + + assertEquals(1, vertexDiff.getRemoved().size()); + assertEquals("id", vertexDiff.getRemoved().get(0).getPropertyName()); + assertEquals("same", vertexDiff.getRemoved().get(0).getOldValue()); + assertEquals(null, vertexDiff.getRemoved().get(0).getNewValue()); + assertEquals(DiffType.REMOVED, vertexDiff.getRemoved().get(0).getDiffType()); + } + + @Test + public void testNullAndEmptyValuesAsDifferent() { + Map oldProps = new HashMap<>(); + oldProps.put("title", null); + + Map newProps = new HashMap<>(); + newProps.put("title", ""); + + VertexPropertyDiff diff = VertexPropertyComparator.compare(oldProps, newProps); + assertEquals(1, diff.getChanged().size()); + assertEquals("title", diff.getChanged().get(0).getPropertyName()); + } + + @Test + public void testCustomIgnoreFilter() { + Map oldProps = new HashMap<>(); + oldProps.put("keep", "a"); + oldProps.put("skip_me", "old"); + + Map newProps = new HashMap<>(); + newProps.put("keep", "b"); + newProps.put("skip_me", "new"); + + VertexPropertyDiff diff = VertexPropertyComparator.compare(oldProps, newProps, name -> !name.startsWith("skip")); + assertEquals(1, diff.getChanged().size()); + assertEquals("keep", diff.getChanged().get(0).getPropertyName()); + assertTrue(diff.getAdded().isEmpty()); + assertTrue(diff.getRemoved().isEmpty()); + } + +} From f076f85dd1fb04f7bc22ccee9558c5c1931d22a5 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Thu, 11 Dec 2025 18:19:42 +0100 Subject: [PATCH 02/15] Create state snapshots from the state models --- .../ActionDescriptionProvider.java | 38 ++++++ .../StateDifferenceFinder.java | 114 ++++++++++++++++++ .../changedetection/StateSnapshot.java | 89 ++++++++++++++ .../changedetection/StateSnapshotFactory.java | 83 +++++++++++++ .../StateDifferenceFinderTest.java | 86 +++++++++++++ .../StateSnapshotFactoryTest.java | 54 +++++++++ 6 files changed, 464 insertions(+) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java new file mode 100644 index 000000000..f6e6b7896 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java @@ -0,0 +1,38 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +@FunctionalInterface +public interface ActionDescriptionProvider { + + String getDescription(String abstractActionId); + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java b/statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java new file mode 100644 index 000000000..89bf18167 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java @@ -0,0 +1,114 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.testar.statemodel.changedetection.DeltaAction.Direction; + +/** + * Computes added/removed state deltas between two model snapshots. + */ +public class StateDifferenceFinder { + + /** + * Find states present only in {@code newModelStates} (i.e. newly added). + */ + public List findAddedStates(List oldModelStates, + List newModelStates, + ActionDescriptionProvider descriptionProvider) { + Objects.requireNonNull(oldModelStates, "oldModelStates cannot be null"); + Objects.requireNonNull(newModelStates, "newModelStates cannot be null"); + Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); + + Set oldModelIds = oldModelStates.stream().map(StateSnapshot::getStateId).collect(Collectors.toSet()); + Map newModelById = indexById(newModelStates); + + List result = new ArrayList<>(); + for (String stateId : newModelById.keySet()) { + if (!oldModelIds.contains(stateId)) { + result.add(toDeltaState(newModelById.get(stateId), descriptionProvider)); + } + } + return result; + } + + /** + * Find states present only in {@code oldModelStates} (i.e. removed). + */ + public List findRemovedStates(List oldModelStates, + List newModelStates, + ActionDescriptionProvider descriptionProvider) { + Objects.requireNonNull(oldModelStates, "oldModelStates cannot be null"); + Objects.requireNonNull(newModelStates, "newModelStates cannot be null"); + Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); + + Set newModelIds = newModelStates.stream().map(StateSnapshot::getStateId).collect(Collectors.toSet()); + Map oldModelById = indexById(oldModelStates); + + List result = new ArrayList<>(); + for (String stateId : oldModelById.keySet()) { + if (!newModelIds.contains(stateId)) { + result.add(toDeltaState(oldModelById.get(stateId), descriptionProvider)); + } + } + return result; + } + + private static Map indexById(List stateSnapshots) { + Map map = new HashMap<>(); + for (StateSnapshot state : stateSnapshots) { + map.put(state.getStateId(), state); + } + return map; + } + + private static DeltaState toDeltaState(StateSnapshot stateSnapshot, ActionDescriptionProvider descriptionProvider) { + List incoming = stateSnapshot.getIncomingActionIds().stream() + .map(id -> new DeltaAction(id, descriptionProvider.getDescription(id), Direction.INCOMING)) + .collect(Collectors.toList()); + List outgoing = stateSnapshot.getOutgoingActionIds().stream() + .map(id -> new DeltaAction(id, descriptionProvider.getDescription(id), Direction.OUTGOING)) + .collect(Collectors.toList()); + + return new DeltaState(stateSnapshot.getStateId(), + new ArrayList<>(stateSnapshot.getConcreteStateIds()), + incoming, + outgoing); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java b/statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java new file mode 100644 index 000000000..398b4848b --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java @@ -0,0 +1,89 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * State used for change detection. + */ +public class StateSnapshot { + + private final String stateId; + private final List concreteStateIds; + private final List incomingActionIds; + private final List outgoingActionIds; + + public StateSnapshot(String stateId, + List concreteStateIds, + List incomingActionIds, + List outgoingActionIds) { + this.stateId = Objects.requireNonNull(stateId, "stateId cannot be null"); + if (stateId.trim().isEmpty()) { + throw new IllegalArgumentException("stateId cannot be empty or blank"); + } + this.concreteStateIds = copySafe(concreteStateIds, "concrete state id"); + this.incomingActionIds = copySafe(incomingActionIds, "incoming action id"); + this.outgoingActionIds = copySafe(outgoingActionIds, "outgoing action id"); + } + + private static List copySafe(List source, String label) { + if (source == null) { + return Collections.emptyList(); + } + for (String value : source) { + Objects.requireNonNull(value, label + " cannot be null"); + if (value.trim().isEmpty()) { + throw new IllegalArgumentException(label + " cannot be empty or blank"); + } + } + return Collections.unmodifiableList(source); + } + + public String getStateId() { + return stateId; + } + + public List getConcreteStateIds() { + return concreteStateIds; + } + + public List getIncomingActionIds() { + return incomingActionIds; + } + + public List getOutgoingActionIds() { + return outgoingActionIds; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java b/statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java new file mode 100644 index 000000000..f713f35d1 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java @@ -0,0 +1,83 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.AbstractStateTransition; + +/** + * Builds {@link StateSnapshot}s from an {@link AbstractStateModel}. + */ +public class StateSnapshotFactory { + + private StateSnapshotFactory() { } + + public static List from(AbstractStateModel model) { + Objects.requireNonNull(model, "model cannot be null"); + + Map> incomingActions = new HashMap<>(); + Map> outgoingActions = new HashMap<>(); + + // collect incoming/outgoing action ids per state using the model transitions + for (AbstractState state : model.getStates()) { + incomingActions.put(state.getStateId(), new HashSet<>()); + outgoingActions.put(state.getStateId(), new HashSet<>()); + } + + for (AbstractState state : model.getStates()) { + for (AbstractStateTransition transition : model.getOutgoingTransitionsForState(state.getStateId())) { + outgoingActions.get(state.getStateId()).add(transition.getActionId()); + incomingActions.computeIfAbsent(transition.getTargetStateId(), id -> new HashSet<>()) + .add(transition.getActionId()); + } + } + + List snapshots = new ArrayList<>(); + for (AbstractState state : model.getStates()) { + List concreteIds = new ArrayList<>(state.getConcreteStateIds()); + List incoming = new ArrayList<>(incomingActions.getOrDefault(state.getStateId(), new HashSet<>())); + List outgoing = new ArrayList<>(outgoingActions.getOrDefault(state.getStateId(), new HashSet<>())); + + snapshots.add(new StateSnapshot(state.getStateId(), concreteIds, incoming, outgoing)); + } + return snapshots; + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java b/statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java new file mode 100644 index 000000000..f5f5a6aab --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java @@ -0,0 +1,86 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.testar.statemodel.changedetection.DeltaAction.Direction; + +public class StateDifferenceFinderTest { + + private StateDifferenceFinder finder; + private ActionDescriptionProvider descriptionProvider; + + @Before + public void setUp() { + finder = new StateDifferenceFinder(); + descriptionProvider = id -> "desc-" + id; + } + + @Test + public void testFindAddedState() { + StateSnapshot state = new StateSnapshot( + "AS1", + Arrays.asList("CS1", "CS4"), + Arrays.asList("AS1"), + Arrays.asList("AS2") + ); + + List oldModelStates = Arrays.asList(state); + List newModelStates = Arrays.asList( + state, + new StateSnapshot( + "AS2", + Arrays.asList("CS2", "CS3"), + Arrays.asList("AS3"), + Arrays.asList("AS4") + ) + ); + + List addedStates = finder.findAddedStates(oldModelStates, newModelStates, descriptionProvider); + + assertEquals(1, addedStates.size()); + DeltaState deltaAddedState = addedStates.get(0); + assertEquals("AS2", deltaAddedState.getStateId()); + assertEquals(Arrays.asList("CS2", "CS3"), deltaAddedState.getConcreteStateIds()); + + assertEquals(1, deltaAddedState.getIncomingDeltaActions().size()); + assertEquals("AS3", deltaAddedState.getIncomingDeltaActions().get(0).getActionId()); + assertEquals(Direction.INCOMING, deltaAddedState.getIncomingDeltaActions().get(0).getDirection()); + assertEquals("desc-AS3", deltaAddedState.getIncomingDeltaActions().get(0).getDescription()); + + assertEquals(1, deltaAddedState.getOutgoingDeltaActions().size()); + assertEquals("AS4", deltaAddedState.getOutgoingDeltaActions().get(0).getActionId()); + assertEquals(Direction.OUTGOING, deltaAddedState.getOutgoingDeltaActions().get(0).getDirection()); + assertEquals("desc-AS4", deltaAddedState.getOutgoingDeltaActions().get(0).getDescription()); + } + + @Test + public void testFindRemovedStates() { + StateSnapshot state = new StateSnapshot( + "AS1", + Arrays.asList("CS1", "CS4"), + Arrays.asList("AS1"), + Arrays.asList("AS2") + ); + + List oldModelStates = Arrays.asList( + state, + new StateSnapshot("AS2", Arrays.asList("CS2"), Arrays.asList("AS3"), Arrays.asList("AS4"))); + List newModelStates = Collections.singletonList(state); + + List removedStates = finder.findRemovedStates(oldModelStates, newModelStates, descriptionProvider); + + assertEquals(1, removedStates.size()); + DeltaState deltaRemovedState = removedStates.get(0); + assertEquals("AS2", deltaRemovedState.getStateId()); + assertTrue(deltaRemovedState.getIncomingDeltaActions().stream().anyMatch(a -> a.getActionId().equals("AS3"))); + assertTrue(deltaRemovedState.getOutgoingDeltaActions().stream().anyMatch(a -> a.getActionId().equals("AS4"))); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java b/statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java new file mode 100644 index 000000000..97bacd5c4 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java @@ -0,0 +1,54 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.junit.Test; +import org.testar.monkey.alayer.Tags; +import org.testar.statemodel.AbstractAction; +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.exceptions.StateModelException; + +public class StateSnapshotFactoryTest { + + @Test + public void testStateSnapshotsFactory() throws StateModelException { + AbstractAction aa1 = new AbstractAction("AA1"); + AbstractAction aa2 = new AbstractAction("AA2"); + + AbstractState as1 = new AbstractState("AS1", new HashSet<>(Arrays.asList(aa1))); + AbstractState as2 = new AbstractState("AS2", new HashSet<>(Arrays.asList(aa2))); + + as1.addConcreteStateId("CS1"); + as2.addConcreteStateId("CS2"); + + AbstractStateModel model = new AbstractStateModel("model-1", "app", "1.0", + Collections.singleton(Tags.Title)); + + // create circular transitions: AS1 -> AA1 -> AS2 -> AA2 -> AS1 + model.addTransition(as1, as2, aa1); + model.addTransition(as2, as1, aa2); + + List stateSnapshots = StateSnapshotFactory.from(model); + assertEquals(2, stateSnapshots.size()); + + StateSnapshot stateOne = stateSnapshots.stream().filter(s -> s.getStateId().equals("AS1")).findFirst().orElseThrow(); + StateSnapshot stateTwo = stateSnapshots.stream().filter(s -> s.getStateId().equals("AS2")).findFirst().orElseThrow(); + + assertEquals(Collections.singletonList("CS1"), stateOne.getConcreteStateIds()); + assertEquals(Collections.singletonList("CS2"), stateTwo.getConcreteStateIds()); + + assertTrue(stateOne.getOutgoingActionIds().contains("AA1")); + assertTrue(stateOne.getIncomingActionIds().contains("AA2")); + + assertTrue(stateTwo.getOutgoingActionIds().contains("AA2")); + assertTrue(stateTwo.getIncomingActionIds().contains("AA1")); + } + +} From 4073f6a4ed843c5bfda97c25faee566f344acdb7 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Thu, 11 Dec 2025 18:45:19 +0100 Subject: [PATCH 03/15] Add state property extractor and comparator --- .../StatePropertyComparator.java | 56 ++++++++++ .../StatePropertyExtractor.java | 65 ++++++++++++ .../StatePropertyComparatorTest.java | 100 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java b/statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java new file mode 100644 index 000000000..cd25f8e6f --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java @@ -0,0 +1,56 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Map; +import java.util.Objects; + +import org.testar.statemodel.AbstractState; + +/** + * Compares two abstract states by extracting their Tag attributes into string properties + * and delegating to {@link VertexPropertyComparator}. + */ +public class StatePropertyComparator { + + private StatePropertyComparator() { } + + public static VertexPropertyDiff compare(AbstractState oldState, AbstractState newState) { + Objects.requireNonNull(oldState, "oldState cannot be null"); + Objects.requireNonNull(newState, "newState cannot be null"); + + Map oldProps = StatePropertyExtractor.extract(oldState); + Map newProps = StatePropertyExtractor.extract(newState); + + return VertexPropertyComparator.compare(oldProps, newProps); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java b/statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java new file mode 100644 index 000000000..3abb398ad --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java @@ -0,0 +1,65 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.testar.monkey.alayer.Tag; +import org.testar.statemodel.AbstractState; + +/** + * Utility to extract comparable string properties from an {@link AbstractState}: + * - stateId + * - all attached Tag attributes converted to string values + */ +public class StatePropertyExtractor { + + private StatePropertyExtractor() { } + + public static Map extract(AbstractState state) { + Objects.requireNonNull(state, "state cannot be null"); + + Map props = new HashMap<>(); + props.put("stateId", state.getStateId()); + + for (Tag tag : state.getAttributes().tags()) { + Object value = state.getAttributes().get(tag, null); + if (value != null) { + props.put(tag.name(), value.toString()); + } + } + + return props; + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java new file mode 100644 index 000000000..37a7d89c5 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java @@ -0,0 +1,100 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; +import org.testar.monkey.alayer.Tag; +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractAction; + +public class StatePropertyComparatorTest { + + @Test + public void testUnchangedAttributes() { + AbstractAction action = new AbstractAction("AA1"); + AbstractState oldState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + AbstractState newState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + + Tag title = Tag.from("title", String.class); + + oldState.addAttribute(title, "Home"); + newState.addAttribute(title, "Home"); + + VertexPropertyDiff diff = StatePropertyComparator.compare(oldState, newState); + + assertEquals(0, diff.getChanged().size()); + assertEquals(0, diff.getAdded().size()); + assertEquals(0, diff.getRemoved().size()); + } + + @Test + public void testChangedAttributes() { + AbstractAction action = new AbstractAction("AA1"); + AbstractState oldState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + AbstractState newState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + + Tag title = Tag.from("title", String.class); + + oldState.addAttribute(title, "Old"); + newState.addAttribute(title, "New"); + + VertexPropertyDiff diff = StatePropertyComparator.compare(oldState, newState); + + assertEquals(0, diff.getAdded().size()); + assertEquals(0, diff.getRemoved().size()); + + assertEquals(1, diff.getChanged().size()); + assertEquals("title", diff.getChanged().get(0).getPropertyName()); + assertEquals("Old", diff.getChanged().get(0).getOldValue()); + assertEquals("New", diff.getChanged().get(0).getNewValue()); + } + + @Test + public void testAddedAttribute() { + AbstractAction action = new AbstractAction("AA1"); + AbstractState oldState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + AbstractState newState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + + Tag title = Tag.from("title", String.class); + Tag header = Tag.from("header", String.class); + + oldState.addAttribute(title, "Home"); + newState.addAttribute(title, "Home"); + newState.addAttribute(header, "NewHeader"); + + VertexPropertyDiff diff = StatePropertyComparator.compare(oldState, newState); + + assertEquals(0, diff.getChanged().size()); + assertEquals(0, diff.getRemoved().size()); + + assertEquals(1, diff.getAdded().size()); + assertEquals("header", diff.getAdded().get(0).getPropertyName()); + assertEquals("NewHeader", diff.getAdded().get(0).getNewValue()); + } + + @Test + public void testRemovedAttribute() { + AbstractAction action = new AbstractAction("AA1"); + AbstractState oldState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + AbstractState newState = new AbstractState("AS1", new HashSet<>(Collections.singleton(action))); + + Tag title = Tag.from("title", String.class); + Tag header = Tag.from("header", String.class); + + oldState.addAttribute(title, "Home"); + oldState.addAttribute(header, "OldHeader"); + newState.addAttribute(title, "Home"); + + VertexPropertyDiff diff = StatePropertyComparator.compare(oldState, newState); + + assertEquals(0, diff.getChanged().size()); + assertEquals(0, diff.getAdded().size()); + + assertEquals(1, diff.getRemoved().size()); + assertEquals("header", diff.getRemoved().get(0).getPropertyName()); + assertEquals("OldHeader", diff.getRemoved().get(0).getOldValue()); + } + +} From be39cb95306bc18aa709a076ab9ed336c1e08135 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Thu, 11 Dec 2025 19:24:07 +0100 Subject: [PATCH 04/15] Add change detection action descriptor provider --- .../DefaultActionDescriptionProvider.java | 43 +++++++++++ .../OrientDbActionDescriptionProvider.java | 77 +++++++++++++++++++ .../DefaultActionDescriptionProviderTest.java | 15 ++++ ...OrientDbActionDescriptionProviderTest.java | 36 +++++++++ 4 files changed, 171 insertions(+) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java new file mode 100644 index 000000000..c8ac0270d --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java @@ -0,0 +1,43 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +/** + * Fallback description provider that returns the action id when no better description is available. + */ +public class DefaultActionDescriptionProvider implements ActionDescriptionProvider { + + @Override + public String getDescription(String abstractActionId) { + return abstractActionId; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java new file mode 100644 index 000000000..725d10713 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java @@ -0,0 +1,77 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Objects; + +import org.testar.statemodel.persistence.orientdb.entity.Connection; + +import com.orientechnologies.orient.core.sql.executor.OResult; +import com.orientechnologies.orient.core.sql.executor.OResultSet; + +/** + * Connects with OrientDB to retrieve an action description (ConcreteAction) for the given actionId. + * Falls back to returning the abstract action id if no description is found. + * + * This connection class is needed because the model extractor only provides the Abstract layer. + */ +public class OrientDbActionDescriptionProvider implements ActionDescriptionProvider { + + private final Connection connection; + + public OrientDbActionDescriptionProvider(Connection connection) { + this.connection = Objects.requireNonNull(connection, "connection cannot be null"); + } + + @Override + public String getDescription(String abstractActionId) { + String description = queryConcreteActionDescription(abstractActionId); + return (description == null || description.isEmpty()) ? abstractActionId : description; + } + + /** + * Queries OrientDB for a ConcreteAction description matching the given action id. + */ + protected String queryConcreteActionDescription(String abstractActionId) { + String sql = "SELECT 'Desc' FROM ConcreteAction WHERE actionId = ?"; + try (OResultSet rs = connection.getDatabaseSession().query(sql, abstractActionId)) { + if (rs.hasNext()) { + OResult result = rs.next(); + Object desc = result.getProperty("Desc"); + return desc != null ? desc.toString() : null; + } + } catch (Exception e) { + // return null and fallback to id + } + return null; + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java new file mode 100644 index 000000000..39400c8c4 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java @@ -0,0 +1,15 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class DefaultActionDescriptionProviderTest { + + @Test + public void testDefaultActionIdAsDescription() { + DefaultActionDescriptionProvider provider = new DefaultActionDescriptionProvider(); + assertEquals("AA1", provider.getDescription("AA1")); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java new file mode 100644 index 000000000..903ea5402 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java @@ -0,0 +1,36 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.testar.statemodel.persistence.orientdb.entity.Connection; + +public class OrientDbActionDescriptionProviderTest { + + @Test + public void testObtainDescription() { + OrientDbActionDescriptionProvider provider = new OrientDbActionDescriptionProvider(createDummyConnection()) { + @Override + protected String queryConcreteActionDescription(String abstractActionId) { + return "desc-" + abstractActionId; + } + }; + assertEquals("desc-AA1", provider.getDescription("AA1")); + } + + @Test + public void testFallsBackToId() { + OrientDbActionDescriptionProvider provider = new OrientDbActionDescriptionProvider(createDummyConnection()) { + @Override + protected String queryConcreteActionDescription(String abstractActionId) { + return null; + } + }; + assertEquals("AA1", provider.getDescription("AA1")); + } + + private Connection createDummyConnection() { + return new Connection(null, null); + } + +} From 5edb9bdcd4a124ab4af36de4732c060e41b42e7c Mon Sep 17 00:00:00 2001 From: ferpasri Date: Thu, 11 Dec 2025 21:59:14 +0100 Subject: [PATCH 05/15] Add change detection engine and results --- .../changedetection/ActionSetComparator.java | 74 +++++++++ .../changedetection/ActionSetDiff.java | 86 ++++++++++ .../ChangeDetectionEngine.java | 120 ++++++++++++++ .../ChangeDetectionEngineFactory.java | 56 +++++++ .../ChangeDetectionResult.java | 131 +++++++++++++++ .../ChangeDetectionEngineFactoryTest.java | 69 ++++++++ .../ChangeDetectionEngineTest.java | 154 ++++++++++++++++++ .../ChangeDetectionResultTest.java | 67 ++++++++ 8 files changed, 757 insertions(+) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java b/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java new file mode 100644 index 000000000..6fc01a638 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java @@ -0,0 +1,74 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.testar.statemodel.changedetection.DeltaAction.Direction; + +/** + * Compares incoming/outgoing action sets between two states. + */ +public class ActionSetComparator { + + private ActionSetComparator() { } + + public static ActionSetDiff compare(StateSnapshot oldState, + StateSnapshot newState, + ActionDescriptionProvider descriptionProvider) { + Objects.requireNonNull(oldState, "oldState cannot be null"); + Objects.requireNonNull(newState, "newState cannot be null"); + Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); + + List addedIncoming = diff(newState.getIncomingActionIds(), oldState.getIncomingActionIds(), Direction.INCOMING, descriptionProvider); + List removedIncoming = diff(oldState.getIncomingActionIds(), newState.getIncomingActionIds(), Direction.INCOMING, descriptionProvider); + List addedOutgoing = diff(newState.getOutgoingActionIds(), oldState.getOutgoingActionIds(), Direction.OUTGOING, descriptionProvider); + List removedOutgoing = diff(oldState.getOutgoingActionIds(), newState.getOutgoingActionIds(), Direction.OUTGOING, descriptionProvider); + + return new ActionSetDiff(addedIncoming, removedIncoming, addedOutgoing, removedOutgoing); + } + + private static List diff(List primary, List secondary, Direction direction, ActionDescriptionProvider descriptionProvider) { + Set secondarySet = new HashSet<>(secondary); + List deltas = new ArrayList<>(); + for (String actionId : primary) { + if (!secondarySet.contains(actionId)) { + deltas.add(new DeltaAction(actionId, descriptionProvider.getDescription(actionId), direction)); + } + } + return deltas; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java b/statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java new file mode 100644 index 000000000..b70f55819 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java @@ -0,0 +1,86 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Captures added/removed incoming and outgoing actions for a state that exists in both models. + */ +public class ActionSetDiff { + + private final List addedIncoming; + private final List removedIncoming; + private final List addedOutgoing; + private final List removedOutgoing; + + public ActionSetDiff(List addedIncoming, + List removedIncoming, + List addedOutgoing, + List removedOutgoing) { + this.addedIncoming = requireNoNulls(addedIncoming, "addedIncoming"); + this.removedIncoming = requireNoNulls(removedIncoming, "removedIncoming"); + this.addedOutgoing = requireNoNulls(addedOutgoing, "addedOutgoing"); + this.removedOutgoing = requireNoNulls(removedOutgoing, "removedOutgoing"); + } + + private static List requireNoNulls(List source, String label) { + Objects.requireNonNull(source, label + " cannot be null"); + for (DeltaAction action : source) { + Objects.requireNonNull(action, label + " entry cannot be null"); + } + return source; + } + + public List getAddedIncoming() { + return Collections.unmodifiableList(addedIncoming); + } + + public List getRemovedIncoming() { + return Collections.unmodifiableList(removedIncoming); + } + + public List getAddedOutgoing() { + return Collections.unmodifiableList(addedOutgoing); + } + + public List getRemovedOutgoing() { + return Collections.unmodifiableList(removedOutgoing); + } + + public boolean isEmpty() { + return addedIncoming.isEmpty() && removedIncoming.isEmpty() + && addedOutgoing.isEmpty() && removedOutgoing.isEmpty(); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java new file mode 100644 index 000000000..429354fd7 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java @@ -0,0 +1,120 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.HashMap; +import java.util.stream.Collectors; + +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.AbstractState; + +/** + * Entry point to run change detection between two state models. + */ +public class ChangeDetectionEngine { + + private final ActionDescriptionProvider actionDescriptionProvider; + private final StateDifferenceFinder stateDifferenceFinder; + + public ChangeDetectionEngine(ActionDescriptionProvider actionDescriptionProvider, StateDifferenceFinder stateDifferenceFinder) { + this.actionDescriptionProvider = Objects.requireNonNull(actionDescriptionProvider, "actionDescriptionProvider cannot be null"); + this.stateDifferenceFinder = Objects.requireNonNull(stateDifferenceFinder, "stateDifferenceFinder cannot be null"); + } + + public ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { + Objects.requireNonNull(oldModel, "oldModel cannot be null"); + Objects.requireNonNull(newModel, "newModel cannot be null"); + + List oldStateSnapshots = StateSnapshotFactory.from(oldModel); + List newStateSnapshots = StateSnapshotFactory.from(newModel); + + List addedStates = stateDifferenceFinder.findAddedStates(oldStateSnapshots, newStateSnapshots, actionDescriptionProvider); + List removedStates = stateDifferenceFinder.findRemovedStates(oldStateSnapshots, newStateSnapshots, actionDescriptionProvider); + Map changedStates = computeChangedStates(oldModel, newModel); + Map changedActions = computeChangedActions(oldStateSnapshots, newStateSnapshots); + + return new ChangeDetectionResult(oldModel.getModelIdentifier(), newModel.getModelIdentifier(), addedStates, removedStates, changedStates, changedActions); + } + + private Map computeChangedStates(AbstractStateModel oldModel, AbstractStateModel newModel) { + Map changed = new HashMap<>(); + + Set oldIds = oldModel.getStates().stream().map(AbstractState::getStateId).collect(Collectors.toSet()); + for (String stateId : oldIds) { + if (newModel.containsState(stateId)) { + VertexPropertyDiff diff = StatePropertyComparator.compare( + getState(oldModel, stateId), + getState(newModel, stateId)); + if (!diff.isEmpty()) { + changed.put(stateId, diff); + } + } + } + return changed; + } + + private static AbstractState getState(AbstractStateModel model, String stateId) { + try { + return model.getState(stateId); + } catch (Exception ex) { + throw new IllegalStateException("State with id " + stateId + " not found in model " + model.getModelIdentifier(), ex); + } + } + + private Map computeChangedActions(List oldStateSnapshots, List newStateSnapshots) { + Map oldStatesById = indexById(oldStateSnapshots); + Map newStatesById = indexById(newStateSnapshots); + + Map diffs = new HashMap<>(); + for (String stateId : oldStatesById.keySet()) { + if (newStatesById.containsKey(stateId)) { + ActionSetDiff diff = ActionSetComparator.compare(oldStatesById.get(stateId), newStatesById.get(stateId), actionDescriptionProvider); + if (!diff.isEmpty()) { + diffs.put(stateId, diff); + } + } + } + return diffs; + } + + private static Map indexById(List stateSnapshots) { + Map map = new HashMap<>(); + for (StateSnapshot state : stateSnapshots) { + map.put(state.getStateId(), state); + } + return map; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java new file mode 100644 index 000000000..841996b71 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java @@ -0,0 +1,56 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.Objects; + +import org.testar.statemodel.persistence.orientdb.entity.Connection; +import org.testar.statemodel.persistence.PersistenceManager; + +public class ChangeDetectionEngineFactory { + + private ChangeDetectionEngineFactory() { } + + public static ChangeDetectionEngine createWithDefaultDescription() { + return new ChangeDetectionEngine(new DefaultActionDescriptionProvider(), new StateDifferenceFinder()); + } + + public static ChangeDetectionEngine createWithOrientDb(Connection connection) { + Objects.requireNonNull(connection, "connection cannot be null"); + return new ChangeDetectionEngine(new OrientDbActionDescriptionProvider(connection), new StateDifferenceFinder()); + } + + public static ChangeDetectionEngine createWithPersistence(PersistenceManager persistenceManager) { + Objects.requireNonNull(persistenceManager, "persistenceManager cannot be null"); + return createWithOrientDb(persistenceManager.getEntityManager().getConnection()); + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java new file mode 100644 index 000000000..24b22c80c --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java @@ -0,0 +1,131 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Objects; + +/** + * Aggregated result for a change detection run between two state models. + */ +public class ChangeDetectionResult { + + private final String oldModelIdentifier; + private final String newModelIdentifier; + private final List addedStates; + private final List removedStates; + private final Map changedStates; + private final Map changedActions; + + public ChangeDetectionResult(String oldModelIdentifier, + String newModelIdentifier, + List addedStates, + List removedStates, + Map changedStates, + Map changedActions) { + this.oldModelIdentifier = Objects.requireNonNull(oldModelIdentifier, "oldModelIdentifier cannot be null"); + this.newModelIdentifier = Objects.requireNonNull(newModelIdentifier, "newModelIdentifier cannot be null"); + if (oldModelIdentifier.trim().isEmpty() || newModelIdentifier.trim().isEmpty()) { + throw new IllegalArgumentException("model identifiers cannot be empty or blank"); + } + this.addedStates = copyStates(addedStates); + this.removedStates = copyStates(removedStates); + this.changedStates = copyChanged(changedStates); + this.changedActions = copyChangedActions(changedActions); + } + + private static List copyStates(List source) { + List copy = new ArrayList<>(); + if (source != null) { + for (DeltaState state : source) { + copy.add(Objects.requireNonNull(state, "delta state cannot be null")); + } + } + return copy; + } + + private static Map copyChanged(Map source) { + Map copy = new HashMap<>(); + if (source != null) { + for (Map.Entry entry : source.entrySet()) { + Objects.requireNonNull(entry.getKey(), "changed state id cannot be null"); + Objects.requireNonNull(entry.getValue(), "changed diff cannot be null"); + copy.put(entry.getKey(), entry.getValue()); + } + } + return copy; + } + + private static Map copyChangedActions(Map source) { + Map copy = new HashMap<>(); + if (source != null) { + for (Map.Entry entry : source.entrySet()) { + Objects.requireNonNull(entry.getKey(), "changed actions state id cannot be null"); + Objects.requireNonNull(entry.getValue(), "changed actions diff cannot be null"); + copy.put(entry.getKey(), entry.getValue()); + } + } + return copy; + } + + public String getOldModelIdentifier() { + return oldModelIdentifier; + } + + public String getNewModelIdentifier() { + return newModelIdentifier; + } + + public List getAddedStates() { + return Collections.unmodifiableList(addedStates); + } + + public List getRemovedStates() { + return Collections.unmodifiableList(removedStates); + } + + public Map getChangedStates() { + return Collections.unmodifiableMap(changedStates); + } + + public Map getChangedActions() { + return Collections.unmodifiableMap(changedActions); + } + + public boolean hasDifferences() { + return !addedStates.isEmpty() || !removedStates.isEmpty() || !changedStates.isEmpty() || !changedActions.isEmpty(); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java new file mode 100644 index 000000000..15105a2a9 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java @@ -0,0 +1,69 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; +import org.testar.monkey.alayer.Tags; +import org.testar.statemodel.AbstractAction; +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.exceptions.StateModelException; + +public class ChangeDetectionEngineFactoryTest { + + @Test + public void testEngineFactoryWithDefaultProvider() throws StateModelException { + ChangeDetectionEngine engine = ChangeDetectionEngineFactory.createWithDefaultDescription(); + + AbstractStateModel oldModel = createModel("old", new String[]{"AS1"}, new String[][]{}, new String[][]{}); + AbstractStateModel newModel = createModel("new", new String[]{"AS1", "AS2"}, new String[][]{}, new String[][]{}); + + ChangeDetectionResult result = engine.compare(oldModel, newModel); + assertEquals(1, result.getAddedStates().size()); + assertTrue(result.getChangedActions().isEmpty()); + } + + private AbstractStateModel createModel(String modelId, + String[] stateIds, + String[][] transitions, + String[][] concreteIdsPerState + ) throws StateModelException { + + AbstractStateModel model = new AbstractStateModel( + modelId, + "app", + "1.0", + Collections.singleton(Tags.Title) + ); + + // create states with no actions; + for (String s : stateIds) { + model.addState(new AbstractState(s, new HashSet<>())); + } + + // add action transitions as in/out sets + for (String[] t : transitions) { + String source = t[0]; + String actionId = t[1]; + String target = t[2]; + AbstractAction action = new AbstractAction(actionId); + model.addTransition(model.getState(source), model.getState(target), action); + } + + // add concrete ids + for (String[] entry : concreteIdsPerState) { + String stateId = entry[0]; + AbstractState state = model.getState(stateId); + for (int i = 1; i < entry.length; i++) { + state.addConcreteStateId(entry[i]); + } + } + + return model; + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java new file mode 100644 index 000000000..b7d8e49d1 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java @@ -0,0 +1,154 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Before; +import org.junit.Test; +import org.testar.monkey.alayer.Tag; +import org.testar.monkey.alayer.Tags; +import org.testar.statemodel.AbstractAction; +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.exceptions.StateModelException; + +public class ChangeDetectionEngineTest { + + private ChangeDetectionEngine engine; + + @Before + public void setUp() { + ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + engine = new ChangeDetectionEngine(descriptionProvider, new StateDifferenceFinder()); + } + + @Test + public void testDetectsAddedState() throws StateModelException { + AbstractStateModel oldModel = createModel("old-id", + new String[]{"AS1"}, new String[][]{}, new String[][]{}); + + AbstractStateModel newModel = createModel("new-id", + new String[]{"AS1", "AS2"}, new String[][]{}, new String[][]{}); + + ChangeDetectionResult result = engine.compare(oldModel, newModel); + + assertEquals("old-id", result.getOldModelIdentifier()); + assertEquals("new-id", result.getNewModelIdentifier()); + assertEquals(1, result.getAddedStates().size()); + assertEquals("AS2", result.getAddedStates().get(0).getStateId()); + + assertEquals(0, result.getRemovedStates().size()); + + assertTrue(result.getChangedStates().isEmpty()); + assertTrue(result.getChangedActions().isEmpty()); + + assertTrue(result.hasDifferences()); + } + + @Test + public void testDetectsRemovedState() throws StateModelException { + AbstractStateModel oldModel = createModel("old-id", + new String[]{"AS1", "AS2"}, new String[][]{}, new String[][]{}); + + AbstractStateModel newModel = createModel("new-id", + new String[]{"AS2"}, new String[][]{}, new String[][]{}); + + ChangeDetectionResult result = engine.compare(oldModel, newModel); + + assertEquals("old-id", result.getOldModelIdentifier()); + assertEquals("new-id", result.getNewModelIdentifier()); + assertEquals(1, result.getRemovedStates().size()); + assertEquals("AS1", result.getRemovedStates().get(0).getStateId()); + + assertEquals(0, result.getAddedStates().size()); + + assertTrue(result.getChangedStates().isEmpty()); + assertTrue(result.getChangedActions().isEmpty()); + + assertTrue(result.hasDifferences()); + } + + @Test + public void testDetectsChangedState() throws StateModelException { + AbstractStateModel oldModel = createModel("old-id", + new String[]{"AS1"}, new String[][]{}, new String[][]{}); + AbstractStateModel newModel = createModel("new-id", + new String[]{"AS1"}, new String[][]{}, new String[][]{}); + + Tag title = Tag.from("title", String.class); + oldModel.getState("AS1").addAttribute(title, "Old"); + newModel.getState("AS1").addAttribute(title, "New"); + + ChangeDetectionResult result = engine.compare(oldModel, newModel); + + assertEquals(1, result.getChangedStates().size()); + assertTrue(result.getChangedStates().containsKey("AS1")); + + VertexPropertyDiff diff = result.getChangedStates().get("AS1"); + assertEquals(1, diff.getChanged().size()); + assertEquals("title", diff.getChanged().get(0).getPropertyName()); + assertEquals("Old", diff.getChanged().get(0).getOldValue()); + assertEquals("New", diff.getChanged().get(0).getNewValue()); + } + + @Test + public void testDetectsChangedAction() throws StateModelException { + AbstractStateModel oldModel = createModel("old-id", + new String[]{"AS1"}, new String[][]{{"AS1", "AA1", "AS1"}}, new String[][]{}); + AbstractStateModel newModel = createModel("new-id", + new String[]{"AS1"}, new String[][]{{"AS1", "AA2", "AS1"}}, new String[][]{}); + + ChangeDetectionResult result = engine.compare(oldModel, newModel); + + assertEquals(1, result.getChangedActions().size()); + + ActionSetDiff diff = result.getChangedActions().get("AS1"); + assertEquals(1, diff.getAddedOutgoing().size()); + assertEquals("AA2", diff.getAddedOutgoing().get(0).getActionId()); + assertEquals(1, diff.getRemovedOutgoing().size()); + assertEquals("AA1", diff.getRemovedOutgoing().get(0).getActionId()); + } + + private AbstractStateModel createModel(String modelId, + String[] stateIds, + String[][] transitions, + String[][] concreteIdsPerState + ) throws StateModelException { + + AbstractStateModel model = new AbstractStateModel( + modelId, + "app", + "1.0", + Collections.singleton(Tags.Title) + ); + + // create states with no actions; + for (String s : stateIds) { + model.addState(new AbstractState(s, new HashSet<>())); + } + + // add action transitions as in/out sets + for (String[] t : transitions) { + String source = t[0]; + String actionId = t[1]; + String target = t[2]; + AbstractAction action = new AbstractAction(actionId); + model.addTransition(model.getState(source), model.getState(target), action); + } + + // add concrete ids + for (String[] entry : concreteIdsPerState) { + String stateId = entry[0]; + AbstractState state = model.getState(stateId); + for (int i = 1; i < entry.length; i++) { + state.addConcreteStateId(entry[i]); + } + } + + return model; + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java new file mode 100644 index 000000000..4e9d0e92e --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java @@ -0,0 +1,67 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; + +import org.junit.Test; +import org.testar.statemodel.changedetection.DeltaAction.Direction; + +public class ChangeDetectionResultTest { + + @Test + public void testChangedResultsOfAddedRemovedStates() { + DeltaState addedState = new DeltaState("AS1", Collections.singletonList("CS1"), + Collections.singletonList(new DeltaAction("AA1", "desc-AA1", Direction.INCOMING)), + Collections.emptyList()); + + DeltaState removedState = new DeltaState("AS2", Collections.singletonList("CS2"), + Collections.singletonList(new DeltaAction("AA2", "desc-AA2", Direction.INCOMING)), + Collections.emptyList()); + + ChangeDetectionResult result = new ChangeDetectionResult("old", "new", + Collections.singletonList(addedState), + Collections.singletonList(removedState), + Collections.emptyMap(), + Collections.emptyMap()); + + assertEquals("old", result.getOldModelIdentifier()); + assertEquals("new", result.getNewModelIdentifier()); + assertEquals(1, result.getAddedStates().size()); + assertEquals(1, result.getRemovedStates().size()); + assertTrue(result.hasDifferences()); + } + + @Test + public void testNoDifferencesDetected() { + ChangeDetectionResult result = new ChangeDetectionResult("old", "new", + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyMap(), + Collections.emptyMap()); + assertFalse(result.hasDifferences()); + } + + @Test(expected = NullPointerException.class) + public void testNullOldModelIdentifier() { + new ChangeDetectionResult(null, "new", Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); + } + + @Test(expected = NullPointerException.class) + public void testNullNewModelIdentifier() { + new ChangeDetectionResult("old", null, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyOldModelIdentifier() { + new ChangeDetectionResult(" ", "new", Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyNewModelIdentifier() { + new ChangeDetectionResult("old", " ", Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap()); + } + +} From 793316eff65eb1c55d2585ce03e9b34892517cac Mon Sep 17 00:00:00 2001 From: ferpasri Date: Fri, 12 Dec 2025 17:29:54 +0100 Subject: [PATCH 06/15] Change StateDifference to GraphTraversalComparator --- .../ChangeDetectionEngine.java | 71 +--- .../ChangeDetectionEngineFactory.java | 4 +- .../GraphTraversalComparator.java | 347 ++++++++++++++++++ .../OrientDbActionDescriptionProvider.java | 69 +++- .../StateDifferenceFinder.java | 114 ------ .../ChangeDetectionEngineFactoryTest.java | 69 ---- .../ChangeDetectionEngineTest.java | 150 ++------ .../GraphTraversalComparatorTest.java | 154 ++++++++ .../StateDifferenceFinderTest.java | 86 ----- 9 files changed, 592 insertions(+), 472 deletions(-) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java delete mode 100644 statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java delete mode 100644 statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java delete mode 100644 statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java index 429354fd7..1f16276f5 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java @@ -30,15 +30,9 @@ package org.testar.statemodel.changedetection; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.HashMap; -import java.util.stream.Collectors; import org.testar.statemodel.AbstractStateModel; -import org.testar.statemodel.AbstractState; /** * Entry point to run change detection between two state models. @@ -46,75 +40,18 @@ public class ChangeDetectionEngine { private final ActionDescriptionProvider actionDescriptionProvider; - private final StateDifferenceFinder stateDifferenceFinder; + private final GraphTraversalComparator comparator; - public ChangeDetectionEngine(ActionDescriptionProvider actionDescriptionProvider, StateDifferenceFinder stateDifferenceFinder) { + public ChangeDetectionEngine(ActionDescriptionProvider actionDescriptionProvider) { this.actionDescriptionProvider = Objects.requireNonNull(actionDescriptionProvider, "actionDescriptionProvider cannot be null"); - this.stateDifferenceFinder = Objects.requireNonNull(stateDifferenceFinder, "stateDifferenceFinder cannot be null"); + this.comparator = new GraphTraversalComparator(this.actionDescriptionProvider); } public ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { Objects.requireNonNull(oldModel, "oldModel cannot be null"); Objects.requireNonNull(newModel, "newModel cannot be null"); - List oldStateSnapshots = StateSnapshotFactory.from(oldModel); - List newStateSnapshots = StateSnapshotFactory.from(newModel); - - List addedStates = stateDifferenceFinder.findAddedStates(oldStateSnapshots, newStateSnapshots, actionDescriptionProvider); - List removedStates = stateDifferenceFinder.findRemovedStates(oldStateSnapshots, newStateSnapshots, actionDescriptionProvider); - Map changedStates = computeChangedStates(oldModel, newModel); - Map changedActions = computeChangedActions(oldStateSnapshots, newStateSnapshots); - - return new ChangeDetectionResult(oldModel.getModelIdentifier(), newModel.getModelIdentifier(), addedStates, removedStates, changedStates, changedActions); - } - - private Map computeChangedStates(AbstractStateModel oldModel, AbstractStateModel newModel) { - Map changed = new HashMap<>(); - - Set oldIds = oldModel.getStates().stream().map(AbstractState::getStateId).collect(Collectors.toSet()); - for (String stateId : oldIds) { - if (newModel.containsState(stateId)) { - VertexPropertyDiff diff = StatePropertyComparator.compare( - getState(oldModel, stateId), - getState(newModel, stateId)); - if (!diff.isEmpty()) { - changed.put(stateId, diff); - } - } - } - return changed; - } - - private static AbstractState getState(AbstractStateModel model, String stateId) { - try { - return model.getState(stateId); - } catch (Exception ex) { - throw new IllegalStateException("State with id " + stateId + " not found in model " + model.getModelIdentifier(), ex); - } - } - - private Map computeChangedActions(List oldStateSnapshots, List newStateSnapshots) { - Map oldStatesById = indexById(oldStateSnapshots); - Map newStatesById = indexById(newStateSnapshots); - - Map diffs = new HashMap<>(); - for (String stateId : oldStatesById.keySet()) { - if (newStatesById.containsKey(stateId)) { - ActionSetDiff diff = ActionSetComparator.compare(oldStatesById.get(stateId), newStatesById.get(stateId), actionDescriptionProvider); - if (!diff.isEmpty()) { - diffs.put(stateId, diff); - } - } - } - return diffs; - } - - private static Map indexById(List stateSnapshots) { - Map map = new HashMap<>(); - for (StateSnapshot state : stateSnapshots) { - map.put(state.getStateId(), state); - } - return map; + return comparator.compare(oldModel, newModel); } } diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java index 841996b71..cfcbed2ec 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java @@ -40,12 +40,12 @@ public class ChangeDetectionEngineFactory { private ChangeDetectionEngineFactory() { } public static ChangeDetectionEngine createWithDefaultDescription() { - return new ChangeDetectionEngine(new DefaultActionDescriptionProvider(), new StateDifferenceFinder()); + return new ChangeDetectionEngine(new DefaultActionDescriptionProvider()); } public static ChangeDetectionEngine createWithOrientDb(Connection connection) { Objects.requireNonNull(connection, "connection cannot be null"); - return new ChangeDetectionEngine(new OrientDbActionDescriptionProvider(connection), new StateDifferenceFinder()); + return new ChangeDetectionEngine(new OrientDbActionDescriptionProvider(connection)); } public static ChangeDetectionEngine createWithPersistence(PersistenceManager persistenceManager) { diff --git a/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java b/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java new file mode 100644 index 000000000..c37b2f575 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java @@ -0,0 +1,347 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection; + +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.AbstractStateTransition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Traversal-based state models comparator. + * Connects to the initial abstract state and uses the action descriptions as the matching key (desc or action id), + * and tracks handled states/actions to avoid double counting. + */ +class GraphTraversalComparator { + + private final ActionDescriptionProvider descriptionProvider; + + GraphTraversalComparator(ActionDescriptionProvider descriptionProvider) { + this.descriptionProvider = Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); + } + + ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { + Graph oldGraph = buildGraph(oldModel); + Graph newGraph = buildGraph(newModel); + + TraversalContext ctx = new TraversalContext(oldGraph, newGraph); + + Node oldStart = chooseInitialState(oldGraph); + Node newStart = chooseInitialState(newGraph); + + if (oldStart != null && newStart != null) { + compareStates(oldStart, newStart, ctx); + } + + // Any remaining unmapped states/actions are added/removed + List addedStates = computeAddedStates(ctx); + List removedStates = computeRemovedStates(ctx); + + Map changedActions = computeChangedActions(ctx); + Map changedStates = ctx.changedStates; + + return new ChangeDetectionResult( + oldModel.getModelIdentifier(), + newModel.getModelIdentifier(), + addedStates, + removedStates, + changedStates, + changedActions + ); + } + + private void compareStates(Node oldNode, Node newNode, TraversalContext ctx) { + if (ctx.isMapped(oldNode.id, newNode.id)) { + return; + } + + ctx.mapStates(oldNode.id, newNode.id); + oldNode.handled = true; + newNode.handled = true; + + VertexPropertyDiff propDiff = StatePropertyComparator.compare(oldNode.state, newNode.state); + if (!propDiff.isEmpty()) { + ctx.changedStates.put(newNode.id, propDiff); + } + + // Match outgoing actions by comparable key (description preferred) + for (Edge newEdge : newNode.outgoing) { + if (newEdge.handled) { + continue; + } + Edge match = ctx.findMatchingOutgoing(oldNode, newEdge.comparableKey); + if (match == null) { + ctx.addedEdges.add(newEdge); + } else { + match.handled = true; + newEdge.handled = true; + ctx.matchedEdges.add(new EdgePair(match, newEdge)); + + Node oldTarget = ctx.oldGraph.nodes.get(match.targetId); + Node newTarget = ctx.newGraph.nodes.get(newEdge.targetId); + + if (oldTarget != null && newTarget != null) { + // if mapping already exists but points elsewhere, skip recursion + if (!ctx.hasConflictingMapping(oldTarget.id, newTarget.id) && !oldTarget.handled && !newTarget.handled) { + compareStates(oldTarget, newTarget, ctx); + } + } + } + } + + // Remaining unhandled outgoing actions on old node are removed + for (Edge oldEdge : oldNode.outgoing) { + if (!oldEdge.handled) { + ctx.removedEdges.add(oldEdge); + } + } + } + + private List computeAddedStates(TraversalContext ctx) { + List added = new ArrayList<>(); + for (Node newNode : ctx.newGraph.nodes.values()) { + if (!ctx.newToOld.containsKey(newNode.id)) { + added.add(toDeltaState(newNode, ctx.newGraph)); + } + } + return added; + } + + private List computeRemovedStates(TraversalContext ctx) { + List removed = new ArrayList<>(); + for (Node oldNode : ctx.oldGraph.nodes.values()) { + if (!ctx.oldToNew.containsKey(oldNode.id)) { + removed.add(toDeltaState(oldNode, ctx.oldGraph)); + } + } + return removed; + } + + private Map computeChangedActions(TraversalContext ctx) { + Map diffByNewState = new HashMap<>(); + + for (Edge added : ctx.addedEdges) { + // source outgoing + MutableActionDiff sourceDiff = diffByNewState.computeIfAbsent(added.sourceId, k -> new MutableActionDiff()); + sourceDiff.addOutgoing.add(new DeltaAction(added.actionId, added.description, DeltaAction.Direction.OUTGOING)); + // target incoming + MutableActionDiff targetDiff = diffByNewState.computeIfAbsent(added.targetId, k -> new MutableActionDiff()); + targetDiff.addIncoming.add(new DeltaAction(added.actionId, added.description, DeltaAction.Direction.INCOMING)); + // mark state as changed if not already present + ctx.changedStates.putIfAbsent(added.sourceId, new VertexPropertyDiff(new ArrayList<>(), new ArrayList<>(), new ArrayList<>())); + } + + for (Edge removed : ctx.removedEdges) { + String mappedSource = ctx.oldToNew.get(removed.sourceId); + String mappedTarget = ctx.oldToNew.get(removed.targetId); + if (mappedSource != null) { + MutableActionDiff sourceDiff = diffByNewState.computeIfAbsent(mappedSource, k -> new MutableActionDiff()); + sourceDiff.remOutgoing.add(new DeltaAction(removed.actionId, removed.description, DeltaAction.Direction.OUTGOING)); + ctx.changedStates.putIfAbsent(mappedSource, new VertexPropertyDiff(new ArrayList<>(), new ArrayList<>(), new ArrayList<>())); + } + if (mappedTarget != null) { + MutableActionDiff targetDiff = diffByNewState.computeIfAbsent(mappedTarget, k -> new MutableActionDiff()); + targetDiff.remIncoming.add(new DeltaAction(removed.actionId, removed.description, DeltaAction.Direction.INCOMING)); + ctx.changedStates.putIfAbsent(mappedTarget, new VertexPropertyDiff(new ArrayList<>(), new ArrayList<>(), new ArrayList<>())); + } + } + + Map result = new HashMap<>(); + for (Map.Entry entry : diffByNewState.entrySet()) { + MutableActionDiff d = entry.getValue(); + result.put(entry.getKey(), new ActionSetDiff(d.addIncoming, d.remIncoming, d.addOutgoing, d.remOutgoing)); + } + return result; + } + + private DeltaState toDeltaState(Node node, Graph graph) { + List incoming = node.incoming.stream() + .map(e -> new DeltaAction(e.actionId, e.description, DeltaAction.Direction.INCOMING)) + .collect(Collectors.toList()); + List outgoing = node.outgoing.stream() + .map(e -> new DeltaAction(e.actionId, e.description, DeltaAction.Direction.OUTGOING)) + .collect(Collectors.toList()); + return new DeltaState(node.id, new ArrayList<>(node.concreteIds), incoming, outgoing); + } + + private Graph buildGraph(AbstractStateModel model) { + Map nodes = new HashMap<>(); + List edges = new ArrayList<>(); + + for (AbstractState s : model.getStates()) { + nodes.put(s.getStateId(), new Node(s)); + } + + for (Node n : nodes.values()) { + Collection outgoing = model.getOutgoingTransitionsForState(n.id); + for (AbstractStateTransition t : outgoing) { + Edge e = toEdge(t, nodes); + n.outgoing.add(e); + nodes.get(e.targetId).incoming.add(e); + edges.add(e); + } + } + + return new Graph(nodes, edges); + } + + private Edge toEdge(AbstractStateTransition t, Map nodes) { + String actionId = t.getActionId(); + String desc = descriptionProvider.getDescription(actionId); + String key = comparableKey(actionId, desc); + return new Edge(t.getSourceStateId(), t.getTargetStateId(), actionId, desc, key); + } + + private String comparableKey(String actionId, String description) { + if (description == null || description.trim().isEmpty() || description.contains("at ''")) { + return actionId; + } + return description; + } + + private Node chooseInitialState(Graph graph) { + for (Node n : graph.nodes.values()) { + if (n.initial) { + return n; + } + } + return null; + } + + private static final class TraversalContext { + final Graph oldGraph; + final Graph newGraph; + final Map oldToNew = new HashMap<>(); + final Map newToOld = new HashMap<>(); + final Map changedStates = new HashMap<>(); + final List addedEdges = new ArrayList<>(); + final List removedEdges = new ArrayList<>(); + final List matchedEdges = new ArrayList<>(); + + TraversalContext(Graph oldGraph, Graph newGraph) { + this.oldGraph = oldGraph; + this.newGraph = newGraph; + } + + void mapStates(String oldId, String newId) { + oldToNew.put(oldId, newId); + newToOld.put(newId, oldId); + } + + boolean isMapped(String oldId, String newId) { + return newId.equals(oldToNew.get(oldId)) || oldId.equals(newToOld.get(newId)); + } + + boolean hasConflictingMapping(String oldId, String newId) { + String mapped = oldToNew.get(oldId); + return mapped != null && !mapped.equals(newId); + } + + Edge findMatchingOutgoing(Node oldNode, String comparableKey) { + for (Edge e : oldNode.outgoing) { + if (!e.handled && e.comparableKey.equals(comparableKey)) { + return e; + } + } + return null; + } + } + + private static final class Graph { + final Map nodes; + final List edges; + + Graph(Map nodes, List edges) { + this.nodes = nodes; + this.edges = edges; + } + } + + private static final class Node { + final String id; + final AbstractState state; + final boolean initial; + final Set concreteIds; + final List outgoing = new ArrayList<>(); + final List incoming = new ArrayList<>(); + boolean handled = false; + + Node(AbstractState state) { + this.state = state; + this.id = state.getStateId(); + this.initial = state.isInitial(); + this.concreteIds = new HashSet<>(state.getConcreteStateIds()); + } + } + + private static final class Edge { + final String sourceId; + final String targetId; + final String actionId; + final String description; + final String comparableKey; + boolean handled = false; + + Edge(String sourceId, String targetId, String actionId, String description, String comparableKey) { + this.sourceId = sourceId; + this.targetId = targetId; + this.actionId = actionId; + this.description = description; + this.comparableKey = comparableKey; + } + } + + private static final class EdgePair { + final Edge oldEdge; + final Edge newEdge; + + EdgePair(Edge oldEdge, Edge newEdge) { + this.oldEdge = oldEdge; + this.newEdge = newEdge; + } + } + + private static final class MutableActionDiff { + final List addIncoming = new ArrayList<>(); + final List remIncoming = new ArrayList<>(); + final List addOutgoing = new ArrayList<>(); + final List remOutgoing = new ArrayList<>(); + } +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java index 725d10713..0a3f0f621 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java @@ -30,7 +30,9 @@ package org.testar.statemodel.changedetection; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import org.testar.statemodel.persistence.orientdb.entity.Connection; @@ -46,6 +48,7 @@ public class OrientDbActionDescriptionProvider implements ActionDescriptionProvider { private final Connection connection; + private final Map cache = new ConcurrentHashMap<>(); public OrientDbActionDescriptionProvider(Connection connection) { this.connection = Objects.requireNonNull(connection, "connection cannot be null"); @@ -53,25 +56,81 @@ public OrientDbActionDescriptionProvider(Connection connection) { @Override public String getDescription(String abstractActionId) { + String cached = cache.get(abstractActionId); + if (cached != null) { + return cached; + } String description = queryConcreteActionDescription(abstractActionId); - return (description == null || description.isEmpty()) ? abstractActionId : description; + String resolved = (description == null || description.isEmpty()) ? abstractActionId : description; + cache.put(abstractActionId, resolved); + return resolved; } /** - * Queries OrientDB for a ConcreteAction description matching the given action id. + * Queries OrientDB for a ConcreteAction description matching the given abstract action id. */ protected String queryConcreteActionDescription(String abstractActionId) { - String sql = "SELECT 'Desc' FROM ConcreteAction WHERE actionId = ?"; + try { + String desc = fetchFirstConcreteDescription(abstractActionId); + return desc; + } catch (Exception e) { + return null; + } + } + + private String fetchFirstConcreteDescription(String abstractActionId) { + for (String concreteId : fetchConcreteActionIds(abstractActionId)) { + String desc = fetchConcreteDescription(concreteId); + if (desc != null && !desc.isEmpty()) { + return desc; + } + } + return null; + } + + private java.util.List fetchConcreteActionIds(String abstractActionId) { + java.util.List idsList = new java.util.ArrayList<>(); + String sql = "SELECT concreteActionIds FROM AbstractAction WHERE actionId = ? LIMIT 1"; try (OResultSet rs = connection.getDatabaseSession().query(sql, abstractActionId)) { + if (rs.hasNext()) { + OResult result = rs.next(); + Object ids = result.getProperty("concreteActionIds"); + collectIds(idsList, ids); + } + } + return idsList; + } + + private String fetchConcreteDescription(String concreteActionId) { + String sql = "SELECT `Desc` FROM ConcreteAction WHERE actionId = ? LIMIT 1"; + try (OResultSet rs = connection.getDatabaseSession().query(sql, concreteActionId)) { if (rs.hasNext()) { OResult result = rs.next(); Object desc = result.getProperty("Desc"); return desc != null ? desc.toString() : null; } - } catch (Exception e) { - // return null and fallback to id } return null; } + private void collectIds(java.util.List target, Object ids) { + if (ids == null) { + return; + } + if (ids instanceof Iterable) { + for (Object id : (Iterable) ids) { + if (id != null) { + target.add(id.toString()); + } + } + } else { + String raw = ids.toString(); + if (raw.startsWith("[") && raw.endsWith("]") && raw.length() > 2) { + target.add(raw.substring(1, raw.length() - 1)); + } else { + target.add(raw); + } + } + } + } diff --git a/statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java b/statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java deleted file mode 100644 index 89bf18167..000000000 --- a/statemodel/src/org/testar/statemodel/changedetection/StateDifferenceFinder.java +++ /dev/null @@ -1,114 +0,0 @@ -/*************************************************************************************************** - * - * Copyright (c) 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *******************************************************************************************************/ - -package org.testar.statemodel.changedetection; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import org.testar.statemodel.changedetection.DeltaAction.Direction; - -/** - * Computes added/removed state deltas between two model snapshots. - */ -public class StateDifferenceFinder { - - /** - * Find states present only in {@code newModelStates} (i.e. newly added). - */ - public List findAddedStates(List oldModelStates, - List newModelStates, - ActionDescriptionProvider descriptionProvider) { - Objects.requireNonNull(oldModelStates, "oldModelStates cannot be null"); - Objects.requireNonNull(newModelStates, "newModelStates cannot be null"); - Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); - - Set oldModelIds = oldModelStates.stream().map(StateSnapshot::getStateId).collect(Collectors.toSet()); - Map newModelById = indexById(newModelStates); - - List result = new ArrayList<>(); - for (String stateId : newModelById.keySet()) { - if (!oldModelIds.contains(stateId)) { - result.add(toDeltaState(newModelById.get(stateId), descriptionProvider)); - } - } - return result; - } - - /** - * Find states present only in {@code oldModelStates} (i.e. removed). - */ - public List findRemovedStates(List oldModelStates, - List newModelStates, - ActionDescriptionProvider descriptionProvider) { - Objects.requireNonNull(oldModelStates, "oldModelStates cannot be null"); - Objects.requireNonNull(newModelStates, "newModelStates cannot be null"); - Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); - - Set newModelIds = newModelStates.stream().map(StateSnapshot::getStateId).collect(Collectors.toSet()); - Map oldModelById = indexById(oldModelStates); - - List result = new ArrayList<>(); - for (String stateId : oldModelById.keySet()) { - if (!newModelIds.contains(stateId)) { - result.add(toDeltaState(oldModelById.get(stateId), descriptionProvider)); - } - } - return result; - } - - private static Map indexById(List stateSnapshots) { - Map map = new HashMap<>(); - for (StateSnapshot state : stateSnapshots) { - map.put(state.getStateId(), state); - } - return map; - } - - private static DeltaState toDeltaState(StateSnapshot stateSnapshot, ActionDescriptionProvider descriptionProvider) { - List incoming = stateSnapshot.getIncomingActionIds().stream() - .map(id -> new DeltaAction(id, descriptionProvider.getDescription(id), Direction.INCOMING)) - .collect(Collectors.toList()); - List outgoing = stateSnapshot.getOutgoingActionIds().stream() - .map(id -> new DeltaAction(id, descriptionProvider.getDescription(id), Direction.OUTGOING)) - .collect(Collectors.toList()); - - return new DeltaState(stateSnapshot.getStateId(), - new ArrayList<>(stateSnapshot.getConcreteStateIds()), - incoming, - outgoing); - } - -} diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java deleted file mode 100644 index 15105a2a9..000000000 --- a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineFactoryTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.testar.statemodel.changedetection; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Collections; -import java.util.HashSet; - -import org.junit.Test; -import org.testar.monkey.alayer.Tags; -import org.testar.statemodel.AbstractAction; -import org.testar.statemodel.AbstractState; -import org.testar.statemodel.AbstractStateModel; -import org.testar.statemodel.exceptions.StateModelException; - -public class ChangeDetectionEngineFactoryTest { - - @Test - public void testEngineFactoryWithDefaultProvider() throws StateModelException { - ChangeDetectionEngine engine = ChangeDetectionEngineFactory.createWithDefaultDescription(); - - AbstractStateModel oldModel = createModel("old", new String[]{"AS1"}, new String[][]{}, new String[][]{}); - AbstractStateModel newModel = createModel("new", new String[]{"AS1", "AS2"}, new String[][]{}, new String[][]{}); - - ChangeDetectionResult result = engine.compare(oldModel, newModel); - assertEquals(1, result.getAddedStates().size()); - assertTrue(result.getChangedActions().isEmpty()); - } - - private AbstractStateModel createModel(String modelId, - String[] stateIds, - String[][] transitions, - String[][] concreteIdsPerState - ) throws StateModelException { - - AbstractStateModel model = new AbstractStateModel( - modelId, - "app", - "1.0", - Collections.singleton(Tags.Title) - ); - - // create states with no actions; - for (String s : stateIds) { - model.addState(new AbstractState(s, new HashSet<>())); - } - - // add action transitions as in/out sets - for (String[] t : transitions) { - String source = t[0]; - String actionId = t[1]; - String target = t[2]; - AbstractAction action = new AbstractAction(actionId); - model.addTransition(model.getState(source), model.getState(target), action); - } - - // add concrete ids - for (String[] entry : concreteIdsPerState) { - String stateId = entry[0]; - AbstractState state = model.getState(stateId); - for (int i = 1; i < entry.length; i++) { - state.addConcreteStateId(entry[i]); - } - } - - return model; - } - -} diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java index b7d8e49d1..4d339b0c9 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java @@ -1,154 +1,46 @@ package org.testar.statemodel.changedetection; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.util.Collections; import java.util.HashSet; -import org.junit.Before; import org.junit.Test; import org.testar.monkey.alayer.Tag; -import org.testar.monkey.alayer.Tags; -import org.testar.statemodel.AbstractAction; -import org.testar.statemodel.AbstractState; import org.testar.statemodel.AbstractStateModel; import org.testar.statemodel.exceptions.StateModelException; public class ChangeDetectionEngineTest { - private ChangeDetectionEngine engine; - - @Before - public void setUp() { - ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; - engine = new ChangeDetectionEngine(descriptionProvider, new StateDifferenceFinder()); - } - @Test - public void testDetectsAddedState() throws StateModelException { - AbstractStateModel oldModel = createModel("old-id", - new String[]{"AS1"}, new String[][]{}, new String[][]{}); - - AbstractStateModel newModel = createModel("new-id", - new String[]{"AS1", "AS2"}, new String[][]{}, new String[][]{}); - - ChangeDetectionResult result = engine.compare(oldModel, newModel); - - assertEquals("old-id", result.getOldModelIdentifier()); - assertEquals("new-id", result.getNewModelIdentifier()); - assertEquals(1, result.getAddedStates().size()); - assertEquals("AS2", result.getAddedStates().get(0).getStateId()); - - assertEquals(0, result.getRemovedStates().size()); - - assertTrue(result.getChangedStates().isEmpty()); - assertTrue(result.getChangedActions().isEmpty()); - - assertTrue(result.hasDifferences()); - } - - @Test - public void testDetectsRemovedState() throws StateModelException { - AbstractStateModel oldModel = createModel("old-id", - new String[]{"AS1", "AS2"}, new String[][]{}, new String[][]{}); - - AbstractStateModel newModel = createModel("new-id", - new String[]{"AS2"}, new String[][]{}, new String[][]{}); - - ChangeDetectionResult result = engine.compare(oldModel, newModel); - - assertEquals("old-id", result.getOldModelIdentifier()); - assertEquals("new-id", result.getNewModelIdentifier()); - assertEquals(1, result.getRemovedStates().size()); - assertEquals("AS1", result.getRemovedStates().get(0).getStateId()); - - assertEquals(0, result.getAddedStates().size()); - - assertTrue(result.getChangedStates().isEmpty()); - assertTrue(result.getChangedActions().isEmpty()); + public void testDescriptionProviderConstructor() throws StateModelException { + ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); - assertTrue(result.hasDifferences()); + AbstractStateModel oldModel = new AbstractStateModel("oldModel", "app", "version", new HashSet>(Collections.emptySet())); + AbstractStateModel newModel = new AbstractStateModel("newModel", "app", "version", new HashSet>(Collections.emptySet())); + engine.compare(oldModel, newModel); } - @Test - public void testDetectsChangedState() throws StateModelException { - AbstractStateModel oldModel = createModel("old-id", - new String[]{"AS1"}, new String[][]{}, new String[][]{}); - AbstractStateModel newModel = createModel("new-id", - new String[]{"AS1"}, new String[][]{}, new String[][]{}); - - Tag title = Tag.from("title", String.class); - oldModel.getState("AS1").addAttribute(title, "Old"); - newModel.getState("AS1").addAttribute(title, "New"); - - ChangeDetectionResult result = engine.compare(oldModel, newModel); - - assertEquals(1, result.getChangedStates().size()); - assertTrue(result.getChangedStates().containsKey("AS1")); - - VertexPropertyDiff diff = result.getChangedStates().get("AS1"); - assertEquals(1, diff.getChanged().size()); - assertEquals("title", diff.getChanged().get(0).getPropertyName()); - assertEquals("Old", diff.getChanged().get(0).getOldValue()); - assertEquals("New", diff.getChanged().get(0).getNewValue()); + @Test(expected = NullPointerException.class) + public void testNullDescriptionProvider() throws StateModelException { + new ChangeDetectionEngine(null); } - @Test - public void testDetectsChangedAction() throws StateModelException { - AbstractStateModel oldModel = createModel("old-id", - new String[]{"AS1"}, new String[][]{{"AS1", "AA1", "AS1"}}, new String[][]{}); - AbstractStateModel newModel = createModel("new-id", - new String[]{"AS1"}, new String[][]{{"AS1", "AA2", "AS1"}}, new String[][]{}); - - ChangeDetectionResult result = engine.compare(oldModel, newModel); - - assertEquals(1, result.getChangedActions().size()); + @Test(expected = NullPointerException.class) + public void testNullOldModel() throws StateModelException { + ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); - ActionSetDiff diff = result.getChangedActions().get("AS1"); - assertEquals(1, diff.getAddedOutgoing().size()); - assertEquals("AA2", diff.getAddedOutgoing().get(0).getActionId()); - assertEquals(1, diff.getRemovedOutgoing().size()); - assertEquals("AA1", diff.getRemovedOutgoing().get(0).getActionId()); + AbstractStateModel newModel = new AbstractStateModel("newModel", "app", "version", new HashSet>(Collections.emptySet())); + engine.compare(null, newModel); } - private AbstractStateModel createModel(String modelId, - String[] stateIds, - String[][] transitions, - String[][] concreteIdsPerState - ) throws StateModelException { - - AbstractStateModel model = new AbstractStateModel( - modelId, - "app", - "1.0", - Collections.singleton(Tags.Title) - ); - - // create states with no actions; - for (String s : stateIds) { - model.addState(new AbstractState(s, new HashSet<>())); - } - - // add action transitions as in/out sets - for (String[] t : transitions) { - String source = t[0]; - String actionId = t[1]; - String target = t[2]; - AbstractAction action = new AbstractAction(actionId); - model.addTransition(model.getState(source), model.getState(target), action); - } - - // add concrete ids - for (String[] entry : concreteIdsPerState) { - String stateId = entry[0]; - AbstractState state = model.getState(stateId); - for (int i = 1; i < entry.length; i++) { - state.addConcreteStateId(entry[i]); - } - } + @Test(expected = NullPointerException.class) + public void testNullNewModel() throws StateModelException { + ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); - return model; + AbstractStateModel oldModel = new AbstractStateModel("oldModel", "app", "version", new HashSet>(Collections.emptySet())); + engine.compare(oldModel, null); } } diff --git a/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java new file mode 100644 index 000000000..6e633b451 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java @@ -0,0 +1,154 @@ +package org.testar.statemodel.changedetection; + +import org.junit.Before; +import org.junit.Test; +import org.testar.monkey.alayer.Tag; +import org.testar.monkey.alayer.Tags; +import org.testar.statemodel.AbstractAction; +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.exceptions.StateModelException; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static org.junit.Assert.*; + +public class GraphTraversalComparatorTest { + + private Map descByAction; + private ActionDescriptionProvider descriptionProvider; + + @Before + public void setup() { + descByAction = new HashMap<>(); + descriptionProvider = id -> descByAction.getOrDefault(id, id); + } + + @Test + public void testUnchangedMatchesActionsByDescription() throws Exception { + descByAction.put("a1", "click"); + descByAction.put("b1", "click"); // different id but same description + + AbstractStateModel oldModel = model("old", new String[]{"S1", "S2"}, new String[][]{{"S1", "a1", "S2"}}); + AbstractStateModel newModel = model("new", new String[]{"S1", "S2"}, new String[][]{{"S1", "b1", "S2"}}); + + ChangeDetectionResult result = new GraphTraversalComparator(descriptionProvider).compare(oldModel, newModel); + + assertTrue(result.getAddedStates().isEmpty()); + assertTrue(result.getRemovedStates().isEmpty()); + assertTrue(result.getChangedStates().isEmpty()); + assertTrue(result.getChangedActions().isEmpty()); + } + + @Test + public void testDetectsChangedStates() throws Exception { + AbstractStateModel oldModel = model("old", new String[]{"S1", "S2"}, new String[][]{{"S1", "a1", "S2"}}); + AbstractStateModel newModel = model("new", new String[]{"S1", "S3"}, new String[][]{{"S1", "a1", "S3"}}); + + ChangeDetectionResult result = new GraphTraversalComparator(descriptionProvider).compare(oldModel, newModel); + + assertTrue(result.getAddedStates().isEmpty()); + assertTrue(result.getRemovedStates().isEmpty()); + + assertEquals(1, result.getChangedStates().size()); + assertTrue(result.getChangedStates().containsKey("S3")); + } + + @Test + public void testChangedStateDueToDifferentAction() throws Exception { + AbstractStateModel oldModel = model("old", new String[]{"S1", "S2"}, new String[][]{{"S1", "a1", "S2"}}); + AbstractStateModel newModel = model("new", new String[]{"S1", "S3"}, new String[][]{{"S1", "b2", "S3"}}); + + ChangeDetectionResult result = new GraphTraversalComparator(descriptionProvider).compare(oldModel, newModel); + + // If the corresponding abstract states are different, or the abstract actions of the corresponding states do not match + // The states contain changes + + assertEquals(1, result.getChangedStates().size()); + assertTrue(result.getChangedStates().containsKey("S1")); + + assertEquals(1, result.getAddedStates().size()); + assertTrue(result.getAddedStates().get(0).getStateId().equals("S3")); + + assertEquals(1, result.getRemovedStates().size()); + assertTrue(result.getRemovedStates().get(0).getStateId().equals("S2")); + } + + @Test + public void testDetectsStatePropertyChanges() throws Exception { + AbstractStateModel oldModel = model("old", new String[]{"S1"}, new String[][]{{"S1", "a1", "S1"}}); + AbstractStateModel newModel = model("new", new String[]{"S1"}, new String[][]{{"S1", "a1", "S1"}}); + + Tag title = Tags.Title; + oldModel.getState("S1").addAttribute(title, "Old"); + newModel.getState("S1").addAttribute(title, "New"); + + ChangeDetectionResult result = new GraphTraversalComparator(descriptionProvider).compare(oldModel, newModel); + + assertTrue(result.getChangedStates().containsKey("S1")); + assertEquals(1, result.getChangedStates().get("S1").getChanged().size()); + assertEquals("Old", result.getChangedStates().get("S1").getChanged().get(0).getOldValue()); + assertEquals("New", result.getChangedStates().get("S1").getChanged().get(0).getNewValue()); + } + + @Test + public void testDetectsAddedActionsOnMatchedStates() throws Exception { + descByAction.put("a1", "click"); + descByAction.put("a2", "drag"); + + AbstractStateModel oldModel = model("old", new String[]{"S1"}, new String[][]{{"S1", "a1", "S1"}}); + AbstractStateModel newModel = model("new", new String[]{"S1"}, new String[][]{{"S1", "a1", "S1"}, {"S1", "a2", "S1"}}); + + ChangeDetectionResult result = new GraphTraversalComparator(descriptionProvider).compare(oldModel, newModel); + + assertEquals(1, result.getChangedActions().size()); + ActionSetDiff diff = result.getChangedActions().get("S1"); + assertEquals(1, diff.getAddedOutgoing().size()); + assertEquals("a2", diff.getAddedOutgoing().get(0).getActionId()); + + // If the corresponding abstract states are different, or the abstract actions of the corresponding states do not match + // The states contain changes + + assertTrue(result.getChangedStates().containsKey("S1")); + } + + @Test + public void testHandledNodesAvoidsInfiniteLoops() throws Exception { + AbstractStateModel oldModel = model("old", new String[]{"S1", "S2"}, new String[][]{ + {"S1", "a1", "S2"}, + {"S2", "a2", "S1"} + }); + AbstractStateModel newModel = model("new", new String[]{"S1", "S2"}, new String[][]{ + {"S1", "a1", "S2"}, + {"S2", "a2", "S1"} + }); + + ChangeDetectionResult result = new GraphTraversalComparator(descriptionProvider).compare(oldModel, newModel); + assertTrue(result.getAddedStates().isEmpty()); + assertTrue(result.getRemovedStates().isEmpty()); + assertTrue(result.getChangedStates().isEmpty()); + assertTrue(result.getChangedActions().isEmpty()); + } + + private AbstractStateModel model(String id, String[] states, String[][] transitions) throws StateModelException { + AbstractStateModel model = new AbstractStateModel(id, "app", "1.0", Collections.singleton(Tags.Title)); + boolean first = true; + for (String s : states) { + AbstractState st = new AbstractState(s, new HashSet<>()); + st.setInitial(first); + first = false; + model.addState(st); + } + for (String[] t : transitions) { + String source = t[0]; + String actionId = t[1]; + String target = t[2]; + model.addTransition(model.getState(source), model.getState(target), new AbstractAction(actionId)); + } + return model; + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java b/statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java deleted file mode 100644 index f5f5a6aab..000000000 --- a/statemodel/test/org/testar/statemodel/changedetection/StateDifferenceFinderTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.testar.statemodel.changedetection; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.Before; -import org.junit.Test; -import org.testar.statemodel.changedetection.DeltaAction.Direction; - -public class StateDifferenceFinderTest { - - private StateDifferenceFinder finder; - private ActionDescriptionProvider descriptionProvider; - - @Before - public void setUp() { - finder = new StateDifferenceFinder(); - descriptionProvider = id -> "desc-" + id; - } - - @Test - public void testFindAddedState() { - StateSnapshot state = new StateSnapshot( - "AS1", - Arrays.asList("CS1", "CS4"), - Arrays.asList("AS1"), - Arrays.asList("AS2") - ); - - List oldModelStates = Arrays.asList(state); - List newModelStates = Arrays.asList( - state, - new StateSnapshot( - "AS2", - Arrays.asList("CS2", "CS3"), - Arrays.asList("AS3"), - Arrays.asList("AS4") - ) - ); - - List addedStates = finder.findAddedStates(oldModelStates, newModelStates, descriptionProvider); - - assertEquals(1, addedStates.size()); - DeltaState deltaAddedState = addedStates.get(0); - assertEquals("AS2", deltaAddedState.getStateId()); - assertEquals(Arrays.asList("CS2", "CS3"), deltaAddedState.getConcreteStateIds()); - - assertEquals(1, deltaAddedState.getIncomingDeltaActions().size()); - assertEquals("AS3", deltaAddedState.getIncomingDeltaActions().get(0).getActionId()); - assertEquals(Direction.INCOMING, deltaAddedState.getIncomingDeltaActions().get(0).getDirection()); - assertEquals("desc-AS3", deltaAddedState.getIncomingDeltaActions().get(0).getDescription()); - - assertEquals(1, deltaAddedState.getOutgoingDeltaActions().size()); - assertEquals("AS4", deltaAddedState.getOutgoingDeltaActions().get(0).getActionId()); - assertEquals(Direction.OUTGOING, deltaAddedState.getOutgoingDeltaActions().get(0).getDirection()); - assertEquals("desc-AS4", deltaAddedState.getOutgoingDeltaActions().get(0).getDescription()); - } - - @Test - public void testFindRemovedStates() { - StateSnapshot state = new StateSnapshot( - "AS1", - Arrays.asList("CS1", "CS4"), - Arrays.asList("AS1"), - Arrays.asList("AS2") - ); - - List oldModelStates = Arrays.asList( - state, - new StateSnapshot("AS2", Arrays.asList("CS2"), Arrays.asList("AS3"), Arrays.asList("AS4"))); - List newModelStates = Collections.singletonList(state); - - List removedStates = finder.findRemovedStates(oldModelStates, newModelStates, descriptionProvider); - - assertEquals(1, removedStates.size()); - DeltaState deltaRemovedState = removedStates.get(0); - assertEquals("AS2", deltaRemovedState.getStateId()); - assertTrue(deltaRemovedState.getIncomingDeltaActions().stream().anyMatch(a -> a.getActionId().equals("AS3"))); - assertTrue(deltaRemovedState.getOutgoingDeltaActions().stream().anyMatch(a -> a.getActionId().equals("AS4"))); - } - -} From ceb414119e9a1fb669b82051d76c3cf2f83d2ce2 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Fri, 12 Dec 2025 18:33:28 +0100 Subject: [PATCH 07/15] Refactor action primary key class --- ...rovider.java => ActionPrimaryKeyProvider.java} | 9 ++++++--- .../changedetection/ActionSetComparator.java | 6 +++--- .../changedetection/ChangeDetectionEngine.java | 8 ++++---- .../ChangeDetectionEngineFactory.java | 4 ++-- ....java => DefaultActionPrimaryKeyProvider.java} | 8 ++++---- .../changedetection/GraphTraversalComparator.java | 8 ++++---- ...java => OrientDbActionPrimaryKeyProvider.java} | 8 +++----- .../ChangeDetectionEngineTest.java | 6 +++--- .../DefaultActionDescriptionProviderTest.java | 15 --------------- .../DefaultActionPrimaryKeyProviderTest.java | 15 +++++++++++++++ .../GraphTraversalComparatorTest.java | 2 +- ... => OrientDbActionPrimaryKeyProviderTest.java} | 12 ++++++------ 12 files changed, 51 insertions(+), 50 deletions(-) rename statemodel/src/org/testar/statemodel/changedetection/{ActionDescriptionProvider.java => ActionPrimaryKeyProvider.java} (89%) rename statemodel/src/org/testar/statemodel/changedetection/{DefaultActionDescriptionProvider.java => DefaultActionPrimaryKeyProvider.java} (87%) rename statemodel/src/org/testar/statemodel/changedetection/{OrientDbActionDescriptionProvider.java => OrientDbActionPrimaryKeyProvider.java} (94%) delete mode 100644 statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java create mode 100644 statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java rename statemodel/test/org/testar/statemodel/changedetection/{OrientDbActionDescriptionProviderTest.java => OrientDbActionPrimaryKeyProviderTest.java} (60%) diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/ActionPrimaryKeyProvider.java similarity index 89% rename from statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java rename to statemodel/src/org/testar/statemodel/changedetection/ActionPrimaryKeyProvider.java index f6e6b7896..561197706 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ActionDescriptionProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ActionPrimaryKeyProvider.java @@ -30,9 +30,12 @@ package org.testar.statemodel.changedetection; -@FunctionalInterface -public interface ActionDescriptionProvider { +/** + * Resolves the primary key for an action comparison. + * The key prioritizes the action description and fall back to the action id. + */ +public interface ActionPrimaryKeyProvider { - String getDescription(String abstractActionId); + String getPrimaryKey(String actionId); } diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java b/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java index 6fc01a638..c5f545f95 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java @@ -47,7 +47,7 @@ private ActionSetComparator() { } public static ActionSetDiff compare(StateSnapshot oldState, StateSnapshot newState, - ActionDescriptionProvider descriptionProvider) { + ActionPrimaryKeyProvider descriptionProvider) { Objects.requireNonNull(oldState, "oldState cannot be null"); Objects.requireNonNull(newState, "newState cannot be null"); Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); @@ -60,12 +60,12 @@ public static ActionSetDiff compare(StateSnapshot oldState, return new ActionSetDiff(addedIncoming, removedIncoming, addedOutgoing, removedOutgoing); } - private static List diff(List primary, List secondary, Direction direction, ActionDescriptionProvider descriptionProvider) { + private static List diff(List primary, List secondary, Direction direction, ActionPrimaryKeyProvider descriptionProvider) { Set secondarySet = new HashSet<>(secondary); List deltas = new ArrayList<>(); for (String actionId : primary) { if (!secondarySet.contains(actionId)) { - deltas.add(new DeltaAction(actionId, descriptionProvider.getDescription(actionId), direction)); + deltas.add(new DeltaAction(actionId, descriptionProvider.getPrimaryKey(actionId), direction)); } } return deltas; diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java index 1f16276f5..416446fbe 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java @@ -39,12 +39,12 @@ */ public class ChangeDetectionEngine { - private final ActionDescriptionProvider actionDescriptionProvider; + private final ActionPrimaryKeyProvider actionPrimaryKeyProvider; private final GraphTraversalComparator comparator; - public ChangeDetectionEngine(ActionDescriptionProvider actionDescriptionProvider) { - this.actionDescriptionProvider = Objects.requireNonNull(actionDescriptionProvider, "actionDescriptionProvider cannot be null"); - this.comparator = new GraphTraversalComparator(this.actionDescriptionProvider); + public ChangeDetectionEngine(ActionPrimaryKeyProvider actionPrimaryKeyProvider) { + this.actionPrimaryKeyProvider = Objects.requireNonNull(actionPrimaryKeyProvider, "actionPrimaryKeyProvider cannot be null"); + this.comparator = new GraphTraversalComparator(this.actionPrimaryKeyProvider); } public ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java index cfcbed2ec..3c7063b0a 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java @@ -40,12 +40,12 @@ public class ChangeDetectionEngineFactory { private ChangeDetectionEngineFactory() { } public static ChangeDetectionEngine createWithDefaultDescription() { - return new ChangeDetectionEngine(new DefaultActionDescriptionProvider()); + return new ChangeDetectionEngine(new DefaultActionPrimaryKeyProvider()); } public static ChangeDetectionEngine createWithOrientDb(Connection connection) { Objects.requireNonNull(connection, "connection cannot be null"); - return new ChangeDetectionEngine(new OrientDbActionDescriptionProvider(connection)); + return new ChangeDetectionEngine(new OrientDbActionPrimaryKeyProvider(connection)); } public static ChangeDetectionEngine createWithPersistence(PersistenceManager persistenceManager) { diff --git a/statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProvider.java similarity index 87% rename from statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java rename to statemodel/src/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProvider.java index c8ac0270d..8799e7e82 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/DefaultActionDescriptionProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProvider.java @@ -31,13 +31,13 @@ package org.testar.statemodel.changedetection; /** - * Fallback description provider that returns the action id when no better description is available. + * Default implementation that simply returns the action id as the primary key. */ -public class DefaultActionDescriptionProvider implements ActionDescriptionProvider { +public class DefaultActionPrimaryKeyProvider implements ActionPrimaryKeyProvider { @Override - public String getDescription(String abstractActionId) { - return abstractActionId; + public String getPrimaryKey(String actionId) { + return actionId; } } diff --git a/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java b/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java index c37b2f575..0e9f69c7d 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java +++ b/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java @@ -51,10 +51,10 @@ */ class GraphTraversalComparator { - private final ActionDescriptionProvider descriptionProvider; + private final ActionPrimaryKeyProvider primaryKeyProvider; - GraphTraversalComparator(ActionDescriptionProvider descriptionProvider) { - this.descriptionProvider = Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); + GraphTraversalComparator(ActionPrimaryKeyProvider primaryKeyProvider) { + this.primaryKeyProvider = Objects.requireNonNull(primaryKeyProvider, "primaryKeyProvider cannot be null"); } ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { @@ -224,7 +224,7 @@ private Graph buildGraph(AbstractStateModel model) { private Edge toEdge(AbstractStateTransition t, Map nodes) { String actionId = t.getActionId(); - String desc = descriptionProvider.getDescription(actionId); + String desc = primaryKeyProvider.getPrimaryKey(actionId); String key = comparableKey(actionId, desc); return new Edge(t.getSourceStateId(), t.getTargetStateId(), actionId, desc, key); } diff --git a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java b/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProvider.java similarity index 94% rename from statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java rename to statemodel/src/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProvider.java index 0a3f0f621..3ff2755cf 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionDescriptionProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProvider.java @@ -42,20 +42,18 @@ /** * Connects with OrientDB to retrieve an action description (ConcreteAction) for the given actionId. * Falls back to returning the abstract action id if no description is found. - * - * This connection class is needed because the model extractor only provides the Abstract layer. */ -public class OrientDbActionDescriptionProvider implements ActionDescriptionProvider { +public class OrientDbActionPrimaryKeyProvider implements ActionPrimaryKeyProvider { private final Connection connection; private final Map cache = new ConcurrentHashMap<>(); - public OrientDbActionDescriptionProvider(Connection connection) { + public OrientDbActionPrimaryKeyProvider(Connection connection) { this.connection = Objects.requireNonNull(connection, "connection cannot be null"); } @Override - public String getDescription(String abstractActionId) { + public String getPrimaryKey(String abstractActionId) { String cached = cache.get(abstractActionId); if (cached != null) { return cached; diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java index 4d339b0c9..38aebe2a6 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java @@ -12,7 +12,7 @@ public class ChangeDetectionEngineTest { @Test public void testDescriptionProviderConstructor() throws StateModelException { - ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + ActionPrimaryKeyProvider descriptionProvider = id -> "desc-" + id; ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); AbstractStateModel oldModel = new AbstractStateModel("oldModel", "app", "version", new HashSet>(Collections.emptySet())); @@ -27,7 +27,7 @@ public void testNullDescriptionProvider() throws StateModelException { @Test(expected = NullPointerException.class) public void testNullOldModel() throws StateModelException { - ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + ActionPrimaryKeyProvider descriptionProvider = id -> "desc-" + id; ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); AbstractStateModel newModel = new AbstractStateModel("newModel", "app", "version", new HashSet>(Collections.emptySet())); @@ -36,7 +36,7 @@ public void testNullOldModel() throws StateModelException { @Test(expected = NullPointerException.class) public void testNullNewModel() throws StateModelException { - ActionDescriptionProvider descriptionProvider = id -> "desc-" + id; + ActionPrimaryKeyProvider descriptionProvider = id -> "desc-" + id; ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); AbstractStateModel oldModel = new AbstractStateModel("oldModel", "app", "version", new HashSet>(Collections.emptySet())); diff --git a/statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java deleted file mode 100644 index 39400c8c4..000000000 --- a/statemodel/test/org/testar/statemodel/changedetection/DefaultActionDescriptionProviderTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.testar.statemodel.changedetection; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class DefaultActionDescriptionProviderTest { - - @Test - public void testDefaultActionIdAsDescription() { - DefaultActionDescriptionProvider provider = new DefaultActionDescriptionProvider(); - assertEquals("AA1", provider.getDescription("AA1")); - } - -} diff --git a/statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java new file mode 100644 index 000000000..b88ee7466 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java @@ -0,0 +1,15 @@ +package org.testar.statemodel.changedetection; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class DefaultActionPrimaryKeyProviderTest { + + @Test + public void testDefaultActionIdAsPrimaryKey() { + DefaultActionPrimaryKeyProvider provider = new DefaultActionPrimaryKeyProvider(); + assertEquals("AA1", provider.getPrimaryKey("AA1")); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java index 6e633b451..a58c613b6 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java @@ -19,7 +19,7 @@ public class GraphTraversalComparatorTest { private Map descByAction; - private ActionDescriptionProvider descriptionProvider; + private ActionPrimaryKeyProvider descriptionProvider; @Before public void setup() { diff --git a/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProviderTest.java similarity index 60% rename from statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java rename to statemodel/test/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProviderTest.java index 903ea5402..bafb02bf4 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionDescriptionProviderTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProviderTest.java @@ -5,28 +5,28 @@ import org.junit.Test; import org.testar.statemodel.persistence.orientdb.entity.Connection; -public class OrientDbActionDescriptionProviderTest { +public class OrientDbActionPrimaryKeyProviderTest { @Test - public void testObtainDescription() { - OrientDbActionDescriptionProvider provider = new OrientDbActionDescriptionProvider(createDummyConnection()) { + public void testObtainPrimaryKeyFromDescription() { + OrientDbActionPrimaryKeyProvider provider = new OrientDbActionPrimaryKeyProvider(createDummyConnection()) { @Override protected String queryConcreteActionDescription(String abstractActionId) { return "desc-" + abstractActionId; } }; - assertEquals("desc-AA1", provider.getDescription("AA1")); + assertEquals("desc-AA1", provider.getPrimaryKey("AA1")); } @Test public void testFallsBackToId() { - OrientDbActionDescriptionProvider provider = new OrientDbActionDescriptionProvider(createDummyConnection()) { + OrientDbActionPrimaryKeyProvider provider = new OrientDbActionPrimaryKeyProvider(createDummyConnection()) { @Override protected String queryConcreteActionDescription(String abstractActionId) { return null; } }; - assertEquals("AA1", provider.getDescription("AA1")); + assertEquals("AA1", provider.getPrimaryKey("AA1")); } private Connection createDummyConnection() { From d975340d5d969988cf210b668ddcc355f12d0666 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 14:29:39 +0100 Subject: [PATCH 08/15] refactor GraphTraversalComparator algorithm --- .../ChangeDetectionEngine.java | 1 + .../GraphTraversalComparator.java | 177 +++++------------- .../algorithm/MutableActionDiff.java | 45 +++++ .../algorithm/TraversalContext.java | 87 +++++++++ .../algorithm/TraversalEdge.java | 53 ++++++ .../algorithm/TraversalEdgePair.java | 46 +++++ .../algorithm/TraversalGraph.java | 49 +++++ .../algorithm/TraversalNode.java | 60 ++++++ .../GraphTraversalComparatorTest.java | 5 +- 9 files changed, 388 insertions(+), 135 deletions(-) rename statemodel/src/org/testar/statemodel/changedetection/{ => algorithm}/GraphTraversalComparator.java (63%) create mode 100644 statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdge.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdgePair.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalGraph.java create mode 100644 statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalNode.java rename statemodel/test/org/testar/statemodel/changedetection/{ => algorithm}/GraphTraversalComparatorTest.java (96%) diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java index 416446fbe..ef5971b53 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java @@ -33,6 +33,7 @@ import java.util.Objects; import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.changedetection.algorithm.GraphTraversalComparator; /** * Entry point to run change detection between two state models. diff --git a/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java similarity index 63% rename from statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java rename to statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java index 0e9f69c7d..95f02268f 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/GraphTraversalComparator.java +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java @@ -28,20 +28,24 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.algorithm; import org.testar.statemodel.AbstractState; import org.testar.statemodel.AbstractStateModel; import org.testar.statemodel.AbstractStateTransition; - +import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.ActionSetDiff; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.DeltaAction; +import org.testar.statemodel.changedetection.DeltaState; +import org.testar.statemodel.changedetection.StatePropertyComparator; +import org.testar.statemodel.changedetection.VertexPropertyDiff; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; /** @@ -49,22 +53,22 @@ * Connects to the initial abstract state and uses the action descriptions as the matching key (desc or action id), * and tracks handled states/actions to avoid double counting. */ -class GraphTraversalComparator { +public class GraphTraversalComparator { private final ActionPrimaryKeyProvider primaryKeyProvider; - GraphTraversalComparator(ActionPrimaryKeyProvider primaryKeyProvider) { + public GraphTraversalComparator(ActionPrimaryKeyProvider primaryKeyProvider) { this.primaryKeyProvider = Objects.requireNonNull(primaryKeyProvider, "primaryKeyProvider cannot be null"); } - ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { - Graph oldGraph = buildGraph(oldModel); - Graph newGraph = buildGraph(newModel); + public ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { + TraversalGraph oldGraph = buildGraph(oldModel); + TraversalGraph newGraph = buildGraph(newModel); TraversalContext ctx = new TraversalContext(oldGraph, newGraph); - Node oldStart = chooseInitialState(oldGraph); - Node newStart = chooseInitialState(newGraph); + TraversalNode oldStart = chooseInitialState(oldGraph); + TraversalNode newStart = chooseInitialState(newGraph); if (oldStart != null && newStart != null) { compareStates(oldStart, newStart, ctx); @@ -87,7 +91,7 @@ ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel ne ); } - private void compareStates(Node oldNode, Node newNode, TraversalContext ctx) { + private void compareStates(TraversalNode oldNode, TraversalNode newNode, TraversalContext ctx) { if (ctx.isMapped(oldNode.id, newNode.id)) { return; } @@ -102,20 +106,20 @@ private void compareStates(Node oldNode, Node newNode, TraversalContext ctx) { } // Match outgoing actions by comparable key (description preferred) - for (Edge newEdge : newNode.outgoing) { + for (TraversalEdge newEdge : newNode.outgoing) { if (newEdge.handled) { continue; } - Edge match = ctx.findMatchingOutgoing(oldNode, newEdge.comparableKey); + TraversalEdge match = ctx.findMatchingOutgoing(oldNode, newEdge.comparableKey); if (match == null) { ctx.addedEdges.add(newEdge); } else { match.handled = true; newEdge.handled = true; - ctx.matchedEdges.add(new EdgePair(match, newEdge)); + ctx.matchedEdges.add(new TraversalEdgePair(match, newEdge)); - Node oldTarget = ctx.oldGraph.nodes.get(match.targetId); - Node newTarget = ctx.newGraph.nodes.get(newEdge.targetId); + TraversalNode oldTarget = ctx.oldGraph.nodes.get(match.targetId); + TraversalNode newTarget = ctx.newGraph.nodes.get(newEdge.targetId); if (oldTarget != null && newTarget != null) { // if mapping already exists but points elsewhere, skip recursion @@ -127,7 +131,7 @@ private void compareStates(Node oldNode, Node newNode, TraversalContext ctx) { } // Remaining unhandled outgoing actions on old node are removed - for (Edge oldEdge : oldNode.outgoing) { + for (TraversalEdge oldEdge : oldNode.outgoing) { if (!oldEdge.handled) { ctx.removedEdges.add(oldEdge); } @@ -136,7 +140,7 @@ private void compareStates(Node oldNode, Node newNode, TraversalContext ctx) { private List computeAddedStates(TraversalContext ctx) { List added = new ArrayList<>(); - for (Node newNode : ctx.newGraph.nodes.values()) { + for (TraversalNode newNode : ctx.newGraph.nodes.values()) { if (!ctx.newToOld.containsKey(newNode.id)) { added.add(toDeltaState(newNode, ctx.newGraph)); } @@ -146,7 +150,7 @@ private List computeAddedStates(TraversalContext ctx) { private List computeRemovedStates(TraversalContext ctx) { List removed = new ArrayList<>(); - for (Node oldNode : ctx.oldGraph.nodes.values()) { + for (TraversalNode oldNode : ctx.oldGraph.nodes.values()) { if (!ctx.oldToNew.containsKey(oldNode.id)) { removed.add(toDeltaState(oldNode, ctx.oldGraph)); } @@ -157,7 +161,7 @@ private List computeRemovedStates(TraversalContext ctx) { private Map computeChangedActions(TraversalContext ctx) { Map diffByNewState = new HashMap<>(); - for (Edge added : ctx.addedEdges) { + for (TraversalEdge added : ctx.addedEdges) { // source outgoing MutableActionDiff sourceDiff = diffByNewState.computeIfAbsent(added.sourceId, k -> new MutableActionDiff()); sourceDiff.addOutgoing.add(new DeltaAction(added.actionId, added.description, DeltaAction.Direction.OUTGOING)); @@ -168,7 +172,7 @@ private Map computeChangedActions(TraversalContext ctx) { ctx.changedStates.putIfAbsent(added.sourceId, new VertexPropertyDiff(new ArrayList<>(), new ArrayList<>(), new ArrayList<>())); } - for (Edge removed : ctx.removedEdges) { + for (TraversalEdge removed : ctx.removedEdges) { String mappedSource = ctx.oldToNew.get(removed.sourceId); String mappedTarget = ctx.oldToNew.get(removed.targetId); if (mappedSource != null) { @@ -191,7 +195,7 @@ private Map computeChangedActions(TraversalContext ctx) { return result; } - private DeltaState toDeltaState(Node node, Graph graph) { + private DeltaState toDeltaState(TraversalNode node, TraversalGraph graph) { List incoming = node.incoming.stream() .map(e -> new DeltaAction(e.actionId, e.description, DeltaAction.Direction.INCOMING)) .collect(Collectors.toList()); @@ -201,43 +205,47 @@ private DeltaState toDeltaState(Node node, Graph graph) { return new DeltaState(node.id, new ArrayList<>(node.concreteIds), incoming, outgoing); } - private Graph buildGraph(AbstractStateModel model) { - Map nodes = new HashMap<>(); - List edges = new ArrayList<>(); + private TraversalGraph buildGraph(AbstractStateModel model) { + Map nodes = new HashMap<>(); + List edges = new ArrayList<>(); for (AbstractState s : model.getStates()) { - nodes.put(s.getStateId(), new Node(s)); + nodes.put(s.getStateId(), new TraversalNode(s)); } - for (Node n : nodes.values()) { + for (TraversalNode n : nodes.values()) { Collection outgoing = model.getOutgoingTransitionsForState(n.id); for (AbstractStateTransition t : outgoing) { - Edge e = toEdge(t, nodes); + TraversalEdge e = toEdge(t, nodes); n.outgoing.add(e); nodes.get(e.targetId).incoming.add(e); edges.add(e); } } - return new Graph(nodes, edges); + return new TraversalGraph(nodes, edges); } - private Edge toEdge(AbstractStateTransition t, Map nodes) { + private TraversalEdge toEdge(AbstractStateTransition t, Map nodes) { String actionId = t.getActionId(); String desc = primaryKeyProvider.getPrimaryKey(actionId); String key = comparableKey(actionId, desc); - return new Edge(t.getSourceStateId(), t.getTargetStateId(), actionId, desc, key); + return new TraversalEdge(t.getSourceStateId(), t.getTargetStateId(), actionId, desc, key); } private String comparableKey(String actionId, String description) { - if (description == null || description.trim().isEmpty() || description.contains("at ''")) { + if (description == null || description.trim().isEmpty()) { + return actionId; + } + // Workaround for actions with empty descriptions + if (description.contains("at ''")) { return actionId; } return description; } - private Node chooseInitialState(Graph graph) { - for (Node n : graph.nodes.values()) { + private TraversalNode chooseInitialState(TraversalGraph graph) { + for (TraversalNode n : graph.nodes.values()) { if (n.initial) { return n; } @@ -245,103 +253,4 @@ private Node chooseInitialState(Graph graph) { return null; } - private static final class TraversalContext { - final Graph oldGraph; - final Graph newGraph; - final Map oldToNew = new HashMap<>(); - final Map newToOld = new HashMap<>(); - final Map changedStates = new HashMap<>(); - final List addedEdges = new ArrayList<>(); - final List removedEdges = new ArrayList<>(); - final List matchedEdges = new ArrayList<>(); - - TraversalContext(Graph oldGraph, Graph newGraph) { - this.oldGraph = oldGraph; - this.newGraph = newGraph; - } - - void mapStates(String oldId, String newId) { - oldToNew.put(oldId, newId); - newToOld.put(newId, oldId); - } - - boolean isMapped(String oldId, String newId) { - return newId.equals(oldToNew.get(oldId)) || oldId.equals(newToOld.get(newId)); - } - - boolean hasConflictingMapping(String oldId, String newId) { - String mapped = oldToNew.get(oldId); - return mapped != null && !mapped.equals(newId); - } - - Edge findMatchingOutgoing(Node oldNode, String comparableKey) { - for (Edge e : oldNode.outgoing) { - if (!e.handled && e.comparableKey.equals(comparableKey)) { - return e; - } - } - return null; - } - } - - private static final class Graph { - final Map nodes; - final List edges; - - Graph(Map nodes, List edges) { - this.nodes = nodes; - this.edges = edges; - } - } - - private static final class Node { - final String id; - final AbstractState state; - final boolean initial; - final Set concreteIds; - final List outgoing = new ArrayList<>(); - final List incoming = new ArrayList<>(); - boolean handled = false; - - Node(AbstractState state) { - this.state = state; - this.id = state.getStateId(); - this.initial = state.isInitial(); - this.concreteIds = new HashSet<>(state.getConcreteStateIds()); - } - } - - private static final class Edge { - final String sourceId; - final String targetId; - final String actionId; - final String description; - final String comparableKey; - boolean handled = false; - - Edge(String sourceId, String targetId, String actionId, String description, String comparableKey) { - this.sourceId = sourceId; - this.targetId = targetId; - this.actionId = actionId; - this.description = description; - this.comparableKey = comparableKey; - } - } - - private static final class EdgePair { - final Edge oldEdge; - final Edge newEdge; - - EdgePair(Edge oldEdge, Edge newEdge) { - this.oldEdge = oldEdge; - this.newEdge = newEdge; - } - } - - private static final class MutableActionDiff { - final List addIncoming = new ArrayList<>(); - final List remIncoming = new ArrayList<>(); - final List addOutgoing = new ArrayList<>(); - final List remOutgoing = new ArrayList<>(); - } } diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java new file mode 100644 index 000000000..aa6b90d97 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java @@ -0,0 +1,45 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection.algorithm; + +import java.util.ArrayList; +import java.util.List; + +import org.testar.statemodel.changedetection.DeltaAction; + +final class MutableActionDiff { + + final List addIncoming = new ArrayList<>(); + final List remIncoming = new ArrayList<>(); + final List addOutgoing = new ArrayList<>(); + final List remOutgoing = new ArrayList<>(); + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java new file mode 100644 index 000000000..340d3f9f1 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java @@ -0,0 +1,87 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection.algorithm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.testar.statemodel.changedetection.VertexPropertyDiff; + +/** + * Traversal context used by {@link GraphTraversalComparator}. + * + * It maintains: + * - state mapping old->new (and inverse) + * - handled edges/states to avoid infinite traversal loops + * - collected added/removed edges and changed states + */ +final class TraversalContext { + + final TraversalGraph oldGraph; + final TraversalGraph newGraph; + final Map oldToNew = new HashMap<>(); + final Map newToOld = new HashMap<>(); + final Map changedStates = new HashMap<>(); + final List addedEdges = new ArrayList<>(); + final List removedEdges = new ArrayList<>(); + final List matchedEdges = new ArrayList<>(); + + TraversalContext(TraversalGraph oldGraph, TraversalGraph newGraph) { + this.oldGraph = oldGraph; + this.newGraph = newGraph; + } + + void mapStates(String oldId, String newId) { + oldToNew.put(oldId, newId); + newToOld.put(newId, oldId); + } + + boolean isMapped(String oldId, String newId) { + return newId.equals(oldToNew.get(oldId)) || oldId.equals(newToOld.get(newId)); + } + + boolean hasConflictingMapping(String oldId, String newId) { + String mapped = oldToNew.get(oldId); + return mapped != null && !mapped.equals(newId); + } + + TraversalEdge findMatchingOutgoing(TraversalNode oldNode, String comparableKey) { + for (TraversalEdge e : oldNode.outgoing) { + if (!e.handled && e.comparableKey.equals(comparableKey)) { + return e; + } + } + return null; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdge.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdge.java new file mode 100644 index 000000000..81ce7ad71 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdge.java @@ -0,0 +1,53 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection.algorithm; + +/** + * Directed action edge between two abstract states in the traversal graph. + */ +final class TraversalEdge { + + final String sourceId; + final String targetId; + final String actionId; + final String description; + final String comparableKey; + boolean handled = false; + + TraversalEdge(String sourceId, String targetId, String actionId, String description, String comparableKey) { + this.sourceId = sourceId; + this.targetId = targetId; + this.actionId = actionId; + this.description = description; + this.comparableKey = comparableKey; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdgePair.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdgePair.java new file mode 100644 index 000000000..e3819cf08 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalEdgePair.java @@ -0,0 +1,46 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection.algorithm; + +/** + * Pair of a matched old and new edge during traversal. + */ +final class TraversalEdgePair { + + final TraversalEdge oldEdge; + final TraversalEdge newEdge; + + TraversalEdgePair(TraversalEdge oldEdge, TraversalEdge newEdge) { + this.oldEdge = oldEdge; + this.newEdge = newEdge; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalGraph.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalGraph.java new file mode 100644 index 000000000..68467376f --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalGraph.java @@ -0,0 +1,49 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection.algorithm; + +import java.util.List; +import java.util.Map; + +/** + * Graph representation used by {@link GraphTraversalComparator}. + */ +final class TraversalGraph { + + final Map nodes; + final List edges; + + TraversalGraph(Map nodes, List edges) { + this.nodes = nodes; + this.edges = edges; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalNode.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalNode.java new file mode 100644 index 000000000..8b72a2270 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalNode.java @@ -0,0 +1,60 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.changedetection.algorithm; + +import org.testar.statemodel.AbstractState; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Node in the traversal graph used by the comparator. + */ +final class TraversalNode { + + final String id; + final AbstractState state; + final boolean initial; + final Set concreteIds; + final List outgoing = new ArrayList<>(); + final List incoming = new ArrayList<>(); + boolean handled = false; + + TraversalNode(AbstractState state) { + this.state = state; + this.id = state.getStateId(); + this.initial = state.isInitial(); + this.concreteIds = new HashSet<>(state.getConcreteStateIds()); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java similarity index 96% rename from statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java rename to statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java index a58c613b6..ad5f817f2 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/GraphTraversalComparatorTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java @@ -1,4 +1,4 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.algorithm; import org.junit.Before; import org.junit.Test; @@ -7,6 +7,9 @@ import org.testar.statemodel.AbstractAction; import org.testar.statemodel.AbstractState; import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.ActionSetDiff; +import org.testar.statemodel.changedetection.ChangeDetectionResult; import org.testar.statemodel.exceptions.StateModelException; import java.util.Collections; From 9e297fef7e9040a9cf98ba5036e1e1a37a2f66d2 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 17:29:02 +0100 Subject: [PATCH 09/15] Update action AbstractID using state+widget+role --- core/src/org/testar/CodingManager.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/org/testar/CodingManager.java b/core/src/org/testar/CodingManager.java index 617b96319..19c296fd3 100644 --- a/core/src/org/testar/CodingManager.java +++ b/core/src/org/testar/CodingManager.java @@ -173,12 +173,18 @@ public static synchronized void buildIDs(Widget widget){ */ public static synchronized void buildIDs(State state, Set actions){ for (Action a : actions) { + // Create the Action AbstractID based on: State AbstractID + Widget AbstractID + Action Role + a.set(Tags.AbstractID, ID_PREFIX_ACTION + ID_PREFIX_ABSTRACT + + lowCollisionID(state.get(Tags.AbstractID) + a.get(Tags.OriginWidget).get(Tags.AbstractID) + a.get(Tags.Role, ActionRoles.Action))); + // Create the Action ConcreteID based on: State ConcreteID + Widget ConcreteID + Action Role + Action description a.set(Tags.ConcreteID, ID_PREFIX_ACTION + ID_PREFIX_CONCRETE + - CodingManager.codify(state.get(Tags.ConcreteID), a)); + lowCollisionID(state.get(Tags.ConcreteID) + a.get(Tags.OriginWidget).get(Tags.ConcreteID) + a.get(Tags.Role, ActionRoles.Action) + a.toString())); + } // for the abstract action identifier, we first sort the actions by their path in the widget tree // and then set their ids using incremental counters + /* Map roleCounter = new HashMap<>(); actions.stream(). filter(action -> { @@ -200,6 +206,7 @@ public static synchronized void buildIDs(State state, Set actions){ lowCollisionID(state.get(Tags.AbstractID) + getAbstractActionIdentifier(action, roleCounter))); } ); + */ } /** From f7ff0723192d70a4a70f3f00e79298e278065053 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 18:16:25 +0100 Subject: [PATCH 10/15] Add changedetection analysis servlet and frontend --- .gitignore | 1 + .../resources/graphs/changedetection.jsp | 585 ++++++++++++++++++ statemodel/resources/graphs/models.jsp | 53 ++ .../resources/graphs/pixelmatchInterop.js | 63 ++ .../statemodel/analysis/AnalysisManager.java | 39 +- .../ChangeDetectionAnalysisService.java | 81 +++ .../ChangeDetectionGraphBuilder.java | 201 ++++++ .../ChangeDetectionGraphServlet.java | 119 ++++ .../ChangeDetectionServlet.java | 104 ++++ .../helpers/ActionEdgeJoiner.java | 150 +++++ .../changedetection/helpers/ElementUtils.java | 96 +++ .../helpers/GraphElementIndexer.java | 174 ++++++ .../helpers/MergedGraphFilter.java | 119 ++++ .../helpers/ScreenshotAssigner.java | 182 ++++++ .../analysis/webserver/JettyServer.java | 4 + .../ChangeDetectionGraphBuilderTest.java | 326 ++++++++++ 16 files changed, 2289 insertions(+), 8 deletions(-) create mode 100644 statemodel/resources/graphs/changedetection.jsp create mode 100644 statemodel/resources/graphs/pixelmatchInterop.js create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionAnalysisService.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ActionEdgeJoiner.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ElementUtils.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/MergedGraphFilter.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java create mode 100644 statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java diff --git a/.gitignore b/.gitignore index f556ec7fe..9c274501c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.vscode .gradle/ bin/ target/ diff --git a/statemodel/resources/graphs/changedetection.jsp b/statemodel/resources/graphs/changedetection.jsp new file mode 100644 index 000000000..85b649e21 --- /dev/null +++ b/statemodel/resources/graphs/changedetection.jsp @@ -0,0 +1,585 @@ + + + + + + Change Detection + + + + + + + + + + + + + + + + + + + +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + +
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
BETA: Results depend strongly on the abstraction attributes used to infer each model.
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + + + + + + diff --git a/statemodel/resources/graphs/models.jsp b/statemodel/resources/graphs/models.jsp index 14450f21a..e2c41ea3c 100644 --- a/statemodel/resources/graphs/models.jsp +++ b/statemodel/resources/graphs/models.jsp @@ -17,6 +17,46 @@ +
+
+
Compare two models
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+

Available Models

@@ -161,6 +201,19 @@ } }); }) + + $('#compareBtn').on('click', function(e) { + const oldId = $('#oldModel').val(); + const newId = $('#newModel').val(); + if (!oldId || !newId) { + e.preventDefault(); + alert('Please select both models to compare'); + return false; + } + $('#oldModelHidden').val(oldId); + $('#newModelHidden').val(newId); + return true; + }); }); diff --git a/statemodel/resources/graphs/pixelmatchInterop.js b/statemodel/resources/graphs/pixelmatchInterop.js new file mode 100644 index 000000000..a8e574b33 --- /dev/null +++ b/statemodel/resources/graphs/pixelmatchInterop.js @@ -0,0 +1,63 @@ +window.pixelmatchInterop = { + compareImages: (newImage, oldImage, thresholdValue) => { + return new Promise((resolve, reject) => { + // Create image elements for the new and old images + const newImg = new Image(); + newImg.src = 'data:image/png;base64,' + newImage; + + const oldImg = new Image(); + oldImg.src = 'data:image/png;base64,' + oldImage; + + // Ensure both images are loaded before comparing + Promise.all([new Promise(resolve => newImg.onload = resolve), new Promise(resolve => oldImg.onload = resolve)]) + .then(() => { + console.log("Images loaded successfully."); + // Create canvas elements for drawing the images + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = newImg.width; + canvas.height = newImg.height; + + // Draw the images on the canvas to get the pixel data of the images + context.drawImage(newImg, 0, 0); + const newImageData = context.getImageData(0, 0, canvas.width, canvas.height); + context.drawImage(oldImg, 0, 0); + const oldImageData = context.getImageData(0, 0, canvas.width, canvas.height); + + // Create canvas for the diff image + const diffCanvas = document.createElement('canvas'); + const diffContext = diffCanvas.getContext('2d'); + diffCanvas.width = newImg.width; + diffCanvas.height = newImg.height; + + // Use pixelmatch to compare images and generate diff + const diff = new Uint8Array(newImageData.data.length); + const numDiffPixels = pixelmatch(oldImageData.data, newImageData.data, diff, diffCanvas.width, diffCanvas.height, { + threshold: thresholdValue, + includeAA: true, + alpha: 0.6, + aaColor: [0, 0, 0], + diffColor: [255, 0, 0], + diffColorAlt: [0, 0, 255], + diffMask: null + }); + + // Create ImageData object from the diff data + const diffImageData = new ImageData(new Uint8ClampedArray(diff), newImg.width, newImg.height); + + // Apply the diff data to the diff canvas + diffContext.clearRect(0, 0, canvas.width, canvas.height); + diffContext.putImageData(diffImageData, 0, 0); + + // Convert the diff canvas to a base64 string + const diffBase64 = diffCanvas.toDataURL('image/png').split(',')[1]; + + resolve(diffBase64); + }) + .catch(error => { + console.error("Error during image comparison:", error); + reject(error); + }); + }); + } +}; diff --git a/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java b/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java index feaa33e23..c19280cce 100644 --- a/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java +++ b/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java @@ -109,6 +109,20 @@ public void shutdown() { } } + /** + * Exposes the database configuration. + */ + public Config getDbConfig() { + return dbConfig; + } + + /** + * Exposes the output directory used to store generated graph/json/screenshot files. + */ + public String getOutputDir() { + return outputDir; + } + /** * Checks whether the connection should be shutdown. */ @@ -334,14 +348,9 @@ public List fetchTestSequence(String sequenceId) { } /** - * This model generates graph data for a given abstract state model and writes it to a json file. - * @param modelIdentifier the abstract state model identifier - * @param abstractLayerRequired true if the abstract state layer needs to be exported - * @param concreteLayerRequired true if the concrete state layer needs to be exported - * @param sequenceLayerRequired true if the sequence layer needs to be exported - * @return + * Builds the graph elements for a model in memory */ - public String fetchGraphForModel(String modelIdentifier, boolean abstractLayerRequired, boolean concreteLayerRequired, boolean sequenceLayerRequired, boolean showCompoundGraph) { + public List fetchGraphElementsForModel(String modelIdentifier, boolean abstractLayerRequired, boolean concreteLayerRequired, boolean sequenceLayerRequired, boolean showCompoundGraph) { startUp(); ArrayList elements = new ArrayList<>(); if (abstractLayerRequired || concreteLayerRequired || sequenceLayerRequired) { @@ -367,6 +376,20 @@ public String fetchGraphForModel(String modelIdentifier, boolean abstractLayerRe } } } + checkShutDown(); + return elements; + } + + /** + * This model generates graph data for a given abstract state model and writes it to a json file. + * @param modelIdentifier the abstract state model identifier + * @param abstractLayerRequired true if the abstract state layer needs to be exported + * @param concreteLayerRequired true if the concrete state layer needs to be exported + * @param sequenceLayerRequired true if the sequence layer needs to be exported + * @return + */ + public String fetchGraphForModel(String modelIdentifier, boolean abstractLayerRequired, boolean concreteLayerRequired, boolean sequenceLayerRequired, boolean showCompoundGraph) { + List elements = fetchGraphElementsForModel(modelIdentifier, abstractLayerRequired, concreteLayerRequired, sequenceLayerRequired, showCompoundGraph); StringBuilder builder = new StringBuilder(modelIdentifier); builder.append("_"); @@ -378,7 +401,7 @@ public String fetchGraphForModel(String modelIdentifier, boolean abstractLayerRe builder.append("_elements.json"); String filename = builder.toString(); checkShutDown(); - return writeJson(elements, filename, modelIdentifier); + return writeJson(new ArrayList<>(elements), filename, modelIdentifier); } /** diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionAnalysisService.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionAnalysisService.java new file mode 100644 index 000000000..b2b1a78cc --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionAnalysisService.java @@ -0,0 +1,81 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; + +import org.testar.monkey.alayer.Tag; +import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.changedetection.ChangeDetectionEngine; +import org.testar.statemodel.changedetection.ChangeDetectionEngineFactory; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.persistence.PersistenceManager; + +/** + * Service to run change detection between two persisted state models. + * It extracts the models using the provided {@link PersistenceManager} + * and delegates comparison to {@link ChangeDetectionEngine}. + */ +public class ChangeDetectionAnalysisService { + + private final PersistenceManager persistenceManager; + private final ChangeDetectionEngine engine; + + public ChangeDetectionAnalysisService(PersistenceManager persistenceManager) { + this(persistenceManager, ChangeDetectionEngineFactory.createWithPersistence(persistenceManager)); + } + + public ChangeDetectionAnalysisService(PersistenceManager persistenceManager, ChangeDetectionEngine engine) { + this.persistenceManager = Objects.requireNonNull(persistenceManager, "persistenceManager cannot be null"); + this.engine = Objects.requireNonNull(engine, "engine cannot be null"); + } + + public ChangeDetectionResult compare(String oldModelIdentifier, String newModelIdentifier) { + AbstractStateModel oldModel = extract(oldModelIdentifier); + AbstractStateModel newModel = extract(newModelIdentifier); + return engine.compare(oldModel, newModel); + } + + private AbstractStateModel extract(String modelIdentifier) { + // The model is extracted using the modelIdentifier, other params are ignored + AbstractStateModel model = new AbstractStateModel( + modelIdentifier, + "dummy-app", + "dummy-version", + new HashSet>(Collections.emptySet())); + // the persistence manager extracts the model from datastore if present + persistenceManager.initAbstractStateModel(model); + return model; + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java new file mode 100644 index 000000000..ffe740475 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java @@ -0,0 +1,201 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection; + +import org.testar.statemodel.analysis.changedetection.helpers.ActionEdgeJoiner; +import org.testar.statemodel.analysis.changedetection.helpers.ElementUtils; +import org.testar.statemodel.analysis.changedetection.helpers.GraphElementIndexer; +import org.testar.statemodel.analysis.changedetection.helpers.MergedGraphFilter; +import org.testar.statemodel.analysis.changedetection.helpers.ScreenshotAssigner; +import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.ChangeDetectionResult; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Builds the merged graph elements used by the change detection frontend. + */ +public class ChangeDetectionGraphBuilder { + + private final GraphElementIndexer elementIndexer; + private final ScreenshotAssigner screenshotAssigner = new ScreenshotAssigner(); + private final ActionEdgeJoiner edgeJoiner = new ActionEdgeJoiner(); + private final MergedGraphFilter graphFilter = new MergedGraphFilter(); + + public ChangeDetectionGraphBuilder(ActionPrimaryKeyProvider primaryKeyProvider) { + this.elementIndexer = new GraphElementIndexer(primaryKeyProvider); + } + + /** + * Build merged graph elements (nodes + edges) from old/new model elements and change detection result. + */ + public List> build(String oldModelId, + String newModelId, + List> oldElements, + List> newElements, + ChangeDetectionResult result) { + + Objects.requireNonNull(oldElements, "oldElements"); + Objects.requireNonNull(newElements, "newElements"); + Objects.requireNonNull(result, "result"); + + Map idToStateOld = new HashMap(); + Map idToStateNew = new HashMap(); + Map> oldNodes = elementIndexer.indexNodesByStateId(oldElements, idToStateOld); + Map> newNodes = elementIndexer.indexNodesByStateId(newElements, idToStateNew); + Map> oldEdges = elementIndexer.indexEdgesByComparableKey(oldElements, idToStateOld); + Map> newEdges = elementIndexer.indexEdgesByComparableKey(newElements, idToStateNew); + + Map> mergedNodes = new LinkedHashMap>(); + for (Map.Entry> entry : newNodes.entrySet()) { + mergedNodes.put(entry.getKey(), entry.getValue()); + } + for (Map.Entry> entry : oldNodes.entrySet()) { + if (!mergedNodes.containsKey(entry.getKey())) { + mergedNodes.put(entry.getKey(), entry.getValue()); + } + } + + Map statusByState = new HashMap(); + for (org.testar.statemodel.changedetection.DeltaState s : result.getAddedStates()) { + statusByState.put(s.getStateId(), "added"); + } + for (org.testar.statemodel.changedetection.DeltaState s : result.getRemovedStates()) { + statusByState.put(s.getStateId(), "removed"); + } + for (String id : result.getChangedStates().keySet()) { + statusByState.putIfAbsent(id, "changed"); + } + for (String id : result.getChangedActions().keySet()) { + if (result.getChangedActions().get(id) != null && !result.getChangedActions().get(id).isEmpty()) { + statusByState.putIfAbsent(id, "changed"); + } + } + + // apply status to nodes and keep screenshots (old/new) when available + for (Map el : mergedNodes.values()) { + Map data = ElementUtils.getData(el); + if (data == null) { + continue; + } + String stateKey = ElementUtils.extractStateId(data); + String status = statusByState.containsKey(stateKey) ? statusByState.get(stateKey) : "unchanged"; + data.put("status", status); + } + + // Assign screenshots to abstract nodes from concrete screenshots + screenshotAssigner.assign(mergedNodes, oldNodes, oldEdges, newNodes, newEdges, oldModelId, newModelId, result); + + Map> mergedEdges = new LinkedHashMap>(); + Set edgeKeys = new HashSet(); + edgeKeys.addAll(newEdges.keySet()); + edgeKeys.addAll(oldEdges.keySet()); + + for (String key : edgeKeys) { + Map newEl = newEdges.get(key); + Map oldEl = oldEdges.get(key); + + String status; + Map targetEl; + if (newEl != null && oldEl != null) { + // When the comparable key matches, the action is considered the same + status = "unchanged"; + targetEl = newEl; + } else if (newEl != null) { + status = "added"; + targetEl = newEl; + } else { + status = "removed"; + targetEl = oldEl; + } + + Map targetData = ElementUtils.getData(targetEl); + if (targetData != null) { + targetData.put("status", status); + } + mergedEdges.put(key, targetEl); + } + + // Resolve primary keys and enforce label=primaryKey for all merged action edges. + for (Map edgeEl : mergedEdges.values()) { + Map data = ElementUtils.getData(edgeEl); + if (data == null) { + continue; + } + elementIndexer.ensureEdgePrimaryKeyAndLabel(data); + } + + edgeJoiner.joinActionsByPrimaryKey(mergedNodes, mergedEdges); + + // assign parents for layer grouping + boolean hasAbstract = false; + for (Map el : mergedNodes.values()) { + if (ElementUtils.hasClass(el, "AbstractState") || ElementUtils.hasClass(el, "BlackHole")) { + Map data = ElementUtils.getData(el); + if (data != null) { + data.put("parent", "abstract-layer"); + } + hasAbstract = true; + } + } + + List> merged = new ArrayList>(); + if (hasAbstract) { + merged.add(layerNode("abstract-layer", "AbstractLayer", "AbstractLayer")); + } + merged.addAll(mergedNodes.values()); + merged.addAll(mergedEdges.values()); + + return graphFilter.filter(merged); + } + + private Map layerNode(String id, String label, String className) { + Map data = new HashMap(); + data.put("id", id); + data.put("label", label); + Map el = new HashMap(); + el.put("group", "nodes"); + el.put("data", data); + List classes = new ArrayList(); + classes.add("Layer"); + classes.add(className); + el.put("classes", classes); + return el; + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java new file mode 100644 index 000000000..21625ae07 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java @@ -0,0 +1,119 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.testar.statemodel.analysis.AnalysisManager; +import org.testar.statemodel.analysis.jsonformat.Element; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.OrientDbActionPrimaryKeyProvider; +import org.testar.statemodel.persistence.PersistenceManager; +import org.testar.statemodel.persistence.orientdb.OrientDBManager; +import org.testar.statemodel.persistence.orientdb.entity.EntityManager; +import org.testar.statemodel.util.EventHelper; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Builds and returns the merged change-detection graph for the frontend. + */ +public class ChangeDetectionGraphServlet extends HttpServlet { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String oldModelId = req.getParameter("oldModelIdentifier"); + String newModelId = req.getParameter("newModelIdentifier"); + + if (oldModelId == null || newModelId == null || oldModelId.trim().isEmpty() || newModelId.trim().isEmpty()) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + resp.getWriter().write("Both oldModelIdentifier and newModelIdentifier are required."); + return; + } + + ServletContext ctx = getServletContext(); + AnalysisManager analysisManager = (AnalysisManager) ctx.getAttribute("analysisManager"); + + PersistenceManager pm = buildPersistenceManager(analysisManager); + boolean pmClosed = false; + try { + ChangeDetectionAnalysisService service = new ChangeDetectionAnalysisService(pm); + ChangeDetectionResult result = service.compare(oldModelId, newModelId); + + // close persistence used by the comparator to avoid plocal OrientDB lock conflicts + pm.shutdown(); + pmClosed = true; + + List> oldElements = loadGraphElements(analysisManager, oldModelId); + List> newElements = loadGraphElements(analysisManager, newModelId); + + // prepare the action primary key provider and builder to merge + EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); + try { + OrientDbActionPrimaryKeyProvider actionPrimaryKeyProvider = new OrientDbActionPrimaryKeyProvider(entityManager.getConnection()); + ChangeDetectionGraphBuilder graphBuilder = new ChangeDetectionGraphBuilder(actionPrimaryKeyProvider); + List> merged = graphBuilder.build(oldModelId, newModelId, oldElements, newElements, result); + resp.setContentType("application/json"); + resp.getWriter().write(mapper.writeValueAsString(merged)); + } finally { + entityManager.releaseConnection(); + } + } catch (Exception ex) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.getWriter().write("Failed to build merged graph: " + ex.getMessage()); + ex.printStackTrace(); + } finally { + if (!pmClosed) { + pm.shutdown(); + } + } + } + + private PersistenceManager buildPersistenceManager(AnalysisManager analysisManager) { + EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); + return new OrientDBManager(new EventHelper(), entityManager); + } + + private List> loadGraphElements(AnalysisManager analysisManager, String modelId) { + List elements = analysisManager.fetchGraphElementsForModel(modelId, true, true, false, false); + return mapper.convertValue(elements, List.class); + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java new file mode 100644 index 000000000..0f7fe22e8 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java @@ -0,0 +1,104 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.testar.statemodel.analysis.AnalysisManager; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.persistence.PersistenceManager; +import org.testar.statemodel.persistence.orientdb.OrientDBManager; +import org.testar.statemodel.persistence.orientdb.entity.EntityManager; +import org.testar.statemodel.util.EventHelper; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * - GET: forwards to changedetection.jsp with the list of models (and optional preselected ids) + * - POST: executes the change detection and returns JSON + */ +public class ChangeDetectionServlet extends HttpServlet { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletContext servletContext = getServletContext(); + AnalysisManager analysisManager = (AnalysisManager) servletContext.getAttribute("analysisManager"); + req.setAttribute("models", analysisManager.fetchModels()); + + // propagate optional preselected ids + req.setAttribute("oldModelIdentifier", req.getParameter("oldModelIdentifier")); + req.setAttribute("newModelIdentifier", req.getParameter("newModelIdentifier")); + + RequestDispatcher dispatcher = servletContext.getRequestDispatcher("/changedetection.jsp"); + dispatcher.forward(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String oldModelId = req.getParameter("oldModelIdentifier"); + String newModelId = req.getParameter("newModelIdentifier"); + + if (oldModelId == null || newModelId == null || oldModelId.trim().isEmpty() || newModelId.trim().isEmpty()) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + resp.getWriter().write("Both oldModelIdentifier and newModelIdentifier are required."); + return; + } + + ServletContext ctx = getServletContext(); + AnalysisManager analysisManager = (AnalysisManager) ctx.getAttribute("analysisManager"); + PersistenceManager pm = buildPersistenceManager(analysisManager); + try { + ChangeDetectionAnalysisService service = new ChangeDetectionAnalysisService(pm); + ChangeDetectionResult result = service.compare(oldModelId, newModelId); + + resp.setContentType("application/json"); + resp.getWriter().write(mapper.writeValueAsString(result)); + } finally { + if (pm != null) { + pm.shutdown(); + } + } + } + + private PersistenceManager buildPersistenceManager(AnalysisManager analysisManager) { + EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); + return new OrientDBManager(new EventHelper(), entityManager); + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ActionEdgeJoiner.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ActionEdgeJoiner.java new file mode 100644 index 000000000..c9c1a54d7 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ActionEdgeJoiner.java @@ -0,0 +1,150 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection.helpers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Visual post-processing for change detection graphs. + * + * Join edge action of same source + same primary key. + * + * In the merged visualization we collapse this pair into a single edge: + * - keep the "added" edge element + * - drop the "removed" edge element + * - mark the edge status as {@code unchanged} + * - mark the target node as {@code changed} + */ +public class ActionEdgeJoiner { + + public ActionEdgeJoiner() { } + + public void joinActionsByPrimaryKey(Map> mergedNodes, + Map> mergedEdges) { + Map> bySourceAndPk = new HashMap>(); + for (Map.Entry> entry : mergedEdges.entrySet()) { + Map data = ElementUtils.getData(entry.getValue()); + if (data == null) { + continue; + } + String src = ElementUtils.asString(data.get("source")); + String pk = ElementUtils.asString(data.get("label")); + if (src == null || pk == null) { + continue; + } + if (ElementUtils.hasClass(entry.getValue(), "isAbstractedBy")) { + continue; + } + String key = src + "|" + pk; + if (!bySourceAndPk.containsKey(key)) { + bySourceAndPk.put(key, new ArrayList()); + } + bySourceAndPk.get(key).add(entry.getKey()); + } + + Set toRemove = new HashSet(); + for (Map.Entry> group : bySourceAndPk.entrySet()) { + List edgeKeys = group.getValue(); + if (edgeKeys.size() < 2) { + continue; + } + + List validKeys = new ArrayList(); + for (String ek : edgeKeys) { + Map el = mergedEdges.get(ek); + if (el != null && ElementUtils.getData(el) != null) { + validKeys.add(ek); + } + } + if (validKeys.size() < 2) { + continue; + } + + boolean hasAdded = false; + boolean hasRemoved = false; + for (String k : validKeys) { + String st = ElementUtils.asString(ElementUtils.getData(mergedEdges.get(k)).get("status")); + if ("added".equals(st)) { + hasAdded = true; + } + if ("removed".equals(st)) { + hasRemoved = true; + } + } + if (!hasAdded || !hasRemoved) { + continue; + } + + String addedKey = null; + for (String k : validKeys) { + String st = ElementUtils.asString(ElementUtils.getData(mergedEdges.get(k)).get("status")); + if ("added".equals(st)) { + addedKey = k; + break; + } + } + + if (addedKey != null) { + Map data = ElementUtils.getData(mergedEdges.get(addedKey)); + data.put("status", "unchanged"); + + String targetId = ElementUtils.asString(data.get("target")); + Map targetNode = mergedNodes.get(targetId); + if (targetNode != null) { + Map tData = ElementUtils.getData(targetNode); + if (tData != null) { + String cur = ElementUtils.asString(tData.get("status")); + if (!"added".equals(cur) && !"removed".equals(cur)) { + tData.put("status", "changed"); + } + } + } + } + + for (String k : validKeys) { + String st = ElementUtils.asString(ElementUtils.getData(mergedEdges.get(k)).get("status")); + if ("removed".equals(st)) { + toRemove.add(k); + } + } + } + + for (String key : toRemove) { + mergedEdges.remove(key); + } + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ElementUtils.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ElementUtils.java new file mode 100644 index 000000000..982011680 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ElementUtils.java @@ -0,0 +1,96 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection.helpers; + +import java.util.List; +import java.util.Map; + +public class ElementUtils { + + private ElementUtils() { } + + @SuppressWarnings("unchecked") + public static Map getData(Map element) { + if (element == null) { + return null; + } + Object data = element.get("data"); + if (data instanceof Map) { + return (Map) data; + } + return null; + } + + public static boolean hasClass(Map element, String cls) { + Object classes = element != null ? element.get("classes") : null; + if (classes instanceof List) { + List list = (List) classes; + return list.contains(cls); + } + return false; + } + + public static String asString(Object value) { + if (value instanceof String) { + String v = (String) value; + return v.isEmpty() ? null : v; + } + return null; + } + + public static String firstNonNull(String... values) { + if (values == null) { + return null; + } + for (String v : values) { + if (v != null && !v.isEmpty()) { + return v; + } + } + return null; + } + + public static String extractStateId(Map data) { + if (data == null) { + return null; + } + Object stateId = data.get("stateId"); + if (stateId instanceof String && !((String) stateId).isEmpty()) { + return (String) stateId; + } + Object id = data.get("id"); + if (id instanceof String && !((String) id).isEmpty()) { + return (String) id; + } + return null; + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java new file mode 100644 index 000000000..3305fa89f --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java @@ -0,0 +1,174 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection.helpers; + +import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Normalizes and indexes Cytoscape graph elements to support stable merging: + * - node ids are normalized to their state id (AbstractId / ConcreteId) + * - original node ids are stored as {@code rawId} for screenshot paths + * - edge endpoints are normalized from raw node ids to state ids + * - edge labels are resolved via {@link ActionPrimaryKeyProvider} (Desc preferred, fallback actionId) + */ +public class GraphElementIndexer { + + private final ActionPrimaryKeyProvider primaryKeyProvider; + + public GraphElementIndexer(ActionPrimaryKeyProvider primaryKeyProvider) { + this.primaryKeyProvider = primaryKeyProvider; + } + + public Map> indexNodesByStateId(List> elements, Map rawIdToStateId) { + Objects.requireNonNull(elements, "elements"); + Objects.requireNonNull(rawIdToStateId, "rawIdToStateId"); + + Map> nodes = new LinkedHashMap>(); + for (Map el : elements) { + if (!"nodes".equals(el.get("group"))) { + continue; + } + Map data = ElementUtils.getData(el); + if (data == null) { + continue; + } + String stateId = ElementUtils.extractStateId(data); + String rawId = ElementUtils.asString(data.get("id")); + if (rawId != null && stateId != null) { + rawIdToStateId.put(rawId, stateId); + } + if (stateId != null) { + if (rawId != null) { + data.put("rawId", rawId); + } + data.put("id", stateId); + nodes.put(stateId, el); + } + } + return nodes; + } + + public Map> indexEdgesByComparableKey(List> elements, Map rawIdToStateId) { + Objects.requireNonNull(elements, "elements"); + Objects.requireNonNull(rawIdToStateId, "rawIdToStateId"); + + Map> edges = new LinkedHashMap>(); + for (Map el : elements) { + if (!"edges".equals(el.get("group"))) { + continue; + } + Map data = ElementUtils.getData(el); + if (data == null) { + continue; + } + + ensureEdgeLabel(data); + normalizeEdgeEndpoints(data, rawIdToStateId); + String key = edgeComparableKey(data); + if (key != null) { + edges.put(key, el); + } + } + return edges; + } + + public void ensureEdgePrimaryKeyAndLabel(Map data) { + Objects.requireNonNull(data, "data"); + String pk = resolvePrimaryKey(data); + data.put("primaryKey", pk); + data.put("label", pk); + } + + private void ensureEdgeLabel(Map data) { + if (ElementUtils.asString(data.get("label")) == null) { + ensureEdgePrimaryKeyAndLabel(data); + } + } + + private void normalizeEdgeEndpoints(Map data, Map rawIdToStateId) { + String sourceRaw = ElementUtils.asString(data.get("source")); + String targetRaw = ElementUtils.asString(data.get("target")); + String sourceCanonical = canonicalStateId(sourceRaw, rawIdToStateId); + String targetCanonical = canonicalStateId(targetRaw, rawIdToStateId); + if (sourceCanonical != null) { + data.put("source", sourceCanonical); + } + if (targetCanonical != null) { + data.put("target", targetCanonical); + } + } + + private String canonicalStateId(String rawNodeId, Map rawIdToStateId) { + if (rawNodeId == null) { + return null; + } + String state = rawIdToStateId.get(rawNodeId); + return state != null ? state : rawNodeId; + } + + String edgeComparableKey(Map data) { + String source = ElementUtils.asString(data.get("source")); + String target = ElementUtils.asString(data.get("target")); + if (source == null || target == null) { + return null; + } + String label = ElementUtils.asString(data.get("label")); + return source + "|" + target + "|" + (label == null ? "" : label); + } + + private String resolvePrimaryKey(Map data) { + String actionId = ElementUtils.asString(data.get("actionId")); + String pk = null; + String existingText = ElementUtils.firstNonNull( + ElementUtils.asString(data.get("primaryKey")), + ElementUtils.asString(data.get("label")) + ); + if (primaryKeyProvider != null && actionId != null) { + try { + pk = primaryKeyProvider.getPrimaryKey(actionId); + } catch (Exception ignored) { + } + } + if (pk == null) { + pk = existingText; + } + if (pk == null) { + pk = actionId; + } + return pk != null ? pk : ""; + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/MergedGraphFilter.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/MergedGraphFilter.java new file mode 100644 index 000000000..73e732776 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/MergedGraphFilter.java @@ -0,0 +1,119 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection.helpers; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Filters the merged graph elements to keep the change-detection view usable: + * - Black holes and connected action edges + * - Concrete state nodes and any edges connected to them + */ +public class MergedGraphFilter { + + public MergedGraphFilter() { } + + public List> filter(List> merged) { + Set blackHoleIds = collectIdsByClass(merged, "BlackHole"); + Set concreteIds = collectIdsByClass(merged, "ConcreteState"); + + List> filtered = new ArrayList>(); + Set connectedIds = new HashSet(); + + for (Map el : merged) { + Object group = el.get("group"); + if ("edges".equals(group)) { + Map data = ElementUtils.getData(el); + String source = data != null ? ElementUtils.asString(data.get("source")) : null; + String target = data != null ? ElementUtils.asString(data.get("target")) : null; + if ((source != null && (blackHoleIds.contains(source) || concreteIds.contains(source))) + || (target != null && (blackHoleIds.contains(target) || concreteIds.contains(target)))) { + continue; + } + if (source != null) { + connectedIds.add(source); + } + if (target != null) { + connectedIds.add(target); + } + filtered.add(el); + } + } + + for (Map el : merged) { + Object group = el.get("group"); + if ("nodes".equals(group)) { + Map data = ElementUtils.getData(el); + String id = data != null ? ElementUtils.asString(data.get("id")) : null; + if (id != null && (blackHoleIds.contains(id) || concreteIds.contains(id))) { + continue; + } + String status = data != null ? ElementUtils.asString(data.get("status")) : null; + boolean keep = (id != null && connectedIds.contains(id)) || (status != null && !"unchanged".equals(status)); + if (keep) { + filtered.add(el); + } + } else if (!"edges".equals(group)) { + Map data = ElementUtils.getData(el); + String id = data != null ? ElementUtils.asString(data.get("id")) : null; + if ("abstract-layer".equals(id)) { + filtered.add(el); + } + } + } + + return filtered; + } + + private Set collectIdsByClass(List> elements, String cls) { + Set ids = new HashSet(); + for (Map el : elements) { + if (!"nodes".equals(el.get("group"))) { + continue; + } + if (ElementUtils.hasClass(el, cls)) { + Map data = ElementUtils.getData(el); + if (data != null) { + String id = ElementUtils.asString(data.get("id")); + if (id != null) { + ids.add(id); + } + } + } + } + return ids; + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java new file mode 100644 index 000000000..28f453c33 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java @@ -0,0 +1,182 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection.helpers; + +import org.testar.statemodel.analysis.AnalysisManager; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.PropertyDiff; +import org.testar.statemodel.changedetection.VertexPropertyDiff; + +import java.util.HashMap; +import java.util.Map; + +/** + * Assigns screenshot URLs from concrete nodes to abstract state nodes in the merged change-detection graph. + */ +public class ScreenshotAssigner { + + public ScreenshotAssigner() { } + + public void assign(Map> mergedNodes, + Map> oldNodes, + Map> oldEdges, + Map> newNodes, + Map> newEdges, + String oldModelId, + String newModelId, + ChangeDetectionResult result) { + + Map oldAbstractShot = computeAbstractScreenshotMap(oldNodes, oldEdges, oldModelId); + Map newAbstractShot = computeAbstractScreenshotMap(newNodes, newEdges, newModelId); + Map newToOld = extractChangedStateMapping(result); + + for (Map.Entry> entry : mergedNodes.entrySet()) { + String stateId = entry.getKey(); + Map el = entry.getValue(); + if (!ElementUtils.hasClass(el, "AbstractState")) { + continue; + } + Map data = ElementUtils.getData(el); + if (data == null) { + continue; + } + + String oldShot = oldAbstractShot.get(stateId); + String newShot = newAbstractShot.get(stateId); + if (oldShot != null) { + data.put("oldScreenshot", oldShot); + } + if (newShot != null) { + data.put("newScreenshot", newShot); + } + + String mappedOld = newToOld.get(stateId); + if (mappedOld != null) { + String mappedOldShot = oldAbstractShot.get(mappedOld); + if (mappedOldShot != null) { + data.put("oldScreenshot", mappedOldShot); + } + data.put("oldStateId", mappedOld); + } + + String fallback = ElementUtils.firstNonNull( + ElementUtils.asString(data.get("newScreenshot")), + ElementUtils.asString(data.get("oldScreenshot")) + ); + if (fallback != null) { + data.put("screenshot", fallback); + } + } + } + + private Map computeAbstractScreenshotMap(Map> nodesByStateId, + Map> edgesByKey, + String modelId) { + Map concreteShotByConcreteStateId = new HashMap(); + for (Map nodeEl : nodesByStateId.values()) { + if (!ElementUtils.hasClass(nodeEl, "ConcreteState")) { + continue; + } + Map data = ElementUtils.getData(nodeEl); + if (data == null) { + continue; + } + String concreteStateId = ElementUtils.asString(data.get("id")); + String path = buildScreenshotPath(modelId, data); + if (concreteStateId != null && path != null) { + concreteShotByConcreteStateId.put(concreteStateId, path); + } + } + + Map abstractShotByAbstractStateId = new HashMap(); + for (Map edgeEl : edgesByKey.values()) { + if (!ElementUtils.hasClass(edgeEl, "isAbstractedBy")) { + continue; + } + Map data = ElementUtils.getData(edgeEl); + if (data == null) { + continue; + } + String concreteStateId = ElementUtils.asString(data.get("source")); + String abstractStateId = ElementUtils.asString(data.get("target")); + if (concreteStateId == null || abstractStateId == null) { + continue; + } + if (abstractShotByAbstractStateId.containsKey(abstractStateId)) { + continue; + } + String shot = concreteShotByConcreteStateId.get(concreteStateId); + if (shot != null) { + abstractShotByAbstractStateId.put(abstractStateId, shot); + } + } + + return abstractShotByAbstractStateId; + } + + private Map extractChangedStateMapping(ChangeDetectionResult result) { + Map newToOld = new HashMap(); + for (Map.Entry entry : result.getChangedStates().entrySet()) { + String newId = entry.getKey(); + VertexPropertyDiff diff = entry.getValue(); + if (diff == null) { + continue; + } + for (PropertyDiff pd : diff.getChanged()) { + if (pd == null) { + continue; + } + if ("stateId".equals(pd.getPropertyName()) + && pd.getOldValue() != null + && !pd.getOldValue().trim().isEmpty()) { + newToOld.put(newId, pd.getOldValue()); + break; + } + } + } + return newToOld; + } + + private String buildScreenshotPath(String modelId, Map data) { + if (modelId == null) { + return null; + } + Object raw = data.get("rawId"); + if (raw == null) { + raw = data.get("id"); + } + if (raw instanceof String && !((String) raw).isEmpty()) { + return modelId + "/" + raw + ".png"; + } + return null; + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/webserver/JettyServer.java b/statemodel/src/org/testar/statemodel/analysis/webserver/JettyServer.java index ed9bf4d89..52c403641 100644 --- a/statemodel/src/org/testar/statemodel/analysis/webserver/JettyServer.java +++ b/statemodel/src/org/testar/statemodel/analysis/webserver/JettyServer.java @@ -33,6 +33,8 @@ import org.testar.statemodel.analysis.AnalysisManager; import org.testar.statemodel.analysis.GraphServlet; import org.testar.statemodel.analysis.StateModelServlet; +import org.testar.statemodel.analysis.changedetection.ChangeDetectionGraphServlet; +import org.testar.statemodel.analysis.changedetection.ChangeDetectionServlet; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -69,6 +71,8 @@ public void start(String resourceBase, AnalysisManager analysisManager) throws E webAppContext.setResourceBase(resourceBase); webAppContext.addServlet(new ServletHolder(new StateModelServlet()), "/models"); webAppContext.addServlet(new ServletHolder(new GraphServlet()), "/graph"); + webAppContext.addServlet(new ServletHolder(new ChangeDetectionServlet()), "/changedetection"); + webAppContext.addServlet(new ServletHolder(new ChangeDetectionGraphServlet()), "/changedetection-graph"); webAppContext.setAttribute("analysisManager", analysisManager); Configuration.ClassList classlist = Configuration.ClassList diff --git a/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java b/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java new file mode 100644 index 000000000..0c736d8c6 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java @@ -0,0 +1,326 @@ +package org.testar.statemodel.analysis.changedetection; + +import org.junit.Test; +import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.ActionSetDiff; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.DeltaAction; +import org.testar.statemodel.changedetection.DeltaState; +import org.testar.statemodel.changedetection.DiffType; +import org.testar.statemodel.changedetection.PropertyDiff; +import org.testar.statemodel.changedetection.VertexPropertyDiff; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ChangeDetectionGraphBuilderTest { + + private final ActionPrimaryKeyProvider pkProvider = new ActionPrimaryKeyProvider() { + @Override + public String getPrimaryKey(String actionId) { + if(actionId.equals("actionId-11")) return "click Help"; + else if(actionId.equals("actionId-12")) return "click View"; + else return "click action"; + } + }; + + @Test + public void testGraphJoinsActionEdgesByKeyProvider() { + List> oldElements = new ArrayList>(); + List> newElements = new ArrayList>(); + oldElements.add(node("S1")); + oldElements.add(node("S2")); + newElements.add(node("S1")); + newElements.add(node("S3")); + + oldElements.add(edge("S1", "S2", "A1")); + newElements.add(edge("S1", "S3", "A2")); + + ChangeDetectionResult result = new ChangeDetectionResult( + "old", "new", + Arrays.asList(delta("S3")), // added states + Arrays.asList(delta("S2")), // removed states + Collections.emptyMap(), + Collections.emptyMap()); + + ChangeDetectionGraphBuilder builder = new ChangeDetectionGraphBuilder(pkProvider); + List> merged = builder.build("old", "new", oldElements, newElements, result); + + List> actionEdges = new ArrayList>(); + for (Map el : merged) { + if ("edges".equals(el.get("group"))) { + actionEdges.add(el); + } + } + + assertEquals("Only one action edge should remain after merge process", 1, actionEdges.size()); + Map actionData = getData(actionEdges.get(0)); + assertEquals("unchanged", actionData.get("status")); + assertEquals("S3", actionData.get("target")); + assertEquals("click action", actionData.get("label")); + assertEquals("click action", actionData.get("primaryKey")); + + for (Map e : actionEdges) { + Map d = getData(e); + assertFalse("No edge should be marked removed", "removed".equals(d.get("status"))); + } + } + + @Test + public void testGraphShowsAddedRemovedStates() { + List> oldElements = new ArrayList>(); + List> newElements = new ArrayList>(); + oldElements.add(node("S1")); + oldElements.add(node("S2")); + newElements.add(node("S1")); + newElements.add(node("S3")); + + oldElements.add(edge("S1", "S2", "actionId-11")); + newElements.add(edge("S1", "S3", "actionId-12")); + + ChangeDetectionResult result = new ChangeDetectionResult( + "old", "new", + Arrays.asList(delta("S3")), // added states + Arrays.asList(delta("S2")), // removed states + Collections.emptyMap(), + Collections.emptyMap()); + + ChangeDetectionGraphBuilder builder = new ChangeDetectionGraphBuilder(pkProvider); + List> merged = builder.build("old", "new", oldElements, newElements, result); + + List> actionEdges = new ArrayList>(); + for (Map el : merged) { + if ("edges".equals(el.get("group"))) { + actionEdges.add(el); + } + } + + assertEquals("Two action edges should remain after merge process", 2, actionEdges.size()); + Map actionDataOne = getData(actionEdges.get(0)); + Map actionDataTwo = getData(actionEdges.get(1)); + + assertNotEquals("unchanged", actionDataOne.get("status")); + assertNotEquals("unchanged", actionDataTwo.get("status")); + + assertNotEquals("changed", actionDataOne.get("status")); + assertNotEquals("changed", actionDataTwo.get("status")); + + assertTrue("Edge status must be added or removed", + "added".equals(actionDataOne.get("status")) || "removed".equals(actionDataOne.get("status"))); + assertTrue("Edge status must be added or removed", + "added".equals(actionDataTwo.get("status")) || "removed".equals(actionDataTwo.get("status"))); + + assertTrue("Edge target must be S2 or S3", + "S2".equals(actionDataOne.get("target")) || "S3".equals(actionDataOne.get("target"))); + assertTrue("Edge target must be S2 or S3", + "S2".equals(actionDataTwo.get("target")) || "S3".equals(actionDataTwo.get("target"))); + + assertTrue("Edge label must be resolved to a primary key", + "click Help".equals(actionDataOne.get("label")) || "click View".equals(actionDataOne.get("label"))); + assertTrue("Edge label must be resolved to a primary key", + "click Help".equals(actionDataTwo.get("label")) || "click View".equals(actionDataTwo.get("label"))); + + assertTrue("Edge primaryKey must be resolved to a primary key", + "click Help".equals(actionDataOne.get("primaryKey")) || "click View".equals(actionDataOne.get("primaryKey"))); + assertTrue("Edge primaryKey must be resolved to a primary key", + "click Help".equals(actionDataTwo.get("primaryKey")) || "click View".equals(actionDataTwo.get("primaryKey"))); + + int addedCount = 0; + int removedCount = 0; + for (Map e : actionEdges) { + Map d = getData(e); + String status = (String) d.get("status"); + assertTrue("Edge status must be added or removed", "added".equals(status) || "removed".equals(status)); + if ("added".equals(status)) { + addedCount++; + } else if ("removed".equals(status)) { + removedCount++; + } + } + assertEquals("Expected exactly one added edge", 1, addedCount); + assertEquals("Expected exactly one removed edge", 1, removedCount); + } + + @Test + public void testChangedStateShowsOldAndNewScreenshots() { + List> oldElements = new ArrayList>(); + List> newElements = new ArrayList>(); + + // Old: concrete -> abstract + oldElements.add(abstractNode("nA_old", "SA_old")); + oldElements.add(concreteNode("nC_old", "SC_old")); + oldElements.add(isAbstractedBy("nC_old", "nA_old")); + + // New: concrete -> abstract + newElements.add(abstractNode("nA_new", "SA_new")); + newElements.add(concreteNode("nC_new", "SC_new")); + newElements.add(isAbstractedBy("nC_new", "nA_new")); + + // Result contains a mapping as a stateId diff (old -> new) + VertexPropertyDiff diff = new VertexPropertyDiff( + Collections.emptyList(), + Collections.emptyList(), + Arrays.asList(new PropertyDiff("stateId", "SA_old", "SA_new", DiffType.CHANGED)) + ); + Map changedStates = new HashMap(); + changedStates.put("SA_new", diff); + + ChangeDetectionResult result = new ChangeDetectionResult( + "old", "new", + Collections.emptyList(), + Collections.emptyList(), + changedStates, + Collections.emptyMap() + ); + + ChangeDetectionGraphBuilder builder = new ChangeDetectionGraphBuilder(actionId -> actionId); + List> merged = builder.build("old", "new", oldElements, newElements, result); + + Map newNode = findNode(merged, "SA_new"); + assertNotNull(newNode); + Map data = getData(newNode); + assertEquals("changed", data.get("status")); + assertEquals("old/nC_old.png", data.get("oldScreenshot")); + assertEquals("new/nC_new.png", data.get("newScreenshot")); + assertEquals("new/nC_new.png", data.get("screenshot")); + assertEquals("SA_old", data.get("oldStateId")); + } + + @Test + public void testAddedStateIsNotChanged() { + List> oldElements = new ArrayList>(); + List> newElements = new ArrayList>(); + oldElements.add(node("S1")); + newElements.add(node("S1")); + newElements.add(node("S3")); + + newElements.add(edge("S1", "S3", "A11")); + + Map changedActions = new HashMap(); + changedActions.put("S3", new ActionSetDiff( + Arrays.asList(new DeltaAction("A11", "click", DeltaAction.Direction.INCOMING)), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + )); + + ChangeDetectionResult result = new ChangeDetectionResult( + "old", "new", + Arrays.asList(delta("S3")), + Collections.emptyList(), + Collections.emptyMap(), + changedActions + ); + + ChangeDetectionGraphBuilder builder = new ChangeDetectionGraphBuilder(pkProvider); + List> merged = builder.build("old", "new", oldElements, newElements, result); + + Map addedNode = findNode(merged, "S3"); + assertNotNull(addedNode); + assertEquals("added", getData(addedNode).get("status")); + } + + private Map node(String id) { + Map data = new HashMap(); + data.put("id", id); + List classes = new ArrayList(); + classes.add("AbstractState"); + Map el = new HashMap(); + el.put("group", "nodes"); + el.put("data", data); + el.put("classes", classes); + return el; + } + + private Map abstractNode(String rawId, String stateId) { + Map data = new HashMap(); + data.put("id", rawId); + data.put("stateId", stateId); + List classes = new ArrayList(); + classes.add("AbstractState"); + Map el = new HashMap(); + el.put("group", "nodes"); + el.put("data", data); + el.put("classes", classes); + return el; + } + + private Map concreteNode(String rawId, String stateId) { + Map data = new HashMap(); + data.put("id", rawId); + data.put("stateId", stateId); + List classes = new ArrayList(); + classes.add("ConcreteState"); + Map el = new HashMap(); + el.put("group", "nodes"); + el.put("data", data); + el.put("classes", classes); + return el; + } + + private Map isAbstractedBy(String rawConcreteId, String rawAbstractId) { + Map data = new HashMap(); + data.put("source", rawConcreteId); + data.put("target", rawAbstractId); + List classes = new ArrayList(); + classes.add("isAbstractedBy"); + Map el = new HashMap(); + el.put("group", "edges"); + el.put("data", data); + el.put("classes", classes); + return el; + } + + private Map edge(String source, String target, String actionId) { + Map data = new HashMap(); + data.put("source", source); + data.put("target", target); + data.put("actionId", actionId); + List classes = new ArrayList(); + classes.add("AbstractAction"); + Map el = new HashMap(); + el.put("group", "edges"); + el.put("data", data); + el.put("classes", classes); + return el; + } + + private DeltaState delta(String stateId) { + return new DeltaState(stateId, Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList()); + } + + @SuppressWarnings("unchecked") + private Map getData(Map el) { + Object d = el.get("data"); + if (d instanceof Map) { + return (Map) d; + } + return Collections.emptyMap(); + } + + private Map findNode(List> elements, String nodeId) { + for (Map el : elements) { + if (!"nodes".equals(el.get("group"))) { + continue; + } + Map data = getData(el); + if (nodeId.equals(data.get("id"))) { + return el; + } + } + return null; + } + +} From a042ab841e8b0ebb603c08abb3496f438c481381 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 18:42:35 +0100 Subject: [PATCH 11/15] update action coding manager test --- core/test/org/testar/TestCodingManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/test/org/testar/TestCodingManager.java b/core/test/org/testar/TestCodingManager.java index 21ccc92c7..23ee11b23 100644 --- a/core/test/org/testar/TestCodingManager.java +++ b/core/test/org/testar/TestCodingManager.java @@ -69,7 +69,7 @@ public void testActionCodingIDs() { // Build and check IDs for the action are set correctly CodingManager.buildIDs(state, Collections.singleton(action)); - Assert.assertEquals(action.get(Tags.AbstractID), "AA1sahtjg1c4157641605"); - Assert.assertEquals(action.get(Tags.ConcreteID), "ACd7vwql27266850918"); + Assert.assertEquals(action.get(Tags.AbstractID), "AA15k44gp2d1871300549"); + Assert.assertEquals(action.get(Tags.ConcreteID), "AC1dn3okk412646313092"); } } From ed1deb9b3afc25d169d99cc60ad3f02cbf83ae32 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 18:45:26 +0100 Subject: [PATCH 12/15] Changedetection analysis builder and servlet refactor --- .../ChangeDetectionFacade.java | 109 ++++++++++++++++++ .../ChangeDetectionGraphBuilder.java | 20 +--- .../ChangeDetectionGraphServlet.java | 49 +------- .../ChangeDetectionServlet.java | 23 +--- .../helpers/StatusResolver.java | 75 ++++++++++++ 5 files changed, 196 insertions(+), 80 deletions(-) create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java create mode 100644 statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java new file mode 100644 index 000000000..fbc917653 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java @@ -0,0 +1,109 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testar.statemodel.analysis.AnalysisManager; +import org.testar.statemodel.analysis.jsonformat.Element; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.OrientDbActionPrimaryKeyProvider; +import org.testar.statemodel.persistence.PersistenceManager; +import org.testar.statemodel.persistence.orientdb.OrientDBManager; +import org.testar.statemodel.persistence.orientdb.entity.EntityManager; +import org.testar.statemodel.util.EventHelper; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Facade for change detection analysis: + * - build a persistence manager to extract abstract state models + * - run the change detection engine + * - load graph elements for old/new models + * - merge them into a change-detection visualization graph + */ +final class ChangeDetectionFacade { + + private final AnalysisManager analysisManager; + private final ObjectMapper mapper; + + public ChangeDetectionFacade(AnalysisManager analysisManager, ObjectMapper mapper) { + this.analysisManager = Objects.requireNonNull(analysisManager, "analysisManager"); + this.mapper = Objects.requireNonNull(mapper, "mapper"); + } + + public ChangeDetectionResult compare(String oldModelId, String newModelId) { + PersistenceManager pm = buildPersistenceManager(); + try { + ChangeDetectionAnalysisService service = new ChangeDetectionAnalysisService(pm); + return service.compare(oldModelId, newModelId); + } finally { + pm.shutdown(); + } + } + + public List> buildMergedGraph(String oldModelId, String newModelId) { + // Run comparison first and close the PM before loading graphs (prevents local plocal locks). + PersistenceManager pm = buildPersistenceManager(); + ChangeDetectionResult result; + try { + ChangeDetectionAnalysisService service = new ChangeDetectionAnalysisService(pm); + result = service.compare(oldModelId, newModelId); + } finally { + pm.shutdown(); + } + + List> oldElements = loadGraphElements(oldModelId); + List> newElements = loadGraphElements(newModelId); + + EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); + try { + OrientDbActionPrimaryKeyProvider actionPrimaryKeyProvider = new OrientDbActionPrimaryKeyProvider(entityManager.getConnection()); + ChangeDetectionGraphBuilder graphBuilder = new ChangeDetectionGraphBuilder(actionPrimaryKeyProvider); + return graphBuilder.build(oldModelId, newModelId, oldElements, newElements, result); + } finally { + entityManager.releaseConnection(); + } + } + + private PersistenceManager buildPersistenceManager() { + EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); + return new OrientDBManager(new EventHelper(), entityManager); + } + + @SuppressWarnings("unchecked") + private List> loadGraphElements(String modelId) { + List elements = analysisManager.fetchGraphElementsForModel(modelId, true, true, false, false); + return mapper.convertValue(elements, List.class); + } + +} diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java index ffe740475..f94bfe1e4 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java @@ -35,6 +35,7 @@ import org.testar.statemodel.analysis.changedetection.helpers.GraphElementIndexer; import org.testar.statemodel.analysis.changedetection.helpers.MergedGraphFilter; import org.testar.statemodel.analysis.changedetection.helpers.ScreenshotAssigner; +import org.testar.statemodel.analysis.changedetection.helpers.StatusResolver; import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; import org.testar.statemodel.changedetection.ChangeDetectionResult; @@ -56,6 +57,7 @@ public class ChangeDetectionGraphBuilder { private final ScreenshotAssigner screenshotAssigner = new ScreenshotAssigner(); private final ActionEdgeJoiner edgeJoiner = new ActionEdgeJoiner(); private final MergedGraphFilter graphFilter = new MergedGraphFilter(); + private final StatusResolver statusResolver = new StatusResolver(); public ChangeDetectionGraphBuilder(ActionPrimaryKeyProvider primaryKeyProvider) { this.elementIndexer = new GraphElementIndexer(primaryKeyProvider); @@ -91,21 +93,7 @@ public List> build(String oldModelId, } } - Map statusByState = new HashMap(); - for (org.testar.statemodel.changedetection.DeltaState s : result.getAddedStates()) { - statusByState.put(s.getStateId(), "added"); - } - for (org.testar.statemodel.changedetection.DeltaState s : result.getRemovedStates()) { - statusByState.put(s.getStateId(), "removed"); - } - for (String id : result.getChangedStates().keySet()) { - statusByState.putIfAbsent(id, "changed"); - } - for (String id : result.getChangedActions().keySet()) { - if (result.getChangedActions().get(id) != null && !result.getChangedActions().get(id).isEmpty()) { - statusByState.putIfAbsent(id, "changed"); - } - } + Map statusByState = statusResolver.buildStatusByState(result); // apply status to nodes and keep screenshots (old/new) when available for (Map el : mergedNodes.values()) { @@ -114,7 +102,7 @@ public List> build(String oldModelId, continue; } String stateKey = ElementUtils.extractStateId(data); - String status = statusByState.containsKey(stateKey) ? statusByState.get(stateKey) : "unchanged"; + String status = statusByState.containsKey(stateKey) ? statusByState.get(stateKey) : StatusResolver.UNCHANGED; data.put("status", status); } diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java index 21625ae07..86f01c652 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java @@ -33,13 +33,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.testar.statemodel.analysis.AnalysisManager; -import org.testar.statemodel.analysis.jsonformat.Element; -import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.changedetection.OrientDbActionPrimaryKeyProvider; -import org.testar.statemodel.persistence.PersistenceManager; -import org.testar.statemodel.persistence.orientdb.OrientDBManager; -import org.testar.statemodel.persistence.orientdb.entity.EntityManager; -import org.testar.statemodel.util.EventHelper; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -47,7 +40,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.List; import java.util.Map; /** @@ -71,49 +63,16 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se ServletContext ctx = getServletContext(); AnalysisManager analysisManager = (AnalysisManager) ctx.getAttribute("analysisManager"); - PersistenceManager pm = buildPersistenceManager(analysisManager); - boolean pmClosed = false; try { - ChangeDetectionAnalysisService service = new ChangeDetectionAnalysisService(pm); - ChangeDetectionResult result = service.compare(oldModelId, newModelId); - - // close persistence used by the comparator to avoid plocal OrientDB lock conflicts - pm.shutdown(); - pmClosed = true; - - List> oldElements = loadGraphElements(analysisManager, oldModelId); - List> newElements = loadGraphElements(analysisManager, newModelId); - - // prepare the action primary key provider and builder to merge - EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); - try { - OrientDbActionPrimaryKeyProvider actionPrimaryKeyProvider = new OrientDbActionPrimaryKeyProvider(entityManager.getConnection()); - ChangeDetectionGraphBuilder graphBuilder = new ChangeDetectionGraphBuilder(actionPrimaryKeyProvider); - List> merged = graphBuilder.build(oldModelId, newModelId, oldElements, newElements, result); - resp.setContentType("application/json"); - resp.getWriter().write(mapper.writeValueAsString(merged)); - } finally { - entityManager.releaseConnection(); - } + ChangeDetectionFacade facade = new ChangeDetectionFacade(analysisManager, mapper); + java.util.List> merged = facade.buildMergedGraph(oldModelId, newModelId); + resp.setContentType("application/json"); + resp.getWriter().write(mapper.writeValueAsString(merged)); } catch (Exception ex) { resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); resp.getWriter().write("Failed to build merged graph: " + ex.getMessage()); ex.printStackTrace(); - } finally { - if (!pmClosed) { - pm.shutdown(); - } } } - private PersistenceManager buildPersistenceManager(AnalysisManager analysisManager) { - EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); - return new OrientDBManager(new EventHelper(), entityManager); - } - - private List> loadGraphElements(AnalysisManager analysisManager, String modelId) { - List elements = analysisManager.fetchGraphElementsForModel(modelId, true, true, false, false); - return mapper.convertValue(elements, List.class); - } - } diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java index 0f7fe22e8..ed9c496af 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.java @@ -34,10 +34,6 @@ import org.testar.statemodel.analysis.AnalysisManager; import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.persistence.PersistenceManager; -import org.testar.statemodel.persistence.orientdb.OrientDBManager; -import org.testar.statemodel.persistence.orientdb.entity.EntityManager; -import org.testar.statemodel.util.EventHelper; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; @@ -82,23 +78,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S ServletContext ctx = getServletContext(); AnalysisManager analysisManager = (AnalysisManager) ctx.getAttribute("analysisManager"); - PersistenceManager pm = buildPersistenceManager(analysisManager); - try { - ChangeDetectionAnalysisService service = new ChangeDetectionAnalysisService(pm); - ChangeDetectionResult result = service.compare(oldModelId, newModelId); - resp.setContentType("application/json"); - resp.getWriter().write(mapper.writeValueAsString(result)); - } finally { - if (pm != null) { - pm.shutdown(); - } - } - } + ChangeDetectionFacade facade = new ChangeDetectionFacade(analysisManager, mapper); + ChangeDetectionResult result = facade.compare(oldModelId, newModelId); - private PersistenceManager buildPersistenceManager(AnalysisManager analysisManager) { - EntityManager entityManager = new EntityManager(analysisManager.getDbConfig()); - return new OrientDBManager(new EventHelper(), entityManager); + resp.setContentType("application/json"); + resp.getWriter().write(mapper.writeValueAsString(result)); } } diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java new file mode 100644 index 000000000..edc999f06 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java @@ -0,0 +1,75 @@ +/*************************************************************************************************** + * + * Copyright (c) 2025 Open Universiteit - www.ou.nl + * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************************************/ + +package org.testar.statemodel.analysis.changedetection.helpers; + +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.DeltaState; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Computes node statuses for change detection visualization: + * - added/removed dominates everything + * - changed applies only if the state is not already added/removed + * - otherwise unchanged + */ +public final class StatusResolver { + + public static final String ADDED = "added"; + public static final String REMOVED = "removed"; + public static final String CHANGED = "changed"; + public static final String UNCHANGED = "unchanged"; + + public Map buildStatusByState(ChangeDetectionResult result) { + Objects.requireNonNull(result, "result"); + + Map statusByState = new HashMap(); + for (DeltaState s : result.getAddedStates()) { + statusByState.put(s.getStateId(), ADDED); + } + for (DeltaState s : result.getRemovedStates()) { + statusByState.put(s.getStateId(), REMOVED); + } + for (String id : result.getChangedStates().keySet()) { + statusByState.putIfAbsent(id, CHANGED); + } + for (String id : result.getChangedActions().keySet()) { + if (result.getChangedActions().get(id) != null && !result.getChangedActions().get(id).isEmpty()) { + statusByState.putIfAbsent(id, CHANGED); + } + } + + return statusByState; + } + +} From c647eed9596c7324d97d5237a56dbea7063877e6 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 18:55:22 +0100 Subject: [PATCH 13/15] clean changedetection classes --- .../changedetection/ActionSetComparator.java | 74 --------------- .../changedetection/StateSnapshot.java | 89 ------------------- .../changedetection/StateSnapshotFactory.java | 83 ----------------- .../StateSnapshotFactoryTest.java | 54 ----------- 4 files changed, 300 deletions(-) delete mode 100644 statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java delete mode 100644 statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java delete mode 100644 statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java delete mode 100644 statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java b/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java deleted file mode 100644 index c5f545f95..000000000 --- a/statemodel/src/org/testar/statemodel/changedetection/ActionSetComparator.java +++ /dev/null @@ -1,74 +0,0 @@ -/*************************************************************************************************** - * - * Copyright (c) 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *******************************************************************************************************/ - -package org.testar.statemodel.changedetection; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import org.testar.statemodel.changedetection.DeltaAction.Direction; - -/** - * Compares incoming/outgoing action sets between two states. - */ -public class ActionSetComparator { - - private ActionSetComparator() { } - - public static ActionSetDiff compare(StateSnapshot oldState, - StateSnapshot newState, - ActionPrimaryKeyProvider descriptionProvider) { - Objects.requireNonNull(oldState, "oldState cannot be null"); - Objects.requireNonNull(newState, "newState cannot be null"); - Objects.requireNonNull(descriptionProvider, "descriptionProvider cannot be null"); - - List addedIncoming = diff(newState.getIncomingActionIds(), oldState.getIncomingActionIds(), Direction.INCOMING, descriptionProvider); - List removedIncoming = diff(oldState.getIncomingActionIds(), newState.getIncomingActionIds(), Direction.INCOMING, descriptionProvider); - List addedOutgoing = diff(newState.getOutgoingActionIds(), oldState.getOutgoingActionIds(), Direction.OUTGOING, descriptionProvider); - List removedOutgoing = diff(oldState.getOutgoingActionIds(), newState.getOutgoingActionIds(), Direction.OUTGOING, descriptionProvider); - - return new ActionSetDiff(addedIncoming, removedIncoming, addedOutgoing, removedOutgoing); - } - - private static List diff(List primary, List secondary, Direction direction, ActionPrimaryKeyProvider descriptionProvider) { - Set secondarySet = new HashSet<>(secondary); - List deltas = new ArrayList<>(); - for (String actionId : primary) { - if (!secondarySet.contains(actionId)) { - deltas.add(new DeltaAction(actionId, descriptionProvider.getPrimaryKey(actionId), direction)); - } - } - return deltas; - } - -} diff --git a/statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java b/statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java deleted file mode 100644 index 398b4848b..000000000 --- a/statemodel/src/org/testar/statemodel/changedetection/StateSnapshot.java +++ /dev/null @@ -1,89 +0,0 @@ -/*************************************************************************************************** - * - * Copyright (c) 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *******************************************************************************************************/ - -package org.testar.statemodel.changedetection; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * State used for change detection. - */ -public class StateSnapshot { - - private final String stateId; - private final List concreteStateIds; - private final List incomingActionIds; - private final List outgoingActionIds; - - public StateSnapshot(String stateId, - List concreteStateIds, - List incomingActionIds, - List outgoingActionIds) { - this.stateId = Objects.requireNonNull(stateId, "stateId cannot be null"); - if (stateId.trim().isEmpty()) { - throw new IllegalArgumentException("stateId cannot be empty or blank"); - } - this.concreteStateIds = copySafe(concreteStateIds, "concrete state id"); - this.incomingActionIds = copySafe(incomingActionIds, "incoming action id"); - this.outgoingActionIds = copySafe(outgoingActionIds, "outgoing action id"); - } - - private static List copySafe(List source, String label) { - if (source == null) { - return Collections.emptyList(); - } - for (String value : source) { - Objects.requireNonNull(value, label + " cannot be null"); - if (value.trim().isEmpty()) { - throw new IllegalArgumentException(label + " cannot be empty or blank"); - } - } - return Collections.unmodifiableList(source); - } - - public String getStateId() { - return stateId; - } - - public List getConcreteStateIds() { - return concreteStateIds; - } - - public List getIncomingActionIds() { - return incomingActionIds; - } - - public List getOutgoingActionIds() { - return outgoingActionIds; - } - -} diff --git a/statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java b/statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java deleted file mode 100644 index f713f35d1..000000000 --- a/statemodel/src/org/testar/statemodel/changedetection/StateSnapshotFactory.java +++ /dev/null @@ -1,83 +0,0 @@ -/*************************************************************************************************** - * - * Copyright (c) 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *******************************************************************************************************/ - -package org.testar.statemodel.changedetection; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.testar.statemodel.AbstractState; -import org.testar.statemodel.AbstractStateModel; -import org.testar.statemodel.AbstractStateTransition; - -/** - * Builds {@link StateSnapshot}s from an {@link AbstractStateModel}. - */ -public class StateSnapshotFactory { - - private StateSnapshotFactory() { } - - public static List from(AbstractStateModel model) { - Objects.requireNonNull(model, "model cannot be null"); - - Map> incomingActions = new HashMap<>(); - Map> outgoingActions = new HashMap<>(); - - // collect incoming/outgoing action ids per state using the model transitions - for (AbstractState state : model.getStates()) { - incomingActions.put(state.getStateId(), new HashSet<>()); - outgoingActions.put(state.getStateId(), new HashSet<>()); - } - - for (AbstractState state : model.getStates()) { - for (AbstractStateTransition transition : model.getOutgoingTransitionsForState(state.getStateId())) { - outgoingActions.get(state.getStateId()).add(transition.getActionId()); - incomingActions.computeIfAbsent(transition.getTargetStateId(), id -> new HashSet<>()) - .add(transition.getActionId()); - } - } - - List snapshots = new ArrayList<>(); - for (AbstractState state : model.getStates()) { - List concreteIds = new ArrayList<>(state.getConcreteStateIds()); - List incoming = new ArrayList<>(incomingActions.getOrDefault(state.getStateId(), new HashSet<>())); - List outgoing = new ArrayList<>(outgoingActions.getOrDefault(state.getStateId(), new HashSet<>())); - - snapshots.add(new StateSnapshot(state.getStateId(), concreteIds, incoming, outgoing)); - } - return snapshots; - } - -} diff --git a/statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java b/statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java deleted file mode 100644 index 97bacd5c4..000000000 --- a/statemodel/test/org/testar/statemodel/changedetection/StateSnapshotFactoryTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.testar.statemodel.changedetection; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; - -import org.junit.Test; -import org.testar.monkey.alayer.Tags; -import org.testar.statemodel.AbstractAction; -import org.testar.statemodel.AbstractState; -import org.testar.statemodel.AbstractStateModel; -import org.testar.statemodel.exceptions.StateModelException; - -public class StateSnapshotFactoryTest { - - @Test - public void testStateSnapshotsFactory() throws StateModelException { - AbstractAction aa1 = new AbstractAction("AA1"); - AbstractAction aa2 = new AbstractAction("AA2"); - - AbstractState as1 = new AbstractState("AS1", new HashSet<>(Arrays.asList(aa1))); - AbstractState as2 = new AbstractState("AS2", new HashSet<>(Arrays.asList(aa2))); - - as1.addConcreteStateId("CS1"); - as2.addConcreteStateId("CS2"); - - AbstractStateModel model = new AbstractStateModel("model-1", "app", "1.0", - Collections.singleton(Tags.Title)); - - // create circular transitions: AS1 -> AA1 -> AS2 -> AA2 -> AS1 - model.addTransition(as1, as2, aa1); - model.addTransition(as2, as1, aa2); - - List stateSnapshots = StateSnapshotFactory.from(model); - assertEquals(2, stateSnapshots.size()); - - StateSnapshot stateOne = stateSnapshots.stream().filter(s -> s.getStateId().equals("AS1")).findFirst().orElseThrow(); - StateSnapshot stateTwo = stateSnapshots.stream().filter(s -> s.getStateId().equals("AS2")).findFirst().orElseThrow(); - - assertEquals(Collections.singletonList("CS1"), stateOne.getConcreteStateIds()); - assertEquals(Collections.singletonList("CS2"), stateTwo.getConcreteStateIds()); - - assertTrue(stateOne.getOutgoingActionIds().contains("AA1")); - assertTrue(stateOne.getIncomingActionIds().contains("AA2")); - - assertTrue(stateTwo.getOutgoingActionIds().contains("AA2")); - assertTrue(stateTwo.getIncomingActionIds().contains("AA1")); - } - -} From 0565ef9d0119258bc5418013a810d442cbd8b162 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 19:14:42 +0100 Subject: [PATCH 14/15] refactor changedetection packages --- .../changedetection/ChangeDetectionFacade.java | 2 +- .../ChangeDetectionGraphBuilder.java | 2 +- .../helpers/GraphElementIndexer.java | 4 ++-- .../helpers/ScreenshotAssigner.java | 4 ++-- .../changedetection/helpers/StatusResolver.java | 2 +- .../changedetection/ChangeDetectionEngine.java | 1 + .../ChangeDetectionEngineFactory.java | 2 ++ .../changedetection/ChangeDetectionResult.java | 4 ++++ .../algorithm/GraphTraversalComparator.java | 13 +++++++------ .../algorithm/MutableActionDiff.java | 2 +- .../algorithm/TraversalContext.java | 2 +- .../changedetection/{ => delta}/ActionSetDiff.java | 2 +- .../changedetection/{ => delta}/DeltaAction.java | 2 +- .../changedetection/{ => delta}/DeltaState.java | 2 +- .../changedetection/{ => delta}/DiffType.java | 2 +- .../{ => key}/ActionPrimaryKeyProvider.java | 2 +- .../{ => key}/DefaultActionPrimaryKeyProvider.java | 2 +- .../OrientDbActionPrimaryKeyProvider.java | 2 +- .../{ => property}/PropertyDiff.java | 4 +++- .../{ => property}/StatePropertyComparator.java | 2 +- .../{ => property}/StatePropertyExtractor.java | 2 +- .../{ => property}/VertexPropertyComparator.java | 4 +++- .../{ => property}/VertexPropertyDiff.java | 2 +- .../ChangeDetectionGraphBuilderTest.java | 14 +++++++------- .../changedetection/ChangeDetectionEngineTest.java | 1 + .../changedetection/ChangeDetectionResultTest.java | 4 +++- .../algorithm/GraphTraversalComparatorTest.java | 4 ++-- .../{ => delta}/DeltaActionTest.java | 4 ++-- .../{ => delta}/DeltaStateTest.java | 4 ++-- .../DefaultActionPrimaryKeyProviderTest.java | 2 +- .../OrientDbActionPrimaryKeyProviderTest.java | 2 +- .../StatePropertyComparatorTest.java | 2 +- .../VertexPropertyComparatorTest.java | 3 ++- 33 files changed, 61 insertions(+), 45 deletions(-) rename statemodel/src/org/testar/statemodel/changedetection/{ => delta}/ActionSetDiff.java (98%) rename statemodel/src/org/testar/statemodel/changedetection/{ => delta}/DeltaAction.java (98%) rename statemodel/src/org/testar/statemodel/changedetection/{ => delta}/DeltaState.java (98%) rename statemodel/src/org/testar/statemodel/changedetection/{ => delta}/DiffType.java (97%) rename statemodel/src/org/testar/statemodel/changedetection/{ => key}/ActionPrimaryKeyProvider.java (97%) rename statemodel/src/org/testar/statemodel/changedetection/{ => key}/DefaultActionPrimaryKeyProvider.java (97%) rename statemodel/src/org/testar/statemodel/changedetection/{ => key}/OrientDbActionPrimaryKeyProvider.java (99%) rename statemodel/src/org/testar/statemodel/changedetection/{ => property}/PropertyDiff.java (96%) rename statemodel/src/org/testar/statemodel/changedetection/{ => property}/StatePropertyComparator.java (97%) rename statemodel/src/org/testar/statemodel/changedetection/{ => property}/StatePropertyExtractor.java (97%) rename statemodel/src/org/testar/statemodel/changedetection/{ => property}/VertexPropertyComparator.java (97%) rename statemodel/src/org/testar/statemodel/changedetection/{ => property}/VertexPropertyDiff.java (98%) rename statemodel/test/org/testar/statemodel/changedetection/{ => delta}/DeltaActionTest.java (93%) rename statemodel/test/org/testar/statemodel/changedetection/{ => delta}/DeltaStateTest.java (95%) rename statemodel/test/org/testar/statemodel/changedetection/{ => key}/DefaultActionPrimaryKeyProviderTest.java (87%) rename statemodel/test/org/testar/statemodel/changedetection/{ => key}/OrientDbActionPrimaryKeyProviderTest.java (95%) rename statemodel/test/org/testar/statemodel/changedetection/{ => property}/StatePropertyComparatorTest.java (98%) rename statemodel/test/org/testar/statemodel/changedetection/{ => property}/VertexPropertyComparatorTest.java (97%) diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java index fbc917653..e309cbf70 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java @@ -34,7 +34,7 @@ import org.testar.statemodel.analysis.AnalysisManager; import org.testar.statemodel.analysis.jsonformat.Element; import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.changedetection.OrientDbActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.key.OrientDbActionPrimaryKeyProvider; import org.testar.statemodel.persistence.PersistenceManager; import org.testar.statemodel.persistence.orientdb.OrientDBManager; import org.testar.statemodel.persistence.orientdb.entity.EntityManager; diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java index f94bfe1e4..f583c043c 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java @@ -36,8 +36,8 @@ import org.testar.statemodel.analysis.changedetection.helpers.MergedGraphFilter; import org.testar.statemodel.analysis.changedetection.helpers.ScreenshotAssigner; import org.testar.statemodel.analysis.changedetection.helpers.StatusResolver; -import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; import java.util.ArrayList; import java.util.HashMap; diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java index 3305fa89f..e90d694a4 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/GraphElementIndexer.java @@ -30,13 +30,13 @@ package org.testar.statemodel.analysis.changedetection.helpers; -import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; - import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; + /** * Normalizes and indexes Cytoscape graph elements to support stable merging: * - node ids are normalized to their state id (AbstractId / ConcreteId) diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java index 28f453c33..777fb724a 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/ScreenshotAssigner.java @@ -32,8 +32,8 @@ import org.testar.statemodel.analysis.AnalysisManager; import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.changedetection.PropertyDiff; -import org.testar.statemodel.changedetection.VertexPropertyDiff; +import org.testar.statemodel.changedetection.property.PropertyDiff; +import org.testar.statemodel.changedetection.property.VertexPropertyDiff; import java.util.HashMap; import java.util.Map; diff --git a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java index edc999f06..c5ea01857 100644 --- a/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java @@ -31,7 +31,7 @@ package org.testar.statemodel.analysis.changedetection.helpers; import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.changedetection.DeltaState; +import org.testar.statemodel.changedetection.delta.DeltaState; import java.util.HashMap; import java.util.Map; diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java index ef5971b53..180ef3c29 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java @@ -34,6 +34,7 @@ import org.testar.statemodel.AbstractStateModel; import org.testar.statemodel.changedetection.algorithm.GraphTraversalComparator; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; /** * Entry point to run change detection between two state models. diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java index 3c7063b0a..66111b349 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java @@ -33,6 +33,8 @@ import java.util.Objects; import org.testar.statemodel.persistence.orientdb.entity.Connection; +import org.testar.statemodel.changedetection.key.DefaultActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.key.OrientDbActionPrimaryKeyProvider; import org.testar.statemodel.persistence.PersistenceManager; public class ChangeDetectionEngineFactory { diff --git a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java index 24b22c80c..1cf7ef12d 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java @@ -37,6 +37,10 @@ import java.util.HashMap; import java.util.Objects; +import org.testar.statemodel.changedetection.delta.ActionSetDiff; +import org.testar.statemodel.changedetection.delta.DeltaState; +import org.testar.statemodel.changedetection.property.VertexPropertyDiff; + /** * Aggregated result for a change detection run between two state models. */ diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java index 95f02268f..cc3639d68 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java @@ -33,13 +33,14 @@ import org.testar.statemodel.AbstractState; import org.testar.statemodel.AbstractStateModel; import org.testar.statemodel.AbstractStateTransition; -import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; -import org.testar.statemodel.changedetection.ActionSetDiff; import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.changedetection.DeltaAction; -import org.testar.statemodel.changedetection.DeltaState; -import org.testar.statemodel.changedetection.StatePropertyComparator; -import org.testar.statemodel.changedetection.VertexPropertyDiff; +import org.testar.statemodel.changedetection.delta.ActionSetDiff; +import org.testar.statemodel.changedetection.delta.DeltaAction; +import org.testar.statemodel.changedetection.delta.DeltaState; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.property.StatePropertyComparator; +import org.testar.statemodel.changedetection.property.VertexPropertyDiff; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java index aa6b90d97..46362815f 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/MutableActionDiff.java @@ -33,7 +33,7 @@ import java.util.ArrayList; import java.util.List; -import org.testar.statemodel.changedetection.DeltaAction; +import org.testar.statemodel.changedetection.delta.DeltaAction; final class MutableActionDiff { diff --git a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java index 340d3f9f1..0ec456748 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/TraversalContext.java @@ -35,7 +35,7 @@ import java.util.List; import java.util.Map; -import org.testar.statemodel.changedetection.VertexPropertyDiff; +import org.testar.statemodel.changedetection.property.VertexPropertyDiff; /** * Traversal context used by {@link GraphTraversalComparator}. diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java b/statemodel/src/org/testar/statemodel/changedetection/delta/ActionSetDiff.java similarity index 98% rename from statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java rename to statemodel/src/org/testar/statemodel/changedetection/delta/ActionSetDiff.java index b70f55819..01c6b5d8b 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ActionSetDiff.java +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/ActionSetDiff.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.delta; import java.util.Collections; import java.util.List; diff --git a/statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java b/statemodel/src/org/testar/statemodel/changedetection/delta/DeltaAction.java similarity index 98% rename from statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java rename to statemodel/src/org/testar/statemodel/changedetection/delta/DeltaAction.java index c59afea41..0991fdde3 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/DeltaAction.java +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/DeltaAction.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.delta; import java.util.Objects; diff --git a/statemodel/src/org/testar/statemodel/changedetection/DeltaState.java b/statemodel/src/org/testar/statemodel/changedetection/delta/DeltaState.java similarity index 98% rename from statemodel/src/org/testar/statemodel/changedetection/DeltaState.java rename to statemodel/src/org/testar/statemodel/changedetection/delta/DeltaState.java index ee881ab2f..b9bf014e8 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/DeltaState.java +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/DeltaState.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.delta; import java.util.ArrayList; import java.util.Collections; diff --git a/statemodel/src/org/testar/statemodel/changedetection/DiffType.java b/statemodel/src/org/testar/statemodel/changedetection/delta/DiffType.java similarity index 97% rename from statemodel/src/org/testar/statemodel/changedetection/DiffType.java rename to statemodel/src/org/testar/statemodel/changedetection/delta/DiffType.java index f2a6cf57f..fdc754906 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/DiffType.java +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/DiffType.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.delta; public enum DiffType { ADDED, diff --git a/statemodel/src/org/testar/statemodel/changedetection/ActionPrimaryKeyProvider.java b/statemodel/src/org/testar/statemodel/changedetection/key/ActionPrimaryKeyProvider.java similarity index 97% rename from statemodel/src/org/testar/statemodel/changedetection/ActionPrimaryKeyProvider.java rename to statemodel/src/org/testar/statemodel/changedetection/key/ActionPrimaryKeyProvider.java index 561197706..dfa39adc2 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/ActionPrimaryKeyProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/key/ActionPrimaryKeyProvider.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.key; /** * Resolves the primary key for an action comparison. diff --git a/statemodel/src/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProvider.java b/statemodel/src/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProvider.java similarity index 97% rename from statemodel/src/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProvider.java rename to statemodel/src/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProvider.java index 8799e7e82..9d0e45634 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProvider.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.key; /** * Default implementation that simply returns the action id as the primary key. diff --git a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProvider.java b/statemodel/src/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProvider.java similarity index 99% rename from statemodel/src/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProvider.java rename to statemodel/src/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProvider.java index 3ff2755cf..0ffc3f578 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProvider.java +++ b/statemodel/src/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProvider.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.key; import java.util.Map; import java.util.Objects; diff --git a/statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java b/statemodel/src/org/testar/statemodel/changedetection/property/PropertyDiff.java similarity index 96% rename from statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java rename to statemodel/src/org/testar/statemodel/changedetection/property/PropertyDiff.java index 09e69aa49..0897841b4 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/PropertyDiff.java +++ b/statemodel/src/org/testar/statemodel/changedetection/property/PropertyDiff.java @@ -28,10 +28,12 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import java.util.Objects; +import org.testar.statemodel.changedetection.delta.DiffType; + public class PropertyDiff { private final String propertyName; diff --git a/statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java b/statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyComparator.java similarity index 97% rename from statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java rename to statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyComparator.java index cd25f8e6f..af1b3b589 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/StatePropertyComparator.java +++ b/statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyComparator.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import java.util.Map; import java.util.Objects; diff --git a/statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java b/statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyExtractor.java similarity index 97% rename from statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java rename to statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyExtractor.java index 3abb398ad..fb47ff56a 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/StatePropertyExtractor.java +++ b/statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyExtractor.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import java.util.HashMap; import java.util.Map; diff --git a/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyComparator.java similarity index 97% rename from statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java rename to statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyComparator.java index 52e25c5b9..ce2f69956 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyComparator.java +++ b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyComparator.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import java.util.ArrayList; import java.util.List; @@ -36,6 +36,8 @@ import java.util.Objects; import java.util.function.Predicate; +import org.testar.statemodel.changedetection.delta.DiffType; + /** * Compares two sets of vertex properties and classifies their differences. * - properties starting with "CD_" or custom predicate are ignored diff --git a/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyDiff.java similarity index 98% rename from statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java rename to statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyDiff.java index 838051d70..b4ec252b9 100644 --- a/statemodel/src/org/testar/statemodel/changedetection/VertexPropertyDiff.java +++ b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyDiff.java @@ -28,7 +28,7 @@ * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************************************/ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import java.util.ArrayList; import java.util.Collections; diff --git a/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java b/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java index 0c736d8c6..94a96db16 100644 --- a/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java +++ b/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java @@ -1,14 +1,14 @@ package org.testar.statemodel.analysis.changedetection; import org.junit.Test; -import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; -import org.testar.statemodel.changedetection.ActionSetDiff; import org.testar.statemodel.changedetection.ChangeDetectionResult; -import org.testar.statemodel.changedetection.DeltaAction; -import org.testar.statemodel.changedetection.DeltaState; -import org.testar.statemodel.changedetection.DiffType; -import org.testar.statemodel.changedetection.PropertyDiff; -import org.testar.statemodel.changedetection.VertexPropertyDiff; +import org.testar.statemodel.changedetection.delta.ActionSetDiff; +import org.testar.statemodel.changedetection.delta.DeltaAction; +import org.testar.statemodel.changedetection.delta.DeltaState; +import org.testar.statemodel.changedetection.delta.DiffType; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.property.PropertyDiff; +import org.testar.statemodel.changedetection.property.VertexPropertyDiff; import java.util.ArrayList; import java.util.Arrays; diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java index 38aebe2a6..8109d815a 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java @@ -6,6 +6,7 @@ import org.junit.Test; import org.testar.monkey.alayer.Tag; import org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; import org.testar.statemodel.exceptions.StateModelException; public class ChangeDetectionEngineTest { diff --git a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java index 4e9d0e92e..3fe76f08d 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java @@ -7,7 +7,9 @@ import java.util.Collections; import org.junit.Test; -import org.testar.statemodel.changedetection.DeltaAction.Direction; +import org.testar.statemodel.changedetection.delta.DeltaAction; +import org.testar.statemodel.changedetection.delta.DeltaState; +import org.testar.statemodel.changedetection.delta.DeltaAction.Direction; public class ChangeDetectionResultTest { diff --git a/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java index ad5f817f2..2a0d12844 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java @@ -7,9 +7,9 @@ import org.testar.statemodel.AbstractAction; import org.testar.statemodel.AbstractState; import org.testar.statemodel.AbstractStateModel; -import org.testar.statemodel.changedetection.ActionPrimaryKeyProvider; -import org.testar.statemodel.changedetection.ActionSetDiff; import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.delta.ActionSetDiff; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; import org.testar.statemodel.exceptions.StateModelException; import java.util.Collections; diff --git a/statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaActionTest.java similarity index 93% rename from statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java rename to statemodel/test/org/testar/statemodel/changedetection/delta/DeltaActionTest.java index f8b950bbb..bb8fa4dce 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/DeltaActionTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaActionTest.java @@ -1,10 +1,10 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.delta; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import org.junit.Test; -import org.testar.statemodel.changedetection.DeltaAction.Direction; +import org.testar.statemodel.changedetection.delta.DeltaAction.Direction; public class DeltaActionTest { diff --git a/statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaStateTest.java similarity index 95% rename from statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java rename to statemodel/test/org/testar/statemodel/changedetection/delta/DeltaStateTest.java index e4d19fb55..8bc12bfa6 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/DeltaStateTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaStateTest.java @@ -1,4 +1,4 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.delta; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; @@ -8,7 +8,7 @@ import java.util.Collections; import org.junit.Test; -import org.testar.statemodel.changedetection.DeltaAction.Direction; +import org.testar.statemodel.changedetection.delta.DeltaAction.Direction; public class DeltaStateTest { diff --git a/statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProviderTest.java similarity index 87% rename from statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java rename to statemodel/test/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProviderTest.java index b88ee7466..0eea67900 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/DefaultActionPrimaryKeyProviderTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProviderTest.java @@ -1,4 +1,4 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.key; import static org.junit.Assert.assertEquals; diff --git a/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProviderTest.java similarity index 95% rename from statemodel/test/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProviderTest.java rename to statemodel/test/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProviderTest.java index bafb02bf4..a8a95b8ec 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/OrientDbActionPrimaryKeyProviderTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProviderTest.java @@ -1,4 +1,4 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.key; import static org.junit.Assert.assertEquals; diff --git a/statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/property/StatePropertyComparatorTest.java similarity index 98% rename from statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java rename to statemodel/test/org/testar/statemodel/changedetection/property/StatePropertyComparatorTest.java index 37a7d89c5..17f3a4292 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/StatePropertyComparatorTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/property/StatePropertyComparatorTest.java @@ -1,4 +1,4 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import static org.junit.Assert.assertEquals; import java.util.Collections; diff --git a/statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/property/VertexPropertyComparatorTest.java similarity index 97% rename from statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java rename to statemodel/test/org/testar/statemodel/changedetection/property/VertexPropertyComparatorTest.java index db9169a7a..c89ae0a98 100644 --- a/statemodel/test/org/testar/statemodel/changedetection/VertexPropertyComparatorTest.java +++ b/statemodel/test/org/testar/statemodel/changedetection/property/VertexPropertyComparatorTest.java @@ -1,4 +1,4 @@ -package org.testar.statemodel.changedetection; +package org.testar.statemodel.changedetection.property; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -7,6 +7,7 @@ import java.util.Map; import org.junit.Test; +import org.testar.statemodel.changedetection.delta.DiffType; public class VertexPropertyComparatorTest { From 88b60fc47fb356896ac6655b6c924f35bf698413 Mon Sep 17 00:00:00 2001 From: ferpasri Date: Sat, 13 Dec 2025 19:38:34 +0100 Subject: [PATCH 15/15] Add changedetection readme --- statemodel/README_changedetection.md | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 statemodel/README_changedetection.md diff --git a/statemodel/README_changedetection.md b/statemodel/README_changedetection.md new file mode 100644 index 000000000..d83581f16 --- /dev/null +++ b/statemodel/README_changedetection.md @@ -0,0 +1,113 @@ +# TESTAR StateModel – Change Detection + +This document describes the **Change Detection** implementation in the `statemodel` framework: + +- Core comparison logic (`statemodel\src\org\testar\statemodel\changedetection`) +- Visualization analysis (`statemodel\src\org\testar\statemodel\analysis\changedetection`) +- Merged graph rendering (`statemodel\resources\graphs/changedetection.jsp`) + +## Goals and scope + +- Compare **two persisted OrientDB State Models** and classify differences as: + - **Added** (states and actions that exists only in new model) + - **Removed** (states and actions that exists only in old model) + - **Changed** (matched state exists in both but differs in properties and/or its action set) + - **Unchanged** (matched state and relevant properties/actions are the same) +- Build a **merged visualization graph** for the web analysis mode. +- Use **action descriptions** (Desc) as the preferred key for matching actions across models, with **actionId fallback**. + +The comparison is primarily performed on the **Abstract layer** (abstract states and abstract actions). Concrete layer entities are still used **server-side** for screenshots and additional metadata such as descriptions. + +### Web entry points + +- `GET /changedetection` → `ChangeDetectionServlet` + - Renders the page and model selectors. +- `GET /changedetection-graph?oldModelIdentifier=...&newModelIdentifier=...` → `ChangeDetectionGraphServlet` + - Returns the merged graph as JSON (nodes/edges + status + metadata). + +Both servlets use `ChangeDetectionFacade` to ensure the persistence layer is opened/closed consistently. + +## Core comparison logic + +The core comparison logic is under `org.testar.statemodel.changedetection`: + +- `...changedetection` + - `ChangeDetectionEngine` – runs the comparison. + - `ChangeDetectionEngineFactory` – creates engines with different primary-key providers. + - `ChangeDetectionResult` – aggregated result (added/removed/changed). +- `...changedetection.delta` (**DTOs / deltas**) + - `DeltaState`, `DeltaAction`, `ActionSetDiff`, `DiffType` +- `...changedetection.diff` (**property diffing**) + - `PropertyDiff`, `VertexPropertyDiff` + - `VertexPropertyComparator`, `StatePropertyExtractor`, `StatePropertyComparator` +- `...changedetection.key` (**action primary-key providers**) + - `ActionPrimaryKeyProvider` + - `DefaultActionPrimaryKeyProvider` (fallback implementation) + - `OrientDbActionPrimaryKeyProvider` (Desc preferred, actionId fallback; includes caching) +- `...changedetection.algorithm` (**traversal-based comparator internals**) + - `GraphTraversalComparator` and traversal helper classes (`Traversal*`, etc.) + +## Visualization analysis + +Visualization-specific logic lives under `org.testar.statemodel.analysis.changedetection`: + +- `ChangeDetectionFacade` + - Opens persistence, loads model graph elements, builds the merged graph JSON, then closes everything. +- `ChangeDetectionAnalysisService` + - Loads `AbstractStateModel` instances and invokes the core engine. +- `ChangeDetectionGraphBuilder` + - Converts `ChangeDetectionResult` + graph elements into the JSON format expected by the JSP/Cytoscape frontend. + - Assigns screenshots to abstract states using related concrete state screenshots. +- Helpers: `...analysis.changedetection.helpers.*` + - Example responsibilities: resolve statuses with precedence rules, index graph elements, assign screenshots. + +## Merged graph rendering + +- Frontend: `statemodel/resources/graphs/changedetection.jsp` + - Cytoscape rendering, details sidebar, and pixel-diff (pixelmatch) for old vs new screenshots. + +## Core algorithm (what “changed” means) + +### Traversal-based matching + +`GraphTraversalComparator` performs a traversal starting from the **initial abstract state** in both models: + +- States are **mapped** old <-> new during traversal to avoid double-counting and infinite loops. +- For a mapped state pair: + - Properties are compared (via `StatePropertyComparator` → `VertexPropertyComparator`). + - Outgoing action edges are compared using an **action primary key** (see below). +- Remaining unmapped states become: + - **Added** (only reachable in new) + - **Removed** (only reachable in old) + +### Action identity: `primaryKey` + +The comparison and merge logic use **one canonical identity for actions**, called `primaryKey`: + +1. Prefer the action **Desc** resolved from OrientDB (via `OrientDbActionPrimaryKeyProvider`). +2. If no Desc exists (or it is empty/invalid), fallback to the **actionId**. + +This same `primaryKey` is used for: + +- Matching outgoing action sets between old/new during traversal. +- Merging edges in the merged visualization graph. +- Displaying action labels in the UI. + +## Visualization semantics (status precedence) + +The UI shows node/edge status based on the merged result. A key rule: + +- **Added/Removed dominates Changed** for visualization. + +This prevents a state from being shown as “changed” when it is actually new/removed but happens to appear in `changedActions` (e.g., due to incoming/outgoing diffs being reported for a newly added state). + +## Screenshots and diffs + +- State node screenshots are derived from the first concrete state screenshot linked to the abstract state. +- The frontend uses `pixelmatchInterop.js` to compute and display a visual change diff (when both screenshots exist). + +## Notes / limitations + +- Results depend heavily on the **abstraction attributes** used when building the models (small abstraction changes can create large diffs). +- Very large graphs can be expensive to render. +- If you see OrientDB “file locked by another process” errors when using plocal mode, ensure analysis requests always go through `ChangeDetectionFacade` so connections are closed promptly.