From c47948b052b833f5c16709e0a98448078e5bef39 Mon Sep 17 00:00:00 2001
From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de>
Date: Thu, 9 Jan 2025 12:28:18 +0100
Subject: [PATCH] Added volumes for the backend host

---
 .../dtabackend/utils/DockerUtil.java          |  65 ++-
 .../dtabackend/utils/ExecuteTestUtil.java     | 382 +++++++-----------
 2 files changed, 214 insertions(+), 233 deletions(-)

diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/DockerUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/DockerUtil.java
index bf2d598..28510e0 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/utils/DockerUtil.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/utils/DockerUtil.java
@@ -5,6 +5,9 @@ import com.github.dockerjava.api.command.CreateContainerResponse;
 import com.github.dockerjava.api.exception.DockerException;
 import com.github.dockerjava.api.model.Bind;
 import com.github.dockerjava.api.model.HostConfig;
+import com.github.dockerjava.api.model.Mount;
+import com.github.dockerjava.api.model.MountType;
+import com.github.dockerjava.api.model.Volume;
 import com.github.dockerjava.core.DefaultDockerClientConfig;
 import com.github.dockerjava.core.DockerClientConfig;
 import com.github.dockerjava.core.DockerClientImpl;
@@ -15,6 +18,8 @@ import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 
 @Component
 public class DockerUtil {
@@ -37,7 +42,7 @@ public class DockerUtil {
         dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient);
     }
 
