Skip to content

Commit

Permalink
Merge branch 'develop' into feature/programming-exercises/cpp-template
Browse files Browse the repository at this point in the history
  • Loading branch information
magaupp committed Oct 12, 2024
2 parents 0c36fed + d801ef8 commit 3886c42
Show file tree
Hide file tree
Showing 158 changed files with 2,404 additions and 1,367 deletions.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ module.exports = {
coverageThreshold: {
global: {
// TODO: in the future, the following values should increase to at least 90%
statements: 87.44,
branches: 73.73,
functions: 82.09,
lines: 87.49,
statements: 87.39,
branches: 73.60,
functions: 81.97,
lines: 87.45,
},
},
coverageReporters: ['clover', 'json', 'lcov', 'text-summary'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,14 @@ SELECT COUNT(DISTINCT p)
*/
boolean existsByParticipationId(long participationId);

/**
* Checks if a result exists for the given submission ID.
*
* @param submissionId the ID of the submission to check.
* @return true if a result exists for the given submission ID, false otherwise.
*/
boolean existsBySubmissionId(long submissionId);

/**
* Returns true if there is at least one result for the given exercise.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record BuildConfig(String buildScript, String dockerImage, String commitHashToBuild, String assignmentCommitHash, String testCommitHash, String branch,
ProgrammingLanguage programmingLanguage, ProjectType projectType, boolean scaEnabled, boolean sequentialTestRunsEnabled, boolean testwiseCoverageEnabled,
List<String> resultPaths) implements Serializable {
List<String> resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath) implements Serializable {

@Override
public String dockerImage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -275,11 +276,22 @@ public String getIDOfRunningContainer(String containerName) {
* @param auxiliaryRepositoriesPaths An array of paths for auxiliary repositories to be included in the build process.
* @param auxiliaryRepositoryCheckoutDirectories An array of directory names within the container where each auxiliary repository should be checked out.
* @param programmingLanguage The programming language of the repositories, which influences directory naming conventions.
* @param assignmentCheckoutPath The directory within the container where the assignment repository should be checked out; can be null if not applicable,
* default would be used.
* @param testCheckoutPath The directory within the container where the test repository should be checked out; can be null if not applicable, default
* would be used.
* @param solutionCheckoutPath The directory within the container where the solution repository should be checked out; can be null if not applicable, default
* would be used.
*/
public void populateBuildJobContainer(String buildJobContainerId, Path assignmentRepositoryPath, Path testRepositoryPath, Path solutionRepositoryPath,
Path[] auxiliaryRepositoriesPaths, String[] auxiliaryRepositoryCheckoutDirectories, ProgrammingLanguage programmingLanguage) {
String testCheckoutPath = RepositoryCheckoutPath.TEST.forProgrammingLanguage(programmingLanguage);
String assignmentCheckoutPath = RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(programmingLanguage);
Path[] auxiliaryRepositoriesPaths, String[] auxiliaryRepositoryCheckoutDirectories, ProgrammingLanguage programmingLanguage, String assignmentCheckoutPath,
String testCheckoutPath, String solutionCheckoutPath) {

assignmentCheckoutPath = (!StringUtils.isBlank(assignmentCheckoutPath)) ? assignmentCheckoutPath
: RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(programmingLanguage);

String defaultTestCheckoutPath = RepositoryCheckoutPath.TEST.forProgrammingLanguage(programmingLanguage);
testCheckoutPath = (!StringUtils.isBlank(defaultTestCheckoutPath) && !StringUtils.isBlank(testCheckoutPath)) ? testCheckoutPath : defaultTestCheckoutPath;

// Make sure to create the working directory in case it does not exist.
// In case the test checkout path is the working directory, we only create up to the parent, as the working directory is created below.
Expand All @@ -292,7 +304,8 @@ public void populateBuildJobContainer(String buildJobContainerId, Path assignmen
// Copy the assignment repository to the container and move it to the assignment checkout path
addAndPrepareDirectory(buildJobContainerId, assignmentRepositoryPath, LOCALCI_WORKING_DIRECTORY + "/testing-dir/" + assignmentCheckoutPath);
if (solutionRepositoryPath != null) {
String solutionCheckoutPath = RepositoryCheckoutPath.SOLUTION.forProgrammingLanguage(programmingLanguage);
solutionCheckoutPath = (!StringUtils.isBlank(solutionCheckoutPath)) ? solutionCheckoutPath
: RepositoryCheckoutPath.SOLUTION.forProgrammingLanguage(programmingLanguage);
addAndPrepareDirectory(buildJobContainerId, solutionRepositoryPath, LOCALCI_WORKING_DIRECTORY + "/testing-dir/" + solutionCheckoutPath);
}
for (int i = 0; i < auxiliaryRepositoriesPaths.length; i++) {
Expand All @@ -309,6 +322,7 @@ private void createScriptFile(String buildJobContainerId) {

private void addAndPrepareDirectory(String containerId, Path repositoryPath, String newDirectoryName) {
copyToContainer(repositoryPath.toString(), containerId);
addDirectory(containerId, getParentFolderPath(newDirectoryName), true);
renameDirectoryOrFile(containerId, LOCALCI_WORKING_DIRECTORY + "/" + repositoryPath.getFileName().toString(), newDirectoryName);
}

Expand Down Expand Up @@ -428,4 +442,9 @@ private Container getContainerForName(String containerName) {
List<Container> containers = dockerClient.listContainersCmd().withShowAll(true).exec();
return containers.stream().filter(container -> container.getNames()[0].equals("/" + containerName)).findFirst().orElse(null);
}

private String getParentFolderPath(String path) {
Path parentPath = Paths.get(path).normalize().getParent();
return parentPath != null ? parentPath.toString() : "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String
buildLogsMap.appendBuildLogEntry(buildJob.id(), msg);
log.debug(msg);
buildJobContainerService.populateBuildJobContainer(containerId, assignmentRepositoryPath, testsRepositoryPath, solutionRepositoryPath, auxiliaryRepositoriesPaths,
buildJob.repositoryInfo().auxiliaryRepositoryCheckoutDirectories(), buildJob.buildConfig().programmingLanguage());
buildJob.repositoryInfo().auxiliaryRepositoryCheckoutDirectories(), buildJob.buildConfig().programmingLanguage(), buildJob.buildConfig().assignmentCheckoutPath(),
buildJob.buildConfig().testCheckoutPath(), buildJob.buildConfig().solutionCheckoutPath());

msg = "~~~~~~~~~~~~~~~~~~~~ Executing Build Script for Build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~";
buildLogsMap.appendBuildLogEntry(buildJob.id(), msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public class BuildJobManagementService {

private final ReentrantLock lock = new ReentrantLock();

@Value("${artemis.continuous-integration.timeout-seconds:240}")
@Value("${artemis.continuous-integration.timeout-seconds:120}")
private int timeoutSeconds;

@Value("${artemis.continuous-integration.asynchronous:true}")
Expand Down Expand Up @@ -149,9 +149,17 @@ public CompletableFuture<BuildResult> executeBuildJob(BuildJobQueueItem buildJob
lock.unlock();
}

int buildJobTimeoutSeconds;
if (buildJobItem.buildConfig().timeoutSeconds() != 0 && buildJobItem.buildConfig().timeoutSeconds() < this.timeoutSeconds) {
buildJobTimeoutSeconds = buildJobItem.buildConfig().timeoutSeconds();
}
else {
buildJobTimeoutSeconds = this.timeoutSeconds;
}

CompletableFuture<BuildResult> futureResult = createCompletableFuture(() -> {
try {
return future.get(timeoutSeconds, TimeUnit.SECONDS);
return future.get(buildJobTimeoutSeconds, TimeUnit.SECONDS);
}
catch (Exception e) {
// RejectedExecutionException is thrown if the queue size limit (defined in "artemis.continuous-integration.queue-size-limit") is reached.
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public final class Constants {
// Used to cut off CI specific path segments when receiving static code analysis reports
public static final String ASSIGNMENT_DIRECTORY = "/" + ASSIGNMENT_REPO_NAME + "/";

public static final String TEST_WORKING_DIRECTORY = "test";

// Used as a value for <sourceDirectory> for the Java template pom.xml
public static final String STUDENT_WORKING_DIRECTORY = ASSIGNMENT_DIRECTORY + "src";

Expand Down Expand Up @@ -390,6 +392,18 @@ public final class Constants {
*/
public static final int MIN_SCORE_ORANGE = 40;

public static final String ASSIGNMENT_REPO_PLACEHOLDER = "${studentWorkingDirectory}";

public static final String TEST_REPO_PLACEHOLDER = "${testWorkingDirectory}";

public static final String SOLUTION_REPO_PLACEHOLDER = "${solutionWorkingDirectory}";

public static final String ASSIGNMENT_REPO_PARENT_PLACEHOLDER = "${studentParentWorkingDirectoryName}";

public static final String ASSIGNMENT_REPO_PLACEHOLDER_NO_SLASH = "${studentWorkingDirectoryNoSlash}";

public static final Pattern ALLOWED_CHECKOUT_DIRECTORY = Pattern.compile("[\\w-]+(/[\\w-]+)*$");

private Constants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

Expand Down Expand Up @@ -281,7 +280,7 @@ public ResponseEntity<Void> deleteUser(@PathVariable String login) {
* @return the ResponseEntity with status 200 (OK)
*/
@DeleteMapping("users")
public ResponseEntity<List<String>> deleteUsers(@RequestParam(name = "login") List<String> logins) {
public ResponseEntity<List<String>> deleteUsers(@RequestBody List<String> logins) {
log.debug("REST request to delete {} users", logins.size());
List<String> deletedUsers = Collections.synchronizedList(new java.util.ArrayList<>());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,26 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) {
studentExam = optionalStudentExam.get();
}
else {
// Only Test Exams can be self-created by the user.
Exam examWithExerciseGroupsAndExercises = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId);
// An exam can be started 5 minutes before the start time, which is when programming exercises are unlocked
boolean canExamBeStarted = ZonedDateTime.now().isAfter(ExamDateService.getExamProgrammingExerciseUnlockDate(examWithExerciseGroupsAndExercises));
boolean isExamEnded = ZonedDateTime.now().isAfter(examWithExerciseGroupsAndExercises.getEndDate());
// Generate a student exam if the following conditions are met:
// 1. The exam has not ended.
// 2. The exam is either a test exam, OR it is a normal exam where the user is registered and can click the start button.
// Allowing student exams to be generated only when students can click the start button prevents inconsistencies.
// For example, this avoids a scenario where a student generates an exam and an instructor adds an exercise group afterward.
if (!isExamEnded
&& (examWithExerciseGroupsAndExercises.isTestExam() || (examRegistrationService.isUserRegisteredForExam(examId, currentUser.getId()) && canExamBeStarted))) {
studentExam = studentExamService.generateIndividualStudentExam(examWithExerciseGroupsAndExercises, currentUser);
// For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource
studentExam.setExercises(null);

if (!examWithExerciseGroupsAndExercises.isTestExam()) {
}
else {
// We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam
throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME,
"StudentExamGenerationOnlyForTestExams", true);
throw new BadRequestAlertException("Cannot generate student exam for exam ID " + examId + ".", ENTITY_NAME, "cannotGenerateStudentExam", true);
}
studentExam = studentExamService.generateTestExam(examWithExerciseGroupsAndExercises, currentUser);
// For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource
studentExam.setExercises(null);
}

Exam exam = studentExam.getExam();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,12 @@ public class ExamRegistrationService {

private static final boolean IS_TEST_RUN = false;

private final StudentExamService studentExamService;

public ExamRegistrationService(ExamUserRepository examUserRepository, ExamRepository examRepository, UserService userService, ParticipationService participationService,
UserRepository userRepository, AuditEventRepository auditEventRepository, CourseRepository courseRepository, StudentExamRepository studentExamRepository,
StudentParticipationRepository studentParticipationRepository, AuthorizationCheckService authorizationCheckService, ExamUserService examUserService) {
StudentParticipationRepository studentParticipationRepository, AuthorizationCheckService authorizationCheckService, ExamUserService examUserService,
StudentExamService studentExamService) {
this.examRepository = examRepository;
this.userService = userService;
this.userRepository = userRepository;
Expand All @@ -86,6 +89,7 @@ public ExamRegistrationService(ExamUserRepository examUserRepository, ExamReposi
this.authorizationCheckService = authorizationCheckService;
this.examUserRepository = examUserRepository;
this.examUserService = examUserService;
this.studentExamService = studentExamService;
}

/**
Expand Down Expand Up @@ -193,6 +197,7 @@ public boolean isUserRegisteredForExam(Long examId, Long userId) {
* Registers student to the exam. In order to do this, we add the user to the course group, because the user only has access to the exam of a course if the student also has
* access to the course of the exam.
* We only need to add the user to the course group, if the student is not yet part of it, otherwise the student cannot access the exam (within the course).
* If the exam has already started, a student exam is additionally generated.
*
* @param course the course containing the exam
* @param exam the exam for which we want to register a student
Expand All @@ -216,6 +221,11 @@ public void registerStudentToExam(Course course, Exam exam, User student) {
registeredExamUser = examUserRepository.save(registeredExamUser);
exam.addExamUser(registeredExamUser);
examRepository.save(exam);
// Generate a student exam for the registered student if the exam has already started
if (exam.isStarted()) {
Exam examWithExerciseGroupsAndExercises = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(exam.getId());
studentExamService.generateIndividualStudentExam(examWithExerciseGroupsAndExercises, student);
}
}
else {
log.warn("Student {} is already registered for the exam {}", student.getLogin(), exam.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -797,16 +796,17 @@ public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, L
}

/**
* Generates a new test exam for the student and stores it in the database
* Generates a new individual StudentExam for the specified student and stores it in the database.
*
* @param exam the exam with loaded exercise groups and exercises for which the StudentExam should be created
* @param student the corresponding student
* @return a StudentExam for the student and exam
* @param exam The exam with eagerly loaded users, exercise groups, and exercises.
* @param student The student for whom the StudentExam should be created.
* @return The generated StudentExam.
*/
public StudentExam generateTestExam(Exam exam, User student) {
public StudentExam generateIndividualStudentExam(Exam exam, User student) {
// To create a new StudentExam, the Exam with loaded ExerciseGroups and Exercises is needed
long start = System.nanoTime();
StudentExam studentExam = generateIndividualStudentExam(exam, student);
Set<User> userSet = Collections.singleton(student);
StudentExam studentExam = studentExamRepository.createRandomStudentExams(exam, userSet, examQuizQuestionsGenerator).getFirst();
// we need to break a cycle for the serialization
studentExam.getExam().setExerciseGroups(null);
studentExam.getExam().setStudentExams(null);
Expand All @@ -816,20 +816,6 @@ public StudentExam generateTestExam(Exam exam, User student) {
return studentExam;
}

/**
* Generates an individual StudentExam
*
* @param exam with eagerly loaded users, exerciseGroups and exercises loaded
* @param student the student for which the StudentExam should be created
* @return the generated StudentExam
*/
private StudentExam generateIndividualStudentExam(Exam exam, User student) {
// StudentExams are saved in the called method
HashSet<User> userHashSet = new HashSet<>();
userHashSet.add(student);
return studentExamRepository.createRandomStudentExams(exam, userHashSet, examQuizQuestionsGenerator).getFirst();
}

/**
* Generates the student exams randomly based on the exam configuration and the exercise groups
* Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded)
Expand Down
Loading

0 comments on commit 3886c42

Please sign in to comment.