From: Mateusz Warowny Date: Tue, 18 Jul 2023 14:45:55 +0000 (+0200) Subject: Merge branch 'mmw/JAL-4199-web-services-testing' into development/Release_2_12_Branch X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=80d446099e8d1a83a73e64870c8e08903a003b00;hp=8e3a12f81d347540eac7c75f5f4201ec6ad60bb6;p=jalview.git Merge branch 'mmw/JAL-4199-web-services-testing' into development/Release_2_12_Branch --- diff --git a/j11lib/slivka-client.jar b/j11lib/slivka-client.jar index 49ab4fc..11b2f93 100644 Binary files a/j11lib/slivka-client.jar and b/j11lib/slivka-client.jar differ diff --git a/src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java b/src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java index 999951a..5c98cbe 100644 --- a/src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java +++ b/src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java @@ -65,8 +65,7 @@ public class SlivkaAnnotationServiceInstance extends SlivkaWSInstance implements RemoteFile featFile = null; try { - var slivkaJob = client.getJob(jobId.getJobId()); - Collection files = slivkaJob.getResults(); + Collection files = client.fetchFilesList(jobId.getJobId()); for (RemoteFile f : files) { if (f.getMediaType().equals("application/jalview-annotations")) diff --git a/src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java b/src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java index 374d2eb..337073e 100644 --- a/src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java +++ b/src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java @@ -40,8 +40,7 @@ public class SlivkaMsaServiceInstance extends SlivkaWSInstance implements Multip Collection files; try { - var slivkaJob = client.getJob(jobId.getJobId()); - files = slivkaJob.getResults(); + files = client.fetchFilesList(jobId.getJobId()); for (RemoteFile f : files) { if (f.getMediaType().equals("application/clustal")) diff --git a/src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java b/src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java index d21d5d1..104560a 100644 --- a/src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java +++ b/src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java @@ -94,7 +94,7 @@ public class SlivkaWSDiscoverer implements WSDiscovererI for (String url : getServiceUrls()) { - SlivkaClient client = new SlivkaClient(url); + SlivkaClient client = SlivkaClient.newInstance(url); List services; try @@ -220,7 +220,7 @@ public class SlivkaWSDiscoverer implements WSDiscovererI { try { - List services = new SlivkaClient(url).getServices(); + List services = SlivkaClient.newInstance(url).getServices(); return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK; } catch (IOException | org.json.JSONException e) { diff --git a/src/jalview/ws/slivkaws/SlivkaWSInstance.java b/src/jalview/ws/slivkaws/SlivkaWSInstance.java index 613c702..d3701ac 100644 --- a/src/jalview/ws/slivkaws/SlivkaWSInstance.java +++ b/src/jalview/ws/slivkaws/SlivkaWSInstance.java @@ -30,7 +30,7 @@ import javajs.http.ClientProtocolException; import java.util.Collection; import uk.ac.dundee.compbio.slivkaclient.Job; -import uk.ac.dundee.compbio.slivkaclient.JobRequest; +import uk.ac.dundee.compbio.slivkaclient.RequestValues; import uk.ac.dundee.compbio.slivkaclient.Parameter; import uk.ac.dundee.compbio.slivkaclient.RemoteFile; import uk.ac.dundee.compbio.slivkaclient.SlivkaClient; @@ -75,7 +75,7 @@ public abstract class SlivkaWSInstance extends ServiceWithParameters WsParamSetI preset, List args) throws Throwable { var parameters = service.getParameters(); - var request = new JobRequest(); + var request = new RequestValues(); for (Parameter param : parameters) { if (param instanceof Parameter.FileParameter) @@ -126,8 +126,8 @@ public abstract class SlivkaWSInstance extends ServiceWithParameters } } } - var job = service.submitJob(request); - return new JobId(service.getName(), service.getName(), job.getId()); + var jobId = client.submitJob(service, request); + return new JobId(service.getName(), service.getName(), jobId); } @Override @@ -135,8 +135,7 @@ public abstract class SlivkaWSInstance extends ServiceWithParameters { try { - var slivkaJob = client.getJob(job.getJobId()); - job.setState(stateMap.get(slivkaJob.getStatus())); + job.setState(stateMap.get(client.fetchJobStatus(job.getJobId()))); } catch (IOException e) { throw new IOError(e); @@ -146,8 +145,7 @@ public abstract class SlivkaWSInstance extends ServiceWithParameters @Override public final boolean updateJobProgress(WsJob job) throws IOException { - var slivkaJob = client.getJob(job.getJobId()); - Collection files = slivkaJob.getResults(); + Collection files = client.fetchFilesList(job.getJobId()); RemoteFile logFile=null; for (RemoteFile f : files) { @@ -161,7 +159,7 @@ public abstract class SlivkaWSInstance extends ServiceWithParameters if (logFile!=null) { ByteArrayOutputStream output = new ByteArrayOutputStream(); - logFile.writeTo(output); + client.writeFileTo(logFile, output); if (output.size() > job.getNextChunk()) { newContent = true; @@ -185,7 +183,7 @@ public abstract class SlivkaWSInstance extends ServiceWithParameters if (errLogFile!=null) { ByteArrayOutputStream output = new ByteArrayOutputStream(); - errLogFile.writeTo(output); + client.writeFileTo(errLogFile, output); if (output.size() > 0) { newContent = true; diff --git a/src/jalview/ws2/actions/AbstractPollableTask.java b/src/jalview/ws2/actions/AbstractPollableTask.java index e692c68..b61711c 100644 --- a/src/jalview/ws2/actions/AbstractPollableTask.java +++ b/src/jalview/ws2/actions/AbstractPollableTask.java @@ -49,7 +49,7 @@ public abstract class AbstractPollableTask implements Task private final TaskEventSupport eventHandler; - protected JobStatus taskStatus = null; + protected JobStatus taskStatus = JobStatus.CREATED; private Future future = null; diff --git a/src/jalview/ws2/actions/BaseJob.java b/src/jalview/ws2/actions/BaseJob.java index 945c7b0..8376d20 100644 --- a/src/jalview/ws2/actions/BaseJob.java +++ b/src/jalview/ws2/actions/BaseJob.java @@ -31,7 +31,7 @@ public abstract class BaseJob implements JobI protected final List inputSeqs; - protected JobStatus status = null; + protected JobStatus status = JobStatus.CREATED; protected String log = ""; diff --git a/src/jalview/ws2/api/JobStatus.java b/src/jalview/ws2/api/JobStatus.java index 3341a69..8957343 100644 --- a/src/jalview/ws2/api/JobStatus.java +++ b/src/jalview/ws2/api/JobStatus.java @@ -2,6 +2,8 @@ package jalview.ws2.api; public enum JobStatus { + /** Initial status before the job is started. */ + CREATED, /** Job has invalid inputs and cannot be started. */ INVALID, /** Job is created and ready for submission. */ @@ -39,6 +41,7 @@ public enum JobStatus case CANCELLED: case SERVER_ERROR: return true; + case CREATED: case READY: case SUBMITTED: case QUEUED: @@ -60,6 +63,7 @@ public enum JobStatus JobStatus.UNKNOWN, // unknown prevents successful completion but not // running or failure JobStatus.READY, + JobStatus.CREATED, JobStatus.SUBMITTED, JobStatus.QUEUED, JobStatus.RUNNING, diff --git a/src/jalview/ws2/client/slivka/SlivkaWSClient.java b/src/jalview/ws2/client/slivka/SlivkaWSClient.java index bef502b..7dcdae1 100644 --- a/src/jalview/ws2/client/slivka/SlivkaWSClient.java +++ b/src/jalview/ws2/client/slivka/SlivkaWSClient.java @@ -46,10 +46,10 @@ public class SlivkaWSClient implements WebServiceClientI final SlivkaClient client; - SlivkaWSClient(SlivkaService service) + SlivkaWSClient(SlivkaClient client, SlivkaService service) { this.service = service; - this.client = service.getClient(); + this.client = client; } @Override @@ -72,7 +72,7 @@ public class SlivkaWSClient implements WebServiceClientI public WebServiceJobHandle submit(List sequences, List args, Credentials credentials) throws IOException { - var request = new uk.ac.dundee.compbio.slivkaclient.JobRequest(); + var request = new uk.ac.dundee.compbio.slivkaclient.RequestValues(); for (Parameter param : service.getParameters()) { // TODO: restrict input sequences parameter name to "sequences" @@ -131,8 +131,8 @@ public class SlivkaWSClient implements WebServiceClientI } } } - var job = service.submitJob(request); - return createJobHandle(job.getId()); + var jobId = client.submitJob(service, request); + return createJobHandle(jobId); } protected WebServiceJobHandle createJobHandle(String jobId) @@ -145,8 +145,7 @@ public class SlivkaWSClient implements WebServiceClientI @Override public JobStatus getStatus(WebServiceJobHandle job) throws IOException { - var slivkaJob = client.getJob(job.getJobId()); - return statusMap.getOrDefault(slivkaJob.getStatus(), JobStatus.UNKNOWN); + return statusMap.getOrDefault(client.fetchJobStatus(job.getJobId()), JobStatus.UNKNOWN); } protected static final EnumMap statusMap = new EnumMap<>(Job.Status.class); @@ -168,13 +167,12 @@ public class SlivkaWSClient implements WebServiceClientI @Override public String getLog(WebServiceJobHandle job) throws IOException { - var slivkaJob = client.getJob(job.getJobId()); - for (var f : slivkaJob.getResults()) + for (var f : client.fetchFilesList(job.getJobId())) { if (f.getLabel().equals("log")) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); - f.writeTo(stream); + client.writeFileTo(f, stream); return stream.toString("UTF-8"); } } @@ -184,13 +182,12 @@ public class SlivkaWSClient implements WebServiceClientI @Override public String getErrorLog(WebServiceJobHandle job) throws IOException { - var slivkaJob = client.getJob(job.getJobId()); - for (var f : slivkaJob.getResults()) + for (var f : client.fetchFilesList(job.getJobId())) { if (f.getLabel().equals("error-log")) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); - f.writeTo(stream); + client.writeFileTo(f, stream); return stream.toString("UTF-8"); } } @@ -210,16 +207,15 @@ class SlivkaAlignmentWSClient extends SlivkaWSClient implements AlignmentWebServiceClientI { - SlivkaAlignmentWSClient(SlivkaService service) + SlivkaAlignmentWSClient(SlivkaClient client, SlivkaService service) { - super(service); + super(client, service); } @Override public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException { - var slivkaJob = client.getJob(job.getJobId()); - for (var f : slivkaJob.getResults()) + for (var f : client.fetchFilesList(job.getJobId())) { // TODO: restrict result file label to "alignment" FileFormat format; @@ -245,9 +241,9 @@ class SlivkaAlignmentWSClient extends SlivkaWSClient class SlivkaAnnotationWSClient extends SlivkaWSClient implements AnnotationWebServiceClientI { - SlivkaAnnotationWSClient(SlivkaService service) + SlivkaAnnotationWSClient(SlivkaClient client, SlivkaService service) { - super(service); + super(client, service); } @Override @@ -255,10 +251,9 @@ class SlivkaAnnotationWSClient extends SlivkaWSClient List sequences, Map colours, Map filters) throws IOException { - var slivkaJob = client.getJob(job.getJobId()); var aln = new Alignment(sequences.toArray(new SequenceI[sequences.size()])); boolean featPresent = false, annotPresent = false; - for (var f : slivkaJob.getResults()) + for (var f : client.fetchFilesList(job.getJobId())) { // TODO: restrict file label to "annotations" or "features" var match = mediaTypePattern.matcher(f.getMediaType()); diff --git a/src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java b/src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java index 70e1c94..51e27ad 100644 --- a/src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java +++ b/src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java @@ -2,10 +2,12 @@ package jalview.ws2.client.slivka; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import jalview.bin.Cache; import jalview.bin.Console; @@ -36,15 +38,18 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer private static SlivkaWSDiscoverer instance = null; private static ParamManager paramManager = null; + + private final Function clientFactory; - private SlivkaWSDiscoverer() + SlivkaWSDiscoverer(Function clientFactory) { + this.clientFactory = clientFactory; } public static SlivkaWSDiscoverer getInstance() { if (instance == null) - instance = new SlivkaWSDiscoverer(); + instance = new SlivkaWSDiscoverer(SlivkaClient::newInstance); return instance; } @@ -58,8 +63,12 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer { try { - List services = new SlivkaClient(url.toString()).getServices(); + List services = clientFactory.apply(url.toURI()).getServices(); return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK; + } catch (URISyntaxException e) + { + Console.error("invalid URL " + url, e); + return STATUS_INVALID; } catch (IOException e) { Console.error("slivka could not retrieve services from " + url, e); @@ -86,7 +95,7 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer SlivkaClient slivkaClient; try { - slivkaClient = new SlivkaClient(url.toURI()); + slivkaClient = clientFactory.apply(url.toURI()); } catch (URISyntaxException e) { throw new MalformedURLException(e.getMessage()); @@ -97,14 +106,14 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer if (serviceClass == SERVICE_CLASS_MSA) { var wsb = WebService. newBuilder(); - initServiceBuilder(slivkaService, wsb); + initServiceBuilder(slivkaClient, slivkaService, wsb); wsb.category("Alignment"); wsb.interactive(false); wsb.actionClass(AlignmentAction.class); var msaService = wsb.build(); boolean canRealign = msaService.getName().contains("lustal"); - var client = new SlivkaAlignmentWSClient(slivkaService); + var client = new SlivkaAlignmentWSClient(slivkaClient, slivkaService); var actionBuilder = AlignmentAction.newBuilder(client); actionBuilder.name("Alignment"); actionBuilder.webService(msaService); @@ -124,12 +133,12 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer else if (serviceClass == SERVICE_CLASS_PROT_SEQ_ANALYSIS) { var wsb = WebService. newBuilder(); - initServiceBuilder(slivkaService, wsb); + initServiceBuilder(slivkaClient, slivkaService, wsb); wsb.category("Protein Disorder"); wsb.interactive(false); wsb.actionClass(AnnotationAction.class); var psaService = wsb.build(); - var client = new SlivkaAnnotationWSClient(slivkaService); + var client = new SlivkaAnnotationWSClient(slivkaClient, slivkaService); var actionBuilder = AnnotationAction.newBuilder(client); actionBuilder.webService(psaService); actionBuilder.name("Analysis"); @@ -139,12 +148,12 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer else if (serviceClass == SERVICE_CLASS_CONSERVATION) { var wsb = WebService. newBuilder(); - initServiceBuilder(slivkaService, wsb); + initServiceBuilder(slivkaClient, slivkaService, wsb); wsb.category("Conservation"); wsb.interactive(true); wsb.actionClass(AnnotationAction.class); var conService = wsb.build(); - var client = new SlivkaAnnotationWSClient(slivkaService); + var client = new SlivkaAnnotationWSClient(slivkaClient, slivkaService); var actionBuilder = AnnotationAction.newBuilder(client); actionBuilder.webService(conService); actionBuilder.name(""); @@ -157,12 +166,12 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer else if (serviceClass == SERVICE_CLASS_RNA_SEC_STR_PRED) { var wsb = WebService. newBuilder(); - initServiceBuilder(slivkaService, wsb); + initServiceBuilder(slivkaClient, slivkaService, wsb); wsb.category("Secondary Structure Prediction"); wsb.interactive(true); wsb.actionClass(AnnotationAction.class); var predService = wsb.build(); - var client = new SlivkaAnnotationWSClient(slivkaService); + var client = new SlivkaAnnotationWSClient(slivkaClient, slivkaService); var actionBuilder = AnnotationAction.newBuilder(client); actionBuilder.webService(predService); actionBuilder.name("Prediction"); @@ -183,11 +192,11 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer return allServices; } - private void initServiceBuilder(SlivkaService service, WebService.Builder wsBuilder) + private void initServiceBuilder(SlivkaClient client, SlivkaService service, WebService.Builder wsBuilder) { try { - wsBuilder.url(service.getClient().getUrl().toURL()); + wsBuilder.url(client.getUrl().toURL()); } catch (MalformedURLException e) { e.printStackTrace(); @@ -222,10 +231,9 @@ public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer for (String classifier : service.getClassifiers()) { String[] path = classifier.split("\\s*::\\s*"); - if (path.length < 3 || !path[0].equalsIgnoreCase("operation") || - !path[1].equalsIgnoreCase("analysis")) + if (path.length < 3 || !path[0].equalsIgnoreCase("operation")) continue; - // classifier is operation :: analysis :: * + // classifier is operation :: * var tail = path[path.length - 1].toLowerCase(); switch (tail) { diff --git a/test/jalview/ws2/actions/alignment/AlignmentActionTest.java b/test/jalview/ws2/actions/alignment/AlignmentActionTest.java new file mode 100644 index 0000000..5586108 --- /dev/null +++ b/test/jalview/ws2/actions/alignment/AlignmentActionTest.java @@ -0,0 +1,295 @@ +package jalview.ws2.actions.alignment; + +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.help.UnsupportedOperationException; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.mockito.ArgumentCaptor; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import jalview.datamodel.Alignment; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignViewport; +import jalview.viewmodel.AlignmentViewport; +import jalview.ws.params.ParamDatastoreI; +import jalview.ws2.actions.api.JobI; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.api.Credentials; +import jalview.ws2.api.JobStatus; +import jalview.ws2.api.WebService; +import jalview.ws2.api.WebServiceJobHandle; +import jalview.ws2.client.api.AlignmentWebServiceClientI; + +import org.mockito.hamcrest.MockitoHamcrest; +import org.mockito.internal.hamcrest.HamcrestArgumentMatcher; + +import static org.mockito.Mockito.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class AlignmentActionTest +{ + protected AlignmentWebServiceClientI mockClient; + + protected AlignmentAction.Builder actionBuilder; + + protected WebServiceJobHandle jobRef; + + @BeforeMethod + public void setupMockClient() throws IOException + { + jobRef = new WebServiceJobHandle( + "mock", "mock", "http://example.org", "00000001"); + mockClient = mock(AlignmentWebServiceClientI.class); + when(mockClient.getUrl()).thenReturn("http://example.org"); + when(mockClient.getClientName()).thenReturn("mock"); + when(mockClient.submit(anyList(), anyList(), any())).thenReturn(jobRef); + when(mockClient.getLog(jobRef)).thenReturn(""); + when(mockClient.getErrorLog(jobRef)).thenReturn(""); + doThrow(new UnsupportedOperationException()).when(mockClient).cancel(any()); + } + + @BeforeMethod(dependsOnMethods = { "setupMockClient" }) + public void setupActionBuilder() throws IOException + { + actionBuilder = AlignmentAction.newBuilder(mockClient); + actionBuilder.name("mock"); + actionBuilder.webService( + WebService. newBuilder() + .url(new URL("http://example.org")) + .clientName("mock") + .category("Alignment") + .name("mock") + .paramDatastore(mock(ParamDatastoreI.class)) + .actionClass(AlignmentAction.class) + .build()); + } + + @DataProvider + public Object[][] multipleSequencesUnalignedAndAligned() + { + return new Object[][] { + { + new Alignment(new SequenceI[] + { + new Sequence("Seq 1", "----ASTVLITOPDCMMQEGGST-"), + new Sequence("Seq 2", "-ASCGLITO------MMQEGGST-"), + new Sequence("Seq 3", "AS--TVL--OPDTMMQEL------") + }), + new Alignment(new SequenceI[] + { + new Sequence("Sequence0", "ASTV-LITOPDCMMQEGGST----"), + new Sequence("Sequence1", "ASC-GLITO---MMQEGGST----"), + new Sequence("Sequence2", "ASTV-L--OPDTMMQE--L-----") + }) + } + }; + } + + @Test(dataProvider = "multipleSequencesUnalignedAndAligned") + public void submitSequences_verifySequenceNamesUniquified( + Alignment unaligned, Alignment aligned) + throws IOException + { + var viewport = new AlignViewport(unaligned); + when(mockClient.getAlignment(jobRef)).thenReturn(aligned); + when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED); + actionBuilder.submitGaps(false); + performAction(viewport, actionBuilder.build()); + ArgumentCaptor> argument = ArgumentCaptor.forClass(List.class); + verify(mockClient).submit(argument.capture(), eq(List.of()), eq(Credentials.empty())); + assertThat(argument.getValue(), + contains(hasProperty("name", is("Sequence0")), + hasProperty("name", is("Sequence1")), + hasProperty("name", is("Sequence2")))); + } + + @Test(dataProvider = "multipleSequencesUnalignedAndAligned") + public void submitSequences_submitGapsOff_verifySequencesSubmittedWithoutGaps(Alignment unaligned, Alignment aligned) + throws IOException + { + var viewport = new AlignViewport(unaligned); + actionBuilder.submitGaps(false); + when(mockClient.getAlignment(jobRef)).thenReturn(aligned); + when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED); + performAction(viewport, actionBuilder.build()); + ArgumentCaptor> argument = ArgumentCaptor.forClass(List.class); + verify(mockClient).submit(argument.capture(), eq(List.of()), eq(Credentials.empty())); + assertThat(argument.getValue(), + contains( + matchesSequence("ASTVLITOPDCMMQEGGST"), + matchesSequence("ASCGLITOMMQEGGST"), + matchesSequence("ASTVLOPDTMMQEL"))); + } + + @Test(dataProvider = "multipleSequencesUnalignedAndAligned") + public void submitSequences_submitGapsOn_verifySequencesSubmittedWithGaps( + Alignment unaligned, Alignment aligned) + throws IOException + { + var viewport = new AlignViewport(unaligned); + actionBuilder.submitGaps(true); + when(mockClient.getAlignment(jobRef)).thenReturn(aligned); + when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED); + performAction(viewport, actionBuilder.build()); + ArgumentCaptor> argument = ArgumentCaptor.forClass(List.class); + verify(mockClient).submit(argument.capture(), eq(List.of()), eq(Credentials.empty())); + assertThat(argument.getValue(), + contains( + matchesSequence("----ASTVLITOPDCMMQEGGST-"), + matchesSequence("-ASCGLITO------MMQEGGST-"), + matchesSequence("AS--TVL--OPDTMMQEL------"))); + } + + @Test(dataProvider = "multipleSequencesUnalignedAndAligned") + public void retrieveResult_verifySequencesAligned( + Alignment unaligned, Alignment aligned) + throws IOException + { + var viewport = new AlignViewport(unaligned); + actionBuilder.submitGaps(false); + when(mockClient.getAlignment(jobRef)).thenReturn(aligned); + when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED); + var mockListener = performAction(viewport, actionBuilder.build()); + var argument = ArgumentCaptor.forClass(AlignmentResult.class); + verify(mockListener).taskCompleted(any(), argument.capture()); + var alignmentResult = argument.getValue().getAlignment(); + assertThat(alignmentResult, hasProperty("sequences", contains( + matchesSequence("ASTV-LITOPDCMMQEGGST----"), + matchesSequence("ASC-GLITO---MMQEGGST----"), + matchesSequence("ASTV-L--OPDTMMQE--L-----")))); + } + + protected static Matcher matchesSequence(String sequence) + { + return new TypeSafeMatcher() + { + @Override + public boolean matchesSafely(SequenceI obj) + { + if (!(obj instanceof SequenceI)) + return false; + var seq = (SequenceI) obj; + return seq.getSequenceAsString().equals(sequence); + } + + @Override + public void describeTo(Description description) + { + description.appendText("a sequence ").appendValue(sequence); + } + + @Override + public void describeMismatchSafely(SequenceI item, Description description) + { + description.appendText("was ").appendValue(item.getSequenceAsString()); + } + }; + } + + protected TaskEventListener performAction( + AlignmentViewport viewport, AlignmentAction action) + throws IOException + { + TaskEventListener listener = mock(TaskEventListener.class); + var latch = new CountDownLatch(1); + doAnswer(invocation -> { + latch.countDown(); + return null; + }) + .when(listener).taskCompleted(any(), any()); + action.perform(viewport, List.of(), Credentials.empty(), listener); + try + { + latch.await(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) + { + } + return listener; + } +} + +class AlignmentActionListenerNotifiedTest extends AlignmentActionTest +{ + private AlignViewport viewport; + + @BeforeMethod + public void setupViewport() + { + viewport = new AlignViewport(new Alignment(new SequenceI[] { + new Sequence("Seq 1", "----ASTVLITOPDCMMQEGGST-"), + new Sequence("Seq 2", "-ASCGLITO------MMQEGGST-"), + new Sequence("Seq 3", "AS--TVL--OPDTMMQEL------") + })); + } + + @DataProvider + public JobStatus[] jobStatuses() + { + // CREATED, INVALID and READY should not be returned by the server + return new JobStatus[] { + JobStatus.SUBMITTED, + JobStatus.QUEUED, + JobStatus.RUNNING, + JobStatus.COMPLETED, + JobStatus.FAILED, + JobStatus.CANCELLED, + JobStatus.SERVER_ERROR, + JobStatus.UNKNOWN + }; + } + + @Test + public void allJobsStarted_taskStartedCalled() + throws IOException + { + when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED); + var mockListener = performAction(viewport, actionBuilder.build()); + verify(mockListener).taskStarted(any(), anyList()); + } + + @Test + public void allJobsStarted_taskStatusChangedCalledWithReadyThenSubmitted() + throws IOException + { + when(mockClient.getStatus(jobRef)).thenReturn(JobStatus.COMPLETED); + var mockListener = performAction(viewport, actionBuilder.build()); + var inOrder = inOrder(mockListener); + inOrder.verify(mockListener).taskStatusChanged(any(), eq(JobStatus.READY)); + inOrder.verify(mockListener).taskStatusChanged(any(), eq(JobStatus.SUBMITTED)); + } + + @Test(dataProvider = "jobStatuses") + public void jobStatusChanged_taskStatusChangedCalledWithJobStatus(JobStatus status) + throws IOException + { + when(mockClient.getStatus(jobRef)) + .thenReturn(status) + .thenReturn(JobStatus.COMPLETED); + var mockListener = performAction(viewport, actionBuilder.build()); + verify(mockListener).taskStatusChanged(any(), eq(status)); + } + + @Test(dataProvider = "jobStatuses") + public void jobStatusChanged_subJobStatusChangedCalledWithJobStatus(JobStatus status) + throws IOException + { + when(mockClient.getStatus(jobRef)) + .thenReturn(status) + .thenReturn(JobStatus.COMPLETED); + var mockListener = performAction(viewport, actionBuilder.build()); + verify(mockListener).subJobStatusChanged(any(), any(), eq(status)); + } +} \ No newline at end of file diff --git a/test/jalview/ws2/client/slivka/SlivkaWSDiscovererTest.java b/test/jalview/ws2/client/slivka/SlivkaWSDiscovererTest.java index 5519156..870acb1 100644 --- a/test/jalview/ws2/client/slivka/SlivkaWSDiscovererTest.java +++ b/test/jalview/ws2/client/slivka/SlivkaWSDiscovererTest.java @@ -1,33 +1,659 @@ package jalview.ws2.client.slivka; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.hamcrest.Matcher; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import jalview.bin.Cache; +import jalview.bin.Console; +import jalview.ws.params.ValueConstrainI.ValueType; +import jalview.ws.params.simple.DoubleParameter; +import jalview.ws.params.simple.IntegerParameter; +import jalview.ws.params.simple.StringParameter; +import jalview.ws2.actions.alignment.AlignmentAction; +import jalview.ws2.actions.annotation.AnnotationAction; +import jalview.ws2.client.api.WebServiceDiscovererI; +import uk.ac.dundee.compbio.slivkaclient.Parameter; +import uk.ac.dundee.compbio.slivkaclient.SlivkaClient; +import uk.ac.dundee.compbio.slivkaclient.SlivkaService; + public class SlivkaWSDiscovererTest { - @BeforeClass - public void setupClass() throws IOException + private static final String URLS_PROPERTY_NAME = "SLIVKAHOSTURLS"; + + SlivkaClient clientMock; + + Function factoryMock; + + @BeforeClass(alwaysRun = true) + public void setupProperties() + { + Cache.loadProperties("test/jalview/ws2/client/slivka/default.jvprops"); + Console.initLogger(); + } + + @BeforeMethod + public void setupDiscoverer() throws IOException + { + clientMock = mock(SlivkaClient.class); + } + + @Test + public void getStatusForUrl_servicesReturned_statusIsOK() throws Exception + { + when(clientMock.getServices()) + .thenReturn(List.of(mock(SlivkaService.class))); + var discoverer = new SlivkaWSDiscoverer( + url -> url.toString().equals("http://example.org") ? clientMock + : null); + assertThat(discoverer.getStatusForUrl(new URL("http://example.org")), + is(WebServiceDiscovererI.STATUS_OK)); + } + + @Test + public void getStatusForUrl_noServicesReturned_statusIsNoServices() + throws Exception + { + when(clientMock.getServices()).thenReturn(List.of()); + var discoverer = new SlivkaWSDiscoverer( + url -> url.toString().equals("http://example.org") ? clientMock + : null); + assertThat(discoverer.getStatusForUrl(new URL("http://example.org")), + is(WebServiceDiscovererI.STATUS_NO_SERVICES)); + } + + @Test + public void getStatusForUrl_exceptionThrown_statusIsInvalid() + throws Exception + { + when(clientMock.getServices()).thenThrow(new IOException()); + var discoverer = new SlivkaWSDiscoverer( + url -> url.toString().equals("http://example.org") ? clientMock + : null); + assertThat(discoverer.getStatusForUrl(new URL("http://example.org")), + is(WebServiceDiscovererI.STATUS_INVALID)); + } + + @Test + public void testGetUrls_noPropEntry_defaultUrlReturned() + throws MalformedURLException { var discoverer = SlivkaWSDiscoverer.getInstance(); - + assertThat(discoverer.getUrls(), + contains(new URL("https://www.compbio.dundee.ac.uk/slivka/"))); } - + + @DataProvider + public Object[][] urlPropertyValues() throws MalformedURLException + { + return new Object[][] { + { "http://example.org/", List.of(new URL("http://example.org/")) }, + { "https://example.org/slivka/", + List.of(new URL("https://example.org/slivka/")) }, + { "https://www.compbio.dundee.ac.uk/,http://www.example.org/", + List.of(new URL("https://www.compbio.dundee.ac.uk/"), + new URL("http://www.example.org/")) }, + { "http://example.org/,", List.of(new URL("http://example.org/")) }, + { ",http://example.org", List.of(new URL("http://example.org")) }, + { "", List.of() }, + { ",", List.of() }, + { "example.org", List.of() }, + { "example.org,http://example.org", + List.of(new URL("http://example.org")) } }; + } + + @Test(dataProvider = "urlPropertyValues") + public void testGetUrls_urlsProperlyParsed(String propValue, + List expected) + { + Cache.setProperty(URLS_PROPERTY_NAME, propValue); + var discoverer = SlivkaWSDiscoverer.getInstance(); + assertThat(discoverer.getUrls(), equalTo(expected)); + } + @Test - public void testServiceFetch() throws IOException + public void testSetUrls_emptyList_propertyReset() { + Cache.setProperty(URLS_PROPERTY_NAME, "http://www.example.org"); var discoverer = SlivkaWSDiscoverer.getInstance(); - var services = discoverer.fetchServices(discoverer.getDefaultUrl()); - for (var service : services) + discoverer.setUrls(List.of()); + assertThat(Cache.getProperty(URLS_PROPERTY_NAME), is(nullValue())); + } + + @Test + public void testSetUrls_null_propertyReset() + { + Cache.setProperty(URLS_PROPERTY_NAME, "http://www.example.org"); + var discoverer = SlivkaWSDiscoverer.getInstance(); + discoverer.setUrls(null); + assertThat(Cache.getProperty(URLS_PROPERTY_NAME), is(nullValue())); + } + + @DataProvider + public Object[][] urlsList() throws MalformedURLException + { + return new Object[][] { + { List.of(new URL("http://example.org")), "http://example.org" }, + { List.of(new URL("http://example.org/")), "http://example.org/" }, + { List.of(new URL("http://example.org/slivka/")), + "http://example.org/slivka/" }, + { List.of(new URL("https://www.compbio.dundee.ac.uk/slivka/"), + new URL("http://example.org")), + "https://www.compbio.dundee.ac.uk/slivka/,http://example.org" }, }; + } + + @Test(dataProvider = "urlsList") + public void testSetUrls_urlsPropertySet(List urls, String expected) + throws MalformedURLException + { + var discoverer = SlivkaWSDiscoverer.getInstance(); + discoverer.setUrls(urls); + assertThat(Cache.getProperty(URLS_PROPERTY_NAME), equalTo(expected)); + } + + @Test + public void testFetchServices_oneService_basicDataMatches() + throws IOException + { + var service = new SlivkaService( + URI.create("http://example.org/api/services/example"), + "example", "Example name", "Example service description", + "John Smith", "1.0", "MIT License", + List.of("operation::analysis::multiple sequence alignment"), + List.of(), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices, hasSize(1)); + var webService = webServices.get(0); + assertThat(webService.getUrl(), + equalTo(new URL("http://example.org/"))); + assertThat(webService.getClientName(), equalTo("slivka")); + assertThat(webService.getName(), equalTo("Example name")); + assertThat(webService.getDescription(), + equalTo("Example service description")); + } + + @DataProvider + public String[] validMultipleSequenceAlignmentClassifiers() + { + return new String[] { + "Operation :: Analysis :: Multiple sequence alignment", + "operation :: analysis :: multiple sequence alignment", + "Operation\t::\tAnalysis\t::\tMultiple sequence alignment", + "Operation::Analysis::Multiple sequence alignment", + "Operation :: Analysis :: Multiple Sequence Alignment", + "OPERATION :: ANALYSIS :: MULTIPLE SEQUENCE ALIGNMENT", + "Operation :: Analysis :: Sequence alignment :: Multiple sequence alignment", + "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment", + "Operation :: Alignment :: Multiple sequence alignment", + "Operation :: Alignment :: Sequence alignment :: Multiple sequence alignment", + "Operation :: Comparison :: Multiple sequence alignment", + "Operation :: Comparison :: Sequence comparison :: Sequence alignment :: Multiple sequence alignment" }; + + } + + @Test(dataProvider = "validMultipleSequenceAlignmentClassifiers") + public void testFetchServices_multipleSequenceAlignmentClassifier_serviceTypeIsMSA( + String classifier) throws IOException + { + var service = new SlivkaService(URI.create("http://example.org/"), + "example", "name", "description", "author", "1.0", "MIT", + List.of(classifier), List.of(), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices, hasSize(1)); + assertThat(webServices.get(0).getCategory(), equalTo("Alignment")); + assertThat(webServices.get(0).getActionClass(), + typeCompatibleWith(AlignmentAction.class)); + } + + @DataProvider + public SlivkaService[] multipleSequenceAlignmentService() + { + return new SlivkaService[] { + new SlivkaService( + URI.create("http://example.org/"), "example", "Examaple name", + "Example description", "John Smith", "1.0", "MIT", + List.of("Operation :: Analysis :: Multiple sequence alignment"), + List.of(), List.of(), null), + new SlivkaService( + URI.create("http://example.org/api/services/muscle"), + "muscle", "MUSCLE", + "MUltiple Sequence Comparison by Log- Expectation", + "Robert C. Edgar", "3.8.31", "Public domain", + List.of("Topic :: Computational biology :: Sequence analysis", + "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment"), + List.of(), List.of(), null), + new SlivkaService( + URI.create("http://example.org/api/services/tcoffee"), + "tcoffee", "TCoffee", + "Tree-based Consistency Objective Function for Alignment Evaluation", + "Cedric Notredame", "13.41.0", "GNU GPL", + List.of("Topic :: Computational biology :: Sequence analysis", + "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment"), + List.of(), List.of(), null) }; + } + + @Test(dataProvider = "multipleSequenceAlignmentService") + public void testFetchServices_multipleSequenceAlignmentService_actionTypeIsAlignment( + SlivkaService service) throws IOException + { + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices.get(0).getCategory(), equalTo("Alignment")); + assertThat(webServices.get(0).getActionClass(), + typeCompatibleWith(AlignmentAction.class)); + } + + @Test(dataProvider = "multipleSequenceAlignmentService") + public void testFetchServices_multipleSequenceAlignmentService_serviceIsNonInteractive( + SlivkaService service) throws IOException + { + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices.get(0).isInteractive(), is(false)); + } + + @DataProvider + public SlivkaService[] clustalFamilyService() + { + return new SlivkaService[] { + new SlivkaService( + URI.create("http://example.org/api/services/clustalo"), + "clustalo", "ClustalO", + "Clustal Omega is the latest addition to the Clustal family.", + "Fabian Sievers, et al.", "1.2.4", "GNU GPL ver. 2", + List.of("Topic :: Computational biology :: Sequence analysis", + "Operation :: Analysis :: Sequence analysis :: Sequence alignment :: Multiple sequence alignment"), + List.of(), List.of(), null), + new SlivkaService( + URI.create("http://example.org/api/services/clustalw"), + "clustalw", "ClustalW", + "ClustalW is a general purpose multiple alignment program.", + "Larkin MA, et al.", "2.1", "GNU GPL ver. 3", + List.of("Topic :: Computation biology :: Sequence analysis", + "Operation :: Analysis :: Multiple sequence alignment"), + List.of(), List.of(), null), + new SlivkaService( + URI.create("http://example.org/api/services/clustalw2"), + "clustalw2", "ClustalW2", + "ClustalW is a general purpose multiple alignment program.", + "Larkin MA, et al.", "2.1", "GNU GPL ver. 3", + List.of("Topic :: Computation biology :: Sequence analysis", + "Operation :: Analysis :: Multiple sequence alignment"), + List.of(), List.of(), null), }; + } + + @Test(dataProvider = "clustalFamilyService") + public void testFetchService_clustalFamilyService_containsTwoActions( + SlivkaService service) throws IOException + { + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org")); + var actions = webServices.get(0).getActions(); + assertThat(actions, hasSize(2)); + assertThat(actions.get(0), allOf(hasProperty("name", is("Alignment")), + hasProperty("subcategory", is("Align")))); + assertThat(actions.get(1), + allOf(hasProperty("name", is("Re-alignment")), + hasProperty("subcategory", is("Realign")))); + } + + @DataProvider + public String[] validRNASecondaryStructurePredictionClassifiers() + { + return new String[] { + "Operation :: Analysis :: RNA secondary structure prediction", + "operation :: analysis :: rna secondary structure prediction", + "OPERATION :: ANALYSIS :: RNA SECONDARY STRUCTURE PREDICTION", + "Operation\t::\tAnalysis\t::\tRNA secondary structure prediction", + "Operation::Analysis::RNA secondary structure prediction", + "Operation :: Analysis :: Structure analysis :: RNA secondary structure prediction", + "Operation :: Analysis :: Structure analysis :: Nucleic acid structure analysis :: RNA secondary structure analysis :: RNA secondary structure prediction", + "Operation :: Analysis :: Structure analysis :: Nucleic acid structure analysis :: Nucleic acid structure prediction :: RNA secondary structure prediction", + "Operation :: Analysis :: Sequence analysis :: Nucleic acid sequence analysis :: Nucleic acid feature detection :: RNA secondary structure prediction", + "Operation :: Prediction and recognition :: RNA secondary structure prediction", + "Operation :: Prediction and recognition :: Nucleic acid feature detection :: RNA secondary structure prediction", + "Operation :: Prediction and recignition :: Nucleic acid structure prediction :: RNA secondary structure prediction", }; + } + + @DataProvider + public Iterator RNASecondaryStructurePredictionService() + { + var services = new ArrayList<>(); + for (var classifier : validRNASecondaryStructurePredictionClassifiers()) + { + services.add(new SlivkaService(URI.create("http://example.org/"), + "example", "name", "description", "author", "1.0", "MIT", + List.of(classifier), List.of(), List.of(), null)); + } + return services.iterator(); + } + + @Test(dataProvider = "RNASecondaryStructurePredictionService") + public void testFetchServices_RNASecStrPredClassifier_serviceTypeIsRNASecStrPred( + SlivkaService service) throws IOException + { + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices, hasSize(1)); + assertThat(webServices.get(0).getCategory(), + equalTo("Secondary Structure Prediction")); + assertThat(webServices.get(0).getActionClass(), + typeCompatibleWith(AnnotationAction.class)); + } + + @DataProvider + public String[] validConservationAnalysisClassifiers() + { + return new String[] { + "Operation :: Analysis :: Sequence alignment analysis (conservation)", + "Operation::Analysis::Sequence alignment analysis (conservation)", + "Operation\t::\tAnalysis\t::\tSequence alignment analysis (conservation)", + "Operation :: Analysis :: Sequence analysis :: Sequence alignment analysis (conservation)", + "Operation :: Analysis :: Sequence analysis :: Sequence alignment analysis :: Sequence alignment analysis (conservation)", }; + } + + @DataProvider + public Iterator ConservationAnalysisService() + { + var services = new ArrayList<>(); + for (var classifier : validConservationAnalysisClassifiers()) { - System.out.format("Service(%s>%s @%s)%n", service.getCategory(), - service.getName(), service.getUrl()); - var datastore = service.getParamDatastore(); - for (var param : datastore.getServiceParameters()) - { - System.out.format(" %s :%s%n", param.getName(), param.getClass().getSimpleName()); - } + services.add(new SlivkaService(URI.create("http://example.org/"), + "example", "name", "description", "author", "1.0", "MIT", + List.of(classifier), List.of(), List.of(), null)); } + return services.iterator(); + } + + @Test(dataProvider = "validConservationAnalysisClassifiers") + public void testFetchServices_conservationAnalysisClassifier_serviceTypeIsConservation( + String classifier) throws IOException + { + var service = new SlivkaService(URI.create("http://example.org/"), + "example", "name", "description", "author", "1.0", "MIT", + List.of(classifier), List.of(), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices, hasSize(1)); + assertThat(webServices.get(0).getCategory(), equalTo("Conservation")); + assertThat(webServices.get(0).getActionClass(), + typeCompatibleWith(AnnotationAction.class)); + } + + @DataProvider + public Object[] validProteinSequenceAnalysisClassifiers() + { + return new Object[] { + "Operation :: Analysis :: Sequence analysis :: Protein sequence analysis", }; + } + + @Test(dataProvider = "validProteinSequenceAnalysisClassifiers") + public void testFetchServices_proteinSequenceAnalysisClassifier_serviceTypeIsProtSeqAnalysis( + String classifier) throws IOException + { + var service = new SlivkaService(URI.create("http://example.org/"), + "example", "name", "description", "author", "1.0", "MIT", + List.of(classifier), List.of(), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices, hasSize(1)); + assertThat(webServices.get(0).getCategory(), + equalTo("Protein Disorder")); + assertThat(webServices.get(0).getActionClass(), + typeCompatibleWith(AnnotationAction.class)); + } + + @DataProvider + public Object[] validProteinSecondaryStructurePredictionClassifiers() + { + return new Object[] { + "Operation ;: Analysis :: Protein secondary structure prediction", + "Operation :: Analysis :: Structure analysis :: Protein structure analysis :: Protein secondary structure analysis :: Protein secondary structure prediction", + "Operation :: Analysis :: Sequence analysis :: Protein sequence analysis :: Protein feature detection :: Protein secondary structure prediction", + "Operation :: Analysis :: Sequence analysis :: Protein sequence analysis :: Protein secondary structure prediction", + "Operation :: Prediction and recognition :: Protein secondary structure prediction", + "Operation :: Prediction and recognition :: Protein feature detection :: Protein secondary structure prediction", }; + } + + @Test( + enabled = false, // sec. str. pred. not implemented for slivka + dataProvider = "validProteinSecondaryStructurePredictionClassifiers") + public void testFetchServices_proteinSecStrPredClassifier_serviceTypeIsProtSecStrPred( + String classifier) throws IOException + { + var service = new SlivkaService(URI.create("http://example.org/"), + "example", "name", "description", "author", "1.0", "MIT", + List.of(classifier), List.of(), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + assertThat(webServices, hasSize(1)); + assertThat(webServices.get(0).getCategory(), + equalTo("Protein Disorder")); + assertThat(webServices.get(0).getActionClass(), + typeCompatibleWith(AnnotationAction.class)); + } + + @DataProvider + public SlivkaService[] unrecognisedService() + { + return new SlivkaService[] { + new SlivkaService(URI.create("http://example.org/"), "example", + "Example name", "Example description", "John Smith", + "1.0.0", "Apache License, version 2.0", + List.of("This :: Classifier :: Does not exist"), List.of(), + List.of(), null) }; + } + + @Test(dataProvider = "unrecognisedService") + public void testFetchServices_unrecognisedService_noServiceDiscovered( + SlivkaService service) throws IOException + { + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org")); + assertThat(webServices, hasSize(0)); + } + + @DataProvider + public Object[] serviceParameterAndMappedClass() + { + return new Object[][] { + { + new Parameter.IntegerParameter("param", "Parameter", "Description", + true, false, null, Map.of(), null, null), + IntegerParameter.class + }, + { + new Parameter.DecimalParameter("param", "Parameter", + "Description", true, false, null, Map.of(), null, null, + false, false), + DoubleParameter.class + }, + { + new Parameter.TextParameter("param", "Parameter", "Description", + true, false, null, Map.of(), 0, null), + StringParameter.class + }, + { + new Parameter.FlagParameter("param", "Parameter", "Description", + true, false, null, Map.of()), + StringParameter.class + }, + { + new Parameter.ChoiceParameter("param", "Parameter", "Description", + true, false, null, Map.of(), List.of()), + StringParameter.class + }, + }; + } + + @Test(dataProvider = "serviceParameterAndMappedClass") + public void testServiceParameter_slivkaParameterMappedToJalviewParameter( + Parameter slivkaParameter, Class expectedClass) + throws IOException + { + var service = new SlivkaService(URI.create("http://example.org"), + "example", "name", "description", "author", "1.0", + "MIT License", + List.of("Operation :: Analysis :: Multiple sequence alignment"), + List.of(slivkaParameter), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org")); + var paramDatastore = webServices.get(0).getParamDatastore(); + var arguments = paramDatastore.getServiceParameters(); + assertThat(arguments.get(0), instanceOf(expectedClass)); + } + + @DataProvider + public Object[][] serviceParametersAndPropertyMatcher() + { + return new Object[][] { + { + new Parameter.IntegerParameter("param1", "Parameter 1", + "Description of parameter 1", true, false, null, Map.of(), + null, null), + allOf( + hasProperty("name", equalTo("param1")), + hasProperty("label", equalTo("Parameter 1")), + hasProperty("description", equalTo("Description of parameter 1")), + hasProperty("required", is(true)), + hasProperty("value", nullValue())) + }, + { + new Parameter.IntegerParameter("param2", null, null, true, false, + null, Map.of(), null, null), + allOf( + hasProperty("name", equalTo("param2")), + hasProperty("label", equalTo("param2")), + hasProperty("description", nullValue()), + hasProperty("required", is(true)), + hasProperty("value", nullValue())) + }, + { + new Parameter.IntegerParameter("param3", "Parameter 3", "", false, + false, 12, Map.of(), null, null), + allOf( + hasProperty("name", equalTo("param3")), + hasProperty("label", equalTo("Parameter 3")), + hasProperty("description", equalTo("")), + hasProperty("required", is(false)), + hasProperty("value", equalTo("12"))) + }, + }; + } + + @Test(dataProvider = "serviceParametersAndInfoMatcher") + public void testServiceParameters_testBasicParameterProperties( + Parameter parameter, Matcher matcher) throws IOException + { + var service = new SlivkaService(URI.create("http://example.org/"), + "example", "Example name", "Example description", "John Smith", + "1.0", "MIT", + List.of("Operation :: Analysis :: Multiple sequence alignment"), + List.of(parameter), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + var paramDatastore = webServices.get(0).getParamDatastore(); + var arguments = paramDatastore.getServiceParameters(); + assertThat(arguments.get(0), matcher); + } + + @DataProvider + public Object[][] integerParametersAndPropertyMatcher() + { + return new Object[][] { + { + new Parameter.IntegerParameter("param", null, null, true, false, + null, Map.of(), null, null), + hasProperty("validValue", hasProperty("type", is(ValueType.Integer))) + }, + { + new Parameter.IntegerParameter("param", null, null, true, false, + null, Map.of(), null, null), + hasProperty("validValue", allOf( + hasProperty("min", nullValue()), + hasProperty("max", nullValue()))), + }, + { + new Parameter.IntegerParameter("param", null, null, true, false, + null, Map.of(), -12, 42), + hasProperty("validValue", allOf( + hasProperty("min", is(-12)), + hasProperty("max", is(42)))) + }, + }; + } + + @Test(dataProvider = "integerParametersAndPropertyMatcher") + public void testServiceParameters_testIntegerProperties( + Parameter parameter, Matcher matcher) throws IOException + { + var service = new SlivkaService(URI.create("http://example.org"), + "example", "Example name", "Example description", "John Smith", + "1.0", "MIT", + List.of("Operation :: Analysis :: Multiple Sequence Alignment"), + List.of(parameter), List.of(), null); + when(clientMock.getServices()).thenReturn(List.of(service)); + when(clientMock.getUrl()).thenReturn(URI.create("http://example.org/")); + var discoverer = new SlivkaWSDiscoverer(url -> clientMock); + var webServices = discoverer + .fetchServices(new URL("http://example.org/")); + var paramDatastore = webServices.get(0).getParamDatastore(); + var arguments = paramDatastore.getServiceParameters(); + assertThat(arguments.get(0), matcher); } } diff --git a/test/jalview/ws2/client/slivka/default.jvprops b/test/jalview/ws2/client/slivka/default.jvprops new file mode 100644 index 0000000..190ca63 --- /dev/null +++ b/test/jalview/ws2/client/slivka/default.jvprops @@ -0,0 +1,2 @@ +#---JalviewX Properties File--- +#Wed Jun 07 18:01:12 CET 2023 diff --git a/utils/jalviewjs/libjs/slivka-client-site.zip b/utils/jalviewjs/libjs/slivka-client-site.zip index 2e64029..c0b582d 100644 Binary files a/utils/jalviewjs/libjs/slivka-client-site.zip and b/utils/jalviewjs/libjs/slivka-client-site.zip differ diff --git a/utils/testnglibs/hamcrest-2.2-sources.jar b/utils/testnglibs/hamcrest-2.2-sources.jar new file mode 100644 index 0000000..6124211 Binary files /dev/null and b/utils/testnglibs/hamcrest-2.2-sources.jar differ diff --git a/utils/testnglibs/hamcrest-2.2.jar b/utils/testnglibs/hamcrest-2.2.jar new file mode 100644 index 0000000..7106578 Binary files /dev/null and b/utils/testnglibs/hamcrest-2.2.jar differ