diff --git a/app/save-and-restore/app/doc/images/configuration-editor.png b/app/save-and-restore/app/doc/images/configuration-editor.png
index 2ae0b0422b..1aa47998f9 100644
Binary files a/app/save-and-restore/app/doc/images/configuration-editor.png and b/app/save-and-restore/app/doc/images/configuration-editor.png differ
diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst
index b573884709..cc08a6da49 100644
--- a/app/save-and-restore/app/doc/index.rst
+++ b/app/save-and-restore/app/doc/index.rst
@@ -157,7 +157,7 @@ Configuration View
------------------
A new configuration is created from the context menu launched when right-clicking on a folder node in the tree view.
-This will launch the configuration editor:
+This screenshot shows the configuration editor:
.. image:: images/configuration-editor.png
:width: 80%
@@ -170,6 +170,9 @@ with PVs in the order they appear.
PV entries in a configuration marked as read only will be omitted whe performing a restore operation.
+Compare Mode and Tolerance data is optional. This is used by the service when a client requests comparison between
+stored and live values of a snapshot. More information on this feature is found in the service documentation.
+
To add a very large number of PVs, user should consider the import feature available via the "Import Configuration file to this folder"
option in the context menu of a folder node in the tree view.
diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java
index b7c00bd22b..660debeb9a 100644
--- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java
+++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java
@@ -79,6 +79,7 @@ public class Messages {
public static String duplicatePVNamesAdditionalItems;
public static String duplicatePVNamesCheckFailed;
public static String duplicatePVNamesFoundInSelection;
+ public static String duplicatePVNamesNotSupported;
public static String Edit;
public static String editFilter;
public static String errorActionFailed;
diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigPvEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigPvEntry.java
new file mode 100644
index 0000000000..8a57a6ed87
--- /dev/null
+++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigPvEntry.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 European Spallation Source ERIC.
+ */
+
+package org.phoebus.applications.saveandrestore.ui.configuration;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import org.phoebus.applications.saveandrestore.model.Comparison;
+import org.phoebus.applications.saveandrestore.model.ConfigPv;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
+
+import java.util.Objects;
+
+/**
+ * Wrapper around a {@link ConfigPv} instance for the purpose of facilitating
+ * configuration and data binding in (for instance) a {@link javafx.scene.control.TableView}.
+ */
+public class ConfigPvEntry implements Comparable {
+
+ private final StringProperty pvNameProperty;
+ private final StringProperty readBackPvNameProperty;
+ private final BooleanProperty readOnlyProperty;
+ private final ObjectProperty comparisonModeProperty;
+ private final ObjectProperty toleranceProperty;
+
+ public ConfigPvEntry(ConfigPv configPv) {
+ this.pvNameProperty = new SimpleStringProperty(this, "pvNameProperty", configPv.getPvName());
+ this.readBackPvNameProperty = new SimpleStringProperty(configPv.getReadbackPvName());
+ this.readOnlyProperty = new SimpleBooleanProperty(configPv.isReadOnly());
+ this.comparisonModeProperty = new SimpleObjectProperty<>(configPv.getComparison() == null ? null : configPv.getComparison().getComparisonMode());
+ this.toleranceProperty = new SimpleObjectProperty<>(configPv.getComparison() == null ? null : configPv.getComparison().getTolerance());
+ }
+
+ public StringProperty getPvNameProperty() {
+ return pvNameProperty;
+ }
+
+ public StringProperty getReadBackPvNameProperty() {
+ return readBackPvNameProperty;
+ }
+
+ public BooleanProperty getReadOnlyProperty() {
+ return readOnlyProperty;
+ }
+
+ public ObjectProperty getComparisonModeProperty() {
+ return comparisonModeProperty;
+ }
+
+ public ObjectProperty getToleranceProperty() {
+ return toleranceProperty;
+ }
+
+ public void setPvNameProperty(String pvNameProperty) {
+ this.pvNameProperty.set(pvNameProperty);
+ }
+
+ public void setReadBackPvNameProperty(String readBackPvNameProperty) {
+ this.readBackPvNameProperty.set(readBackPvNameProperty);
+ }
+
+ public void setComparisonModeProperty(ComparisonMode comparisonModeProperty) {
+ this.comparisonModeProperty.set(comparisonModeProperty);
+ }
+
+ public void setToleranceProperty(Double toleranceProperty) {
+ this.toleranceProperty.set(toleranceProperty );
+ }
+
+ public ConfigPv toConfigPv() {
+ ConfigPv configPv = ConfigPv.builder()
+ .pvName(pvNameProperty.get())
+ .readbackPvName(readBackPvNameProperty.get())
+ .readOnly(readOnlyProperty.get())
+ .build();
+ if(comparisonModeProperty.isNotNull().get() && toleranceProperty.isNotNull().get()){
+ configPv.setComparison(new Comparison(comparisonModeProperty.get(), toleranceProperty.get()));
+ }
+ return configPv;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ConfigPvEntry otherConfigPv) {
+ return Objects.equals(pvNameProperty, otherConfigPv.getPvNameProperty()) &&
+ Objects.equals(readBackPvNameProperty, otherConfigPv.getReadBackPvNameProperty()) &&
+ Objects.equals(readOnlyProperty.get(), otherConfigPv.getReadOnlyProperty().get());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pvNameProperty, readBackPvNameProperty, readOnlyProperty);
+ }
+
+ @Override
+ public int compareTo(ConfigPvEntry other) {
+ return pvNameProperty.get().compareTo(other.getPvNameProperty().get());
+ }
+}
diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java
index 602a4c8c79..d5d9dbb831 100644
--- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java
+++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java
@@ -21,6 +21,7 @@
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
+import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -28,6 +29,7 @@
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
@@ -37,16 +39,18 @@
import javafx.scene.control.MenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
-import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
-import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.control.cell.CheckBoxTableCell;
+import javafx.scene.control.cell.ComboBoxTableCell;
+import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
-import javafx.util.Callback;
+import javafx.util.StringConverter;
+import javafx.util.converter.DoubleStringConverter;
import org.phoebus.applications.saveandrestore.Messages;
import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication;
import org.phoebus.applications.saveandrestore.model.ConfigPv;
@@ -54,10 +58,12 @@
import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
import org.phoebus.applications.saveandrestore.ui.NodeChangedListener;
import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController;
import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService;
import org.phoebus.core.types.ProcessVariable;
+import org.phoebus.framework.nls.NLS;
import org.phoebus.framework.selection.SelectionService;
import org.phoebus.ui.application.ContextMenuHelper;
import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog;
@@ -65,11 +71,14 @@
import org.phoebus.ui.javafx.ImageCache;
import org.phoebus.util.time.TimestampFormats;
+import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.ResourceBundle;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -78,30 +87,55 @@
public class ConfigurationController extends SaveAndRestoreBaseController implements NodeChangedListener {
@FXML
+ @SuppressWarnings("unused")
private BorderPane root;
@FXML
- private TableColumn pvNameColumn;
+ @SuppressWarnings("unused")
+ private TableColumn pvNameColumn;
@FXML
- private TableView pvTable;
+ @SuppressWarnings("unused")
+ private TableColumn readbackPvNameColumn;
@FXML
+ @SuppressWarnings("unused")
+ private TableColumn comparisonModeColumn;
+
+ @FXML
+ @SuppressWarnings("unused")
+ private TableColumn toleranceColumn;
+
+ @FXML
+ @SuppressWarnings("unused")
+ private TableColumn readOnlyColumn;
+
+ @FXML
+ @SuppressWarnings("unused")
+ private TableView pvTable;
+
+ @FXML
+ @SuppressWarnings("unused")
private TextArea descriptionTextArea;
@FXML
+ @SuppressWarnings("unused")
private Button saveButton;
@FXML
+ @SuppressWarnings("unused")
private TextField pvNameField;
@FXML
+ @SuppressWarnings("unused")
private TextField readbackPvNameField;
@FXML
+ @SuppressWarnings("unused")
private Button addPvButton;
@FXML
+ @SuppressWarnings("unused")
private CheckBox readOnlyCheckBox;
@FXML
@@ -113,25 +147,28 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem
private final SimpleBooleanProperty readOnlyProperty = new SimpleBooleanProperty(false);
@FXML
+ @SuppressWarnings("unused")
private TextField configurationNameField;
@FXML
+ @SuppressWarnings("unused")
private Label configurationCreatedDateField;
@FXML
+ @SuppressWarnings("unused")
private Label configurationLastModifiedDateField;
@FXML
+ @SuppressWarnings("unused")
private Label createdByField;
@FXML
+ @SuppressWarnings("unused")
private Pane addPVsPane;
private SaveAndRestoreService saveAndRestoreService;
private static final Executor UI_EXECUTOR = Platform::runLater;
- private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(false);
-
- private final ObservableList configurationEntries = FXCollections.observableArrayList();
+ private final ObservableList configurationEntries = FXCollections.observableArrayList();
private final SimpleBooleanProperty selectionEmpty = new SimpleBooleanProperty(false);
private final SimpleStringProperty configurationDescriptionProperty = new SimpleStringProperty();
@@ -146,6 +183,9 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem
private final Logger logger = Logger.getLogger(ConfigurationController.class.getName());
+ private final BooleanProperty loadInProgress = new SimpleBooleanProperty();
+ private final BooleanProperty dirty = new SimpleBooleanProperty();
+
public ConfigurationController(ConfigurationTab configurationTab) {
this.configurationTab = configurationTab;
}
@@ -155,16 +195,17 @@ public void initialize() {
saveAndRestoreService = SaveAndRestoreService.getInstance();
+ dirty.addListener((obs, o, n) -> configurationTab.annotateDirty(n));
+
+ pvTable.editableProperty().bind(userIdentity.isNull().not());
pvTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
pvTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> selectionEmpty.set(nv == null));
- ContextMenu pvNameContextMenu = new ContextMenu();
-
MenuItem deleteMenuItem = new MenuItem(Messages.menuItemDeleteSelectedPVs,
new ImageView(ImageCache.getImage(ConfigurationController.class, "/icons/delete.png")));
deleteMenuItem.setOnAction(ae -> {
configurationEntries.removeAll(pvTable.getSelectionModel().getSelectedItems());
- configurationTab.annotateDirty(true);
+ //configurationTab.annotateDirty(true);
pvTable.refresh();
});
@@ -172,50 +213,114 @@ public void initialize() {
|| userIdentity.isNull().get(),
pvTable.getSelectionModel().getSelectedItems(), userIdentity));
- pvNameColumn.setEditable(true);
- pvNameColumn.setCellValueFactory(new PropertyValueFactory<>("pvName"));
+ ContextMenu contextMenu = new ContextMenu();
+ pvTable.setOnContextMenuRequested(event -> {
+ contextMenu.getItems().clear();
+ contextMenu.getItems().addAll(deleteMenuItem);
+ contextMenu.getItems().add(new SeparatorMenuItem());
+ ObservableList selectedPVs = pvTable.getSelectionModel().getSelectedItems();
+ if (!selectedPVs.isEmpty()) {
+ List selectedPVList = selectedPVs.stream()
+ .map(tableEntry -> new ProcessVariable(tableEntry.getPvNameProperty().get()))
+ .collect(Collectors.toList());
+ SelectionService.getInstance().setSelection(SaveAndRestoreApplication.NAME, selectedPVList);
+ ContextMenuHelper.addSupportedEntries(FocusUtil.setFocusOn(pvTable), contextMenu);
+ }
+ });
- pvNameColumn.setCellFactory(new Callback<>() {
- @Override
- public TableCell call(TableColumn param) {
- final TableCell cell = new TableCell<>() {
- @Override
- public void updateItem(String item, boolean empty) {
- super.updateItem(item, empty);
- selectionEmpty.set(empty);
- if (empty) {
- setText(null);
- } else {
- if (isEditing()) {
- setText(null);
- } else {
- setText(getItem());
- setGraphic(null);
- }
- }
+ pvTable.setContextMenu(contextMenu);
+
+ pvNameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
+ pvNameColumn.setCellValueFactory(cell -> cell.getValue().getPvNameProperty());
+ pvNameColumn.setOnEditCommit(t -> {
+ t.getTableView().getItems().get(t.getTablePosition().getRow()).setPvNameProperty(t.getNewValue());
+ setDirty(true);
+ });
+
+ readbackPvNameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
+ readbackPvNameColumn.setCellValueFactory(cell -> cell.getValue().getReadBackPvNameProperty());
+ readbackPvNameColumn.setOnEditCommit(t -> {
+ t.getTableView().getItems().get(t.getTablePosition().getRow()).setReadBackPvNameProperty(t.getNewValue());
+ setDirty(true);
+ });
+
+ readOnlyColumn.setCellFactory(CheckBoxTableCell.forTableColumn(readOnlyColumn));
+ readOnlyColumn.setCellValueFactory(cell -> {
+ BooleanProperty readOnly = cell.getValue().getReadOnlyProperty();
+ readOnly.addListener((obs, o, n) -> setDirty(true));
+ return readOnly;
+ });
+
+ comparisonModeColumn.setCellValueFactory(cell -> cell.getValue().getComparisonModeProperty());
+ comparisonModeColumn.setCellFactory(callback -> {
+ ObservableList values = FXCollections.observableArrayList(Arrays.stream(ComparisonMode.values()).toList());
+ values.add(0, null);
+ ComboBoxTableCell tableCell = new ComboBoxTableCell<>(values) {
+
+ @Override
+ public void commitEdit(ComparisonMode comparisonMode) {
+ getTableView().getItems().get(getIndex()).setComparisonModeProperty(comparisonMode);
+ if (comparisonMode == null) {
+ getTableView().getItems().get(getIndex()).setToleranceProperty(null);
}
- };
- cell.setOnContextMenuRequested(event -> {
- pvNameContextMenu.hide();
- pvNameContextMenu.getItems().clear();
- pvNameContextMenu.getItems().addAll(deleteMenuItem);
- pvNameContextMenu.getItems().add(new SeparatorMenuItem());
- ObservableList selectedPVs = pvTable.getSelectionModel().getSelectedItems();
- if (!selectedPVs.isEmpty()) {
- List selectedPVList = selectedPVs.stream()
- .map(tableEntry -> new ProcessVariable(tableEntry.getPvName()))
- .collect(Collectors.toList());
- SelectionService.getInstance().setSelection(SaveAndRestoreApplication.NAME, selectedPVList);
-
- ContextMenuHelper.addSupportedEntries(FocusUtil.setFocusOn(cell), pvNameContextMenu);
+ setDirty(true);
+ super.commitEdit(comparisonMode);
+ }
+ };
+
+ StringConverter converter = new StringConverter<>() {
+ @Override
+ public String toString(ComparisonMode object) {
+ if (object == null) {
+ return "";
}
- pvNameContextMenu.show(cell, event.getScreenX(), event.getScreenY());
- });
- cell.setContextMenu(pvNameContextMenu);
+ return object.toString();
+ }
+
+ @Override
+ public ComparisonMode fromString(String string) {
+ if (string == null) {
+ return null;
+ }
+ return ComparisonMode.valueOf(string);
+ }
+ };
+ tableCell.setConverter(converter);
+ return tableCell;
+ });
+
+ toleranceColumn.setCellFactory(callback -> new TextFieldTableCell<>(new DoubleStringConverter() {
+ @Override
+ public String toString(Double value) {
+ return value == null ? null : value.toString();
+ }
- return cell;
+ @Override
+ public Double fromString(String string) {
+ try {
+ double value = Double.parseDouble(string);
+ if(value >= 0){
+ // Tolerance must be >= 0.
+ return value;
+ }
+ return null;
+ } catch (Exception e) {
+ // No logging needed: user has entered text that cannot be parsed as double.
+ return null;
+ }
+ }
+ }) {
+ @Override
+ public void commitEdit(Double value) {
+ if (value == null) {
+ return;
+ }
+ getTableView().getItems().get(getIndex()).setToleranceProperty(value);
+ setDirty(true);
+ super.commitEdit(value);
}
});
+ toleranceColumn.setCellValueFactory(cell -> cell.getValue().getToleranceProperty());
pvNameField.textProperty().bindBidirectional(pvNameProperty);
readbackPvNameField.textProperty().bindBidirectional(readbackPvNameProperty);
@@ -224,17 +329,17 @@ public void updateItem(String item, boolean empty) {
descriptionTextArea.textProperty().bindBidirectional(configurationDescriptionProperty);
descriptionTextArea.disableProperty().bind(userIdentity.isNull());
- configurationEntries.addListener((ListChangeListener) change -> {
+ configurationEntries.addListener((ListChangeListener) change -> {
while (change.next()) {
if (change.wasAdded() || change.wasRemoved()) {
FXCollections.sort(configurationEntries);
- dirty.set(true);
+ setDirty(true);
}
}
});
- configurationNameProperty.addListener((observableValue, oldValue, newValue) -> dirty.set(!newValue.equals(configurationNode.getName())));
- configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> dirty.set(!newValue.equals(configurationNode.get().getDescription())));
+ configurationNameProperty.addListener((observableValue, oldValue, newValue) -> setDirty(!newValue.equals(configurationNode.getName())));
+ configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> setDirty(!newValue.equals(configurationNode.get().getDescription())));
saveButton.disableProperty().bind(Bindings.createBooleanBinding(() -> dirty.not().get() ||
configurationDescriptionProperty.isEmpty().get() ||
@@ -242,7 +347,10 @@ public void updateItem(String item, boolean empty) {
userIdentity.isNull().get(),
dirty, configurationDescriptionProperty, configurationNameProperty, userIdentity));
- addPvButton.disableProperty().bind(pvNameField.textProperty().isEmpty());
+ addPvButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
+ pvNameField.textProperty().isEmpty().get() &&
+ readbackPvNameField.textProperty().isEmpty().get(),
+ pvNameField.textProperty(), readbackPvNameField.textProperty()));
readOnlyCheckBox.selectedProperty().bindBidirectional(readOnlyProperty);
@@ -268,12 +376,13 @@ public void updateItem(String item, boolean empty) {
}
@FXML
+ @SuppressWarnings("unused")
public void saveConfiguration() {
UI_EXECUTOR.execute(() -> {
try {
configurationNode.get().setName(configurationNameProperty.get());
configurationNode.get().setDescription(configurationDescriptionProperty.get());
- configurationData.setPvList(configurationEntries);
+ configurationData.setPvList(configurationEntries.stream().map(ConfigPvEntry::toConfigPv).toList());
Configuration configuration = new Configuration();
configuration.setConfigurationNode(configurationNode.get());
configuration.setConfigurationData(configurationData);
@@ -281,12 +390,10 @@ public void saveConfiguration() {
configuration = saveAndRestoreService.createConfiguration(configurationNodeParent,
configuration);
configurationTab.setId(configuration.getConfigurationNode().getUniqueId());
- configurationTab.updateTabTitle(configuration.getConfigurationNode().getName());
} else {
configuration = saveAndRestoreService.updateConfiguration(configuration);
}
configurationData = configuration.getConfigurationData();
- dirty.set(false);
loadConfiguration(configuration.getConfigurationNode());
} catch (Exception e1) {
ExceptionDetailsErrorDialog.openError(pvTable,
@@ -298,6 +405,7 @@ public void saveConfiguration() {
}
@FXML
+ @SuppressWarnings("unused")
public void addPv() {
UI_EXECUTOR.execute(() -> {
@@ -305,27 +413,44 @@ public void addPv() {
String[] pvNames = pvNameProperty.get().trim().split("[\\s;]+");
String[] readbackPvNames = readbackPvNameProperty.get().trim().split("[\\s;]+");
- ArrayList configPVs = new ArrayList<>();
+ if(!checkForDuplicatePvNames(pvNames)){
+ return;
+ }
+
+ ArrayList configPVs = new ArrayList<>();
for (int i = 0; i < pvNames.length; i++) {
- // Disallow duplicate PV names as in a restore operation this would mean that a PV is written
- // multiple times, possibly with different values.
String pvName = pvNames[i].trim();
- if (configurationEntries.stream().anyMatch(s -> s.getPvName().equals(pvName))) {
- continue;
- }
String readbackPV = i >= readbackPvNames.length ? null : readbackPvNames[i] == null || readbackPvNames[i].isEmpty() ? null : readbackPvNames[i].trim();
ConfigPv configPV = ConfigPv.builder()
.pvName(pvName)
.readOnly(readOnlyProperty.get())
.readbackPvName(readbackPV)
.build();
- configPVs.add(configPV);
+ configPVs.add(new ConfigPvEntry(configPV));
}
configurationEntries.addAll(configPVs);
- configurationTab.annotateDirty(true);
resetAddPv();
});
+ }
+ /**
+ * Checks that added PV names are not added multiple times
+ * @param addedPvNames New PV names added in the UI
+ * @return true if no duplicates are detected, otherwise false
+ */
+ private boolean checkForDuplicatePvNames(String[] addedPvNames){
+ List pvNamesAsList = new ArrayList<>();
+ pvNamesAsList.addAll(Arrays.asList(addedPvNames));
+ pvTable.itemsProperty().get().forEach(i -> pvNamesAsList.add(i.getPvNameProperty().get()));
+ List duplicatePvNames = pvNamesAsList.stream().filter(n -> Collections.frequency(pvNamesAsList, n) > 1).toList();
+
+ if(duplicatePvNames.size() > 0){
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setHeaderText(Messages.duplicatePVNamesNotSupported);
+ alert.showAndWait();
+ return false;
+ }
+ return true;
}
private void resetAddPv() {
@@ -348,7 +473,7 @@ public void newConfiguration(Node parentNode) {
configurationData = new ConfigurationData();
pvTable.setItems(configurationEntries);
UI_EXECUTOR.execute(() -> configurationNameField.requestFocus());
- dirty.set(false);
+ setDirty(false);
}
/**
@@ -363,6 +488,7 @@ public void loadConfiguration(final Node node) {
ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e);
return;
}
+ loadInProgress.set(true);
// Create a cloned Node object to avoid changes in the Node object contained in the tree view.
configurationNode.set(Node.builder().uniqueId(node.getUniqueId())
.name(node.getName())
@@ -372,16 +498,16 @@ public void loadConfiguration(final Node node) {
.created(node.getCreated())
.lastModified(node.getLastModified())
.build());
- loadConfigurationData();
+ loadConfigurationData(() -> loadInProgress.set(false));
}
- private void loadConfigurationData() {
+ private void loadConfigurationData(Runnable completion) {
UI_EXECUTOR.execute(() -> {
try {
Collections.sort(configurationData.getPvList());
- configurationEntries.setAll(configurationData.getPvList());
+ configurationEntries.setAll(configurationData.getPvList().stream().map(ConfigPvEntry::new).toList());
pvTable.setItems(configurationEntries);
- dirty.set(false);
+ completion.run();
} catch (Exception e) {
logger.log(Level.WARNING, "Unable to load existing configuration");
}
@@ -412,4 +538,8 @@ public void nodeChanged(Node node) {
.build());
}
}
+
+ private void setDirty(boolean dirty) {
+ this.dirty.set(dirty && !loadInProgress.get());
+ }
}
diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java
index ad4d54cba9..572a7f4a2c 100644
--- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java
+++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java
@@ -95,7 +95,7 @@ public void configureForNewConfiguration(Node parentNode) {
@Override
public void nodeChanged(Node node) {
if (node.getUniqueId().equals(getId())) {
- textProperty().set(node.getName());
+ Platform.runLater(() -> textProperty().set(node.getName()));
}
}
@@ -104,15 +104,21 @@ public void nodeChanged(Node node) {
*
* @param tabTitle The wanted tab title.
*/
- public void updateTabTitle(String tabTitle) {
+ private void updateTabTitle(String tabTitle) {
Platform.runLater(() -> textProperty().set(tabTitle));
}
+ /**
+ * Updates the tab to indicate if the data is dirty and needs to be saved.
+ * @param dirty If true, an asterisk is prepended, otherwise
+ * only the name {@link org.phoebus.applications.saveandrestore.model.Configuration}
+ * is rendered.
+ */
public void annotateDirty(boolean dirty) {
String tabTitle = textProperty().get();
- if (dirty && !tabTitle.contains("*")) {
+ if (dirty && !tabTitle.startsWith("*")) {
updateTabTitle("* " + tabTitle);
- } else if (!dirty) {
+ } else if (!dirty && tabTitle.startsWith("*")) {
updateTabTitle(tabTitle.substring(2));
}
}
diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties
index 0b54021a05..4936a88f4c 100644
--- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties
+++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties
@@ -19,6 +19,8 @@ closeConfigurationWarning=Configuration modified, but not saved. Do you wish to
closeCompositeSnapshotWarning=Composite snapshot modified, but not saved. Do you wish to continue?
closeTabPrompt=Close tab?
comment=Comment
+comparisonMode=Comparison Mode
+comparisonTolerance=Tolerance
compositeSnapshotConsistencyCheckFailed=Failed to check consistency for composite snapshot
copy=Copy
createdDate=Created
@@ -67,8 +69,9 @@ deleteFilterFailed=Failed to delete filter
description=Description
descriptionHint=Specify non-empty description
duplicatePVNamesAdditionalItems={0} additional PV names
-duplicatePVNamesFoundInSelection=Cannot add selected snapshots, duplicate PV names found
duplicatePVNamesCheckFailed=Cannot check if snapshots may be added, remote service off-line?
+duplicatePVNamesFoundInSelection=Cannot add selected snapshots, duplicate PV names found
+duplicatePVNamesNotSupported=Duplicate PV names detected. This is not supported.
editFilter=Edit filter
errorActionFailed=Action failed
errorAddTagFailed=Failed to add tag
diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml
index 0080db9c83..70ac3d2d65 100644
--- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml
+++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml
@@ -5,7 +5,8 @@
-
+
@@ -53,25 +54,15 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Comparison.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Comparison.java
new file mode 100644
index 0000000000..f5fec2a01d
--- /dev/null
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Comparison.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 European Spallation Source ERIC.
+ */
+
+package org.phoebus.applications.saveandrestore.model;
+
+public class Comparison{
+
+ private ComparisonMode comparisonMode;
+ private Double tolerance;
+
+ public Comparison(){
+
+ }
+
+ public Comparison(ComparisonMode comparisonMode, Double tolerance){
+ this.comparisonMode = comparisonMode;
+ this.tolerance = tolerance;
+ }
+
+ public ComparisonMode getComparisonMode() {
+ return comparisonMode;
+ }
+
+ public void setComparisonMode(ComparisonMode comparisonMode) {
+ this.comparisonMode = comparisonMode;
+ }
+
+ public Double getTolerance() {
+ return tolerance;
+ }
+
+ public void setTolerance(Double tolerance) {
+ this.tolerance = tolerance;
+ }
+}
diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/PvCompareMode.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ComparisonMode.java
similarity index 88%
rename from app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/PvCompareMode.java
rename to app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ComparisonMode.java
index 96619b5e4a..7add697a99 100644
--- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/PvCompareMode.java
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ComparisonMode.java
@@ -7,7 +7,7 @@
/**
* Specifies how comparison between PV values should be performed.
*/
-public enum PvCompareMode {
+public enum ComparisonMode {
RELATIVE,
ABSOLUTE;
}
diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompareResult.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ComparisonResult.java
similarity index 74%
rename from app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompareResult.java
rename to app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ComparisonResult.java
index 859765317b..b158543364 100644
--- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompareResult.java
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ComparisonResult.java
@@ -18,12 +18,11 @@
* For the live {@link VType} value, null indicates failure to connect to the PV.
*/
@SuppressWarnings("unused")
-public class CompareResult{
+public class ComparisonResult {
private String pvName;
private boolean equal;
- private PvCompareMode pvCompareMode;
- private double tolerance;
+ private Comparison comparison;
@JsonSerialize(using = VTypeSerializer.class)
@JsonDeserialize(using = VTypeDeserializer.class)
private VType storedValue;
@@ -35,20 +34,18 @@ public class CompareResult{
/**
* Needed by unit tests. Do not remove.
*/
- public CompareResult(){}
+ public ComparisonResult(){}
- public CompareResult(String pvName,
- boolean equal,
- PvCompareMode pvCompareMode,
- double tolerance,
- VType storedValue,
- VType liveValue,
- String delta){
+ public ComparisonResult(String pvName,
+ boolean equal,
+ Comparison comparison,
+ VType storedValue,
+ VType liveValue,
+ String delta){
this.pvName = pvName;
this.equal = equal;
- this.pvCompareMode = pvCompareMode;
- this.tolerance = tolerance;
+ this.comparison = comparison;
this.storedValue = storedValue;
this.liveValue = liveValue;
this.delta = delta;
@@ -58,16 +55,12 @@ public boolean isEqual() {
return equal;
}
- public PvCompareMode getPvCompareMode() {
- return pvCompareMode;
+ public Comparison getComparison() {
+ return comparison;
}
- public double getTolerance() {
- return tolerance;
- }
-
- public void setTolerance(double tolerance) {
- this.tolerance = tolerance;
+ public void setComparison(Comparison comparison) {
+ this.comparison = comparison;
}
/**
diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ConfigPv.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ConfigPv.java
index 52fbd1ae15..0a2c2180e5 100644
--- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ConfigPv.java
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/ConfigPv.java
@@ -1,16 +1,16 @@
-/**
+/**
* Copyright (C) 2018 European Spallation Source ERIC.
- *
+ *
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
@@ -18,107 +18,149 @@
package org.phoebus.applications.saveandrestore.model;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
import java.util.Objects;
/**
* Class encapsulating data to describe a PV subject to a save operation. A read-back PV name
* is optionally associated with the PV name. A PV record is uniquely identified by both the PV name
* and the read-back PV name (if it has been specified).
+ *
+ * A {@link ComparisonMode} can optionally be specified to
+ * indicate how the stored value (as defined by the {@link #pvName} field) is compared to a live value
+ * if a client requests it. A non-null {@link ComparisonMode} must be paired non-null and a
+ * {@link #tolerance} value ≥0.
+ *
* @author georgweiss
* Created 1 Oct 2018
*/
-public class ConfigPv implements Comparable{
+public class ConfigPv implements Comparable {
+
+ /**
+ * Set-point PV name.
+ */
+ private String pvName;
+
+ /**
+ * Optional read-back PV name
+ */
+ private String readbackPvName;
+
+ /**
+ * Flag indicating if set-point PV value should be restored or not.
+ */
+ private boolean readOnly = false;
+
+ private Comparison comparison;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Double tolerance = null;
+
+ public String getPvName() {
+ return pvName;
+ }
- private String pvName;
- private String readbackPvName;
- private boolean readOnly = false;
+ public void setPvName(String pvName) {
+ this.pvName = pvName;
+ }
- public String getPvName() {
- return pvName;
- }
+ public void setReadbackPvName(String readbackPvName) {
+ this.readbackPvName = readbackPvName;
+ }
- public void setPvName(String pvName) {
- this.pvName = pvName;
- }
+ public String getReadbackPvName(){
+ return readbackPvName;
+ }
- public String getReadbackPvName() {
- return readbackPvName;
- }
+ public boolean isReadOnly() {
+ return readOnly;
+ }
- public void setReadbackPvName(String readbackPvName) {
- this.readbackPvName = readbackPvName;
- }
+ public void setReadOnly(boolean readOnly) {
+ this.readOnly = readOnly;
+ }
- public boolean isReadOnly() {
- return readOnly;
- }
+ public Comparison getComparison() {
+ return comparison;
+ }
- public void setReadOnly(boolean readOnly) {
- this.readOnly = readOnly;
- }
+ public void setComparison(Comparison comparison) {
+ this.comparison = comparison;
+ }
- @Override
- public boolean equals(Object other) {
- if(other instanceof ConfigPv otherConfigPv) {
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ConfigPv otherConfigPv) {
return Objects.equals(pvName, otherConfigPv.getPvName()) && Objects.equals(readbackPvName, otherConfigPv.getReadbackPvName()) && Objects.equals(readOnly, otherConfigPv.isReadOnly());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(pvName, readbackPvName, readOnly);
- }
-
- @Override
- public String toString() {
-
- return new StringBuffer()
- .append("PV name=").append(pvName)
- .append(", readback PV name=").append(readbackPvName == null ? "null" : readbackPvName)
- .append(", readOnly=").append(readOnly)
- .toString();
- }
-
- /**
- * Comparison is simply a comparison of the {@code pvName} field.
- * @param other The object to compare to.
- * @return The comparison result, typically used to sort list of {@link ConfigPv}s by name
- */
- @Override
- public int compareTo(ConfigPv other) {
- return pvName.compareTo(other.getPvName());
- }
-
- public static Builder builder(){
- return new Builder();
- }
-
- public static class Builder{
-
- private final ConfigPv configPv;
-
- private Builder(){
- configPv = new ConfigPv();
- }
-
- public Builder pvName(String pvName){
- configPv.setPvName(pvName);
- return this;
- }
-
- public Builder readbackPvName(String readbackPvName){
- configPv.setReadbackPvName(readbackPvName);
- return this;
- }
-
- public Builder readOnly(boolean readOnly){
- configPv.setReadOnly(readOnly);
- return this;
- }
-
- public ConfigPv build(){
- return configPv;
- }
- }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pvName, readbackPvName, readOnly);
+ }
+
+ @Override
+ public String toString() {
+
+ StringBuilder stringBuffer = new StringBuilder()
+ .append("PV name=").append(pvName)
+ .append(", readback PV name=").append(readbackPvName)
+ .append(", readOnly=").append(readOnly)
+ .append(", tolerance=").append(tolerance);
+ if(comparison != null){
+ stringBuffer.append(", comparison mode=").append(comparison.getComparisonMode());
+ stringBuffer.append(", tolerance=").append(comparison.getTolerance());
+ }
+ return stringBuffer.toString();
+ }
+
+ /**
+ * Comparison is simply a comparison of the {@code pvName} field.
+ * @param other The object to compare to.
+ * @return The comparison result, typically used to sort list of {@link ConfigPv}s by name
+ */
+ @Override
+ public int compareTo(ConfigPv other) {
+ return pvName.compareTo(other.getPvName());
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final ConfigPv configPv;
+
+ private Builder() {
+ configPv = new ConfigPv();
+ }
+
+ public Builder pvName(String pvName) {
+ configPv.setPvName(pvName);
+ return this;
+ }
+
+ public Builder readbackPvName(String readbackPvName) {
+ configPv.setReadbackPvName(readbackPvName);
+ return this;
+ }
+
+ public Builder readOnly(boolean readOnly) {
+ configPv.setReadOnly(readOnly);
+ return this;
+ }
+
+ public Builder comparison(Comparison comparison) {
+ configPv.setComparison(comparison);
+ return this;
+ }
+
+ public ConfigPv build() {
+ return configPv;
+ }
+ }
}
diff --git a/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/ConfigPvTest.java b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/ConfigPvTest.java
index 2912f7c266..0814aa6bba 100644
--- a/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/ConfigPvTest.java
+++ b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/ConfigPvTest.java
@@ -33,84 +33,89 @@
*/
public class ConfigPvTest {
- @Test
- public void testConfigPv() {
- assertNull(ConfigPv.builder().pvName("a").build().getReadbackPvName());
- assertFalse(ConfigPv.builder().pvName("a").build().isReadOnly());
-
- ConfigPv configPV = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
-
- assertEquals("b", configPV.getReadbackPvName());
- assertTrue(configPV.isReadOnly());
- }
-
- @Test
- public void testEquals() {
-
-
- ConfigPv configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- assertNotEquals(configPV1, new Object());
- assertNotEquals(null, configPV1);
- ConfigPv configPV2 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- ConfigPv configPV3 = ConfigPv.builder().pvName("a").readbackPvName("c").readOnly(true).build();
-
- assertEquals(configPV1, configPV2);
- assertNotEquals(configPV1, configPV3);
-
- configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- configPV2 = ConfigPv.builder().pvName("b").readbackPvName("b").readOnly(true).build();
-
- assertNotEquals(configPV1, configPV2);
-
- configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
- configPV2 = ConfigPv.builder().pvName("b").readOnly(true).build();
-
- assertNotEquals(configPV1, configPV2);
-
- configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
- configPV2 = ConfigPv.builder().pvName("a").readOnly(true).build();
-
- assertEquals(configPV1, configPV2);
- }
-
- @Test
- public void testHashCode() {
- ConfigPv configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- ConfigPv configPV2 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
-
- assertEquals(configPV1.hashCode(), configPV2.hashCode());
-
- configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- configPV2 = ConfigPv.builder().pvName("b").readbackPvName("b").readOnly(true).build();
-
- assertNotEquals(configPV1.hashCode(), configPV2.hashCode());
-
- configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
- configPV2 = ConfigPv.builder().pvName("b").readOnly(true).build();
-
- assertNotEquals(configPV1.hashCode(), configPV2.hashCode());
-
- configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
- configPV2 = ConfigPv.builder().pvName("a").readOnly(true).build();
-
- assertEquals(configPV1.hashCode(), configPV2.hashCode());
- }
-
- @Test
- public void testToString() {
- assertNotNull(ConfigPv.builder().build().toString());
- assertNotNull(ConfigPv.builder().readbackPvName("a").build().toString());
- assertNotNull(ConfigPv.builder().readbackPvName("").build().toString());
- }
-
- @Test
- public void testCompareTo(){
- ConfigPv configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- ConfigPv configPV2 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
- ConfigPv configPV3 = ConfigPv.builder().pvName("b").readbackPvName("b").readOnly(true).build();
-
- assertEquals(0, configPV1.compareTo(configPV2));
- assertTrue(configPV1.compareTo(configPV3) < 0);
- assertTrue(configPV3.compareTo(configPV1) > 0);
- }
+ @Test
+ public void testConfigPv() {
+ assertNull(ConfigPv.builder().pvName("a").build().getReadbackPvName());
+ assertFalse(ConfigPv.builder().pvName("a").build().isReadOnly());
+
+ ConfigPv configPV = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+
+ assertEquals("b", configPV.getReadbackPvName());
+ assertTrue(configPV.isReadOnly());
+ assertNull(configPV.getComparison());
+
+ configPV = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).comparison(new Comparison(ComparisonMode.ABSOLUTE, 1.0)).build();
+ assertEquals(ComparisonMode.ABSOLUTE, configPV.getComparison().getComparisonMode());
+ assertEquals(1.0, configPV.getComparison().getTolerance());
+ }
+
+ @Test
+ public void testEquals() {
+
+
+ ConfigPv configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ assertNotEquals(configPV1, new Object());
+ assertNotEquals(null, configPV1);
+ ConfigPv configPV2 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ ConfigPv configPV3 = ConfigPv.builder().pvName("a").readbackPvName("c").readOnly(true).build();
+
+ assertEquals(configPV1, configPV2);
+ assertNotEquals(configPV1, configPV3);
+
+ configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ configPV2 = ConfigPv.builder().pvName("b").readbackPvName("b").readOnly(true).build();
+
+ assertNotEquals(configPV1, configPV2);
+
+ configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
+ configPV2 = ConfigPv.builder().pvName("b").readOnly(true).build();
+
+ assertNotEquals(configPV1, configPV2);
+
+ configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
+ configPV2 = ConfigPv.builder().pvName("a").readOnly(true).build();
+
+ assertEquals(configPV1, configPV2);
+ }
+
+ @Test
+ public void testHashCode() {
+ ConfigPv configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ ConfigPv configPV2 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+
+ assertEquals(configPV1.hashCode(), configPV2.hashCode());
+
+ configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ configPV2 = ConfigPv.builder().pvName("b").readbackPvName("b").readOnly(true).build();
+
+ assertNotEquals(configPV1.hashCode(), configPV2.hashCode());
+
+ configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
+ configPV2 = ConfigPv.builder().pvName("b").readOnly(true).build();
+
+ assertNotEquals(configPV1.hashCode(), configPV2.hashCode());
+
+ configPV1 = ConfigPv.builder().pvName("a").readOnly(true).build();
+ configPV2 = ConfigPv.builder().pvName("a").readOnly(true).build();
+
+ assertEquals(configPV1.hashCode(), configPV2.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ assertNotNull(ConfigPv.builder().build().toString());
+ assertNotNull(ConfigPv.builder().readbackPvName("a").build().toString());
+ assertNotNull(ConfigPv.builder().readbackPvName("").build().toString());
+ }
+
+ @Test
+ public void testCompareTo() {
+ ConfigPv configPV1 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ ConfigPv configPV2 = ConfigPv.builder().pvName("a").readbackPvName("b").readOnly(true).build();
+ ConfigPv configPV3 = ConfigPv.builder().pvName("b").readbackPvName("b").readOnly(true).build();
+
+ assertEquals(0, configPV1.compareTo(configPV2));
+ assertTrue(configPV1.compareTo(configPV3) < 0);
+ assertTrue(configPV3.compareTo(configPV1) > 0);
+ }
}
diff --git a/app/save-and-restore/util/pom.xml b/app/save-and-restore/util/pom.xml
index f7083fcdf3..7eab278d22 100644
--- a/app/save-and-restore/util/pom.xml
+++ b/app/save-and-restore/util/pom.xml
@@ -34,6 +34,12 @@
4.7.4-SNAPSHOT
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
+
org.junit.jupiterjunit-jupiter
diff --git a/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/SnapshotUtil.java b/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/SnapshotUtil.java
index a5a9b8460c..0e670522a0 100644
--- a/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/SnapshotUtil.java
+++ b/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/SnapshotUtil.java
@@ -1,11 +1,14 @@
package org.phoebus.saveandrestore.util;
+import org.apache.commons.collections4.CollectionUtils;
+import org.epics.vtype.VNumber;
import org.epics.vtype.VType;
-import org.phoebus.applications.saveandrestore.model.CompareResult;
+import org.phoebus.applications.saveandrestore.model.Comparison;
+import org.phoebus.applications.saveandrestore.model.ComparisonResult;
import org.phoebus.applications.saveandrestore.model.ConfigPv;
import org.phoebus.applications.saveandrestore.model.Configuration;
import org.phoebus.applications.saveandrestore.model.ConfigurationData;
-import org.phoebus.applications.saveandrestore.model.PvCompareMode;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
import org.phoebus.applications.saveandrestore.model.RestoreResult;
import org.phoebus.applications.saveandrestore.model.SnapshotItem;
import org.phoebus.core.vtypes.VTypeHelper;
@@ -16,6 +19,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -39,8 +43,6 @@ public class SnapshotUtil {
private final int connectionTimeout = Preferences.connectionTimeout;
- private final int writeTimeout = Preferences.writeTimeout;
-
private final ExecutorService executorService = Executors.newCachedThreadPool();
public SnapshotUtil() {
@@ -275,47 +277,175 @@ public void release() {
}
/**
- * Performs comparison between PV values to determine equality. The idea is to generate a return value mimicking
- * the save-and-restore snapshot view, i.e. to show both stored and live values, plus an indication of equality.
- * The comparison algorithm is the same as employed by the snapshot view.
+ * @see #comparePvs(List, List, double, ComparisonMode, boolean)
+ */
+ public List comparePvs(final List savedSnapshotItems,
+ double tolerance,
+ ComparisonMode compareMode,
+ boolean skipReadback) {
+ return comparePvs(savedSnapshotItems, null, tolerance, compareMode, skipReadback);
+ }
+
+ /**
+ * Performs comparison between stored PV values and live values. Note that comparison uses optional data stored
+ * in the snapshot's parent configuration data, see {@link ConfigPv}, if defined. Caller is responsible for
+ * passing a list of {@link ConfigPv}s matching the list of {@link SnapshotItem}s. Since configurations may
+ * have been updated (e.g. PVs added) after snapshots have been created, any {@link SnapshotItem}'s PV not found
+ * in the list of {@link ConfigPv}s will be compared using the provided {@link ComparisonMode} and tolerance.
+ * If the list of {@link ConfigPv}s is null or empty, the provided {@link ComparisonMode} and tolerance is used.
+ *
+ * Equality between a stored value and the live value is determined on each PV like so:
+ *
+ *
If the configuration of a PV does not specify a {@link ComparisonMode} and tolerance,
+ * the compareMode and tolerance parameters are used.
+ * compareMode however is optional and defaults to {@link ComparisonMode#ABSOLUTE},
+ * while tolerance defaults to zero.
+ *
+ *
+ * The base (reference) value is always the value stored in the value field of a {@link org.phoebus.applications.saveandrestore.model.SnapshotItem}
+ * object. It corresponds to the pvName field, i.e. never the readbackPvName of
+ * a {@link ConfigPv} object.
+ *
+ *
+ * The live value used in the comparison is either the value corresponding to pvName, or
+ * readbackPvName if specified. The latter can be overridden with the skipReadback
+ * parameter.
+ *
+ *
+ * Comparison will consider {@link ComparisonMode} and tolerance only on numeric scalar types.
+ * See {@link Utilities}.
+ *
+ *
+ *
*
* @param savedSnapshotItems A list if {@link SnapshotItem}s as pulled from a stored snapshot.
- * @param tolerance A tolerance (must be >=0) value used in the comparison. Comparisons use the tolerance
- * value for a relative comparison.
- * @return A list of {@link CompareResult}s, one for each {@link SnapshotItem} in the provided input. Note though that
- * if the comparison evaluates to equal, then the actual live and stored value are not added to the {@link CompareResult}
- * objects in order to avoid handling/transferring potentially large amounts of data.
+ * @param configPvs The list of {@link ConfigPv}s the items in savedSnapshotItems.
+ * May be null or empty.
+ * @param tolerance A tolerance (must be >=0) value used in the comparison.
+ * @param comparisonMode Determines if comparison is relative or absolute.
+ * @param skipReadback Indicates that comparison should not use the read-back PV, even if specified.
+ * @return A list of {@link ComparisonResult}s, one for each {@link SnapshotItem}. Note though that
+ * if the comparison evaluates to equal, then the actual live and stored value are not added to the {@link ComparisonResult}
+ * objects in order to avoid transferring potentially large amounts of data (e.g. large arrays).
*/
- public List comparePvs(final List savedSnapshotItems, double tolerance) {
+ public List comparePvs(final List savedSnapshotItems,
+ final List configPvs,
+ double tolerance,
+ ComparisonMode comparisonMode,
+ boolean skipReadback) {
if (tolerance < 0) {
throw new RuntimeException("Tolerance value must be >=0");
}
- List compareResults = new ArrayList<>();
+ // Default to absolute.
+ if(comparisonMode == null){
+ comparisonMode = ComparisonMode.ABSOLUTE;
+ }
+ final Comparison defaultComparison = new Comparison(comparisonMode, tolerance);
+ List comparisonResults = new ArrayList<>();
// Extract the list of ConfigPvs and...
- List configPvs = savedSnapshotItems.stream().map(si -> si.getConfigPv()).toList();
+ List configPvsFromSnapshot = savedSnapshotItems.stream().map(SnapshotItem::getConfigPv).toList();
// ...take snapshot to retrieve live values
- List liveSnapshotItems = takeSnapshot(configPvs);
+ List liveSnapshotItems = takeSnapshot(configPvsFromSnapshot);
savedSnapshotItems.forEach(savedItem -> {
SnapshotItem liveSnapshotItem = liveSnapshotItems.stream().filter(si -> si.getConfigPv().getPvName().equals(savedItem.getConfigPv().getPvName())).findFirst().orElse(null);
if (liveSnapshotItem == null) {
throw new RuntimeException("Unable to match stored PV " + savedItem.getConfigPv().getPvName() + " in list of live PVs");
}
- VType storedValue = savedItem.getValue();
+ VType storedValue = savedItem.getValue(); // Always PV name field, even if read-back PV is specified
VType liveValue = liveSnapshotItem.getValue();
- Threshold threshold = new Threshold<>(tolerance);
- boolean equal = Utilities.areValuesEqual(storedValue, liveValue, Optional.of(threshold));
- CompareResult compareResult = new CompareResult(savedItem.getConfigPv().getPvName(),
+ VType liveReadbackValue = liveSnapshotItem.getReadbackValue();
+
+ Comparison finalComparison =
+ new Comparison(defaultComparison.getComparisonMode(), defaultComparison.getTolerance());
+ // Does this SnapshotItems configuration define per-PV Comparison?
+ Comparison perPvComparison = getComparison(configPvs, savedItem.getConfigPv().getPvName());
+ if(perPvComparison != null){
+ finalComparison = perPvComparison;
+ }
+
+ // Determine if comparison is made on read-back or not.
+ VType referenceValue = getReferenceValue(liveValue, liveReadbackValue, skipReadback);
+
+ // For relative tolerance and scalar types, compute an absolute tolerance
+ // since this is what Utilities.areValuesEqual expects.
+ if(finalComparison.getTolerance() > 0 &&
+ finalComparison.getComparisonMode().equals(ComparisonMode.RELATIVE) &&
+ referenceValue instanceof VNumber){
+ finalComparison.setTolerance(VTypeHelper.toDouble(referenceValue) * finalComparison.getTolerance());
+ }
+
+ Threshold threshold = new Threshold<>(finalComparison.getTolerance());
+ boolean equal = Utilities.areValuesEqual(storedValue, referenceValue, Optional.of(threshold));
+ ComparisonResult comparisonResult = new ComparisonResult(savedItem.getConfigPv().getPvName(),
equal,
- PvCompareMode.RELATIVE,
- tolerance,
+ finalComparison,
equal ? null : storedValue, // Do not add potentially large amounts of data if comparison shows equal
equal ? null : liveValue, // Do not add potentially large amounts of data if comparison shows equal
Utilities.deltaValueToString(storedValue, liveValue, Optional.of(threshold)).getString());
- compareResults.add(compareResult);
+ comparisonResults.add(comparisonResult);
});
- return compareResults;
+ comparisonResults.addAll(handleConfigSnapshotDiff(configPvs, savedSnapshotItems));
+
+ return comparisonResults;
+ }
+
+ protected VType getReferenceValue(final VType liveValue, final VType liveReadbackValue, final boolean skipReadback){
+ if(skipReadback){
+ return liveValue;
+ }
+ return liveReadbackValue != null ? liveReadbackValue : liveValue;
+ }
+
+ /**
+ * Locates the {@link ConfigPv} for a PV name as defined in the snapshot. Note that this is needed as the
+ * configuration may have changed (e.g. with respect to {@link Comparison} data) since the snapshot was created.
+ * @param configPvs List of {@link ConfigPv}s, may be null or empty.
+ * @param pvName The PV name to look for.
+ * @return A {@link Comparison} object if the corresponding configuration defines it for this PV name.
+ * Otherwise null.
+ */
+ protected Comparison getComparison(List configPvs, String pvName){
+ if(configPvs == null){
+ return null;
+ }
+ Optional configPvOptional = configPvs.stream().filter(cp -> cp.getPvName().equals(pvName)).findFirst();
+ if(configPvOptional.isPresent()){
+ return configPvOptional.get().getComparison();
+ }
+ return null;
+ }
+
+ /**
+ * Check if list of ConfigPvs contains items not found in the snapshot. Since such items cannot be compared,
+ * they are by definition non-equal.
+ * @param configPvs List of {@link ConfigPv}s, may be null or empty.
+ * @param savedSnapshotItems List of saved {@link SnapshotItem}s
+ * @return A potentially empty list of {@link ComparisonResult}s, each indicating that a PV was found in the
+ * configuration, but not in the saved snapshot.
+ */
+ protected List handleConfigSnapshotDiff(List configPvs, List savedSnapshotItems){
+ if(configPvs == null){
+ return Collections.emptyList();
+ }
+ List comparisonResults = new ArrayList<>();
+
+ if(configPvs != null){
+ Collection pvNamesInConfig = configPvs.stream().map(ConfigPv::getPvName).toList();
+ Collection pvNamesInSnapshot = savedSnapshotItems.stream().map(i -> i.getConfigPv().getPvName()).toList();
+ Collection pvNameDiff = CollectionUtils.removeAll(pvNamesInConfig, pvNamesInSnapshot);
+ pvNameDiff.forEach(pvName -> {
+ ComparisonResult comparisonResult = new ComparisonResult(pvName,
+ false,
+ null,
+ null,
+ null,
+ "PV found in config but not in snapshot");
+ comparisonResults.add(comparisonResult);
+ });
+ }
+ return comparisonResults;
}
}
diff --git a/app/save-and-restore/util/src/test/java/org/phoebus/saveandrestore/util/SnapshotUtilTest.java b/app/save-and-restore/util/src/test/java/org/phoebus/saveandrestore/util/SnapshotUtilTest.java
index fc2c9c4178..3d3eb42762 100644
--- a/app/save-and-restore/util/src/test/java/org/phoebus/saveandrestore/util/SnapshotUtilTest.java
+++ b/app/save-and-restore/util/src/test/java/org/phoebus/saveandrestore/util/SnapshotUtilTest.java
@@ -12,9 +12,11 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
-import org.phoebus.applications.saveandrestore.model.CompareResult;
+import org.phoebus.applications.saveandrestore.model.Comparison;
+import org.phoebus.applications.saveandrestore.model.ComparisonResult;
import org.phoebus.applications.saveandrestore.model.ConfigPv;
import org.phoebus.applications.saveandrestore.model.ConfigurationData;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
import org.phoebus.applications.saveandrestore.model.SnapshotItem;
import org.phoebus.core.vtypes.VTypeHelper;
import org.phoebus.pv.PV;
@@ -98,7 +100,7 @@ public void testComparePvs(){
snapshotItem2.setConfigPv(configPv2);
snapshotItem2.setValue(VDouble.of(771.0, Alarm.none(), Time.now(), Display.none()));
- List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.0);
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.0, ComparisonMode.ABSOLUTE, false);
assertTrue(compareResults.get(0).isEqual());
assertTrue(compareResults.get(1).isEqual());
@@ -106,7 +108,7 @@ public void testComparePvs(){
snapshotItem1.setValue(VDouble.of(43.0, Alarm.none(), Time.now(), Display.none()));
snapshotItem2.setValue(VDouble.of(771.0, Alarm.none(), Time.now(), Display.none()));
- compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.0);
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.0, ComparisonMode.ABSOLUTE, false);
assertFalse(compareResults.get(0).isEqual());
assertNotNull(compareResults.get(0).getStoredValue());
@@ -118,12 +120,308 @@ public void testComparePvs(){
snapshotItem1.setValue(VDouble.of(43.0, Alarm.none(), Time.now(), Display.none()));
snapshotItem2.setValue(VDouble.of(771.0, Alarm.none(), Time.now(), Display.none()));
- compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 10.0);
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 10.0, ComparisonMode.ABSOLUTE, false);
assertTrue(compareResults.get(0).isEqual());
assertTrue(compareResults.get(1).isEqual());
- assertThrows(RuntimeException.class, () -> snapshotUtil.comparePvs(null, -77));
+ assertThrows(RuntimeException.class, () -> snapshotUtil.comparePvs(null, -77, ComparisonMode.ABSOLUTE, false));
}
+
+ @Test
+ public void testComparePvsRelative(){
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://xx(42.0)");
+ ConfigPv configPv2 = new ConfigPv();
+ configPv2.setPvName("loc://yy(771.0)");
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(42.0, Alarm.none(), Time.now(), Display.none()));
+ SnapshotItem snapshotItem2 = new SnapshotItem();
+ snapshotItem2.setConfigPv(configPv2);
+ snapshotItem2.setValue(VDouble.of(771.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.0, ComparisonMode.RELATIVE, false);
+
+ assertTrue(compareResults.get(0).isEqual());
+ assertTrue(compareResults.get(1).isEqual());
+
+ snapshotItem1.setValue(VDouble.of(43.0, Alarm.none(), Time.now(), Display.none()));
+ snapshotItem2.setValue(VDouble.of(771.0, Alarm.none(), Time.now(), Display.none()));
+
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.0, ComparisonMode.RELATIVE, false);
+
+ assertFalse(compareResults.get(0).isEqual());
+ assertNotNull(compareResults.get(0).getStoredValue());
+ assertNotNull(compareResults.get(0).getLiveValue());
+ assertTrue(compareResults.get(1).isEqual());
+ assertNull(compareResults.get(1).getStoredValue());
+ assertNull(compareResults.get(1).getLiveValue());
+
+ snapshotItem1.setValue(VDouble.of(43.0, Alarm.none(), Time.now(), Display.none()));
+ snapshotItem2.setValue(VDouble.of(999.0, Alarm.none(), Time.now(), Display.none()));
+
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 0.1, ComparisonMode.RELATIVE, false);
+
+ assertTrue(compareResults.get(0).isEqual());
+ assertFalse(compareResults.get(1).isEqual());
+
+ assertThrows(RuntimeException.class, () -> snapshotUtil.comparePvs(null, -77, ComparisonMode.RELATIVE, false));
+
+ }
+
+ @Test
+ public void testCompareWithZeroStored(){
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://b(0.0)");
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(1.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 10.0, ComparisonMode.RELATIVE, false);
+ assertFalse(compareResults.get(0).isEqual());
+
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 2.0, ComparisonMode.ABSOLUTE, false);
+ assertTrue(compareResults.get(0).isEqual());
+
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 10.0, ComparisonMode.RELATIVE, false);
+ assertFalse(compareResults.get(0).isEqual());
+
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 0.5, ComparisonMode.ABSOLUTE, false);
+ assertFalse(compareResults.get(0).isEqual());
+
+ snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(0.0, Alarm.none(), Time.now(), Display.none()));
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 10.0, ComparisonMode.RELATIVE, false);
+
+ assertTrue(compareResults.get(0).isEqual());
+ }
+
+ @Test
+ public void testComparePvsWithReadbacks() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setReadbackPvName("loc://aa(50.0)");
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(42.0, Alarm.none(), Time.now(), Display.none()));
+ snapshotItem1.setReadbackValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 8.0, ComparisonMode.ABSOLUTE, false);
+ assertTrue(compareResults.get(0).isEqual());
+
+ compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 7.0, ComparisonMode.ABSOLUTE, false);
+ assertFalse(compareResults.get(0).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues1() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setComparison(new Comparison(ComparisonMode.RELATIVE, 0.3));
+
+ ConfigPv configPv2 = new ConfigPv();
+ configPv2.setPvName("loc://aa(42.0)");
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ SnapshotItem snapshotItem2 = new SnapshotItem();
+ snapshotItem2.setConfigPv(configPv2);
+ snapshotItem2.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2), 9.0, ComparisonMode.ABSOLUTE, false);
+ assertTrue(compareResults.get(0).isEqual());
+ assertTrue(compareResults.get(1).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues2() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setComparison(new Comparison(ComparisonMode.RELATIVE, 0.15));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 0.0, null, false);
+ assertFalse(compareResults.get(0).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues3() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setComparison(new Comparison(ComparisonMode.ABSOLUTE, 9.0));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 0.5, ComparisonMode.RELATIVE, false);
+ assertTrue(compareResults.get(0).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues4() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+
+ ConfigPv configPv11 = new ConfigPv();
+ configPv11.setPvName("loc://a(42.0)");
+ configPv11.setComparison(new Comparison(ComparisonMode.ABSOLUTE, 7.0));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1),
+ List.of(configPv11),
+ 0.5,
+ ComparisonMode.RELATIVE,
+ false);
+ assertFalse(compareResults.get(0).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues5() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setReadbackPvName("loc://aa(49.0)");
+
+ ConfigPv configPv11 = new ConfigPv();
+ configPv11.setPvName("loc://a(42.0)");
+ configPv11.setComparison(new Comparison(ComparisonMode.RELATIVE, 0.1));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1),
+ List.of(configPv11),
+ 0.0,
+ null,
+ false);
+ assertTrue(compareResults.get(0).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues6() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setReadbackPvName("loc://aa(49.0)");
+ configPv1.setComparison(new Comparison(ComparisonMode.RELATIVE, 0.1));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1), 0.0, null, true);
+ assertFalse(compareResults.get(0).isEqual());
+
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues7() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setReadbackPvName("loc://aa(49.0)");
+
+ ConfigPv configPv11 = new ConfigPv();
+ configPv11.setPvName("loc://a(42.0)");
+ configPv11.setComparison(new Comparison(ComparisonMode.ABSOLUTE, 1.1));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1),
+ List.of(configPv11),
+ 0.0,
+ null,
+ false);
+ assertTrue(compareResults.get(0).isEqual());
+ }
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues8() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setReadbackPvName("loc://aa(49.0)");
+
+ ConfigPv configPv2 = new ConfigPv();
+ configPv2.setPvName("loc://aa(42.0)");
+ configPv2.setReadbackPvName("loc://aaa(49.0)");
+
+ ConfigPv configPv11 = new ConfigPv();
+ configPv11.setPvName("loc://a(42.0)");
+ configPv11.setComparison(new Comparison(ComparisonMode.ABSOLUTE, 11.0));
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ SnapshotItem snapshotItem2 = new SnapshotItem();
+ snapshotItem2.setConfigPv(configPv2);
+ snapshotItem2.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1, snapshotItem2),
+ List.of(configPv11),
+ 0.0,
+ null,
+ false);
+ assertTrue(compareResults.get(0).isEqual());
+ assertFalse(compareResults.get(1).isEqual());
+ }
+
+
+
+ @Test
+ public void testComparePvsWithIndividualToleranceValues9() {
+
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("loc://a(42.0)");
+ configPv1.setReadbackPvName("loc://aa(49.0)");
+
+ ConfigPv configPv11 = new ConfigPv();
+ configPv11.setPvName("loc://a(42.0)");
+ configPv11.setComparison(new Comparison(ComparisonMode.ABSOLUTE, 1.1));
+
+ ConfigPv configPv22 = new ConfigPv();
+ configPv22.setPvName("loc://aaa(42.0)");
+
+ SnapshotItem snapshotItem1 = new SnapshotItem();
+ snapshotItem1.setConfigPv(configPv1);
+ snapshotItem1.setValue(VDouble.of(50.0, Alarm.none(), Time.now(), Display.none()));
+
+ List compareResults = snapshotUtil.comparePvs(List.of(snapshotItem1),
+ List.of(configPv11, configPv22),
+ 0.0,
+ null,
+ false);
+ assertTrue(compareResults.get(0).isEqual());
+ assertFalse(compareResults.get(1).isEqual());
+ }
}
diff --git a/services/save-and-restore/doc/index.rst b/services/save-and-restore/doc/index.rst
index 7e3990224a..a9cef42746 100644
--- a/services/save-and-restore/doc/index.rst
+++ b/services/save-and-restore/doc/index.rst
@@ -265,6 +265,8 @@ The a list of all the children nodes of the node with id `{uniqueNodeId}`
}
]
+.. _Get a configuration:
+
Get a configuration
"""""""""""""""""""
@@ -295,7 +297,11 @@ Return: object describing the configuration data, essentially a list of PVs.
"pvName": "13SIM1:{SimDetector-Cam:1}cam1:BinX"
},
{
- "pvName": "13SIM1:{SimDetector-Cam:1}cam1:BinY"
+ "pvName": "13SIM1:{SimDetector-Cam:1}cam1:BinY",
+ "comparison":{
+ "comparisonMode":"ABSOLUTE",
+ "tolerance":2.7
+ }
},
{
"pvName": "13SIM1:{SimDetector-Cam:2}cam2:BinX",
@@ -313,6 +319,8 @@ Return: object describing the configuration data, essentially a list of PVs.
Here the ``uniqueId`` field matches the ``unqiueId`` field of the configuration node.
+The ``comparison`` field is optional and can be set individually on each element in the list.
+
Create a configuration
""""""""""""""""""""""
@@ -346,7 +354,11 @@ Body:
{
"pvName": "13SIM1:{SimDetector-Cam:2}cam2:BinX",
"readbackPvName": null,
- "readOnly": false
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode":"ABSOLUTE",
+ "tolerance": 2.7
+ }
},
{
"pvName": "13SIM1:{SimDetector-Cam:2}cam2:BinY",
@@ -361,6 +373,9 @@ Body:
The request parameter ``parentNodeId`` is mandatory and must identify an existing folder node. The client
needs to specify a name for the new configuration node, as well as a user identity.
+The ``comparison`` field is optional and can be set individually on each element in the list. If specified,
+the ``comparisonMode`` must be either "ABSOLUTE" or "RELATIVE", and the ``tolerance`` must be >=0.
+
Update a configuration
""""""""""""""""""""""
@@ -380,6 +395,8 @@ body will be removed.
Snapshot Endpoints
------------------
+.. _Get a snapshot:
+
Get a snapshot
""""""""""""""
@@ -640,7 +657,7 @@ body will be removed.
Get restorable items of a composite snapshot
""""""""""""""""""""""""""""""""""""""""""""
-***.../composite-snapshot/{uniqueId}/items**
+**.../composite-snapshot/{uniqueId}/items**
Method: GET
@@ -759,6 +776,54 @@ Body:
}
]
+Server Take Snapshot Endpoints
+------------------------------
+
+**.../take-snapshot/{configNodeId}**
+
+Method: GET
+
+This will read PV values for all items listed in the configuration identified by ``configNodeId``. Upon successful
+completion, the response will hold an array of objects where each element is on the form:
+
+.. code-block:: JSON
+
+ {
+ "configPv": {
+ "pvName": "RFQ-010:RFS-EVR-101:RFSyncWdt-SP",
+ "readbackPvName": null,
+ "readOnly": false
+ },
+ "value": {
+ "type": {
+ "name": "VDouble",
+ "version": 1
+ },
+ "value": 100.0,
+ "alarm": {
+ "severity": "NONE",
+ "status": "NONE",
+ "name": "NONE"
+ },
+ "time": {
+ "unixSec": 1639063122,
+ "nanoSec": 320431469
+ },
+ "display": {
+ "units": ""
+ }
+ }
+
+**.../take-snapshot/{configNodeId}**
+
+Method: PUT
+
+This will read PV values for all items listed in the configuration identified by ``configNodeId``. Upon successful
+completion the data is persisted into the database. ``name`` and ``comment`` are optional query parameters and will
+default to the current date/time on the format ``yyyy-MM-dd HH:mm:ss.SSS``.
+
+The response is a snapshot object representing the persisted data, see :ref:`Get a snapshot`
+
Server Restore Endpoints
------------------------
@@ -858,21 +923,40 @@ from an existing node rather than providing them explicitly. It returns the same
Compare Endpoint
----------------
-**.../compare/{uniqueId}[?tolerance=]**
+**.../compare/{uniqueId}[?tolerance=&compareMode=&skipReadback=]**
Method: GET
-The path variable ``{uniqueId}`` must identify an existing snapshot or composite snapshot. The ``tolerance`` query parameter is
-optional and defaults to zero. If specified it must be >= 0.
+The path variable ``{uniqueId}`` must identify an existing snapshot or composite snapshot.
+
+The ``tolerance`` query parameter is optional and defaults to zero. If specified it must be >= 0. Non-numeric values
+will trigger a HTTP 400 response.
-This endpoint can be used to compare stored snapshot values to live values for each set-point PV in the snapshot.
-Comparisons are performed in the same manner as in the client UI, i.e.:
+The ``compareMode`` query parameter is optional and defaults to ``ABSOLUTE``. This is case sensitive, values other
+than ``ABSOLUTE`` or ``RELATIVE`` will trigger a HTTP 400 response.
-* Scalar PVs are compared using the the optional relative tolerance, or compared using zero tolerance.
+The ``skipReadback`` query parameter is optional and defaults to ``false``. This is case insensitive, values
+that cannot be evaluated as boolean will trigger a HTTP 400 response.
+
+This endpoint can be used to compare stored snapshot values to live values for each set-point PV in the snapshot. The
+reference value for the comparison is always the one corresponding to the ``PV Name`` column in the configuration.
+
+Comparisons are performed like so:
+
+* Scalar PVs are compared using the tolerance and compare mode (absolute or relative), or compared using zero tolerance.
* Array PVs are compared element wise, always using zero tolerance. Arrays must be of equal length.
* Table PVs are compared element wise, always using zero tolerance. Tables must be of same dimensions, and data types must match between columns.
* Enum PVs are compared using zero tolerance.
+The ``compareMode`` and ``tolerance`` are applied to all comparison operations, but can be overridden on each individual
+item in a configuration, see :ref:`Get a configuration`.
+
+Equality between a stored value and the live value is determined on each PV like so:
+
+* If the configuration of a PV does not specify a comparison mode and tolerance, the ``comparisonMode`` and ``tolerance`` request parameters are used. These however are optional and default to ABSOLUTE and zero respectively.
+* The base (reference) value is always the value stored in the value field of a snapshot item object. It corresponds to the ``pvName``` field, i.e. never the ``readbackPvName`` of a configuration item.
+* The live value used in the comparison is either the value corresponding to ``pvName``, or ``readbackPvName`` if specified. The latter can be overridden with the ``skipReadback`` request parameter.
+
Return value: a list of comparison results, one for each PV in the snapshot, e.g.:
.. code-block:: JSON
diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonController.java
index 7f5c9c6389..24edbbba3a 100644
--- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonController.java
+++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonController.java
@@ -17,9 +17,11 @@
*/
package org.phoebus.service.saveandrestore.web.controllers;
-import org.phoebus.applications.saveandrestore.model.CompareResult;
+import org.phoebus.applications.saveandrestore.model.ComparisonResult;
import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData;
+import org.phoebus.applications.saveandrestore.model.ConfigPv;
import org.phoebus.applications.saveandrestore.model.Node;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
import org.phoebus.applications.saveandrestore.model.SnapshotItem;
import org.phoebus.saveandrestore.util.SnapshotUtil;
import org.phoebus.service.saveandrestore.NodeNotFoundException;
@@ -54,12 +56,15 @@ public class ComparisonController extends BaseController {
/**
*
* @param nodeId The unique node id of a snapshot or composite snapshot.
- * @return A list of {@link CompareResult}s, one for each PV item in the snapshot/composite snapshot. The
- * {@link CompareResult#getLiveValue()} and {@link CompareResult#getStoredValue()} will return null if
+ * @return A list of {@link ComparisonResult}s, one for each PV item in the snapshot/composite snapshot. The
+ * {@link ComparisonResult#getLiveValue()} and {@link ComparisonResult#getStoredValue()} will return null if
* comparison evaluates to "equal" for a PV.
*/
@GetMapping(value = "/{nodeId}", produces = JSON)
- public List compare(@PathVariable String nodeId, @RequestParam(value = "tolerance", required = false, defaultValue = "0") double tolerance) {
+ public List compare(@PathVariable String nodeId,
+ @RequestParam(value = "tolerance", required = false, defaultValue = "0") double tolerance,
+ @RequestParam(value = "compareMode", required = false, defaultValue = "ABSOLUTE") ComparisonMode compareMode,
+ @RequestParam(value = "skipReadback", required = false, defaultValue = "false") boolean skipReadback) {
if(tolerance < 0){
throw new IllegalArgumentException("Tolerance must be >=0");
}
@@ -67,26 +72,51 @@ public List compare(@PathVariable String nodeId, @RequestParam(va
if (node == null) {
throw new NodeNotFoundException("Node " + nodeId + " does not exist");
}
+ List configPvs = new ArrayList<>();
+ List snapshotItems = new ArrayList<>();
switch (node.getNodeType()) {
- case SNAPSHOT:
- return snapshotUtil.comparePvs(getSnapshotItems(node.getUniqueId()), tolerance);
- case COMPOSITE_SNAPSHOT:
- return snapshotUtil.comparePvs(getCompositeSnapshotItems(node.getUniqueId()), tolerance);
+ case SNAPSHOT: {
+ getSnapshotItemsAndConfig(node.getUniqueId(), snapshotItems, configPvs);
+ return snapshotUtil.comparePvs(snapshotItems, configPvs, tolerance, compareMode, skipReadback);
+ }
+ case COMPOSITE_SNAPSHOT: {
+ getCompositeSnapshotItemsAndConfig(node.getUniqueId(), snapshotItems, configPvs);
+ return snapshotUtil.comparePvs(snapshotItems, configPvs, tolerance, compareMode, skipReadback);
+ }
default:
throw new IllegalArgumentException("Node type" + node.getNodeType() + " cannot be compared");
}
}
- private List getSnapshotItems(String nodeId) {
- return nodeDAO.getSnapshotData(nodeId).getSnapshotItems();
+ /**
+ * Collects {@link SnapshotItem}s from the snapshot and {@link ConfigPv}s from the associated configuration.
+ * @param snapshotNodeId The snapshot's unique id.
+ * @param snapshotItems {@link List} into which {@link SnapshotItem}s are added.
+ * @param configPvs {@link List} into which {@link ConfigPv}s are added.
+ */
+ private void getSnapshotItemsAndConfig(String snapshotNodeId, List snapshotItems, List configPvs){
+ snapshotItems.addAll(nodeDAO.getSnapshotData(snapshotNodeId).getSnapshotItems());
+ Node configNode = nodeDAO.getParentNode(snapshotNodeId);
+ configPvs.addAll(nodeDAO.getConfigurationData(configNode.getUniqueId()).getPvList());
}
- private List getCompositeSnapshotItems(String nodeId) {
- CompositeSnapshotData compositeSnapshotData = nodeDAO.getCompositeSnapshotData(nodeId);
+ /**
+ * Collects {@link SnapshotItem}s from the composite snapshot and {@link ConfigPv}s from the associated configuration.
+ * Recursive calls are done when composite snapshots contains composite snapshots.
+ * @param compositeSnapshotNodeId The composite snapshot's unique id.
+ * @param snapshotItems {@link List} into which {@link SnapshotItem}s are added.
+ * @param configPvs {@link List} into which {@link ConfigPv}s are added.
+ */
+ protected void getCompositeSnapshotItemsAndConfig(String compositeSnapshotNodeId, List snapshotItems, List configPvs){
+ CompositeSnapshotData compositeSnapshotData = nodeDAO.getCompositeSnapshotData(compositeSnapshotNodeId);
List referencedSnapshots = compositeSnapshotData.getReferencedSnapshotNodes();
- List snapshotItems = new ArrayList<>();
- referencedSnapshots.forEach(id -> snapshotItems.addAll(getSnapshotItems(id)));
- return snapshotItems;
+ referencedSnapshots.forEach(id -> {
+ Node node = nodeDAO.getNode(id);
+ switch (node.getNodeType()){
+ case SNAPSHOT -> getSnapshotItemsAndConfig(id, snapshotItems, configPvs);
+ case COMPOSITE_SNAPSHOT -> getCompositeSnapshotItemsAndConfig(id, snapshotItems, configPvs);
+ }
+ });
}
}
diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java
index 4e832bfcc3..c769e99fbd 100644
--- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java
+++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java
@@ -17,6 +17,7 @@
*/
package org.phoebus.service.saveandrestore.web.controllers;
+import org.phoebus.applications.saveandrestore.model.ConfigPv;
import org.phoebus.applications.saveandrestore.model.Configuration;
import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
@@ -58,6 +59,17 @@ public class ConfigurationController extends BaseController {
public Configuration createConfiguration(@RequestParam(value = "parentNodeId") String parentNodeId,
@RequestBody Configuration configuration,
Principal principal) {
+ for(ConfigPv configPv : configuration.getConfigurationData().getPvList()){
+ // Compare mode is set, verify tolerance is non-null
+ if(configPv.getComparison() != null && (configPv.getComparison().getComparisonMode() == null || configPv.getComparison().getTolerance() == null)){
+ throw new IllegalArgumentException("PV item \"" + configPv.getPvName() + "\" specifies comparison but no comparison or tolerance value");
+ }
+ // Tolerance is set...
+ if(configPv.getComparison() != null && configPv.getComparison().getTolerance() < 0){
+ //Tolerance is less than zero, which does not make sense as comparison considers tolerance as upper and lower limit.
+ throw new IllegalArgumentException("PV item \"" + configPv.getPvName() + "\" specifies zero tolerance");
+ }
+ }
configuration.getConfigurationNode().setUserName(principal.getName());
return nodeDAO.createConfiguration(parentNodeId, configuration);
}
diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java
index 897dc5dd2f..5afdf55862 100644
--- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java
+++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java
@@ -12,9 +12,10 @@
import org.epics.vtype.VDouble;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.phoebus.applications.saveandrestore.model.CompareResult;
+import org.phoebus.applications.saveandrestore.model.ComparisonResult;
import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData;
import org.phoebus.applications.saveandrestore.model.ConfigPv;
+import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
import org.phoebus.applications.saveandrestore.model.SnapshotData;
@@ -91,7 +92,16 @@ public void testSingleSnapshot() throws Exception {
ConfigPv configPv2 = new ConfigPv();
configPv2.setPvName("loc://y(771.0)");
+ when(nodeDAO.getParentNode("nodeId")).thenReturn(Node.builder().nodeType(NodeType.CONFIGURATION).uniqueId("configId").build());
+
+ ConfigurationData configurationData = new ConfigurationData();
+ configurationData.setUniqueId("configId");
+ configurationData.setPvList(List.of(configPv1, configPv2));
+
+ when(nodeDAO.getConfigurationData("configId")).thenReturn(configurationData);
+
SnapshotData snapshotData = new SnapshotData();
+ //snapshotData.setUniqueId("uniqueId");
SnapshotItem snapshotItem1 = new SnapshotItem();
snapshotItem1.setConfigPv(configPv1);
snapshotItem1.setValue(VDouble.of(42.0, Alarm.none(),
@@ -104,11 +114,11 @@ public void testSingleSnapshot() throws Exception {
when(nodeDAO.getSnapshotData("nodeId")).thenReturn(snapshotData);
- MockHttpServletRequestBuilder request = get("/compare/nodeId");
+ MockHttpServletRequestBuilder request = get("/compare/nodeId?skipReadback=TRUE");
MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON))
.andReturn();
- List compareResults =
+ List compareResults =
objectMapper.readValue(result.getResponse().getContentAsString(),
new TypeReference<>() {
});
@@ -148,11 +158,26 @@ public void testCompositeSnapshot() throws Exception{
when(nodeDAO.getSnapshotData("id1")).thenReturn(snapshotData1);
when(nodeDAO.getSnapshotData("id2")).thenReturn(snapshotData2);
+ when(nodeDAO.getNode("id1")).thenReturn(Node.builder().name("id1").uniqueId("id1").nodeType(NodeType.SNAPSHOT).build());
+ when(nodeDAO.getNode("id2")).thenReturn(Node.builder().name("id2").uniqueId("id2").nodeType(NodeType.SNAPSHOT).build());
+ when(nodeDAO.getParentNode("id1")).thenReturn(Node.builder().nodeType(NodeType.CONFIGURATION).name("id1parent").uniqueId("id1parent").build());
+ when(nodeDAO.getParentNode("id2")).thenReturn(Node.builder().nodeType(NodeType.CONFIGURATION).name("id2parent").uniqueId("id2parent").build());
+
+ ConfigurationData configurationData1 = new ConfigurationData();
+ configurationData1.setPvList(List.of(configPv1, configPv2));
+
+ when(nodeDAO.getConfigurationData("id1parent")).thenReturn(configurationData1);
+
+ ConfigurationData configurationData2 = new ConfigurationData();
+ configurationData2.setPvList(List.of(configPv1, configPv2));
+
+ when(nodeDAO.getConfigurationData("id2parent")).thenReturn(configurationData2);
+
MockHttpServletRequestBuilder request = get("/compare/nodeId");
MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON))
.andReturn();
- List compareResults =
+ List compareResults =
objectMapper.readValue(result.getResponse().getContentAsString(),
new TypeReference<>() {
});
diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTestIT.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTestIT.java
new file mode 100644
index 0000000000..324dfba344
--- /dev/null
+++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTestIT.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2025 European Spallation Source ERIC.
+ */
+
+package org.phoebus.service.saveandrestore.web.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.phoebus.applications.saveandrestore.model.CompositeSnapshot;
+import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData;
+import org.phoebus.applications.saveandrestore.model.ConfigPv;
+import org.phoebus.applications.saveandrestore.model.Configuration;
+import org.phoebus.applications.saveandrestore.model.Node;
+import org.phoebus.applications.saveandrestore.model.NodeType;
+import org.phoebus.applications.saveandrestore.model.Snapshot;
+import org.phoebus.applications.saveandrestore.model.SnapshotItem;
+import org.phoebus.service.saveandrestore.persistence.config.ElasticConfig;
+import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Profile;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@SpringBootTest
+@ContextConfiguration(classes = ElasticConfig.class)
+@TestPropertySource(locations = "classpath:test_application.properties")
+@Profile("IT")
+public class ComparisonControllerTestIT {
+
+ @Autowired
+ private ComparisonController comparisonController;
+
+ @Autowired
+ private NodeDAO nodeDAO;
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Test
+ public void testGetSnapshotItemsAndConfig() throws Exception{
+ Node topLevelFolder = Node.builder().nodeType(NodeType.FOLDER).name(UUID.randomUUID().toString())
+ .build();
+ topLevelFolder = nodeDAO.createNode(Node.ROOT_FOLDER_UNIQUE_ID, topLevelFolder);
+
+ CompositeSnapshot compositeSnapshot1 = null;
+ CompositeSnapshot compositeSnapshot2 = null;
+
+ try {
+
+ Configuration configuration1 = objectMapper.readValue(getClass().getResourceAsStream("/IT-test1-config.json"), Configuration.class);
+ configuration1 = nodeDAO.createConfiguration(topLevelFolder.getUniqueId(), configuration1);
+
+ Snapshot snapshot1 = objectMapper.readValue(getClass().getResourceAsStream("/IT-test1-snapshot.json"), Snapshot.class);
+ snapshot1 = nodeDAO.createSnapshot(configuration1.getConfigurationNode().getUniqueId(), snapshot1);
+
+ Configuration configuration2 = objectMapper.readValue(getClass().getResourceAsStream("/IT-test2-config.json"), Configuration.class);
+ configuration2 = nodeDAO.createConfiguration(topLevelFolder.getUniqueId(), configuration2);
+
+ Snapshot snapshot2 = objectMapper.readValue(getClass().getResourceAsStream("/IT-test2-snapshot.json"), Snapshot.class);
+ snapshot2 = nodeDAO.createSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2);
+
+ Configuration configuration3 = objectMapper.readValue(getClass().getResourceAsStream("/IT-test3-config.json"), Configuration.class);
+ configuration3 = nodeDAO.createConfiguration(topLevelFolder.getUniqueId(), configuration3);
+
+ Snapshot snapshot3 = objectMapper.readValue(getClass().getResourceAsStream("/IT-test3-snapshot.json"), Snapshot.class);
+ snapshot3 = nodeDAO.createSnapshot(configuration3.getConfigurationNode().getUniqueId(), snapshot3);
+
+ Node compositeSnapshotSimple = Node.builder().name("CompositeSimple").nodeType(NodeType.COMPOSITE_SNAPSHOT).build();
+ CompositeSnapshotData compositeSnapshotData1 = new CompositeSnapshotData();
+ compositeSnapshotData1.setReferencedSnapshotNodes(List.of(snapshot1.getSnapshotNode().getUniqueId(), snapshot2.getSnapshotNode().getUniqueId()));
+ compositeSnapshot1 = new CompositeSnapshot();
+ compositeSnapshot1.setCompositeSnapshotNode(compositeSnapshotSimple);
+ compositeSnapshot1.setCompositeSnapshotData(compositeSnapshotData1);
+
+ compositeSnapshot1 = nodeDAO.createCompositeSnapshot(topLevelFolder.getUniqueId(), compositeSnapshot1);
+
+ Node compositeSnapshotComposite = Node.builder().name("CompositeComposite").nodeType(NodeType.COMPOSITE_SNAPSHOT).build();
+ CompositeSnapshotData compositeSnapshotData2 = new CompositeSnapshotData();
+ compositeSnapshotData2.setReferencedSnapshotNodes(List.of(compositeSnapshot1.getCompositeSnapshotNode().getUniqueId(), snapshot3.getSnapshotNode().getUniqueId()));
+ compositeSnapshot2 = new CompositeSnapshot();
+ compositeSnapshot2.setCompositeSnapshotNode(compositeSnapshotComposite);
+ compositeSnapshot2.setCompositeSnapshotData(compositeSnapshotData2);
+
+ compositeSnapshot2 = nodeDAO.createCompositeSnapshot(topLevelFolder.getUniqueId(), compositeSnapshot2);
+
+ List snapshotItems = new ArrayList<>();
+ List configPvs = new ArrayList<>();
+
+ comparisonController.getCompositeSnapshotItemsAndConfig(compositeSnapshot1.getCompositeSnapshotNode().getUniqueId(), snapshotItems, configPvs);
+
+ assertEquals(snapshotItems.size(), configPvs.size());
+
+ snapshotItems.clear();
+ configPvs.clear();
+
+ comparisonController.getCompositeSnapshotItemsAndConfig(compositeSnapshot2.getCompositeSnapshotNode().getUniqueId(), snapshotItems, configPvs);
+
+ assertEquals(snapshotItems.size(), configPvs.size());
+
+ } finally {
+ if(compositeSnapshot1 != null && compositeSnapshot1.getCompositeSnapshotNode() != null && compositeSnapshot1.getCompositeSnapshotNode().getUniqueId() != null){
+ nodeDAO.deleteNode(compositeSnapshot1.getCompositeSnapshotNode().getUniqueId());
+ }
+ if(compositeSnapshot2 != null && compositeSnapshot2.getCompositeSnapshotNode() != null && compositeSnapshot2.getCompositeSnapshotNode().getUniqueId() != null){
+ nodeDAO.deleteNode(compositeSnapshot2.getCompositeSnapshotNode().getUniqueId());
+ }
+ nodeDAO.deleteNode(topLevelFolder.getUniqueId());
+ }
+ }
+}
diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java
index 502df6b8f3..d84aebc598 100644
--- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java
+++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java
@@ -23,6 +23,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.phoebus.applications.saveandrestore.model.Configuration;
+import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO;
@@ -36,6 +37,8 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import java.util.Collections;
+
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON;
@@ -74,6 +77,9 @@ public void testCreateConfiguration() throws Exception {
Configuration configuration = new Configuration();
configuration.setConfigurationNode(Node.builder().build());
+ ConfigurationData configurationData = new ConfigurationData();
+ configurationData.setPvList(Collections.emptyList());
+ configuration.setConfigurationData(configurationData);
MockHttpServletRequestBuilder request = put("/config?parentNodeId=a")
.header(HttpHeaders.AUTHORIZATION, userAuthorization)
.contentType(JSON).content(objectMapper.writeValueAsString(configuration));
diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java
index 4ab5544360..bf355ff07f 100644
--- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java
+++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java
@@ -22,9 +22,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.phoebus.applications.saveandrestore.model.Comparison;
+import org.phoebus.applications.saveandrestore.model.ConfigPv;
import org.phoebus.applications.saveandrestore.model.Configuration;
+import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO;
import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +40,8 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import java.util.List;
+
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON;
@@ -77,6 +83,10 @@ public void testCreateConfiguration() throws Exception {
Configuration configuration = new Configuration();
configuration.setConfigurationNode(Node.builder().build());
+ ConfigurationData configurationData = new ConfigurationData();
+ configurationData.setPvList(List.of(ConfigPv.builder().pvName("foo").readbackPvName("bar").build()));
+
+ configuration.setConfigurationData(configurationData);
MockHttpServletRequestBuilder request = put("/config?parentNodeId=a")
.header(HttpHeaders.AUTHORIZATION, adminAuthorization)
.contentType(JSON).content(objectMapper.writeValueAsString(configuration));
@@ -155,6 +165,46 @@ public void testUpdateConfiguration() throws Exception {
mockMvc.perform(request).andExpect(status().isUnauthorized()
);
+ }
+
+ @Test
+ public void testCreateInvalidConfiguration() throws Exception {
+
+ reset(nodeDAO);
+
+ Configuration configuration = new Configuration();
+ configuration.setConfigurationNode(Node.builder().build());
+ ConfigurationData configurationData = new ConfigurationData();
+ configuration.setConfigurationData(configurationData);
+ configurationData.setPvList(List.of(ConfigPv.builder().pvName("foo").build(),
+ ConfigPv.builder().pvName("fooo").comparison(new Comparison(null, 0.1)).build()));
+ MockHttpServletRequestBuilder request = put("/config?parentNodeId=a")
+ .header(HttpHeaders.AUTHORIZATION, adminAuthorization)
+ .contentType(JSON).content(objectMapper.writeValueAsString(configuration));
+
+ mockMvc.perform(request).andExpect(status().isBadRequest());
+
+ configurationData.setPvList(List.of(
+ ConfigPv.builder().pvName("fooo").readbackPvName("bar").comparison(new Comparison(null, 0.1)).build()));
+
+ configuration.setConfigurationData(configurationData);
+
+ request = put("/config?parentNodeId=a")
+ .header(HttpHeaders.AUTHORIZATION, adminAuthorization)
+ .contentType(JSON).content(objectMapper.writeValueAsString(configuration));
+
+ mockMvc.perform(request).andExpect(status().isBadRequest());
+
+ configurationData.setPvList(List.of(
+ ConfigPv.builder().pvName("fooo").readbackPvName("bar").comparison(new Comparison(ComparisonMode.RELATIVE, -0.1))
+ .build()));
+
+ configuration.setConfigurationData(configurationData);
+
+ request = put("/config?parentNodeId=a")
+ .header(HttpHeaders.AUTHORIZATION, adminAuthorization)
+ .contentType(JSON).content(objectMapper.writeValueAsString(configuration));
+ mockMvc.perform(request).andExpect(status().isBadRequest());
}
}
diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java
index 60bc8075cc..37fc550b12 100644
--- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java
+++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java
@@ -25,7 +25,11 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
+import org.phoebus.applications.saveandrestore.model.Comparison;
+import org.phoebus.applications.saveandrestore.model.ComparisonMode;
+import org.phoebus.applications.saveandrestore.model.ConfigPv;
import org.phoebus.applications.saveandrestore.model.Configuration;
+import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
import org.phoebus.applications.saveandrestore.model.Tag;
@@ -164,6 +168,9 @@ public void testCreateConfig() throws Exception {
Configuration configuration = new Configuration();
configuration.setConfigurationNode(config);
+ ConfigurationData configurationData = new ConfigurationData();
+ configurationData.setPvList(Collections.emptyList());
+ configuration.setConfigurationData(configurationData);
when(nodeDAO.createConfiguration(Mockito.any(String.class), Mockito.any(Configuration.class))).thenAnswer((Answer) invocation -> configuration);
@@ -179,6 +186,91 @@ public void testCreateConfig() throws Exception {
objectMapper.readValue(result.getResponse().getContentAsString(), Configuration.class);
}
+ @Test
+ public void testCreateConfigWithToleranceData() throws Exception {
+
+ reset(nodeDAO);
+
+ Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh")
+ .userName("user").build();
+
+ Configuration configuration = new Configuration();
+ configuration.setConfigurationNode(config);
+ ConfigurationData configurationData = new ConfigurationData();
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("name");
+ configPv1.setComparison(new Comparison(ComparisonMode.ABSOLUTE, 1.0));
+ configurationData.setPvList(List.of(configPv1));
+ configuration.setConfigurationData(configurationData);
+
+ when(nodeDAO.createConfiguration(Mockito.any(String.class), Mockito.any(Configuration.class))).thenAnswer((Answer) invocation -> configuration);
+
+ MockHttpServletRequestBuilder request = put("/config?parentNodeId=p")
+ .header(HttpHeaders.AUTHORIZATION, userAuthorization)
+ .contentType(JSON)
+ .content(objectMapper.writeValueAsString(configuration));
+
+ MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON))
+ .andReturn();
+
+ // Make sure response contains expected data
+ objectMapper.readValue(result.getResponse().getContentAsString(), Configuration.class);
+ }
+
+ @Test
+ public void testCreateConfigWithBadToleranceData1() throws Exception {
+
+ reset(nodeDAO);
+
+ Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh")
+ .userName("user").build();
+
+ Configuration configuration = new Configuration();
+ configuration.setConfigurationNode(config);
+ ConfigurationData configurationData = new ConfigurationData();
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("name");
+ configPv1.setComparison(new Comparison(ComparisonMode.ABSOLUTE, null));
+ configurationData.setPvList(List.of(configPv1));
+ configuration.setConfigurationData(configurationData);
+
+ when(nodeDAO.createConfiguration(Mockito.any(String.class), Mockito.any(Configuration.class))).thenAnswer((Answer) invocation -> configuration);
+
+ MockHttpServletRequestBuilder request = put("/config?parentNodeId=p")
+ .header(HttpHeaders.AUTHORIZATION, userAuthorization)
+ .contentType(JSON)
+ .content(objectMapper.writeValueAsString(configuration));
+
+ mockMvc.perform(request).andExpect(status().isBadRequest());
+ }
+
+ @Test
+ public void testCreateConfigWithBadToleranceData2() throws Exception {
+
+ reset(nodeDAO);
+
+ Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh")
+ .userName("user").build();
+
+ Configuration configuration = new Configuration();
+ configuration.setConfigurationNode(config);
+ ConfigurationData configurationData = new ConfigurationData();
+ ConfigPv configPv1 = new ConfigPv();
+ configPv1.setPvName("name");
+ configPv1.setComparison(new Comparison(null, 0.1));
+ configurationData.setPvList(List.of(configPv1));
+ configuration.setConfigurationData(configurationData);
+
+ when(nodeDAO.createConfiguration(Mockito.any(String.class), Mockito.any(Configuration.class))).thenAnswer((Answer) invocation -> configuration);
+
+ MockHttpServletRequestBuilder request = put("/config?parentNodeId=p")
+ .header(HttpHeaders.AUTHORIZATION, userAuthorization)
+ .contentType(JSON)
+ .content(objectMapper.writeValueAsString(configuration));
+
+ mockMvc.perform(request).andExpect(status().isBadRequest());
+ }
+
@Test
public void testUpdateConfig() throws Exception {
reset(nodeDAO);
diff --git a/services/save-and-restore/src/test/resources/IT-test1-config.json b/services/save-and-restore/src/test/resources/IT-test1-config.json
new file mode 100644
index 0000000000..d80c0dbe91
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/IT-test1-config.json
@@ -0,0 +1,23 @@
+{
+ "configurationNode": {
+ "name": "IT-test1",
+ "description": "IT-test1",
+ "nodeType": "CONFIGURATION"
+ },
+ "configurationData": {
+ "pvList": [
+ {
+ "pvName": "ao1",
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode": "ABSOLUTE",
+ "tolerance": 1
+ }
+ },
+ {
+ "pvName": "ao2",
+ "readOnly": false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/save-and-restore/src/test/resources/IT-test1-snapshot.json b/services/save-and-restore/src/test/resources/IT-test1-snapshot.json
new file mode 100644
index 0000000000..179bde6bd4
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/IT-test1-snapshot.json
@@ -0,0 +1,76 @@
+{
+ "snapshotNode": {
+ "name" : "IT-test1-snapshot",
+ "description": "IT-test1-snapshot",
+ "nodeType": "SNAPSHOT"
+ },
+ "snapshotData": {
+ "snapshotItems": [
+ {
+ "configPv": {
+ "pvName": "ao1",
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode": "ABSOLUTE",
+ "tolerance": 1
+ }
+ },
+ "value": {
+ "type": {
+ "name": "VDouble",
+ "version": 1
+ },
+ "value": 1,
+ "alarm": {
+ "severity": "NONE",
+ "status": "NONE",
+ "name": "UDF_ALARM"
+ },
+ "time": {
+ "unixSec": 631152000,
+ "nanoSec": 0,
+ "userTag": 0
+ },
+ "display": {
+ "lowAlarm": -30,
+ "highAlarm": 30,
+ "lowDisplay": -50,
+ "highDisplay": 50,
+ "lowWarning": -20,
+ "highWarning": 2,
+ "units": "kW"
+ }
+ }
+ },
+ {
+ "configPv": {
+ "pvName": "ao2",
+ "readOnly": false
+ },
+ "value": {
+ "type": {
+ "name": "VDouble",
+ "version": 1
+ },
+ "value": 0,
+ "alarm": {
+ "severity": "NONE",
+ "status": "NONE",
+ "name": "UDF_ALARM"
+ },
+ "time": {
+ "unixSec": 631152000,
+ "nanoSec": 0,
+ "userTag": 0
+ },
+ "display": {
+ "highAlarm": 1,
+ "lowDisplay": 0,
+ "highDisplay": 0,
+ "units": ""
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/save-and-restore/src/test/resources/IT-test2-config.json b/services/save-and-restore/src/test/resources/IT-test2-config.json
new file mode 100644
index 0000000000..6561b824fe
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/IT-test2-config.json
@@ -0,0 +1,19 @@
+{
+ "configurationNode": {
+ "name": "IT-test2",
+ "description": "IT-test2",
+ "nodeType": "CONFIGURATION"
+ },
+ "configurationData": {
+ "pvList": [
+ {
+ "pvName": "ao3",
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode": "RELATIVE",
+ "tolerance": 0.3
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/save-and-restore/src/test/resources/IT-test2-snapshot.json b/services/save-and-restore/src/test/resources/IT-test2-snapshot.json
new file mode 100644
index 0000000000..ee7875114f
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/IT-test2-snapshot.json
@@ -0,0 +1,47 @@
+{
+ "snapshotNode": {
+ "name" : "IT-test2-snapshot",
+ "description": "IT-test2-snapshot",
+ "nodeType": "SNAPSHOT"
+ },
+ "snapshotData": {
+ "snapshotItems": [
+ {
+ "configPv": {
+ "pvName": "ao3",
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode": "RELATIVE",
+ "tolerance": 0.3
+ }
+ },
+ "value": {
+ "type": {
+ "name": "VDouble",
+ "version": 1
+ },
+ "value": 1,
+ "alarm": {
+ "severity": "NONE",
+ "status": "NONE",
+ "name": "UDF_ALARM"
+ },
+ "time": {
+ "unixSec": 631152000,
+ "nanoSec": 0,
+ "userTag": 0
+ },
+ "display": {
+ "lowAlarm": -5,
+ "highAlarm": 20,
+ "lowDisplay": 0,
+ "highDisplay": 0,
+ "lowWarning": 0,
+ "highWarning": 10,
+ "units": ""
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/save-and-restore/src/test/resources/IT-test3-config.json b/services/save-and-restore/src/test/resources/IT-test3-config.json
new file mode 100644
index 0000000000..74f7338d4f
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/IT-test3-config.json
@@ -0,0 +1,23 @@
+{
+ "configurationNode": {
+ "name": "IT-test3",
+ "description": "IT-test3",
+ "nodeType": "CONFIGURATION"
+ },
+ "configurationData": {
+ "pvList": [
+ {
+ "pvName": "ao7",
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode": "ABSOLUTE",
+ "tolerance": 7
+ }
+ },
+ {
+ "pvName": "COUNTER10",
+ "readOnly": false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/save-and-restore/src/test/resources/IT-test3-snapshot.json b/services/save-and-restore/src/test/resources/IT-test3-snapshot.json
new file mode 100644
index 0000000000..ed59ecc77f
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/IT-test3-snapshot.json
@@ -0,0 +1,74 @@
+{
+ "snapshotNode": {
+ "name" : "IT-test3-snapshot",
+ "description": "IT-test3-snapshot",
+ "nodeType": "SNAPSHOT"
+ },
+ "snapshotData": {
+ "snapshotItems": [
+ {
+ "configPv": {
+ "pvName": "ao7",
+ "readOnly": false,
+ "comparison": {
+ "comparisonMode": "ABSOLUTE",
+ "tolerance": 7
+ }
+ },
+ "value": {
+ "type": {
+ "name": "VDouble",
+ "version": 1
+ },
+ "value": 1,
+ "alarm": {
+ "severity": "NONE",
+ "status": "NONE",
+ "name": "UDF_ALARM"
+ },
+ "time": {
+ "unixSec": 631152000,
+ "nanoSec": 0,
+ "userTag": 0
+ },
+ "display": {
+ "lowAlarm": -30,
+ "highAlarm": 30,
+ "lowDisplay": -50,
+ "highDisplay": 50,
+ "lowWarning": -20,
+ "highWarning": 2,
+ "units": "kW"
+ }
+ }
+ },
+ {
+ "configPv": {
+ "pvName": "COUNTER10",
+ "readOnly": false
+ },
+ "value": {
+ "type": {
+ "name": "VDouble",
+ "version": 1
+ },
+ "value": 332,
+ "alarm": {
+ "severity": "NONE",
+ "status": "NONE",
+ "name": "NO_ALARM"
+ },
+ "time": {
+ "unixSec": 1744789601,
+ "nanoSec": 453044000
+ },
+ "display": {
+ "lowDisplay": 0,
+ "highDisplay": 0,
+ "units": ""
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/save-and-restore/src/test/resources/composite_compiste_snapshot.json b/services/save-and-restore/src/test/resources/composite_compiste_snapshot.json
new file mode 100644
index 0000000000..a14a55c981
--- /dev/null
+++ b/services/save-and-restore/src/test/resources/composite_compiste_snapshot.json
@@ -0,0 +1,7 @@
+{
+ "uniqueId": "ca9971af-cf4e-4629-89e1-c08b4ac79ad8",
+ "referencedSnapshotNodes": [
+ "6c849765-0286-473f-98ff-56e77aeec2e5",
+ "288bbc33-4f52-43ff-ac92-d63f567d3d18"
+ ]
+}
\ No newline at end of file