-    public int runContainer(String image, Bind... binds) throws InterruptedException, IOException
+    public int runContainerWithBinds(String image, Bind... binds) throws InterruptedException, IOException
     {
         LOG.debug(String.format("pull image: %s", image));
         try {
@@ -87,4 +92,62 @@ public class DockerUtil {
 
         return ret;
     }
+
+    public int runContainerWithVolumes(String image, Volume... volumes) throws InterruptedException, IOException {
+    LOG.debug(String.format("Pulling image: %s", image));
+    try {
+        dockerClient.pullImageCmd(image)
+            .start()
+            .awaitCompletion()
+            .close();
+    } catch (DockerException e) {
+        LOG.error(String.format(
+            "Pulling Docker image %s failed with %s, trying with local image",
+            image,
+            e.getMessage()));
+    }
+
+    LOG.debug("Creating container");
+    CreateContainerResponse containerResponse;
+    try {
+        // Prepare the host configuration with volumes
+        HostConfig hostConfig = HostConfig.newHostConfig()
+            .withMounts(
+                Arrays.stream(volumes)
+                    .map(volume -> new Mount().withTarget(volume.getPath()).withType(MountType.VOLUME))
+                    .collect(Collectors.toList())
+            );
+
+        // Create the container with the configured volumes
+        containerResponse = dockerClient.createContainerCmd("testcontainer")
+            .withImage(image)
+            .withHostConfig(hostConfig)
+            .exec();
+    } catch (DockerException e) {
+        LOG.error(String.format(
+            "Creating Docker Testrunner container failed with %s", e.getMessage()));
+        throw e;
+    }
+
+    LOG.debug(String.format("Container created: %s", containerResponse.getId()));
+
+    LOG.debug(String.format("Starting container %s", containerResponse.getId()));
+    dockerClient.startContainerCmd(containerResponse.getId()).exec();
+
+    LOG.debug(String.format("Waiting for completion of container %s", containerResponse.getId()));
+    int ret = dockerClient
+        .waitContainerCmd(containerResponse.getId())
+        .start()
+        .awaitCompletion()
+        .awaitStatusCode();
+    LOG.debug(String.format("Container completed with status %d", ret));
+
+    LOG.debug(String.format("Deleting container %s", containerResponse.getId()));
+    dockerClient.removeContainerCmd(containerResponse.getId())
+        .withRemoveVolumes(true)
+        .exec();
+
+    return ret;
+}
+
 }
diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
index e8bf1df..f35c69a 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
@@ -3,7 +3,6 @@ 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;
@@ -31,12 +30,11 @@ import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 @Component
-public class ExecuteTestUtil  {
+public class ExecuteTestUtil {
     private static final Logger LOG = LogManager.getLogger(ExecuteTestUtil.class);
 
     private final DockerUtil dockerUtil;
     private final String assignmentBasePath;
-    private final Path testTmpPathHost;
 
     public ExecuteTestUtil(
         Environment env,
@@ -49,281 +47,201 @@ public class ExecuteTestUtil  {
             env.getProperty("data.dir"),
             env.getProperty("data.dir.test.folder.name"));
         this.assignmentBasePath = p.toAbsolutePath().toString();
-
-        // set path of temporary directory on host _and_ inside our container, _must_ be identical 
-        this.testTmpPathHost = Paths.get(env.getProperty("host.tests.tmp.dir"));
     }
 
     public ResultSummary runTests(String assignmentId, Path workDirectory) throws IOException, InterruptedException {
 
-        // define paths for the test, the submission and where the result is to be expected afterwards
-        Path testPath   = Paths.get(workDirectory.toAbsolutePath().toString(), "/test");
-        Path srcPath    = Paths.get(workDirectory.toAbsolutePath().toString(), "/src");
-        Path resultPath = Paths.get(workDirectory.toAbsolutePath().toString(), "/result");
+        // Define paths for the test, the submission, and where the result is expected afterwards
+        Path testPath = Paths.get("/data/test");   // Volume mount path in the container
+        Path srcPath = Paths.get("/data/src");    // Volume mount path in the container
+        Path resultPath = Paths.get("/data/result"); // Volume mount path in the container
 
-        // clone stored test to tmpdir
-        LOG.debug("copying pre-downloaded unitttest repo");
+        // Clone stored test to testPath
+        LOG.debug("Copying pre-downloaded unit test repo");
         FileUtil.copyFolder(
             Paths.get(assignmentBasePath, assignmentId),
             testPath);
 
-		LOG.debug("copying exercise manifest from: %s in testPath: %s",
-		assignmentBasePath + assignmentId + "_checkout",
-		testPath.toString() );
-		Files.copy(Paths.get(
+        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));
+                CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME),
+            Paths.get(
+                testPath.toString(), 
+                CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME));
 
-        LOG.debug("copy test config");
+        LOG.debug("Copying test config");
         Files.copy(
             Paths.get(assignmentBasePath, assignmentId + ".txt"),
             Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt"));
 
         Files.createDirectory(resultPath);
 
-        LOG.info("reading test config");
+        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="";
-        if(config==null)
-        {
-        	config = RegexUtil.extractConfig(
-                    new FileInputStream(Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt").toFile()), Pattern.compile(RegexUtil.TESTCONFIGREGEX));
-        	if(config==null)
-        	{
-           		throw new RuntimeException("couldn't find repo config for unittest image extraction");
-        	}
-        	image=config.group(4);
-        }
-        else
-        {
-        	image=config.group(5);
+        String image = "";
+        if (config == null) {
+            config = RegexUtil.extractConfig(
+                new FileInputStream(Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt").toFile()), Pattern.compile(RegexUtil.TESTCONFIGREGEX));
+            if (config == null) {
+                throw new RuntimeException("Couldn't find repo config for unit test image extraction");
+            }
+            image = config.group(4);
+        } else {
+            image = config.group(5);
         }
-        // define the paths to mount as Binds from Host to the test-container
-        Path testPathHost = Paths.get(
-            testTmpPathHost.toAbsolutePath().toString(),
-            workDirectory.getName(workDirectory.getNameCount()-1).toString(),
-            testPath.getName(testPath.getNameCount()-1).toString()
-        );
-        Path srcPathHost = Paths.get(
-            testTmpPathHost.toAbsolutePath().toString(),
-            workDirectory.getName(workDirectory.getNameCount()-1).toString(),
-            srcPath.getName(srcPath.getNameCount()-1).toString()
-        );
-        Path resultPathHost = Paths.get(
-            testTmpPathHost.toAbsolutePath().toString(),
-            workDirectory.getName(workDirectory.getNameCount()-1).toString(),
-            resultPath.getName(resultPath.getNameCount()-1).toString()
-        );
 
-        // start test-container with professor given image and bind mounts for test, submission and result
+        // Start the test-container with professor-given image and volume mounts for test, submission, and result
         dockerUtil.runContainer(
             image,
-            new Bind(testPathHost.toAbsolutePath().toString(), new Volume("/data/test")),
-            new Bind(srcPathHost.toAbsolutePath().toString(), new Volume("/data/src")),
-            new Bind(resultPathHost.toAbsolutePath().toString(), new Volume("/data/result"))
+            new Volume("/data/test"),
+            new Volume("/data/src"),
+            new Volume("/data/result")
         );
 
-        ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPathHost);
+        ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPath);
         
         return resultSummary;
     }
 
-	private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost)
-			throws IOException, StreamReadException, DatabindException, MalformedURLException {
-		// define expected result file
+    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();
 
-        // check if result file is there
+        // Check if result file is there
         if (!resultFile.exists() || !resultFile.isFile()) {
             LOG.error(String.format("Could not find result file in %s", resultFile.getAbsolutePath()));
-            throw new RuntimeException("no resultfile found");
+            throw new RuntimeException("No result file found");
         }
 
-        LOG.debug("parse results json");
+        LOG.debug("Parse 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 " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results.");
         
-        LOG.info("Checking for optional test competency profile information for paedagogical agent functionality...");
-        List<TestCompetencyProfile> testCompetencyProfiles=CompetencyAssessmentUtil.readTestCompetencyProfiles(testPathHost, CompetencyAssessmentUtil.TEST_COMPETENCY_MANIFEST_FILE_NAME);
+        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(
-			"Reading Test Competency Profiles: basePath=%s, fileName=%s",
-			testPathHost,
-			CompetencyAssessmentUtil.TEST_COMPETENCY_MANIFEST_FILE_NAME
-		));
-		if(testCompetencyProfiles!=null) {
-        	LOG.info("Found optional test competency profiles, generating agent profile data...");
-        	resultSummary.overallTestCompetencyProfile=CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles));
-        	resultSummary.successfulTestCompetencyProfile=CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true));
-        	
-        	LOG.info("Checking for optional exercise competency profile information for paedagogical agent exercise recommendation functionality...");
-        	//testPathHost or assignmentBasePath
-        	Path exerciseManifestFile = Paths.get(testPathHost.toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
-			LOG.debug(String.format(
-				"Constructing Path for exercise manifest: testPath=%s, fileName=%s -> Resulting Path=%s",
-				testPathHost.toString(),
-				CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME,
-				exerciseManifestFile.toString()
-			));
-			if (Files.exists(exerciseManifestFile)) {
-				LOG.info("Found optional exercise competency profiles, generating recommendations...");
-				resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary);
-			}
+            "Reading Test Competency Profiles: basePath=%s, fileName=%s",
+            testPath,
+            CompetencyAssessmentUtil.TEST_COMPETENCY_MANIFEST_FILE_NAME
+        ));
+        if (testCompetencyProfiles != null) {
+            LOG.info("Found optional test competency profiles, generating agent profile data...");
+            resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles));
+            resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true));
+            
+            LOG.info("Checking for optional exercise competency profile information for pedagogical agent exercise recommendation functionality...");
+            Path exerciseManifestFile = Paths.get(testPath.toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
+            LOG.debug(String.format(
+                "Constructing Path for exercise manifest: testPath=%s, fileName=%s -> Resulting Path=%s",
+                testPath.toString(),
+                CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME,
+                exerciseManifestFile.toString()
+            ));
+            if (Files.exists(exerciseManifestFile)) {
+                LOG.info("Found optional exercise competency profiles, generating recommendations...");
+                resultSummary.recommendations = recommendNextExercises(assignmentId, testPath, testCompetencyProfiles, resultSummary);
+            }
         }
