diff --git a/submarine-server/server-core/pom.xml b/submarine-server/server-core/pom.xml index 34d67cf7c5..819ac39900 100644 --- a/submarine-server/server-core/pom.xml +++ b/submarine-server/server-core/pom.xml @@ -67,7 +67,7 @@ org.javassist - javassist + javassist @@ -423,22 +423,22 @@ test - - org.apache.httpcomponents - httpclient - ${httpclient.version} - - - commons-codec - commons-codec - - - - - org.apache.httpcomponents - httpcore - ${httpcore.version} - + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + commons-codec + commons-codec + + + + + org.apache.httpcomponents + httpcore + ${httpcore.version} + org.eclipse.jgit org.eclipse.jgit @@ -450,6 +450,11 @@ + + org.mockito + mockito-core + test + diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java index d6c223d218..b57ae1d504 100644 --- a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java +++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java @@ -32,6 +32,7 @@ import javax.ws.rs.core.Response; import java.util.List; +import com.google.common.annotations.VisibleForTesting; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -50,20 +51,27 @@ @Path(RestConstants.V1 + "/" + RestConstants.EXPERIMENT) @Produces({MediaType.APPLICATION_JSON + "; " + RestConstants.CHARSET_UTF8}) public class ExperimentRestApi { - private final ExperimentManager experimentManager = ExperimentManager.getInstance(); + private ExperimentManager experimentManager = ExperimentManager.getInstance(); + + @VisibleForTesting + public void setExperimentManager(ExperimentManager experimentManager) { + this.experimentManager = experimentManager; + } + /** * Return the Pong message for test the connectivity + * * @return Pong message */ @GET @Path(RestConstants.PING) @Consumes(MediaType.APPLICATION_JSON) @Operation(summary = "Ping submarine server", - tags = {"experiment"}, - description = "Return the Pong message for test the connectivity", - responses = { - @ApiResponse(responseCode = "200", description = "successful operation", - content = @Content(schema = @Schema(implementation = String.class)))}) + tags = {"experiment"}, + description = "Return the Pong message for test the connectivity", + responses = { + @ApiResponse(responseCode = "200", description = "successful operation", + content = @Content(schema = @Schema(implementation = String.class)))}) public Response ping() { return new JsonResponse.Builder(Response.Status.OK) .success(true).result("Pong").build(); @@ -71,16 +79,17 @@ public Response ping() { /** * Returns the contents of {@link Experiment} that submitted by user. + * * @param spec spec * @return the contents of experiment */ @POST @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON}) @Operation(summary = "Create an experiment", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class)))}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class)))}) public Response createExperiment(ExperimentSpec spec) { try { Experiment experiment = experimentManager.createExperiment(spec); @@ -93,14 +102,15 @@ public Response createExperiment(ExperimentSpec spec) { /** * List all experiment for the user + * * @return experiment list */ @GET @Operation(summary = "List experiments", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class)))}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class)))}) public Response listExperiments(@QueryParam("status") String status) { try { List experimentList = experimentManager.listExperimentsByStatus(status); @@ -113,17 +123,18 @@ public Response listExperiments(@QueryParam("status") String status) { /** * Returns the experiment detailed info by specified experiment id + * * @param id experiment id * @return the detailed info of experiment */ @GET @Path("/{id}") @Operation(summary = "Get the experiment's detailed info by id", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class))), - @ApiResponse(responseCode = "404", description = "Experiment not found")}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class))), + @ApiResponse(responseCode = "404", description = "Experiment not found")}) public Response getExperiment(@PathParam(RestConstants.ID) String id) { try { Experiment experiment = experimentManager.getExperiment(id); @@ -138,11 +149,11 @@ public Response getExperiment(@PathParam(RestConstants.ID) String id) { @Path("/{id}") @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON}) @Operation(summary = "Update the experiment in the submarine server with spec", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class))), - @ApiResponse(responseCode = "404", description = "Experiment not found")}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class))), + @ApiResponse(responseCode = "404", description = "Experiment not found")}) public Response patchExperiment(@PathParam(RestConstants.ID) String id, ExperimentSpec spec) { try { Experiment experiment = experimentManager.patchExperiment(id, spec); @@ -155,17 +166,18 @@ public Response patchExperiment(@PathParam(RestConstants.ID) String id, Experime /** * Returns the experiment that deleted + * * @param id experiment id * @return the detailed info about deleted experiment */ @DELETE @Path("/{id}") @Operation(summary = "Delete the experiment", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class))), - @ApiResponse(responseCode = "404", description = "Experiment not found")}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class))), + @ApiResponse(responseCode = "404", description = "Experiment not found")}) public Response deleteExperiment(@PathParam(RestConstants.ID) String id) { try { Experiment experiment = experimentManager.deleteExperiment(id); @@ -179,10 +191,10 @@ public Response deleteExperiment(@PathParam(RestConstants.ID) String id) { @GET @Path("/logs") @Operation(summary = "List experiment's log", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class)))}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class)))}) public Response listLog(@QueryParam("status") String status) { try { List experimentLogList = experimentManager.listExperimentLogsByStatus(status); @@ -197,11 +209,11 @@ public Response listLog(@QueryParam("status") String status) { @GET @Path("/logs/{id}") @Operation(summary = "Log experiment by id", - tags = {"experiment"}, - responses = { - @ApiResponse(description = "successful operation", content = @Content( - schema = @Schema(implementation = JsonResponse.class))), - @ApiResponse(responseCode = "404", description = "Experiment not found")}) + tags = {"experiment"}, + responses = { + @ApiResponse(description = "successful operation", content = @Content( + schema = @Schema(implementation = JsonResponse.class))), + @ApiResponse(responseCode = "404", description = "Experiment not found")}) public Response getLog(@PathParam(RestConstants.ID) String id) { try { ExperimentLog experimentLog = experimentManager.getExperimentLog(id); diff --git a/submarine-server/server-core/src/test/java/org/apache/submarine/server/rest/ExperimentRestApiTest.java b/submarine-server/server-core/src/test/java/org/apache/submarine/server/rest/ExperimentRestApiTest.java new file mode 100644 index 0000000000..844f53de95 --- /dev/null +++ b/submarine-server/server-core/src/test/java/org/apache/submarine/server/rest/ExperimentRestApiTest.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.submarine.server.rest; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonArray; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.submarine.server.SubmarineServer; +import org.apache.submarine.server.api.experiment.Experiment; +import org.apache.submarine.server.api.experiment.ExperimentId; +import org.apache.submarine.server.api.experiment.ExperimentLog; +import org.apache.submarine.server.api.spec.EnvironmentSpec; +import org.apache.submarine.server.api.spec.ExperimentMeta; +import org.apache.submarine.server.api.spec.ExperimentSpec; +import org.apache.submarine.server.api.spec.KernelSpec; +import org.apache.submarine.server.experiment.ExperimentManager; +import org.apache.submarine.server.gson.ExperimentIdDeserializer; +import org.apache.submarine.server.gson.ExperimentIdSerializer; +import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.Before; + +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ExperimentRestApiTest { + + private static ExperimentRestApi experimentRestApi; + private static ExperimentManager mockExperimentManager; + private final AtomicInteger experimentCounter = new AtomicInteger(0); + EnvironmentSpec environmentSpec = new EnvironmentSpec(); + KernelSpec kernelSpec = new KernelSpec(); + ExperimentMeta meta = new ExperimentMeta(); + ExperimentSpec experimentSpec = new ExperimentSpec(); + private Experiment actualExperiment; + + private static final GsonBuilder gsonBuilder = new GsonBuilder() + .registerTypeAdapter(ExperimentId.class, new ExperimentIdSerializer()) + .registerTypeAdapter(ExperimentId.class, new ExperimentIdDeserializer()); + private static Gson gson = gsonBuilder.setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + + private static final String experimentAcceptedTime = "2020-08-06T08:39:22.000+08:00"; + private static final String experimentCreatedTime = "2020-08-06T08:39:22.000+08:00"; + private static final String experimentRunningTime = "2020-08-06T08:39:23.000+08:00"; + private static final String experimentFinishedTime = "2020-08-06T08:41:07.000+08:00"; + private static final String experimentName = "tf-example"; + private static final String experimentUid = "0b617cea-81fa-40b6-bbff-da3e400d2be4"; + private static final String experimentStatus = "Succeeded"; + private static final String metaName = "foo"; + private static final String metaFramework = "TensorFlow"; + private static final String metaNamespace = "fooNamespace"; + private static final String dockerImage = "continuumio/anaconda3"; + private static final String kernelSpecName = "team_default_python_3"; + private static final List kernelChannels = Arrays.asList("defaults", "anaconda"); + private static final List kernelDependencies = Arrays.asList( + "_ipyw_jlab_nb_ext_conf=0.1.0=py37_0", + "alabaster=0.7.12=py37_0", + "anaconda=2020.02=py37_0", + "anaconda-client=1.7.2=py37_0", + "anaconda-navigator=1.9.12=py37_0"); + private final ExperimentId experimentId = ExperimentId.newInstance(SubmarineServer.getServerTimeStamp(), + experimentCounter.incrementAndGet()); + + private final String dummyId = "experiment_1597012631706_0001"; + + @BeforeClass + public static void init() { + mockExperimentManager = mock(ExperimentManager.class); + experimentRestApi = new ExperimentRestApi(); + experimentRestApi.setExperimentManager(mockExperimentManager); + } + + @Before + public void testCreateExperiment() { + actualExperiment = new Experiment(); + actualExperiment.setAcceptedTime(experimentAcceptedTime); + actualExperiment.setCreatedTime(experimentCreatedTime); + actualExperiment.setRunningTime(experimentRunningTime); + actualExperiment.setFinishedTime(experimentFinishedTime); + actualExperiment.setUid(experimentUid); + actualExperiment.setName(experimentName); + actualExperiment.setStatus(experimentStatus); + actualExperiment.setExperimentId(experimentId); + kernelSpec.setName(kernelSpecName); + kernelSpec.setChannels(kernelChannels); + kernelSpec.setDependencies(kernelDependencies); + meta.setName(metaName); + meta.setFramework(metaFramework); + meta.setNamespace(metaNamespace); + environmentSpec.setDockerImage(dockerImage); + environmentSpec.setKernelSpec(kernelSpec); + experimentSpec.setMeta(meta); + experimentSpec.setEnvironment(environmentSpec); + actualExperiment.setSpec(experimentSpec); + when(mockExperimentManager.createExperiment(any(ExperimentSpec.class))).thenReturn(actualExperiment); + Response createExperimentResponse = experimentRestApi.createExperiment(experimentSpec); + assertEquals(Response.Status.OK.getStatusCode(), createExperimentResponse.getStatus()); + Experiment result = getResultFromResponse(createExperimentResponse, Experiment.class); + verifyResult(result, experimentUid); + } + + @Test + public void testGetExperiment() { + when(mockExperimentManager.getExperiment(any(String.class))).thenReturn(actualExperiment); + Response getExperimentResponse = experimentRestApi.getExperiment(dummyId); + Experiment result = getResultFromResponse(getExperimentResponse, Experiment.class); + verifyResult(result, experimentUid); + } + + @Test + public void testPatchExperiment() { + when(mockExperimentManager.patchExperiment(any(String.class), any(ExperimentSpec.class))). + thenReturn(actualExperiment); + Response patchExperimentResponse = experimentRestApi.patchExperiment(dummyId, new ExperimentSpec()); + Experiment result = getResultFromResponse(patchExperimentResponse, Experiment.class); + verifyResult(result, experimentUid); + } + + @Test + public void testListLog() { + List experimentLogList = new ArrayList<>(); + ExperimentLog log1 = new ExperimentLog(); + log1.setExperimentId(dummyId); + experimentLogList.add(log1); + when(mockExperimentManager.listExperimentLogsByStatus(any(String.class))).thenReturn(experimentLogList); + Response listLogResponse = experimentRestApi.listLog("running"); + List result = getResultListFromResponse(listLogResponse, ExperimentLog.class); + assertEquals(dummyId, result.get(0).getExperimentId()); + } + + @Test + public void testGetLog() { + ExperimentLog log1 = new ExperimentLog(); + log1.setExperimentId(dummyId); + when(mockExperimentManager.getExperimentLog(any(String.class))).thenReturn(log1); + Response logResponse = experimentRestApi.getLog(dummyId); + ExperimentLog result = getResultFromResponse(logResponse, ExperimentLog.class); + assertEquals(dummyId, result.getExperimentId()); + } + + @Test + public void testListExperiment() { + Experiment experiment2 = new Experiment(); + experiment2.rebuild(actualExperiment); + String experiment2Uid = "0b617cea-81fa-40b6-bbff-da3e400d2be5"; + experiment2.setUid(experiment2Uid); + experiment2.setExperimentId(experimentId); + List experimentList = new ArrayList<>(); + experimentList.add(actualExperiment); + experimentList.add(experiment2); + when(mockExperimentManager.listExperimentsByStatus(any(String.class))).thenReturn(experimentList); + Response listExperimentResponse = experimentRestApi.listExperiments(Response.Status.OK.toString()); + List result = getResultListFromResponse(listExperimentResponse, Experiment.class); + verifyResult(result.get(0), experimentUid); + verifyResult(result.get(1), experiment2Uid); + } + + @Test + public void testDeleteExperiment() { + String log1ID = "experiment_1597012631706_0002"; + when(mockExperimentManager.deleteExperiment(log1ID)).thenReturn(actualExperiment); + Response deleteExperimentResponse = experimentRestApi.deleteExperiment(log1ID); + Experiment result = getResultFromResponse(deleteExperimentResponse, Experiment.class); + verifyResult(result, experimentUid); + } + + private T getResultFromResponse(Response response, Class typeT) { + String entity = (String) response.getEntity(); + JsonObject object = new JsonParser().parse(entity).getAsJsonObject(); + JsonElement result = object.get("result"); + return gson.fromJson(result, typeT); + } + + private List getResultListFromResponse(Response response, Class typeT) { + String entity = (String) response.getEntity(); + JsonObject object = new JsonParser().parse(entity).getAsJsonObject(); + JsonElement result = object.get("result"); + List list = new ArrayList(); + JsonArray array = result.getAsJsonArray(); + for (JsonElement jsonElement : array) { + list.add(gson.fromJson(jsonElement, typeT)); + } + return list; + } + + private void verifyResult(Experiment experiment, String uid) { + assertEquals(uid, experiment.getUid()); + assertEquals(experimentCreatedTime, experiment.getCreatedTime()); + assertEquals(experimentRunningTime, experiment.getRunningTime()); + assertEquals(experimentAcceptedTime, experiment.getAcceptedTime()); + assertEquals(experimentName, experiment.getName()); + assertEquals(experimentStatus, experiment.getStatus()); + assertEquals(experimentId, experiment.getExperimentId()); + assertEquals(experimentFinishedTime, experiment.getFinishedTime()); + assertEquals(metaName, experiment.getSpec().getMeta().getName()); + assertEquals(metaFramework, experiment.getSpec().getMeta().getFramework()); + assertEquals(metaNamespace, experiment.getSpec().getMeta().getNamespace()); + assertEquals(dockerImage, experiment.getSpec().getEnvironment().getDockerImage()); + assertEquals(kernelChannels, experiment.getSpec().getEnvironment().getKernelSpec().getChannels()); + assertEquals(kernelSpecName, experiment.getSpec().getEnvironment().getKernelSpec().getName()); + assertEquals(kernelDependencies, experiment.getSpec().getEnvironment().getKernelSpec().getDependencies()); + } +}