From 375b37bf188ffd26fdd0bf0d7aeab0170b63ea32 Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Wed, 13 Nov 2024 17:56:41 +0100 Subject: [PATCH 1/8] Refactor for backtracking manifesto --- .../utils/CompetencyAssessmentUtil.java | 284 ++++++++------ .../dtabackend/utils/ExecuteTestUtil.java | 345 ++++++++---------- 2 files changed, 321 insertions(+), 308 deletions(-) diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java index cdc3b43..4fcd484 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java @@ -3,134 +3,188 @@ package de.hftstuttgart.dtabackend.utils; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; -import java.net.MalformedURLException; +import java.nio.file.Files; import java.nio.file.Path; -import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import com.fasterxml.jackson.core.exc.StreamReadException; -import com.fasterxml.jackson.databind.DatabindException; - import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Result; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; -import java.io.FileNotFoundException; - public class CompetencyAssessmentUtil { - private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); - - public static String TEST_COMPETENCY_MANIFEST_FILE_NAME="competency-tests.mft"; - public static String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME="exercise-tests.mft"; - - /*public static void main(String[] args) throws StreamReadException, DatabindException, MalformedURLException, IOException { - ResultSummary summary=ExecuteTestUtil.generateResult("1", Path.of(args[0]), Path.of(args[1])); - System.out.println(summary.successfulTestCompetencyProfile); - } - */ - public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { - float[] tcpTotalProfile=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for(TestCompetencyProfile currentProfile: testCompetencyProfiles) { - tcpTotalProfile=ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); - } - return tcpTotalProfile; - } - - public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { - float[] sumSuccessful=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for(Result currentResult: resultSummary.results) { - boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); - if (isSuccess == success) { - TestCompetencyProfile currentProfile=new TestCompetencyProfile(); - currentProfile.testPackageName=(currentResult.packageName!=null)?currentResult.packageName:""; - currentProfile.testClassName=(currentResult.className!=null)?currentResult.className:""; - currentProfile.testName=(currentResult.name!=null)?currentResult.name:""; - int testIndex=testCompetencyProfiles.indexOf(currentProfile); - if(testIndex!=-1) { - sumSuccessful=ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); - } - } - } - return sumSuccessful; - } - - public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path testPath, String fileName) { - List<TestCompetencyProfile> testCompetencyProfiles=new ArrayList<TestCompetencyProfile>(); - try { - BufferedReader testCompetencyManifest=new BufferedReader(new FileReader(new File(testPath.toFile(), fileName))); - String testEntry=testCompetencyManifest.readLine(); - while(testEntry!=null) - { - String[] testEntyComponents=testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); - TestCompetencyProfile currentProfile=new TestCompetencyProfile(); - currentProfile.testPackageName=testEntyComponents[0]; - currentProfile.testClassName=testEntyComponents[1]; - currentProfile.testName=testEntyComponents[2]; - for(int competencyIndex=0; competencyIndex<ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex]=Float.valueOf(testEntyComponents[competencyIndex+3]); - } - testCompetencyProfiles.add(currentProfile); - testEntry=testCompetencyManifest.readLine(); - } - testCompetencyManifest.close(); - LOG.info("Added "+testCompetencyProfiles.size()+" test competency profiles from test competency manifest. Optional agent functionality enabled."); - } catch (FileNotFoundException e) { - LOG.info("Test competency manifest file for agent feedback not found. Skipping optional functionality."); - testCompetencyProfiles=null; - } catch (IOException e) { - LOG.info("Test competency manifest file for agent feedback unreadable. Skipping optional functionality."); - testCompetencyProfiles=null; - } - return testCompetencyProfiles; - } - - public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { - List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); - - try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(new File(exercisePath.toFile(), fileName)))) { - String exerciseEntry = exerciseCompetencyManifest.readLine(); - - while (exerciseEntry != null) { - String[] exerciseEntyComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); - ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); - - currentProfile.exerciseTopicName = exerciseEntyComponents[0]; - currentProfile.exerciseName = exerciseEntyComponents[1]; - currentProfile.exerciseURL = exerciseEntyComponents[2]; - - for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntyComponents[competencyIndex+3]); - } - - currentProfile.difficulty = Float.parseFloat(exerciseEntyComponents[19]); - - exerciseCompetencyProfiles.add(currentProfile); - - exerciseEntry = exerciseCompetencyManifest.readLine(); - } - exerciseCompetencyManifest.close(); - LOG.info("Added " + exerciseCompetencyProfiles.size() + " test competency profiles from exercise competency manifest."); - } catch (FileNotFoundException e) { - LOG.info("Exercise competency manifest file not found."); - } catch (IOException e) { - LOG.info("Exercise competency manifest file unreadable."); - } - - return exerciseCompetencyProfiles; - } - - public static String packFloats(float[] array) { - return IntStream.range(0, array.length) - .mapToObj(i -> String.valueOf(array[i])) - .collect(Collectors.joining(";")); - } + private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); + + public static final String TEST_COMPETENCY_MANIFEST_FILE_NAME = "competency-tests.mft"; + public static final String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME = "exercise-tests.mft"; + + /** + * Retrieves the base directory where the test competency manifest is located + * by traversing upwards until it finds the file "exercise-tests.mft". + * + * @param startDir Starting directory path to begin the search + * @return Path of the base directory if found; otherwise, null + */ + public static Path getBaseDirectory(Path startDir) { + Path currentDir = startDir; + while (currentDir != null) { + Path manifestPath = currentDir.resolve(EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + if (Files.exists(manifestPath)) { + return currentDir; + } + currentDir = currentDir.getParent(); + } + LOG.warn("Base directory with " + EXERCISE_COMPETENCY_MANIFEST_FILE_NAME + " not found starting from " + startDir); + return null; + } + + /** + * Reads and loads test competency profiles from the base directory. + * + * @param exercisePath Path to the starting exercise directory + * @return List of TestCompetencyProfile instances loaded from the manifest + */ + public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path exercisePath) { + Path baseDir = getBaseDirectory(exercisePath); + if (baseDir == null) { + LOG.error("Unable to locate the base directory for reading test competency profiles."); + return null; + } + return readTestCompetencyProfiles(baseDir, TEST_COMPETENCY_MANIFEST_FILE_NAME); + } + + /** + * Reads and loads exercise competency profiles from the specified exercise directory. + * + * @param exercisePath Path to the exercise directory + * @return List of ExerciseCompetencyProfile instances loaded from the manifest + */ + public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath) { + return readExerciseCompetencyProfiles(exercisePath, EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + } + + private static List<TestCompetencyProfile> readTestCompetencyProfiles(Path baseDir, String fileName) { + List<TestCompetencyProfile> testCompetencyProfiles = new ArrayList<>(); + Path manifestPath = baseDir.resolve(fileName); + + try (BufferedReader testCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { + String testEntry = testCompetencyManifest.readLine(); + while (testEntry != null) { + String[] testEntryComponents = testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); + TestCompetencyProfile currentProfile = new TestCompetencyProfile(); + currentProfile.testPackageName = testEntryComponents[0]; + currentProfile.testClassName = testEntryComponents[1]; + currentProfile.testName = testEntryComponents[2]; + for (int competencyIndex = 0; competencyIndex < ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(testEntryComponents[competencyIndex + 3]); + } + testCompetencyProfiles.add(currentProfile); + testEntry = testCompetencyManifest.readLine(); + } + LOG.info("Loaded " + testCompetencyProfiles.size() + " test competency profiles from " + manifestPath); + } catch (IOException e) { + LOG.error("Error reading test competency manifest file at " + manifestPath, e); + } + + return testCompetencyProfiles; + } + + private static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { + List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); + Path manifestPath = exercisePath.resolve(fileName); + + try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { + String exerciseEntry = exerciseCompetencyManifest.readLine(); + while (exerciseEntry != null) { + String[] exerciseEntryComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); + ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); + currentProfile.exerciseTopicName = exerciseEntryComponents[0]; + currentProfile.exerciseName = exerciseEntryComponents[1]; + currentProfile.exerciseURL = exerciseEntryComponents[2]; + for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntryComponents[competencyIndex + 3]); + } + currentProfile.difficulty = Float.parseFloat(exerciseEntryComponents[19]); + exerciseCompetencyProfiles.add(currentProfile); + exerciseEntry = exerciseCompetencyManifest.readLine(); + } + LOG.info("Loaded " + exerciseCompetencyProfiles.size() + " exercise competency profiles from " + manifestPath); + } catch (IOException e) { + LOG.error("Error reading exercise competency manifest file at " + manifestPath, e); + } + + return exerciseCompetencyProfiles; + } + + /** + * Converts an array of floats into a semicolon-separated string. + * + * @param array Array of float values to pack + * @return Semicolon-separated string of float values + */ + public static String packFloats(float[] array) { + return IntStream.range(0, array.length) + .mapToObj(i -> String.valueOf(array[i])) + .collect(Collectors.joining(";")); + } + + /** + * Sums the competency profiles across all test competency profiles. + * + * @param testCompetencyProfiles List of test competency profiles + * @return An array representing the sum of competencies + */ + public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { + float[] tcpTotalProfile = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for (TestCompetencyProfile currentProfile : testCompetencyProfiles) { + tcpTotalProfile = ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); + } + return tcpTotalProfile; + } + + /** + * Sums only the successful test competency profiles, based on the results summary. + * + * @param testCompetencyProfiles List of test competency profiles + * @param resultSummary The result summary containing test results + * @param success Indicates whether to sum successful (true) or unsuccessful (false) profiles + * @return An array representing the sum of competencies for successful or unsuccessful profiles + */ + public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { + float[] sumSuccessful = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for (Result currentResult : resultSummary.results) { + boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); + if (isSuccess == success) { + TestCompetencyProfile currentProfile = new TestCompetencyProfile(); + currentProfile.testPackageName = (currentResult.packageName != null) ? currentResult.packageName : ""; + currentProfile.testClassName = (currentResult.className != null) ? currentResult.className : ""; + currentProfile.testName = (currentResult.name != null) ? currentResult.name : ""; + int testIndex = testCompetencyProfiles.indexOf(currentProfile); + if (testIndex != -1) { + sumSuccessful = ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); + } + } + } + return sumSuccessful; + } + /** + * Checks if all elements in the competency profile array are zero (indicating full success). + * + * @param competencyProfile Array of competency values + * @return True if all values are zero; false otherwise + */ + public static boolean isFullSuccess(float[] competencyProfile) { + for (float value : competencyProfile) { + if (value != 0.0f) { + return false; + } + } + return true; + } } diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 333b177..73e5ced 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -7,7 +7,6 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; -import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Recommendation; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; @@ -22,10 +21,7 @@ import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -38,217 +34,180 @@ public class ExecuteTestUtil { private final String assignmentBasePath; private final Path testTmpPathHost; - 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( + this.assignmentBasePath = Paths.get( env.getProperty("data.dir"), - env.getProperty("data.dir.test.folder.name")); - this.assignmentBasePath = p.toAbsolutePath().toString(); + env.getProperty("data.dir.test.folder.name")) + .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"); - - // clone stored test to tmpdir - LOG.debug("copying pre-downloaded unitttest repo"); - FileUtil.copyFolder( - Paths.get(assignmentBasePath, assignmentId), - testPath); - - LOG.debug("copy test config"); - Files.copy( - Paths.get(assignmentBasePath, assignmentId + ".txt"), - Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt")); - + // Define paths + Path testPath = workDirectory.resolve("test"); + Path srcPath = workDirectory.resolve("src"); + Path resultPath = workDirectory.resolve("result"); + + // Clone test to temporary directory + LOG.debug("Copying pre-downloaded unittest repo"); + FileUtil.copyFolder(Paths.get(assignmentBasePath, assignmentId), testPath); + + // Copy configuration file + LOG.debug("Copy test config"); + Files.copy(Paths.get(assignmentBasePath, assignmentId + ".txt"), workDirectory.resolve("config.txt")); Files.createDirectory(resultPath); - 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); - } - // 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() - ); + // Load container image configuration + LOG.info("Reading test config"); + String image = loadImageConfig(workDirectory); + + // Define paths to mount in the container + Path testPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(testPath.getFileName()); + Path srcPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(srcPath.getFileName()); + Path resultPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(resultPath.getFileName()); - // start test-container with professor given image and bind mounts for test, submission and result + // Start container with mounts 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 Bind(testPathHost.toString(), new Volume("/data/test")), + new Bind(srcPathHost.toString(), new Volume("/data/src")), + new Bind(resultPathHost.toString(), new Volume("/data/result")) ); - ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPathHost); - - return resultSummary; + return generateResult(assignmentId, resultPath, testPathHost); + } + + private String loadImageConfig(Path workDirectory) throws FileNotFoundException { + Matcher config = RegexUtil.extractConfig( + new FileInputStream(workDirectory.resolve("config.txt").toFile()), + Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)); + + if (config == null) { + config = RegexUtil.extractConfig( + new FileInputStream(workDirectory.resolve("config.txt").toFile()), + Pattern.compile(RegexUtil.TESTCONFIGREGEX)); + + if (config == null) { + throw new RuntimeException("Couldn't find repo config for unittest image extraction"); + } + return config.group(4); + } + return config.group(5); } - private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) - throws IOException, StreamReadException, DatabindException, MalformedURLException { - // define expected result file - File resultFile = Paths.get(resultPath.toAbsolutePath().toString(), "result.json").toFile(); + private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) + throws IOException, StreamReadException, DatabindException, MalformedURLException { + 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())); - 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 at " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results."); + + LOG.info("Checking for optional test competency profile information..."); + List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPathHost); + + if (testCompetencyProfiles != null) { + LOG.info("Found test competency profiles, generating profile data..."); + resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles)); + resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true)); + + Path exerciseManifestFile = testPathHost.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + if (Files.exists(exerciseManifestFile)) { + LOG.info("Found exercise competency profiles, generating recommendations..."); + resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); + } + } + return resultSummary; + } + + public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary) + throws FileNotFoundException { + + String testRepoURL = RegexUtil.extractConfig( + new FileInputStream(Paths.get(assignmentBasePath, assignmentId + ".txt").toFile()), + Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)).group(1); + + List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost); + + int currentTopicIndex = 0; + float currentDifficulty = 0.0f; + Map<String, Integer> topicOrder = new HashMap<>(); + int order = 1; + + // Determine currentTopicIndex and set currentDifficulty based on exercise profiles + for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) { + if (!topicOrder.containsKey(e.exerciseTopicName)) { + topicOrder.put(e.exerciseTopicName, order++); + } + if (e.exerciseURL.equals(testRepoURL)) { + currentTopicIndex = order; + currentDifficulty = e.difficulty; // Directly assign to currentDifficulty + } + } + + // Sum competencies for unsuccessful tests + float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false); + + // Filter exercises based on topics and difficulty + List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty( + exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL, currentDifficulty, unsuccessful); + + // Generate recommendations without using lambda expressions + List<Recommendation> recommendedExercises = new ArrayList<>(); + for (ExerciseCompetencyProfile profile : filteredExercises) { + float score = calculateScore(profile, unsuccessful, topicOrder, currentDifficulty); + Recommendation recommendation = new Recommendation( + profile.exerciseTopicName, + profile.exerciseURL, + profile.exerciseName, + profile.difficulty, + score); + recommendedExercises.add(recommendation); + } + + // Sort the recommendations using Collections.sort and a comparator + Collections.sort(recommendedExercises, Recommendation.COMPARE_BY_SCORE); + + return recommendedExercises; +} + + public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> profiles, + Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful) { - 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); - 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.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - if (Files.exists(exerciseManifestFile)) { - LOG.info("Found optional exercise competency profiles, generating recommendations..."); - resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); - } + List<ExerciseCompetencyProfile> filteredExercises = profiles.stream() + .filter(profile -> topicOrder.get(profile.exerciseTopicName) <= currentTopicIndex) + .collect(Collectors.toList()); + + if (CompetencyAssessmentUtil.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()); } - return resultSummary; - } - - /* - * 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; - } - } - - //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; - } - - 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()); - } - - return filteredExercises; - } - - 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; - //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; - } + + return filteredExercises; + } + + public static float calculateScore(ExerciseCompetencyProfile profile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) { + float score = 1.0f; + for (int i = 0; i < profile.competencyAssessments.length - 1; i++) { + score += profile.competencyAssessments[i] * unsuccessful[i]; + } + + score *= profile.difficulty * (0.5f + Math.abs(currentDifficulty - profile.difficulty)); + score *= topicOrder.getOrDefault(profile.exerciseTopicName, 1); + + return Math.round(score * 10.0f) / 10.0f; + } } \ No newline at end of file -- GitLab From 33313e942cacd5965e709aa532e4fddcf6b102a3 Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Tue, 19 Nov 2024 14:38:40 +0100 Subject: [PATCH 2/8] Revert "Refactor for backtracking manifesto" This reverts commit 375b37bf188ffd26fdd0bf0d7aeab0170b63ea32. --- .../utils/CompetencyAssessmentUtil.java | 284 ++++++-------- .../dtabackend/utils/ExecuteTestUtil.java | 345 ++++++++++-------- 2 files changed, 308 insertions(+), 321 deletions(-) diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java index 4fcd484..cdc3b43 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java @@ -3,188 +3,134 @@ package de.hftstuttgart.dtabackend.utils; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; -import java.nio.file.Files; +import java.net.MalformedURLException; import java.nio.file.Path; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; + import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Result; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; -public class CompetencyAssessmentUtil { - private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); - - public static final String TEST_COMPETENCY_MANIFEST_FILE_NAME = "competency-tests.mft"; - public static final String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME = "exercise-tests.mft"; - - /** - * Retrieves the base directory where the test competency manifest is located - * by traversing upwards until it finds the file "exercise-tests.mft". - * - * @param startDir Starting directory path to begin the search - * @return Path of the base directory if found; otherwise, null - */ - public static Path getBaseDirectory(Path startDir) { - Path currentDir = startDir; - while (currentDir != null) { - Path manifestPath = currentDir.resolve(EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - if (Files.exists(manifestPath)) { - return currentDir; - } - currentDir = currentDir.getParent(); - } - LOG.warn("Base directory with " + EXERCISE_COMPETENCY_MANIFEST_FILE_NAME + " not found starting from " + startDir); - return null; - } - - /** - * Reads and loads test competency profiles from the base directory. - * - * @param exercisePath Path to the starting exercise directory - * @return List of TestCompetencyProfile instances loaded from the manifest - */ - public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path exercisePath) { - Path baseDir = getBaseDirectory(exercisePath); - if (baseDir == null) { - LOG.error("Unable to locate the base directory for reading test competency profiles."); - return null; - } - return readTestCompetencyProfiles(baseDir, TEST_COMPETENCY_MANIFEST_FILE_NAME); - } - - /** - * Reads and loads exercise competency profiles from the specified exercise directory. - * - * @param exercisePath Path to the exercise directory - * @return List of ExerciseCompetencyProfile instances loaded from the manifest - */ - public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath) { - return readExerciseCompetencyProfiles(exercisePath, EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - } - - private static List<TestCompetencyProfile> readTestCompetencyProfiles(Path baseDir, String fileName) { - List<TestCompetencyProfile> testCompetencyProfiles = new ArrayList<>(); - Path manifestPath = baseDir.resolve(fileName); +import java.io.FileNotFoundException; - try (BufferedReader testCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { - String testEntry = testCompetencyManifest.readLine(); - while (testEntry != null) { - String[] testEntryComponents = testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); - TestCompetencyProfile currentProfile = new TestCompetencyProfile(); - currentProfile.testPackageName = testEntryComponents[0]; - currentProfile.testClassName = testEntryComponents[1]; - currentProfile.testName = testEntryComponents[2]; - for (int competencyIndex = 0; competencyIndex < ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(testEntryComponents[competencyIndex + 3]); - } - testCompetencyProfiles.add(currentProfile); - testEntry = testCompetencyManifest.readLine(); - } - LOG.info("Loaded " + testCompetencyProfiles.size() + " test competency profiles from " + manifestPath); - } catch (IOException e) { - LOG.error("Error reading test competency manifest file at " + manifestPath, e); - } - - return testCompetencyProfiles; - } - - private static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { - List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); - Path manifestPath = exercisePath.resolve(fileName); - - try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { - String exerciseEntry = exerciseCompetencyManifest.readLine(); - while (exerciseEntry != null) { - String[] exerciseEntryComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); - ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); - currentProfile.exerciseTopicName = exerciseEntryComponents[0]; - currentProfile.exerciseName = exerciseEntryComponents[1]; - currentProfile.exerciseURL = exerciseEntryComponents[2]; - for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntryComponents[competencyIndex + 3]); - } - currentProfile.difficulty = Float.parseFloat(exerciseEntryComponents[19]); - exerciseCompetencyProfiles.add(currentProfile); - exerciseEntry = exerciseCompetencyManifest.readLine(); - } - LOG.info("Loaded " + exerciseCompetencyProfiles.size() + " exercise competency profiles from " + manifestPath); - } catch (IOException e) { - LOG.error("Error reading exercise competency manifest file at " + manifestPath, e); - } - - return exerciseCompetencyProfiles; - } - - /** - * Converts an array of floats into a semicolon-separated string. - * - * @param array Array of float values to pack - * @return Semicolon-separated string of float values - */ - public static String packFloats(float[] array) { - return IntStream.range(0, array.length) - .mapToObj(i -> String.valueOf(array[i])) - .collect(Collectors.joining(";")); - } - - /** - * Sums the competency profiles across all test competency profiles. - * - * @param testCompetencyProfiles List of test competency profiles - * @return An array representing the sum of competencies - */ - public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { - float[] tcpTotalProfile = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for (TestCompetencyProfile currentProfile : testCompetencyProfiles) { - tcpTotalProfile = ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); - } - return tcpTotalProfile; - } - - /** - * Sums only the successful test competency profiles, based on the results summary. - * - * @param testCompetencyProfiles List of test competency profiles - * @param resultSummary The result summary containing test results - * @param success Indicates whether to sum successful (true) or unsuccessful (false) profiles - * @return An array representing the sum of competencies for successful or unsuccessful profiles - */ - public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { - float[] sumSuccessful = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for (Result currentResult : resultSummary.results) { - boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); - if (isSuccess == success) { - TestCompetencyProfile currentProfile = new TestCompetencyProfile(); - currentProfile.testPackageName = (currentResult.packageName != null) ? currentResult.packageName : ""; - currentProfile.testClassName = (currentResult.className != null) ? currentResult.className : ""; - currentProfile.testName = (currentResult.name != null) ? currentResult.name : ""; - int testIndex = testCompetencyProfiles.indexOf(currentProfile); - if (testIndex != -1) { - sumSuccessful = ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); - } - } - } - return sumSuccessful; - } +public class CompetencyAssessmentUtil { + private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); + + public static String TEST_COMPETENCY_MANIFEST_FILE_NAME="competency-tests.mft"; + public static String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME="exercise-tests.mft"; + + /*public static void main(String[] args) throws StreamReadException, DatabindException, MalformedURLException, IOException { + ResultSummary summary=ExecuteTestUtil.generateResult("1", Path.of(args[0]), Path.of(args[1])); + System.out.println(summary.successfulTestCompetencyProfile); + } + */ + public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { + float[] tcpTotalProfile=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for(TestCompetencyProfile currentProfile: testCompetencyProfiles) { + tcpTotalProfile=ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); + } + return tcpTotalProfile; + } + + public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { + float[] sumSuccessful=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for(Result currentResult: resultSummary.results) { + boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); + if (isSuccess == success) { + TestCompetencyProfile currentProfile=new TestCompetencyProfile(); + currentProfile.testPackageName=(currentResult.packageName!=null)?currentResult.packageName:""; + currentProfile.testClassName=(currentResult.className!=null)?currentResult.className:""; + currentProfile.testName=(currentResult.name!=null)?currentResult.name:""; + int testIndex=testCompetencyProfiles.indexOf(currentProfile); + if(testIndex!=-1) { + sumSuccessful=ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); + } + } + } + return sumSuccessful; + } + + public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path testPath, String fileName) { + List<TestCompetencyProfile> testCompetencyProfiles=new ArrayList<TestCompetencyProfile>(); + try { + BufferedReader testCompetencyManifest=new BufferedReader(new FileReader(new File(testPath.toFile(), fileName))); + String testEntry=testCompetencyManifest.readLine(); + while(testEntry!=null) + { + String[] testEntyComponents=testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); + TestCompetencyProfile currentProfile=new TestCompetencyProfile(); + currentProfile.testPackageName=testEntyComponents[0]; + currentProfile.testClassName=testEntyComponents[1]; + currentProfile.testName=testEntyComponents[2]; + for(int competencyIndex=0; competencyIndex<ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex]=Float.valueOf(testEntyComponents[competencyIndex+3]); + } + testCompetencyProfiles.add(currentProfile); + testEntry=testCompetencyManifest.readLine(); + } + testCompetencyManifest.close(); + LOG.info("Added "+testCompetencyProfiles.size()+" test competency profiles from test competency manifest. Optional agent functionality enabled."); + } catch (FileNotFoundException e) { + LOG.info("Test competency manifest file for agent feedback not found. Skipping optional functionality."); + testCompetencyProfiles=null; + } catch (IOException e) { + LOG.info("Test competency manifest file for agent feedback unreadable. Skipping optional functionality."); + testCompetencyProfiles=null; + } + return testCompetencyProfiles; + } + + public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { + List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); + + try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(new File(exercisePath.toFile(), fileName)))) { + String exerciseEntry = exerciseCompetencyManifest.readLine(); + + while (exerciseEntry != null) { + String[] exerciseEntyComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); + ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); + + currentProfile.exerciseTopicName = exerciseEntyComponents[0]; + currentProfile.exerciseName = exerciseEntyComponents[1]; + currentProfile.exerciseURL = exerciseEntyComponents[2]; + + for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntyComponents[competencyIndex+3]); + } + + currentProfile.difficulty = Float.parseFloat(exerciseEntyComponents[19]); + + exerciseCompetencyProfiles.add(currentProfile); + + exerciseEntry = exerciseCompetencyManifest.readLine(); + } + exerciseCompetencyManifest.close(); + LOG.info("Added " + exerciseCompetencyProfiles.size() + " test competency profiles from exercise competency manifest."); + } catch (FileNotFoundException e) { + LOG.info("Exercise competency manifest file not found."); + } catch (IOException e) { + LOG.info("Exercise competency manifest file unreadable."); + } + + return exerciseCompetencyProfiles; + } + + public static String packFloats(float[] array) { + return IntStream.range(0, array.length) + .mapToObj(i -> String.valueOf(array[i])) + .collect(Collectors.joining(";")); + } - /** - * Checks if all elements in the competency profile array are zero (indicating full success). - * - * @param competencyProfile Array of competency values - * @return True if all values are zero; false otherwise - */ - public static boolean isFullSuccess(float[] competencyProfile) { - for (float value : competencyProfile) { - if (value != 0.0f) { - return false; - } - } - return true; - } } diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 73e5ced..333b177 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -7,6 +7,7 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; +import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Recommendation; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; @@ -21,7 +22,10 @@ import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -34,180 +38,217 @@ public class ExecuteTestUtil { private final String assignmentBasePath; private final Path testTmpPathHost; - public ExecuteTestUtil(Environment env, DockerUtil dockerUtil) { + public ExecuteTestUtil( + Environment env, + DockerUtil dockerUtil + ) { this.dockerUtil = dockerUtil; - this.assignmentBasePath = Paths.get( + + // set base path for assignments to be stored + Path p = Paths.get( env.getProperty("data.dir"), - env.getProperty("data.dir.test.folder.name")) - .toAbsolutePath().toString(); + 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 - Path testPath = workDirectory.resolve("test"); - Path srcPath = workDirectory.resolve("src"); - Path resultPath = workDirectory.resolve("result"); - - // Clone test to temporary directory - LOG.debug("Copying pre-downloaded unittest repo"); - FileUtil.copyFolder(Paths.get(assignmentBasePath, assignmentId), testPath); - - // Copy configuration file - LOG.debug("Copy test config"); - Files.copy(Paths.get(assignmentBasePath, assignmentId + ".txt"), workDirectory.resolve("config.txt")); - Files.createDirectory(resultPath); - // Load container image configuration - LOG.info("Reading test config"); - String image = loadImageConfig(workDirectory); + // 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 to mount in the container - Path testPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(testPath.getFileName()); - Path srcPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(srcPath.getFileName()); - Path resultPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(resultPath.getFileName()); + // clone stored test to tmpdir + LOG.debug("copying pre-downloaded unitttest repo"); + FileUtil.copyFolder( + Paths.get(assignmentBasePath, assignmentId), + testPath); - // Start container with mounts - dockerUtil.runContainer( - image, - new Bind(testPathHost.toString(), new Volume("/data/test")), - new Bind(srcPathHost.toString(), new Volume("/data/src")), - new Bind(resultPathHost.toString(), new Volume("/data/result")) - ); + LOG.debug("copy test config"); + Files.copy( + Paths.get(assignmentBasePath, assignmentId + ".txt"), + Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt")); - return generateResult(assignmentId, resultPath, testPathHost); - } + Files.createDirectory(resultPath); - private String loadImageConfig(Path workDirectory) throws FileNotFoundException { + LOG.info("reading test config"); Matcher config = RegexUtil.extractConfig( - new FileInputStream(workDirectory.resolve("config.txt").toFile()), - Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)); - - if (config == null) { - config = RegexUtil.extractConfig( - new FileInputStream(workDirectory.resolve("config.txt").toFile()), - Pattern.compile(RegexUtil.TESTCONFIGREGEX)); - - if (config == null) { - throw new RuntimeException("Couldn't find repo config for unittest image extraction"); - } - return config.group(4); + 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); } - return 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 + 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")) + ); + + ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPathHost); + + return resultSummary; } - private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) - throws IOException, StreamReadException, DatabindException, MalformedURLException { - File resultFile = resultPath.resolve("result.json").toFile(); + private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) + 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 if (!resultFile.exists() || !resultFile.isFile()) { LOG.error(String.format("Could not find result file in %s", resultFile.getAbsolutePath())); - throw new RuntimeException("No result file found"); + throw new RuntimeException("no resultfile 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 at " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results."); - - LOG.info("Checking for optional test competency profile information..."); - List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPathHost); - - if (testCompetencyProfiles != null) { - LOG.info("Found test competency profiles, generating profile data..."); - resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles)); - resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true)); - - Path exerciseManifestFile = testPathHost.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - if (Files.exists(exerciseManifestFile)) { - LOG.info("Found exercise competency profiles, generating recommendations..."); - resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); - } - } - return resultSummary; - } - - public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary) - throws FileNotFoundException { - - String testRepoURL = RegexUtil.extractConfig( - new FileInputStream(Paths.get(assignmentBasePath, assignmentId + ".txt").toFile()), - Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)).group(1); - - List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost); - - int currentTopicIndex = 0; - float currentDifficulty = 0.0f; - Map<String, Integer> topicOrder = new HashMap<>(); - int order = 1; - - // Determine currentTopicIndex and set currentDifficulty based on exercise profiles - for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) { - if (!topicOrder.containsKey(e.exerciseTopicName)) { - topicOrder.put(e.exerciseTopicName, order++); - } - if (e.exerciseURL.equals(testRepoURL)) { - currentTopicIndex = order; - currentDifficulty = e.difficulty; // Directly assign to currentDifficulty - } - } - - // Sum competencies for unsuccessful tests - float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false); - - // Filter exercises based on topics and difficulty - List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty( - exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL, currentDifficulty, unsuccessful); - - // Generate recommendations without using lambda expressions - List<Recommendation> recommendedExercises = new ArrayList<>(); - for (ExerciseCompetencyProfile profile : filteredExercises) { - float score = calculateScore(profile, unsuccessful, topicOrder, currentDifficulty); - Recommendation recommendation = new Recommendation( - profile.exerciseTopicName, - profile.exerciseURL, - profile.exerciseName, - profile.difficulty, - score); - recommendedExercises.add(recommendation); - } - - // Sort the recommendations using Collections.sort and a comparator - Collections.sort(recommendedExercises, Recommendation.COMPARE_BY_SCORE); - - return recommendedExercises; -} - - public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> profiles, - Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful) { + LOG.debug("result json returned time "+ resultSummary.timestamp + " with "+resultSummary.results.size()+ " test results."); - List<ExerciseCompetencyProfile> filteredExercises = profiles.stream() - .filter(profile -> topicOrder.get(profile.exerciseTopicName) <= currentTopicIndex) - .collect(Collectors.toList()); - - if (CompetencyAssessmentUtil.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()); + 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); + 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.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + if (Files.exists(exerciseManifestFile)) { + LOG.info("Found optional exercise competency profiles, generating recommendations..."); + resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); + } } - - return filteredExercises; - } - - public static float calculateScore(ExerciseCompetencyProfile profile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) { - float score = 1.0f; - for (int i = 0; i < profile.competencyAssessments.length - 1; i++) { - score += profile.competencyAssessments[i] * unsuccessful[i]; - } - - score *= profile.difficulty * (0.5f + Math.abs(currentDifficulty - profile.difficulty)); - score *= topicOrder.getOrDefault(profile.exerciseTopicName, 1); - - return Math.round(score * 10.0f) / 10.0f; - } + return resultSummary; + } + + /* + * 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; + } + } + + //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; + } + + 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()); + } + + return filteredExercises; + } + + 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; + //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 From b2f0dbbf742960c03e344c3ad68a7e347b37b267 Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Tue, 19 Nov 2024 14:41:34 +0100 Subject: [PATCH 3/8] Replaced tetsPathHost to assignmentBasePath --- .../java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 333b177..3f1b8dc 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -147,7 +147,7 @@ public class ExecuteTestUtil { LOG.info("Checking for optional exercise competency profile information for paedagogical agent exercise recommendation functionality..."); //testPathHost or assignmentBasePath - Path exerciseManifestFile = Paths.get(testPathHost.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + Path exerciseManifestFile = Paths.get(assignmentBasePath, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); if (Files.exists(exerciseManifestFile)) { LOG.info("Found optional exercise competency profiles, generating recommendations..."); resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); -- GitLab From d59a136b09925df8eb944adccbeac74c138e25f1 Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Thu, 21 Nov 2024 12:01:29 +0100 Subject: [PATCH 4/8] Added debug logs and exercise-manifest copy --- .gitignore | 2 + .../dtabackend/rest/v1/task/TaskUpload.java | 219 ++++++++--------- .../rest/v1/unittest/UnitTestUpload.java | 229 +++++++++--------- .../dtabackend/utils/ArchiveUtil.java | 2 + .../dtabackend/utils/ExecuteTestUtil.java | 25 +- .../dtabackend/utils/FileUtil.java | 3 + .../dtabackend/utils/RepoUtil.java | 2 +- 7 files changed, 257 insertions(+), 225 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac8dcdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/local-maven-repo \ No newline at end of file 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 7b14090..2eec4e1 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 @@ -1,109 +1,110 @@ -package de.hftstuttgart.dtabackend.rest.v1.task; - -import de.hftstuttgart.dtabackend.utils.*; -import de.hftstuttgart.dtabackend.models.ResultSummary; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.tika.Tika; -import org.springframework.core.env.Environment; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import jakarta.servlet.annotation.MultipartConfig; - -/** - * Rest controller for everything related to the TASK files - */ -@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; - - 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")); - } - - @RequestMapping(method = RequestMethod.POST) - public ResultSummary uploadAndTestFile(@RequestParam("taskFile") MultipartFile taskFileRef, - @RequestParam("assignmentId") String assignmentId - ) throws IOException, InterruptedException { - LOG.info("submission for testing received"); - - 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())); - - // define paths for the test, the submission and where the result is to be expected afterwards - Path srcPath = Paths.get(workDirectory.toAbsolutePath().toString(), "src"); - - String mimeInfo = new Tika().detect(taskFileRef.getInputStream()); - switch (mimeInfo) { - 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); - break; - - case "application/zip": - LOG.debug("zip archive uploaded, extracting content as student submission"); - ArchiveUtil.extractProjectFromZip(taskFileRef.getInputStream(), srcPath.toAbsolutePath()); - break; - - default: - String msg = String.format("couldn't process uploaded file with mime type %s", mimeInfo); - LOG.error(msg); - throw new RuntimeException(msg); - } - - // run test - LOG.debug("calling test execution"); - ResultSummary resultSummary = executeTestUtil.runTests(assignmentId, workDirectory); - - if (mimeInfo.equals("text/plain")) { - LOG.info("check for provided Ticketsystem information"); - UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary); - } - - taskFileRef.getInputStream().close(); - - LOG.info("submission tested successfully"); - return resultSummary; - } -} +package de.hftstuttgart.dtabackend.rest.v1.task; + +import de.hftstuttgart.dtabackend.utils.*; +import de.hftstuttgart.dtabackend.models.ResultSummary; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tika.Tika; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.servlet.annotation.MultipartConfig; + +/** + * Rest controller for everything related to the TASK files + */ +@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; + + 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")); + } + + @RequestMapping(method = RequestMethod.POST) + public ResultSummary uploadAndTestFile(@RequestParam("taskFile") MultipartFile taskFileRef, + @RequestParam("assignmentId") String assignmentId + ) throws IOException, InterruptedException { + LOG.info("submission for testing received"); + + 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())); + + // 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())); + + String mimeInfo = new Tika().detect(taskFileRef.getInputStream()); + switch (mimeInfo) { + 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); + break; + + case "application/zip": + LOG.debug("zip archive uploaded, extracting content as student submission"); + ArchiveUtil.extractProjectFromZip(taskFileRef.getInputStream(), srcPath.toAbsolutePath()); + break; + + default: + String msg = String.format("couldn't process uploaded file with mime type %s", mimeInfo); + LOG.error(msg); + throw new RuntimeException(msg); + } + + // run test + LOG.debug("calling test execution"); + ResultSummary resultSummary = executeTestUtil.runTests(assignmentId, workDirectory); + + if (mimeInfo.equals("text/plain")) { + LOG.info("check for provided Ticketsystem information"); + UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary); + } + + taskFileRef.getInputStream().close(); + + LOG.info("submission tested successfully"); + return resultSummary; + } +} diff --git a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java index 79cc5ed..924f982 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java +++ b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java @@ -1,113 +1,116 @@ -package de.hftstuttgart.dtabackend.rest.v1.unittest; - -import de.hftstuttgart.dtabackend.utils.RepoUtil; -import de.hftstuttgart.dtabackend.utils.RegexUtil; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.core.env.Environment; -import org.springframework.util.FileSystemUtils; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import jakarta.servlet.annotation.MultipartConfig; - -import java.io.*; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Rest controller for anything related to the TEST files. - */ -@RestController -@RequestMapping("/v1/unittest") -@MultipartConfig -public class UnitTestUpload { - - private static final Logger LOG = LogManager.getLogger(UnitTestUpload.class); - private final RepoUtil repoUtil; - private final String assignmentBasePath; - - public UnitTestUpload(Environment env, RepoUtil repoUtil) { - this.repoUtil = repoUtil; - - Path p = Paths.get(env.getProperty("data.dir"), env.getProperty("data.dir.test.folder.name")); - this.assignmentBasePath = p.toAbsolutePath().toString(); - } - - /** - * Create a subfolder for the specific assignment. - * This is called when the teacher creates an assignment and uploads the JUnit test files - * - * @param unitTestFileRef The text file which contains the JUnit tests meta data - * @param assignmentId ID of the created assignment. Generated by Moodle - */ - @RequestMapping(method = RequestMethod.POST) - public void uploadUnitTestFile( - @RequestParam("unitTestFile") MultipartFile unitTestFileRef, - @RequestParam("assignmentId") String assignmentId - ) throws IOException { - LOG.info("received new assignment"); - - File file = Paths.get(this.assignmentBasePath, assignmentId + ".txt").toFile(); - file.mkdirs(); - - // save assignment config - unitTestFileRef.transferTo(file); - LOG.debug(String.format("saved config file to: %s", file.getAbsolutePath())); - - String subDir=""; - Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX); - - Matcher config = RegexUtil.extractConfig(new FileInputStream(file), pattern); - if (config == null) { - pattern=Pattern.compile(RegexUtil.TESTCONFIGREGEX); - config = RegexUtil.extractConfig(new FileInputStream(file), pattern); - if(config==null) - { - throw new RuntimeException("couldn't find repo config for unittest clone"); - } - } - else { - subDir=config.group(4); - } - LOG.debug("calling test repo clone"); - // cloning assignment repo to persistent space - repoUtil.cloneRepository(config, Paths.get(this.assignmentBasePath, assignmentId).toAbsolutePath().toString(), subDir); - - LOG.info(String.format("stored new assignment: %s", file.getAbsolutePath())); - } - - /** - * Delete the folder for the assignment. - * Called when the teacher deletes the JUnitTest assignment - * <p> - * {{url}}:8080/v1/unittest?assignmentId=111 - * - * @param assignmentId ID of the assignment to delete. Generated by Moodle - */ - @RequestMapping(method = RequestMethod.DELETE) - public void deleteUnitTestFiles(@RequestParam("assignmentId") String assignmentId) { - LOG.info(String.format("received deletion order for assignment %s", assignmentId)); - - // deleting config file - File file = Paths.get( - this.assignmentBasePath, - assignmentId + ".txt") - .toFile(); - file.delete(); - - // deleting local copy of repository - file = Paths.get( - this.assignmentBasePath, - assignmentId).toFile(); - FileSystemUtils.deleteRecursively(file); - - LOG.info(String.format("assignment %s deletion complete", assignmentId)); - } -} +package de.hftstuttgart.dtabackend.rest.v1.unittest; + +import de.hftstuttgart.dtabackend.utils.RepoUtil; +import de.hftstuttgart.dtabackend.utils.RegexUtil; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.core.env.Environment; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.annotation.MultipartConfig; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Rest controller for anything related to the TEST files. + */ +@RestController +@RequestMapping("/v1/unittest") +@MultipartConfig +public class UnitTestUpload { + + private static final Logger LOG = LogManager.getLogger(UnitTestUpload.class); + private final RepoUtil repoUtil; + private final String assignmentBasePath; + + public UnitTestUpload(Environment env, RepoUtil repoUtil) { + this.repoUtil = repoUtil; + + Path p = Paths.get(env.getProperty("data.dir"), env.getProperty("data.dir.test.folder.name")); + this.assignmentBasePath = p.toAbsolutePath().toString(); + LOG.debug(String.format("Assignment base path initialized as: %s", this.assignmentBasePath)); + } + + /** + * Create a subfolder for the specific assignment. + * This is called when the teacher creates an assignment and uploads the JUnit test files + * + * @param unitTestFileRef The text file which contains the JUnit tests meta data + * @param assignmentId ID of the created assignment. Generated by Moodle + */ + @RequestMapping(method = RequestMethod.POST) + public void uploadUnitTestFile( + @RequestParam("unitTestFile") MultipartFile unitTestFileRef, + @RequestParam("assignmentId") String assignmentId + ) throws IOException { + LOG.info("received new assignment"); + + File file = Paths.get(this.assignmentBasePath, assignmentId + ".txt").toFile(); + file.mkdirs(); + + // save assignment config + unitTestFileRef.transferTo(file); + LOG.debug(String.format("saved config file to: %s", file.getAbsolutePath())); + + String subDir=""; + Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX); + + Matcher config = RegexUtil.extractConfig(new FileInputStream(file), pattern); + if (config == null) { + pattern=Pattern.compile(RegexUtil.TESTCONFIGREGEX); + config = RegexUtil.extractConfig(new FileInputStream(file), pattern); + if(config==null) + { + throw new RuntimeException("couldn't find repo config for unittest clone"); + } + } + else { + subDir=config.group(4); + } + LOG.debug("calling test repo clone"); + // cloning assignment repo to persistent space + repoUtil.cloneRepository(config, Paths.get(this.assignmentBasePath, assignmentId).toAbsolutePath().toString(), subDir); + + LOG.info(String.format("stored new assignment: %s", file.getAbsolutePath())); + } + + /** + * Delete the folder for the assignment. + * Called when the teacher deletes the JUnitTest assignment + * <p> + * {{url}}:8080/v1/unittest?assignmentId=111 + * + * @param assignmentId ID of the assignment to delete. Generated by Moodle + */ + @RequestMapping(method = RequestMethod.DELETE) + public void deleteUnitTestFiles(@RequestParam("assignmentId") String assignmentId) { + LOG.info(String.format("received deletion order for assignment %s", assignmentId)); + + // deleting config file + File file = Paths.get( + this.assignmentBasePath, + assignmentId + ".txt") + .toFile(); + file.delete(); + LOG.debug(String.format("Deleted file: %s", file.getAbsolutePath())); + + // deleting local copy of repository + file = Paths.get( + this.assignmentBasePath, + assignmentId).toFile(); + FileSystemUtils.deleteRecursively(file); + LOG.debug(String.format("Deleted directory: %s", file.getAbsolutePath())); + + LOG.info(String.format("assignment %s deletion complete", assignmentId)); + } +} diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java index 0408c09..8bd3208 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java @@ -19,6 +19,7 @@ public class ArchiveUtil ZipEntry entry; while ((entry = stream.getNextEntry()) != null) { + LOG.debug(String.format("Processing zip entry: %s", entry.getName())); File file = outDir.resolve(entry.getName()).toFile(); LOG.debug(String.format("processing entry %s", file.getAbsolutePath())); @@ -28,6 +29,7 @@ public class ArchiveUtil LOG.debug("creating new file"); file.createNewFile(); + LOG.debug(String.format("Created new file: %s", file.getAbsolutePath())); byte[] buffer = new byte[2048]; try (FileOutputStream fos = new FileOutputStream(file); diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 3f1b8dc..1fb3861 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -67,6 +67,16 @@ public class ExecuteTestUtil { Paths.get(assignmentBasePath, assignmentId), testPath); + LOG.debug("copying exercise manifest from: %s in testPath: %s", + assignmentBasePath + assignmentId + "_checkout", + testPath.toString() ); + Files.copy(Paths.get( + assignmentBasePath, assignmentId + "_checkout", + CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME), + Paths.get( + testPath.toString(), + CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME)); + LOG.debug("copy test config"); Files.copy( Paths.get(assignmentBasePath, assignmentId + ".txt"), @@ -140,14 +150,25 @@ public class ExecuteTestUtil { 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); - if(testCompetencyProfiles!=null) { + 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(assignmentBasePath, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + 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); diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java index edf8324..f185b9c 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java @@ -24,10 +24,12 @@ public class FileUtil { deleteFolderRecursively(f); } else { f.delete(); + System.out.println(String.format("Deleted file: %s", f.getAbsolutePath())); } } } folder.delete(); + System.out.println(String.format("Deleted folder: %s", folder.getAbsolutePath())); } public static void copyFolder(Path src, Path dst) throws IOException { @@ -35,6 +37,7 @@ public class FileUtil { .forEach(source -> { try { Files.copy(source, dst.resolve(src.relativize(source))); + System.out.println(String.format("Copying file from: %s to %s", source.toString(), dst.resolve(src.relativize(source)).toString())); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java index f2c8587..334bcd7 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java @@ -104,7 +104,7 @@ public class RepoUtil { //copy appropriate path from checkout directory to target directory FileSystemUtils.copyRecursively(new File(checkoutDirectory+subDir), new File(targetPath)); } - LOG.debug(String.format("cloned from %s to %s", config.group(1), targetDirectory)); + LOG.debug(String.format("cloned from %s via %s to %s", config.group(1), checkoutDirectory+subDir, targetDirectory)); } catch (IOException e) { LOG.error(String.format("Error while cloning from %s: could not copy to unit test dir", config.group(1)), e); -- GitLab From c188404598d85eea11f64b9b458228d91a24b6fb Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Wed, 13 Nov 2024 17:56:41 +0100 Subject: [PATCH 5/8] Refactor for backtracking manifesto --- .../utils/CompetencyAssessmentUtil.java | 284 ++++++++------ .../dtabackend/utils/ExecuteTestUtil.java | 345 ++++++++---------- 2 files changed, 321 insertions(+), 308 deletions(-) diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java index cdc3b43..4fcd484 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java @@ -3,134 +3,188 @@ package de.hftstuttgart.dtabackend.utils; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; -import java.net.MalformedURLException; +import java.nio.file.Files; import java.nio.file.Path; -import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import com.fasterxml.jackson.core.exc.StreamReadException; -import com.fasterxml.jackson.databind.DatabindException; - import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Result; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; -import java.io.FileNotFoundException; - public class CompetencyAssessmentUtil { - private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); - - public static String TEST_COMPETENCY_MANIFEST_FILE_NAME="competency-tests.mft"; - public static String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME="exercise-tests.mft"; - - /*public static void main(String[] args) throws StreamReadException, DatabindException, MalformedURLException, IOException { - ResultSummary summary=ExecuteTestUtil.generateResult("1", Path.of(args[0]), Path.of(args[1])); - System.out.println(summary.successfulTestCompetencyProfile); - } - */ - public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { - float[] tcpTotalProfile=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for(TestCompetencyProfile currentProfile: testCompetencyProfiles) { - tcpTotalProfile=ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); - } - return tcpTotalProfile; - } - - public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { - float[] sumSuccessful=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for(Result currentResult: resultSummary.results) { - boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); - if (isSuccess == success) { - TestCompetencyProfile currentProfile=new TestCompetencyProfile(); - currentProfile.testPackageName=(currentResult.packageName!=null)?currentResult.packageName:""; - currentProfile.testClassName=(currentResult.className!=null)?currentResult.className:""; - currentProfile.testName=(currentResult.name!=null)?currentResult.name:""; - int testIndex=testCompetencyProfiles.indexOf(currentProfile); - if(testIndex!=-1) { - sumSuccessful=ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); - } - } - } - return sumSuccessful; - } - - public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path testPath, String fileName) { - List<TestCompetencyProfile> testCompetencyProfiles=new ArrayList<TestCompetencyProfile>(); - try { - BufferedReader testCompetencyManifest=new BufferedReader(new FileReader(new File(testPath.toFile(), fileName))); - String testEntry=testCompetencyManifest.readLine(); - while(testEntry!=null) - { - String[] testEntyComponents=testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); - TestCompetencyProfile currentProfile=new TestCompetencyProfile(); - currentProfile.testPackageName=testEntyComponents[0]; - currentProfile.testClassName=testEntyComponents[1]; - currentProfile.testName=testEntyComponents[2]; - for(int competencyIndex=0; competencyIndex<ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex]=Float.valueOf(testEntyComponents[competencyIndex+3]); - } - testCompetencyProfiles.add(currentProfile); - testEntry=testCompetencyManifest.readLine(); - } - testCompetencyManifest.close(); - LOG.info("Added "+testCompetencyProfiles.size()+" test competency profiles from test competency manifest. Optional agent functionality enabled."); - } catch (FileNotFoundException e) { - LOG.info("Test competency manifest file for agent feedback not found. Skipping optional functionality."); - testCompetencyProfiles=null; - } catch (IOException e) { - LOG.info("Test competency manifest file for agent feedback unreadable. Skipping optional functionality."); - testCompetencyProfiles=null; - } - return testCompetencyProfiles; - } - - public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { - List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); - - try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(new File(exercisePath.toFile(), fileName)))) { - String exerciseEntry = exerciseCompetencyManifest.readLine(); - - while (exerciseEntry != null) { - String[] exerciseEntyComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); - ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); - - currentProfile.exerciseTopicName = exerciseEntyComponents[0]; - currentProfile.exerciseName = exerciseEntyComponents[1]; - currentProfile.exerciseURL = exerciseEntyComponents[2]; - - for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntyComponents[competencyIndex+3]); - } - - currentProfile.difficulty = Float.parseFloat(exerciseEntyComponents[19]); - - exerciseCompetencyProfiles.add(currentProfile); - - exerciseEntry = exerciseCompetencyManifest.readLine(); - } - exerciseCompetencyManifest.close(); - LOG.info("Added " + exerciseCompetencyProfiles.size() + " test competency profiles from exercise competency manifest."); - } catch (FileNotFoundException e) { - LOG.info("Exercise competency manifest file not found."); - } catch (IOException e) { - LOG.info("Exercise competency manifest file unreadable."); - } - - return exerciseCompetencyProfiles; - } - - public static String packFloats(float[] array) { - return IntStream.range(0, array.length) - .mapToObj(i -> String.valueOf(array[i])) - .collect(Collectors.joining(";")); - } + private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); + + public static final String TEST_COMPETENCY_MANIFEST_FILE_NAME = "competency-tests.mft"; + public static final String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME = "exercise-tests.mft"; + + /** + * Retrieves the base directory where the test competency manifest is located + * by traversing upwards until it finds the file "exercise-tests.mft". + * + * @param startDir Starting directory path to begin the search + * @return Path of the base directory if found; otherwise, null + */ + public static Path getBaseDirectory(Path startDir) { + Path currentDir = startDir; + while (currentDir != null) { + Path manifestPath = currentDir.resolve(EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + if (Files.exists(manifestPath)) { + return currentDir; + } + currentDir = currentDir.getParent(); + } + LOG.warn("Base directory with " + EXERCISE_COMPETENCY_MANIFEST_FILE_NAME + " not found starting from " + startDir); + return null; + } + + /** + * Reads and loads test competency profiles from the base directory. + * + * @param exercisePath Path to the starting exercise directory + * @return List of TestCompetencyProfile instances loaded from the manifest + */ + public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path exercisePath) { + Path baseDir = getBaseDirectory(exercisePath); + if (baseDir == null) { + LOG.error("Unable to locate the base directory for reading test competency profiles."); + return null; + } + return readTestCompetencyProfiles(baseDir, TEST_COMPETENCY_MANIFEST_FILE_NAME); + } + + /** + * Reads and loads exercise competency profiles from the specified exercise directory. + * + * @param exercisePath Path to the exercise directory + * @return List of ExerciseCompetencyProfile instances loaded from the manifest + */ + public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath) { + return readExerciseCompetencyProfiles(exercisePath, EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + } + + private static List<TestCompetencyProfile> readTestCompetencyProfiles(Path baseDir, String fileName) { + List<TestCompetencyProfile> testCompetencyProfiles = new ArrayList<>(); + Path manifestPath = baseDir.resolve(fileName); + + try (BufferedReader testCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { + String testEntry = testCompetencyManifest.readLine(); + while (testEntry != null) { + String[] testEntryComponents = testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); + TestCompetencyProfile currentProfile = new TestCompetencyProfile(); + currentProfile.testPackageName = testEntryComponents[0]; + currentProfile.testClassName = testEntryComponents[1]; + currentProfile.testName = testEntryComponents[2]; + for (int competencyIndex = 0; competencyIndex < ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(testEntryComponents[competencyIndex + 3]); + } + testCompetencyProfiles.add(currentProfile); + testEntry = testCompetencyManifest.readLine(); + } + LOG.info("Loaded " + testCompetencyProfiles.size() + " test competency profiles from " + manifestPath); + } catch (IOException e) { + LOG.error("Error reading test competency manifest file at " + manifestPath, e); + } + + return testCompetencyProfiles; + } + + private static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { + List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); + Path manifestPath = exercisePath.resolve(fileName); + + try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { + String exerciseEntry = exerciseCompetencyManifest.readLine(); + while (exerciseEntry != null) { + String[] exerciseEntryComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); + ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); + currentProfile.exerciseTopicName = exerciseEntryComponents[0]; + currentProfile.exerciseName = exerciseEntryComponents[1]; + currentProfile.exerciseURL = exerciseEntryComponents[2]; + for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntryComponents[competencyIndex + 3]); + } + currentProfile.difficulty = Float.parseFloat(exerciseEntryComponents[19]); + exerciseCompetencyProfiles.add(currentProfile); + exerciseEntry = exerciseCompetencyManifest.readLine(); + } + LOG.info("Loaded " + exerciseCompetencyProfiles.size() + " exercise competency profiles from " + manifestPath); + } catch (IOException e) { + LOG.error("Error reading exercise competency manifest file at " + manifestPath, e); + } + + return exerciseCompetencyProfiles; + } + + /** + * Converts an array of floats into a semicolon-separated string. + * + * @param array Array of float values to pack + * @return Semicolon-separated string of float values + */ + public static String packFloats(float[] array) { + return IntStream.range(0, array.length) + .mapToObj(i -> String.valueOf(array[i])) + .collect(Collectors.joining(";")); + } + + /** + * Sums the competency profiles across all test competency profiles. + * + * @param testCompetencyProfiles List of test competency profiles + * @return An array representing the sum of competencies + */ + public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { + float[] tcpTotalProfile = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for (TestCompetencyProfile currentProfile : testCompetencyProfiles) { + tcpTotalProfile = ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); + } + return tcpTotalProfile; + } + + /** + * Sums only the successful test competency profiles, based on the results summary. + * + * @param testCompetencyProfiles List of test competency profiles + * @param resultSummary The result summary containing test results + * @param success Indicates whether to sum successful (true) or unsuccessful (false) profiles + * @return An array representing the sum of competencies for successful or unsuccessful profiles + */ + public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { + float[] sumSuccessful = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for (Result currentResult : resultSummary.results) { + boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); + if (isSuccess == success) { + TestCompetencyProfile currentProfile = new TestCompetencyProfile(); + currentProfile.testPackageName = (currentResult.packageName != null) ? currentResult.packageName : ""; + currentProfile.testClassName = (currentResult.className != null) ? currentResult.className : ""; + currentProfile.testName = (currentResult.name != null) ? currentResult.name : ""; + int testIndex = testCompetencyProfiles.indexOf(currentProfile); + if (testIndex != -1) { + sumSuccessful = ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); + } + } + } + return sumSuccessful; + } + /** + * Checks if all elements in the competency profile array are zero (indicating full success). + * + * @param competencyProfile Array of competency values + * @return True if all values are zero; false otherwise + */ + public static boolean isFullSuccess(float[] competencyProfile) { + for (float value : competencyProfile) { + if (value != 0.0f) { + return false; + } + } + return true; + } } diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 333b177..73e5ced 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -7,7 +7,6 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; -import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Recommendation; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; @@ -22,10 +21,7 @@ import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -38,217 +34,180 @@ public class ExecuteTestUtil { private final String assignmentBasePath; private final Path testTmpPathHost; - 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( + this.assignmentBasePath = Paths.get( env.getProperty("data.dir"), - env.getProperty("data.dir.test.folder.name")); - this.assignmentBasePath = p.toAbsolutePath().toString(); + env.getProperty("data.dir.test.folder.name")) + .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"); - - // clone stored test to tmpdir - LOG.debug("copying pre-downloaded unitttest repo"); - FileUtil.copyFolder( - Paths.get(assignmentBasePath, assignmentId), - testPath); - - LOG.debug("copy test config"); - Files.copy( - Paths.get(assignmentBasePath, assignmentId + ".txt"), - Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt")); - + // Define paths + Path testPath = workDirectory.resolve("test"); + Path srcPath = workDirectory.resolve("src"); + Path resultPath = workDirectory.resolve("result"); + + // Clone test to temporary directory + LOG.debug("Copying pre-downloaded unittest repo"); + FileUtil.copyFolder(Paths.get(assignmentBasePath, assignmentId), testPath); + + // Copy configuration file + LOG.debug("Copy test config"); + Files.copy(Paths.get(assignmentBasePath, assignmentId + ".txt"), workDirectory.resolve("config.txt")); Files.createDirectory(resultPath); - 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); - } - // 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() - ); + // Load container image configuration + LOG.info("Reading test config"); + String image = loadImageConfig(workDirectory); + + // Define paths to mount in the container + Path testPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(testPath.getFileName()); + Path srcPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(srcPath.getFileName()); + Path resultPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(resultPath.getFileName()); - // start test-container with professor given image and bind mounts for test, submission and result + // Start container with mounts 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 Bind(testPathHost.toString(), new Volume("/data/test")), + new Bind(srcPathHost.toString(), new Volume("/data/src")), + new Bind(resultPathHost.toString(), new Volume("/data/result")) ); - ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPathHost); - - return resultSummary; + return generateResult(assignmentId, resultPath, testPathHost); + } + + private String loadImageConfig(Path workDirectory) throws FileNotFoundException { + Matcher config = RegexUtil.extractConfig( + new FileInputStream(workDirectory.resolve("config.txt").toFile()), + Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)); + + if (config == null) { + config = RegexUtil.extractConfig( + new FileInputStream(workDirectory.resolve("config.txt").toFile()), + Pattern.compile(RegexUtil.TESTCONFIGREGEX)); + + if (config == null) { + throw new RuntimeException("Couldn't find repo config for unittest image extraction"); + } + return config.group(4); + } + return config.group(5); } - private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) - throws IOException, StreamReadException, DatabindException, MalformedURLException { - // define expected result file - File resultFile = Paths.get(resultPath.toAbsolutePath().toString(), "result.json").toFile(); + private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) + throws IOException, StreamReadException, DatabindException, MalformedURLException { + 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())); - 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 at " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results."); + + LOG.info("Checking for optional test competency profile information..."); + List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPathHost); + + if (testCompetencyProfiles != null) { + LOG.info("Found test competency profiles, generating profile data..."); + resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles)); + resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true)); + + Path exerciseManifestFile = testPathHost.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + if (Files.exists(exerciseManifestFile)) { + LOG.info("Found exercise competency profiles, generating recommendations..."); + resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); + } + } + return resultSummary; + } + + public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary) + throws FileNotFoundException { + + String testRepoURL = RegexUtil.extractConfig( + new FileInputStream(Paths.get(assignmentBasePath, assignmentId + ".txt").toFile()), + Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)).group(1); + + List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost); + + int currentTopicIndex = 0; + float currentDifficulty = 0.0f; + Map<String, Integer> topicOrder = new HashMap<>(); + int order = 1; + + // Determine currentTopicIndex and set currentDifficulty based on exercise profiles + for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) { + if (!topicOrder.containsKey(e.exerciseTopicName)) { + topicOrder.put(e.exerciseTopicName, order++); + } + if (e.exerciseURL.equals(testRepoURL)) { + currentTopicIndex = order; + currentDifficulty = e.difficulty; // Directly assign to currentDifficulty + } + } + + // Sum competencies for unsuccessful tests + float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false); + + // Filter exercises based on topics and difficulty + List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty( + exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL, currentDifficulty, unsuccessful); + + // Generate recommendations without using lambda expressions + List<Recommendation> recommendedExercises = new ArrayList<>(); + for (ExerciseCompetencyProfile profile : filteredExercises) { + float score = calculateScore(profile, unsuccessful, topicOrder, currentDifficulty); + Recommendation recommendation = new Recommendation( + profile.exerciseTopicName, + profile.exerciseURL, + profile.exerciseName, + profile.difficulty, + score); + recommendedExercises.add(recommendation); + } + + // Sort the recommendations using Collections.sort and a comparator + Collections.sort(recommendedExercises, Recommendation.COMPARE_BY_SCORE); + + return recommendedExercises; +} + + public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> profiles, + Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful) { - 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); - 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.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - if (Files.exists(exerciseManifestFile)) { - LOG.info("Found optional exercise competency profiles, generating recommendations..."); - resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); - } + List<ExerciseCompetencyProfile> filteredExercises = profiles.stream() + .filter(profile -> topicOrder.get(profile.exerciseTopicName) <= currentTopicIndex) + .collect(Collectors.toList()); + + if (CompetencyAssessmentUtil.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()); } - return resultSummary; - } - - /* - * 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; - } - } - - //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; - } - - 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()); - } - - return filteredExercises; - } - - 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; - //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; - } + + return filteredExercises; + } + + public static float calculateScore(ExerciseCompetencyProfile profile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) { + float score = 1.0f; + for (int i = 0; i < profile.competencyAssessments.length - 1; i++) { + score += profile.competencyAssessments[i] * unsuccessful[i]; + } + + score *= profile.difficulty * (0.5f + Math.abs(currentDifficulty - profile.difficulty)); + score *= topicOrder.getOrDefault(profile.exerciseTopicName, 1); + + return Math.round(score * 10.0f) / 10.0f; + } } \ No newline at end of file -- GitLab From 73c101298424038d18285088d317a368b3dd97e5 Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Tue, 19 Nov 2024 14:38:40 +0100 Subject: [PATCH 6/8] Revert "Refactor for backtracking manifesto" This reverts commit 375b37bf188ffd26fdd0bf0d7aeab0170b63ea32. --- .../utils/CompetencyAssessmentUtil.java | 284 ++++++-------- .../dtabackend/utils/ExecuteTestUtil.java | 345 ++++++++++-------- 2 files changed, 308 insertions(+), 321 deletions(-) diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java index 4fcd484..cdc3b43 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java @@ -3,188 +3,134 @@ package de.hftstuttgart.dtabackend.utils; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; -import java.nio.file.Files; +import java.net.MalformedURLException; import java.nio.file.Path; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; + import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Result; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; -public class CompetencyAssessmentUtil { - private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); - - public static final String TEST_COMPETENCY_MANIFEST_FILE_NAME = "competency-tests.mft"; - public static final String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME = "exercise-tests.mft"; - - /** - * Retrieves the base directory where the test competency manifest is located - * by traversing upwards until it finds the file "exercise-tests.mft". - * - * @param startDir Starting directory path to begin the search - * @return Path of the base directory if found; otherwise, null - */ - public static Path getBaseDirectory(Path startDir) { - Path currentDir = startDir; - while (currentDir != null) { - Path manifestPath = currentDir.resolve(EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - if (Files.exists(manifestPath)) { - return currentDir; - } - currentDir = currentDir.getParent(); - } - LOG.warn("Base directory with " + EXERCISE_COMPETENCY_MANIFEST_FILE_NAME + " not found starting from " + startDir); - return null; - } - - /** - * Reads and loads test competency profiles from the base directory. - * - * @param exercisePath Path to the starting exercise directory - * @return List of TestCompetencyProfile instances loaded from the manifest - */ - public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path exercisePath) { - Path baseDir = getBaseDirectory(exercisePath); - if (baseDir == null) { - LOG.error("Unable to locate the base directory for reading test competency profiles."); - return null; - } - return readTestCompetencyProfiles(baseDir, TEST_COMPETENCY_MANIFEST_FILE_NAME); - } - - /** - * Reads and loads exercise competency profiles from the specified exercise directory. - * - * @param exercisePath Path to the exercise directory - * @return List of ExerciseCompetencyProfile instances loaded from the manifest - */ - public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath) { - return readExerciseCompetencyProfiles(exercisePath, EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - } - - private static List<TestCompetencyProfile> readTestCompetencyProfiles(Path baseDir, String fileName) { - List<TestCompetencyProfile> testCompetencyProfiles = new ArrayList<>(); - Path manifestPath = baseDir.resolve(fileName); +import java.io.FileNotFoundException; - try (BufferedReader testCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { - String testEntry = testCompetencyManifest.readLine(); - while (testEntry != null) { - String[] testEntryComponents = testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); - TestCompetencyProfile currentProfile = new TestCompetencyProfile(); - currentProfile.testPackageName = testEntryComponents[0]; - currentProfile.testClassName = testEntryComponents[1]; - currentProfile.testName = testEntryComponents[2]; - for (int competencyIndex = 0; competencyIndex < ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(testEntryComponents[competencyIndex + 3]); - } - testCompetencyProfiles.add(currentProfile); - testEntry = testCompetencyManifest.readLine(); - } - LOG.info("Loaded " + testCompetencyProfiles.size() + " test competency profiles from " + manifestPath); - } catch (IOException e) { - LOG.error("Error reading test competency manifest file at " + manifestPath, e); - } - - return testCompetencyProfiles; - } - - private static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { - List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); - Path manifestPath = exercisePath.resolve(fileName); - - try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(manifestPath.toFile()))) { - String exerciseEntry = exerciseCompetencyManifest.readLine(); - while (exerciseEntry != null) { - String[] exerciseEntryComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); - ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); - currentProfile.exerciseTopicName = exerciseEntryComponents[0]; - currentProfile.exerciseName = exerciseEntryComponents[1]; - currentProfile.exerciseURL = exerciseEntryComponents[2]; - for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { - currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntryComponents[competencyIndex + 3]); - } - currentProfile.difficulty = Float.parseFloat(exerciseEntryComponents[19]); - exerciseCompetencyProfiles.add(currentProfile); - exerciseEntry = exerciseCompetencyManifest.readLine(); - } - LOG.info("Loaded " + exerciseCompetencyProfiles.size() + " exercise competency profiles from " + manifestPath); - } catch (IOException e) { - LOG.error("Error reading exercise competency manifest file at " + manifestPath, e); - } - - return exerciseCompetencyProfiles; - } - - /** - * Converts an array of floats into a semicolon-separated string. - * - * @param array Array of float values to pack - * @return Semicolon-separated string of float values - */ - public static String packFloats(float[] array) { - return IntStream.range(0, array.length) - .mapToObj(i -> String.valueOf(array[i])) - .collect(Collectors.joining(";")); - } - - /** - * Sums the competency profiles across all test competency profiles. - * - * @param testCompetencyProfiles List of test competency profiles - * @return An array representing the sum of competencies - */ - public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { - float[] tcpTotalProfile = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for (TestCompetencyProfile currentProfile : testCompetencyProfiles) { - tcpTotalProfile = ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); - } - return tcpTotalProfile; - } - - /** - * Sums only the successful test competency profiles, based on the results summary. - * - * @param testCompetencyProfiles List of test competency profiles - * @param resultSummary The result summary containing test results - * @param success Indicates whether to sum successful (true) or unsuccessful (false) profiles - * @return An array representing the sum of competencies for successful or unsuccessful profiles - */ - public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { - float[] sumSuccessful = new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; - for (Result currentResult : resultSummary.results) { - boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); - if (isSuccess == success) { - TestCompetencyProfile currentProfile = new TestCompetencyProfile(); - currentProfile.testPackageName = (currentResult.packageName != null) ? currentResult.packageName : ""; - currentProfile.testClassName = (currentResult.className != null) ? currentResult.className : ""; - currentProfile.testName = (currentResult.name != null) ? currentResult.name : ""; - int testIndex = testCompetencyProfiles.indexOf(currentProfile); - if (testIndex != -1) { - sumSuccessful = ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); - } - } - } - return sumSuccessful; - } +public class CompetencyAssessmentUtil { + private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class); + + public static String TEST_COMPETENCY_MANIFEST_FILE_NAME="competency-tests.mft"; + public static String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME="exercise-tests.mft"; + + /*public static void main(String[] args) throws StreamReadException, DatabindException, MalformedURLException, IOException { + ResultSummary summary=ExecuteTestUtil.generateResult("1", Path.of(args[0]), Path.of(args[1])); + System.out.println(summary.successfulTestCompetencyProfile); + } + */ + public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) { + float[] tcpTotalProfile=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for(TestCompetencyProfile currentProfile: testCompetencyProfiles) { + tcpTotalProfile=ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments); + } + return tcpTotalProfile; + } + + public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) { + float[] sumSuccessful=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS]; + for(Result currentResult: resultSummary.results) { + boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal()); + if (isSuccess == success) { + TestCompetencyProfile currentProfile=new TestCompetencyProfile(); + currentProfile.testPackageName=(currentResult.packageName!=null)?currentResult.packageName:""; + currentProfile.testClassName=(currentResult.className!=null)?currentResult.className:""; + currentProfile.testName=(currentResult.name!=null)?currentResult.name:""; + int testIndex=testCompetencyProfiles.indexOf(currentProfile); + if(testIndex!=-1) { + sumSuccessful=ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments); + } + } + } + return sumSuccessful; + } + + public static List<TestCompetencyProfile> readTestCompetencyProfiles(Path testPath, String fileName) { + List<TestCompetencyProfile> testCompetencyProfiles=new ArrayList<TestCompetencyProfile>(); + try { + BufferedReader testCompetencyManifest=new BufferedReader(new FileReader(new File(testPath.toFile(), fileName))); + String testEntry=testCompetencyManifest.readLine(); + while(testEntry!=null) + { + String[] testEntyComponents=testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR); + TestCompetencyProfile currentProfile=new TestCompetencyProfile(); + currentProfile.testPackageName=testEntyComponents[0]; + currentProfile.testClassName=testEntyComponents[1]; + currentProfile.testName=testEntyComponents[2]; + for(int competencyIndex=0; competencyIndex<ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex]=Float.valueOf(testEntyComponents[competencyIndex+3]); + } + testCompetencyProfiles.add(currentProfile); + testEntry=testCompetencyManifest.readLine(); + } + testCompetencyManifest.close(); + LOG.info("Added "+testCompetencyProfiles.size()+" test competency profiles from test competency manifest. Optional agent functionality enabled."); + } catch (FileNotFoundException e) { + LOG.info("Test competency manifest file for agent feedback not found. Skipping optional functionality."); + testCompetencyProfiles=null; + } catch (IOException e) { + LOG.info("Test competency manifest file for agent feedback unreadable. Skipping optional functionality."); + testCompetencyProfiles=null; + } + return testCompetencyProfiles; + } + + public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) { + List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>(); + + try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(new File(exercisePath.toFile(), fileName)))) { + String exerciseEntry = exerciseCompetencyManifest.readLine(); + + while (exerciseEntry != null) { + String[] exerciseEntyComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR); + ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile(); + + currentProfile.exerciseTopicName = exerciseEntyComponents[0]; + currentProfile.exerciseName = exerciseEntyComponents[1]; + currentProfile.exerciseURL = exerciseEntyComponents[2]; + + for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) { + currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntyComponents[competencyIndex+3]); + } + + currentProfile.difficulty = Float.parseFloat(exerciseEntyComponents[19]); + + exerciseCompetencyProfiles.add(currentProfile); + + exerciseEntry = exerciseCompetencyManifest.readLine(); + } + exerciseCompetencyManifest.close(); + LOG.info("Added " + exerciseCompetencyProfiles.size() + " test competency profiles from exercise competency manifest."); + } catch (FileNotFoundException e) { + LOG.info("Exercise competency manifest file not found."); + } catch (IOException e) { + LOG.info("Exercise competency manifest file unreadable."); + } + + return exerciseCompetencyProfiles; + } + + public static String packFloats(float[] array) { + return IntStream.range(0, array.length) + .mapToObj(i -> String.valueOf(array[i])) + .collect(Collectors.joining(";")); + } - /** - * Checks if all elements in the competency profile array are zero (indicating full success). - * - * @param competencyProfile Array of competency values - * @return True if all values are zero; false otherwise - */ - public static boolean isFullSuccess(float[] competencyProfile) { - for (float value : competencyProfile) { - if (value != 0.0f) { - return false; - } - } - return true; - } } diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 73e5ced..333b177 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -7,6 +7,7 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile; +import de.hftstuttgart.dtabackend.models.ICompetencyProfile; import de.hftstuttgart.dtabackend.models.Recommendation; import de.hftstuttgart.dtabackend.models.ResultSummary; import de.hftstuttgart.dtabackend.models.TestCompetencyProfile; @@ -21,7 +22,10 @@ import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -34,180 +38,217 @@ public class ExecuteTestUtil { private final String assignmentBasePath; private final Path testTmpPathHost; - public ExecuteTestUtil(Environment env, DockerUtil dockerUtil) { + public ExecuteTestUtil( + Environment env, + DockerUtil dockerUtil + ) { this.dockerUtil = dockerUtil; - this.assignmentBasePath = Paths.get( + + // set base path for assignments to be stored + Path p = Paths.get( env.getProperty("data.dir"), - env.getProperty("data.dir.test.folder.name")) - .toAbsolutePath().toString(); + 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 - Path testPath = workDirectory.resolve("test"); - Path srcPath = workDirectory.resolve("src"); - Path resultPath = workDirectory.resolve("result"); - - // Clone test to temporary directory - LOG.debug("Copying pre-downloaded unittest repo"); - FileUtil.copyFolder(Paths.get(assignmentBasePath, assignmentId), testPath); - - // Copy configuration file - LOG.debug("Copy test config"); - Files.copy(Paths.get(assignmentBasePath, assignmentId + ".txt"), workDirectory.resolve("config.txt")); - Files.createDirectory(resultPath); - // Load container image configuration - LOG.info("Reading test config"); - String image = loadImageConfig(workDirectory); + // 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 to mount in the container - Path testPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(testPath.getFileName()); - Path srcPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(srcPath.getFileName()); - Path resultPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(resultPath.getFileName()); + // clone stored test to tmpdir + LOG.debug("copying pre-downloaded unitttest repo"); + FileUtil.copyFolder( + Paths.get(assignmentBasePath, assignmentId), + testPath); - // Start container with mounts - dockerUtil.runContainer( - image, - new Bind(testPathHost.toString(), new Volume("/data/test")), - new Bind(srcPathHost.toString(), new Volume("/data/src")), - new Bind(resultPathHost.toString(), new Volume("/data/result")) - ); + LOG.debug("copy test config"); + Files.copy( + Paths.get(assignmentBasePath, assignmentId + ".txt"), + Paths.get(workDirectory.toAbsolutePath().toString(), "config.txt")); - return generateResult(assignmentId, resultPath, testPathHost); - } + Files.createDirectory(resultPath); - private String loadImageConfig(Path workDirectory) throws FileNotFoundException { + LOG.info("reading test config"); Matcher config = RegexUtil.extractConfig( - new FileInputStream(workDirectory.resolve("config.txt").toFile()), - Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)); - - if (config == null) { - config = RegexUtil.extractConfig( - new FileInputStream(workDirectory.resolve("config.txt").toFile()), - Pattern.compile(RegexUtil.TESTCONFIGREGEX)); - - if (config == null) { - throw new RuntimeException("Couldn't find repo config for unittest image extraction"); - } - return config.group(4); + 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); } - return 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 + 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")) + ); + + ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPathHost); + + return resultSummary; } - private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) - throws IOException, StreamReadException, DatabindException, MalformedURLException { - File resultFile = resultPath.resolve("result.json").toFile(); + private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost) + 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 if (!resultFile.exists() || !resultFile.isFile()) { LOG.error(String.format("Could not find result file in %s", resultFile.getAbsolutePath())); - throw new RuntimeException("No result file found"); + throw new RuntimeException("no resultfile 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 at " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results."); - - LOG.info("Checking for optional test competency profile information..."); - List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPathHost); - - if (testCompetencyProfiles != null) { - LOG.info("Found test competency profiles, generating profile data..."); - resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles)); - resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true)); - - Path exerciseManifestFile = testPathHost.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); - if (Files.exists(exerciseManifestFile)) { - LOG.info("Found exercise competency profiles, generating recommendations..."); - resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); - } - } - return resultSummary; - } - - public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary) - throws FileNotFoundException { - - String testRepoURL = RegexUtil.extractConfig( - new FileInputStream(Paths.get(assignmentBasePath, assignmentId + ".txt").toFile()), - Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)).group(1); - - List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost); - - int currentTopicIndex = 0; - float currentDifficulty = 0.0f; - Map<String, Integer> topicOrder = new HashMap<>(); - int order = 1; - - // Determine currentTopicIndex and set currentDifficulty based on exercise profiles - for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) { - if (!topicOrder.containsKey(e.exerciseTopicName)) { - topicOrder.put(e.exerciseTopicName, order++); - } - if (e.exerciseURL.equals(testRepoURL)) { - currentTopicIndex = order; - currentDifficulty = e.difficulty; // Directly assign to currentDifficulty - } - } - - // Sum competencies for unsuccessful tests - float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false); - - // Filter exercises based on topics and difficulty - List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty( - exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL, currentDifficulty, unsuccessful); - - // Generate recommendations without using lambda expressions - List<Recommendation> recommendedExercises = new ArrayList<>(); - for (ExerciseCompetencyProfile profile : filteredExercises) { - float score = calculateScore(profile, unsuccessful, topicOrder, currentDifficulty); - Recommendation recommendation = new Recommendation( - profile.exerciseTopicName, - profile.exerciseURL, - profile.exerciseName, - profile.difficulty, - score); - recommendedExercises.add(recommendation); - } - - // Sort the recommendations using Collections.sort and a comparator - Collections.sort(recommendedExercises, Recommendation.COMPARE_BY_SCORE); - - return recommendedExercises; -} - - public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> profiles, - Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful) { + LOG.debug("result json returned time "+ resultSummary.timestamp + " with "+resultSummary.results.size()+ " test results."); - List<ExerciseCompetencyProfile> filteredExercises = profiles.stream() - .filter(profile -> topicOrder.get(profile.exerciseTopicName) <= currentTopicIndex) - .collect(Collectors.toList()); - - if (CompetencyAssessmentUtil.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()); + 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); + 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.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + if (Files.exists(exerciseManifestFile)) { + LOG.info("Found optional exercise competency profiles, generating recommendations..."); + resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); + } } - - return filteredExercises; - } - - public static float calculateScore(ExerciseCompetencyProfile profile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) { - float score = 1.0f; - for (int i = 0; i < profile.competencyAssessments.length - 1; i++) { - score += profile.competencyAssessments[i] * unsuccessful[i]; - } - - score *= profile.difficulty * (0.5f + Math.abs(currentDifficulty - profile.difficulty)); - score *= topicOrder.getOrDefault(profile.exerciseTopicName, 1); - - return Math.round(score * 10.0f) / 10.0f; - } + return resultSummary; + } + + /* + * 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; + } + } + + //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; + } + + 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()); + } + + return filteredExercises; + } + + 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; + //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 From f8817cf608a34cc8d8a2c132f9e701f54ad4fca9 Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Tue, 19 Nov 2024 14:41:34 +0100 Subject: [PATCH 7/8] Replaced tetsPathHost to assignmentBasePath --- .../java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 333b177..3f1b8dc 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -147,7 +147,7 @@ public class ExecuteTestUtil { LOG.info("Checking for optional exercise competency profile information for paedagogical agent exercise recommendation functionality..."); //testPathHost or assignmentBasePath - Path exerciseManifestFile = Paths.get(testPathHost.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + Path exerciseManifestFile = Paths.get(assignmentBasePath, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); if (Files.exists(exerciseManifestFile)) { LOG.info("Found optional exercise competency profiles, generating recommendations..."); resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary); -- GitLab From b8793634e87adca5bb1e8dd2a15470b4c7e5d43e Mon Sep 17 00:00:00 2001 From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de> Date: Thu, 21 Nov 2024 12:01:29 +0100 Subject: [PATCH 8/8] Added debug logs and exercise-manifest copy --- .gitignore | 2 + .../dtabackend/rest/v1/task/TaskUpload.java | 219 ++++++++--------- .../rest/v1/unittest/UnitTestUpload.java | 229 +++++++++--------- .../dtabackend/utils/ArchiveUtil.java | 2 + .../dtabackend/utils/ExecuteTestUtil.java | 25 +- .../dtabackend/utils/FileUtil.java | 3 + .../dtabackend/utils/RepoUtil.java | 2 +- 7 files changed, 257 insertions(+), 225 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac8dcdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/local-maven-repo \ No newline at end of file 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 7b14090..2eec4e1 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 @@ -1,109 +1,110 @@ -package de.hftstuttgart.dtabackend.rest.v1.task; - -import de.hftstuttgart.dtabackend.utils.*; -import de.hftstuttgart.dtabackend.models.ResultSummary; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.tika.Tika; -import org.springframework.core.env.Environment; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import jakarta.servlet.annotation.MultipartConfig; - -/** - * Rest controller for everything related to the TASK files - */ -@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; - - 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")); - } - - @RequestMapping(method = RequestMethod.POST) - public ResultSummary uploadAndTestFile(@RequestParam("taskFile") MultipartFile taskFileRef, - @RequestParam("assignmentId") String assignmentId - ) throws IOException, InterruptedException { - LOG.info("submission for testing received"); - - 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())); - - // define paths for the test, the submission and where the result is to be expected afterwards - Path srcPath = Paths.get(workDirectory.toAbsolutePath().toString(), "src"); - - String mimeInfo = new Tika().detect(taskFileRef.getInputStream()); - switch (mimeInfo) { - 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); - break; - - case "application/zip": - LOG.debug("zip archive uploaded, extracting content as student submission"); - ArchiveUtil.extractProjectFromZip(taskFileRef.getInputStream(), srcPath.toAbsolutePath()); - break; - - default: - String msg = String.format("couldn't process uploaded file with mime type %s", mimeInfo); - LOG.error(msg); - throw new RuntimeException(msg); - } - - // run test - LOG.debug("calling test execution"); - ResultSummary resultSummary = executeTestUtil.runTests(assignmentId, workDirectory); - - if (mimeInfo.equals("text/plain")) { - LOG.info("check for provided Ticketsystem information"); - UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary); - } - - taskFileRef.getInputStream().close(); - - LOG.info("submission tested successfully"); - return resultSummary; - } -} +package de.hftstuttgart.dtabackend.rest.v1.task; + +import de.hftstuttgart.dtabackend.utils.*; +import de.hftstuttgart.dtabackend.models.ResultSummary; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tika.Tika; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.servlet.annotation.MultipartConfig; + +/** + * Rest controller for everything related to the TASK files + */ +@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; + + 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")); + } + + @RequestMapping(method = RequestMethod.POST) + public ResultSummary uploadAndTestFile(@RequestParam("taskFile") MultipartFile taskFileRef, + @RequestParam("assignmentId") String assignmentId + ) throws IOException, InterruptedException { + LOG.info("submission for testing received"); + + 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())); + + // 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())); + + String mimeInfo = new Tika().detect(taskFileRef.getInputStream()); + switch (mimeInfo) { + 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); + break; + + case "application/zip": + LOG.debug("zip archive uploaded, extracting content as student submission"); + ArchiveUtil.extractProjectFromZip(taskFileRef.getInputStream(), srcPath.toAbsolutePath()); + break; + + default: + String msg = String.format("couldn't process uploaded file with mime type %s", mimeInfo); + LOG.error(msg); + throw new RuntimeException(msg); + } + + // run test + LOG.debug("calling test execution"); + ResultSummary resultSummary = executeTestUtil.runTests(assignmentId, workDirectory); + + if (mimeInfo.equals("text/plain")) { + LOG.info("check for provided Ticketsystem information"); + UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary); + } + + taskFileRef.getInputStream().close(); + + LOG.info("submission tested successfully"); + return resultSummary; + } +} diff --git a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java index 79cc5ed..924f982 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java +++ b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java @@ -1,113 +1,116 @@ -package de.hftstuttgart.dtabackend.rest.v1.unittest; - -import de.hftstuttgart.dtabackend.utils.RepoUtil; -import de.hftstuttgart.dtabackend.utils.RegexUtil; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.core.env.Environment; -import org.springframework.util.FileSystemUtils; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import jakarta.servlet.annotation.MultipartConfig; - -import java.io.*; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Rest controller for anything related to the TEST files. - */ -@RestController -@RequestMapping("/v1/unittest") -@MultipartConfig -public class UnitTestUpload { - - private static final Logger LOG = LogManager.getLogger(UnitTestUpload.class); - private final RepoUtil repoUtil; - private final String assignmentBasePath; - - public UnitTestUpload(Environment env, RepoUtil repoUtil) { - this.repoUtil = repoUtil; - - Path p = Paths.get(env.getProperty("data.dir"), env.getProperty("data.dir.test.folder.name")); - this.assignmentBasePath = p.toAbsolutePath().toString(); - } - - /** - * Create a subfolder for the specific assignment. - * This is called when the teacher creates an assignment and uploads the JUnit test files - * - * @param unitTestFileRef The text file which contains the JUnit tests meta data - * @param assignmentId ID of the created assignment. Generated by Moodle - */ - @RequestMapping(method = RequestMethod.POST) - public void uploadUnitTestFile( - @RequestParam("unitTestFile") MultipartFile unitTestFileRef, - @RequestParam("assignmentId") String assignmentId - ) throws IOException { - LOG.info("received new assignment"); - - File file = Paths.get(this.assignmentBasePath, assignmentId + ".txt").toFile(); - file.mkdirs(); - - // save assignment config - unitTestFileRef.transferTo(file); - LOG.debug(String.format("saved config file to: %s", file.getAbsolutePath())); - - String subDir=""; - Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX); - - Matcher config = RegexUtil.extractConfig(new FileInputStream(file), pattern); - if (config == null) { - pattern=Pattern.compile(RegexUtil.TESTCONFIGREGEX); - config = RegexUtil.extractConfig(new FileInputStream(file), pattern); - if(config==null) - { - throw new RuntimeException("couldn't find repo config for unittest clone"); - } - } - else { - subDir=config.group(4); - } - LOG.debug("calling test repo clone"); - // cloning assignment repo to persistent space - repoUtil.cloneRepository(config, Paths.get(this.assignmentBasePath, assignmentId).toAbsolutePath().toString(), subDir); - - LOG.info(String.format("stored new assignment: %s", file.getAbsolutePath())); - } - - /** - * Delete the folder for the assignment. - * Called when the teacher deletes the JUnitTest assignment - * <p> - * {{url}}:8080/v1/unittest?assignmentId=111 - * - * @param assignmentId ID of the assignment to delete. Generated by Moodle - */ - @RequestMapping(method = RequestMethod.DELETE) - public void deleteUnitTestFiles(@RequestParam("assignmentId") String assignmentId) { - LOG.info(String.format("received deletion order for assignment %s", assignmentId)); - - // deleting config file - File file = Paths.get( - this.assignmentBasePath, - assignmentId + ".txt") - .toFile(); - file.delete(); - - // deleting local copy of repository - file = Paths.get( - this.assignmentBasePath, - assignmentId).toFile(); - FileSystemUtils.deleteRecursively(file); - - LOG.info(String.format("assignment %s deletion complete", assignmentId)); - } -} +package de.hftstuttgart.dtabackend.rest.v1.unittest; + +import de.hftstuttgart.dtabackend.utils.RepoUtil; +import de.hftstuttgart.dtabackend.utils.RegexUtil; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.core.env.Environment; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.annotation.MultipartConfig; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Rest controller for anything related to the TEST files. + */ +@RestController +@RequestMapping("/v1/unittest") +@MultipartConfig +public class UnitTestUpload { + + private static final Logger LOG = LogManager.getLogger(UnitTestUpload.class); + private final RepoUtil repoUtil; + private final String assignmentBasePath; + + public UnitTestUpload(Environment env, RepoUtil repoUtil) { + this.repoUtil = repoUtil; + + Path p = Paths.get(env.getProperty("data.dir"), env.getProperty("data.dir.test.folder.name")); + this.assignmentBasePath = p.toAbsolutePath().toString(); + LOG.debug(String.format("Assignment base path initialized as: %s", this.assignmentBasePath)); + } + + /** + * Create a subfolder for the specific assignment. + * This is called when the teacher creates an assignment and uploads the JUnit test files + * + * @param unitTestFileRef The text file which contains the JUnit tests meta data + * @param assignmentId ID of the created assignment. Generated by Moodle + */ + @RequestMapping(method = RequestMethod.POST) + public void uploadUnitTestFile( + @RequestParam("unitTestFile") MultipartFile unitTestFileRef, + @RequestParam("assignmentId") String assignmentId + ) throws IOException { + LOG.info("received new assignment"); + + File file = Paths.get(this.assignmentBasePath, assignmentId + ".txt").toFile(); + file.mkdirs(); + + // save assignment config + unitTestFileRef.transferTo(file); + LOG.debug(String.format("saved config file to: %s", file.getAbsolutePath())); + + String subDir=""; + Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX); + + Matcher config = RegexUtil.extractConfig(new FileInputStream(file), pattern); + if (config == null) { + pattern=Pattern.compile(RegexUtil.TESTCONFIGREGEX); + config = RegexUtil.extractConfig(new FileInputStream(file), pattern); + if(config==null) + { + throw new RuntimeException("couldn't find repo config for unittest clone"); + } + } + else { + subDir=config.group(4); + } + LOG.debug("calling test repo clone"); + // cloning assignment repo to persistent space + repoUtil.cloneRepository(config, Paths.get(this.assignmentBasePath, assignmentId).toAbsolutePath().toString(), subDir); + + LOG.info(String.format("stored new assignment: %s", file.getAbsolutePath())); + } + + /** + * Delete the folder for the assignment. + * Called when the teacher deletes the JUnitTest assignment + * <p> + * {{url}}:8080/v1/unittest?assignmentId=111 + * + * @param assignmentId ID of the assignment to delete. Generated by Moodle + */ + @RequestMapping(method = RequestMethod.DELETE) + public void deleteUnitTestFiles(@RequestParam("assignmentId") String assignmentId) { + LOG.info(String.format("received deletion order for assignment %s", assignmentId)); + + // deleting config file + File file = Paths.get( + this.assignmentBasePath, + assignmentId + ".txt") + .toFile(); + file.delete(); + LOG.debug(String.format("Deleted file: %s", file.getAbsolutePath())); + + // deleting local copy of repository + file = Paths.get( + this.assignmentBasePath, + assignmentId).toFile(); + FileSystemUtils.deleteRecursively(file); + LOG.debug(String.format("Deleted directory: %s", file.getAbsolutePath())); + + LOG.info(String.format("assignment %s deletion complete", assignmentId)); + } +} diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java index 0408c09..8bd3208 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ArchiveUtil.java @@ -19,6 +19,7 @@ public class ArchiveUtil ZipEntry entry; while ((entry = stream.getNextEntry()) != null) { + LOG.debug(String.format("Processing zip entry: %s", entry.getName())); File file = outDir.resolve(entry.getName()).toFile(); LOG.debug(String.format("processing entry %s", file.getAbsolutePath())); @@ -28,6 +29,7 @@ public class ArchiveUtil LOG.debug("creating new file"); file.createNewFile(); + LOG.debug(String.format("Created new file: %s", file.getAbsolutePath())); byte[] buffer = new byte[2048]; try (FileOutputStream fos = new FileOutputStream(file); diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java index 3f1b8dc..1fb3861 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java @@ -67,6 +67,16 @@ public class ExecuteTestUtil { Paths.get(assignmentBasePath, assignmentId), testPath); + LOG.debug("copying exercise manifest from: %s in testPath: %s", + assignmentBasePath + assignmentId + "_checkout", + testPath.toString() ); + Files.copy(Paths.get( + assignmentBasePath, assignmentId + "_checkout", + CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME), + Paths.get( + testPath.toString(), + CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME)); + LOG.debug("copy test config"); Files.copy( Paths.get(assignmentBasePath, assignmentId + ".txt"), @@ -140,14 +150,25 @@ public class ExecuteTestUtil { 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); - if(testCompetencyProfiles!=null) { + 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(assignmentBasePath, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME); + 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); diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java index edf8324..f185b9c 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/FileUtil.java @@ -24,10 +24,12 @@ public class FileUtil { deleteFolderRecursively(f); } else { f.delete(); + System.out.println(String.format("Deleted file: %s", f.getAbsolutePath())); } } } folder.delete(); + System.out.println(String.format("Deleted folder: %s", folder.getAbsolutePath())); } public static void copyFolder(Path src, Path dst) throws IOException { @@ -35,6 +37,7 @@ public class FileUtil { .forEach(source -> { try { Files.copy(source, dst.resolve(src.relativize(source))); + System.out.println(String.format("Copying file from: %s to %s", source.toString(), dst.resolve(src.relativize(source)).toString())); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java index f2c8587..334bcd7 100644 --- a/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java +++ b/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java @@ -104,7 +104,7 @@ public class RepoUtil { //copy appropriate path from checkout directory to target directory FileSystemUtils.copyRecursively(new File(checkoutDirectory+subDir), new File(targetPath)); } - LOG.debug(String.format("cloned from %s to %s", config.group(1), targetDirectory)); + LOG.debug(String.format("cloned from %s via %s to %s", config.group(1), checkoutDirectory+subDir, targetDirectory)); } catch (IOException e) { LOG.error(String.format("Error while cloning from %s: could not copy to unit test dir", config.group(1)), e); -- GitLab