diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index e783181..5bc31ca 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -64,6 +64,9 @@ jobs: DOCKER_ARGS: -v ${{ env.HOME }}/.m2:/root/.m2 -v ${{ github.workspace }}/target:/src/target -t - name: Set folders ownership back to current user run: sudo chown -R $(id -u):$(id -g) $GITHUB_WORKSPACE && sudo chown -R $(id -u):$(id -g) $HOME + - name: Upload to codecov after successful tests + if: ${{ success() }} + run: bash <(curl -s https://codecov.io/bash) - name: Upload Artifact uses: actions/upload-artifact@v2 with: diff --git a/README.md b/README.md index 35c53cd..25a49c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ A plugin for ImageJ to provide macro extensions to access OMERO. ## How to install -1. Install the [OMERO.insight plugin for Fiji](https://omero-guides.readthedocs.io/en/latest/fiji/docs/installation.html) (if you haven't already). +1. Install + the [OMERO.insight plugin for Fiji](https://omero-guides.readthedocs.io/en/latest/fiji/docs/installation.html) (if + you haven't already). 2. Download the JAR file for this [library](https://github.com/GReD-Clermont/simple-omero-client/releases/latest/). 3. Download the JAR file ([for this plugin](https://github.com/GReD-Clermont/omero_macro-extensions/releases/latest/)). 4. Place these JAR files in your plugins folder. @@ -33,6 +35,12 @@ Then, switching group can be performed through: Ext.switchGroup(groupId); ``` +You can also choose to only list objects for a given user (or all if username is empty or equal to "all"): + +``` +Ext.listForUser(username); +``` + When done, you can disconnect using: ``` @@ -165,7 +173,7 @@ Tags can be created with *Ext.createTag*: tagId = Ext.createTag(name, description); ``` -### Linking objects +### Linking/unlinking objects Objects can be linked with *Ext.link*, e.g.: @@ -173,6 +181,12 @@ Objects can be linked with *Ext.link*, e.g.: Ext.link("dataset", datasetId, "tag", tagId); ``` +They can also be unlinked with *Ext.unlink*, e.g.: + +``` +Ext.unlink("dataset", datasetId, "tag", tagId); +``` + ### Deleting objects Objects can be deleted with *Ext.delete*: @@ -189,13 +203,13 @@ Pixel intensities can be retrieved from images: imageplusID = Ext.getImage(imageIds[0]); ``` -ROIs from OMERO can also be added to the ROI manager (and the current image). ROIs composed of multiple shapes (eg -3D/4D) will share the same values in the "ROI" and "ROI_ID" properties in ImageJ. These can be optionnally changed with -the "property" parameter: local indices will be in "property" while OMERO IDs will be in "property + _ID". This is -achieved through: +ROIs from OMERO can also be added to the ROI manager or to the Overlay of the current image (boolean toOverlay). ROIs +composed of multiple shapes (eg 3D/4D) will share the same values in the "ROI" and "ROI_ID" properties in ImageJ. These +can be optionally changed with the "property" parameter: local indices will be in "property" while OMERO IDs will be +in "property + _ID". This is achieved through: ``` -nIJROIs = Ext.getROIs(imageIds[0], property); +nIJROIs = Ext.getROIs(imageIds[0], toOverlay, property); ``` Conversely, ImageJ ROIs can also be saved to OMERO (the property is used to group ImageJ shapes into a single 3D/4D ROI @@ -245,10 +259,10 @@ The table can then be saved to a project/dataset/image through *Ext.saveTable*: Ext.saveTable(tableName, 'dataset', datasetId); ``` -It can then be saved to a tab-separated text file through *Ext.saveTableAsTXT*: +It can then be saved to a delimited text file through *Ext.saveTableAsFile* (default separator is ','): ``` -Ext.saveTableAsTXT(tableName, pathToTXT); +Ext.saveTableAsFile(tableName, pathToFile, delimiter); ``` ### Work as another user (sudo) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..d20fb04 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +coverage: + range: "60...90" + status: + patch: + default: + only_pulls: true diff --git a/pom.xml b/pom.xml index bb263ec..a9d4925 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ fr.igred omero_macro-extensions - 1.0.1 + 1.1.0 ImageJ OMERO macro extensions Plugin providing macro extensions for OMERO. @@ -110,13 +110,13 @@ org.junit.jupiter junit-jupiter-api - 5.7.1 + 5.8.2 test org.junit.jupiter junit-jupiter-params - 5.7.1 + 5.8.2 test diff --git a/src/main/java/fr/igred/ij/plugin/OMEROMacroExtension.java b/src/main/java/fr/igred/ij/plugin/OMEROMacroExtension.java index 8c3f698..466937a 100644 --- a/src/main/java/fr/igred/ij/plugin/OMEROMacroExtension.java +++ b/src/main/java/fr/igred/ij/plugin/OMEROMacroExtension.java @@ -23,6 +23,7 @@ import fr.igred.omero.exception.AccessException; import fr.igred.omero.exception.OMEROServerError; import fr.igred.omero.exception.ServiceException; +import fr.igred.omero.meta.ExperimenterWrapper; import fr.igred.omero.repository.DatasetWrapper; import fr.igred.omero.repository.GenericRepositoryObjectWrapper; import fr.igred.omero.repository.ImageWrapper; @@ -45,7 +46,13 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -63,23 +70,25 @@ public class OMEROMacroExtension implements PlugIn, MacroExtension { private final ExtensionDescriptor[] extensions = { newDescriptor("connectToOMERO", this, ARG_STRING, ARG_NUMBER, ARG_STRING, ARG_STRING), newDescriptor("switchGroup", this, ARG_NUMBER), + newDescriptor("listForUser", this, ARG_STRING), newDescriptor("list", this, ARG_STRING, ARG_STRING + ARG_OPTIONAL, ARG_NUMBER + ARG_OPTIONAL), newDescriptor("createDataset", this, ARG_STRING, ARG_STRING, ARG_NUMBER + ARG_OPTIONAL), newDescriptor("createProject", this, ARG_STRING, ARG_STRING), newDescriptor("createTag", this, ARG_STRING, ARG_STRING), newDescriptor("link", this, ARG_STRING, ARG_NUMBER, ARG_STRING, ARG_NUMBER), + newDescriptor("unlink", this, ARG_STRING, ARG_NUMBER, ARG_STRING, ARG_NUMBER), newDescriptor("addFile", this, ARG_STRING, ARG_NUMBER, ARG_STRING), newDescriptor("addToTable", this, ARG_STRING, ARG_STRING + ARG_OPTIONAL, ARG_NUMBER + ARG_OPTIONAL, ARG_STRING + ARG_OPTIONAL), newDescriptor("saveTable", this, ARG_STRING, ARG_STRING, ARG_NUMBER), - newDescriptor("saveTableAsTXT", this, ARG_STRING, ARG_STRING), + newDescriptor("saveTableAsFile", this, ARG_STRING, ARG_STRING, ARG_STRING + ARG_OPTIONAL), newDescriptor("clearTable", this, ARG_STRING), newDescriptor("importImage", this, ARG_NUMBER, ARG_STRING + ARG_OPTIONAL), newDescriptor("downloadImage", this, ARG_NUMBER, ARG_STRING), newDescriptor("delete", this, ARG_STRING, ARG_NUMBER), newDescriptor("getName", this, ARG_STRING, ARG_NUMBER), newDescriptor("getImage", this, ARG_NUMBER), - newDescriptor("getROIs", this, ARG_NUMBER, ARG_STRING + ARG_OPTIONAL), + newDescriptor("getROIs", this, ARG_NUMBER, ARG_NUMBER + ARG_OPTIONAL, ARG_STRING + ARG_OPTIONAL), newDescriptor("saveROIs", this, ARG_NUMBER, ARG_STRING + ARG_OPTIONAL), newDescriptor("sudo", this, ARG_STRING), newDescriptor("endSudo", this), @@ -91,6 +100,8 @@ public class OMEROMacroExtension implements PlugIn, MacroExtension { private Client client = new Client(); private Client switched; + private ExperimenterWrapper user = null; + private static > String listToIDs(List list) { return list.stream() @@ -122,6 +133,12 @@ private static ResultsTable getTable(String resultsName) { } + private > List filterUser(List list) { + if (user == null) return list; + else return list.stream().filter(o -> o.getOwner().getId() == user.getId()).collect(Collectors.toList()); + } + + private GenericObjectWrapper getObject(String type, long id) { String singularType = singularType(type); @@ -176,11 +193,34 @@ public boolean connect(String host, int port, String username, String password) } + public long setUser(String userName) { + long id = -1L; + if (userName != null && !userName.trim().isEmpty() && !userName.equalsIgnoreCase("all")) { + if (user != null) id = user.getId(); + ExperimenterWrapper newUser = user; + try { + newUser = client.getUser(userName); + } catch (ExecutionException | ServiceException | AccessException e) { + IJ.error("Could not retrieve user: " + userName); + } + if (newUser == null) { + IJ.log("Could not retrieve user: " + userName); + } else { + user = newUser; + id = user.getId(); + } + } else { + user = null; + } + return id; + } + + public String downloadImage(long imageId, String path) { List files = new ArrayList<>(); try { files = client.getImage(imageId).download(client, path); - } catch (ServiceException | AccessException | OMEROServerError | ExecutionException e) { + } catch (ServiceException | AccessException | OMEROServerError | ExecutionException | NoSuchElementException e) { IJ.error("Could not download image: " + e.getMessage()); } return files.stream().map(File::toString).collect(Collectors.joining(",")); @@ -243,7 +283,7 @@ public void deleteFile(long fileId) { public void addToTable(String tableName, ResultsTable results, Long imageId, List ijRois, String property) { TableWrapper table = tables.get(tableName); - if(results == null) { + if (results == null) { IJ.error("Results table does not exist."); } else { try { @@ -261,29 +301,36 @@ public void addToTable(String tableName, ResultsTable results, Long imageId, Lis } - public void saveTableAsTXT(String tableName, String path) { + public void saveTableAsFile(String tableName, String path, String delim) { TableWrapper table = tables.get(tableName); Object[][] data = table.getData(); int nColumns = table.getColumnCount(); StringBuilder sb = new StringBuilder(); File f = new File(path); + + String sep = delim == null ? "\",\"" : String.format("\"%s\"", delim); try (PrintWriter stream = new PrintWriter(f)) { + sb.append("\""); for (int i = 0; i < nColumns; i++) { sb.append(table.getColumnName(i)); - if (i != (nColumns - 1)) { - sb.append("\t"); + if (i != nColumns - 1) { + sb.append(sep); } } - sb.append("\n"); + sb.append("\"\n"); for (int i = 0; i < table.getRowCount(); i++) { + sb.append("\""); for (int j = 0; j < nColumns; j++) { Object value = data[j][i]; + if(value.getClass().getSimpleName().endsWith("Data")) { + value = value.toString().replaceAll(".*\\(id=(\\d+)\\)", "$1"); + } sb.append(value); - if (i != table.getRowCount() - 1) { - sb.append("\t"); + if (j != nColumns - 1) { + sb.append(sep); } } - sb.append("\n"); + sb.append("\"\n"); } stream.write(sb.toString()); } catch (FileNotFoundException e) { @@ -292,15 +339,15 @@ public void saveTableAsTXT(String tableName, String path) { } - public void saveTable(String name, String type, long id) { + public void saveTable(String tableName, String type, long id) { GenericRepositoryObjectWrapper object = getRepositoryObject(type, id); if (object != null) { - TableWrapper table = tables.get(name); + TableWrapper table = tables.get(tableName); if (table != null) { String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); String newName; - if (name == null || name.equals("")) newName = timestamp + "_" + table.getName(); - else newName = timestamp + "_" + name; + if (tableName == null || tableName.equals("")) newName = timestamp + "_" + table.getName(); + else newName = timestamp + "_" + tableName; table.setName(newName); try { object.addTable(client, table); @@ -377,19 +424,19 @@ public String list(String type) { switch (singularType) { case PROJECT: List projects = client.getProjects(); - results = listToIDs(projects); + results = listToIDs(filterUser(projects)); break; case DATASET: List datasets = client.getDatasets(); - results = listToIDs(datasets); + results = listToIDs(filterUser(datasets)); break; case IMAGE: List images = client.getImages(); - results = listToIDs(images); + results = listToIDs(filterUser(images)); break; case TAG: List tags = client.getTags(); - results = listToIDs(tags); + results = listToIDs(filterUser(tags)); break; default: IJ.error(INVALID + ": " + type + ". Possible values are: projects, datasets, images or tags."); @@ -409,25 +456,25 @@ public String list(String type, String name) { switch (singularType) { case PROJECT: List projects = client.getProjects(name); - results = listToIDs(projects); + results = listToIDs(filterUser(projects)); break; case DATASET: List datasets = client.getDatasets(name); - results = listToIDs(datasets); + results = listToIDs(filterUser(datasets)); break; case IMAGE: List images = client.getImages(name); - results = listToIDs(images); + results = listToIDs(filterUser(images)); break; case TAG: List tags = client.getTags(name); - results = listToIDs(tags); + results = listToIDs(filterUser(tags)); break; default: IJ.error(INVALID + ": " + type + ". Possible values are: projects, datasets, images or tags."); } } catch (ServiceException | AccessException | OMEROServerError | ExecutionException e) { - IJ.error("Could not retrieve project name: " + e.getMessage()); + IJ.error(String.format("Could not retrieve %s with name \"%s\": %s", type, name, e.getMessage())); } return results; } @@ -445,15 +492,15 @@ public String list(String type, String parent, long id) { switch (singularType) { case DATASET: List datasets = project.getDatasets(); - results = listToIDs(datasets); + results = listToIDs(filterUser(datasets)); break; case IMAGE: List images = project.getImages(client); - results = listToIDs(images); + results = listToIDs(filterUser(images)); break; case TAG: List tags = project.getTags(client); - results = listToIDs(tags); + results = listToIDs(filterUser(tags)); break; default: IJ.error(INVALID + ": " + type + ". Possible values are: datasets, images or tags."); @@ -464,11 +511,11 @@ public String list(String type, String parent, long id) { switch (singularType) { case IMAGE: List images = dataset.getImages(client); - results = listToIDs(images); + results = listToIDs(filterUser(images)); break; case TAG: List tags = dataset.getTags(client); - results = listToIDs(tags); + results = listToIDs(filterUser(tags)); break; default: IJ.error(INVALID + ": " + type + ". Possible values are: images or tags."); @@ -476,7 +523,7 @@ public String list(String type, String parent, long id) { break; case IMAGE: if (singularType.equals(TAG)) { - results = listToIDs(client.getImage(id).getTags(client)); + results = listToIDs(filterUser(client.getImage(id).getTags(client))); } else { IJ.error("Invalid type: " + type + ". Only possible value is: tags."); } @@ -486,15 +533,15 @@ public String list(String type, String parent, long id) { switch (singularType) { case PROJECT: List projects = tag.getProjects(client); - results = listToIDs(projects); + results = listToIDs(filterUser(projects)); break; case DATASET: List datasets = tag.getDatasets(client); - results = listToIDs(datasets); + results = listToIDs(filterUser(datasets)); break; case IMAGE: List images = tag.getImages(client); - results = listToIDs(images); + results = listToIDs(filterUser(images)); break; default: IJ.error(INVALID + ": " + type + ". Possible values are: projects, datasets or images."); @@ -514,7 +561,7 @@ public void sudo(String user) { switched = client; try { client = switched.sudoGetUser(user); - } catch (ServiceException | AccessException | ExecutionException e) { + } catch (ServiceException | AccessException | ExecutionException | NullPointerException e) { IJ.error("Could not switch user: " + e.getMessage()); } } @@ -538,8 +585,8 @@ public void link(String type1, long id1, String type2, long id2) { map.put(t1, id1); map.put(t2, id2); - Long datasetId = map.get(DATASET); Long projectId = map.get(PROJECT); + Long datasetId = map.get(DATASET); Long imageId = map.get(IMAGE); Long tagId = map.get(TAG); @@ -551,7 +598,7 @@ public void link(String type1, long id1, String type2, long id2) { GenericRepositoryObjectWrapper object = getRepositoryObject(obj, map.get(obj)); if (object != null) object.addTag(client, tagId); } else if (datasetId == null || (projectId == null && imageId == null)) { - IJ.error("Cannot link " + type1 + " and " + type2); + IJ.error(String.format("Cannot link %s and %s", type1, type2)); } else { // Or link dataset to image or project DatasetWrapper dataset = client.getDataset(datasetId); if (projectId != null) { @@ -561,7 +608,46 @@ public void link(String type1, long id1, String type2, long id2) { } } } catch (ServiceException | AccessException | ExecutionException e) { - IJ.error("Could not link " + type2 + " and " + type1 + ": " + e.getMessage()); + IJ.error(String.format("Cannot link %s and %s: %s", type1, type2, e.getMessage())); + } + } + + + public void unlink(String type1, long id1, String type2, long id2) { + String t1 = singularType(type1); + String t2 = singularType(type2); + + Map map = new HashMap<>(2); + map.put(t1, id1); + map.put(t2, id2); + + Long projectId = map.get(PROJECT); + Long datasetId = map.get(DATASET); + Long imageId = map.get(IMAGE); + Long tagId = map.get(TAG); + + try { + // Unlink tag from repository object + if (t1.equals(TAG) ^ t2.equals(TAG)) { + String obj = t1.equals(TAG) ? t2 : t1; + + GenericRepositoryObjectWrapper object = getRepositoryObject(obj, map.get(obj)); + if (object != null) object.unlink(client, client.getTag(tagId)); + } else if (datasetId == null || (projectId == null && imageId == null)) { + IJ.error(String.format("Cannot unlink %s and %s", type1, type2)); + } else { // Or unlink dataset from image or project + DatasetWrapper dataset = client.getDataset(datasetId); + if (projectId != null) { + client.getProject(projectId).removeDataset(client, dataset); + } else { + dataset.removeImage(client, client.getImage(imageId)); + } + } + } catch (ServiceException | AccessException | ExecutionException | OMEROServerError e) { + IJ.error(String.format("Cannot unlink %s and %s: %s", type1, type2, e.getMessage())); + } catch (InterruptedException e) { + IJ.error(String.format("Cannot unlink %s and %s: %s", type1, type2, e.getMessage())); + Thread.currentThread().interrupt(); } } @@ -570,12 +656,8 @@ public String getName(String type, long id) { String name = null; GenericObjectWrapper object = getObject(type, id); - if (object instanceof ProjectWrapper) { - name = ((ProjectWrapper) object).getName(); - } else if (object instanceof DatasetWrapper) { - name = ((DatasetWrapper) object).getName(); - } else if (object instanceof ImageWrapper) { - name = ((ImageWrapper) object).getName(); + if (object instanceof GenericRepositoryObjectWrapper) { + name = ((GenericRepositoryObjectWrapper) object).getName(); } else if (object instanceof TagAnnotationWrapper) { name = ((TagAnnotationWrapper) object).getName(); } @@ -588,60 +670,86 @@ public ImagePlus getImage(long id) { try { ImageWrapper image = client.getImage(id); imp = image.toImagePlus(client); - } catch (ServiceException | AccessException | ExecutionException e) { + } catch (ServiceException | AccessException | ExecutionException | NoSuchElementException e) { IJ.error("Could not retrieve image: " + e.getMessage()); } return imp; } - public int getROIs(long id, String property) { + public int getROIs(ImagePlus imp, long id, boolean toOverlay, String property) { List rois = new ArrayList<>(); try { ImageWrapper image = client.getImage(id); rois = image.getROIs(client); } catch (ServiceException | AccessException | ExecutionException e) { - IJ.error("Could not retrieve image with ROIs: " + e.getMessage()); + IJ.error("Could not retrieve ROIs: " + e.getMessage()); } - ImagePlus imp = IJ.getImage(); - List ijRois = ROIWrapper.toImageJ(rois, property); - RoiManager rm = RoiManager.getInstance(); - if (rm == null) rm = RoiManager.getRoiManager(); - for (Roi roi : ijRois) { - roi.setImage(imp); - rm.addRoi(roi); + if (toOverlay) { + Overlay overlay = imp.getOverlay(); + if (overlay == null) { + overlay = new Overlay(); + imp.setOverlay(overlay); + } + for (Roi roi : ijRois) { + roi.setImage(imp); + overlay.add(roi); + } + } else { + RoiManager rm = RoiManager.getInstance(); + if (rm == null) rm = RoiManager.getRoiManager(); + for (Roi roi : ijRois) { + roi.setImage(imp); + rm.addRoi(roi); + } } return ijRois.size(); } - public int saveROIs(long id, String property) { + public int saveROIs(ImagePlus imp, long id, String property) { int result = 0; try { ImageWrapper image = client.getImage(id); - ImagePlus imp = IJ.getImage(); - Overlay overlay = imp.getOverlay(); - RoiManager rm = RoiManager.getInstance(); - if (rm == null) rm = RoiManager.getRoiManager(); - - List ijRois = new ArrayList<>(); + Overlay overlay = imp.getOverlay(); if (overlay != null) { - ijRois.addAll(Arrays.asList(overlay.toArray())); + List ijRois = Arrays.asList(overlay.toArray()); + + List rois = ROIWrapper.fromImageJ(ijRois, property); + rois.forEach(roi -> roi.setImage(image)); + for (ROIWrapper roi : rois) { + image.saveROI(client, roi); + } + result += rois.size(); + overlay.clear(); + List newRois = ROIWrapper.toImageJ(rois, property); + for (Roi roi : newRois) { + roi.setImage(imp); + overlay.add(roi); + } } - ijRois.addAll(Arrays.asList(rm.getRoisAsArray())); - List rois = ROIWrapper.fromImageJ(ijRois, property); - rois.forEach(roi -> roi.setImage(image)); - for (ROIWrapper roi : rois) { - image.saveROI(client, roi); + RoiManager rm = RoiManager.getInstance(); + if (rm != null) { + List ijRois = Arrays.asList(rm.getRoisAsArray()); + + List rois = ROIWrapper.fromImageJ(ijRois, property); + rois.forEach(roi -> roi.setImage(image)); + for (ROIWrapper roi : rois) { + image.saveROI(client, roi); + } + result += rois.size(); + rm.reset(); + List newRois = ROIWrapper.toImageJ(rois, property); + for (Roi roi : newRois) { + roi.setImage(imp); + rm.addRoi(roi); + } } - result = rois.size(); - rm.reset(); - this.getROIs(id, property); } catch (ServiceException | AccessException | ExecutionException e) { IJ.error("Could not save ROIs to image: " + e.getMessage()); } @@ -692,6 +800,10 @@ public String handleExtension(String name, Object[] args) { results = String.valueOf(client.getCurrentGroupId()); break; + case "listForUser": + results = String.valueOf(setUser((String) args[0])); + break; + case "importImage": long datasetId = ((Double) args[0]).longValue(); path = ((String) args[1]); @@ -745,10 +857,11 @@ public String handleExtension(String name, Object[] args) { addToTable(tableName, rt, imageId, ijRois, property); break; - case "saveTableAsTXT": + case "saveTableAsFile": tableName = (String) args[0]; path = (String) args[1]; - saveTableAsTXT(tableName, path); + String delimiter = (String) args[2]; + saveTableAsFile(tableName, path, delimiter); break; case "saveTable": @@ -792,6 +905,14 @@ public String handleExtension(String name, Object[] args) { link(type1, id1, type2, id2); break; + case "unlink": + type1 = (String) args[0]; + id1 = ((Double) args[1]).longValue(); + type2 = (String) args[2]; + id2 = ((Double) args[3]).longValue(); + unlink(type1, id1, type2, id2); + break; + case "getName": type = (String) args[0]; id = ((Double) args[1]).longValue(); @@ -808,15 +929,17 @@ public String handleExtension(String name, Object[] args) { case "getROIs": id = ((Double) args[0]).longValue(); - property = (String) args[1]; - int nIJRois = getROIs(id, property); + Double ov = (Double) args[1]; + boolean toOverlay = ov != null && ov != 0; + property = (String) args[2]; + int nIJRois = getROIs(IJ.getImage(), id, toOverlay, property); results = String.valueOf(nIJRois); break; case "saveROIs": id = ((Double) args[0]).longValue(); property = (String) args[1]; - int nROIs = saveROIs(id, property); + int nROIs = saveROIs(IJ.getImage(), id, property); results = String.valueOf(nROIs); break; diff --git a/src/test/java/fr/igred/ij/plugin/OMEROExtensionErrorTest.java b/src/test/java/fr/igred/ij/plugin/OMEROExtensionErrorTest.java new file mode 100644 index 0000000..cd1c85d --- /dev/null +++ b/src/test/java/fr/igred/ij/plugin/OMEROExtensionErrorTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2021 GReD + * + * 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., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package fr.igred.ij.plugin; + + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +@ExtendWith(TestResultLogger.class) +class OMEROExtensionErrorTest { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private OMEROMacroExtension ext; + + + @BeforeEach + public void setUp() { + ext = new OMEROMacroExtension(); + Object[] args = {"omero", 4064d, "testUser", "password"}; + ext.handleExtension("connectToOMERO", args); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + + @AfterEach + public void tearDown() { + ext.handleExtension("disconnect", null); + System.setOut(originalOut); + System.setErr(originalErr); + } + + + @Test + void testRun() { + ext.run(""); + String expected = "Cannot install extensions from outside a macro!"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testNoSuchMethod() { + ext.handleExtension("hello", null); + String expected = "No such method: hello"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testConnectionError() { + ext.handleExtension("disconnect", null); + Object[] args = {"omero", 4064d, "omero", "password"}; + ext.handleExtension("connectToOMERO", args); + String expected = "Could not connect: Cannot connect to OMERO"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testDownloadImageError() { + Object[] args = {-1.0d, "."}; + ext.handleExtension("downloadImage", args); + String expected = "Could not download image: Image -1 doesn't exist in this context"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testImportImageError() throws IOException { + String path = "./8bit-unsigned&pixelType=uint8&sizeZ=3&sizeC=5&sizeT=7&sizeX=512&sizeY=512.fake"; + File f = new File(path); + if (!f.createNewFile()) { + System.err.println("\"" + f.getCanonicalPath() + "\" could not be created."); + fail(); + } + + Object[] args1 = {-1.0d, path}; + ext.handleExtension("importImage", args1); + String expected = "Could not import image: Dataset -1 doesn't exist in this context"; + assertEquals(expected, outContent.toString().trim()); + Files.deleteIfExists(f.toPath()); + } + + + @Test + void testGetImageError() { + Object[] args = {-1.0d}; + ext.handleExtension("getImage", args); + String expected = "Could not retrieve image: Image -1 doesn't exist in this context"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testListForUserError() { + Object[] args = {"hello"}; + ext.handleExtension("listForUser", args); + String expected = "Could not retrieve user: hello"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testSudoError() { + Object[] args = {"roger"}; + ext.handleExtension("sudo", args); + String expected = "Could not switch user: null"; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testEndSudoError() { + ext.handleExtension("endSudo", null); + String expected = "No sudo has been used before."; + assertEquals(expected, outContent.toString().trim()); + } + + + @Test + void testListInvalidArgs() { + Object[] args = {"dataset", null, 2.0}; + ext.handleExtension("list", args); + String expected = "Second argument should not be null."; + assertEquals(expected, outContent.toString().trim()); + } + + + @ParameterizedTest + @CsvSource(delimiter = ';', value = {"tag;hello;2", + "hello;project;2", + "hello;dataset;2", + "hello;image;2", + "hello;tag;2", + "hello;TestDatasetImport;", + "hello;;",}) + void testListInvalidType(String type1, String type2, Double id) { + Object[] args = {type1, type2, id}; + String output = ext.handleExtension("list", args); + String error = outContent.toString().trim(); + assertTrue(output.isEmpty()); + assertTrue(error.startsWith("Invalid type: hello. ")); + } + + + @ParameterizedTest + @ValueSource(strings = {"link", "unlink"}) + void testLinkUnlinkInvalidType(String function) { + Object[] args = {"tag", 2.0, "hello", 2.0}; + ext.handleExtension(function, args); + String expected = "Invalid type: hello."; + assertEquals(expected, outContent.toString().trim()); + } + + + @ParameterizedTest + @ValueSource(strings = {"link", "unlink"}) + void testCannotLinkOrUnlink(String function) { + Object[] args = {"hello", 2.0, "world", 2.0}; + ext.handleExtension(function, args); + String expected = String.format("Cannot %s hello and world", function); + assertEquals(expected, outContent.toString().trim()); + } + +} diff --git a/src/test/java/fr/igred/ij/plugin/OMEROExtensionTest.java b/src/test/java/fr/igred/ij/plugin/OMEROExtensionTest.java index 844e9fe..47ca97a 100644 --- a/src/test/java/fr/igred/ij/plugin/OMEROExtensionTest.java +++ b/src/test/java/fr/igred/ij/plugin/OMEROExtensionTest.java @@ -19,10 +19,13 @@ import fr.igred.omero.Client; import fr.igred.omero.annotations.TableWrapper; import ij.ImagePlus; +import ij.gui.Overlay; +import ij.gui.Roi; import ij.measure.ResultsTable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -31,14 +34,21 @@ import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; +import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +@ExtendWith(TestResultLogger.class) class OMEROExtensionTest { private OMEROMacroExtension ext; @@ -58,6 +68,32 @@ public void tearDown() { } + @Test + void testSwitchGroup() { + Object[] args = {4.0d}; + Object[] args2 = {3.0d}; + String result = ext.handleExtension("switchGroup", args); + String result2 = ext.handleExtension("switchGroup", args2); + assertEquals(args[0], Double.parseDouble(result)); + assertEquals(args2[0], Double.parseDouble(result2)); + } + + + @ParameterizedTest + @CsvSource(delimiter = ';', value = {"testUser;2", + "all;-1", + "'';-1", + ";-1",}) + void testListForUser(String user, double output) { + Object[] args = {user}; + String result = ext.handleExtension("listForUser", args); + Object[] args2 = {"projects", null, null}; + String result2 = ext.handleExtension("list", args2); + assertEquals(output, Double.parseDouble(result)); + assertEquals("1,2", result2); + } + + @ParameterizedTest @CsvSource(delimiter = ';', value = {"list;projects;1,2", "list;project;1,2", @@ -70,7 +106,7 @@ public void tearDown() { void testListAll(String extension, String type, String output) { Object[] args = {type, null, null}; String result = ext.handleExtension(extension, args); - assertEquals(output, result); + assertEquals(output, result, String.format("\"%s\" failed for: %s", extension, type)); } @@ -86,7 +122,7 @@ void testListAll(String extension, String type, String output) { void testListByName(String extension, String type, String name, String output) { Object[] args = {type, name, null}; String result = ext.handleExtension(extension, args); - assertEquals(output, result); + assertEquals(output, result, String.format("\"%s\" failed for: %s,%s", extension, type, name)); } @@ -101,12 +137,14 @@ void testListByName(String extension, String type, String name, String output) { "list;project;tags;1.0;2", "list;datasets;tag;1.0;3", "list;dataset;tags;1.0;3", + "list;images;project;1.0;1,2,3", + "list;image;projects;1.0;1,2,3", "list;images;tag;1.0;1,2,4", "list;image;tags;1.0;1,2,4",}) void testListFrom(String extension, String type, String parent, double id, String output) { Object[] args = {type, parent, id}; String result = ext.handleExtension(extension, args); - assertEquals(output, result); + assertEquals(output, result, String.format("\"%s\" failed for: %s,%s,%f", extension, type, parent, id)); } @@ -122,7 +160,7 @@ void testListFrom(String extension, String type, String parent, double id, Strin void testGetName(String extension, String type, double id, String output) { Object[] args = {type, id}; String result = ext.handleExtension(extension, args); - assertEquals(output, result); + assertEquals(output, result, String.format("\"%s\" failed for: %s,%f", extension, type, id)); } @@ -161,6 +199,31 @@ void testCreateAndLinkTag() { } + @ParameterizedTest + @CsvSource(delimiter = ';', value = {"tag;1.0;project;2.0", + "tag;1.0;dataset;3.0", + "dataset;3.0;project;2.0", + "image;1.0;dataset;1.0",}) + void testUnlinkThenLink(String type1, double id1, String type2, double id2) { + Object[] listArgs = {type1, type2, id2}; + Object[] args = {type1, id1, type2, id2}; + + String res = ext.handleExtension("list", listArgs); + int size = res.isEmpty() ? 0 : res.split(",").length; + + ext.handleExtension("unlink", args); + res = ext.handleExtension("list", listArgs); + int newSize = res.isEmpty() ? 0 : res.split(",").length; + + ext.handleExtension("link", args); + res = ext.handleExtension("list", listArgs); + int checkSize = res.isEmpty() ? 0 : res.split(",").length; + + assertEquals(size - 1, newSize, String.format("Unlinking failed for: %s,%f,%s,%f", type1, id1, type2, id2)); + assertEquals(size, checkSize, String.format("Linking failed for: %s,%f,%s,%f", type1, id1, type2, id2)); + } + + @ParameterizedTest @CsvSource(delimiter = ';', value = {"project;1.0;./test1.txt", "projects;1.0;./test2.txt", @@ -201,6 +264,24 @@ void testGetImage() { } + @Test + void testSaveAndGetROIs() { + ImagePlus imp = ext.getImage(1L); + Overlay overlay = new Overlay(); + Roi roi = new Roi(25, 30, 70, 50); + roi.setImage(imp); + overlay.add(roi); + imp.setOverlay(overlay); + int savedROIs = ext.saveROIs(imp, 1L, ""); + overlay.clear(); + int loadedROIs = ext.getROIs(imp, 1L, true, ""); + + assertEquals(1, savedROIs); + assertEquals(1, loadedROIs); + assertEquals(1, imp.getOverlay().size()); + } + + @Test void testImportImage() throws IOException { String path = "./8bit-unsigned&pixelType=uint8&sizeZ=3&sizeC=5&sizeT=7&sizeX=512&sizeY=512.fake"; @@ -222,6 +303,21 @@ void testImportImage() throws IOException { Object[] args3 = {"image", "dataset", 2.0D}; listIds = ext.handleExtension("list", args3); assertEquals("", listIds); + Files.deleteIfExists(f.toPath()); + } + + + @Test + void testDownloadImage() throws IOException { + Object[] args = {1.0d, "."}; + String results = ext.handleExtension("downloadImage", args); + String[] paths = results.split(","); + List files = Arrays.stream(paths).map(File::new).collect(Collectors.toList()); + assertEquals(2, paths.length); + assertTrue(files.get(0).exists()); + assertTrue(files.get(1).exists()); + Files.deleteIfExists(files.get(0).toPath()); + Files.deleteIfExists(files.get(1).toPath()); } @@ -258,11 +354,10 @@ void testTable() throws Exception { rt1.incrementCounter(); rt1.setLabel("test", 0); rt1.setValue("Size", 0, 25.0); - rt1.show("test"); ResultsTable rt2 = new ResultsTable(); rt2.incrementCounter(); - rt2.setLabel("test", 0); + rt2.setLabel("test2", 0); rt2.setValue("Size", 0, 50.0); ext.addToTable("test_table", rt1, 1L, new ArrayList<>(0), null); @@ -271,6 +366,19 @@ void testTable() throws Exception { Object[] args2 = {"test_table", "dataset", 1.0d}; ext.handleExtension("saveTable", args2); + File textFile = new File("./test.txt"); + Object[] args3 = {"test_table", textFile.getCanonicalPath(), null}; + ext.handleExtension("saveTableAsFile", args3); + List expected = Arrays.asList("\"Image\",\"Label\",\"Size\"", + "\"1\",\"test\",\"25.0\"", + "\"1\",\"test2\",\"50.0\""); + List actual = Files.readAllLines(textFile.toPath()); + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + assertEquals(expected.get(i), actual.get(i)); + } + Files.deleteIfExists(textFile.toPath()); + Client client = new Client(); client.connect("omero", 4064, "testUser", "password".toCharArray()); List tables = client.getDataset(1L).getTables(client); diff --git a/src/test/java/fr/igred/ij/plugin/TestResultLogger.java b/src/test/java/fr/igred/ij/plugin/TestResultLogger.java new file mode 100644 index 0000000..1ac79c4 --- /dev/null +++ b/src/test/java/fr/igred/ij/plugin/TestResultLogger.java @@ -0,0 +1,132 @@ +package fr.igred.ij.plugin; + + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; + +import java.util.Optional; +import java.util.logging.Logger; + + +public class TestResultLogger implements TestWatcher, BeforeTestExecutionCallback, BeforeAllCallback { + + public static final String ANSI_RESET = "\u001B[0m"; + public static final String ANSI_RED = "\u001B[31m"; + public static final String ANSI_GREEN = "\u001B[32m"; + public static final String ANSI_YELLOW = "\u001B[33m"; + public static final String ANSI_BLUE = "\u001B[34m"; + + private static final String FORMAT = "[%-25s]\t%-40s\t%s (%.3f s)"; + + private long start = System.currentTimeMillis(); + + private Logger logger; + + + /** + * Callback that is invoked once before all tests in the current container. + * + * @param context the current extension context; never {@code null} + */ + @Override + public void beforeAll(ExtensionContext context) { + System.setProperty("java.util.logging.SimpleFormatter.format", "[%1$tF %1$tT] [%4$-7s] %5$s %n"); + final String klass = context.getTestClass() + .orElse(context.getRequiredTestClass()) + .getName(); + logger = Logger.getLogger(klass); + } + + + /** + * Callback that is invoked immediately before an individual test is executed but after any user-defined + * setup methods have been executed for that test. + * + * @param context the current extension context; never {@code null} + */ + @Override + public void beforeTestExecution(ExtensionContext context) { + start = System.currentTimeMillis(); + } + + + /** + * Invoked after a disabled test has been skipped. + * + *

The default implementation does nothing. Concrete implementations can + * override this method as appropriate. + * + * @param context the current extension context; never {@code null} + * @param reason the reason the test is disabled; never {@code null} but + */ + @Override + public void testDisabled(ExtensionContext context, Optional reason) { + float time = (float) (System.currentTimeMillis() - start) / 1000; + String testName = context.getTestMethod().orElse(context.getRequiredTestMethod()).getName(); + String displayName = context.getDisplayName(); + String status = ANSI_BLUE + "DISABLED" + ANSI_RESET; + logger.info(String.format(FORMAT, testName, displayName, status, time)); + } + + + /** + * Invoked after a test has completed successfully. + * + *

The default implementation does nothing. Concrete implementations can + * override this method as appropriate. + * + * @param context the current extension context; never {@code null} + */ + @Override + public void testSuccessful(ExtensionContext context) { + float time = (float) (System.currentTimeMillis() - start) / 1000; + String testName = context.getTestMethod().orElse(context.getRequiredTestMethod()).getName(); + String displayName = context.getDisplayName(); + displayName = displayName.equals(testName + "()") ? "" : displayName; + String status = ANSI_GREEN + "SUCCEEDED" + ANSI_RESET; + logger.info(String.format(FORMAT, testName, displayName, status, time)); + } + + + /** + * Invoked after a test has been aborted. + * + *

The default implementation does nothing. Concrete implementations can + * override this method as appropriate. + * + * @param context the current extension context; never {@code null} + * @param cause the throwable responsible for the test being aborted; may be {@code null} + */ + @Override + public void testAborted(ExtensionContext context, Throwable cause) { + float time = (float) (System.currentTimeMillis() - start) / 1000; + String testName = context.getTestMethod().orElse(context.getRequiredTestMethod()).getName(); + String displayName = context.getDisplayName(); + displayName = displayName.equals(testName + "()") ? "" : displayName; + String status = ANSI_YELLOW + "ABORTED" + ANSI_RESET; + logger.info(String.format(FORMAT, testName, displayName, status, time)); + } + + + /** + * Invoked after a test has failed. + * + *

The default implementation does nothing. Concrete implementations can + * override this method as appropriate. + * + * @param context the current extension context; never {@code null} + * @param cause the throwable that caused test failure; may be {@code null} + */ + @Override + public void testFailed(ExtensionContext context, Throwable cause) { + float time = (float) (System.currentTimeMillis() - start) / 1000; + String testName = context.getTestMethod().orElse(context.getRequiredTestMethod()).getName(); + String displayName = context.getDisplayName(); + displayName = displayName.equals(testName + "()") ? "" : displayName; + String status = ANSI_RED + "FAILED" + ANSI_RESET; + logger.info(String.format(FORMAT, testName, displayName, status, time)); + } + +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index d1cfd9e..49ba851 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -1,10 +1,12 @@ - [%d{yyyy-MM-dd} %d{HH:mm:ss.SSS}] [%-5p] [%-27.27logger{0}] %m%n + [%d{yyyy-MM-dd} %d{HH:mm:ss}] [%-5p] [%-27.27logger{0}] %m%n + + \ No newline at end of file