diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index 84f500b9..f3192d3e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -14,7 +14,10 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipException; import java.util.logging.Level; +import java.util.concurrent.CompletableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -76,40 +79,40 @@ private SubmissionTemplateModel.SubmissionResult runStructureTest(ZipFile file, private DockerOutput runDockerTest(ZipFile file, TestEntity testEntity, Path outputPath) throws IOException { - // Get the test file from the server - String testScript = testEntity.getDockerTestScript(); - String testTemplate = testEntity.getDockerTestTemplate(); - String image = testEntity.getDockerImage(); + // Get the test file from the server + String testScript = testEntity.getDockerTestScript(); + String testTemplate = testEntity.getDockerTestTemplate(); + String image = testEntity.getDockerImage(); - // The first script must always be null, otherwise there is nothing to run on the container - if(testScript == null){ - return null; - } - - // Init container and add input files - DockerSubmissionTestModel model = new DockerSubmissionTestModel(image); - model.addZipInputFiles(file); + // The first script must always be null, otherwise there is nothing to run on the container + if (testScript == null) { + return null; + } - // Copy artifacts to the destination - List artifacts = model.getArtifacts(); + // Init container and add input files + DockerSubmissionTestModel model = new DockerSubmissionTestModel(image); + model.addZipInputFiles(file); + DockerOutput output; + + if (testTemplate == null) { + // This docker test is configured in the simple mode (store test console logs) + output = model.runSubmission(testScript); + } else { + // This docker test is configured in the template mode (store json with feedback) + output = model.runSubmissionWithTemplate(testScript, testTemplate); + } + // Get list of artifact files generated on submission + List artifacts = model.getArtifacts(); - // filehandler copy zips - if (!artifacts.isEmpty()) { - Filehandler.copyFilesAsZip(artifacts, outputPath); - } + // Copy all files as zip into the output directory + Filehandler.copyFilesAsZip(artifacts, outputPath); + // Cleanup garbage files and container + model.cleanUp(); - // cleanup docker - model.cleanUp(); + return output; - if(testTemplate == null){ - // This docker test is configured in the simple mode (store test console logs) - return model.runSubmission(testScript); - }else{ - // This docker test is configured in the template mode (store json with feedback) - return model.runSubmissionWithTemplate(testScript, testTemplate); - } - } + } /** * Function to get a submission by its ID @@ -132,8 +135,8 @@ public ResponseEntity getSubmission(@PathVariable("submissionid") long submis SubmissionEntity submission = checkResult.getData(); SubmissionJson submissionJson = entityToJsonConverter.getSubmissionJson(submission); - return ResponseEntity.ok(submissionJson); - } + return ResponseEntity.ok(submissionJson); + } /** * Function to get all submissions @@ -238,51 +241,68 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P fileEntity.setPath(pathname); fileRepository.save(fileEntity); + // Run structure tests + TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null); + SubmissionTemplateModel.SubmissionResult structureTestResult; + if (testEntity == null) { + Logger.getLogger("SubmissionController").info("no tests"); + submission.setStructureFeedback("No specific structure requested for this project."); + submission.setStructureAccepted(true); + } else { + + // Check file structure + structureTestResult = runStructureTest(new ZipFile(savedFile), testEntity); + if (structureTestResult == null) { + submission.setStructureFeedback( + "No specific structure requested for this project."); + submission.setStructureAccepted(true); + } else { + submission.setStructureAccepted(structureTestResult.passed); + submission.setStructureFeedback(structureTestResult.feedback); + } + // Define docker test as running (1) + submission.setDockerTestState(2); + + // save the first feedback, without docker feedback + submissionRepository.save(submission); + + if (testEntity.getDockerTestScript() != null) { + // run docker tests in background + File finalSavedFile = savedFile; + CompletableFuture.runAsync(() -> { + try { + // Check if docker tests succeed + DockerOutput dockerOutput = runDockerTest(new ZipFile(finalSavedFile), testEntity, + Filehandler.getSubmissionPath(projectid, groupId, submission.getId())); + if (dockerOutput == null) { + throw new RuntimeException("Error while running docker tests."); + } + // Representation of dockerOutput, this will be a json(easily displayable in frontend) if it is a template test + // or a string if it is a simple test + submission.setDockerFeedback(dockerOutput.toString()); + submission.setDockerAccepted(dockerOutput.isAllowed()); + + submission.setDockerTestState(0); + submissionRepository.save(submission); + } catch (Exception e) { + + submission.setDockerFeedback(""); + submission.setDockerAccepted(false); + + submission.setDockerTestState(-1); + submissionRepository.save(submission); - // Run structure tests - TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null); - SubmissionTemplateModel.SubmissionResult structureTestResult; - DockerOutput dockerOutput; - if (testEntity == null) { - Logger.getLogger("SubmissionController").info("no tests"); - submission.setStructureFeedback("No specific structure requested for this project."); - submission.setStructureAccepted(true); - } else { - - // Check file structure - structureTestResult = runStructureTest(new ZipFile(savedFile), testEntity); - if (structureTestResult == null) { - submission.setStructureFeedback( - "No specific structure requested for this project."); - submission.setStructureAccepted(true); - } else { - submission.setStructureAccepted(structureTestResult.passed); - submission.setStructureFeedback(structureTestResult.feedback); - } - // Check if docker tests succeed - dockerOutput = runDockerTest(new ZipFile(savedFile), testEntity, Filehandler.getSubmissionPath(projectid, groupId, submission.getId()).resolve("artifacts.zip")); - if (dockerOutput == null) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error while running docker tests."); - } - // Representation of dockerOutput, this will be a json(easily displayable in frontend) if it is a template test - // or a string if it is a simple test - submission.setDockerFeedback(dockerOutput.getFeedbackAsString()); - submission.setDockerAccepted(dockerOutput.isAllowed()); } - submission.setTestFinished(true); - submissionRepository.save(submissionEntity); - // Update the dataabse - submission = submissionRepository.save(submission); - submissionRepository.save(submission); - - return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submissionEntity)); - } catch (Exception e) { - Logger.getGlobal().log(Level.SEVERE, e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving file: " + e.getMessage()); + }); } + } + return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submissionEntity)); + } catch (IOException ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to save submissions on file server."); } + } /** * Function to get a submission file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java index c2cbb01f..39a74e19 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java @@ -8,6 +8,8 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.*; @@ -136,45 +138,46 @@ private ResponseEntity alterTests( testEntity = new TestEntity(); } - // delete test entry - if(httpMethod.equals(HttpMethod.DELETE)){ - // first check if docker image is not used anywhere else - if(!testRepository.imageIsUsed(dockerImage)){ - // image is no longer required for any tests - DockerSubmissionTestModel.removeDockerImage(dockerImage); - } - - // delete test - testRepository.deleteById(testEntity.getId()); - projectEntity.setTestId(null); - projectRepository.save(projectEntity); - return ResponseEntity.ok().build(); - } + // Docker test + if(!(dockerImage == null && dockerScript == null && dockerTemplate == null)) { - //Update fields - if (dockerImage != null || !httpMethod.equals(HttpMethod.PATCH)) { + // update/install image if possible, do so in a seperate thread to reduce wait time. + String finalDockerImage = dockerImage; + CompletableFuture.runAsync(() -> { + if (finalDockerImage != null) { + DockerSubmissionTestModel.installImage(finalDockerImage); + } + }); testEntity.setDockerImage(dockerImage); - if (dockerImage == null && !testRepository.imageIsUsed(dockerImage)) { - DockerSubmissionTestModel.removeDockerImage(dockerImage); //TODO: move this to different thread if takes a while - } - } - if (dockerScript != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestScript(dockerScript); + testEntity.setDockerTestTemplate( + dockerTemplate); // If present, the test is in template mode + //Update fields + if (dockerImage != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerImage(dockerImage); + if (dockerImage == null && !testRepository.imageIsUsed(dockerImage)) { + DockerSubmissionTestModel.removeDockerImage( + dockerImage); //TODO: move this to different thread if takes a while + } + } + if (dockerScript != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestScript(dockerScript); + } + if (dockerTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestTemplate(dockerTemplate); + } + if (structureTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setStructureTemplate(structureTemplate); + } } - if (dockerTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { - testEntity.setDockerTestTemplate(dockerTemplate); - } - if (structureTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { - testEntity.setStructureTemplate(structureTemplate); - } - - // save test entity - testEntity = testRepository.save(testEntity); - projectEntity.setTestId(testEntity.getId()); - projectRepository.save(projectEntity); // make sure to update test id in project + // save test entity + testEntity = testRepository.save(testEntity); + projectEntity.setTestId(testEntity.getId()); + projectRepository.save(projectEntity); // make sure to update test id in project - return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); + return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java index a625c839..8352774c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java @@ -36,8 +36,8 @@ public class SubmissionEntity { @Column(name="docker_feedback") private String dockerFeedback; - @Column(name="test_finished") - private Boolean testFinished; + @Column(name="docker_test_state") + private Integer dockerTestState; public SubmissionEntity() { } @@ -119,13 +119,11 @@ public String getDockerFeedback() { public void setDockerFeedback(String dockerFeedbackFileId) { this.dockerFeedback = dockerFeedbackFileId; } - - public Boolean getTestFinished() { - return testFinished; + public Integer getDockerTestState() { + return dockerTestState; } - public void setTestFinished(Boolean testFinished) { - this.testFinished = testFinished; + public void setDockerTestState(Integer dockerTestState) { + this.dockerTestState = dockerTestState; } - } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java index b6fc5268..97529ab6 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java @@ -112,7 +112,7 @@ void templateTest() throws InterruptedException { DockerSubmissionTestModel.installImage("alpine:latest"); // Load docker container - DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); stm.addInputFiles(files); DockerTemplateTestOutput result = stm.runSubmissionWithTemplate(script, template); diff --git a/backend/database/start_database.sql b/backend/database/start_database.sql index 73487139..a07dcd10 100644 --- a/backend/database/start_database.sql +++ b/backend/database/start_database.sql @@ -108,7 +108,7 @@ CREATE TABLE submissions ( docker_accepted BOOLEAN NOT NULL, structure_feedback TEXT, docker_feedback TEXT, - tests_finished BOOLEAN DEFAULT FALSE, + docker_test_state INT DEFAULT 1, submission_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );