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