diff --git a/armadillo/src/main/java/org/molgenis/armadillo/metadata/ProfileConfig.java b/armadillo/src/main/java/org/molgenis/armadillo/metadata/ProfileConfig.java index 8236da05f..a429188d6 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/metadata/ProfileConfig.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/metadata/ProfileConfig.java @@ -78,6 +78,7 @@ public EnvironmentConfigProps toEnvironmentConfigProps() { props.setName(getName()); props.setHost(getHost()); props.setPort(getPort()); + props.setImage(getImage()); return props; } } diff --git a/armadillo/src/main/java/org/molgenis/armadillo/profile/DockerService.java b/armadillo/src/main/java/org/molgenis/armadillo/profile/DockerService.java index 47dd4a815..81fafe851 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/profile/DockerService.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/profile/DockerService.java @@ -95,7 +95,7 @@ public Map getAllProfileStatuses() { */ String asContainerName(String profileName) { if (!inContainer) { - LOG.warn("NO ".repeat(100) + " " + profileName); + LOG.warn("Profile not running in docker container: " + profileName); return profileName; } @@ -104,7 +104,7 @@ String asContainerName(String profileName) { return profileName; } - LOG.warn("YES ".repeat(100) + " " + profileName); + LOG.warn("Profile running in docker container: " + profileName); return containerPrefix + profileName + "-1"; } @@ -148,12 +148,13 @@ public void startProfile(String profileName) { startContainer(containerName); } - private void installImage(ProfileConfig profileConfig) { + void installImage(ProfileConfig profileConfig) { if (profileConfig.getImage() == null) { throw new MissingImageException(profileConfig.getImage()); } - int imageExposed = 8085; + // if rock is in the image name, it's rock + int imageExposed = profileConfig.getImage().contains("rock") ? 8085 : 6311; ExposedPort exposed = ExposedPort.tcp(imageExposed); Ports portBindings = new Ports(); portBindings.bind(exposed, Ports.Binding.bindPort(profileConfig.getPort())); diff --git a/armadillo/src/test/java/org/molgenis/armadillo/controller/ArmadilloUtilsTest.java b/armadillo/src/test/java/org/molgenis/armadillo/controller/ArmadilloUtilsTest.java index 65005a50e..8b8591554 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/controller/ArmadilloUtilsTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/controller/ArmadilloUtilsTest.java @@ -5,14 +5,13 @@ import static org.molgenis.armadillo.controller.ArmadilloUtils.createRawResponse; import java.nio.charset.Charset; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.molgenis.r.RServerResult; import org.molgenis.r.exceptions.RExecutionException; -import org.molgenis.r.rock.RockResult; +import org.molgenis.r.rserve.RserveResult; import org.rosuda.REngine.REXPRaw; @ExtendWith(MockitoExtension.class) @@ -27,18 +26,11 @@ void testSerializeCommand() { assertEquals("try(base::serialize({meanDS(D$age}, NULL))", serializedCommand); } - @Disabled @Test void testCreateRawResponse() { byte[] bytes = {0x01, 0x02, 0x03}; REXPRaw rexpDouble = new REXPRaw(bytes); - // rexpDouble renders to String "org.rosuda.REngine.REXPRaw@3a3f96ab[3]" - byte[] actual = createRawResponse(new RockResult(rexpDouble)); - StringBuilder sb = new StringBuilder(); - for (byte b : actual) { - sb.append((char) b); - } - String str = sb.toString(); + byte[] actual = createRawResponse(new RserveResult(rexpDouble)); assertArrayEquals(bytes, actual); } diff --git a/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java b/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java index 1d3c5da66..1751d9603 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/controller/DataControllerTest.java @@ -24,7 +24,6 @@ import java.security.Principal; import java.util.*; import java.util.concurrent.CompletableFuture; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -45,6 +44,7 @@ import org.molgenis.armadillo.storage.ArmadilloStorageService; import org.molgenis.r.model.RPackage; import org.molgenis.r.rock.RockResult; +import org.molgenis.r.rserve.RserveResult; import org.obiba.datashield.core.DSEnvironment; import org.obiba.datashield.core.DSMethod; import org.obiba.datashield.core.impl.DefaultDSMethod; @@ -288,7 +288,6 @@ void getAggregateMethods() throws Exception { Map.of("sessionId", sessionId, "roles", List.of("ROLE_USER")))); } - @Disabled @Test @WithMockUser void testGetLastResultNoResult() throws Exception { @@ -297,13 +296,12 @@ void testGetLastResultNoResult() throws Exception { mockMvc.perform(asyncDispatch(result)).andExpect(status().isNotFound()); } - @Disabled @Test @WithMockUser void testGetLastResult() throws Exception { byte[] bytes = {0x0, 0x1, 0x2}; when(commands.getLastExecution()) - .thenReturn(Optional.of(completedFuture(new RockResult(new REXPRaw(bytes))))); + .thenReturn(Optional.of(completedFuture(new RserveResult(new REXPRaw(bytes))))); MvcResult result = mockMvc.perform(get("/lastresult").accept(APPLICATION_OCTET_STREAM)).andReturn(); diff --git a/armadillo/src/test/java/org/molgenis/armadillo/metadata/ProfileConfigTest.java b/armadillo/src/test/java/org/molgenis/armadillo/metadata/ProfileConfigTest.java new file mode 100644 index 000000000..ff5c5f322 --- /dev/null +++ b/armadillo/src/test/java/org/molgenis/armadillo/metadata/ProfileConfigTest.java @@ -0,0 +1,47 @@ +package org.molgenis.armadillo.metadata; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.molgenis.armadillo.metadata.ProfileConfig.create; + +import java.util.HashMap; +import java.util.HashSet; +import org.junit.jupiter.api.Test; +import org.molgenis.r.config.EnvironmentConfigProps; + +public class ProfileConfigTest { + + @Test + public void testToEnvironmentConfigProps() { + String name = "myName"; + String img = "myImage"; + String host = "localhost"; + int port = 6311; + ProfileConfig config = + create(name, img, host, port, new HashSet<>(), new HashSet<>(), new HashMap<>()); + EnvironmentConfigProps actual = config.toEnvironmentConfigProps(); + assertEquals(img, actual.getImage()); + } + + @Test + public void testToEnvironmentConfigPropsDoesNotThrowErrorWhenImageNull() { + ProfileConfig config = + create( + "myName", null, "localhost", 6311, new HashSet<>(), new HashSet<>(), new HashMap<>()); + assertDoesNotThrow(config::toEnvironmentConfigProps); + } + + @Test + public void testCreateEmptyHost() { + ProfileConfig config = + create("myName", null, null, 6311, new HashSet<>(), new HashSet<>(), new HashMap<>()); + assertEquals("localhost", config.getHost()); + } + + @Test + public void testCreateEmptyOptions() { + ProfileConfig config = + create("myName", null, null, 6311, new HashSet<>(), new HashSet<>(), null); + assertEquals("java.util.ImmutableCollections$MapN", config.getOptions().getClass().getName()); + } +} diff --git a/armadillo/src/test/java/org/molgenis/armadillo/config/DockerServiceTest.java b/armadillo/src/test/java/org/molgenis/armadillo/profile/DockerServiceTest.java similarity index 87% rename from armadillo/src/test/java/org/molgenis/armadillo/config/DockerServiceTest.java rename to armadillo/src/test/java/org/molgenis/armadillo/profile/DockerServiceTest.java index c99a0147e..d1a636391 100644 --- a/armadillo/src/test/java/org/molgenis/armadillo/config/DockerServiceTest.java +++ b/armadillo/src/test/java/org/molgenis/armadillo/profile/DockerServiceTest.java @@ -1,9 +1,8 @@ -package org.molgenis.armadillo.config; +package org.molgenis.armadillo.profile; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -29,8 +28,6 @@ import org.molgenis.armadillo.metadata.ProfileConfig; import org.molgenis.armadillo.metadata.ProfileService; import org.molgenis.armadillo.metadata.ProfileStatus; -import org.molgenis.armadillo.profile.ContainerInfo; -import org.molgenis.armadillo.profile.DockerService; @ExtendWith(MockitoExtension.class) class DockerServiceTest { @@ -125,6 +122,23 @@ void testStartProfileNoImage() { assertThrows(MissingImageException.class, () -> dockerService.startProfile("default")); } + @Test + void testInstallImageNull() { + ProfileConfig profileConfig = mock(ProfileConfig.class); + when(profileConfig.getImage()).thenReturn(null); + assertThrows(MissingImageException.class, () -> dockerService.installImage(profileConfig)); + } + + @Test + void testInstallImage() { + ProfileConfig profileConfig = mock(ProfileConfig.class); + String image = "datashield/rock-something-something:latest"; + when(profileConfig.getImage()).thenReturn(image); + when(profileConfig.getPort()).thenReturn(6311); + assertDoesNotThrow(() -> dockerService.installImage(profileConfig)); + verify(dockerClient).createContainerCmd(image); + } + @SuppressWarnings("ConstantConditions") @Test void testStartProfile() { diff --git a/docker/ci/application.yml b/docker/ci/application.yml index b5f781398..b56c854cd 100644 --- a/docker/ci/application.yml +++ b/docker/ci/application.yml @@ -40,13 +40,17 @@ armadillo: datashield: # the seed can only be 9 digits seed: 342325352 - - name: rock - image: datashield/rock-base:latest - host: rock - port: 8085 + - name: xenon-vanilla + image: datashield/armadillo-rserver_caravan-xenon:latest + host: xenon-vanilla + port: 6012 package-whitelist: - dsBase - resourcer + - dsMediation + - dsMTLBase + - dsSurvival + - dsOmics function-blacklist: [ ] options: datashield: diff --git a/docker/ci/docker-compose.yml b/docker/ci/docker-compose.yml index edcf6a5da..1f2831f1d 100644 --- a/docker/ci/docker-compose.yml +++ b/docker/ci/docker-compose.yml @@ -27,5 +27,3 @@ services: xenon: hostname: xenon image: datashield/rock-dolomite-xenon:latest - environment: - DEBUG: "TRUE" diff --git a/r/src/main/java/org/molgenis/r/RServerConnectionFactory.java b/r/src/main/java/org/molgenis/r/RServerConnectionFactory.java index 5b85bd3fc..058e63fac 100644 --- a/r/src/main/java/org/molgenis/r/RServerConnectionFactory.java +++ b/r/src/main/java/org/molgenis/r/RServerConnectionFactory.java @@ -2,8 +2,10 @@ import java.io.IOException; import java.net.*; +import java.util.Objects; import org.molgenis.r.config.EnvironmentConfigProps; import org.molgenis.r.rock.RockConnectionFactory; +import org.molgenis.r.rserve.RserveConnectionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,24 +48,45 @@ RockStatusCode doHead(String uri) { } } + String getMessageFromStatus(RockStatusCode statusCode, String url) { + if (statusCode == RockStatusCode.SERVER_DOWN) { + return ("Container for '" + url + "' is down"); + } else if (statusCode == RockStatusCode.SERVER_NOT_READY) { + return ("Container for '" + url + "' is not ready"); + } else if (statusCode == RockStatusCode.OK) { + return ("Container for '" + url + "' is running"); + } else if (statusCode == RockStatusCode.UNEXPECTED_RESPONSE_CODE) { + return ("Unexpected response code on " + url); + } else if (statusCode == RockStatusCode.UNEXPECTED_RESPONSE) { + return ("Unexpected response on " + url); + } else if (statusCode == RockStatusCode.UNEXPECTED_URL) { + return ("MalformedURLException on " + url); + } else { + return ""; + } + } + + Boolean isWarningStatus(RockStatusCode rockStatusCode) { + return rockStatusCode == RockStatusCode.SERVER_DOWN + || rockStatusCode == RockStatusCode.SERVER_NOT_READY + || rockStatusCode == RockStatusCode.UNEXPECTED_URL; + } + @Override public RServerConnection tryCreateConnection() { - String url = "http://" + environment.getHost() + ":" + environment.getPort(); - RockStatusCode rockStatus = doHead(url); - if (rockStatus == RockStatusCode.SERVER_DOWN) { - logger.warn("Container for '" + url + "' is down"); - } else if (rockStatus == RockStatusCode.SERVER_NOT_READY) { - logger.warn("Container for '" + url + "' is not ready"); - } else if (rockStatus == RockStatusCode.OK) { - logger.info("Container for '" + url + "' is running"); - } else if (rockStatus == RockStatusCode.UNEXPECTED_RESPONSE_CODE) { - logger.info("Unexpected response code on " + url); - } else if (rockStatus == RockStatusCode.UNEXPECTED_RESPONSE) { - logger.info("Unexpected response on " + url); - } else if (rockStatus == RockStatusCode.UNEXPECTED_URL) { - logger.warn("MalformedURLException on " + url); + if (environment.getImage().contains("rock")) { + String url = "http://" + environment.getHost() + ":" + environment.getPort(); + RockStatusCode rockStatus = doHead(url); + String statusMessage = getMessageFromStatus(rockStatus, url); + if (isWarningStatus(rockStatus)) { + logger.warn(statusMessage); + } else if (!Objects.equals(statusMessage, "")) { + logger.info(statusMessage); + } + return new RockConnectionFactory(environment).tryCreateConnection(); + } else { + return new RserveConnectionFactory(environment).tryCreateConnection(); } - return new RockConnectionFactory(environment).tryCreateConnection(); } } @@ -80,8 +103,4 @@ enum RockStatusCode { RockStatusCode(int code) { this.code = code; } - - public int getCode() { - return this.code; - } } diff --git a/r/src/main/java/org/molgenis/r/config/EnvironmentConfigProps.java b/r/src/main/java/org/molgenis/r/config/EnvironmentConfigProps.java index d8771793e..f38116813 100644 --- a/r/src/main/java/org/molgenis/r/config/EnvironmentConfigProps.java +++ b/r/src/main/java/org/molgenis/r/config/EnvironmentConfigProps.java @@ -6,6 +6,7 @@ public class EnvironmentConfigProps { @NotEmpty private String name; @NotEmpty private String host = "localhost"; + private String image = ""; @Positive private int port = 6311; public String getHost() { @@ -31,4 +32,12 @@ public String getName() { public void setName(String name) { this.name = name; } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } } diff --git a/r/src/main/java/org/molgenis/r/rserve/RserveConnection.java b/r/src/main/java/org/molgenis/r/rserve/RserveConnection.java new file mode 100644 index 000000000..6e5912999 --- /dev/null +++ b/r/src/main/java/org/molgenis/r/rserve/RserveConnection.java @@ -0,0 +1,102 @@ +package org.molgenis.r.rserve; + +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.apache.commons.io.IOUtils; +import org.molgenis.r.RServerConnection; +import org.molgenis.r.RServerException; +import org.molgenis.r.RServerResult; +import org.molgenis.r.exceptions.RExecutionException; +import org.rosuda.REngine.REXP; +import org.rosuda.REngine.REXPMismatchException; +import org.rosuda.REngine.Rserve.RConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Direct connection with Rserve, through its java API. */ +public class RserveConnection implements RServerConnection { + + public static final int RFILE_BUFFER_SIZE = 65536; + + private static final Logger logger = LoggerFactory.getLogger(RserveConnection.class); + + private final RConnection connection; + + public RserveConnection(RConnection connection) { + this.connection = connection; + } + + @Override + public RServerResult eval(String expr, boolean serialized) throws RServerException { + if (serialized) + return new RserveResult(evalREXP(format("try(base::serialize({%s}, NULL))", expr))); + else return new RserveResult(evalREXP(format("try({%s})", expr))); + } + + @Override + public void writeFile(String fileName, InputStream in) throws RServerException { + Stopwatch sw = Stopwatch.createStarted(); + try (OutputStream os = connection.createFile(fileName); + BufferedOutputStream bos = new BufferedOutputStream(os, RFILE_BUFFER_SIZE)) { + long size = IOUtils.copyLarge(in, bos); + if (logger.isDebugEnabled()) { + var elapsed = sw.elapsed(TimeUnit.MICROSECONDS); + logger.debug( + "Copied {} in {}ms [{} MB/s]", + byteCountToDisplaySize(size), + elapsed / 1000, + format("%.03f", size * 1.0 / elapsed)); + } + } catch (IOException e) { + throw new RserveException(e); + } + } + + @Override + public void readFile(String fileName, Consumer inputStreamConsumer) + throws RServerException { + try (InputStream is = connection.openFile(".RData")) { + inputStreamConsumer.accept(is); + } catch (IOException e) { + throw new RserveException(e); + } + } + + @Override + public boolean close() { + return connection.close(); + } + + @VisibleForTesting + public RConnection getConnection() { + return connection; + } + + // + // Private methods + // + + REXP evalREXP(String expr) throws RServerException { + try { + REXP rexp = connection.eval(expr); + if (rexp.inherits("try-error")) { + throw new RExecutionException( + stream(rexp.asStrings()).map(String::trim).collect(joining("; "))); + } + return rexp; + } catch (org.rosuda.REngine.Rserve.RserveException | REXPMismatchException e) { + throw new RserveException(e); + } + } +} diff --git a/r/src/main/java/org/molgenis/r/rserve/RserveConnectionFactory.java b/r/src/main/java/org/molgenis/r/rserve/RserveConnectionFactory.java new file mode 100644 index 000000000..1e26ca524 --- /dev/null +++ b/r/src/main/java/org/molgenis/r/rserve/RserveConnectionFactory.java @@ -0,0 +1,57 @@ +package org.molgenis.r.rserve; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import org.molgenis.r.RConnectionVendorFactory; +import org.molgenis.r.RServerConnection; +import org.molgenis.r.config.EnvironmentConfigProps; +import org.molgenis.r.exceptions.ConnectionCreationFailedException; +import org.rosuda.REngine.Rserve.RConnection; +import org.rosuda.REngine.Rserve.RserveException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RserveConnectionFactory implements RConnectionVendorFactory { + + private static final Logger logger = LoggerFactory.getLogger(RserveConnectionFactory.class); + + private final EnvironmentConfigProps environment; + + public RserveConnectionFactory(EnvironmentConfigProps environment) { + this.environment = requireNonNull(environment); + } + + @Override + public RServerConnection tryCreateConnection() { + if (logger.isDebugEnabled()) { + logger.debug( + format( + "Trying to connect to instance: [ %s ] on [ %s ]", + environment.getHost(), environment.getPort())); + } + try { + RConnection rConnection = newConnection(environment.getHost(), environment.getPort()); + if (logger.isDebugEnabled()) { + logger.debug( + format( + "Connected to instance: [ %s ] on [ %s ]", + environment.getHost(), environment.getPort())); + } + return new RserveConnection(rConnection); + } catch (RserveException ex) { + throw new ConnectionCreationFailedException(ex); + } + } + + @VisibleForTesting + public RConnection newConnection(String host, int port) throws RserveException { + return new RConnection(host, port); + } + + @Override + public String toString() { + return "RConnectionFactoryImpl{" + "environment=" + environment.getName() + '}'; + } +} diff --git a/r/src/main/java/org/molgenis/r/rserve/RserveException.java b/r/src/main/java/org/molgenis/r/rserve/RserveException.java new file mode 100644 index 000000000..b8cbd598c --- /dev/null +++ b/r/src/main/java/org/molgenis/r/rserve/RserveException.java @@ -0,0 +1,15 @@ +package org.molgenis.r.rserve; + +import org.molgenis.r.RServerException; + +public class RserveException extends RServerException { + + public RserveException(Throwable throwable) { + super(throwable); + } + + @Override + public String getMessage() { + return super.getMessage(); + } +} diff --git a/r/src/main/java/org/molgenis/r/rserve/RserveNamedList.java b/r/src/main/java/org/molgenis/r/rserve/RserveNamedList.java new file mode 100644 index 000000000..9bbc1c63a --- /dev/null +++ b/r/src/main/java/org/molgenis/r/rserve/RserveNamedList.java @@ -0,0 +1,124 @@ +package org.molgenis.r.rserve; + +import static com.google.common.collect.Lists.newArrayList; +import static org.rosuda.REngine.REXPLogical.TRUE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import java.util.*; +import org.molgenis.r.RNamedList; +import org.molgenis.r.RServerResult; +import org.rosuda.REngine.REXP; +import org.rosuda.REngine.REXPMismatchException; +import org.rosuda.REngine.RList; + +@VisibleForTesting +public class RserveNamedList implements RNamedList { + + private final Map namedList = Maps.newLinkedHashMap(); + + private List names = Lists.newArrayList(); + + public RserveNamedList(REXP rexp) throws REXPMismatchException { + if (rexp != null && rexp.isList()) { + RList list = rexp.asList(); + initialize(list); + } + } + + public RserveNamedList(RList list) { + initialize(list); + } + + @Override + public List getNames() { + return names; + } + + @Override + public int size() { + return namedList.size(); + } + + @Override + public boolean isEmpty() { + return namedList.isEmpty(); + } + + @Override + public boolean containsKey(Object o) { + return namedList.containsKey(o); + } + + @Override + public boolean containsValue(Object o) { + return namedList.containsValue(o); + } + + @Override + public RServerResult get(Object o) { + return namedList.get(o); + } + + @Override + public Set keySet() { + return namedList.keySet(); + } + + @Override + public Collection values() { + return namedList.values(); + } + + @Override + public Set> entrySet() { + return namedList.entrySet(); + } + + @Override + public List> asRows() { + List> rows = newArrayList(); + if (names.isEmpty()) return rows; + + var numRows = namedList.get(names.get(0)).length(); + for (int rowNum = 0; rowNum < numRows; rowNum++) { + Map converted = new LinkedHashMap<>(); + rows.add(converted); + for (String name : names) { + RServerResult values = namedList.get(name); + getValueAtIndex(values, rowNum).ifPresent(value -> converted.put(name, value)); + } + } + return rows; + } + + private Optional getValueAtIndex(RServerResult values, int rowNum) { + if (values.isNA()[rowNum]) { + return Optional.empty(); + } + if (values.isInteger()) { + return Optional.of(values.asIntegers()[rowNum]); + } else if (values.isLogical()) { + return Optional.of(values.asIntegers()[rowNum]).map(it -> Integer.valueOf(TRUE).equals(it)); + } else if (values.isNumeric()) { + return Optional.of(values.asDoubles()[rowNum]); + } else if (values.isString()) { + return Optional.ofNullable(values.asStrings()[rowNum]); + } else { + return Optional.empty(); + } + } + + private void initialize(RList list) { + if (list.isNamed()) { + this.names = + ((List) list.names).stream().map(n -> n == null ? null : n.toString()).toList(); + for (int i = 0; i < list.names.size(); i++) { + String key = list.names.get(i).toString(); + REXP value = list.at(key); + namedList.put(key, new RserveResult(value)); + } + } + } +} diff --git a/r/src/main/java/org/molgenis/r/rserve/RserveResult.java b/r/src/main/java/org/molgenis/r/rserve/RserveResult.java new file mode 100644 index 000000000..d7c1ebcc5 --- /dev/null +++ b/r/src/main/java/org/molgenis/r/rserve/RserveResult.java @@ -0,0 +1,173 @@ +package org.molgenis.r.rserve; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import java.util.List; +import org.molgenis.r.RNamedList; +import org.molgenis.r.RServerResult; +import org.molgenis.r.exceptions.RExecutionException; +import org.rosuda.REngine.REXP; +import org.rosuda.REngine.REXPLogical; +import org.rosuda.REngine.REXPMismatchException; + +/** Extract result from Rserve java API. */ +@VisibleForTesting +public class RserveResult implements RServerResult { + + private final REXP result; + + public RserveResult(REXP result) { + this.result = result; + } + + @Override + public int length() { + try { + return result.length(); + } catch (REXPMismatchException e) { + return -1; + } + } + + @Override + public byte[] asBytes() { + if (result.isRaw()) { + try { + return result.asBytes(); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + return new byte[0]; + } + + @Override + public boolean isNumeric() { + return result.isNumeric(); + } + + @Override + public double[] asDoubles() { + if (isNumeric()) { + try { + return result.asDoubles(); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + return new double[0]; + } + + @Override + public boolean isInteger() { + return result.isInteger(); + } + + @Override + public int[] asIntegers() { + if (isInteger() || isLogical()) { + try { + return result.asIntegers(); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + return new int[0]; + } + + @Override + public int asInteger() { + int[] ints = asIntegers(); + if (ints.length == 0) throw new RExecutionException("Not an integer vector"); + return ints[0]; + } + + @Override + public boolean isLogical() { + return result.isLogical(); + } + + @Override + public boolean asLogical() { + if (result.isLogical()) { + REXPLogical logical = (REXPLogical) result; + return logical.length() > 0 && logical.isTRUE()[0]; + } + return false; + } + + @Override + public boolean isNull() { + return result == null || result.isNull(); + } + + @Override + public boolean isString() { + return result.isString(); + } + + @Override + public String[] asStrings() { + try { + return result.asStrings(); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + + @Override + public boolean isList() { + return result.isList(); + } + + @Override + public List asList() { + List rval = Lists.newArrayList(); + if (result.isList()) { + try { + for (Object obj : result.asList()) { + rval.add(new RserveResult((REXP) obj)); + } + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + return rval; + } + + @Override + public boolean isNamedList() { + try { + return result.isList() && result.asList().isNamed(); + } catch (REXPMismatchException e) { + return false; + } + } + + @Override + public RNamedList asNamedList() { + try { + return isNamedList() ? new RserveNamedList(result) : new RserveNamedList((REXP) null); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + + @Override + public boolean[] isNA() { + try { + return result.isNA(); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } + + @Override + public Object asNativeJavaObject() { + try { + return result.asNativeJavaObject(); + } catch (REXPMismatchException e) { + throw new RExecutionException(e); + } + } +} diff --git a/r/src/test/java/org/molgenis/r/RserveNamedListTest.java b/r/src/test/java/org/molgenis/r/RserveNamedListTest.java new file mode 100644 index 000000000..81265b3c9 --- /dev/null +++ b/r/src/test/java/org/molgenis/r/RserveNamedListTest.java @@ -0,0 +1,153 @@ +package org.molgenis.r; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.molgenis.r.rserve.RserveNamedList; +import org.rosuda.REngine.*; + +class RserveNamedListTest { + + private REXPString rownames = new REXPString(new String[] {"base", "desc"}); + REXPString colnames = new REXPString(new String[] {"Package", "Version"}); + RList dimnames = new RList(new REXP[] {rownames, colnames}); + + static Stream intsProvider() { + return Stream.of( + Arguments.of(0, Optional.of(0)), Arguments.of(REXPInteger.NA, Optional.empty())); + } + + static Stream logicalsProvider() { + return Stream.of( + Arguments.of(REXPLogical.TRUE, Optional.of(true)), + Arguments.of(REXPLogical.FALSE, Optional.of(false)), + Arguments.of(REXPLogical.NA, Optional.empty())); + } + + @Test + void parseTibbleTransposesTheDataStructure() throws RServerException { + RList rList = + new RList( + List.of( + new REXPString(new String[] {"id1", "id2"}), + new REXPString(new String[] {"label1", "label2"})), + new String[] {"id", "label"}); + var parsed = new RserveNamedList(rList).asRows(); + + assertEquals( + List.of(Map.of("id", "id1", "label", "label1"), Map.of("id", "id2", "label", "label2")), + parsed); + } + + @Test + void testParseTibbleSkipsNAValues() { + RList rList = + new RList( + List.of( + new REXPString(new String[] {"id1", "id2"}), + new REXPLogical(new byte[] {REXPLogical.NA, REXPLogical.FALSE})), + new String[] {"id", "valid"}); + var parsed = new RserveNamedList(rList).asRows(); + + assertEquals(List.of(Map.of("id", "id1"), Map.of("id", "id2", "valid", false)), parsed); + } + + @Test + void testParseTibbleSkipsNullValues() { + RList rList = + new RList( + List.of( + new REXPString(new String[] {"id1", "id2"}), + new REXPString(new String[] {null, "label2"})), + new String[] {"id", "label"}); + var parsed = new RserveNamedList(rList).asRows(); + + assertEquals(List.of(Map.of("id", "id1"), Map.of("id", "id2", "label", "label2")), parsed); + } + + @ParameterizedTest + @MethodSource("logicalsProvider") + void testGetValueAtIndexParsesLogicals(byte value, Optional expected) { + RList rList = new RList(List.of(new REXPLogical(new byte[] {value})), new String[] {"id"}); + var parsed = new RserveNamedList(rList).asRows(); + assertEquals(expected.orElse(null), parsed.get(0).get("id")); + } + + @ParameterizedTest + @MethodSource("doublesProvider") + void testGetValueAtIndexParsesDoubles(Double value, Optional expected) { + RList rList = new RList(List.of(new REXPDouble(new double[] {value})), new String[] {"id"}); + var parsed = new RserveNamedList(rList).asRows(); + assertEquals(expected.orElse(null), parsed.get(0).get("id")); + } + + static Stream doublesProvider() { + return Stream.of( + Arguments.of(0.0, Optional.of(0.0)), Arguments.of(REXPDouble.NA, Optional.empty())); + } + + @ParameterizedTest + @MethodSource("intsProvider") + void testGetValueAtIndexParsesIntegers(int value, Optional expected) { + RList rList = new RList(List.of(new REXPInteger(new int[] {value})), new String[] {"id"}); + var parsed = new RserveNamedList(rList).asRows(); + assertEquals(expected.orElse(null), parsed.get(0).get("id")); + } + + @Test + void testGetValueAtIndexSkipsTheUnknown() throws REXPMismatchException { + var vector = new REXPGenericVector(dimnames); + RList rList = new RList(List.of(vector), new String[] {"id"}); + var parsed = new RserveNamedList(rList).asRows(); + assertNull(parsed.get(0).get("id")); + } + + @Test + void testRserveNamedList() { + assertDoesNotThrow(() -> new RserveNamedList(new REXP())); + } + + @Test + void testIsEmpty() { + RList rList = new RList(); + assertTrue(new RserveNamedList(rList).isEmpty()); + } + + @Test + void testSize() { + RList rList = + new RList( + List.of( + new REXPString(new String[] {"id1", "id2"}), + new REXPString(new String[] {null, "label2"})), + new String[] {"id", "label"}); + RserveNamedList list = new RserveNamedList(rList); + assertEquals(2, list.size()); + } + + @Test + void testContainsKey() { + RList rList = + new RList( + List.of( + new REXPString(new String[] {"id1", "id2"}), + new REXPString(new String[] {null, "label2"})), + new String[] {"id", "label"}); + RserveNamedList list = new RserveNamedList(rList); + assertTrue(list.containsKey("id")); + } + + @Test + void asRowsEmptyNames() throws RServerException { + RList rList = new RList(); + var parsed = new RserveNamedList(rList); + assertTrue(parsed.isEmpty()); + } +} diff --git a/r/src/test/java/org/molgenis/r/RserverConnectionFactoryTest.java b/r/src/test/java/org/molgenis/r/RserverConnectionFactoryTest.java new file mode 100644 index 000000000..b82b828f1 --- /dev/null +++ b/r/src/test/java/org/molgenis/r/RserverConnectionFactoryTest.java @@ -0,0 +1,91 @@ +package org.molgenis.r; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.molgenis.r.config.EnvironmentConfigProps; + +public class RserverConnectionFactoryTest { + EnvironmentConfigProps props = new EnvironmentConfigProps(); + RServerConnectionFactory connectionFactory = new RServerConnectionFactory(props); + String url = "http://my-url.nl"; + + @Test + public void testGetMessageFromStatusDown() { + String actual = connectionFactory.getMessageFromStatus(RockStatusCode.SERVER_DOWN, url); + String expected = "Container for '" + url + "' is down"; + assertEquals(expected, actual); + } + + @Test + public void testGetMessageFromStatusServerNotReady() { + String actual = connectionFactory.getMessageFromStatus(RockStatusCode.SERVER_NOT_READY, url); + String expected = "Container for '" + url + "' is not ready"; + assertEquals(expected, actual); + } + + @Test + public void testGetMessageFromStatusOk() { + String actual = connectionFactory.getMessageFromStatus(RockStatusCode.OK, url); + String expected = "Container for '" + url + "' is running"; + assertEquals(expected, actual); + } + + @Test + public void testGetMessageFromStatusUnexpectedResponseCode() { + String actual = + connectionFactory.getMessageFromStatus(RockStatusCode.UNEXPECTED_RESPONSE_CODE, url); + String expected = "Unexpected response code on " + url; + assertEquals(expected, actual); + } + + @Test + public void testGetMessageFromStatusUnexpectedResponse() { + String actual = connectionFactory.getMessageFromStatus(RockStatusCode.UNEXPECTED_RESPONSE, url); + String expected = "Unexpected response on " + url; + assertEquals(expected, actual); + } + + @Test + public void testGetMessageFromStatusUnexpectedUrl() { + String actual = connectionFactory.getMessageFromStatus(RockStatusCode.UNEXPECTED_URL, url); + String expected = "MalformedURLException on " + url; + assertEquals(expected, actual); + } + + @Test + public void testIsWarningStatusServerDown() { + Boolean isWarning = connectionFactory.isWarningStatus(RockStatusCode.SERVER_DOWN); + assertTrue(isWarning); + } + + @Test + public void testIsWarningStatusServerNotReady() { + Boolean isWarning = connectionFactory.isWarningStatus(RockStatusCode.SERVER_NOT_READY); + assertTrue(isWarning); + } + + @Test + public void testIsWarningStatusServerUnexpectedUrl() { + Boolean isWarning = connectionFactory.isWarningStatus(RockStatusCode.UNEXPECTED_URL); + assertTrue(isWarning); + } + + @Test + public void testIsWarningStatusServerOk() { + Boolean isWarning = connectionFactory.isWarningStatus(RockStatusCode.OK); + assertFalse(isWarning); + } + + @Test + public void testIsWarningStatusServerUnexpectedResponse() { + Boolean isWarning = connectionFactory.isWarningStatus(RockStatusCode.UNEXPECTED_RESPONSE); + assertFalse(isWarning); + } + + @Test + public void testIsWarningStatusServerUnexpectedResponseCode() { + Boolean isWarning = connectionFactory.isWarningStatus(RockStatusCode.UNEXPECTED_RESPONSE_CODE); + assertFalse(isWarning); + } +} diff --git a/r/src/test/java/org/molgenis/r/config/EnvironmentConfigPropsTest.java b/r/src/test/java/org/molgenis/r/config/EnvironmentConfigPropsTest.java new file mode 100644 index 000000000..3310af70e --- /dev/null +++ b/r/src/test/java/org/molgenis/r/config/EnvironmentConfigPropsTest.java @@ -0,0 +1,33 @@ +package org.molgenis.r.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class EnvironmentConfigPropsTest { + EnvironmentConfigProps props = new EnvironmentConfigProps(); + + @Test + public void testHost() { + props.setHost("notlocalhost"); + assertEquals("notlocalhost", props.getHost()); + } + + @Test + public void testPort() { + props.setPort(8085); + assertEquals(8085, props.getPort()); + } + + @Test + public void testName() { + props.setName("bofke"); + assertEquals("bofke", props.getName()); + } + + @Test + public void testImage() { + props.setImage("datashield/rock-dolomite-xenon"); + assertEquals("datashield/rock-dolomite-xenon", props.getImage()); + } +} diff --git a/r/src/test/java/org/molgenis/r/rserve/RserveConnectionFactoryTest.java b/r/src/test/java/org/molgenis/r/rserve/RserveConnectionFactoryTest.java new file mode 100644 index 000000000..5c2553ee8 --- /dev/null +++ b/r/src/test/java/org/molgenis/r/rserve/RserveConnectionFactoryTest.java @@ -0,0 +1,38 @@ +package org.molgenis.r.rserve; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.molgenis.r.config.EnvironmentConfigProps; + +class RserveConnectionFactoryTest { + + @Mock private EnvironmentConfigProps environmentConfigProps; + + private RserveConnectionFactory connectionFactory; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(environmentConfigProps.getHost()).thenReturn("localhost"); + when(environmentConfigProps.getPort()).thenReturn(6311); + connectionFactory = new RserveConnectionFactory(environmentConfigProps); + } + + @Test + void testToString() { + // Arrange + String expectedString = "RConnectionFactoryImpl{environment=TestEnvironment}"; + when(environmentConfigProps.getName()).thenReturn("TestEnvironment"); + + // Act + String result = connectionFactory.toString(); + + // Assert + assertEquals(expectedString, result); + } +} diff --git a/r/src/test/java/org/molgenis/r/rserve/RserveConnectionTest.java b/r/src/test/java/org/molgenis/r/rserve/RserveConnectionTest.java new file mode 100644 index 000000000..846e883ff --- /dev/null +++ b/r/src/test/java/org/molgenis/r/rserve/RserveConnectionTest.java @@ -0,0 +1,119 @@ +package org.molgenis.r.rserve; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.*; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import org.molgenis.r.RServerException; +import org.molgenis.r.RServerResult; +import org.molgenis.r.exceptions.RExecutionException; +import org.rosuda.REngine.REXP; +import org.rosuda.REngine.REXPMismatchException; +import org.rosuda.REngine.Rserve.RConnection; +import org.rosuda.REngine.Rserve.RFileInputStream; +import org.rosuda.REngine.Rserve.RserveException; + +@ExtendWith(MockitoExtension.class) +class RserveConnectionTest { + + @InjectMocks private RserveConnection rserveConnection; + @Mock private RConnection rConnection; + @Mock private REXP rexp; + + RserveConnectionTest() {} + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + rserveConnection = new RserveConnection(rConnection); + } + + @Test + void testEvalREXP() throws RServerException, RserveException { + String exp = "mean(age)"; + RserveConnection conn = new RserveConnection(rConnection); + when(rConnection.eval(exp)).thenReturn(rexp); + REXP result = rserveConnection.evalREXP(exp); + assertEquals(rexp, result); + } + + @Test + void testEvalREXPThrowsExceptionWhenEvalError() throws RserveException, REXPMismatchException { + String exp = "mean(age)"; + String[] error = {"error 1 ", "error 2 "}; + when(rConnection.eval(exp)).thenReturn(rexp); + when(rexp.inherits("try-error")).thenReturn(Boolean.TRUE); + when(rexp.asStrings()).thenReturn(error); + Exception exception = + assertThrows(RExecutionException.class, () -> rserveConnection.evalREXP(exp)); + String expected = "error 1; error 2"; + assertTrue(exception.getMessage().contains(expected)); + } + + @Test + void testEvalREXPThrowsExceptionWhenError() throws RserveException { + String exp = "mean(age)"; + when(rConnection.eval(exp)).thenThrow(RserveException.class); + assertThrows(org.molgenis.r.rserve.RserveException.class, () -> rserveConnection.evalREXP(exp)); + } + + @Test + void testEvalREXPThrowsExceptionWhenMismatchError() + throws RserveException, REXPMismatchException { + String exp = "mean(age)"; + when(rConnection.eval(exp)).thenReturn(rexp); + when(rexp.inherits("try-error")).thenReturn(Boolean.TRUE); + when(rexp.asStrings()).thenThrow(REXPMismatchException.class); + assertThrows(org.molgenis.r.rserve.RserveException.class, () -> rserveConnection.evalREXP(exp)); + } + + @Test + void testReadFile() throws Exception { + String fileName = ".RData"; + RFileInputStream mockInputStream = mock(RFileInputStream.class); + Consumer mockConsumer = mock(Consumer.class); + when(rConnection.openFile(fileName)).thenReturn(mockInputStream); + rserveConnection.readFile(fileName, mockConsumer); + verify(rConnection, times(1)).openFile(fileName); + verify(mockConsumer, times(1)).accept(mockInputStream); + } + + @Test + void testReadFileThrowsIOException() throws Exception { + String fileName = ".RData"; + Consumer mockConsumer = mock(Consumer.class); + when(rConnection.openFile(fileName)).thenThrow(new IOException("Connection failed")); + assertThrows( + org.molgenis.r.rserve.RserveException.class, + () -> rserveConnection.readFile(fileName, mockConsumer)); + } + + @Test + void testEvalSerialized() throws Exception { + String expression = "1 + 1"; + when(rConnection.eval(anyString())).thenReturn(rexp); + RServerResult result = rserveConnection.eval(expression, true); + + assertNotNull(result); + verify(rConnection, times(1)).eval(format("try(base::serialize({%s}, NULL))", expression)); + } + + @Test + void testEvalNonSerialized() throws Exception { + String expression = "2 + 2"; + when(rConnection.eval(anyString())).thenReturn(rexp); + RServerResult result = rserveConnection.eval(expression, false); + + assertNotNull(result); + verify(rConnection, times(1)).eval(format("try({%s})", expression)); + } +} diff --git a/r/src/test/java/org/molgenis/r/rserve/RserveResultTest.java b/r/src/test/java/org/molgenis/r/rserve/RserveResultTest.java new file mode 100644 index 000000000..89c93837e --- /dev/null +++ b/r/src/test/java/org/molgenis/r/rserve/RserveResultTest.java @@ -0,0 +1,195 @@ +package org.molgenis.r.rserve; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.molgenis.r.RServerResult; +import org.molgenis.r.exceptions.RExecutionException; +import org.rosuda.REngine.REXP; +import org.rosuda.REngine.REXPMismatchException; +import org.rosuda.REngine.RList; + +class RserveResultTest { + + @Mock private REXP mockREXP; + + private RserveResult rserveResult; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + rserveResult = new RserveResult(mockREXP); + } + + @Test + void testLength() throws REXPMismatchException { + // Simulate result.length() + when(mockREXP.length()).thenReturn(10); + int length = rserveResult.length(); + assertEquals(10, length); + + // Simulate an exception in length() + when(mockREXP.length()).thenThrow(REXPMismatchException.class); + length = rserveResult.length(); + assertEquals(-1, length); + } + + @Test + void testAsBytes() throws REXPMismatchException { + byte[] expectedBytes = {1, 2, 3, 4}; + + // Simulate result.isRaw() and result.asBytes() + when(mockREXP.isRaw()).thenReturn(true); + when(mockREXP.asBytes()).thenReturn(expectedBytes); + byte[] actualBytes = rserveResult.asBytes(); + assertArrayEquals(expectedBytes, actualBytes); + + // If not raw, should return empty byte array + when(mockREXP.isRaw()).thenReturn(false); + actualBytes = rserveResult.asBytes(); + assertArrayEquals(new byte[0], actualBytes); + } + + @Test + void testIsNumeric() { + when(mockREXP.isNumeric()).thenReturn(true); + assertTrue(rserveResult.isNumeric()); + + when(mockREXP.isNumeric()).thenReturn(false); + assertFalse(rserveResult.isNumeric()); + } + + @Test + void testAsDoubles() throws REXPMismatchException { + double[] expectedDoubles = {1.1, 2.2, 3.3}; + + // Simulate numeric REXP + when(mockREXP.isNumeric()).thenReturn(true); + when(mockREXP.asDoubles()).thenReturn(expectedDoubles); + double[] actualDoubles = rserveResult.asDoubles(); + assertArrayEquals(expectedDoubles, actualDoubles); + + // Simulate a non-numeric case + when(mockREXP.isNumeric()).thenReturn(false); + actualDoubles = rserveResult.asDoubles(); + assertArrayEquals(new double[0], actualDoubles); + } + + @Test + void testIsInteger() { + when(mockREXP.isInteger()).thenReturn(true); + assertTrue(rserveResult.isInteger()); + + when(mockREXP.isInteger()).thenReturn(false); + assertFalse(rserveResult.isInteger()); + } + + @Test + void testAsIntegers() throws REXPMismatchException { + int[] expectedIntegers = {1, 2, 3}; + + // Simulate integer REXP + when(mockREXP.isInteger()).thenReturn(true); + when(mockREXP.asIntegers()).thenReturn(expectedIntegers); + int[] actualIntegers = rserveResult.asIntegers(); + assertArrayEquals(expectedIntegers, actualIntegers); + + // Simulate logical REXP + when(mockREXP.isInteger()).thenReturn(false); + when(mockREXP.isLogical()).thenReturn(true); + when(mockREXP.asIntegers()).thenReturn(expectedIntegers); + actualIntegers = rserveResult.asIntegers(); + assertArrayEquals(expectedIntegers, actualIntegers); + + // Non-integer and non-logical should return empty array + when(mockREXP.isInteger()).thenReturn(false); + when(mockREXP.isLogical()).thenReturn(false); + actualIntegers = rserveResult.asIntegers(); + assertArrayEquals(new int[0], actualIntegers); + } + + @Test + void testAsInteger() throws REXPMismatchException { + // Simulate result.asIntegers() returning a valid integer + int[] ints = {42}; + when(mockREXP.isInteger()).thenReturn(true); + when(mockREXP.asIntegers()).thenReturn(ints); + assertEquals(42, rserveResult.asInteger()); + + // Simulate an empty integer array + ints = new int[0]; + when(mockREXP.asIntegers()).thenReturn(ints); + assertThrows(RExecutionException.class, () -> rserveResult.asInteger()); + } + + @Test + void testIsLogical() { + when(mockREXP.isLogical()).thenReturn(true); + assertTrue(rserveResult.isLogical()); + + when(mockREXP.isLogical()).thenReturn(false); + assertFalse(rserveResult.isLogical()); + } + + @Test + void testIsNull() { + when(mockREXP.isNull()).thenReturn(true); + assertTrue(rserveResult.isNull()); + + when(mockREXP.isNull()).thenReturn(false); + assertFalse(rserveResult.isNull()); + } + + @Test + void testIsString() { + when(mockREXP.isString()).thenReturn(true); + assertTrue(rserveResult.isString()); + + when(mockREXP.isString()).thenReturn(false); + assertFalse(rserveResult.isString()); + } + + @Test + void testAsStrings() throws REXPMismatchException { + String[] expectedStrings = {"hello", "world"}; + when(mockREXP.asStrings()).thenReturn(expectedStrings); + String[] actualStrings = rserveResult.asStrings(); + assertArrayEquals(expectedStrings, actualStrings); + } + + @Test + void testIsList() { + when(mockREXP.isList()).thenReturn(true); + assertTrue(rserveResult.isList()); + + when(mockREXP.isList()).thenReturn(false); + assertFalse(rserveResult.isList()); + } + + @Test + void testAsList() throws REXPMismatchException { + RList expectedList = new RList(); + when(mockREXP.isList()).thenReturn(true); + when(mockREXP.asList()).thenReturn(expectedList); + List actualList = rserveResult.asList(); + assertEquals(expectedList, actualList); + } + + @Test + void testIsNamedList() throws REXPMismatchException { + RList rList = mock(RList.class); + when(mockREXP.isList()).thenReturn(true); + // Simulate named list + when(mockREXP.asList()).thenReturn(rList); + when(rList.isNamed()).thenReturn(true); + assertTrue(rserveResult.isNamedList()); + + // Simulate non-named list + when(rList.isNamed()).thenReturn(false); + assertFalse(rserveResult.isNamedList()); + } +} diff --git a/r/src/test/java/org/molgenis/r/service/ProcessServiceImplTest.java b/r/src/test/java/org/molgenis/r/service/ProcessServiceImplTest.java index 2b3cdd41b..913c0c18a 100644 --- a/r/src/test/java/org/molgenis/r/service/ProcessServiceImplTest.java +++ b/r/src/test/java/org/molgenis/r/service/ProcessServiceImplTest.java @@ -12,7 +12,6 @@ import java.util.Map; import org.json.JSONArray; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -23,7 +22,9 @@ import org.molgenis.r.RServerResult; import org.molgenis.r.model.RProcess; import org.molgenis.r.model.RProcess.Status; -import org.molgenis.r.rock.RockResult; +import org.molgenis.r.rserve.RserveResult; +import org.rosuda.REngine.REXPInteger; +import org.rosuda.REngine.REXPList; import org.rosuda.REngine.REXPMismatchException; import org.skyscreamer.jsonassert.JSONParser; @@ -57,19 +58,10 @@ void testTerminate() throws RServerException { } @Test - @Disabled void testCountRserveProcesses() throws REXPMismatchException, RServerException { - // we need [{"n":3}] from RockResult which has no String parser containing a list JSONArray result = (JSONArray) JSONParser.parseJSON("[{\"n\":3}]"); - - /* - * FIXME this gives: - * org.mockito.exceptions.misusing.WrongTypeOfReturnValue: - * RockResult cannot be returned by toString() - * oString() should return String - */ when(rExecutorService.execute(COUNT_RSERVE_PROCESSES_COMMAND, rConnection)) - .thenReturn(new RockResult(result)); + .thenReturn(new RserveResult(new REXPList(new REXPInteger(3), "n"))); assertEquals(3, processService.countRserveProcesses(rConnection)); } diff --git a/r/src/test/java/org/molgenis/r/service/RExecutorServiceImplTest.java b/r/src/test/java/org/molgenis/r/service/RExecutorServiceImplTest.java index f097831aa..e0790f333 100644 --- a/r/src/test/java/org/molgenis/r/service/RExecutorServiceImplTest.java +++ b/r/src/test/java/org/molgenis/r/service/RExecutorServiceImplTest.java @@ -9,7 +9,6 @@ import java.util.List; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -21,6 +20,7 @@ import org.molgenis.r.exceptions.RExecutionException; import org.molgenis.r.rock.RockResult; import org.molgenis.r.rock.RockServerException; +import org.molgenis.r.rserve.RserveResult; import org.rosuda.REngine.REXPLogical; import org.rosuda.REngine.REXPNull; import org.rosuda.REngine.Rserve.RFileInputStream; @@ -251,16 +251,15 @@ void testInstallPackageFails() { } @Test - @Disabled void testInstallPackage() throws IOException, RServerException { when(rConnection.eval( "remotes::install_local('location__test_.tar.gz', dependencies = TRUE, upgrade = 'never')", false)) - .thenReturn(new RockResult(new REXPNull())); + .thenReturn(new RserveResult(new REXPNull())); when(rConnection.eval("require('location/_test')", false)) - .thenReturn(new RockResult(new REXPLogical(true))); + .thenReturn(new RserveResult(new REXPLogical(true))); when(rConnection.eval("file.remove('location/_test_.tar.gz')", false)) - .thenReturn(new RockResult(new REXPNull())); + .thenReturn(new RserveResult(new REXPNull())); Resource resource = new InMemoryResource("Hello"); String fileName = "location/_test_.tar.gz"; diff --git a/scripts/release/release-test.R b/scripts/release/release-test.R index a6b690494..323a2d659 100755 --- a/scripts/release/release-test.R +++ b/scripts/release/release-test.R @@ -57,110 +57,123 @@ cli_h2("Preparing resource for tests") source("test-cases/download-resources.R") prepare_resources(resource_path = test_config$rda_dir, url = test_config$rda_url, skip_tests = test_config$skip_tests) -cli_h2("Determining whether to run with password or token") -source("test-cases/set-admin-mode.R") -token <- set_admin_or_get_token(admin_pwd = test_config$admin_pwd, url = test_config$armadillo_url, skip_tests = test_config$skip_test, ADMIN_MODE = test_config$ADMIN_MODE) - -cli_h2("Configuring profiles") -source("test-cases/setup-profiles.R") -profile_info <- setup_profiles(auth_type = test_config$auth_type, token = token, skip_tests = test_config$skip_tests, url = test_config$armadillo_url, as_docker_container = test_config$as_docker_container, profile = test_config$profile, user = test_config$user, interactive = test_config$interactive, profile_defaults = test_config$profile_defaults) - -cli_h1("Starting release test") -source("lib/release-test-info.R") -test_message <- show_test_info(version = test_config$version, url = test_config$armadillo_url, user = test_config$user, admin_pwd = test_config$admin_pwd, dest = test_config$dest, profile = test_config$profile, ADMIN_MODE = test_config$ADMIN_MODE, skip_tests = test_config$skip_tests) - -cli_h2("Logging in as data manager") -source("test-cases/dm-login.R") -dm_login(url = test_config$armadillo_url, ADMIN_MODE = test_config$ADMIN_MODE, admin_pwd = test_config$admin_pwd, skip_tests = test_config$skip_tests) - -cli_h2("Generating a random project name") -project1 <- generate_random_project_name(skip_tests = test_config$skip_tests) - -cli_h2("Creating a test project") -source("test-cases/create-test-project.R") -create_test_project(target_project_name = project1, skip_tests = test_config$skip_tests) - -cli_h2("Uploading test data") -source("test-cases/upload-data.R") -upload_test_data(project = project1, dest = test_config$default_parquet_path, skip_tests = test_config$skip_tests) - -cli_h2("Uploading resource source file") -source("test-cases/upload-resource.R") -upload_resource(project = project1, rda_dir = test_config$rda_dir, url = test_config$armadillo_url, token = token, folder = "ewas", file_name = "gse66351_1.rda", auth_type = test_config$auth_type, skip_tests = test_config$skip_tests) - -cli_h2("Creating resource") -source("test-cases/create-resource.R") -resGSE1 <- create_resource(target_project = project1, url = test_config$armadillo_url, folder = "ewas", file_name = "gse66351_1.rda", resource_name = "GSE66351_1", format = "ExpressionSet", skip_tests = test_config$skip_tests) - -cli_h2("Uploading resource file") -armadillo.upload_resource(project = project1, folder = "ewas", resource = resGSE1, name = "GSE66351_1") - -cli_h2("Starting manual UI test") -source("test-cases/manual-test.R") -interactive_test(project1, test_config$interactive, test_config$skip_tests) - -cli_alert_info("\nNow you're going to test as researcher") -cli_h2("Setting researcher permissions") -source("test-cases/set_researcher_access.R") -set_researcher_access(url = test_config$armadillo_url, interactive = test_config$interactive, required_projects = list(project1), user = test_config$user, admin_pwd = test_config$admin_pwd, update_auto = test_config$update_auto, skip_tests = test_config$skip_tests) # Add linked table when working - -cli_h2("Logging in as a researcher") -source("test-cases/researcher-login.R") -conns <- researcher_login(url = test_config$armadillo_url, profile = test_config$profile, admin_pwd = test_config$admin_pwd, token = token, table = "2_1-core-1_0/nonrep", project = project1, object = "nonrep", variables = "coh_country", ADMIN_MODE = test_config$ADMIN_MODE, skip_tests = test_config$skip_tests) - -cli_h2("Verifying connecting to profiles possible") -source("test-cases/verify-profile.R") -verify_profiles(admin_pwd = test_config$admin_pwd, token = token, url = test_config$armadillo_url, profile = test_config$profile, ADMIN_MODE = test_config$ADMIN_MODE, skip_tests = test_config$skip_tests) - -cli_h2("Assigning tables as researcher") -source("test-cases/assigning.R") -check_assigning(project = project1, folder = "2_1-core-1_0", table = "nonrep", object = "nonrep", variable = "coh_country", skip_tests = test_config$skip_tests) - -cli_h2("Testing resources as a researcher") -source("test-cases/verify-resources.R") -verify_resources(project = project1, resource_path = "ewas/GSE66351_1", ADMIN_MODE = test_config$ADMIN_MODE, profile = test_config$profile, profile_info = profile_info, skip_tests = test_config$skip_tests) - -cli_h2("Verifying xenon packages") -cli_alert_info("Verifying dsBase") -source("test-cases/ds-base.R") -verify_ds_base(object = "nonrep", variable = "coh_country", skip_tests = test_config$skip_tests) - -cli_alert_info("Verifying dsMediation") -source("test-cases/xenon-mediate.R") -verify_ds_mediation(skip_tests = test_config$skip_tests) - -cli_alert_info("Testing dsSurvival") -source("test-cases/xenon-survival.R") -run_survival_tests(project = project1, data_path = "/survival/veteran", skip_tests = test_config$skip_tests) - -cli_alert_info("Testing dsMTL") -source("test-cases/xenon-mtl.R") -verify_ds_mtl(skip_tests = test_config$skip_tests) - -cli_alert_info("Testing dsExposome") -source("test-cases/xenon-exposome.R") -run_exposome_tests(project = project1, url = test_config$armadillo_url, token = token, auth_type = test_config$auth_type, - ADMIN_MODE = test_config$ADMIN_MODE, profile = test_config$profile, profile_info = profile_info, - ref = exposome_ref, skip_tests = test_config$skip_tests, - user = test_config$user, admin_pwd = test_config$admin_pwd, interactive = test_config$interactive, - update_auto = test_config$update_auto) - -cli_alert_info("Testing dsOmics") -source("test-cases/xenon-omics.R") -run_omics_tests(project = project1, url = test_config$armadillo_url, token = token, auth_type = test_config$auth_type, - ADMIN_MODE = test_config$ADMIN_MODE, profile = test_config$profile, profile_info = profile_info, - ref = omics_ref, skip_tests = test_config$skip_tests, - user = test_config$user, admin_pwd = test_config$admin_pwd, interactive = test_config$interactive, - update_auto = test_config$update_auto) - -cli_h2("Removing data as admin") -source("test-cases/remove-data.R") # Add link_project once module works -dm_clean_up(user = test_config$user, admin_pwd = test_config$admin_pwd, required_projects = list(project1), update_auto = test_config$update_auto, url = test_config$armadillo_url, skip_tests = test_config$skip_tests, interactive = test_config$interactive) -datashield.logout(conns) - -cli_h2("Testing basic authentification") -source("test-cases/basic-auth.R") -verify_basic_auth(url = test_config$armadillo_url, admin_pwd = test_config$admin_pwd, dest = test_config$default_parquet_path, skip_tests = test_config$skip_tests) - -cli_alert_info("Testing done") -cli_alert_info("Please test rest of UI manually, if impacted this release") +profiles <- unlist(stri_split_fixed(test_config$profile, ",")) + + +run_tests_for_profile <- function(profile) { + start_time <- Sys.time() + cli_h2(paste0("Running for profile: ", profile)) + + cli_h2("Determining whether to run with password or token") + source("test-cases/set-admin-mode.R") + token <- set_admin_or_get_token(admin_pwd = test_config$admin_pwd, url = test_config$armadillo_url, skip_tests = test_config$skip_test, ADMIN_MODE = test_config$ADMIN_MODE) + + cli_h2("Configuring profiles") + source("test-cases/setup-profiles.R") + profile_info <- setup_profiles(auth_type = test_config$auth_type, token = token, skip_tests = test_config$skip_tests, url = test_config$armadillo_url, as_docker_container = test_config$as_docker_container, profile = profile, user = test_config$user, interactive = test_config$interactive, profile_defaults = test_config$profile_defaults) + + cli_h1("Starting release test") + source("lib/release-test-info.R") + test_message <- show_test_info(version = test_config$version, url = test_config$armadillo_url, user = test_config$user, admin_pwd = test_config$admin_pwd, dest = test_config$dest, profile = profile, ADMIN_MODE = test_config$ADMIN_MODE, skip_tests = test_config$skip_tests) + + cli_h2("Logging in as data manager") + source("test-cases/dm-login.R") + dm_login(url = test_config$armadillo_url, ADMIN_MODE = test_config$ADMIN_MODE, admin_pwd = test_config$admin_pwd, skip_tests = test_config$skip_tests) + + cli_h2("Generating a random project name") + project1 <<- generate_random_project_name(skip_tests = test_config$skip_tests) + + cli_h2("Creating a test project") + source("test-cases/create-test-project.R") + create_test_project(target_project_name = project1, skip_tests = test_config$skip_tests) + + cli_h2("Uploading test data") + source("test-cases/upload-data.R") + upload_test_data(project = project1, dest = test_config$default_parquet_path, skip_tests = test_config$skip_tests) + + cli_h2("Uploading resource source file") + source("test-cases/upload-resource.R") + upload_resource(project = project1, rda_dir = test_config$rda_dir, url = test_config$armadillo_url, token = token, folder = "ewas", file_name = "gse66351_1.rda", auth_type = test_config$auth_type, skip_tests = test_config$skip_tests) + + cli_h2("Creating resource") + source("test-cases/create-resource.R") + resGSE1 <- create_resource(target_project = project1, url = test_config$armadillo_url, folder = "ewas", file_name = "gse66351_1.rda", resource_name = "GSE66351_1", format = "ExpressionSet", skip_tests = test_config$skip_tests) + + cli_h2("Uploading resource file") + armadillo.upload_resource(project = project1, folder = "ewas", resource = resGSE1, name = "GSE66351_1") + + cli_h2("Starting manual UI test") + source("test-cases/manual-test.R") + interactive_test(project1, test_config$interactive, test_config$skip_tests) + + cli_alert_info("\nNow you're going to test as researcher") + cli_h2("Setting researcher permissions") + source("test-cases/set_researcher_access.R") + set_researcher_access(url = test_config$armadillo_url, interactive = test_config$interactive, required_projects = list(project1), user = test_config$user, admin_pwd = test_config$admin_pwd, update_auto = test_config$update_auto, skip_tests = test_config$skip_tests) # Add linked table when working + + cli_h2("Logging in as a researcher") + source("test-cases/researcher-login.R") + conns <<- researcher_login(url = test_config$armadillo_url, profile = profile, admin_pwd = test_config$admin_pwd, token = token, table = "2_1-core-1_0/nonrep", project = project1, object = "nonrep", variables = "coh_country", ADMIN_MODE = test_config$ADMIN_MODE, skip_tests = test_config$skip_tests) + + cli_h2("Verifying connecting to profiles possible") + source("test-cases/verify-profile.R") + verify_profiles(admin_pwd = test_config$admin_pwd, token = token, url = test_config$armadillo_url, profile = profile, ADMIN_MODE = test_config$ADMIN_MODE, skip_tests = test_config$skip_tests) + + cli_h2("Assigning tables as researcher") + source("test-cases/assigning.R") + check_assigning(project = project1, folder = "2_1-core-1_0", table = "nonrep", object = "nonrep", variable = "coh_country", skip_tests = test_config$skip_tests) + + cli_h2("Testing resources as a researcher") + source("test-cases/verify-resources.R") + verify_resources(project = project1, resource_path = "ewas/GSE66351_1", ADMIN_MODE = test_config$ADMIN_MODE, profile = profile, profile_info = profile_info, skip_tests = test_config$skip_tests) + + cli_h2("Verifying xenon packages") + cli_alert_info("Verifying dsBase") + source("test-cases/ds-base.R") + verify_ds_base(object = "nonrep", variable = "coh_country", skip_tests = test_config$skip_tests) + + cli_alert_info("Verifying dsMediation") + source("test-cases/xenon-mediate.R") + verify_ds_mediation(skip_tests = test_config$skip_tests) + + cli_alert_info("Testing dsSurvival") + source("test-cases/xenon-survival.R") + run_survival_tests(project = project1, data_path = "/survival/veteran", skip_tests = test_config$skip_tests) + + cli_alert_info("Testing dsMTL") + source("test-cases/xenon-mtl.R") + verify_ds_mtl(skip_tests = test_config$skip_tests) + + cli_alert_info("Testing dsExposome") + source("test-cases/xenon-exposome.R") + run_exposome_tests(project = project1, url = test_config$armadillo_url, token = token, auth_type = test_config$auth_type, + ADMIN_MODE = test_config$ADMIN_MODE, profile = profile, profile_info = profile_info, + ref = exposome_ref, skip_tests = test_config$skip_tests, + user = test_config$user, admin_pwd = test_config$admin_pwd, interactive = test_config$interactive, + update_auto = test_config$update_auto) + + cli_alert_info("Testing dsOmics") + source("test-cases/xenon-omics.R") + run_omics_tests(project = project1, url = test_config$armadillo_url, token = token, auth_type = test_config$auth_type, + ADMIN_MODE = test_config$ADMIN_MODE, profile = profile, profile_info = profile_info, + ref = omics_ref, skip_tests = test_config$skip_tests, + user = test_config$user, admin_pwd = test_config$admin_pwd, interactive = test_config$interactive, + update_auto = test_config$update_auto) + + cli_h2("Removing data as admin") + source("test-cases/remove-data.R") # Add link_project once module works + dm_clean_up(user = test_config$user, admin_pwd = test_config$admin_pwd, required_projects = list(project1), update_auto = test_config$update_auto, url = test_config$armadillo_url, skip_tests = test_config$skip_tests, interactive = test_config$interactive) + datashield.logout(conns) + + cli_h2("Testing basic authentication") + source("test-cases/basic-auth.R") + verify_basic_auth(url = test_config$armadillo_url, admin_pwd = test_config$admin_pwd, dest = test_config$default_parquet_path, skip_tests = test_config$skip_tests) + + cli_alert_info("Testing done") + cli_alert_info("Please test rest of UI manually, if impacted this release") + end_time <- Sys.time() + print(paste0("Running tests for profile [", profile, "] took: ", end_time - start_time)) +} + +lapply(profiles, run_tests_for_profile) +