From d6dd63ad5d49bf38f9a8b257e4e9fc48dbe7a8bb Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 4 Jul 2024 14:54:43 +0200 Subject: [PATCH] Take snapshot on server --- .../applications/saveandrestore/Messages.java | 1 + .../client/SaveAndRestoreClient.java | 7 + .../client/SaveAndRestoreJerseyClient.java | 26 +- .../ui/SaveAndRestoreService.java | 11 + .../BaseSnapshotTableViewController.java | 15 +- .../ui/snapshot/SnapshotController.java | 4 +- .../snapshot/SnapshotTableViewController.java | 96 +++----- .../saveandrestore/messages.properties | 1 + .../epics/SnapshotRestorer.java | 155 ------------ .../saveandrestore/epics/SnapshotUtil.java | 228 ++++++++++++++++++ .../web/config/WebConfiguration.java | 7 +- .../SnapshotRestoreController.java | 8 +- .../controllers/TakeSnapshotController.java | 103 ++++++++ ...estorerTest.java => SnapshotUtilTest.java} | 50 ++-- ...onfig.java => SnapshotUtilTestConfig.java} | 20 +- .../web/config/ControllersTestConfig.java | 11 +- .../SnapshotRestorerControllerTest.java | 12 +- .../TakeSnapshotControllerTest.java | 79 ++++++ 18 files changed, 551 insertions(+), 283 deletions(-) delete mode 100644 services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorer.java create mode 100644 services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotUtil.java create mode 100644 services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotController.java rename services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/{SnapshotRestorerTest.java => SnapshotUtilTest.java} (62%) rename services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/{SnapshotRestorerTestConfig.java => SnapshotUtilTestConfig.java} (56%) create mode 100644 services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java 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 9688a35962..25dac94162 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 @@ -144,6 +144,7 @@ public class Messages { public static String tagCommentLabel; public static String tagRemoveConfirmationTitle; public static String tagRemoveConfirmationContent; + public static String takeSnapshotFailed; public static String timestamp; public static String toolTipTableColumnPVName; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java index 394e514bf5..4fdf741a7a 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java @@ -245,4 +245,11 @@ public interface SaveAndRestoreClient { * @return A @{@link List} of {@link RestoreResult}s with information on potentially failed {@link SnapshotItem}s. */ List restore(String snapshotNodeId); + + /** + * Requests service to take a snapshot. + * @param configurationNodeId The unique id of the {@link Configuration} for which to take the snapshot + * @return A {@link List} of {@link SnapshotItem}s carrying snapshot values read by the service. + */ + List takeSnapshot(String configurationNodeId); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java index 7fd7d1ff8e..810c239259 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java @@ -664,7 +664,6 @@ public List restore(List snapshotItems){ }); } - public List restore(String snapshotNodeId){ WebResource webResource = getClient() @@ -689,4 +688,29 @@ public List restore(String snapshotNodeId){ return response.getEntity(new GenericType<>() { }); } + + @Override + public List takeSnapshot(String configNodeId){ + WebResource webResource = + getClient() + .resource(Preferences.jmasarServiceUrl + "/take-snapshot/" + configNodeId); + ClientResponse response; + try { + response = webResource.accept(CONTENT_TYPE_JSON) + .get(ClientResponse.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { + String message = "Take snapshot failed"; + try { + message = new String(response.getEntityInputStream().readAllBytes()); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to parse response", e); + } + throw new SaveAndRestoreClientException(message); + } + return response.getEntity(new GenericType<>() { + }); + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java index bafc1dd3b1..2a0e979a2b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java @@ -416,4 +416,15 @@ public List restore(String snapshotNodeId) throws Exception{ executor.submit(() -> saveAndRestoreClient.restore(snapshotNodeId)); return future.get(); } + + /** + * Requests service to take a snapshot, i.e. to read PVs as defined in a {@link Configuration}. + * @param configurationNodeId The unique id of the {@link Configuration} for which to take the snapshot + * @return A {@link List} of {@link SnapshotItem}s carrying snapshot values read by the service. + */ + public List takeSnapshot(String configurationNodeId) throws Exception{ + Future> future = + executor.submit(() -> saveAndRestoreClient.takeSnapshot(configurationNodeId)); + return future.get(); + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java index a6d33c89f4..edaa3f00d1 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java @@ -283,6 +283,7 @@ protected void showSnapshotInTable(Snapshot snapshot) { }); connectPVs(); + updateTable(null); } @@ -294,21 +295,21 @@ protected void showSnapshotInTable(Snapshot snapshot) { public void updateTable(List entries) { final ObservableList items = snapshotTableView.getItems(); final boolean notHide = !snapshotController.isHideEqualItems(); - snapshotTableView.getItems().clear(); - tableEntryItems.entrySet().forEach(e -> { + items.clear(); + tableEntryItems.forEach((key, value) -> { // there is no harm if this is executed more than once, because only one line is allowed for these // two properties (see SingleListenerBooleanProperty for more details) - e.getValue().liveStoredEqualProperty().addListener((a, o, n) -> { + value.liveStoredEqualProperty().addListener((a, o, n) -> { if (snapshotController.isHideEqualItems()) { if (n) { - snapshotTableView.getItems().remove(e.getValue()); + snapshotTableView.getItems().remove(value); } else { - snapshotTableView.getItems().add(e.getValue()); + snapshotTableView.getItems().add(value); } } }); - if (notHide || !e.getValue().liveStoredEqualProperty().get()) { - items.add(e.getValue()); + if (notHide || !value.liveStoredEqualProperty().get()) { + items.add(value); } }); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index 29d287f03b..ce9d79c4b4 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -140,7 +140,9 @@ public void takeSnapshot() { snapshotTab.setText(Messages.unnamedSnapshot); snapshotTableViewController.takeSnapshot(snapshot -> { disabledUi.set(false); - snapshotProperty.set(snapshot); + if(snapshot != null){ + snapshotProperty.set(snapshot); + } }); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java index 5a47b08446..f183746f66 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java @@ -53,19 +53,17 @@ import org.phoebus.core.vtypes.VTypeHelper; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.util.time.TimestampFormats; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.logging.Level; @@ -183,71 +181,39 @@ public void updateItem(final Boolean item, final boolean empty) { } public void takeSnapshot(Consumer consumer) { - // Clear snapshots array - snapshots.clear(); - List entries = new ArrayList<>(); - readAll(list -> - Platform.runLater(() -> { - tableEntryItems.clear(); - entries.addAll(list); - Snapshot snapshot = new Snapshot(); - snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT).build()); - SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnapshotItems(entries); - snapshot.setSnapshotData(snapshotData); - showSnapshotInTable(snapshot); - if (!Preferences.default_snapshot_name_date_format.isEmpty()) { - SimpleDateFormat formatter = new SimpleDateFormat(Preferences.default_snapshot_name_date_format); - snapshot.getSnapshotNode().setName(formatter.format(new Date())); - } - consumer.accept(snapshot); - }) - ); - } - - /** - * Reads all PVs using a thread pool. All reads are asynchronous, waiting at most the amount of time - * configured through a preference setting. - * - * @param completion Callback receiving a list of {@link SnapshotItem}s where values for PVs that could - * not be read are set to {@link org.phoebus.applications.saveandrestore.ui.VDisconnectedData#INSTANCE}. - */ - private void readAll(Consumer> completion) { - ExecutorService executorService = Executors.newFixedThreadPool(10); - SnapshotItem[] snapshotEntries = new SnapshotItem[tableEntryItems.values().size()]; JobManager.schedule("Take snapshot", monitor -> { - final CountDownLatch countDownLatch = new CountDownLatch(tableEntryItems.values().size()); - for (TableEntry t : tableEntryItems.values()) { - // Submit read request only if job has not been cancelled - executorService.submit(() -> { - SaveAndRestorePV pv = pvs.get(getPVKey(t.pvNameProperty().get(), t.readOnlyProperty().get())); - VType value = VDisconnectedData.INSTANCE; - try { - value = pv.getPv().asyncRead().get(Preferences.readTimeout, TimeUnit.MILLISECONDS); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to read PV " + pv.getPvName(), e); - } - VType readBackValue = VDisconnectedData.INSTANCE; - if (pv.getReadbackPv() != null && !pv.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { - try { - readBackValue = pv.getReadbackPv().asyncRead().get(Preferences.readTimeout, TimeUnit.MILLISECONDS); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to read read-back PV " + pv.getReadbackPvName(), e); - } - } - SnapshotItem snapshotItem = new SnapshotItem(); - snapshotItem.setConfigPv(t.getConfigPv()); - snapshotItem.setValue(value); - snapshotItem.setReadbackValue(readBackValue); - - snapshotEntries[t.idProperty().get() - 1] = snapshotItem; - countDownLatch.countDown(); - }); + // Clear snapshots array + snapshots.clear(); + List snapshotItems; + try { + snapshotItems = SaveAndRestoreService.getInstance().takeSnapshot(snapshotController.getConfigurationNode().getUniqueId()); + } catch (Exception e) { + ExceptionDetailsErrorDialog.openError(snapshotTableView, Messages.errorGeneric, Messages.takeSnapshotFailed, e); + consumer.accept(null); + return; } - countDownLatch.await(); - completion.accept(Arrays.asList(snapshotEntries)); - executorService.shutdown(); + // Service can only return nulls for disconnected PVs, but UI expects VDisonnectedData + snapshotItems.forEach(si -> { + if (si.getValue() == null) { + si.setValue(VDisconnectedData.INSTANCE); + } + if (si.getReadbackValue() == null) { + si.setReadbackValue(VDisconnectedData.INSTANCE); + } + }); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT).build()); + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(snapshotItems); + snapshot.setSnapshotData(snapshotData); + showSnapshotInTable(snapshot); + if (!Preferences.default_snapshot_name_date_format.isEmpty()) { + SimpleDateFormat formatter = new SimpleDateFormat(Preferences.default_snapshot_name_date_format); + snapshot.getSnapshotNode().setName(formatter.format(new Date())); + } + consumer.accept(snapshot); }); + } public void updateThreshold(Snapshot snapshot, double threshold) { 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 9dc28c7992..87ea67a669 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 @@ -209,6 +209,7 @@ tagCommentLabel=Comment tagRemoveConfirmationTitle=Remove Tag tagRemoveConfirmationContent=Are you sure to remove the tag on the selected snapshot(s)? takeSnapshot=Take Snapshot +takeSnapshotFailed=Failed to take snapshot timestamp=Timestamp Time=Time StartTime=Start Time diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorer.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorer.java deleted file mode 100644 index 3968a5e9b3..0000000000 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorer.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.phoebus.service.saveandrestore.epics; - -import org.phoebus.applications.saveandrestore.model.RestoreResult; -import org.phoebus.applications.saveandrestore.model.SnapshotItem; -import org.phoebus.core.vtypes.VTypeHelper; -import org.phoebus.framework.preferences.PropertyPreferenceLoader; -import org.phoebus.pv.PV; -import org.phoebus.pv.PVPool; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; - -import java.io.File; -import java.io.FileInputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class SnapshotRestorer { - - private final Logger LOG = Logger.getLogger(SnapshotRestorer.class.getName()); - - @Value("${connection.timeout:5000}") - private int connectionTimeout; - - @Value("${write.timeout:5000}") - private int writeTimeout; - - @Autowired - private ExecutorService executorService; - - public SnapshotRestorer() { - final File site_settings = new File("settings.ini"); - if (site_settings.canRead()) { - LOG.config("Loading settings from " + site_settings); - try { - PropertyPreferenceLoader.load(new FileInputStream(site_settings)); - } catch (Exception e) { - LOG.log(Level.WARNING, "Unable to read settings.ini, falling back to default values."); - } - } - } - - /** - * Restore PV values from a list of snapshot items - * - *

- * Writes concurrently the pv value to the non-null set PVs in - * the snapshot items. - * Uses synchronized to ensure only one frontend can write at a time. - * Returns a list of the snapshot items you have set, with an error message if - * an error occurred. - * - * @param snapshotItems {@link SnapshotItem} - */ - public synchronized List restorePVValues(List snapshotItems) { - // Attempt to connect to all PVs before trying to write/restore. - List connectedPvs = connectPVs(snapshotItems); - List failedPvs = new ArrayList<>(); - final CountDownLatch countDownLatch = new CountDownLatch(snapshotItems.size()); - snapshotItems.forEach(item -> { - String pvName = item.getConfigPv().getPvName(); - Optional pvOptional = null; - try { - // Check if PV is connected. If not, do not even try to write/restore. - pvOptional = connectedPvs.stream().filter(pv -> pv.getName().equals(pvName)).findFirst(); - if (pvOptional.isPresent()) { - pvOptional.get().write(VTypeHelper.toObject(item.getValue())); - } else { - RestoreResult restoreResult = new RestoreResult(); - restoreResult.setSnapshotItem(item); - restoreResult.setErrorMsg("PV disconnected"); - failedPvs.add(restoreResult); - } - } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to restore PV " + pvName); - RestoreResult restoreResult = new RestoreResult(); - restoreResult.setSnapshotItem(item); - restoreResult.setErrorMsg(e.getMessage()); - failedPvs.add(restoreResult); - } finally { - if (pvOptional != null && pvOptional.isPresent()) { - PVPool.releasePV(pvOptional.get()); - } - countDownLatch.countDown(); - } - }); - - try { - countDownLatch.await(writeTimeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - LOG.log(Level.INFO, "Encountered InterruptedException", e); - } - - return failedPvs; - } - - /** - * Attempts to connect to all PVs using {@link PVPool}. A connection is considered successful once an - * event is received that does not indicate disconnection. - * - *

- * A timeout of {@link #connectionTimeout} ms is used to wait for a PV to supply a value message indicating - * successful connection. - *

- *

- * An {@link ExecutorService} is used to run connection attempts concurrently. However, no timeout is employed - * for the overall execution of all connection attempts. - *

- * - * @param snapshotItems List of {@link SnapshotItem}s in a snapshot. - * @return A {@link List} of {@link PV}s for which connection succeeded. Ideally this should be all - * PVs as listed in the input argument. - */ - private List connectPVs(List snapshotItems) { - List connectedPvs = new ArrayList<>(); - - List> callables = snapshotItems.stream().map(snapshotItem -> { - return (Callable) () -> { - CountDownLatch countDownLatch = new CountDownLatch(1); - try { - PV pv = PVPool.getPV(snapshotItem.getConfigPv().getPvName()); - pv.onValueEvent().subscribe(value -> { - if (!PV.isDisconnected(value)) { - connectedPvs.add(pv); - countDownLatch.countDown(); - } - }); - // Wait for a value message indicating connection - countDownLatch.await(connectionTimeout, TimeUnit.MILLISECONDS); - } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to connect to PV " + snapshotItem.getConfigPv().getPvName(), e); - countDownLatch.countDown(); - } - return null; - }; - }).toList(); - - try { - executorService.invokeAll(callables); - } catch (InterruptedException e) { - LOG.log(Level.WARNING, "Got exception waiting for all tasks to finish", e); - // Return empty list here? - return Collections.emptyList(); - } - - return connectedPvs; - } -} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotUtil.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotUtil.java new file mode 100644 index 0000000000..9257e0817e --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/epics/SnapshotUtil.java @@ -0,0 +1,228 @@ +package org.phoebus.service.saveandrestore.epics; + +import org.epics.vtype.VType; +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.RestoreResult; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.core.vtypes.VTypeHelper; +import org.phoebus.framework.preferences.PropertyPreferenceLoader; +import org.phoebus.pv.PV; +import org.phoebus.pv.PVPool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides some utility methods to read and write PVs in an asynchronous manner. + */ +public class SnapshotUtil { + + private final Logger LOG = Logger.getLogger(SnapshotUtil.class.getName()); + + @SuppressWarnings("unused") + @Value("${connection.timeout:5000}") + private int connectionTimeout; + + @SuppressWarnings("unused") + @Value("${write.timeout:5000}") + private int writeTimeout; + + @Autowired + private ExecutorService executorService; + + public SnapshotUtil() { + final File site_settings = new File("settings.ini"); + if (site_settings.canRead()) { + LOG.config("Loading settings from " + site_settings); + try { + PropertyPreferenceLoader.load(new FileInputStream(site_settings)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Unable to read settings.ini, falling back to default values."); + } + } + } + + /** + * Restore PV values from a list of snapshot items + * + *

+ * Writes concurrently the pv value to the non-null set PVs in + * the snapshot items. + * Uses synchronized to ensure only one frontend can write at a time. + * Returns a list of the snapshot items you have set, with an error message if + * an error occurred. + * + * @param snapshotItems {@link SnapshotItem} + */ + public synchronized List restore(List snapshotItems) { + // First clean the list of SnapshotItems from read-only elements. + List cleanedSnapshotItems = cleanSnapshotItems(snapshotItems); + List restoreResultList = new ArrayList<>(); + + List> callables = new ArrayList<>(); + for (SnapshotItem si : cleanedSnapshotItems) { + Callable writePvCallable = () -> { + CountDownLatch countDownLatch = new CountDownLatch(1); + PV pv; + try { + pv = PVPool.getPV(si.getConfigPv().getPvName()); + pv.onValueEvent().throttleLatest(1000, TimeUnit.MILLISECONDS).subscribe(value -> { + if (!PV.isDisconnected(value)) { + pv.write(VTypeHelper.toObject(si.getValue())); + PVPool.releasePV(pv); + } + countDownLatch.countDown(); + }); + if (!countDownLatch.await(connectionTimeout, TimeUnit.MILLISECONDS)) { + RestoreResult restoreResult = new RestoreResult(); + restoreResult.setSnapshotItem(si); + restoreResult.setErrorMsg("No monitor event from PV " + si.getConfigPv().getPvName()); + restoreResultList.add(restoreResult); + } + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to write to PV " + si.getConfigPv().getPvName(), e); + RestoreResult restoreResult = new RestoreResult(); + restoreResult.setSnapshotItem(si); + restoreResult.setErrorMsg(e.getMessage()); + restoreResultList.add(restoreResult); + countDownLatch.countDown(); + } + + return null; + }; + callables.add(writePvCallable); + } + + try { + executorService.invokeAll(callables); + } catch (InterruptedException e) { + LOG.log(Level.WARNING, "Got exception waiting for all tasks to finish", e); + // Return empty list here? + return Collections.emptyList(); + } + + return restoreResultList; + } + + /** + * Reads all PVs and read-back PVs as defined in the {@link ConfigurationData} argument. For each + * {@link ConfigPv} item in {@link ConfigurationData} a {@link SnapshotItem} is created holding the + * values read. + * Read operations are concurrent using a thread pool. Failed connections/reads will cause a wait of at most + * {@link #connectionTimeout} ms on each thread. + * + * @param configurationData Identifies which {@link Configuration} user selected to create a snapshot. + * @return A list of {@link SnapshotItem}s holding the values read from IOCs. + */ + public List takeSnapshot(ConfigurationData configurationData) { + List snapshotItems = new ArrayList<>(); + List> callables = new ArrayList<>(); + Map pvValues = new HashMap<>(); + Map readbackPvValues = new HashMap<>(); + for (ConfigPv configPv : configurationData.getPvList()) { + Callable pvValueCallable = () -> { + CountDownLatch countDownLatch = new CountDownLatch(1); + PV pv = null; + try { + pv = PVPool.getPV(configPv.getPvName()); + pv.onValueEvent().subscribe(value -> { + if (!VTypeHelper.isDisconnected(value)) { + pvValues.put(configPv.getPvName(), value); + countDownLatch.countDown(); + } + }); + if (!countDownLatch.await(connectionTimeout, TimeUnit.MILLISECONDS)) { + pvValues.put(configPv.getPvName(), null); + } + } catch (Exception e) { + pvValues.put(configPv.getPvName(), null); + countDownLatch.countDown(); + } finally { + if (pv != null) { + PVPool.releasePV(pv); + } + } + return null; + }; + callables.add(pvValueCallable); + } + + for (ConfigPv configPv : configurationData.getPvList()) { + if (configPv.getReadbackPvName() == null) { + continue; + } + Callable readbackPvValueCallable = () -> { + CountDownLatch countDownLatch = new CountDownLatch(1); + PV pv = null; + try { + pv = PVPool.getPV(configPv.getReadbackPvName()); + pv.onValueEvent().subscribe(value -> { + if (!VTypeHelper.isDisconnected(value)) { + readbackPvValues.put(configPv.getPvName(), value); + countDownLatch.countDown(); + } + }); + if (!countDownLatch.await(connectionTimeout, TimeUnit.MILLISECONDS)) { + readbackPvValues.put(configPv.getPvName(), null); + } + } catch (Exception e) { + readbackPvValues.put(configPv.getPvName(), null); + countDownLatch.countDown(); + } finally { + if (pv != null) { + PVPool.releasePV(pv); + } + } + return null; + }; + callables.add(readbackPvValueCallable); + } + + try { + executorService.invokeAll(callables); + } catch (InterruptedException e) { + LOG.log(Level.WARNING, "Got exception waiting for all read tasks to finish", e); + // Return empty list here? + return Collections.emptyList(); + } + + // Merge data into SnapshotItems + for (String pvName : pvValues.keySet()) { + SnapshotItem snapshotItem = new SnapshotItem(); + for (ConfigPv configPv : configurationData.getPvList()) { + if (configPv.getPvName().equals(pvName)) { + snapshotItem.setConfigPv(configPv); + break; + } + } + VType value = pvValues.get(pvName); + snapshotItem.setValue(value); + VType readbackValue = readbackPvValues.get(pvName); + if (readbackValue != null) { + snapshotItem.setReadbackValue(readbackValue); + } + snapshotItems.add(snapshotItem); + } + + return snapshotItems; + } + + private List cleanSnapshotItems(List snapshotItems) { + return snapshotItems.stream().filter(si -> !si.getConfigPv().isReadOnly()).toList(); + } +} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java index 48087c22fa..c6dcb4ca62 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java @@ -17,7 +17,7 @@ */ package org.phoebus.service.saveandrestore.web.config; -import org.phoebus.service.saveandrestore.epics.SnapshotRestorer; +import org.phoebus.service.saveandrestore.epics.SnapshotUtil; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchDAO; import org.springframework.context.annotation.Bean; @@ -26,7 +26,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; /** * {@link Configuration} class setting up beans for {@link org.springframework.stereotype.Controller} classes. @@ -57,8 +56,8 @@ public AcceptHeaderResolver acceptHeaderResolver() { @SuppressWarnings("unused") @Bean @Scope("singleton") - public SnapshotRestorer snapshotRestorer(){ - return new SnapshotRestorer(); + public SnapshotUtil snapshotRestorer(){ + return new SnapshotUtil(); } @Bean diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestoreController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestoreController.java index 9a51d9647a..5e1253ab85 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestoreController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestoreController.java @@ -19,7 +19,7 @@ import org.phoebus.applications.saveandrestore.model.RestoreResult; import org.phoebus.applications.saveandrestore.model.SnapshotItem; -import org.phoebus.service.saveandrestore.epics.SnapshotRestorer; +import org.phoebus.service.saveandrestore.epics.SnapshotUtil; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; @@ -40,19 +40,19 @@ public class SnapshotRestoreController extends BaseController { private NodeDAO nodeDAO; @Autowired - private SnapshotRestorer snapshotRestorer; + private SnapshotUtil snapshotUtil; @PostMapping(value = "/restore/items", produces = JSON) public List restoreFromSnapshotItems( @RequestBody List snapshotItems) { - return snapshotRestorer.restorePVValues(snapshotItems); + return snapshotUtil.restore(snapshotItems); } @PostMapping(value = "/restore/node", produces = JSON) public List restoreFromSnapshotNode( @RequestParam(value = "nodeId") String nodeId){ var snapshot = nodeDAO.getSnapshotData(nodeId); - return snapshotRestorer.restorePVValues(snapshot.getSnapshotItems()); + return snapshotUtil.restore(snapshot.getSnapshotItems()); } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotController.java new file mode 100644 index 0000000000..3496d17b62 --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotController.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +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.Snapshot; +import org.phoebus.applications.saveandrestore.model.SnapshotData; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.service.saveandrestore.epics.SnapshotUtil; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +@RestController +public class TakeSnapshotController extends BaseController { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private SnapshotUtil snapshotUtil; + + private final SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + /** + * Take a snapshot based on the {@link org.phoebus.applications.saveandrestore.model.Configuration}'s unique id. + * + * @param configNodeId Unique id of a {@link org.phoebus.applications.saveandrestore.model.Configuration} + * @return A {@link List} of {@link SnapshotItem}s, one for each {@link org.phoebus.applications.saveandrestore.model.ConfigPv} + * in the {@link org.phoebus.applications.saveandrestore.model.Configuration}. + */ + @SuppressWarnings("unused") + @GetMapping(value = "/take-snapshot/{configNodeId}", produces = JSON) + public List takeSnapshot(@PathVariable String configNodeId) { + ConfigurationData configurationData = nodeDAO.getConfigurationData(configNodeId); + List snapshotItems; + try { + snapshotItems = snapshotUtil.takeSnapshot(configurationData); + } catch (Exception e) { + throw new RuntimeException(e); + } + return snapshotItems; + } + + /** + * Take a snapshot based on the {@link org.phoebus.applications.saveandrestore.model.Configuration}'s unique id, + * and attempts to save it. If a snapshot name is provided and if the parent configuration node already + * contains a snapshot with the same name, an exception is thrown. + * + * @param configNodeId Unique id of a {@link org.phoebus.applications.saveandrestore.model.Configuration} + * @param snapshotName Optional name of the new snapshot. If not provided or empty, current date and time will + * define the name. + * @param comment Optional comment of the new snapshot. If not provided or empty, current date and time will + * define the comment. + * @return A {@link Snapshot} representing the new snapshot node. + */ + @SuppressWarnings("unused") + @PutMapping(value = "/take-snapshot/{nodeId}", produces = JSON) + public Snapshot takeSnapshotAndSave(@PathVariable String configNodeId, + @RequestParam(name = "name", required = false) String snapshotName, + @RequestParam(name = "comment", required = false) String comment) { + if (snapshotName != null) { + String _snapshotName = snapshotName; + List childNodes = nodeDAO.getChildNodes(configNodeId); + if (childNodes.stream().anyMatch(n -> n.getName().equals(_snapshotName) && + n.getNodeType().equals(NodeType.SNAPSHOT))) { + throw new IllegalArgumentException("Snapshot named " + _snapshotName + + " already exists"); + } + } + + Date now = new Date(); + snapshotName = snapshotName == null || snapshotName.isEmpty() ? + simpleDateFormat.format(now) : + snapshotName; + comment = comment == null || comment.isEmpty() ? + simpleDateFormat.format(now) : + comment; + List snapshotItems = takeSnapshot(configNodeId); + + Node node = Node.builder().nodeType(NodeType.SNAPSHOT).name(snapshotName).description(comment).build(); + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(snapshotItems); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + snapshot.setSnapshotData(snapshotData); + + return nodeDAO.createSnapshot(configNodeId, snapshot); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotUtilTest.java similarity index 62% rename from services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorerTest.java rename to services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotUtilTest.java index 02126392cf..55dbfb7d95 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotUtilTest.java @@ -19,32 +19,34 @@ package org.phoebus.service.saveandrestore.epics; -import org.junit.jupiter.api.Assertions; - -import java.util.Arrays; - -import org.epics.vtype.Time; -import org.epics.vtype.VFloat; import org.epics.vtype.Alarm; import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VFloat; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; 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.SnapshotItem; import org.phoebus.core.vtypes.VTypeHelper; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; -import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.List; + @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = SnapshotRestorerTestConfig.class) -public class SnapshotRestorerTest { +@ContextConfiguration(classes = SnapshotUtilTestConfig.class) +public class SnapshotUtilTest { @Autowired - private SnapshotRestorer snapshotRestorer; + private SnapshotUtil snapshotUtil; @Test public void testRestorePVValues() throws Exception { @@ -55,22 +57,40 @@ public void testRestorePVValues() throws Exception { var testSnapshotItem = new SnapshotItem(); testSnapshotItem.setConfigPv(configPv); testSnapshotItem.setValue(VFloat.of(1.0, Alarm.noValue(), Time.now(), Display.none())); - snapshotRestorer.restorePVValues( - Arrays.asList(testSnapshotItem)); + snapshotUtil.restore(List.of(testSnapshotItem)); var pvValue = pv.asyncRead().get(); Assertions.assertEquals(VTypeHelper.toObject(pvValue), 1.0); } @Test - public void testCannotConnectPV() throws Exception { + public void testCannotConnectPV() { var configPv = new ConfigPv(); configPv.setPvName("pva://x"); var testSnapshotItem = new SnapshotItem(); testSnapshotItem.setConfigPv(configPv); testSnapshotItem.setValue(VFloat.of(1.0, Alarm.noValue(), Time.now(), Display.none())); - var result = snapshotRestorer.restorePVValues( - Arrays.asList(testSnapshotItem)); + var result = snapshotUtil.restore( + List.of(testSnapshotItem)); Assertions.assertNotNull(result.get(0).getErrorMsg()); } + + @Test + public void testTakeSnapshot(){ + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = new ConfigPv(); + configPv.setPvName("loc://x(42.0)"); + configPv.setReadbackPvName("loc://y(777.0)"); + ConfigPv configPv2 = new ConfigPv(); + configPv2.setPvName("loc://xx(44.0)"); + configurationData.setPvList(List.of(configPv, configPv2)); + + List snapshotItems = snapshotUtil.takeSnapshot(configurationData); + Assertions.assertEquals(42.0, VTypeHelper.toDouble(snapshotItems.get(0).getValue())); + Assertions.assertEquals(777.0, VTypeHelper.toDouble(snapshotItems.get(0).getReadbackValue())); + Assertions.assertEquals(44.0, VTypeHelper.toDouble(snapshotItems.get(1).getValue())); + Assertions.assertNull(snapshotItems.get(1).getReadbackValue()); + + + } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorerTestConfig.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotUtilTestConfig.java similarity index 56% rename from services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorerTestConfig.java rename to services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotUtilTestConfig.java index eb0d996797..4b1bf43a61 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotRestorerTestConfig.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/epics/SnapshotUtilTestConfig.java @@ -18,24 +18,10 @@ package org.phoebus.service.saveandrestore.epics; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import org.mockito.Mockito; -import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ConfigurationDataRepository; -import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchTreeRepository; -import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.FilterRepository; -import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.SnapshotDataRepository; -import org.phoebus.service.saveandrestore.search.SearchUtil; -import org.phoebus.service.saveandrestore.web.config.AcceptHeaderResolver; -import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; -import org.springframework.context.annotation.Scope; -import org.springframework.util.Base64Utils; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -44,11 +30,11 @@ @ComponentScan(basePackages = "org.phoebus.service.saveandrestore.epics") @SuppressWarnings("unused") @Profile("!IT") -public class SnapshotRestorerTestConfig { +public class SnapshotUtilTestConfig { @Bean - public SnapshotRestorer snapshotRestorer(){ - return new SnapshotRestorer(); + public SnapshotUtil snapshotRestorer(){ + return new SnapshotUtil(); } @Bean diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java index 55613e09dd..ff8a383ab7 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java @@ -20,7 +20,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import org.mockito.Mockito; -import org.phoebus.service.saveandrestore.epics.SnapshotRestorer; +import org.phoebus.service.saveandrestore.epics.SnapshotUtil; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ConfigurationDataRepository; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchTreeRepository; @@ -124,12 +124,17 @@ public String readOnlyAuthorization() { @SuppressWarnings("unused") @Bean @Scope("singleton") - public SnapshotRestorer snapshotRestorer(){ - return new SnapshotRestorer(); + public SnapshotUtil snapshotRestorer(){ + return new SnapshotUtil(); } @Bean public ExecutorService executorService(){ return Executors.newCachedThreadPool(); } + + @Bean + public SnapshotUtil snapshotUtil(){ + return Mockito.mock(SnapshotUtil.class); + } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java index 221ddbe9ff..c5d33d8c7f 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; - import org.epics.vtype.Alarm; import org.epics.vtype.Display; import org.epics.vtype.Time; @@ -10,10 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.phoebus.applications.saveandrestore.model.ConfigPv; -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.RestoreResult; -import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.model.SnapshotData; import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; @@ -39,7 +35,7 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ControllersTestConfig.class) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") -@WebMvcTest(NodeController.class) +@WebMvcTest(SnapshotRestoreController.class) public class SnapshotRestorerControllerTest { @Autowired @@ -53,14 +49,10 @@ public class SnapshotRestorerControllerTest { @Autowired private MockMvc mockMvc; - @Autowired - private String demoUser; @Test public void testRestoreFromSnapshotNode() throws Exception { - Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); - Snapshot snapshot = new Snapshot(); SnapshotData snapshotData = new SnapshotData(); SnapshotItem item = new SnapshotItem(); ConfigPv configPv = new ConfigPv(); @@ -68,8 +60,6 @@ public void testRestoreFromSnapshotNode() throws Exception { item.setValue(VFloat.of(1.0, Alarm.none(), Time.now(), Display.none())); item.setConfigPv(configPv); snapshotData.setSnapshotItems(List.of(item)); - snapshot.setSnapshotData(snapshotData); - snapshot.setSnapshotNode(node); when(nodeDAO.getSnapshotData("uniqueId")).thenReturn(snapshotData); diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java new file mode 100644 index 0000000000..8a868c7464 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java @@ -0,0 +1,79 @@ +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.core.type.TypeReference; +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.ConfigurationData; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.service.saveandrestore.NodeNotFoundException; +import org.phoebus.service.saveandrestore.epics.SnapshotUtil; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +@WebMvcTest(TakeSnapshotController.class) +public class TakeSnapshotControllerTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private SnapshotUtil snapshotUtil; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private MockMvc mockMvc; + + + @Test + public void testTakeSnapshot() throws Exception { + ConfigurationData configurationData = new ConfigurationData(); + + when(nodeDAO.getConfigurationData("uniqueId")).thenReturn(configurationData); + when(snapshotUtil.takeSnapshot(configurationData)) + .thenReturn(Collections.emptyList()); + + MockHttpServletRequestBuilder request = get("/take-snapshot/uniqueId"); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response is in the Restore Result json format + objectMapper.readValue( + result.getResponse().getContentAsString(), + new TypeReference>() { + }); + } + + @Test + public void testTakeSnapshotBadConfigId() throws Exception { + + when(nodeDAO.getConfigurationData("uniqueId")).thenThrow(new NodeNotFoundException("")); + + MockHttpServletRequestBuilder request = get("/take-snapshot/uniqueId"); + + mockMvc.perform(request).andExpect(status().isNotFound()); + + } +}