Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project revamp and docker integration #251

Merged
merged 30 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
744c649
added zipfile functionality, seperated adding input files from runnin…
Triver1 Apr 27, 2024
3de42d8
temp commit
Triver1 May 2, 2024
9b9151e
temp commit
Triver1 May 2, 2024
7f946da
Revamped Tests, not using files anymore but now using text for the te…
Triver1 May 5, 2024
355aa0f
Added test_finished functionality
Triver1 May 5, 2024
7184232
Merge branch 'development' into docker_integration_test_revamp
Triver1 May 5, 2024
1b19348
Fixed copy test to work with revamp
Triver1 May 5, 2024
25c4a1a
Delete backend/app/d:\test.zip
Aqua-sc May 6, 2024
6021861
small changes to checks and deleting
Aqua-sc May 6, 2024
e8ef87e
small update to checks
Aqua-sc May 6, 2024
1c840ca
PR feedback commit
Triver1 May 7, 2024
8f04b8d
added docer-test-state to database
Triver1 May 7, 2024
b65e433
small fixes
Aqua-sc May 7, 2024
ad2ee51
Updates too checks
Aqua-sc May 7, 2024
7a54c9c
PR feedback commit
Triver1 May 8, 2024
e306fc3
Made image uninstall happen threaded. Changed validTemplate so at lea…
Triver1 May 8, 2024
6aa5957
fixed artifacts not wokring
Triver1 May 8, 2024
adff602
simpele docker testing seems to work now
Aqua-sc May 9, 2024
133e658
Merge branch 'project_revamp_docker_integration' of https://github.co…
Aqua-sc May 9, 2024
7121013
Fixed template test and simple test 😎
Triver1 May 9, 2024
b0377a9
added database to script
Triver1 May 9, 2024
d669531
restructured testfeedback responses
Aqua-sc May 9, 2024
fd00a86
Added route to download artifacts
Aqua-sc May 9, 2024
5e4d1ee
fix zo structuretemplate gets saved correctly
Aqua-sc May 9, 2024
c35e8cb
fixed template validation
Triver1 May 9, 2024
59c24ca
added route to fetch all test related files
Aqua-sc May 9, 2024
b672fda
final fixes
Aqua-sc May 9, 2024
cc22cdd
tests should succeed now
Aqua-sc May 10, 2024
5e1b86a
maybe tests work now
Aqua-sc May 10, 2024
1630a15
hopefully files are excluded properly right now
Aqua-sc May 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ out/
.vscode/
backend/app/data/*
backend/data/*
backend/tmp/*
backend/app/tmp/*

### Secrets ###
backend/app/src/main/resources/application-secrets.properties
Expand Down
5 changes: 4 additions & 1 deletion backend/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,15 @@ dependencies {

task unitTests (type: Test){

exclude '**/DockerSubmissionTestTest.java'
exclude '**/docker'


useJUnitPlatform()
maxHeapSize = '1G'

testLogging {
events "passed"
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ public ResponseEntity<?> getGroupsOfProject(@PathVariable Long projectId, Auth a
* @return ResponseEntity with the status, no content
*/
@DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectId}")
@Roles({UserRole.teacher})
@Roles({UserRole.teacher, UserRole.student})
public ResponseEntity<?> deleteProjectById(@PathVariable long projectId, Auth auth) {
CheckResult<ProjectEntity> projectCheck = projectUtil.getProjectIfAdmin(projectId, auth.getUserEntity());
if (projectCheck.getStatus() != HttpStatus.OK) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
import com.ugent.pidgeon.model.json.GroupJson;
import com.ugent.pidgeon.model.json.LastGroupSubmissionJson;
import com.ugent.pidgeon.model.json.SubmissionJson;
import com.ugent.pidgeon.model.submissionTesting.DockerOutput;
import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel;
import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel;
import com.ugent.pidgeon.postgre.models.*;
import com.ugent.pidgeon.postgre.models.types.DockerTestState;
import com.ugent.pidgeon.postgre.models.types.DockerTestType;
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.logging.Level;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
Expand All @@ -25,12 +31,11 @@
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.zip.ZipFile;

@RestController
public class SubmissionController {
public class SubmissionController {

@Autowired
private GroupRepository groupRepository;
Expand All @@ -55,23 +60,62 @@ public class SubmissionController {
private EntityToJsonConverter entityToJsonConverter;
@Autowired
private CommonDatabaseActions commonDatabaseActions;
@Autowired
private TestUtil testUtil;


private SubmissionTemplateModel.SubmissionResult runStructureTest(ZipFile file, TestEntity testEntity) throws IOException {
// Get the test file from the server
FileEntity testfileEntity = fileRepository.findById(testEntity.getStructureTestId()).orElse(null);
if (testfileEntity == null) {
private SubmissionTemplateModel.SubmissionResult runStructureTest(ZipFile file, TestEntity testEntity) throws IOException {
// There is no structure test for this project
if(testEntity.getStructureTemplate() == null){
return null;
}
String testfile = Filehandler.getStructureTestString(Path.of(testfileEntity.getPath()));
String structureTemplateString = testEntity.getStructureTemplate();

// Parse the file
SubmissionTemplateModel model = new SubmissionTemplateModel();
model.parseSubmissionTemplate(testfile);

model.parseSubmissionTemplate(structureTemplateString);
return model.checkSubmission(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();

// 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);
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<File> artifacts = model.getArtifacts();

// Copy all files as zip into the output directory
if (artifacts != null && !artifacts.isEmpty()) {
Filehandler.copyFilesAsZip(artifacts, outputPath);
}

// Cleanup garbage files and container
model.cleanUp();

return output;

}

/**
* Function to get a submission by its ID
*
Expand All @@ -93,8 +137,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
Expand Down Expand Up @@ -170,7 +214,6 @@ public ResponseEntity<?> submitFile(@RequestParam("file") MultipartFile file, @P

long groupId = checkResult.getData();

//TODO: execute the docker tests onces these are implemented
try {
//Save the file entry in the database to get the id
FileEntity fileEntity = new FileEntity("", "", userId);
Expand Down Expand Up @@ -200,35 +243,78 @@ 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);
}

// Run structure tests
TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null);
SubmissionTemplateModel.SubmissionResult testresult;
if (testEntity == null) {
Logger.getLogger("SubmissionController").info("no test");
testresult = new SubmissionTemplateModel.SubmissionResult(true, "No structure requirements for this project.");
} else {
testresult = runStructureTest(new ZipFile(savedFile), testEntity);
}
if (testresult == null) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while running tests: test files not found");
}
submissionRepository.save(submissionEntity);
// Update the submission with the test resultsetAccepted
submission.setStructureAccepted(testresult.passed);
submission = submissionRepository.save(submission);
if (testEntity.getDockerTestTemplate() != null) {
submission.setDockerType(DockerTestType.TEMPLATE);
} else if (testEntity.getDockerTestScript() != null) {
submission.setDockerType(DockerTestType.SIMPLE);
} else {
submission.setDockerType(DockerTestType.NONE);
}

// Update the submission with the test feedbackfiles
submission.setDockerFeedback("TEMP DOCKER FEEDBACK");
submission.setStructureFeedback(testresult.feedback);
submissionRepository.save(submission);
// save the first feedback, without docker feedback
submissionRepository.save(submission);

if (testEntity.getDockerTestScript() != null) {
// Define docker test as running
submission.setDockerTestState(DockerTestState.running);
// run docker tests in background
File finalSavedFile = savedFile;
CompletableFuture.runAsync(() -> {
try {
// Check if docker tests succeed
DockerOutput dockerOutput = runDockerTest(new ZipFile(finalSavedFile), testEntity,
Filehandler.getSubmissionAritfactPath(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.getFeedbackAsString());
submission.setDockerAccepted(dockerOutput.isAllowed());

submission.setDockerTestState(DockerTestState.finished);
submissionRepository.save(submission);
} catch (Exception e) {
/* Log error */
Logger.getLogger("SubmissionController").log(Level.SEVERE, e.getMessage(), e);

submission.setDockerFeedback("");
submission.setDockerAccepted(false);

submission.setDockerTestState(DockerTestState.aborted);
submissionRepository.save(submission);

return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submissionEntity));
} catch (Exception 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
Expand Down Expand Up @@ -258,7 +344,10 @@ public ResponseEntity<?> getSubmissionFile(@PathVariable("submissionid") long su

// Get the file from the server
try {
Resource zipFile = Filehandler.getSubmissionAsResource(Path.of(file.getPath()));
Resource zipFile = Filehandler.getFileAsResource(Path.of(file.getPath()));
if (zipFile == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found.");
}

// Set headers for the response
HttpHeaders headers = new HttpHeaders();
Expand All @@ -273,54 +362,35 @@ public ResponseEntity<?> getSubmissionFile(@PathVariable("submissionid") long su
}
}


public ResponseEntity<?> getFeedbackReponseEntity(long submissionid, Auth auth, Function<SubmissionEntity, String> feedbackGetter) {

@GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/artifacts") //Route to get a submission
@Roles({UserRole.teacher, UserRole.student})
public ResponseEntity<?> getSubmissionArtifacts(@PathVariable("submissionid") long submissionid, Auth auth) {
CheckResult<SubmissionEntity> checkResult = submissionUtil.canGetSubmission(submissionid, auth.getUserEntity());
if (!checkResult.getStatus().equals(HttpStatus.OK)) {
return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage());
}
SubmissionEntity submission = checkResult.getData();

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.TEXT_PLAIN));
return ResponseEntity.ok().headers(headers).body(feedbackGetter.apply(submission));
}
// Get the file from the server
try {
Resource zipFile = Filehandler.getFileAsResource(Filehandler.getSubmissionAritfactPath(submission.getProjectId(), submission.getGroupId(), submission.getId()));
if (zipFile == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No artifacts found for this submission.");
}
// Set headers for the response
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + zipFile.getFilename());
headers.add(HttpHeaders.CONTENT_TYPE, "application/zip");

/**
* Function to get the structure feedback of a submission
*
* @param submissionid ID of the submission to get the feedback from
* @param auth authentication object of the requesting user
* @return ResponseEntity with the feedback
* @ApiDog <a href="https://apidog.com/apidoc/project-467959/api-6195994">apiDog documentation</a>
* @HttpMethod GET
* @AllowedRoles teacher, student
* @ApiPath /api/submissions/{submissionid}/structurefeedback
*/
@GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/structurefeedback")
//Route to get the structure feedback
@Roles({UserRole.teacher, UserRole.student})
public ResponseEntity<?> getStructureFeedback(@PathVariable("submissionid") long submissionid, Auth auth) {
return getFeedbackReponseEntity(submissionid, auth, SubmissionEntity::getStructureFeedback);
return ResponseEntity.ok()
.headers(headers)
.body(zipFile);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}

/**
* Function to get the docker feedback of a submission
*
* @param submissionid ID of the submission to get the feedback from
* @param auth authentication object of the requesting user
* @return ResponseEntity with the feedback
* @ApiDog <a href="https://apidog.com/apidoc/project-467959/api-6195996">apiDog documentation</a>
* @HttpMethod GET
* @AllowedRoles teacher, student
* @ApiPath /api/submissions/{submissionid}/dockerfeedback
*/
@GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/dockerfeedback") //Route to get the docker feedback
@Roles({UserRole.teacher, UserRole.student})
public ResponseEntity<?> getDockerFeedback(@PathVariable("submissionid") long submissionid, Auth auth) {
return getFeedbackReponseEntity(submissionid, auth, SubmissionEntity::getDockerFeedback);
}



/**
Expand Down
Loading
Loading