-		return resultSummary;
-	}
+        return resultSummary;
+    }
 	
-/*
- * exercise recommendation part
- */
-public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary)
-		throws FileNotFoundException {
-	LOG.debug("Starting recommendNextExercises with assignmentId: {}", assignmentId);
-
-	// fetch repo url from original test upload
-	Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX);
-	LOG.debug("Compiled regex pattern for DTA_TESTCONFIGREGEX.");
-
-	File file = Paths.get(assignmentBasePath, assignmentId + ".txt").toFile();
-	LOG.debug("Resolved file path for assignmentId {}: {}", assignmentId, file.getAbsolutePath());
-
-	FileInputStream configFileStream = new FileInputStream(file);
-	LOG.debug("Opened FileInputStream for file: {}", file.getAbsolutePath());
-
-	Matcher config = RegexUtil.extractConfig(configFileStream, pattern);
-	LOG.debug("Extracted configuration using regex pattern.");
-
-	String testRepoURL = config.group(1) + config.group(4);
-	LOG.debug("Constructed testRepoURL: {}", testRepoURL);
-
-	List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(
-			testPathHost, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
-	LOG.debug("Read exercise competency profiles from path: {}", testPathHost.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME));
-
-	int currentTopicIndex = 0;
-	float currentDifficulty = 0.0f;
-
-	// build course topic order
-	LOG.debug("Building 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++);
-			LOG.debug("Added topic {} to topicOrder with order {}", e.exerciseTopicName, order - 1);
+	/*
+	 * exercise recommendation part
+	 */
+	public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, 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);
+
+		List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
+
+		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;
+			}
 		}
-		if (e.exerciseURL.equals(testRepoURL)) {
-			currentTopicIndex = order;
-			currentDifficulty = e.difficulty;
-			LOG.debug("Matched current testRepoURL to topic: {}, index: {}, difficulty: {}", e.exerciseTopicName, currentTopicIndex, currentDifficulty);
+		
+		//filter exercises according to success
+		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) {
+			Recommendation recommendation = new Recommendation(exerciseProfile.exerciseTopicName, exerciseProfile.exerciseURL, exerciseProfile.exerciseName,
+					exerciseProfile.difficulty, calculateScore(exerciseProfile, unsuccessful, topicOrder, currentDifficulty));
+			recommendedExercises.add(recommendation);
+			LOG.info("Recommending exercise "+recommendation.topic+"/"+recommendation.exerciseName+" with score "+recommendation.score);
 		}
+		//sort the recommendations for successful or resilient learners, otherwise reverse in display
+		recommendedExercises.stream().sorted(Recommendation.COMPARE_BY_SCORE).collect(Collectors.toList());
+		
+		return recommendedExercises;
 	}
 
-	// filter exercises according to success
-	LOG.debug("Filtering exercises according to success.");
-	float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false);
-	LOG.debug("Computed unsuccessful competency profile: {}", unsuccessful);
-
-	List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty(
-			exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL,
-			currentDifficulty, unsuccessful, resultSummary);
-	LOG.debug("Filtered exercises count: {}", filteredExercises.size());
+	public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> exerciseCompetencyProfiles,
+			Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful, 
+			ResultSummary resultSummary) {
+		//filter out all advanced topics in any case
+		//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());
+
+		//filter by difficulty according to success
+		if (isFullSuccess(unsuccessful)) {
+			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty >= currentDifficulty && !testRepoURL.equals(profile.exerciseURL)).collect(Collectors.toList());
+		} else {
+			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty <= currentDifficulty).collect(Collectors.toList());
+		}
 
-	// compute recommendations
-	LOG.debug("Computing recommendations from filtered exercises.");
-	List<Recommendation> recommendedExercises = new ArrayList<>();
-	for (ExerciseCompetencyProfile exerciseProfile : filteredExercises) {
-		Recommendation recommendation = new Recommendation(
-				exerciseProfile.exerciseTopicName, exerciseProfile.exerciseURL, exerciseProfile.exerciseName,
-				exerciseProfile.difficulty, calculateScore(exerciseProfile, unsuccessful, topicOrder, currentDifficulty));
-		recommendedExercises.add(recommendation);
-		LOG.info("Recommending exercise {}/{} with score {}", recommendation.topic, recommendation.exerciseName, recommendation.score);
+		return filteredExercises;
+	}
+	
+	public static boolean isFullSuccess(float[] unsuccessful) {
+		for (float value : unsuccessful) {
+			if (value != 0.0f) {
+				return false;
+			}
+		}
+		return true;
 	}
 
-	// sort the recommendations for successful or resilient learners, otherwise reverse in display
-	LOG.debug("Sorting recommendations.");
-	recommendedExercises.stream().sorted(Recommendation.COMPARE_BY_SCORE).collect(Collectors.toList());
-
-	LOG.debug("Completed recommendNextExercises with {} recommendations.", recommendedExercises.size());
-	return recommendedExercises;
-}
-
-	public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(
-        List<ExerciseCompetencyProfile> exerciseCompetencyProfiles,
-        Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL,
-        float currentDifficulty, float[] unsuccessful, ResultSummary resultSummary) {
-
-    LOG.debug("Starting filterExercisesByTopicsAndDifficulty with currentTopicIndex: {}, currentDifficulty: {}, testRepoURL: {}",
-            currentTopicIndex, currentDifficulty, testRepoURL);
-
-    // Filter out all advanced topics in any case
-    List<ExerciseCompetencyProfile> filteredExercises = exerciseCompetencyProfiles.stream()
-            .filter(testProfile -> topicOrder.get(testProfile.exerciseTopicName) <= currentTopicIndex)
-            .collect(Collectors.toList());
-    LOG.debug("Filtered exercises by topic index. Remaining exercises count: {}", filteredExercises.size());
-
-    // Filter by difficulty according to success
-    if (isFullSuccess(unsuccessful)) {
-        LOG.debug("Detected full success, filtering exercises with difficulty >= {} and excluding current testRepoURL.", currentDifficulty);
-        filteredExercises = filteredExercises.stream()
-                .filter(profile -> profile.difficulty >= currentDifficulty && !testRepoURL.equals(profile.exerciseURL))
-                .collect(Collectors.toList());
-    } else {
-        LOG.debug("Detected partial success, filtering exercises with difficulty <= {}.", currentDifficulty);
-        filteredExercises = filteredExercises.stream()
-                .filter(profile -> profile.difficulty <= currentDifficulty)
-                .collect(Collectors.toList());
-    }
-
-    LOG.debug("Filtered exercises count after difficulty filter: {}", filteredExercises.size());
-    return filteredExercises;
-}
-
-public static boolean isFullSuccess(float[] unsuccessful) {
-    LOG.debug("Checking for full success. Unsuccessful array: {}", unsuccessful);
-    for (float value : unsuccessful) {
-        if (value != 0.0f) {
-            LOG.debug("Found non-zero value in unsuccessful array: {}. Returning false for full success.", value);
-            return false;
-        }
-    }
-    LOG.debug("All values in unsuccessful array are zero. Returning true for full success.");
-    return true;
-}
-
-public static float calculateScore(ExerciseCompetencyProfile exerciseProfile, float[] unsuccessful,
-                                   Map<String, Integer> topicOrder, float currentDifficulty) {
-    LOG.debug("Starting calculateScore for exercise: {}, difficulty: {}, currentDifficulty: {}",
-            exerciseProfile.exerciseName, exerciseProfile.difficulty, currentDifficulty);
-
-    float score = 1.0f;
-    LOG.debug("Initial score set to 1.0");
-
-    // Competency profile difference to not fully achieved competencies component
-    for (int i = 0; i < exerciseProfile.competencyAssessments.length - 1; i++) {
-        float adjustment = exerciseProfile.competencyAssessments[i] * unsuccessful[i];
-        score += adjustment;
-        LOG.debug("Adjusted score by competency index {}: {}, new score: {}", i, adjustment, score);
-    }
-
-    // Difficulty component
-    float difficultyComponent = exerciseProfile.difficulty * (0.5f + Math.abs(currentDifficulty - exerciseProfile.difficulty));
-    score = score * difficultyComponent;
-    LOG.debug("Applied difficulty component: {}, updated score: {}", difficultyComponent, score);
-
-    // Topic component
-    float topicMultiplier = topicOrder.get(exerciseProfile.exerciseTopicName);
-    score *= topicMultiplier;
-    LOG.debug("Applied topic multiplier: {}, updated score: {}", topicMultiplier, score);
-
-    // Round score
-    score = Math.round(score * 10.0f) / 10.0f;
-    LOG.debug("Final rounded score: {}", score);
-
-    return score;
-}
+	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;
+		//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)));
+		
+		//topic component
+		score *= topicOrder.get(exerciseProfile.exerciseTopicName);
+
+		score = Math.round(score * 10.0f) / 10.0f;
+		return score;
+	}
 }
\ No newline at end of file
-- 
GitLab