From 92f938c9ed281cc60a46d8f92beaa7714d518336 Mon Sep 17 00:00:00 2001
From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de>
Date: Fri, 10 Jan 2025 14:27:53 +0100
Subject: [PATCH] Refactor paths to dta-tests-assignments

---
 .../dtabackend/rest/v1/task/TaskUpload.java   | 129 ++++++++-----
 .../dtabackend/utils/ExecuteTestUtil.java     | 177 +++++++++---------
 src/main/resources/application.properties     |   2 +-
 3 files changed, 169 insertions(+), 139 deletions(-)

diff --git a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java
index 2eec4e1..a070bef 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java
@@ -23,88 +23,117 @@ import java.util.regex.Pattern;
 import jakarta.servlet.annotation.MultipartConfig;
 
 /**
- * Rest controller for everything related to the TASK files
+ * Rest controller for handling code assignment file uploads and testing.
  */
 @RestController
 @RequestMapping("/v1/task/*")
 @MultipartConfig
 public class TaskUpload {
+
     private static final Logger LOG = LogManager.getLogger(TaskUpload.class);
 
     private final RepoUtil repoUtil;
     private final Path testTmpPath;
     private final ExecuteTestUtil executeTestUtil;
+    private final Tika tika;
 
-    public TaskUpload(
-        Environment env,
-        RepoUtil repoUtil,
-        ExecuteTestUtil executeTestUtil
-    ) {
+    public TaskUpload(Environment env, RepoUtil repoUtil, ExecuteTestUtil executeTestUtil) {
         this.repoUtil = repoUtil;
         this.executeTestUtil = executeTestUtil;
-
-        // set path of temporary directory on host and inside our container
         this.testTmpPath = Paths.get(env.getProperty("tests.tmp.dir"));
+        this.tika = new Tika();
     }
 
     @RequestMapping(method = RequestMethod.POST)
     public ResultSummary uploadAndTestFile(@RequestParam("taskFile") MultipartFile taskFileRef,
-                                           @RequestParam("assignmentId") String assignmentId
-    ) throws IOException, InterruptedException {
-        LOG.info("submission for testing received");
+                                           @RequestParam("assignmentId") String assignmentId)
+            throws IOException, InterruptedException {
+        LOG.info("Submission for testing received");
+
+        Path workDirectory = createWorkDirectory();
+        Path srcPath = defineSourcePath(workDirectory);
+
+        String mimeType = tika.detect(taskFileRef.getInputStream());
+        processUploadedFile(taskFileRef, srcPath, mimeType);
+
+        ResultSummary resultSummary = executeTestUtil.runTests(assignmentId, workDirectory);
+
+        if (isPlainTextFile(mimeType)) {
+            processTicketingInformation(taskFileRef, resultSummary);
+        }
 
-        LOG.debug("creating new temporary directory");
-        Path workDirectory = Files.createTempDirectory(testTmpPath, "dta");
-        LOG.debug(String.format("working dir for test is: %s", workDirectory.toAbsolutePath().toString()));
+        LOG.info("Submission tested successfully");
+        return resultSummary;
+    }
 
-        // define paths for the test, the submission and where the result is to be expected afterwards
-        Path srcPath    = Paths.get(workDirectory.toAbsolutePath().toString(), "src");
-        LOG.debug(String.format("Source path defined as: %s", srcPath.toAbsolutePath().toString()));
+    private Path createWorkDirectory() throws IOException {
+        LOG.debug("Creating new temporary directory");
+        Path workDirectory = Files.createTempDirectory(testTmpPath, "dta-submission");
+        LOG.debug("Working directory for test: {}", workDirectory.toAbsolutePath());
+        return workDirectory;
+    }
 
-        String mimeInfo = new Tika().detect(taskFileRef.getInputStream());
-        switch (mimeInfo) {
+    private Path defineSourcePath(Path workDirectory) {
+        Path srcPath = workDirectory.resolve("src");
+        LOG.debug("Source path defined as: {}", srcPath.toAbsolutePath());
+        return srcPath;
+    }
+
+    private void processUploadedFile(MultipartFile taskFileRef, Path srcPath, String mimeType) throws IOException {
+        switch (mimeType) {
             case "text/plain":
-                LOG.debug("textfile uploaded, searching for dta config");
-                // find URI in config file
-                String subDir="";
-                Matcher config = RegexUtil.extractConfig(taskFileRef.getInputStream(), Pattern.compile(RegexUtil.DTA_SUBMISSIONCONFIGREGEX));
-                if(config==null) {
-                	config = RegexUtil.extractConfig(taskFileRef.getInputStream(), Pattern.compile(RegexUtil.SUBMISSIONCONFIGREGEX));
-                	if(config==null)
-                	{
-                		throw new RuntimeException("couldn't find repo config for student submission clone");
-                	}
-                }
-                else {
-                	subDir=config.group(4);
-                }
-                LOG.debug("calling repo clone");
-                repoUtil.cloneRepository(config, srcPath.toAbsolutePath().toString(), subDir);
+                handlePlainTextFile(taskFileRef, srcPath);
                 break;
 
             case "application/zip":
-                LOG.debug("zip archive uploaded, extracting content as student submission");
-                ArchiveUtil.extractProjectFromZip(taskFileRef.getInputStream(), srcPath.toAbsolutePath());
+                handleZipFile(taskFileRef, srcPath);
                 break;
 
             default:
-                String msg = String.format("couldn't process uploaded file with mime type %s", mimeInfo);
-                LOG.error(msg);
-                throw new RuntimeException(msg);
+                handleUnsupportedFileType(mimeType);
         }
+    }
 
-        // run test
-        LOG.debug("calling test execution");
-        ResultSummary resultSummary = executeTestUtil.runTests(assignmentId, workDirectory);
+    private void handlePlainTextFile(MultipartFile taskFileRef, Path srcPath) throws IOException {
+        LOG.debug("Text file uploaded, searching for DTA config");
+
+        Matcher config = findRepositoryConfig(taskFileRef);
+        String subDir = config != null ? config.group(4) : "";
+
+        LOG.debug("Cloning repository");
+        repoUtil.cloneRepository(config, srcPath.toAbsolutePath().toString(), subDir);
+    }
 
-        if (mimeInfo.equals("text/plain")) {
-            LOG.info("check for provided Ticketsystem information");
-            UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary);
+    private Matcher findRepositoryConfig(MultipartFile taskFileRef) throws IOException {
+        Matcher config = RegexUtil.extractConfig(taskFileRef.getInputStream(), Pattern.compile(RegexUtil.DTA_SUBMISSIONCONFIGREGEX));
+
+        if (config == null) {
+            config = RegexUtil.extractConfig(taskFileRef.getInputStream(), Pattern.compile(RegexUtil.SUBMISSIONCONFIGREGEX));
+
+            if (config == null) {
+                throw new RuntimeException("Couldn't find repo config for student submission clone");
+            }
         }
-        
-        taskFileRef.getInputStream().close();
+        return config;
+    }
 
-        LOG.info("submission tested successfully");
-        return resultSummary;
+    private void handleZipFile(MultipartFile taskFileRef, Path srcPath) throws IOException {
+        LOG.debug("ZIP archive uploaded, extracting content");
+        ArchiveUtil.extractProjectFromZip(taskFileRef.getInputStream(), srcPath.toAbsolutePath());
+    }
+
+    private void handleUnsupportedFileType(String mimeType) {
+        String msg = String.format("Couldn't process uploaded file with MIME type: %s", mimeType);
+        LOG.error(msg);
+        throw new RuntimeException(msg);
+    }
+
+    private boolean isPlainTextFile(String mimeType) {
+        return "text/plain".equals(mimeType);
+    }
+
+    private void processTicketingInformation(MultipartFile taskFileRef, ResultSummary resultSummary) throws IOException {
+        LOG.info("Checking for provided Ticketing system information");
+        UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary);
     }
 }
diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
index 61541db..90973da 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
@@ -3,6 +3,7 @@ package de.hftstuttgart.dtabackend.utils;
 import com.fasterxml.jackson.core.exc.StreamReadException;
 import com.fasterxml.jackson.databind.DatabindException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.dockerjava.api.model.Bind;
 import com.github.dockerjava.api.model.Volume;
 
 import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile;
@@ -33,59 +34,59 @@ public class ExecuteTestUtil {
     private static final Logger LOG = LogManager.getLogger(ExecuteTestUtil.class);
 
     private final DockerUtil dockerUtil;
-    private final String assignmentBasePath;
-    private final String containerTestDir;
+    private final String basePath;
+    private final String containerBasePath;
 
-    public ExecuteTestUtil(
-        Environment env,
-        DockerUtil dockerUtil
-    ) {
+    public ExecuteTestUtil(Environment env, DockerUtil dockerUtil) {
         this.dockerUtil = dockerUtil;
 
         // set base path for assignments to be stored
         Path p = Paths.get(
             env.getProperty("data.dir"), ///data
             env.getProperty("data.dir.test.folder.name")); //UnitTests
-        this.assignmentBasePath = p.toAbsolutePath().toString();
-        this.containerTestDir = env.getProperty( "data.dir");
+        this.basePath = p.toAbsolutePath().toString();
+        this.containerBasePath = env.getProperty( "data.dir");
     }
 
     public ResultSummary runTests(String assignmentId, Path workDirectory) throws IOException, InterruptedException {
+        // Define paths for the submission-specific directories
+        String containerTestDir = this.containerBasePath + workDirectory.getFileName(); // /dta-test-assignments/dta-submissionID
+        Path testPath = Paths.get(containerTestDir, "test");
+        Path srcPath = Paths.get(containerTestDir, "src");
+        Path resultPath = Paths.get(containerTestDir, "result");
 
-        // Define paths for the test, the submission, and where the result is expected afterwards
-        String containerTestDir = this.containerTestDir; 
-        Path resultPath = Paths.get(containerTestDir, "result"); 
-        Path testPath = Paths.get(containerTestDir, "test");    
-        Path srcPath = Paths.get(containerTestDir, "src");      
+        // Ensure directories exist
+        Files.createDirectories(testPath);
+        Files.createDirectories(srcPath);
+        Files.createDirectories(resultPath);
 
         // Clone stored test to testPath
         LOG.debug("Copying pre-downloaded unit test repo");
-        FileUtil.copyFolder(
-            Paths.get(assignmentBasePath, assignmentId),
-            testPath);
+        FileUtil.copyFolder(Paths.get(basePath, assignmentId), testPath);
 
         LOG.debug("Copying exercise manifest");
-        Files.copy(Paths.get(
-                assignmentBasePath, assignmentId + "_checkout",
-                CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME),
-            Paths.get(
-                testPath.toString(), 
-                CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME));
+        Files.copy(
+            Paths.get(basePath, assignmentId + "_checkout", CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME),
+            testPath.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME)
+        );
 
         LOG.debug("Copying test config");
         Files.copy(
-            Paths.get(assignmentBasePath, assignmentId + ".txt"),
-            Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt"));
-
-        Files.createDirectory(resultPath);
+            Paths.get(basePath, assignmentId + ".txt"),
+            Paths.get(containerTestDir, "config.txt")
+        );
 
         LOG.info("Reading test config");
         Matcher config = RegexUtil.extractConfig(
-            new FileInputStream(Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt").toFile()), Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX));
-        String image = "";
+            new FileInputStream(Paths.get(containerTestDir, "config.txt").toFile()),
+            Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)
+        );
+        String image;
         if (config == null) {
             config = RegexUtil.extractConfig(
-                new FileInputStream(Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt").toFile()), Pattern.compile(RegexUtil.TESTCONFIGREGEX));
+                new FileInputStream(Paths.get(containerTestDir, "config.txt").toFile()),
+                Pattern.compile(RegexUtil.TESTCONFIGREGEX)
+            );
             if (config == null) {
                 throw new RuntimeException("Couldn't find repo config for unit test image extraction");
             }
@@ -94,35 +95,33 @@ public class ExecuteTestUtil {
             image = config.group(5);
         }
 
-        // Start the test-container with professor-given image and volume mounts for test, submission, and result
-        dockerUtil.runContainerWithVolumes(
+        // Start the test-container with professor-given image and submission-specific volume mounts
+        dockerUtil.runContainerWithBinds(
             image,
-            new Volume("test_volume: /data/test"),
-            new Volume("src_volume: /data/src"),
-            new Volume("result_volume: /data/result")
+            new Bind(testPath.toString(), new Volume("/data/test")),
+            new Bind(srcPath.toString(), new Volume("/data/src")),
+            new Bind(resultPath.toString(), new Volume("/data/result"))
         );
 
-        ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPath);
-        
-        return resultSummary;
+        return generateResult(assignmentId, resultPath, testPath);
     }
 
     private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPath)
             throws IOException, StreamReadException, DatabindException, MalformedURLException {
         // Define expected result file
-        File resultFile = Paths.get(resultPath.toAbsolutePath().toString(), "result.json").toFile();
+        File resultFile = resultPath.resolve("result.json").toFile();
 
         // Check if result file is there
         if (!resultFile.exists() || !resultFile.isFile()) {
-            LOG.error(String.format("Could not find result file in %s", resultFile.getAbsolutePath()));
+            LOG.error("Could not find result file in {}", resultFile.getAbsolutePath());
             throw new RuntimeException("No result file found");
         }
 
-        LOG.debug("Parse results JSON");
+        LOG.debug("Parsing results JSON");
         ObjectMapper objectMapper = new ObjectMapper();
         ResultSummary resultSummary = objectMapper.readValue(resultFile.toURI().toURL(), ResultSummary.class);
-        LOG.debug("Result JSON returned time " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results.");
-        
+        LOG.debug("Result JSON returned time {} with {} test results.", resultSummary.timestamp, resultSummary.results.size());
+
         LOG.info("Checking for optional test competency profile information for pedagogical agent functionality...");
         List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPath, CompetencyAssessmentUtil.TEST_COMPETENCY_MANIFEST_FILE_NAME);
         LOG.debug(String.format(
@@ -150,46 +149,48 @@ public class ExecuteTestUtil {
         }
         return resultSummary;
     }
-	
+
 	/*
 	 * exercise recommendation part
 	 */
-	public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary)
-			throws FileNotFoundException {
+	public List<Recommendation> recommendNextExercises(String assignmentId, Path testPath, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary)
+            throws FileNotFoundException {
 		// fetch repo url from original test upload
-		Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX);
-		File file = Paths.get(assignmentBasePath, assignmentId + ".txt").toFile();
-		FileInputStream configFileStream = new FileInputStream(file);
-		Matcher config = RegexUtil.extractConfig(configFileStream, pattern);
-		String testRepoURL = config.group(1);
+        Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX);
+        File configFile = Paths.get(basePath, assignmentId + ".txt").toFile();
+        Matcher config = RegexUtil.extractConfig(new FileInputStream(configFile), pattern);
+        String testRepoURL = config.group(1);
 
-		List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
+        List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(
+            testPath, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME
+        );
 
-		int currentTopicIndex=0;
-		float currentDifficulty=0.0f;
+		int currentTopicIndex = 0;
+		float currentDifficulty = 0.0f;
 
 		//build course topic order
-		Map<String, Integer> topicOrder = new HashMap<>();
-		int order = 1;
-		for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) {
-			if (!topicOrder.containsKey(e.exerciseTopicName)) {
-				topicOrder.put(e.exerciseTopicName, order++);
-			}
-			if (e.exerciseURL.equals(testRepoURL)) {
-				currentTopicIndex = order;
-				currentDifficulty = e.difficulty;
-			}
-		}
-		
+        Map<String, Integer> topicOrder = new HashMap<>();
+        int order = 1;
+
+        for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) {
+            if (!topicOrder.containsKey(e.exerciseTopicName)) {
+                topicOrder.put(e.exerciseTopicName, order++);
+            }
+            if (e.exerciseURL.equals(testRepoURL)) {
+                currentTopicIndex = order;
+                currentDifficulty = e.difficulty;
+            }
+        }
+
 		//filter exercises according to success
-		float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false);
-		List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty(
+        float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false);
+        List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty(
 				exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL,
 				currentDifficulty, unsuccessful, resultSummary);
-		
+
 		//compute recommendations
 		List<Recommendation> recommendedExercises = new ArrayList<>();
-		for (ExerciseCompetencyProfile exerciseProfile : filteredExercises) {
+        for (ExerciseCompetencyProfile exerciseProfile : filteredExercises) {
 			Recommendation recommendation = new Recommendation(exerciseProfile.exerciseTopicName, exerciseProfile.exerciseURL, exerciseProfile.exerciseName,
 					exerciseProfile.difficulty, calculateScore(exerciseProfile, unsuccessful, topicOrder, currentDifficulty));
 			recommendedExercises.add(recommendation);
@@ -199,7 +200,7 @@ public class ExecuteTestUtil {
 		recommendedExercises.stream().sorted(Recommendation.COMPARE_BY_SCORE).collect(Collectors.toList());
 		
 		return recommendedExercises;
-	}
+    }
 
 	public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> exerciseCompetencyProfiles,
 			Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful, 
@@ -208,34 +209,34 @@ public class ExecuteTestUtil {
 		//option for later: include next topic if fullsuccess and current difficulty == max difficulty
 		List<ExerciseCompetencyProfile> filteredExercises = exerciseCompetencyProfiles.stream()
 		        .filter(testProfile -> topicOrder.get(testProfile.exerciseTopicName) <= currentTopicIndex)
-		        .collect(Collectors.toList());
+            .collect(Collectors.toList());
 
 		//filter by difficulty according to success
-		if (isFullSuccess(unsuccessful)) {
+        if (isFullSuccess(unsuccessful)) {
 			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty >= currentDifficulty && !testRepoURL.equals(profile.exerciseURL)).collect(Collectors.toList());
-		} else {
+        } else {
 			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty <= currentDifficulty).collect(Collectors.toList());
-		}
+        }
 
 		return filteredExercises;
-	}
-	
-	public static boolean isFullSuccess(float[] unsuccessful) {
-		for (float value : unsuccessful) {
-			if (value != 0.0f) {
-				return false;
-			}
-		}
-		return true;
-	}
+    }
+
+    public static boolean isFullSuccess(float[] unsuccessful) {
+        for (float value : unsuccessful) {
+            if (value != 0.0f) {
+                return false;
+            }
+        }
+        return true;
+    }
 
 	public static float calculateScore(ExerciseCompetencyProfile exerciseProfile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) {
 		//ensure factor 1 for full success not to blank out score, thus offset the base
-		float score = 1.0f;
+        float score = 1.0f;
 		//competency profile difference to not fully achieved competencies component
 		for (int i = 0; i < exerciseProfile.competencyAssessments.length-1; i++) {
 			score += exerciseProfile.competencyAssessments[i] * unsuccessful[i];
-		}
+        }
 		
 		//difficulty component
 		score = score * (exerciseProfile.difficulty*(0.5f+Math.abs(currentDifficulty-exerciseProfile.difficulty)));
@@ -245,5 +246,5 @@ public class ExecuteTestUtil {
 
 		score = Math.round(score * 10.0f) / 10.0f;
 		return score;
-	}
-}
\ No newline at end of file
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 315cfe1..f0c5b3a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -9,7 +9,7 @@ spring.http.multipart.max-file-size=5Mb
 ###############################################
 
 # Holds the uploaded Zip-Files
-tests.tmp.dir=/tmp/dta-tests
+tests.tmp.dir=/dta-tests-assignments
 host.tests.tmp.dir=${tests.tmp.dir}
 data.dir=/data
 data.dir.test.folder.name=UnitTests
-- 
GitLab