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/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))); } ); + */ } /** 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"); } } 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. 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/ChangeDetectionFacade.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionFacade.java new file mode 100644 index 000000000..e309cbf70 --- /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.key.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 new file mode 100644 index 000000000..f583c043c --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilder.java @@ -0,0 +1,189 @@ +/*************************************************************************************************** + * + * 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.analysis.changedetection.helpers.StatusResolver; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +import org.testar.statemodel.changedetection.key.ActionPrimaryKeyProvider; + +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(); + private final StatusResolver statusResolver = new StatusResolver(); + + 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 = statusResolver.buildStatusByState(result); + + // 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) : StatusResolver.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..86f01c652 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphServlet.java @@ -0,0 +1,78 @@ +/*************************************************************************************************** + * + * 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 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.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"); + + try { + 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(); + } + } + +} 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..ed9c496af --- /dev/null +++ b/statemodel/src/org/testar/statemodel/analysis/changedetection/ChangeDetectionServlet.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.analysis.changedetection; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.testar.statemodel.analysis.AnalysisManager; +import org.testar.statemodel.changedetection.ChangeDetectionResult; + +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"); + + ChangeDetectionFacade facade = new ChangeDetectionFacade(analysisManager, mapper); + ChangeDetectionResult result = facade.compare(oldModelId, newModelId); + + resp.setContentType("application/json"); + resp.getWriter().write(mapper.writeValueAsString(result)); + } + +} 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..e90d694a4 --- /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 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) + * - 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..777fb724a --- /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.property.PropertyDiff; +import org.testar.statemodel.changedetection.property.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/changedetection/helpers/StatusResolver.java b/statemodel/src/org/testar/statemodel/analysis/changedetection/helpers/StatusResolver.java new file mode 100644 index 000000000..c5ea01857 --- /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.delta.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; + } + +} 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/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java new file mode 100644 index 000000000..180ef3c29 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngine.java @@ -0,0 +1,59 @@ +/*************************************************************************************************** + * + * 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.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. + */ +public class ChangeDetectionEngine { + + private final ActionPrimaryKeyProvider actionPrimaryKeyProvider; + private final GraphTraversalComparator comparator; + + 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) { + Objects.requireNonNull(oldModel, "oldModel cannot be null"); + Objects.requireNonNull(newModel, "newModel cannot be null"); + + 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 new file mode 100644 index 000000000..66111b349 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionEngineFactory.java @@ -0,0 +1,58 @@ +/*************************************************************************************************** + * + * 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.changedetection.key.DefaultActionPrimaryKeyProvider; +import org.testar.statemodel.changedetection.key.OrientDbActionPrimaryKeyProvider; +import org.testar.statemodel.persistence.PersistenceManager; + +public class ChangeDetectionEngineFactory { + + private ChangeDetectionEngineFactory() { } + + public static ChangeDetectionEngine createWithDefaultDescription() { + return new ChangeDetectionEngine(new DefaultActionPrimaryKeyProvider()); + } + + public static ChangeDetectionEngine createWithOrientDb(Connection connection) { + Objects.requireNonNull(connection, "connection cannot be null"); + return new ChangeDetectionEngine(new OrientDbActionPrimaryKeyProvider(connection)); + } + + 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..1cf7ef12d --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/ChangeDetectionResult.java @@ -0,0 +1,135 @@ +/*************************************************************************************************** + * + * 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; + +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. + */ +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/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java b/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java new file mode 100644 index 000000000..cc3639d68 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparator.java @@ -0,0 +1,257 @@ +/*************************************************************************************************** + * + * 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 org.testar.statemodel.AbstractStateModel; +import org.testar.statemodel.AbstractStateTransition; +import org.testar.statemodel.changedetection.ChangeDetectionResult; +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; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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. + */ +public class GraphTraversalComparator { + + private final ActionPrimaryKeyProvider primaryKeyProvider; + + public GraphTraversalComparator(ActionPrimaryKeyProvider primaryKeyProvider) { + this.primaryKeyProvider = Objects.requireNonNull(primaryKeyProvider, "primaryKeyProvider cannot be null"); + } + + public ChangeDetectionResult compare(AbstractStateModel oldModel, AbstractStateModel newModel) { + TraversalGraph oldGraph = buildGraph(oldModel); + TraversalGraph newGraph = buildGraph(newModel); + + TraversalContext ctx = new TraversalContext(oldGraph, newGraph); + + TraversalNode oldStart = chooseInitialState(oldGraph); + TraversalNode 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(TraversalNode oldNode, TraversalNode 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 (TraversalEdge newEdge : newNode.outgoing) { + if (newEdge.handled) { + continue; + } + 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 TraversalEdgePair(match, newEdge)); + + 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 + 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 (TraversalEdge oldEdge : oldNode.outgoing) { + if (!oldEdge.handled) { + ctx.removedEdges.add(oldEdge); + } + } + } + + private List computeAddedStates(TraversalContext ctx) { + List added = new ArrayList<>(); + for (TraversalNode 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 (TraversalNode 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 (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)); + // 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 (TraversalEdge 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(TraversalNode node, TraversalGraph 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 TraversalGraph buildGraph(AbstractStateModel model) { + Map nodes = new HashMap<>(); + List edges = new ArrayList<>(); + + for (AbstractState s : model.getStates()) { + nodes.put(s.getStateId(), new TraversalNode(s)); + } + + for (TraversalNode n : nodes.values()) { + Collection outgoing = model.getOutgoingTransitionsForState(n.id); + for (AbstractStateTransition t : outgoing) { + TraversalEdge e = toEdge(t, nodes); + n.outgoing.add(e); + nodes.get(e.targetId).incoming.add(e); + edges.add(e); + } + } + + return new TraversalGraph(nodes, edges); + } + + private TraversalEdge toEdge(AbstractStateTransition t, Map nodes) { + String actionId = t.getActionId(); + String desc = primaryKeyProvider.getPrimaryKey(actionId); + String key = comparableKey(actionId, desc); + return new TraversalEdge(t.getSourceStateId(), t.getTargetStateId(), actionId, desc, key); + } + + private String comparableKey(String actionId, String description) { + if (description == null || description.trim().isEmpty()) { + return actionId; + } + // Workaround for actions with empty descriptions + if (description.contains("at ''")) { + return actionId; + } + return description; + } + + private TraversalNode chooseInitialState(TraversalGraph graph) { + for (TraversalNode n : graph.nodes.values()) { + if (n.initial) { + return n; + } + } + return null; + } + +} 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..46362815f --- /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.delta.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..0ec456748 --- /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.property.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/src/org/testar/statemodel/changedetection/delta/ActionSetDiff.java b/statemodel/src/org/testar/statemodel/changedetection/delta/ActionSetDiff.java new file mode 100644 index 000000000..01c6b5d8b --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/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.delta; + +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/delta/DeltaAction.java b/statemodel/src/org/testar/statemodel/changedetection/delta/DeltaAction.java new file mode 100644 index 000000000..0991fdde3 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/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.delta; + +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/delta/DeltaState.java b/statemodel/src/org/testar/statemodel/changedetection/delta/DeltaState.java new file mode 100644 index 000000000..b9bf014e8 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/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.delta; + +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/delta/DiffType.java b/statemodel/src/org/testar/statemodel/changedetection/delta/DiffType.java new file mode 100644 index 000000000..fdc754906 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/delta/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.delta; + +public enum DiffType { + ADDED, + REMOVED, + CHANGED +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/key/ActionPrimaryKeyProvider.java b/statemodel/src/org/testar/statemodel/changedetection/key/ActionPrimaryKeyProvider.java new file mode 100644 index 000000000..dfa39adc2 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/key/ActionPrimaryKeyProvider.java @@ -0,0 +1,41 @@ +/*************************************************************************************************** + * + * 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.key; + +/** + * 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 getPrimaryKey(String actionId); + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProvider.java b/statemodel/src/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProvider.java new file mode 100644 index 000000000..9d0e45634 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProvider.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.key; + +/** + * Default implementation that simply returns the action id as the primary key. + */ +public class DefaultActionPrimaryKeyProvider implements ActionPrimaryKeyProvider { + + @Override + public String getPrimaryKey(String actionId) { + return actionId; + } + +} diff --git a/statemodel/src/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProvider.java b/statemodel/src/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProvider.java new file mode 100644 index 000000000..0ffc3f578 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProvider.java @@ -0,0 +1,134 @@ +/*************************************************************************************************** + * + * 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.key; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +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. + */ +public class OrientDbActionPrimaryKeyProvider implements ActionPrimaryKeyProvider { + + private final Connection connection; + private final Map cache = new ConcurrentHashMap<>(); + + public OrientDbActionPrimaryKeyProvider(Connection connection) { + this.connection = Objects.requireNonNull(connection, "connection cannot be null"); + } + + @Override + public String getPrimaryKey(String abstractActionId) { + String cached = cache.get(abstractActionId); + if (cached != null) { + return cached; + } + String description = queryConcreteActionDescription(abstractActionId); + String resolved = (description == null || description.isEmpty()) ? abstractActionId : description; + cache.put(abstractActionId, resolved); + return resolved; + } + + /** + * Queries OrientDB for a ConcreteAction description matching the given abstract action id. + */ + protected String queryConcreteActionDescription(String abstractActionId) { + 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; + } + } + 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/property/PropertyDiff.java b/statemodel/src/org/testar/statemodel/changedetection/property/PropertyDiff.java new file mode 100644 index 000000000..0897841b4 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/property/PropertyDiff.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.property; + +import java.util.Objects; + +import org.testar.statemodel.changedetection.delta.DiffType; + +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/property/StatePropertyComparator.java b/statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyComparator.java new file mode 100644 index 000000000..af1b3b589 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/property/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.property; + +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/property/StatePropertyExtractor.java b/statemodel/src/org/testar/statemodel/changedetection/property/StatePropertyExtractor.java new file mode 100644 index 000000000..fb47ff56a --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/property/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.property; + +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/src/org/testar/statemodel/changedetection/property/VertexPropertyComparator.java b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyComparator.java new file mode 100644 index 000000000..ce2f69956 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyComparator.java @@ -0,0 +1,98 @@ +/*************************************************************************************************** + * + * 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.property; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +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 + * - 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/property/VertexPropertyDiff.java b/statemodel/src/org/testar/statemodel/changedetection/property/VertexPropertyDiff.java new file mode 100644 index 000000000..b4ec252b9 --- /dev/null +++ b/statemodel/src/org/testar/statemodel/changedetection/property/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.property; + +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/analysis/changedetection/ChangeDetectionGraphBuilderTest.java b/statemodel/test/org/testar/statemodel/analysis/changedetection/ChangeDetectionGraphBuilderTest.java new file mode 100644 index 000000000..94a96db16 --- /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.ChangeDetectionResult; +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; +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; + } + +} 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..8109d815a --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionEngineTest.java @@ -0,0 +1,47 @@ +package org.testar.statemodel.changedetection; + +import java.util.Collections; +import java.util.HashSet; + +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 { + + @Test + public void testDescriptionProviderConstructor() throws StateModelException { + ActionPrimaryKeyProvider descriptionProvider = id -> "desc-" + id; + ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); + + 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(expected = NullPointerException.class) + public void testNullDescriptionProvider() throws StateModelException { + new ChangeDetectionEngine(null); + } + + @Test(expected = NullPointerException.class) + public void testNullOldModel() throws StateModelException { + ActionPrimaryKeyProvider descriptionProvider = id -> "desc-" + id; + ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); + + AbstractStateModel newModel = new AbstractStateModel("newModel", "app", "version", new HashSet>(Collections.emptySet())); + engine.compare(null, newModel); + } + + @Test(expected = NullPointerException.class) + public void testNullNewModel() throws StateModelException { + ActionPrimaryKeyProvider descriptionProvider = id -> "desc-" + id; + ChangeDetectionEngine engine = new ChangeDetectionEngine(descriptionProvider); + + AbstractStateModel oldModel = new AbstractStateModel("oldModel", "app", "version", new HashSet>(Collections.emptySet())); + engine.compare(oldModel, null); + } + +} 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..3fe76f08d --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/ChangeDetectionResultTest.java @@ -0,0 +1,69 @@ +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.delta.DeltaAction; +import org.testar.statemodel.changedetection.delta.DeltaState; +import org.testar.statemodel.changedetection.delta.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()); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java new file mode 100644 index 000000000..2a0d12844 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/algorithm/GraphTraversalComparatorTest.java @@ -0,0 +1,157 @@ +package org.testar.statemodel.changedetection.algorithm; + +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.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; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static org.junit.Assert.*; + +public class GraphTraversalComparatorTest { + + private Map descByAction; + private ActionPrimaryKeyProvider 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/delta/DeltaActionTest.java b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaActionTest.java new file mode 100644 index 000000000..bb8fa4dce --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaActionTest.java @@ -0,0 +1,60 @@ +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.delta.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/delta/DeltaStateTest.java b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaStateTest.java new file mode 100644 index 000000000..8bc12bfa6 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/delta/DeltaStateTest.java @@ -0,0 +1,64 @@ +package org.testar.statemodel.changedetection.delta; + +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.delta.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/key/DefaultActionPrimaryKeyProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProviderTest.java new file mode 100644 index 000000000..0eea67900 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/key/DefaultActionPrimaryKeyProviderTest.java @@ -0,0 +1,15 @@ +package org.testar.statemodel.changedetection.key; + +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/key/OrientDbActionPrimaryKeyProviderTest.java b/statemodel/test/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProviderTest.java new file mode 100644 index 000000000..a8a95b8ec --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/key/OrientDbActionPrimaryKeyProviderTest.java @@ -0,0 +1,36 @@ +package org.testar.statemodel.changedetection.key; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.testar.statemodel.persistence.orientdb.entity.Connection; + +public class OrientDbActionPrimaryKeyProviderTest { + + @Test + public void testObtainPrimaryKeyFromDescription() { + OrientDbActionPrimaryKeyProvider provider = new OrientDbActionPrimaryKeyProvider(createDummyConnection()) { + @Override + protected String queryConcreteActionDescription(String abstractActionId) { + return "desc-" + abstractActionId; + } + }; + assertEquals("desc-AA1", provider.getPrimaryKey("AA1")); + } + + @Test + public void testFallsBackToId() { + OrientDbActionPrimaryKeyProvider provider = new OrientDbActionPrimaryKeyProvider(createDummyConnection()) { + @Override + protected String queryConcreteActionDescription(String abstractActionId) { + return null; + } + }; + assertEquals("AA1", provider.getPrimaryKey("AA1")); + } + + private Connection createDummyConnection() { + return new Connection(null, null); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/property/StatePropertyComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/property/StatePropertyComparatorTest.java new file mode 100644 index 000000000..17f3a4292 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/property/StatePropertyComparatorTest.java @@ -0,0 +1,100 @@ +package org.testar.statemodel.changedetection.property; + +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()); + } + +} diff --git a/statemodel/test/org/testar/statemodel/changedetection/property/VertexPropertyComparatorTest.java b/statemodel/test/org/testar/statemodel/changedetection/property/VertexPropertyComparatorTest.java new file mode 100644 index 000000000..c89ae0a98 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/changedetection/property/VertexPropertyComparatorTest.java @@ -0,0 +1,127 @@ +package org.testar.statemodel.changedetection.property; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.testar.statemodel.changedetection.delta.DiffType; + +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()); + } + +}