From 3154064515ea84ab13d56e437419d21390ff71a0 Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:04:17 +0200 Subject: [PATCH] Integrated code lifecycle: Configure checkout path and timeout for programming exercises (#9217) --- .../artemis/buildagent/dto/BuildConfig.java | 2 +- .../service/BuildJobContainerService.java | 27 +++- .../service/BuildJobExecutionService.java | 3 +- .../service/BuildJobManagementService.java | 12 +- .../aet/artemis/core/config/Constants.java | 14 ++ .../ProgrammingExerciseBuildConfig.java | 38 ++++- .../service/AuxiliaryRepositoryService.java | 4 +- .../service/BuildScriptProviderService.java | 52 ++++++- .../ProgrammingExerciseRepositoryService.java | 27 +++- .../service/ProgrammingExerciseService.java | 39 +++++ .../service/aeolus/AeolusTemplateService.java | 12 +- .../LocalCIBuildConfigurationService.java | 24 +-- .../localci/LocalCITriggerService.java | 10 +- .../web/ProgrammingExerciseResource.java | 3 + .../config/application-buildagent.yml | 2 +- .../changelog/20240816150000_changelog.xml | 11 ++ .../resources/config/liquibase/master.xml | 1 + .../templates/aeolus/assembler/default.sh | 22 +-- .../templates/aeolus/assembler/default.yaml | 24 +-- src/main/resources/templates/aeolus/c/fact.sh | 14 +- .../resources/templates/aeolus/c/fact.yaml | 12 +- src/main/resources/templates/aeolus/c/gcc.sh | 24 +-- .../resources/templates/aeolus/c/gcc.yaml | 22 +-- .../templates/aeolus/c/gcc_static.sh | 24 +-- .../templates/aeolus/c/gcc_static.yaml | 22 +-- .../aeolus/java/plain_maven_blackbox.sh | 4 +- .../aeolus/java/plain_maven_blackbox.yaml | 4 +- .../java/plain_maven_blackbox_static.sh | 4 +- .../java/plain_maven_blackbox_static.yaml | 4 +- .../templates/aeolus/ocaml/default.sh | 2 +- .../templates/aeolus/ocaml/default.yaml | 2 +- .../resources/templates/aeolus/swift/plain.sh | 8 +- .../templates/aeolus/swift/plain.yaml | 8 +- .../templates/aeolus/swift/plain_static.sh | 14 +- .../templates/aeolus/swift/plain_static.yaml | 14 +- .../templates/aeolus/vhdl/default.sh | 18 +-- .../templates/aeolus/vhdl/default.yaml | 20 +-- .../templates/haskell/test/.gitignore | 4 +- .../templates/haskell/test/readme.md | 4 +- .../resources/templates/haskell/test/run.sh | 6 +- .../templates/haskell/test/test.cabal | 4 +- .../test/blackbox/projectTemplate/.gitignore | 2 +- .../test/gradle/projectTemplate/.gitignore | 2 +- .../test/gradle/projectTemplate/build.gradle | 2 +- .../test/maven/projectTemplate/.gitignore | 2 +- .../templates/java/test/stagePom.xml | 2 +- .../templates/javascript/test/.gitignore | 2 +- .../javascript/test/package-lock.json | 6 +- .../templates/javascript/test/package.json | 2 +- .../test/maven/projectTemplate/.gitignore | 2 +- .../templates/kotlin/test/stagePom.xml | 2 +- .../resources/templates/ocaml/test/.gitignore | 4 +- .../templates/ocaml/test/checker/checker.ml | 2 +- .../resources/templates/ocaml/test/run.sh | 19 +-- .../python/test/behavior/behavior_test.py | 6 +- .../python/test/structural/structural_test.py | 8 +- .../resources/templates/rust/test/.gitignore | 2 +- .../resources/templates/rust/test/Cargo.toml | 2 +- .../resources/templates/rust/test/build.rs | 2 +- .../templates/rust/test/tests/structural.rs | 18 +-- .../templates/swift/Swift-Server-Setup.md | 2 +- .../xcschemes/${appName}Test.xcscheme | 4 +- .../xcschemes/${appName}UITest.xcscheme | 4 +- .../contents.xcworkspacedata | 4 +- .../templates/swift/xcode/test/.swiftlint.yml | 2 +- .../templates/swift/xcode/test/README.md | 4 +- .../programming-exercise-build.config.ts | 4 +- .../programming/programming-exercise.model.ts | 4 +- .../programming-exercise-update.component.ts | 41 +++++ .../programming-exercise-update.module.ts | 6 +- ...xercise-build-configuration.component.html | 45 ++++++ ...-exercise-build-configuration.component.ts | 21 +++ ...se-custom-aeolus-build-plan.component.html | 3 +- ...cise-custom-aeolus-build-plan.component.ts | 4 +- ...-exercise-custom-build-plan.component.html | 5 +- ...ng-exercise-custom-build-plan.component.ts | 31 +++- ...mming-exercise-docker-image.component.html | 16 -- ...ramming-exercise-docker-image.component.ts | 14 -- ...amming-exercise-information.component.html | 23 +++ ...gramming-exercise-information.component.ts | 55 ++++++- ...programming-exercise-language.component.ts | 21 ++- ...e-edit-checkout-directories.component.html | 58 +++++++ ...ise-edit-checkout-directories.component.ts | 113 ++++++++++++++ ...sitory-and-build-plan-details.component.ts | 74 ++++++++- .../app/shared/constants/input.constants.ts | 2 + .../webapp/i18n/de/programmingExercise.json | 17 ++ .../webapp/i18n/en/programmingExercise.json | 17 ++ .../service/BuildAgentDockerServiceTest.java | 2 +- .../icl/LocalCIIntegrationTest.java | 12 ++ .../icl/LocalCIResourceIntegrationTest.java | 4 +- .../programming/icl/LocalCIServiceTest.java | 2 +- ...ExerciseLocalVCLocalCIIntegrationTest.java | 13 ++ ...ise-build-configuration.component.spec.ts} | 19 ++- ...ercise-custom-build-plan.component.spec.ts | 9 +- ...dit-checkout-directories.component.spec.ts | 147 ++++++++++++++++++ ...y-and-build-plan-details.component.spec.ts | 83 +++++++++- ...gramming-exercise-update.component.spec.ts | 43 +++++ ...ing-exercise-information.component.spec.ts | 17 +- 98 files changed, 1307 insertions(+), 290 deletions(-) create mode 100644 src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.html delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.ts create mode 100644 src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.html create mode 100644 src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.ts rename src/test/javascript/spec/component/programming-exercise/{programming-exercise-docker-image.component.spec.ts => programming-exercise-build-configuration.component.spec.ts} (54%) create mode 100644 src/test/javascript/spec/component/programming-exercise/programming-exercise-edit-checkout-directories.component.spec.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java index bce9a9b20e65..9a41fc6fdc20 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java @@ -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 resultPaths) implements Serializable { + List resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath) implements Serializable { @Override public String dockerImage() { diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java index 8df60f45026f..b7c97daa9786 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java @@ -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; @@ -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. @@ -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++) { @@ -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); } @@ -428,4 +442,9 @@ private Container getContainerForName(String containerName) { List 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() : ""; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java index f25d5b6f749d..9c968c453e47 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java @@ -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); diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java index f514d59496cd..804d7d1e50df 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java @@ -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}") @@ -149,9 +149,17 @@ public CompletableFuture 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 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. diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java index 71e4dc0a5775..3f61f7de5ff0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java @@ -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 for the Java template pom.xml public static final String STUDENT_WORKING_DIRECTORY = ASSIGNMENT_DIRECTORY + "src"; @@ -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() { } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java index e8b5e554c37d..a5ada6708999 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java @@ -49,7 +49,13 @@ public class ProgrammingExerciseBuildConfig extends DomainObject { private boolean checkoutSolutionRepository = false; @Column(name = "checkout_path") - private String checkoutPath; + private String testCheckoutPath; + + @Column(name = "assignment_checkout_path") + private String assignmentCheckoutPath; + + @Column(name = "solution_checkout_path") + private String solutionCheckoutPath; @Column(name = "timeout_seconds") private int timeoutSeconds; @@ -85,7 +91,9 @@ public ProgrammingExerciseBuildConfig() { public ProgrammingExerciseBuildConfig(ProgrammingExerciseBuildConfig originalBuildConfig) { this.setBranch(originalBuildConfig.getBranch()); this.setBuildPlanConfiguration(originalBuildConfig.getBuildPlanConfiguration()); - this.setCheckoutPath(originalBuildConfig.getCheckoutPath()); + this.setTestCheckoutPath(originalBuildConfig.getTestCheckoutPath()); + this.setAssignmentCheckoutPath(originalBuildConfig.getAssignmentCheckoutPath()); + this.setSolutionCheckoutPath(originalBuildConfig.getSolutionCheckoutPath()); this.setCheckoutSolutionRepository(originalBuildConfig.getCheckoutSolutionRepository()); this.setDockerFlags(originalBuildConfig.getDockerFlags()); this.setSequentialTestRuns(originalBuildConfig.hasSequentialTestRuns()); @@ -166,12 +174,12 @@ public void setCheckoutSolutionRepository(boolean checkoutSolutionRepository) { this.checkoutSolutionRepository = checkoutSolutionRepository; } - public String getCheckoutPath() { - return checkoutPath; + public String getTestCheckoutPath() { + return testCheckoutPath; } - public void setCheckoutPath(String checkoutPath) { - this.checkoutPath = checkoutPath; + public void setTestCheckoutPath(String testCheckoutPath) { + this.testCheckoutPath = testCheckoutPath; } public int getTimeoutSeconds() { @@ -268,11 +276,27 @@ public void generateAndSetBuildPlanAccessSecret() { buildPlanAccessSecret = UUID.randomUUID().toString(); } + public String getAssignmentCheckoutPath() { + return assignmentCheckoutPath; + } + + public void setAssignmentCheckoutPath(String assignmentCheckoutPath) { + this.assignmentCheckoutPath = assignmentCheckoutPath; + } + + public String getSolutionCheckoutPath() { + return solutionCheckoutPath; + } + + public void setSolutionCheckoutPath(String solutionCheckoutPath) { + this.solutionCheckoutPath = solutionCheckoutPath; + } + @Override public String toString() { return "BuildJobConfig{" + "id=" + getId() + ", sequentialTestRuns=" + sequentialTestRuns + ", branch='" + branch + '\'' + ", buildPlanConfiguration='" + buildPlanConfiguration + '\'' + ", buildScript='" + buildScript + '\'' + ", checkoutSolutionRepository=" + checkoutSolutionRepository + ", checkoutPath='" - + checkoutPath + '\'' + ", timeoutSeconds=" + timeoutSeconds + ", dockerFlags='" + dockerFlags + '\'' + ", testwiseCoverageEnabled=" + testwiseCoverageEnabled + + testCheckoutPath + '\'' + ", timeoutSeconds=" + timeoutSeconds + ", dockerFlags='" + dockerFlags + '\'' + ", testwiseCoverageEnabled=" + testwiseCoverageEnabled + ", theiaImage='" + theiaImage + '\'' + ", allowBranching=" + allowBranching + ", branchRegex='" + branchRegex + '\'' + '}'; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java index 7d9a21297e75..23aa653d7301 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/AuxiliaryRepositoryService.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.programming.service; +import static de.tum.cit.aet.artemis.core.config.Constants.ALLOWED_CHECKOUT_DIRECTORY; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.ArrayList; @@ -7,7 +8,6 @@ import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -27,8 +27,6 @@ public class AuxiliaryRepositoryService { private static final String AUX_REPO_ENTITY_NAME = "programmingExercise"; - private static final Pattern ALLOWED_CHECKOUT_DIRECTORY = Pattern.compile("[\\w-]+(/[\\w-]+)*$"); - private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; public AuxiliaryRepositoryService(AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java index db9e37239d9e..487c92b3c21d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/BuildScriptProviderService.java @@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -18,6 +19,8 @@ import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.core.config.Constants; +import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; @@ -39,14 +42,17 @@ public class BuildScriptProviderService { private final Map scriptCache = new ConcurrentHashMap<>(); + private final ProfileService profileService; + /** * Constructor for BuildScriptProvider, which loads all scripts into the cache to speed up retrieval * during the runtime of the application * * @param resourceLoaderService resourceLoaderService */ - public BuildScriptProviderService(ResourceLoaderService resourceLoaderService) { + public BuildScriptProviderService(ResourceLoaderService resourceLoaderService, ProfileService profileService) { this.resourceLoaderService = resourceLoaderService; + this.profileService = profileService; } /** @@ -69,6 +75,9 @@ public void cacheOnBoot() { String uniqueKey = directory + "_" + filename; byte[] fileContent = IOUtils.toByteArray(resource.getInputStream()); String script = new String(fileContent, StandardCharsets.UTF_8); + if (!profileService.isLocalCiActive()) { + script = replacePlaceholders(script, null, null, null); + } scriptCache.put(uniqueKey, script); } catch (IOException e) { @@ -112,6 +121,9 @@ public String getScriptFor(ProgrammingLanguage programmingLanguage, Optional projectType, Boolean stati } return String.join("_", fileNameComponents) + "." + fileExtension; } + + /** + * Replaces placeholders in the given result paths with the actual paths. + * + * @param resultPaths the result paths to replace the placeholders in + * @param buildConfig the build configuration containing the actual paths + * @return the result paths with the placeholders replaced + */ + public List replaceResultPathsPlaceholders(List resultPaths, ProgrammingExerciseBuildConfig buildConfig) { + List replacedResultPaths = new ArrayList<>(); + for (String resultPath : resultPaths) { + String replacedResultPath = replacePlaceholders(resultPath, buildConfig.getAssignmentCheckoutPath(), buildConfig.getSolutionCheckoutPath(), + buildConfig.getTestCheckoutPath()); + replacedResultPaths.add(replacedResultPath); + } + return replacedResultPaths; + } + + /** + * Replaces placeholders in the given original string with the actual paths. + * + * @param originalString the original string to replace the placeholders in + * @param assignmentRepo the assignment repository name + * @param solutionRepo the solution repository name + * @param testRepo the test repository name + * @return the original string with the placeholders replaced + */ + public String replacePlaceholders(String originalString, String assignmentRepo, String solutionRepo, String testRepo) { + assignmentRepo = !StringUtils.isBlank(assignmentRepo) ? assignmentRepo : Constants.ASSIGNMENT_REPO_NAME; + solutionRepo = solutionRepo != null && !solutionRepo.isBlank() ? solutionRepo : Constants.SOLUTION_REPO_NAME; + testRepo = testRepo != null && !testRepo.isBlank() ? testRepo : Constants.TEST_REPO_NAME; + + String replacedResultPath = originalString.replace(Constants.ASSIGNMENT_REPO_PARENT_PLACEHOLDER, assignmentRepo); + replacedResultPath = replacedResultPath.replace(Constants.ASSIGNMENT_REPO_PLACEHOLDER, "/" + assignmentRepo + "/src"); + replacedResultPath = replacedResultPath.replace(Constants.SOLUTION_REPO_PLACEHOLDER, solutionRepo); + replacedResultPath = replacedResultPath.replace(Constants.TEST_REPO_PLACEHOLDER, testRepo); + return replacedResultPath; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java index db7803c21c22..c302c5f147e5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java @@ -17,6 +17,7 @@ import java.util.Optional; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -662,8 +663,32 @@ void replacePlaceholders(final ProgrammingExercise programmingExercise, final Re replacements.put("${exerciseNamePomXml}", programmingExercise.getTitle().replace(" ", "-")); // Used e.g. in artifactId replacements.put("${exerciseName}", programmingExercise.getTitle()); - replacements.put("${studentWorkingDirectory}", Constants.STUDENT_WORKING_DIRECTORY); replacements.put("${packaging}", programmingExercise.getBuildConfig().hasSequentialTestRuns() ? "pom" : "jar"); + + var buildConfig = programmingExercise.getBuildConfig(); + + // replace checkout directory placeholders + String studentWorkingDirectory = !StringUtils.isBlank(buildConfig.getAssignmentCheckoutPath()) ? buildConfig.getAssignmentCheckoutPath() : Constants.ASSIGNMENT_REPO_NAME; + if (studentWorkingDirectory.startsWith("/")) { + studentWorkingDirectory = studentWorkingDirectory.substring(1); + } + String testWorkingDirectory = buildConfig.getTestCheckoutPath() != null && !buildConfig.getTestCheckoutPath().isBlank() ? buildConfig.getTestCheckoutPath() + : Constants.TEST_REPO_NAME; + String solutionWorkingDirectory = buildConfig.getSolutionCheckoutPath() != null && !buildConfig.getSolutionCheckoutPath().isBlank() ? buildConfig.getSolutionCheckoutPath() + : Constants.SOLUTION_REPO_NAME; + + if (programmingLanguage == ProgrammingLanguage.PYTHON) { + replacements.put(Constants.ASSIGNMENT_REPO_PARENT_PLACEHOLDER, studentWorkingDirectory.replace("/", ".")); + } + else { + replacements.put(Constants.ASSIGNMENT_REPO_PARENT_PLACEHOLDER, studentWorkingDirectory); + } + replacements.put(Constants.ASSIGNMENT_REPO_PLACEHOLDER, "/" + studentWorkingDirectory + "/src"); + replacements.put(Constants.TEST_REPO_PLACEHOLDER, testWorkingDirectory); + replacements.put(Constants.SOLUTION_REPO_PLACEHOLDER, solutionWorkingDirectory); + if ((programmingLanguage == ProgrammingLanguage.JAVA && programmingExercise.getProjectType().isGradle()) || programmingLanguage == ProgrammingLanguage.RUST) { + replacements.put(Constants.ASSIGNMENT_REPO_PLACEHOLDER_NO_SLASH, studentWorkingDirectory + "/src"); + } fileService.replaceVariablesInFileRecursive(repository.getLocalPath().toAbsolutePath(), replacements, List.of("gradle-wrapper.jar")); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index 00e854f1d218..0cbf73da21ff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.programming.service; +import static de.tum.cit.aet.artemis.core.config.Constants.ALLOWED_CHECKOUT_DIRECTORY; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.SOLUTION; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.TEMPLATE; @@ -362,6 +363,7 @@ public void validateNewProgrammingExerciseSettings(ProgrammingExercise programmi programmingExercise.validateGeneralSettings(); programmingExercise.validateProgrammingSettings(); programmingExercise.validateSettingsForFeedbackRequest(); + validateCustomCheckoutPaths(programmingExercise); auxiliaryRepositoryService.validateAndAddAuxiliaryRepositoriesOfProgrammingExercise(programmingExercise, programmingExercise.getAuxiliaryRepositories()); submissionPolicyService.validateSubmissionPolicyCreation(programmingExercise); @@ -427,6 +429,27 @@ private void validatePackageName(ProgrammingExercise programmingExercise, Progra } } + private void validateCustomCheckoutPaths(ProgrammingExercise programmingExercise) { + var buildConfig = programmingExercise.getBuildConfig(); + + boolean assignmentCheckoutPathIsValid = isValidCheckoutPath(buildConfig.getAssignmentCheckoutPath()); + boolean solutionCheckoutPathIsValid = isValidCheckoutPath(buildConfig.getSolutionCheckoutPath()); + boolean testCheckoutPathIsValid = isValidCheckoutPath(buildConfig.getTestCheckoutPath()); + + if (!assignmentCheckoutPathIsValid || !solutionCheckoutPathIsValid || !testCheckoutPathIsValid) { + throw new BadRequestAlertException("The custom checkout paths are invalid", "Exercise", "checkoutDirectoriesInvalid"); + } + } + + private boolean isValidCheckoutPath(String checkoutPath) { + // Checkout paths are optional for the assignment, solution, and test repositories. If not set, the default path is used. + if (checkoutPath == null) { + return true; + } + Matcher matcher = ALLOWED_CHECKOUT_DIRECTORY.matcher(checkoutPath); + return matcher.matches(); + } + /** * Validates static code analysis settings * @@ -438,6 +461,22 @@ public void validateStaticCodeAnalysisSettings(ProgrammingExercise programmingEx programmingExercise.validateStaticCodeAnalysisSettings(programmingLanguageFeature); } + /** + * Validates the settings of an updated programming exercise. Checks if the custom checkout paths have changed. + * + * @param originalProgrammingExercise The original programming exercise + * @param updatedProgrammingExercise The updated programming exercise + */ + public void validateCheckoutDirectoriesUnchanged(ProgrammingExercise originalProgrammingExercise, ProgrammingExercise updatedProgrammingExercise) { + var originalBuildConfig = originalProgrammingExercise.getBuildConfig(); + var updatedBuildConfig = updatedProgrammingExercise.getBuildConfig(); + if (!Objects.equals(originalBuildConfig.getAssignmentCheckoutPath(), updatedBuildConfig.getAssignmentCheckoutPath()) + || !Objects.equals(originalBuildConfig.getSolutionCheckoutPath(), updatedBuildConfig.getSolutionCheckoutPath()) + || !Objects.equals(originalBuildConfig.getTestCheckoutPath(), updatedBuildConfig.getTestCheckoutPath())) { + throw new BadRequestAlertException("The custom checkout paths cannot be changed!", "programmingExercise", "checkoutDirectoriesChanged"); + } + } + /** * Creates build plans for a new programming exercise. * 1. Create the project for the exercise on the CI Server diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java index 14bde3cf26b1..64321ad3b61d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/aeolus/AeolusTemplateService.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import de.tum.cit.aet.artemis.core.config.ProgrammingLanguageConfiguration; +import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; @@ -46,13 +47,16 @@ public class AeolusTemplateService { private final BuildScriptProviderService buildScriptProviderService; + private final ProfileService profileService; + private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); public AeolusTemplateService(ProgrammingLanguageConfiguration programmingLanguageConfiguration, ResourceLoaderService resourceLoaderService, - BuildScriptProviderService buildScriptProviderService) { + BuildScriptProviderService buildScriptProviderService, ProfileService profileService) { this.programmingLanguageConfiguration = programmingLanguageConfiguration; this.resourceLoaderService = resourceLoaderService; this.buildScriptProviderService = buildScriptProviderService; + this.profileService = profileService; } /** @@ -76,6 +80,9 @@ public void cacheOnBoot() { String uniqueKey = directory + "_" + filename; byte[] fileContent = IOUtils.toByteArray(resource.getInputStream()); String script = new String(fileContent, StandardCharsets.UTF_8); + if (!profileService.isLocalCiActive()) { + script = buildScriptProviderService.replacePlaceholders(script, null, null, null); + } Windfile windfile = readWindfile(script); this.addInstanceVariablesToWindfile(windfile, ProgrammingLanguage.valueOf(directory.toUpperCase()), optionalProjectType); templateCache.put(uniqueKey, windfile); @@ -140,6 +147,9 @@ public Windfile getWindfileFor(ProgrammingLanguage programmingLanguage, Optional log.error("No windfile found for key {}", uniqueKey); return null; } + if (!profileService.isLocalCiActive()) { + scriptCache = buildScriptProviderService.replacePlaceholders(scriptCache, null, null, null); + } Windfile windfile = readWindfile(scriptCache); this.addInstanceVariablesToWindfile(windfile, programmingLanguage, projectType); templateCache.put(uniqueKey, windfile); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java index d8c03392661e..e4803619c97d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIBuildConfigurationService.java @@ -11,6 +11,7 @@ import de.tum.cit.aet.artemis.core.exception.LocalCIException; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; +import de.tum.cit.aet.artemis.programming.service.BuildScriptProviderService; import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusTemplateService; import de.tum.cit.aet.artemis.programming.service.aeolus.ScriptAction; import de.tum.cit.aet.artemis.programming.service.aeolus.Windfile; @@ -21,8 +22,11 @@ public class LocalCIBuildConfigurationService { private final AeolusTemplateService aeolusTemplateService; - public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateService) { + private final BuildScriptProviderService buildScriptProviderService; + + public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateService, BuildScriptProviderService buildScriptProviderService) { this.aeolusTemplateService = aeolusTemplateService; + this.buildScriptProviderService = buildScriptProviderService; } /** @@ -34,15 +38,15 @@ public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateServ */ public String createBuildScript(ProgrammingExercise programmingExercise) { - StringBuilder buildScript = new StringBuilder(); - buildScript.append("#!/bin/bash\n"); - buildScript.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); + StringBuilder buildScriptBuilder = new StringBuilder(); + buildScriptBuilder.append("#!/bin/bash\n"); + buildScriptBuilder.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); String customScript = buildConfig.getBuildScript(); // Todo: get default script if custom script is null before trying to get actions from windfile if (customScript != null) { - buildScript.append(customScript); + buildScriptBuilder.append(customScript); } else { List actions; @@ -62,16 +66,16 @@ public String createBuildScript(ProgrammingExercise programmingExercise) { actions.forEach(action -> { String workdir = action.getWorkdir(); if (workdir != null) { - buildScript.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir/").append(workdir).append("\n"); + buildScriptBuilder.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir/").append(workdir).append("\n"); } - buildScript.append(action.getScript()).append("\n"); + buildScriptBuilder.append(action.getScript()).append("\n"); if (workdir != null) { - buildScript.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); + buildScriptBuilder.append("cd ").append(LOCALCI_WORKING_DIRECTORY).append("/testing-dir\n"); } }); } - return buildScript.toString(); + return buildScriptProviderService.replacePlaceholders(buildScriptBuilder.toString(), programmingExercise.getBuildConfig().getAssignmentCheckoutPath(), + programmingExercise.getBuildConfig().getSolutionCheckoutPath(), programmingExercise.getBuildConfig().getTestCheckoutPath()); } - } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java index 10569adbbf30..5da2860ba799 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java @@ -42,6 +42,7 @@ import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; +import de.tum.cit.aet.artemis.programming.service.BuildScriptProviderService; import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusResult; @@ -75,6 +76,8 @@ public class LocalCITriggerService implements ContinuousIntegrationTriggerServic private final AeolusTemplateService aeolusTemplateService; + private final BuildScriptProviderService buildScriptProviderService; + private final ProgrammingLanguageConfiguration programmingLanguageConfiguration; private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; @@ -102,7 +105,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h LocalCIProgrammingLanguageFeatureService programmingLanguageFeatureService, Optional versionControlService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService, - ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService) { this.hazelcastInstance = hazelcastInstance; this.aeolusTemplateService = aeolusTemplateService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; @@ -114,6 +117,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h this.gitService = gitService; this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.exerciseDateService = exerciseDateService; + this.buildScriptProviderService = buildScriptProviderService; } @PostConstruct @@ -304,13 +308,15 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio } List resultPaths = getTestResultPaths(windfile); + resultPaths = buildScriptProviderService.replaceResultPathsPlaceholders(resultPaths, buildConfig); // Todo: If build agent does not have access to filesystem, we need to send the build script to the build agent and execute it there. programmingExercise.setBuildConfig(buildConfig); String buildScript = localCIBuildConfigurationService.createBuildScript(programmingExercise); return new BuildConfig(buildScript, dockerImage, commitHashToBuild, assignmentCommitHash, testCommitHash, branch, programmingLanguage, projectType, - staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths); + staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths, buildConfig.getTimeoutSeconds(), + buildConfig.getAssignmentCheckoutPath(), buildConfig.getTestCheckoutPath(), buildConfig.getSolutionCheckoutPath()); } private ProgrammingExerciseBuildConfig loadBuildConfig(ProgrammingExercise programmingExercise) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 7462f19512c1..2225baa81759 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -327,6 +327,9 @@ public ResponseEntity updateProgrammingExercise(@RequestBod } } + // Verify that the checkout directories have not been changed. This is required since the buildScript and result paths are determined during the creation of the exercise. + programmingExerciseService.validateCheckoutDirectoriesUnchanged(programmingExerciseBeforeUpdate, updatedProgrammingExercise); + // Verify that a theia image is provided when the online IDE is enabled if (updatedProgrammingExercise.isAllowOnlineIde() && updatedProgrammingExercise.getBuildConfig().getTheiaImage() == null) { throw new BadRequestAlertException("You need to provide a Theia image when the online IDE is enabled", ENTITY_NAME, "noTheiaImageProvided"); diff --git a/src/main/resources/config/application-buildagent.yml b/src/main/resources/config/application-buildagent.yml index b013910100e5..1439567b2cc9 100644 --- a/src/main/resources/config/application-buildagent.yml +++ b/src/main/resources/config/application-buildagent.yml @@ -18,7 +18,7 @@ artemis: specify-concurrent-builds: false concurrent-build-size: 1 asynchronous: true - timeout-seconds: 240 + timeout-seconds: 120 build-container-prefix: local-ci- proxies: use-system-proxy: false diff --git a/src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml b/src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml new file mode 100644 index 000000000000..b3fa6c9e1c47 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240816150000_changelog.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 23186d8dbad3..d496528a13ec 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -27,6 +27,7 @@ + diff --git a/src/main/resources/templates/aeolus/assembler/default.sh b/src/main/resources/templates/aeolus/assembler/default.sh index cca62d776297..e5a322facd87 100644 --- a/src/main/resources/templates/aeolus/assembler/default.sh +++ b/src/main/resources/templates/aeolus/assembler/default.sh @@ -8,15 +8,15 @@ provide_environment_information () { python3 --version pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt + pip3 install --user -r ${testWorkingDirectory}/requirements.txt else echo "$REQ_FILE does not exist" fi @@ -25,18 +25,18 @@ provide_environment_information () { prepare_makefile () { echo '⚙️ executing prepare_makefile' #!/usr/bin/env bash - rm -f assignment/{GNUmakefile, Makefile, makefile} - rm -f assignment/io.inc - cp -f tests/Makefile assignment/Makefile || exit 2 - cp -f tests/io.inc assignment/io.inc || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + rm -f ${studentParentWorkingDirectoryName}/io.inc + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cp -f ${testWorkingDirectory}/io.inc ${studentParentWorkingDirectoryName}/io.inc || exit 2 } run_and_compile () { echo '⚙️ executing run_and_compile' - cd tests - python3 compileTest.py ../assignment/ + cd ${testWorkingDirectory} + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml } junit () { diff --git a/src/main/resources/templates/aeolus/assembler/default.yaml b/src/main/resources/templates/aeolus/assembler/default.yaml index 3017be0a331b..7649ec646a51 100644 --- a/src/main/resources/templates/aeolus/assembler/default.yaml +++ b/src/main/resources/templates/aeolus/assembler/default.yaml @@ -7,15 +7,15 @@ actions: python3 --version pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt + pip3 install --user -r ${testWorkingDirectory}/requirements.txt else echo "$REQ_FILE does not exist" fi @@ -23,17 +23,17 @@ actions: - name: prepare_makefile script: |- #!/usr/bin/env bash - rm -f assignment/{GNUmakefile, Makefile, makefile} - rm -f assignment/io.inc - cp -f tests/Makefile assignment/Makefile || exit 2 - cp -f tests/io.inc assignment/io.inc || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + rm -f ${studentParentWorkingDirectoryName}/io.inc + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cp -f ${testWorkingDirectory}/io.inc ${studentParentWorkingDirectoryName}/io.inc || exit 2 runAlways: false - name: run_and_compile script: |- - cd tests - python3 compileTest.py ../assignment/ + cd ${testWorkingDirectory} + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml runAlways: false - name: junit script: |- @@ -41,6 +41,6 @@ actions: runAlways: true results: - name: junit_result.xml - path: assignment/result.xml + path: ${studentParentWorkingDirectoryName}/result.xml type: junit before: true diff --git a/src/main/resources/templates/aeolus/c/fact.sh b/src/main/resources/templates/aeolus/c/fact.sh index 3596904b322b..4f7252eefaba 100644 --- a/src/main/resources/templates/aeolus/c/fact.sh +++ b/src/main/resources/templates/aeolus/c/fact.sh @@ -8,8 +8,8 @@ setup_the_build_environment () { # Task Description: # Build and run all tests # ------------------------------ - # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R || true + # Updating ${studentParentWorkingDirectoryName} and test-reports ownership... + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R || true sudo mkdir test-reports sudo chown artemis_user:artemis_user test-reports/ -R || true } @@ -22,13 +22,13 @@ build_and_run_all_tests () { # Build and run all tests # ------------------------------ - rm -f assignment/GNUmakefile - rm -f assignment/Makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - cd tests + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cd ${testWorkingDirectory} python3 Tests.py rm Tests.py - rm -rf ./tests || true + rm -rf ./${testWorkingDirectory} || true } main () { diff --git a/src/main/resources/templates/aeolus/c/fact.yaml b/src/main/resources/templates/aeolus/c/fact.yaml index 2cca1f9526b3..9d3527e65503 100644 --- a/src/main/resources/templates/aeolus/c/fact.yaml +++ b/src/main/resources/templates/aeolus/c/fact.yaml @@ -8,7 +8,7 @@ actions: # Build and run all tests # ------------------------------ # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R || true + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R || true sudo mkdir test-reports sudo chown artemis_user:artemis_user test-reports/ -R || true runAlways: false @@ -20,13 +20,13 @@ actions: # Build and run all tests # ------------------------------ - rm -f assignment/GNUmakefile - rm -f assignment/Makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - cd tests + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + cd ${testWorkingDirectory} python3 Tests.py rm Tests.py - rm -rf ./tests || true + rm -rf ./${testWorkingDirectory} || true runAlways: false results: - name: junit_test-reports/tests-results.xml diff --git a/src/main/resources/templates/aeolus/c/gcc.sh b/src/main/resources/templates/aeolus/c/gcc.sh index 259f0bd886e0..3272660657a1 100644 --- a/src/main/resources/templates/aeolus/c/gcc.sh +++ b/src/main/resources/templates/aeolus/c/gcc.sh @@ -10,13 +10,13 @@ setup_the_build_environment () { # Build and run all tests # ------------------------------ - # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + # Updating ${studentParentWorkingDirectoryName} and test-reports ownership... + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -35,18 +35,18 @@ setup_makefile () { # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile } build_and_run_all_tests () { @@ -58,10 +58,10 @@ build_and_run_all_tests () { # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true fi } diff --git a/src/main/resources/templates/aeolus/c/gcc.yaml b/src/main/resources/templates/aeolus/c/gcc.yaml index 622b2148279d..29b2e2c635ec 100644 --- a/src/main/resources/templates/aeolus/c/gcc.yaml +++ b/src/main/resources/templates/aeolus/c/gcc.yaml @@ -10,12 +10,12 @@ actions: # ------------------------------ # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -33,18 +33,18 @@ actions: # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile runAlways: false - name: build_and_run_all_tests script: |- @@ -55,10 +55,10 @@ actions: # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true fi runAlways: false diff --git a/src/main/resources/templates/aeolus/c/gcc_static.sh b/src/main/resources/templates/aeolus/c/gcc_static.sh index 96847a3bbc92..2dfa84c2a569 100644 --- a/src/main/resources/templates/aeolus/c/gcc_static.sh +++ b/src/main/resources/templates/aeolus/c/gcc_static.sh @@ -10,13 +10,13 @@ setup_the_build_environment () { # Build and run all tests # ------------------------------ - # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + # Updating ${studentParentWorkingDirectoryName} and test-reports ownership... + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -35,18 +35,18 @@ setup_makefile () { # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile } build_and_run_all_tests () { @@ -58,10 +58,10 @@ build_and_run_all_tests () { # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true else exit 1 diff --git a/src/main/resources/templates/aeolus/c/gcc_static.yaml b/src/main/resources/templates/aeolus/c/gcc_static.yaml index be9e9eb1dc2f..21c0b506f179 100644 --- a/src/main/resources/templates/aeolus/c/gcc_static.yaml +++ b/src/main/resources/templates/aeolus/c/gcc_static.yaml @@ -10,12 +10,12 @@ actions: # ------------------------------ # Updating assignment and test-reports ownership... - sudo chown artemis_user:artemis_user assignment/ -R + sudo chown artemis_user:artemis_user ${studentParentWorkingDirectoryName}/ -R mkdir test-reports chown artemis_user:artemis_user test-reports/ -R # assignment - cd tests + cd ${testWorkingDirectory} REQ_FILE=requirements.txt if [ -f "$REQ_FILE" ]; then pip3 install --user -r requirements.txt || true @@ -33,18 +33,18 @@ actions: # Setup makefile # ------------------------------ - shadowFilePath="../tests/testUtils/c/shadow_exec.c" + shadowFilePath="../${testWorkingDirectory}/testUtils/c/shadow_exec.c" - foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' assignment/Makefile` + foundIncludeDirs=`grep -m 1 'INCLUDEDIRS\s*=' ${studentParentWorkingDirectoryName}/Makefile` - foundSource=`grep -m 1 'SOURCE\s*=' assignment/Makefile` + foundSource=`grep -m 1 'SOURCE\s*=' ${studentParentWorkingDirectoryName}/Makefile` foundSource="$foundSource $shadowFilePath" - rm -f assignment/GNUmakefile - rm -f assignment/makefile + rm -f ${studentParentWorkingDirectoryName}/GNUmakefile + rm -f ${studentParentWorkingDirectoryName}/makefile - cp -f tests/Makefile assignment/Makefile || exit 2 - sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" assignment/Makefile + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 + sed -i "s~\bINCLUDEDIRS\s*=.*~${foundIncludeDirs}~; s~\bSOURCE\s*=.*~${foundSource}~" ${studentParentWorkingDirectoryName}/Makefile runAlways: false - name: build_and_run_all_tests script: |- @@ -55,10 +55,10 @@ actions: # Build and run all tests if the compilation succeeds # ------------------------------ sudo chown artemis_user:artemis_user . - gcc -c -Wall assignment/*.c || error=true + gcc -c -Wall ${studentParentWorkingDirectoryName}/*.c || error=true if [ ! $error ] then - cd tests || exit 0 + cd ${testWorkingDirectory} || exit 0 python3 Tests.py || true else exit 1 diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh index 46186cf50311..190bc88c4831 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.sh @@ -9,9 +9,9 @@ build () { checkers () { echo '⚙️ executing checkers' # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml index 2b4428a585d1..02d0ff15b7ad 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox.yaml @@ -5,9 +5,9 @@ actions: - name: checkers script: |- # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh index 9a6be87561a0..3a1af9d0d213 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.sh @@ -9,9 +9,9 @@ build () { checkers () { echo '⚙️ executing checkers' # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml index 68c71c046d3d..7cd6a6c07773 100644 --- a/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml +++ b/src/main/resources/templates/aeolus/java/plain_maven_blackbox_static.yaml @@ -5,9 +5,9 @@ actions: - name: checkers script: |- # all java files in the assignment folder should have maximal line length 80 - pipeline-helper line-length -l 80 -s assignment/ -e java + pipeline-helper line-length -l 80 -s ${studentParentWorkingDirectoryName}/ -e java # checks that the file exists and is not empty for non gui programs - pipeline-helper file-exists assignment/Tests.txt + pipeline-helper file-exists ${studentParentWorkingDirectoryName}/Tests.txt main_checker_output=$(pipeline-helper main-method -s target/classes) diff --git a/src/main/resources/templates/aeolus/ocaml/default.sh b/src/main/resources/templates/aeolus/ocaml/default.sh index ac1cf8b65b8d..f5b5f595b38d 100644 --- a/src/main/resources/templates/aeolus/ocaml/default.sh +++ b/src/main/resources/templates/aeolus/ocaml/default.sh @@ -3,7 +3,7 @@ set -e export AEOLUS_INITIAL_DIRECTORY=${PWD} build_and_test_the_code () { echo '⚙️ executing build_and_test_the_code' - cd "tests" + cd "${testWorkingDirectory}" # the build process is specified in `run.sh` in the test repository chmod +x run.sh ./run.sh -s diff --git a/src/main/resources/templates/aeolus/ocaml/default.yaml b/src/main/resources/templates/aeolus/ocaml/default.yaml index df5aef046d3a..70cdfcad1b9f 100644 --- a/src/main/resources/templates/aeolus/ocaml/default.yaml +++ b/src/main/resources/templates/aeolus/ocaml/default.yaml @@ -5,7 +5,7 @@ actions: # the build process is specified in `run.sh` in the test repository chmod +x run.sh ./run.sh -s - workdir: tests + workdir: ${testWorkingDirectory} runAlways: false - name: junit script: '#empty script action, just for the results' diff --git a/src/main/resources/templates/aeolus/swift/plain.sh b/src/main/resources/templates/aeolus/swift/plain.sh index 2673e861d564..71387b392783 100644 --- a/src/main/resources/templates/aeolus/swift/plain.sh +++ b/src/main/resources/templates/aeolus/swift/plain.sh @@ -4,12 +4,12 @@ set -e build_and_test_the_code () { echo '⚙️ executing build_and_test_the_code' # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} - # In order to get the correct console output we need to execute the command within the assignment directory + # In order to get the correct console output we need to execute the command within the ${studentParentWorkingDirectoryName} directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] diff --git a/src/main/resources/templates/aeolus/swift/plain.yaml b/src/main/resources/templates/aeolus/swift/plain.yaml index c90994ea4c4a..48211dee715a 100644 --- a/src/main/resources/templates/aeolus/swift/plain.yaml +++ b/src/main/resources/templates/aeolus/swift/plain.yaml @@ -3,12 +3,12 @@ actions: - name: build_and_test_the_code script: |- # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} # In order to get the correct console output we need to execute the command within the assignment directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] @@ -24,6 +24,6 @@ actions: runAlways: false results: - name: junit_tests.xml - path: assignment/tests.xml + path: ${studentParentWorkingDirectoryName}/tests.xml type: junit before: true diff --git a/src/main/resources/templates/aeolus/swift/plain_static.sh b/src/main/resources/templates/aeolus/swift/plain_static.sh index 835494ff5454..3bbaa9fe0662 100644 --- a/src/main/resources/templates/aeolus/swift/plain_static.sh +++ b/src/main/resources/templates/aeolus/swift/plain_static.sh @@ -3,14 +3,14 @@ set -e export AEOLUS_INITIAL_DIRECTORY=${PWD} build_and_test_the_code () { echo '⚙️ executing build_and_test_the_code' - cp -R Sources assignment + cp -R Sources ${studentParentWorkingDirectoryName} # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} - # In order to get the correct console output we need to execute the command within the assignment directory + # In order to get the correct console output we need to execute the command within the ${studentParentWorkingDirectoryName} directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] @@ -28,10 +28,10 @@ build_and_test_the_code () { run_static_code_analysis () { echo '⚙️ executing run_static_code_analysis' # Copy SwiftLint rules - cp .swiftlint.yml assignment || true + cp .swiftlint.yml ${studentParentWorkingDirectoryName} || true # create target directory for SCA Parser mkdir target - cd assignment + cd ${studentParentWorkingDirectoryName} # Execute static code analysis swiftlint > ../target/swiftlint-result.xml } diff --git a/src/main/resources/templates/aeolus/swift/plain_static.yaml b/src/main/resources/templates/aeolus/swift/plain_static.yaml index c900edc82d44..72f683141903 100644 --- a/src/main/resources/templates/aeolus/swift/plain_static.yaml +++ b/src/main/resources/templates/aeolus/swift/plain_static.yaml @@ -2,14 +2,14 @@ api: v0.0.1 actions: - name: build_and_test_the_code script: |- - cp -R Sources assignment + cp -R Sources ${studentParentWorkingDirectoryName} # copy test files - cp -R Tests assignment - cp Package.swift assignment + cp -R Tests ${studentParentWorkingDirectoryName} + cp Package.swift ${studentParentWorkingDirectoryName} # In order to get the correct console output we need to execute the command within the assignment directory # swift build - cd assignment + cd ${studentParentWorkingDirectoryName} swift build || error=true if [ ! $error ] @@ -26,10 +26,10 @@ actions: - name: run_static_code_analysis script: |- # Copy SwiftLint rules - cp .swiftlint.yml assignment || true + cp .swiftlint.yml ${studentParentWorkingDirectoryName} || true # create target directory for SCA Parser mkdir target - cd assignment + cd ${studentParentWorkingDirectoryName} # Execute static code analysis swiftlint > ../target/swiftlint-result.xml runAlways: true @@ -39,6 +39,6 @@ actions: before: false type: static-code-analysis - name: junit_tests.xml - path: assignment/tests.xml + path: ${studentParentWorkingDirectoryName}/tests.xml type: junit before: true diff --git a/src/main/resources/templates/aeolus/vhdl/default.sh b/src/main/resources/templates/aeolus/vhdl/default.sh index f4f5a3f3f609..747c1b20c58f 100644 --- a/src/main/resources/templates/aeolus/vhdl/default.sh +++ b/src/main/resources/templates/aeolus/vhdl/default.sh @@ -9,16 +9,16 @@ provide_environment_information () { pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt || true + pip3 install --user -r ${testWorkingDirectory}/requirements.txt || true else echo "$REQ_FILE does not exist" fi @@ -26,16 +26,16 @@ provide_environment_information () { prepare_makefile () { echo '⚙️ executing prepare_makefile' - rm -f assignment/{GNUmakefile, Makefile, makefile} - cp -f tests/Makefile assignment/Makefile || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 } run_and_compile () { echo '⚙️ executing run_and_compile' - cd "tests" - python3 compileTest.py ../assignment/ + cd "${testWorkingDirectory}" + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml } junit () { diff --git a/src/main/resources/templates/aeolus/vhdl/default.yaml b/src/main/resources/templates/aeolus/vhdl/default.yaml index 872f94622916..64bcb4365fc5 100644 --- a/src/main/resources/templates/aeolus/vhdl/default.yaml +++ b/src/main/resources/templates/aeolus/vhdl/default.yaml @@ -8,37 +8,37 @@ actions: pip3 --version echo "--------------------Contents of tests repository--------------------" - ls -la tests + ls -la ${testWorkingDirectory} echo "---------------------------------------------" echo "--------------------Contents of assignment repository--------------------" - ls -la assignment + ls -la ${studentParentWorkingDirectoryName} echo "---------------------------------------------" #Fallback in case Docker does not work as intended - REQ_FILE=tests/requirements.txt + REQ_FILE=${testWorkingDirectory}/requirements.txt if [ -f "$REQ_FILE" ]; then - pip3 install --user -r tests/requirements.txt || true + pip3 install --user -r ${testWorkingDirectory}/requirements.txt || true else echo "$REQ_FILE does not exist" fi runAlways: false - name: prepare_makefile script: |- - rm -f assignment/{GNUmakefile, Makefile, makefile} - cp -f tests/Makefile assignment/Makefile || exit 2 + rm -f ${studentParentWorkingDirectoryName}/{GNUmakefile, Makefile, makefile} + cp -f ${testWorkingDirectory}/Makefile ${studentParentWorkingDirectoryName}/Makefile || exit 2 runAlways: false - name: run_and_compile script: |- - python3 compileTest.py ../assignment/ + python3 compileTest.py ../${studentParentWorkingDirectoryName}/ rm compileTest.py - cp result.xml ../assignment/result.xml - workdir: tests + cp result.xml ../${studentParentWorkingDirectoryName}/result.xml + workdir: ${testWorkingDirectory} runAlways: false - name: junit script: '#empty script action, just for the results' runAlways: true results: - name: assignment_junit_results - path: assignment/result.xml + path: ${studentParentWorkingDirectoryName}/result.xml type: junit before: true diff --git a/src/main/resources/templates/haskell/test/.gitignore b/src/main/resources/templates/haskell/test/.gitignore index 38ce398dd22a..39312aeed502 100755 --- a/src/main/resources/templates/haskell/test/.gitignore +++ b/src/main/resources/templates/haskell/test/.gitignore @@ -3,10 +3,10 @@ test-reports/ # Subdirectories containing other repositories template/ -solution/ +${solutionWorkingDirectory}/ # Subdirectories with test submission -assignment/ +${studentParentWorkingDirectoryName}/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/haskell/test/readme.md b/src/main/resources/templates/haskell/test/readme.md index 7ab5dcefbf25..45ac2724c923 100644 --- a/src/main/resources/templates/haskell/test/readme.md +++ b/src/main/resources/templates/haskell/test/readme.md @@ -6,8 +6,8 @@ Tests are run using [stack](https://docs.haskellstack.org/en/stable/README/) in ## Setup -The executables specified in `test.cabal` expect the solution repository checked out in the `solution` subdirectory and -the submission checked out in the `assignment` subdirectory. +The executables specified in `test.cabal` expect the solution repository checked out in the `${solutionWorkingDirectory}` subdirectory and +the submission checked out in the `${studentParentWorkingDirectoryName}` subdirectory. Moreover, `test.cabal` provides an executable to test the template repository locally. For this, it expects the template repository in the `template` subdirectory. diff --git a/src/main/resources/templates/haskell/test/run.sh b/src/main/resources/templates/haskell/test/run.sh index 06169da575a2..562c1139cf0f 100755 --- a/src/main/resources/templates/haskell/test/run.sh +++ b/src/main/resources/templates/haskell/test/run.sh @@ -13,20 +13,20 @@ done shift $((OPTIND-1)) # check for symlinks as they might be abused to link to the sample solution -$safe && find assignment/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 1 +$safe && find ${studentParentWorkingDirectoryName}/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 1 # check for unsafe OPTIONS and OPTIONS_GHC pragma as they allow to overwrite command line arguments $safe && \ while IFS= read file; do cat $file | tr -d '\n' | grep -qim 1 "{-#[[:space:]]*options" && \ echo "Cannot build with \"{-# OPTIONS..\" pragma in source." && exit 1 -done < <(find assignment/src -type f) +done < <(find ${studentParentWorkingDirectoryName}/src -type f) # build the libraries - do not forget to set the right compilation flag (Prod) stack build --allow-different-user --flag test:Prod && \ # delete the solution and tests (so that students cannot access it) when in safe mode ($safe && \ - (rm -rf solution && rm -rf test) \ + (rm -rf ${solutionWorkingDirectory} && rm -rf test) \ ) \ # run the test executable and return 0 # Note: as a convention, a failed haskell tasty test suite returns 1, but this stops the JUnit Parser from running. diff --git a/src/main/resources/templates/haskell/test/test.cabal b/src/main/resources/templates/haskell/test/test.cabal index 7cf429c106e3..16977cf154b8 100644 --- a/src/main/resources/templates/haskell/test/test.cabal +++ b/src/main/resources/templates/haskell/test/test.cabal @@ -56,7 +56,7 @@ library submission -- by setting it to a non-existent program called `nonExistentCPP`. See -- https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/phases.html ghc-options: -fpackage-trust -trust base -pgmP nonExistentCPP - hs-source-dirs: assignment/src + hs-source-dirs: ${studentParentWorkingDirectoryName}/src exposed-modules: Exercise -- build the local template @@ -74,7 +74,7 @@ library template -- build the solution library solution import: common-all - hs-source-dirs: solution/src + hs-source-dirs: ${solutionWorkingDirectory}/src exposed-modules: Exercise -- run tests for a submission diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore b/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore index 44c742ee253c..2bb4fca67c4f 100644 --- a/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/.gitignore @@ -1,4 +1,4 @@ -assignment/ +${studentParentWorkingDirectoryName}/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore b/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore index bde9056a0f14..964ec2319a13 100644 --- a/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore +++ b/src/main/resources/templates/java/test/gradle/projectTemplate/.gitignore @@ -1,5 +1,5 @@ .gradle -assignment/ +${studentParentWorkingDirectoryName}/ **/build/ !src/**/build/ target/ diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle b/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle index c6d2e5f6689a..97be1ab67bfd 100644 --- a/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle +++ b/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle @@ -28,7 +28,7 @@ dependencies { // testImplementation(':${exerciseNamePomXml}-Solution') } -def assignmentSrcDir = "assignment/src" +def assignmentSrcDir = "${studentWorkingDirectoryNoSlash}" def studentOutputDir = sourceSets.main.java.destinationDirectory.get() // %static-code-analysis-start% def scaConfigDirectory = "$projectDir/staticCodeAnalysisConfig" diff --git a/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore b/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore index dd177405a0d3..dbf843d6c857 100644 --- a/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore +++ b/src/main/resources/templates/java/test/maven/projectTemplate/.gitignore @@ -1,4 +1,4 @@ -assignment/ +${studentParentWorkingDirectoryName}/ target/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/java/test/stagePom.xml b/src/main/resources/templates/java/test/stagePom.xml index fea2492362ef..18cfa54fc39f 100644 --- a/src/main/resources/templates/java/test/stagePom.xml +++ b/src/main/resources/templates/java/test/stagePom.xml @@ -8,7 +8,7 @@ 4.0.0 ${exerciseNamePomXml}-Tests - ${project.basedir}/../assignment/src + ${project.basedir}/..${studentWorkingDirectory} org.apache.maven.plugins diff --git a/src/main/resources/templates/javascript/test/.gitignore b/src/main/resources/templates/javascript/test/.gitignore index d81d793eaed4..0d3cf4874427 100644 --- a/src/main/resources/templates/javascript/test/.gitignore +++ b/src/main/resources/templates/javascript/test/.gitignore @@ -1,4 +1,4 @@ node_modules/ -/assignment +/${studentParentWorkingDirectoryName} /junit.xml diff --git a/src/main/resources/templates/javascript/test/package-lock.json b/src/main/resources/templates/javascript/test/package-lock.json index b18c57c3b694..79830909997b 100644 --- a/src/main/resources/templates/javascript/test/package-lock.json +++ b/src/main/resources/templates/javascript/test/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "artemis-test", "workspaces": [ - "assignment" + "${studentParentWorkingDirectoryName}" ], "devDependencies": { "@babel/core": "^7.24.7", @@ -17,7 +17,7 @@ "jest-junit": "^16.0.0" } }, - "assignment": { + "${studentParentWorkingDirectoryName}": { "name": "artemis-exercise" }, "node_modules/@ampproject/remapping": { @@ -2499,7 +2499,7 @@ } }, "node_modules/artemis-exercise": { - "resolved": "assignment", + "resolved": "${studentParentWorkingDirectoryName}", "link": true }, "node_modules/babel-jest": { diff --git a/src/main/resources/templates/javascript/test/package.json b/src/main/resources/templates/javascript/test/package.json index 3971d2b0f3c6..782e6c431492 100644 --- a/src/main/resources/templates/javascript/test/package.json +++ b/src/main/resources/templates/javascript/test/package.json @@ -6,7 +6,7 @@ "test:ci": "jest --ci --reporters=default --reporters=jest-junit" }, "workspaces": [ - "assignment" + "${studentParentWorkingDirectoryName}" ], "devDependencies": { "@babel/core": "^7.24.7", diff --git a/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore b/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore index bff99bd68dc5..5c7928c0bc37 100644 --- a/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore +++ b/src/main/resources/templates/kotlin/test/maven/projectTemplate/.gitignore @@ -1,4 +1,4 @@ -assignment/ +${studentParentWorkingDirectoryName}/ target/ # Taken from https://github.com/github/gitignore diff --git a/src/main/resources/templates/kotlin/test/stagePom.xml b/src/main/resources/templates/kotlin/test/stagePom.xml index 89bcbda7d484..d525536c92f6 100644 --- a/src/main/resources/templates/kotlin/test/stagePom.xml +++ b/src/main/resources/templates/kotlin/test/stagePom.xml @@ -8,7 +8,7 @@ 4.0.0 ${exerciseNamePomXml}-Tests - ${project.basedir}/../assignment/src + ${project.basedir}/..${studentWorkingDirectory} ${project.basedir}/test diff --git a/src/main/resources/templates/ocaml/test/.gitignore b/src/main/resources/templates/ocaml/test/.gitignore index e477e6e6a9b6..fdad97ced9bf 100644 --- a/src/main/resources/templates/ocaml/test/.gitignore +++ b/src/main/resources/templates/ocaml/test/.gitignore @@ -1,6 +1,6 @@ # Things generated by the test framework -/solution/*.ml -/assignment/*.ml +/${solutionWorkingDirectory}/*.ml +/${studentParentWorkingDirectoryName}/*.ml /test/runHidden.ml /checker/checker.exe diff --git a/src/main/resources/templates/ocaml/test/checker/checker.ml b/src/main/resources/templates/ocaml/test/checker/checker.ml index b13edbad4032..daa6d48b56fa 100644 --- a/src/main/resources/templates/ocaml/test/checker/checker.ml +++ b/src/main/resources/templates/ocaml/test/checker/checker.ml @@ -96,7 +96,7 @@ let checkFile fn = violation := true; Location.report_exception Format.err_formatter exn -let studentDir = "assignment" +let studentDir = "${studentParentWorkingDirectoryName}" (** check all student files for violations *) let _ = diff --git a/src/main/resources/templates/ocaml/test/run.sh b/src/main/resources/templates/ocaml/test/run.sh index fe73a2830c98..3dc4af3ff7cd 100755 --- a/src/main/resources/templates/ocaml/test/run.sh +++ b/src/main/resources/templates/ocaml/test/run.sh @@ -3,6 +3,7 @@ # copy code from the assignment or solution to the appropriate test folder cp_code() { + mv "$2" "$1" cd "$1" || exit rm ./*.ml >/dev/null 2>&1 # shellcheck disable=SC2086 @@ -44,13 +45,13 @@ else fi # check for symlink is the submission -find ../assignment/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 0 +find ../${studentParentWorkingDirectoryName}/ -type l | grep -q . && echo "Cannot build with symlinks in submission." && exit 0 # include solution and assignment in the tests # this will only pick up *.ml files in the /src folders if other files are required for the tests this needs to be adjusted -cp_code solution -echo 'include Assignment' > solution/solution.ml -cp_code assignment +cp_code ${solutionWorkingDirectory} solution +echo 'include Assignment' > ${solutionWorkingDirectory}/solution.ml +cp_code ${studentParentWorkingDirectoryName} assignment # select if tests are run by generated source code as student toplevel code may run before the tests and be able to spoof a runtime signal echo "let runHidden = $RUN_HIDDEN" > test/runHidden.ml @@ -71,7 +72,7 @@ if ! timeout -s SIGTERM $BUILD_TIMEOUT checker/checker.exe; then fi # build the student submission # don't reference the tests or solution, so that we can show the build output to the student and not leak test / solution code -if ! timeout -s SIGTERM $BUILD_TIMEOUT dune build --force assignment; then +if ! timeout -s SIGTERM $BUILD_TIMEOUT dune build --force ${solutionWorkingDirectory}; then echo "Unable to build submission, please ensure that your code builds and matches the provided interface" >&2 exit 0 fi @@ -85,13 +86,13 @@ fi cd "$BUILD_ROOT" || exit # copy the test executable into the project root -mv -f tests/test/test.exe ./ +mv -f ${testWorkingDirectory}/test/test.exe ./ # to then delete all source code, to prevent access to it while running the code if $SAFE; then - rm -rf assignment - rm -rf solution - rm -rf tests + rm -rf ${studentParentWorkingDirectoryName} + rm -rf ${solutionWorkingDirectory} + rm -rf ${testWorkingDirectory} fi; # running the test executable without arguments to cause them to exit without actually running any tests diff --git a/src/main/resources/templates/python/test/behavior/behavior_test.py b/src/main/resources/templates/python/test/behavior/behavior_test.py index 15ad384df904..31c0491b0a9c 100644 --- a/src/main/resources/templates/python/test/behavior/behavior_test.py +++ b/src/main/resources/templates/python/test/behavior/behavior_test.py @@ -1,7 +1,7 @@ import unittest -from assignment.sorting_algorithms import * -from assignment.context import Context -from assignment.policy import Policy +from ${studentParentWorkingDirectoryName}.sorting_algorithms import * +from ${studentParentWorkingDirectoryName}.context import Context +from ${studentParentWorkingDirectoryName}.policy import Policy class TestSortingBehavior(unittest.TestCase): diff --git a/src/main/resources/templates/python/test/structural/structural_test.py b/src/main/resources/templates/python/test/structural/structural_test.py index 24ed0f758476..96c30edd118e 100644 --- a/src/main/resources/templates/python/test/structural/structural_test.py +++ b/src/main/resources/templates/python/test/structural/structural_test.py @@ -1,8 +1,8 @@ import unittest -from assignment import sorting_algorithms -from assignment import sort_strategy -from assignment import context -from assignment import policy +from ${studentParentWorkingDirectoryName} import sorting_algorithms +from ${studentParentWorkingDirectoryName} import sort_strategy +from ${studentParentWorkingDirectoryName} import context +from ${studentParentWorkingDirectoryName} import policy from structural import structural_helpers diff --git a/src/main/resources/templates/rust/test/.gitignore b/src/main/resources/templates/rust/test/.gitignore index a37c5236a444..aecf1e1c91fa 100644 --- a/src/main/resources/templates/rust/test/.gitignore +++ b/src/main/resources/templates/rust/test/.gitignore @@ -1,2 +1,2 @@ /target -/assignment +/${studentParentWorkingDirectoryName} diff --git a/src/main/resources/templates/rust/test/Cargo.toml b/src/main/resources/templates/rust/test/Cargo.toml index 07f82b3f09f0..f7f39b905be0 100644 --- a/src/main/resources/templates/rust/test/Cargo.toml +++ b/src/main/resources/templates/rust/test/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4.38", default-features = false } -rust-template-exercise = { path = "assignment" } +rust-template-exercise = { path = "${studentParentWorkingDirectoryName}" } syn = { version = "2.0.72", features = ["full"] } rust_template_test_macros = { path = "./rust_template_test_macros" } diff --git a/src/main/resources/templates/rust/test/build.rs b/src/main/resources/templates/rust/test/build.rs index 850fcb846cce..947f4783dbd7 100644 --- a/src/main/resources/templates/rust/test/build.rs +++ b/src/main/resources/templates/rust/test/build.rs @@ -6,7 +6,7 @@ use std::{fs, io}; use syn::{parse_file, FnArg, ImplItem, Item, TraitItem, Type, TypeParamBound}; -const SRC_DIR: &str = "assignment/src"; +const SRC_DIR: &str = "${studentWorkingDirectoryNoSlash}"; fn main() { println!("cargo::rerun-if-changed={SRC_DIR}"); diff --git a/src/main/resources/templates/rust/test/tests/structural.rs b/src/main/resources/templates/rust/test/tests/structural.rs index 70b0d19c9131..cf0bef375605 100644 --- a/src/main/resources/templates/rust/test/tests/structural.rs +++ b/src/main/resources/templates/rust/test/tests/structural.rs @@ -4,14 +4,14 @@ use structural_helpers::*; #[test] fn test_sort_strategy_trait() { - let ast = parse_file("./assignment/src/sort_strategy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/sort_strategy.rs"); check_trait_names(&ast.items, ["SortStrategy"]) .unwrap_or_else(|name| panic!("A trait named \"{name}\" should be defined")); } #[test] fn test_sort_strategy_supertrait() { - let ast = parse_file("./assignment/src/sort_strategy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/sort_strategy.rs"); let sort_strategy = find_trait(&ast.items, "SortStrategy") .expect("A trait named \"SortStrategy\" should be defined"); check_trait_supertrait(sort_strategy, "Any") @@ -20,7 +20,7 @@ fn test_sort_strategy_supertrait() { #[test] fn test_sort_strategy_methods() { - let ast = parse_file("./assignment/src/sort_strategy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/sort_strategy.rs"); let sort_strategy = find_trait(&ast.items, "SortStrategy") .expect("A trait named \"SortStrategy\" should be defined"); check_trait_function_names(&sort_strategy.items, ["perform_sort"]) @@ -29,7 +29,7 @@ fn test_sort_strategy_methods() { #[test] fn test_context_fields() { - let ast = parse_file("./assignment/src/context.rs"); + let ast = parse_file(".${studentWorkingDirectory}/context.rs"); let context = find_struct(&ast.items, "Context").expect("A struct named \"Context\" should be defined"); check_struct_field_names(&context.fields, ["sort_algorithm"]) @@ -38,7 +38,7 @@ fn test_context_fields() { #[test] fn test_context_methods() { - let ast = parse_file("./assignment/src/context.rs"); + let ast = parse_file(".${studentWorkingDirectory}/context.rs"); let context_impl = find_impl(&ast.items, "Context").expect("SortStrategy should implement functions"); check_impl_function_names(&context_impl.items, ["new", "sort", "sort_algorithm"]) @@ -47,7 +47,7 @@ fn test_context_methods() { #[test] fn test_policy_fields() { - let ast = parse_file("./assignment/src/policy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/policy.rs"); let policy = find_struct(&ast.items, "Policy").expect("A struct named \"Policy\" should be defined"); check_struct_field_names(&policy.fields, ["context"]) @@ -56,7 +56,7 @@ fn test_policy_fields() { #[test] fn test_policy_methods() { - let ast = parse_file("./assignment/src/policy.rs"); + let ast = parse_file(".${studentWorkingDirectory}/policy.rs"); let policy_impl = find_impl(&ast.items, "Policy").expect("Policy should implement functions"); check_impl_function_names(&policy_impl.items, ["new", "configure"]) .unwrap_or_else(|name| panic!("Policy should implement the function \"{name}\"")); @@ -64,7 +64,7 @@ fn test_policy_methods() { #[test] fn test_bubble_sort_struct() { - let ast = parse_file("./assignment/src/bubble_sort.rs"); + let ast = parse_file(".${studentWorkingDirectory}/bubble_sort.rs"); find_struct(&ast.items, "BubbleSort").expect("A struct named \"BubbleSort\" should be defined"); find_impl_for(&ast.items, "BubbleSort", "SortStrategy") .expect("BubbleSort should implement the trait \"SortStrategy\""); @@ -72,7 +72,7 @@ fn test_bubble_sort_struct() { #[test] fn test_merge_sort_struct() { - let ast = parse_file("./assignment/src/merge_sort.rs"); + let ast = parse_file("./${studentWorkingDirectory}/merge_sort.rs"); find_struct(&ast.items, "MergeSort").expect("A struct named \"MergeSort\" should be defined"); find_impl_for(&ast.items, "MergeSort", "SortStrategy") .expect("MergeSort should implement the trait \"SortStrategy\""); diff --git a/src/main/resources/templates/swift/Swift-Server-Setup.md b/src/main/resources/templates/swift/Swift-Server-Setup.md index 425f7244566c..58cc83d1a18b 100644 --- a/src/main/resources/templates/swift/Swift-Server-Setup.md +++ b/src/main/resources/templates/swift/Swift-Server-Setup.md @@ -55,7 +55,7 @@ Append following to ~/.bashrc: # Bamboo Build Plan ## Create Tasks Go to Plan Configuration > Default Job > Tasks -- Create default task to checkout repos "tests and assignment" +- Create default task to checkout repos "tests and ${studentParentWorkingDirectoryName}" - Create a task to build the swift project - Name the task `Build swift`. - Interpreter: `Shell` diff --git a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme index fa4508ce15ce..1d83b9c00883 100644 --- a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme +++ b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}Test.xcscheme @@ -41,7 +41,7 @@ BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> @@ -57,7 +57,7 @@ BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> diff --git a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme index 11353953efa7..86d626978f97 100644 --- a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme +++ b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcodeproj/xcshareddata/xcschemes/${appName}UITest.xcscheme @@ -41,7 +41,7 @@ ${appName} BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> @@ -57,7 +57,7 @@ ${appName} BlueprintIdentifier = "4F4474F426BD8F87004E6064" BuildableName = "${appName}.app" BlueprintName = "${appName}" - ReferencedContainer = "container:assignment/${appName}.xcodeproj"> + ReferencedContainer = "container:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> diff --git a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata index 70fc7ea09f74..bcc06bae07c6 100644 --- a/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata +++ b/src/main/resources/templates/swift/xcode/test/${appName}Test.xcworkspace/contents.xcworkspacedata @@ -5,6 +5,6 @@ location = "group:${appName}Test.xcodeproj"> + location = "group:${studentParentWorkingDirectoryName}/${appName}.xcodeproj"> - \ No newline at end of file + diff --git a/src/main/resources/templates/swift/xcode/test/.swiftlint.yml b/src/main/resources/templates/swift/xcode/test/.swiftlint.yml index 5c35904fd2e7..39604f106d66 100644 --- a/src/main/resources/templates/swift/xcode/test/.swiftlint.yml +++ b/src/main/resources/templates/swift/xcode/test/.swiftlint.yml @@ -292,7 +292,7 @@ only_rules: # An XCTFail call should include a description of the assertion. included: # paths to include during linting. `--path` is ignored if present. - - assignment + - ${studentParentWorkingDirectoryName} excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage diff --git a/src/main/resources/templates/swift/xcode/test/README.md b/src/main/resources/templates/swift/xcode/test/README.md index 61991fee8ef8..d0093037df8c 100644 --- a/src/main/resources/templates/swift/xcode/test/README.md +++ b/src/main/resources/templates/swift/xcode/test/README.md @@ -1,7 +1,7 @@ This is the combined repo that will be produced on the build agent by cloning two repos -1) exercise --> everything in the assignment folder -2) tests --> everything except the assignment folder +1) exercise --> everything in the ${studentParentWorkingDirectoryName} folder +2) tests --> everything except the ${studentParentWorkingDirectoryName} folder The tests can be executed as follows diff --git a/src/main/webapp/app/entities/programming/programming-exercise-build.config.ts b/src/main/webapp/app/entities/programming/programming-exercise-build.config.ts index eaa526406e08..55e5f5f487e2 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise-build.config.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise-build.config.ts @@ -5,7 +5,9 @@ export class ProgrammingExerciseBuildConfig { public buildPlanConfiguration?: string; public buildScript?: string; public checkoutSolutionRepository?: boolean; - public checkoutPath?: string; + public assignmentCheckoutPath?: string; + public testCheckoutPath?: string; + public solutionCheckoutPath?: string; public timeoutSeconds?: number; public dockerFlags?: string; public windfile?: WindFile; diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index 17d04e971160..8dde59469c03 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -114,7 +114,9 @@ export function resetProgrammingForImport(exercise: ProgrammingExercise) { export function copyBuildConfigFromExerciseJson(exerciseJson: ProgrammingExerciseBuildConfig): ProgrammingExerciseBuildConfig { const buildConfig = new ProgrammingExerciseBuildConfig(); buildConfig.sequentialTestRuns = exerciseJson.sequentialTestRuns ?? false; - buildConfig.checkoutPath = exerciseJson.checkoutPath ?? ''; + buildConfig.assignmentCheckoutPath = exerciseJson.assignmentCheckoutPath ?? ''; + buildConfig.solutionCheckoutPath = exerciseJson.solutionCheckoutPath ?? ''; + buildConfig.testCheckoutPath = exerciseJson.testCheckoutPath ?? ''; buildConfig.buildPlanConfiguration = exerciseJson.buildPlanConfiguration ?? ''; buildConfig.checkoutSolutionRepository = exerciseJson.checkoutSolutionRepository ?? false; buildConfig.timeoutSeconds = exerciseJson.timeoutSeconds ?? 0; diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index aff229febc88..8b1a5ef9529a 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -411,6 +411,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest tap((segments) => { this.isImportFromExistingExercise = segments.some((segment) => segment.path === 'import'); this.isImportFromFile = segments.some((segment) => segment.path === 'import-from-file'); + this.isEdit = segments.some((segment) => segment.path === 'edit'); }), switchMap(() => this.activatedRoute.params), tap((params) => { @@ -627,6 +628,11 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.programmingExercise.buildConfig!.buildPlanConfiguration = undefined; this.programmingExercise.buildConfig!.windfile = undefined; } + + if (this.programmingExercise.buildConfig?.timeoutSeconds && this.programmingExercise.buildConfig?.timeoutSeconds < 1) { + this.programmingExercise.buildConfig!.timeoutSeconds = 0; + } + // If the programming exercise has a submission policy with a NONE type, the policy is removed altogether if (this.programmingExercise.submissionPolicy && this.programmingExercise.submissionPolicy.type === SubmissionPolicyType.NONE) { this.programmingExercise.submissionPolicy = undefined; @@ -870,6 +876,8 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.validateExerciseBonusPoints(validationErrorReasons); this.validateExerciseSCAMaxPenalty(validationErrorReasons); this.validateExerciseSubmissionLimit(validationErrorReasons); + this.validateTimeout(validationErrorReasons); + this.validateCheckoutPaths(validationErrorReasons); return validationErrorReasons; } @@ -1081,6 +1089,39 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } } + private validateTimeout(validationErrorReasons: ValidationReason[]): void { + if (this.programmingExercise.buildConfig?.timeoutSeconds && this.programmingExercise.buildConfig.timeoutSeconds < 0) { + validationErrorReasons.push({ + translateKey: 'artemisApp.programmingExercise.timeout.alert', + translateValues: {}, + }); + } + } + + private validateCheckoutPaths(validationErrorReasons: ValidationReason[]): void { + const checkoutPaths = [ + this.programmingExercise.buildConfig?.assignmentCheckoutPath, + this.programmingExercise.buildConfig?.solutionCheckoutPath, + this.programmingExercise.buildConfig?.testCheckoutPath, + ]; + if (!this.areValuesUnique(checkoutPaths) || !this.testCheckoutPathsPattern(checkoutPaths)) { + validationErrorReasons.push({ + translateKey: 'artemisApp.programmingExercise.checkoutPath.invalid', + translateValues: {}, + }); + } + } + + private areValuesUnique(values: (string | undefined)[]): boolean { + const filteredValues = values.filter((value): value is string => value !== undefined && value !== ''); + const uniqueValues = new Set(filteredValues); + return filteredValues.length === uniqueValues.size; + } + + private testCheckoutPathsPattern(checkoutPath: (string | undefined)[]): boolean { + return checkoutPath.every((path) => path === undefined || path.trim() === '' || this.invalidDirectoryNamePattern.test(path)); + } + private createProgrammingExerciseForImportFromFile() { this.programmingExercise = cloneDeep(history.state.programmingExerciseForImportFromFile); this.programmingExercise.id = undefined; diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts index 0a2fa9061344..5185968f143f 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts @@ -27,12 +27,13 @@ import { ExerciseUpdateNotificationModule } from 'app/exercises/shared/exercise- import { ExerciseUpdatePlagiarismModule } from 'app/exercises/shared/plagiarism/exercise-update-plagiarism/exercise-update-plagiarism.module'; import { ProgrammingExerciseCustomAeolusBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component'; import { ProgrammingExerciseCustomBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component'; -import { ProgrammingExerciseDockerImageComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component'; +import { ProgrammingExerciseBuildConfigurationComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component'; import { FormsModule } from 'app/forms/forms.module'; import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; +import { ProgrammingExerciseEditCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component'; @NgModule({ imports: [ @@ -59,6 +60,7 @@ import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.co ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent, MonacoEditorComponent, ProgrammingExerciseTheiaComponent, + ProgrammingExerciseEditCheckoutDirectoriesComponent, ], declarations: [ ProgrammingExerciseUpdateComponent, @@ -66,7 +68,7 @@ import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.co ProgrammingExerciseDifficultyComponent, ProgrammingExerciseCustomAeolusBuildPlanComponent, ProgrammingExerciseCustomBuildPlanComponent, - ProgrammingExerciseDockerImageComponent, + ProgrammingExerciseBuildConfigurationComponent, ProgrammingExerciseLanguageComponent, ProgrammingExerciseGradingComponent, ProgrammingExerciseProblemComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html new file mode 100644 index 000000000000..6360bb509017 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html @@ -0,0 +1,45 @@ +
+
+ + + +
+ @if (!isAeolus()) { +
+
+ + +
+
+ +
+ + {{ timeout() }} s + +
+ } +
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts new file mode 100644 index 000000000000..a506333c9d87 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts @@ -0,0 +1,21 @@ +import { Component, input, output, viewChild } from '@angular/core'; +import { NgModel } from '@angular/forms'; + +@Component({ + selector: 'jhi-programming-exercise-build-configuration', + templateUrl: './programming-exercise-build-configuration.component.html', + styleUrls: ['../../../../programming-exercise-form.scss'], +}) +export class ProgrammingExerciseBuildConfigurationComponent { + dockerImage = input.required(); + dockerImageChange = output(); + + timeout = input(); + timeoutChange = output(); + + isAeolus = input.required(); + + dockerImageField = viewChild('dockerImageField'); + + timeoutField = viewChild('timeoutField'); +} diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.html index bcfd1a078ac7..a3d6f1b70d30 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.html @@ -7,9 +7,10 @@ @if (programmingExercise.customizeBuildPlanWithAeolus) {
@if (programmingExercise.buildConfig?.windfile && programmingExercise.buildConfig?.windfile?.metadata && programmingExercise.buildConfig?.windfile?.metadata?.docker) { - }
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.ts index e350d8c201b9..ab406d7e0193 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component.ts @@ -4,7 +4,7 @@ import { ProgrammingExercise, ProgrammingLanguage, ProjectType } from 'app/entit import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; -import { ProgrammingExerciseDockerImageComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component'; +import { ProgrammingExerciseBuildConfigurationComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; @Component({ @@ -16,7 +16,7 @@ export class ProgrammingExerciseCustomAeolusBuildPlanComponent implements OnChan @Input() programmingExercise: ProgrammingExercise; @Input() programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; - @ViewChild(ProgrammingExerciseDockerImageComponent) programmingExerciseDockerImageComponent?: ProgrammingExerciseDockerImageComponent; + @ViewChild(ProgrammingExerciseBuildConfigurationComponent) programmingExerciseDockerImageComponent?: ProgrammingExerciseBuildConfigurationComponent; programmingLanguage?: ProgrammingLanguage; projectType?: ProjectType; diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.html index 0c0e2c702af7..2993020efa28 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.html @@ -7,9 +7,12 @@ @if (programmingExercise.customizeBuildPlanWithAeolus) {
@if (programmingExercise.buildConfig?.windfile && programmingExercise.buildConfig?.windfile?.metadata && programmingExercise.buildConfig?.windfile?.metadata?.docker) { - } diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.ts index 437c22fc0f40..63b22457fe92 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component.ts @@ -3,8 +3,9 @@ import { ProgrammingExercise, ProgrammingLanguage, ProjectType } from 'app/entit import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; -import { ProgrammingExerciseDockerImageComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component'; +import { ProgrammingExerciseBuildConfigurationComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; +import { ASSIGNMENT_REPO_NAME, TEST_REPO_NAME } from 'app/shared/constants/input.constants'; @Component({ selector: 'jhi-programming-exercise-custom-build-plan', @@ -15,7 +16,7 @@ export class ProgrammingExerciseCustomBuildPlanComponent implements OnChanges { @Input() programmingExercise: ProgrammingExercise; @Input() programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; - @ViewChild(ProgrammingExerciseDockerImageComponent) programmingExerciseDockerImageComponent?: ProgrammingExerciseDockerImageComponent; + @ViewChild(ProgrammingExerciseBuildConfigurationComponent) programmingExerciseDockerImageComponent?: ProgrammingExerciseBuildConfigurationComponent; programmingLanguage?: ProgrammingLanguage; projectType?: ProjectType; @@ -60,6 +61,15 @@ export class ProgrammingExerciseCustomBuildPlanComponent implements OnChanges { ); } + shouldReplacePlaceholders(): boolean { + return ( + (!!this.programmingExercise.buildConfig?.assignmentCheckoutPath && this.programmingExercise.buildConfig?.assignmentCheckoutPath.trim() !== '') || + (!!this.programmingExercise.buildConfig?.testCheckoutPath && this.programmingExercise.buildConfig?.testCheckoutPath.trim() !== '') || + !!this.programmingExercise.buildConfig?.buildScript?.includes('${studentParentWorkingDirectoryName}') || + !!this.programmingExercise.buildConfig?.buildScript?.includes('${testWorkingDirectory}') + ); + } + /** * In case the programming language or project type changes, we need to reset the template and the build plan * @private @@ -107,6 +117,7 @@ export class ProgrammingExerciseCustomBuildPlanComponent implements OnChanges { .getAeolusTemplateScript(this.programmingLanguage, this.projectType, this.staticCodeAnalysisEnabled, this.sequentialTestRuns, this.testwiseCoverageEnabled) .subscribe({ next: (file: string) => { + file = this.replacePlaceholders(file); this.codeChanged(file); this.editor?.setText(file); }, @@ -118,6 +129,9 @@ export class ProgrammingExerciseCustomBuildPlanComponent implements OnChanges { if (!this.programmingExercise.buildConfig?.buildScript) { this.resetCustomBuildPlan(); } + if (!this.programmingExercise.buildConfig?.timeoutSeconds) { + this.programmingExercise.buildConfig!.timeoutSeconds = 0; + } } get editor(): MonacoEditorComponent | undefined { @@ -128,6 +142,7 @@ export class ProgrammingExerciseCustomBuildPlanComponent implements OnChanges { codeChanged(code: string): void { this.code = code; + this.editor?.setText(code); this.programmingExercise.buildConfig!.buildScript = code; } @@ -147,4 +162,16 @@ export class ProgrammingExerciseCustomBuildPlanComponent implements OnChanges { } this.programmingExercise.buildConfig!.windfile.metadata.docker.image = dockerImage.trim(); } + + setTimeout(timeout: number) { + this.programmingExercise.buildConfig!.timeoutSeconds = timeout; + } + + replacePlaceholders(buildScript: string): string { + const assignmentRepoName = this.programmingExercise.buildConfig?.assignmentCheckoutPath || ASSIGNMENT_REPO_NAME; + const testRepoName = this.programmingExercise.buildConfig?.testCheckoutPath || TEST_REPO_NAME; + buildScript = buildScript.replaceAll('${studentParentWorkingDirectoryName}', assignmentRepoName); + buildScript = buildScript.replaceAll('${testWorkingDirectory}', testRepoName); + return buildScript; + } } diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.html deleted file mode 100644 index 914f14e4caf5..000000000000 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.html +++ /dev/null @@ -1,16 +0,0 @@ -
- - - -
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.ts deleted file mode 100644 index 85d5f5f8d6fd..000000000000 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { NgModel } from '@angular/forms'; - -@Component({ - selector: 'jhi-programming-exercise-docker-image', - templateUrl: './programming-exercise-docker-image.component.html', - styleUrls: ['../../../../programming-exercise-form.scss'], -}) -export class ProgrammingExerciseDockerImageComponent { - @Input() dockerImage: string; - @Output() dockerImageChange = new EventEmitter(); - - @ViewChild('dockerImageField') dockerImageField?: NgModel; -} diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html index 78be8ae11ee0..acdc4cf0b5e0 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html @@ -56,7 +56,21 @@ [programmingLanguage]="programmingExerciseCreationConfig.selectedProgrammingLanguage" [isLocal]="isLocal" [checkoutSolutionRepository]="programmingExercise.buildConfig?.checkoutSolutionRepository" + (submissionBuildPlanEvent)="updateSubmissionBuildPlanCheckoutDirectories($event)" + [isCreateOrEdit]="true" + [isEditMode]="programmingExerciseCreationConfig.isEdit" + [programmingExerciseBuildConfig]="programmingExercise.buildConfig" /> + @if (editRepositoryCheckoutPath && isLocal && !isImport && !programmingExerciseCreationConfig.isEdit) { + + } @if (!programmingExerciseCreationConfig.isImportFromExistingExercise && programmingExerciseCreationConfig.auxiliaryRepositoriesSupported) {
@if (programmingExercise.auxiliaryRepositories && programmingExercise.auxiliaryRepositories.length > 0) { @@ -127,6 +141,15 @@ } + @if (isLocal && !isImport && !programmingExerciseCreationConfig.isEdit) { + + } (); @@ -35,12 +40,20 @@ export class ProgrammingExerciseInformationComponent implements AfterViewInit, O protected readonly ProjectType = ProjectType; + ButtonType = ButtonType; + ButtonSize = ButtonSize; + faPlus = faPlus; + + editRepositoryCheckoutPath: boolean = false; + submissionBuildPlanCheckoutRepositories: BuildPlanCheckoutDirectoriesDTO; + ngAfterViewInit() { this.inputFieldSubscriptions.push(this.exerciseTitleChannelComponent.titleChannelNameComponent?.formValidChanges.subscribe(() => this.calculateFormValid())); this.inputFieldSubscriptions.push(this.shortNameField.valueChanges?.subscribe(() => this.calculateFormValid())); this.inputFieldSubscriptions.push(this.checkoutSolutionRepositoryField?.valueChanges?.subscribe(() => this.calculateFormValid())); this.inputFieldSubscriptions.push(this.recreateBuildPlansField?.valueChanges?.subscribe(() => this.calculateFormValid())); this.inputFieldSubscriptions.push(this.updateTemplateFilesField?.valueChanges?.subscribe(() => this.calculateFormValid())); + this.inputFieldSubscriptions.push(this.programmingExerciseEditCheckoutDirectories?.formValidChanges.subscribe(() => this.calculateFormValid())); this.tableEditableFields?.changes.subscribe((fields: QueryList) => { fields.toArray().forEach((field) => this.inputFieldSubscriptions.push(field.editingInput.valueChanges?.subscribe(() => this.calculateFormValid()))); }); @@ -57,13 +70,15 @@ export class ProgrammingExerciseInformationComponent implements AfterViewInit, O const isRecreateBuildPlansValid = this.isRecreateBuildPlansValid(); const isUpdateTemplateFilesValid = this.isUpdateTemplateFilesValid(); const areAuxiliaryRepositoriesValid = this.areAuxiliaryRepositoriesValid(); + const areCheckoutPathsValid = this.areCheckoutPathsValid(); this.formValid = Boolean( this.exerciseTitleChannelComponent.titleChannelNameComponent?.formValid && !this.shortNameField.invalid && isCheckoutSolutionRepositoryValid && isRecreateBuildPlansValid && isUpdateTemplateFilesValid && - areAuxiliaryRepositoriesValid, + areAuxiliaryRepositoriesValid && + areCheckoutPathsValid, ); this.formValidChanges.next(this.formValid); } @@ -101,4 +116,42 @@ export class ProgrammingExerciseInformationComponent implements AfterViewInit, O !this.programmingExerciseCreationConfig.checkoutSolutionRepositoryAllowed, ); } + + areCheckoutPathsValid(): boolean { + return Boolean( + !this.programmingExerciseEditCheckoutDirectories || + (this.programmingExerciseEditCheckoutDirectories.formValid && + this.programmingExerciseEditCheckoutDirectories.areValuesUnique([ + this.programmingExercise.buildConfig?.assignmentCheckoutPath, + this.programmingExercise.buildConfig?.testCheckoutPath, + this.programmingExercise.buildConfig?.solutionCheckoutPath, + ])), + ); + } + + toggleEditRepositoryCheckoutPath() { + this.editRepositoryCheckoutPath = !this.editRepositoryCheckoutPath; + } + + updateSubmissionBuildPlanCheckoutDirectories(buildPlanCheckoutDirectoriesDTO: BuildPlanCheckoutDirectoriesDTO) { + this.submissionBuildPlanCheckoutRepositories = buildPlanCheckoutDirectoriesDTO; + } + + onAssigmentRepositoryCheckoutPathChange(event: string) { + this.programmingExercise.buildConfig!.assignmentCheckoutPath = event; + // We need to create a new object to trigger the change detection + this.programmingExercise.buildConfig = { ...this.programmingExercise.buildConfig }; + } + + onTestRepositoryCheckoutPathChange(event: string) { + this.programmingExercise.buildConfig!.testCheckoutPath = event; + // We need to create a new object to trigger the change detection + this.programmingExercise.buildConfig = { ...this.programmingExercise.buildConfig }; + } + + onSolutionRepositoryCheckoutPathChange(event: string) { + this.programmingExercise.buildConfig!.solutionCheckoutPath = event; + // We need to create a new object to trigger the change detection + this.programmingExercise.buildConfig = { ...this.programmingExercise.buildConfig }; + } } diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts index 26951968a7b2..7514543ec6e9 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts @@ -52,11 +52,18 @@ export class ProgrammingExerciseLanguageComponent implements AfterViewChecked, A } const dockerImageField = - this.programmingExerciseCustomBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField ?? - this.programmingExerciseCustomAeolusBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField; + this.programmingExerciseCustomBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField() ?? + this.programmingExerciseCustomAeolusBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField(); if (!(dockerImageField?.valueChanges as EventEmitter)?.observed) { this.fieldSubscriptions.push(dockerImageField?.valueChanges?.subscribe(() => this.calculateFormValid())); } + + const timeoutField = + this.programmingExerciseCustomBuildPlanComponent?.programmingExerciseDockerImageComponent?.timeoutField() ?? + this.programmingExerciseCustomAeolusBuildPlanComponent?.programmingExerciseDockerImageComponent?.timeoutField(); + if (!(timeoutField?.valueChanges as EventEmitter)?.observed) { + this.fieldSubscriptions.push(timeoutField?.valueChanges?.subscribe(() => this.calculateFormValid())); + } } ngOnDestroy() { @@ -88,11 +95,17 @@ export class ProgrammingExerciseLanguageComponent implements AfterViewChecked, A } if (this.programmingExerciseCreationConfig.customBuildPlansSupported === PROFILE_LOCALCI) { - return this.programmingExerciseCustomBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField?.valid ?? false; + return ( + (this.programmingExerciseCustomBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField()?.valid ?? false) && + (this.programmingExerciseCustomBuildPlanComponent?.programmingExerciseDockerImageComponent?.timeoutField()?.valid ?? false) + ); } if (this.programmingExerciseCreationConfig.customBuildPlansSupported === PROFILE_AEOLUS) { - return this.programmingExerciseCustomAeolusBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField?.valid ?? false; + return ( + (this.programmingExerciseCustomAeolusBuildPlanComponent?.programmingExerciseDockerImageComponent?.dockerImageField()?.valid ?? false) && + (this.programmingExerciseCustomAeolusBuildPlanComponent?.programmingExerciseDockerImageComponent?.timeoutField()?.valid ?? false) + ); } return true; diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.html b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.html new file mode 100644 index 000000000000..ac5f11f6362d --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.html @@ -0,0 +1,58 @@ +
+ @if (isAssigmentRepositoryEditable) { + + + + } + @if (isTestRepositoryEditable) { + + + + } + @if (isSolutionRepositoryEditable) { + + + + } + @if (!formValid) { + + } + + +
diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.ts b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.ts new file mode 100644 index 000000000000..460a48c84aea --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component.ts @@ -0,0 +1,113 @@ +import { Component, effect, input, output, viewChild } from '@angular/core'; +import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/programming/build-plan-checkout-directories-dto'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { Subject } from 'rxjs'; +import { NgModel } from '@angular/forms'; + +@Component({ + selector: 'jhi-programming-exercise-edit-checkout-directories', + standalone: true, + imports: [ArtemisSharedComponentModule, ArtemisSharedCommonModule], + templateUrl: './programming-exercise-edit-checkout-directories.component.html', + styleUrls: ['../../../manage/programming-exercise-form.scss'], +}) +export class ProgrammingExerciseEditCheckoutDirectoriesComponent { + programmingExercise = input.required(); + pattern = input.required(); + submissionBuildPlanCheckoutRepositories = input.required(); + assignmentCheckoutPathEvent = output(); + testCheckoutPathEvent = output(); + solutionCheckoutPathEvent = output(); + + assignmentCheckoutPath: string; + testCheckoutPath: string; + solutionCheckoutPath: string; + + isAssigmentRepositoryEditable: boolean = false; + isTestRepositoryEditable: boolean = false; + isSolutionRepositoryEditable: boolean = false; + + formValid: boolean = true; + formValidChanges = new Subject(); + + field_assignmentRepositoryCheckoutPath = viewChild('field_assignmentRepositoryCheckoutPath'); + field_testRepositoryCheckoutPath = viewChild('field_testRepositoryCheckoutPath'); + field_solutionRepositoryCheckoutPath = viewChild('field_solutionRepositoryCheckoutPath'); + + constructor() { + effect(() => { + this.reset(); + }); + } + + reset() { + const submissionBuildPlan = this.submissionBuildPlanCheckoutRepositories(); + this.isAssigmentRepositoryEditable = + !!submissionBuildPlan?.exerciseCheckoutDirectory && + submissionBuildPlan?.exerciseCheckoutDirectory.trim() !== '' && + submissionBuildPlan?.exerciseCheckoutDirectory !== '/'; + if (this.isAssigmentRepositoryEditable) { + this.assignmentCheckoutPath = + this.programmingExercise().buildConfig?.assignmentCheckoutPath || this.removeLeadingSlash(submissionBuildPlan?.exerciseCheckoutDirectory) || ''; + } else { + this.assignmentCheckoutPath = ''; + } + this.isTestRepositoryEditable = + !!submissionBuildPlan?.testCheckoutDirectory && submissionBuildPlan?.testCheckoutDirectory.trim() !== '' && submissionBuildPlan?.testCheckoutDirectory !== '/'; + if (this.isTestRepositoryEditable) { + this.testCheckoutPath = this.programmingExercise().buildConfig?.testCheckoutPath || this.removeLeadingSlash(submissionBuildPlan?.testCheckoutDirectory) || ''; + } else { + this.testCheckoutPath = '/'; + } + this.isSolutionRepositoryEditable = + !!submissionBuildPlan?.solutionCheckoutDirectory && + submissionBuildPlan?.solutionCheckoutDirectory.trim() !== '' && + submissionBuildPlan?.solutionCheckoutDirectory !== '/'; + if (this.isSolutionRepositoryEditable) { + this.solutionCheckoutPath = + this.programmingExercise().buildConfig?.solutionCheckoutPath || this.removeLeadingSlash(submissionBuildPlan?.solutionCheckoutDirectory) || ''; + } else { + this.solutionCheckoutPath = ''; + } + } + + onAssigmentRepositoryCheckoutPathChange(event: string) { + this.assignmentCheckoutPath = event; + this.assignmentCheckoutPathEvent.emit(this.assignmentCheckoutPath); + this.calculateFormValid(); + } + + onTestRepositoryCheckoutPathChange(event: string) { + this.testCheckoutPath = event; + this.testCheckoutPathEvent.emit(this.testCheckoutPath); + this.calculateFormValid(); + } + + onSolutionRepositoryCheckoutPathChange(event: string) { + this.solutionCheckoutPath = event; + this.solutionCheckoutPathEvent.emit(this.solutionCheckoutPath); + this.calculateFormValid(); + } + + private removeLeadingSlash(path?: string): string | undefined { + return path?.replace(/^\//, ''); + } + + calculateFormValid(): void { + const isFormValid = Boolean( + (!this.field_assignmentRepositoryCheckoutPath() || this.field_assignmentRepositoryCheckoutPath()?.valid) && + (!this.field_testRepositoryCheckoutPath() || this.field_testRepositoryCheckoutPath()?.valid) && + (!this.field_solutionRepositoryCheckoutPath() || this.field_solutionRepositoryCheckoutPath()?.valid), + ); + this.formValid = isFormValid && this.areValuesUnique([this.assignmentCheckoutPath, this.testCheckoutPath, this.solutionCheckoutPath]); + this.formValidChanges.next(this.formValid); + } + + areValuesUnique(values: (string | undefined)[]): boolean { + const filteredValues = values.filter((value): value is string => value !== undefined && value.trim() !== ''); + const uniqueValues = new Set(filteredValues); + return filteredValues.length === uniqueValues.size; + } +} diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts index f44d22e06ff7..ae0a3a091ca6 100644 --- a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts @@ -1,12 +1,14 @@ -import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { getCourseFromExercise } from 'app/entities/exercise.model'; import type { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; +import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { Subscription } from 'rxjs'; import type { CheckoutDirectoriesDto } from 'app/entities/programming/checkout-directories-dto'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; +import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/programming/build-plan-checkout-directories-dto'; @Component({ selector: 'jhi-programming-exercise-repository-and-build-plan-details', @@ -17,9 +19,13 @@ import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/ex }) export class ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent implements OnInit, OnChanges, OnDestroy { @Input() programmingExercise: ProgrammingExercise; + @Input() programmingExerciseBuildConfig?: ProgrammingExerciseBuildConfig; @Input() programmingLanguage?: ProgrammingLanguage; @Input() isLocal: boolean; @Input() checkoutSolutionRepository?: boolean = true; + @Input() isCreateOrEdit = false; + @Input() isEditMode = false; + @Output() submissionBuildPlanEvent = new EventEmitter(); constructor(private programmingExerciseService: ProgrammingExerciseService) {} @@ -40,8 +46,16 @@ export class ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent implement const isProgrammingLanguageUpdated = changes.programmingLanguage?.currentValue !== changes.programmingLanguage?.previousValue; const isCheckoutSolutionRepositoryUpdated = changes.checkoutSolutionRepository?.currentValue !== changes.checkoutSolutionRepository?.previousValue; if (this.isLocal && (isProgrammingLanguageUpdated || isCheckoutSolutionRepositoryUpdated)) { + if (this.isCreateOrEdit && !this.isEditMode) { + this.resetProgrammingExerciseBuildCheckoutPaths(); + } this.updateCheckoutDirectories(); } + + const isBuildConfigChanged = this.isBuildConfigAvailable(this.programmingExercise.buildConfig); + if (this.isLocal && this.isCreateOrEdit && isBuildConfigChanged) { + this.checkoutDirectories = this.setCheckoutDirectoriesFromBuildConfig(this.checkoutDirectories); + } } ngOnDestroy() { @@ -59,14 +73,70 @@ export class ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent implement this.checkoutDirectorySubscription = this.programmingExerciseService .getCheckoutDirectoriesForProgrammingLanguage(this.programmingLanguage, this.checkoutSolutionRepository ?? CHECKOUT_SOLUTION_REPOSITORY_DEFAULT) .subscribe((checkoutDirectories) => { - this.checkoutDirectories = checkoutDirectories; + if ((this.isCreateOrEdit && !this.isEditMode) || !this.isBuildConfigAvailable(this.programmingExercise.buildConfig)) { + this.checkoutDirectories = checkoutDirectories; + this.submissionBuildPlanEvent.emit(checkoutDirectories.submissionBuildPlanCheckoutDirectories!); + } else { + this.checkoutDirectories = this.setCheckoutDirectoriesFromBuildConfig(checkoutDirectories); + } }); } + private setCheckoutDirectoriesFromBuildConfig(checkoutDirectories?: CheckoutDirectoriesDto): CheckoutDirectoriesDto | undefined { + if (this.programmingExercise.buildConfig || checkoutDirectories) { + checkoutDirectories = { + solutionBuildPlanCheckoutDirectories: { + solutionCheckoutDirectory: + this.addLeadingSlash(this.programmingExercise.buildConfig?.assignmentCheckoutPath) || + checkoutDirectories?.solutionBuildPlanCheckoutDirectories?.solutionCheckoutDirectory, + testCheckoutDirectory: + this.addLeadingSlash(this.programmingExercise.buildConfig?.testCheckoutPath) || + checkoutDirectories?.solutionBuildPlanCheckoutDirectories?.testCheckoutDirectory || + '/', + }, + submissionBuildPlanCheckoutDirectories: { + exerciseCheckoutDirectory: + this.addLeadingSlash(this.programmingExercise.buildConfig?.assignmentCheckoutPath) || + checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.exerciseCheckoutDirectory, + solutionCheckoutDirectory: + this.addLeadingSlash(this.programmingExercise.buildConfig?.solutionCheckoutPath) || + checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.solutionCheckoutDirectory, + testCheckoutDirectory: + this.addLeadingSlash(this.programmingExercise.buildConfig?.testCheckoutPath) || + checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.testCheckoutDirectory || + '/', + }, + }; + } + return checkoutDirectories; + } + private updateCourseShortName() { if (!this.programmingExercise) { return; } this.courseShortName = getCourseFromExercise(this.programmingExercise)?.shortName; } + + private addLeadingSlash(path?: string): string | undefined { + if (!path) { + return undefined; + } + return path.startsWith('/') ? path : `/${path}`; + } + + private isBuildConfigAvailable(buildConfig?: ProgrammingExerciseBuildConfig): boolean { + return ( + buildConfig !== undefined && + ((buildConfig.assignmentCheckoutPath !== undefined && buildConfig.assignmentCheckoutPath.trim() !== '') || + (buildConfig.testCheckoutPath !== undefined && buildConfig.testCheckoutPath.trim() !== '') || + (buildConfig.solutionCheckoutPath !== undefined && buildConfig.solutionCheckoutPath.trim() !== '')) + ); + } + + private resetProgrammingExerciseBuildCheckoutPaths() { + this.programmingExercise.buildConfig!.assignmentCheckoutPath = undefined; + this.programmingExercise.buildConfig!.testCheckoutPath = undefined; + this.programmingExercise.buildConfig!.solutionCheckoutPath = undefined; + } } diff --git a/src/main/webapp/app/shared/constants/input.constants.ts b/src/main/webapp/app/shared/constants/input.constants.ts index 8b97589a59d6..1b1045b7f56f 100644 --- a/src/main/webapp/app/shared/constants/input.constants.ts +++ b/src/main/webapp/app/shared/constants/input.constants.ts @@ -22,3 +22,5 @@ export const MAX_QUIZ_QUESTION_POINTS = 9999; export const MAX_QUIZ_QUESTION_LENGTH_THRESHOLD = 250; export const MAX_QUIZ_QUESTION_EXPLANATION_LENGTH_THRESHOLD = 500; export const MAX_QUIZ_QUESTION_HINT_LENGTH_THRESHOLD = 255; +export const ASSIGNMENT_REPO_NAME = 'assignment'; +export const TEST_REPO_NAME = 'tests'; diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index b06ff3845c39..d1bf21030478 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -115,6 +115,23 @@ "customizeDockerImage": "Du kannst das Docker-Image anpassen. Stelle sicher, dass du es in amd64 und arm64 bereitstellst und alle Build Abhängigkeiten enthalten sind, um eine kurze Build Dauer zu gewährleisten.", "parameters": "Parameter", "dockerImage": "Docker Image", + "checkoutPath": { + "title": "Checkout-Pfad", + "customize": "Sie können den Checkout-Pfad anpassen, in dem die Repositories im Docker-Container geklont werden. Stellen Sie sicher, dass Sie einen gültigen absoluten UNIX-Pfad angeben. Der Standardpfad ist /var/tmp.", + "alert": "Checkout-Pfade dürfen nur Wörter, '_', '-', und '/' enthalten, dürfen jedoch nicht mit einem '/' beginnen oder enden. Die Pfade müssen für jedes Repository unterschiedlich sein.", + "edit": "Checkout-Pfad der Repositories bearbeiten", + "assignment": "Checkout-Pfad für das Aufgaben-Repository. Das kann nach der Erstellung nicht bearbeitet werden.", + "solution": "Checkout-Pfad für das Lösungs-Repository. Das kann nach der Erstellung nicht bearbeitet werden.", + "tests": "Checkout-Pfad für das Tests-Repository. Das kann nach der Erstellung nicht bearbeitet werden.", + "invalid": "Ungültige Checkout-Pfade", + "warning": "Das Ändern der Checkout-Pfade kann zu Build-Fehlern führen. Einige Programmiersprachen verlassen sich auf Docker-Images, die bestimmte Verzeichnisstrukturen für den Code voraussetzen.", + "warningBuildScript": "Stellen Sie sicher, dass Sie das Build-Skript und die Repositories entsprechend anpassen." + }, + "timeout": { + "title": "Maximale Build-Dauer in Sekunden ", + "customize": "Setze den Wert auf 0, um den Standardwert zu verwenden. Stelle sicher, dass du einen gültigen ganzzahligen Wert angibst. Die Einheit ist Sekunden. Beachte, dass dieser Wert durch das von den Administratoren festgelegte maximale Limit (Standard ist 240s) überschrieben werden kann.", + "alert": "Das Timeout muss ein gültiger ganzzahliger Wert sein." + }, "workdir": "Verzeichnis", "allowOnlineEditor": { "title": "Online-Editor erlauben", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 0362e88038db..9e88733b77e6 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -128,6 +128,23 @@ "dockerImage": "Docker Image", "script": "Build script", "customizeDockerImage": "You can customize the Docker image. Make sure to provide it in amd64 and arm64 and include all build dependencies to guarantee a short build duration.", + "checkoutPath": { + "title": "Checkout Path", + "customize": "You can customize the checkout path where the repositories are cloned inside the Docker container. Make sure to provide a valid absolute UNIX path. The default path is /var/tmp.", + "alert": "Checkout paths must only contain words, '_', '-', and '/' characters, but must not start nor end with a '/' character. The paths must be different for each repository.", + "edit": "Edit repositories checkout path", + "assignment": "Assignment Repository Checkout Path. This can't be edited after creation.", + "solution": "Solution Repository Checkout Path. This can't be edited after creation.", + "tests": "Tests Repository Checkout Path. This can't be edited after creation.", + "invalid": "Invalid checkout paths", + "warning": "Modifying checkout paths may result in build failures. Some programming languages rely on Docker images that assume specific directory structures for the code.", + "warningBuildScript": "Make sure to adjust the build script and repositories accordingly." + }, + "timeout": { + "title": "Maximum build duration in seconds", + "customize": "Set to 0 to use default value. Make sure to provide a valid integer value. The unit is seconds. Note that this value may be overridden by the maximum limit set by the administrators (default is 240s).", + "alert": "The timeout must be a valid integer value." + }, "allowOnlineEditor": { "title": "Allow Online Editor", "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", diff --git a/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java index 1da83f9c87c3..b4012d1f7a6f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java @@ -91,7 +91,7 @@ void testPullDockerImage() { InspectImageCmd inspectImageCmd = mock(InspectImageCmd.class); doReturn(inspectImageCmd).when(dockerClient).inspectImageCmd(anyString()); doThrow(new NotFoundException("")).when(inspectImageCmd).exec(); - BuildConfig buildConfig = new BuildConfig("echo 'test'", "test-image-name", "test", "test", "test", "test", null, null, false, false, false, null); + BuildConfig buildConfig = new BuildConfig("echo 'test'", "test-image-name", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); var build = new BuildJobQueueItem("1", "job1", "address1", 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null); // Pull image try { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java index 46fcf73bdfb3..7d17a712b4ec 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java @@ -490,4 +490,16 @@ private void verifyUserNotification(ProgrammingExerciseStudentParticipation part return true; }), Mockito.eq(participation))); } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testCustomCheckoutPaths() { + var buildConfig = programmingExercise.getBuildConfig(); + buildConfig.setAssignmentCheckoutPath("customAssignmentPath"); + ProgrammingExerciseStudentParticipation participation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); + programmingExerciseBuildConfigRepository.save(programmingExercise.getBuildConfig()); + + localVCServletService.processNewPush(commitHash, studentAssignmentRepository.originGit.getRepository()); + localVCLocalCITestService.testLatestSubmission(participation.getId(), commitHash, 1, false); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java index 83f9d9019f27..3b7ced720320 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java @@ -91,7 +91,7 @@ void createJobs() { JobTimingInfo jobTimingInfo2 = new JobTimingInfo(ZonedDateTime.now(), ZonedDateTime.now().plusMinutes(1), ZonedDateTime.now().plusMinutes(2)); JobTimingInfo jobTimingInfo3 = new JobTimingInfo(ZonedDateTime.now().minusMinutes(10), ZonedDateTime.now().minusMinutes(9), ZonedDateTime.now().plusSeconds(150)); - BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null); + BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); job1 = new BuildJobQueueItem("1", "job1", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); @@ -283,7 +283,7 @@ void testGetFinishedBuildJobs_returnsFilteredJobs() throws Exception { // Create a failed job to filter for JobTimingInfo jobTimingInfo = new JobTimingInfo(ZonedDateTime.now().plusDays(1), ZonedDateTime.now().plusDays(1).plusMinutes(2), ZonedDateTime.now().plusDays(1).plusMinutes(10)); - BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null); + BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); var failedJob1 = new BuildJobQueueItem("5", "job5", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo, buildConfig, null); var jobResult = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java index e61612cbb9bc..040aef91d87e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java @@ -101,7 +101,7 @@ void testReturnCorrectBuildStatus() { ProgrammingExerciseStudentParticipation participation = participationUtilService.addStudentParticipationForProgrammingExercise(exercise, TEST_PREFIX + "student1"); JobTimingInfo jobTimingInfo = new JobTimingInfo(ZonedDateTime.now(), ZonedDateTime.now().plusMinutes(1), ZonedDateTime.now().plusMinutes(2)); - BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null); + BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); BuildJobQueueItem job1 = new BuildJobQueueItem("1", "job1", "address1", participation.getId(), course.getId(), 1, 1, 1, diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index e54b25a55283..f5ec144c6bf0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -201,6 +201,19 @@ void testCreateProgrammingExercise() throws Exception { verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(createdExercise)); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateProgrammingExercise_Invalid_CheckoutPaths() throws Exception { + // The mock commit hashes don't allow the getPushDate() method in the LocalVCService to retrieve the push date using the commit hash. Thus, this method must be mocked. + doReturn(ZonedDateTime.now().minusSeconds(2)).when(versionControlService).getPushDate(any(), any(), any()); + + ProgrammingExercise newExercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); + newExercise.setProjectType(ProjectType.PLAIN_GRADLE); + newExercise.getBuildConfig().setAssignmentCheckoutPath("/invalid/assignment"); + + request.postWithResponseBody("/api/programming-exercises/setup", newExercise, ProgrammingExercise.class, HttpStatus.BAD_REQUEST); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testUpdateProgrammingExercise() throws Exception { diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-docker-image.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts similarity index 54% rename from src/test/javascript/spec/component/programming-exercise/programming-exercise-docker-image.component.spec.ts rename to src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts index 8f6987901422..978f821fb406 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-docker-image.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts @@ -1,34 +1,39 @@ import { TestBed } from '@angular/core/testing'; import { ArtemisTestModule } from '../../test.module'; -import { ProgrammingExerciseDockerImageComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component'; +import { ProgrammingExerciseBuildConfigurationComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component'; import { FormsModule } from '@angular/forms'; import { ArtemisProgrammingExerciseUpdateModule } from 'app/exercises/programming/manage/update/programming-exercise-update.module'; describe('ProgrammingExercise Docker Image', () => { - let comp: ProgrammingExerciseDockerImageComponent; + let comp: ProgrammingExerciseBuildConfigurationComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, FormsModule, ArtemisProgrammingExerciseUpdateModule], - declarations: [ProgrammingExerciseDockerImageComponent], + declarations: [ProgrammingExerciseBuildConfigurationComponent], providers: [], }) .compileComponents() .then(); - const fixture = TestBed.createComponent(ProgrammingExerciseDockerImageComponent); + const fixture = TestBed.createComponent(ProgrammingExerciseBuildConfigurationComponent); comp = fixture.componentInstance; - comp.dockerImage = 'testImage'; + fixture.componentRef.setInput('dockerImage', 'testImage'); + fixture.componentRef.setInput('timeout', 10); }); afterEach(() => { jest.restoreAllMocks(); }); - it('should update docker image', () => { - expect(comp.dockerImage).toBe('testImage'); + it('should update build values', () => { + expect(comp.dockerImage()).toBe('testImage'); comp.dockerImageChange.subscribe((value) => expect(value).toBe('newImage')); comp.dockerImageChange.emit('newImage'); + + expect(comp.timeout()).toBe(10); + comp.timeoutChange.subscribe((value) => expect(value).toBe(20)); + comp.timeoutChange.emit(20); }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-custom-build-plan.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-custom-build-plan.component.spec.ts index 7637950ee528..a02862b32a59 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-custom-build-plan.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-custom-build-plan.component.spec.ts @@ -169,7 +169,7 @@ describe('ProgrammingExercise Custom Build Plan', () => { expect(loadAeolusTemplateSpy).toHaveBeenCalled(); }); - it('should update windfile', () => { + it('should update programming exercise values', () => { comp.programmingExercise.buildConfig!.windfile = undefined; programmingExerciseCreationConfigMock.customBuildPlansSupported = PROFILE_LOCALCI; comp.programmingExerciseCreationConfig = programmingExerciseCreationConfigMock; @@ -177,6 +177,7 @@ describe('ProgrammingExercise Custom Build Plan', () => { jest.spyOn(mockAeolusService, 'getAeolusTemplateScript').mockReturnValue(new Observable((subscriber) => subscriber.next("echo 'test'"))); comp.loadAeolusTemplate(); expect(comp.programmingExercise.buildConfig?.windfile).toBeDefined(); + expect(comp.programmingExercise.buildConfig?.timeoutSeconds).toBe(0); }); it('should call this.resetCustomBuildPlan', () => { @@ -264,4 +265,10 @@ describe('ProgrammingExercise Custom Build Plan', () => { expect(mockAeolusService.getAeolusTemplateFile).not.toHaveBeenCalled(); expect(comp.programmingExercise.buildConfig?.buildScript).toBe('echo "test"'); }); + + it('should set timeout correctly', () => { + comp.programmingExercise.buildConfig!.timeoutSeconds = 100; + comp.setTimeout(10); + expect(comp.programmingExercise.buildConfig?.timeoutSeconds).toBe(10); + }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-edit-checkout-directories.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-edit-checkout-directories.component.spec.ts new file mode 100644 index 000000000000..5702048b3e80 --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-edit-checkout-directories.component.spec.ts @@ -0,0 +1,147 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProgrammingExerciseEditCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { HelpIconComponent } from 'app/shared/components/help-icon.component'; +import { MockComponent } from 'ng-mocks'; +import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; +import { BuildAction, PlatformAction, ScriptAction } from 'app/entities/programming/build.action'; +import { WindFile } from 'app/entities/programming/wind.file'; +import { WindMetadata } from 'app/entities/programming/wind.metadata'; +import { DockerConfiguration } from 'app/entities/programming/docker.configuration'; +import { Course } from 'app/entities/course.model'; +import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/programming/build-plan-checkout-directories-dto'; + +describe('ProgrammingExerciseEditCheckoutDirectoriesComponent', () => { + let component: ProgrammingExerciseEditCheckoutDirectoriesComponent; + let fixture: ComponentFixture; + const course = { id: 123 } as Course; + + let programmingExercise = new ProgrammingExercise(course, undefined); + let windFile: WindFile = new WindFile(); + let actions: BuildAction[] = []; + let gradleBuildAction: ScriptAction = new ScriptAction(); + let cleanBuildAction: ScriptAction = new ScriptAction(); + let platformAction: PlatformAction = new PlatformAction(); + + const submissionBuildPlanCheckoutRepositories: BuildPlanCheckoutDirectoriesDTO = { + exerciseCheckoutDirectory: '/assignment', + solutionCheckoutDirectory: '/solution', + testCheckoutDirectory: '/tests', + }; + + beforeEach(async () => { + programmingExercise = new ProgrammingExercise(course, undefined); + programmingExercise.customizeBuildPlanWithAeolus = true; + windFile = new WindFile(); + const metadata = new WindMetadata(); + metadata.docker = new DockerConfiguration(); + metadata.docker.image = 'testImage'; + windFile.metadata = metadata; + actions = []; + gradleBuildAction = new ScriptAction(); + gradleBuildAction.name = 'gradle'; + gradleBuildAction.script = './gradlew clean test'; + platformAction = new PlatformAction(); + platformAction.name = 'platform'; + platformAction.kind = 'junit'; + cleanBuildAction = new ScriptAction(); + cleanBuildAction.name = 'clean'; + cleanBuildAction.script = `chmod -R 777 .`; + actions.push(gradleBuildAction); + actions.push(cleanBuildAction); + actions.push(platformAction); + windFile.actions = actions; + programmingExercise.buildConfig!.windfile = windFile; + + await TestBed.configureTestingModule({ + imports: [ArtemisSharedComponentModule, ArtemisSharedCommonModule], + declarations: [ProgrammingExerciseEditCheckoutDirectoriesComponent, MockComponent(HelpIconComponent)], + providers: [{ provide: TranslateService, useClass: MockTranslateService }], + }).compileComponents(); + + fixture = TestBed.createComponent(ProgrammingExerciseEditCheckoutDirectoriesComponent); + component = fixture.componentInstance; + + fixture.componentRef.setInput('programmingExercise', programmingExercise); + fixture.componentRef.setInput('submissionBuildPlanCheckoutRepositories', { + testCheckoutDirectory: '/', + }); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('reset should set editable and input fields correctly', () => { + fixture.componentRef.setInput('submissionBuildPlanCheckoutRepositories', submissionBuildPlanCheckoutRepositories); + component.reset(); + expect(component.isAssigmentRepositoryEditable).toBeTrue(); + expect(component.assignmentCheckoutPath).toBe('assignment'); + expect(component.isTestRepositoryEditable).toBeTrue(); + expect(component.testCheckoutPath).toBe('tests'); + expect(component.isSolutionRepositoryEditable).toBeTrue(); + expect(component.solutionCheckoutPath).toBe('solution'); + + fixture.componentRef.setInput('submissionBuildPlanCheckoutRepositories', { + testCheckoutDirectory: '/', + }); + component.reset(); + expect(component.isAssigmentRepositoryEditable).toBeFalse(); + expect(component.assignmentCheckoutPath).toBe(''); + expect(component.isTestRepositoryEditable).toBeFalse(); + expect(component.testCheckoutPath).toBe('/'); + expect(component.isSolutionRepositoryEditable).toBeFalse(); + expect(component.solutionCheckoutPath).toBe(''); + }); + + it('should update fields correctly', () => { + component.onAssigmentRepositoryCheckoutPathChange('assignment'); + expect(component.assignmentCheckoutPath).toBe('assignment'); + expect(component.formValid).toBeTrue(); + component.onTestRepositoryCheckoutPathChange('tests'); + expect(component.formValid).toBeTrue(); + expect(component.testCheckoutPath).toBe('tests'); + component.onSolutionRepositoryCheckoutPathChange('solution'); + expect(component.formValid).toBeTrue(); + expect(component.solutionCheckoutPath).toBe('solution'); + + component.onAssigmentRepositoryCheckoutPathChange('solution'); + expect(component.formValid).toBeFalse(); + + component.calculateFormValid(); + }); + + it('should correctly check if values are unique', () => { + let stringArray: (string | undefined)[] = ['a', 'b', 'c']; + expect(component.areValuesUnique(stringArray)).toBeTrue(); + + stringArray = ['a', 'b', 'a']; + expect(component.areValuesUnique(stringArray)).toBeFalse(); + + stringArray = ['a', 'b', undefined]; + expect(component.areValuesUnique(stringArray)).toBeTrue(); + }); + + it('should should reset values correctly when buildconfig is null', () => { + fixture.componentRef.setInput('programmingExercise', new ProgrammingExercise(course, undefined)); + fixture.componentRef.setInput('submissionBuildPlanCheckoutRepositories', submissionBuildPlanCheckoutRepositories); + component.reset(); + + expect(component.assignmentCheckoutPath).toBe('assignment'); + expect(component.testCheckoutPath).toBe('tests'); + expect(component.solutionCheckoutPath).toBe('solution'); + }); + + it('should set values to their defaults if no buildConfig of submissionBuildPlan available', () => { + fixture.componentRef.setInput('programmingExercise', new ProgrammingExercise(course, undefined)); + fixture.componentRef.setInput('submissionBuildPlanCheckoutRepositories', undefined); + component.reset(); + + expect(component.assignmentCheckoutPath).toBe(''); + expect(component.testCheckoutPath).toBe('/'); + expect(component.solutionCheckoutPath).toBe(''); + }); +}); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts index aa7f5e3e80bb..582b93b71df9 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { CheckoutDirectoriesDto } from 'app/entities/programming/checkout-directories-dto'; import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; +import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; import { HelpIconComponent } from 'app/shared/components/help-icon.component'; import { MockComponent } from 'ng-mocks'; import { Subscription, of } from 'rxjs'; @@ -73,7 +74,7 @@ describe('ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent', () => { programmingExerciseService = TestBed.inject(ProgrammingExerciseService); component.programmingLanguage = ProgrammingLanguage.C; - component.programmingExercise = { id: 1, shortName: 'shortName' } as ProgrammingExercise; + component.programmingExercise = { id: 1, shortName: 'shortName', buildConfig: new ProgrammingExerciseBuildConfig() } as ProgrammingExercise; component.isLocal = true; jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage').mockImplementation( @@ -177,6 +178,11 @@ describe('ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent', () => { // assertion to check if ngOnChanges was executed properly and updated the checkout directories expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).toHaveBeenCalledWith(ProgrammingLanguage.OCAML, true); expect(component.checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.solutionCheckoutDirectory).toBe('/solution'); // was null before with JAVA as programming language + + // should also reset build config + expect(component.programmingExercise?.buildConfig?.solutionCheckoutPath).toBeUndefined(); + expect(component.programmingExercise?.buildConfig?.testCheckoutPath).toBeUndefined(); + expect(component.programmingExercise?.buildConfig?.assignmentCheckoutPath).toBeUndefined(); }); it('should update checkout directories when checkoutSolution flag changes', () => { @@ -195,6 +201,58 @@ describe('ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent', () => { expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).toHaveBeenCalledWith(ProgrammingLanguage.OCAML, false); // solution checkout directory was /solution before with OCaml as programming language and solution checkout allowed expect(component.checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.solutionCheckoutDirectory).toBeUndefined(); + + // should also reset build config + expect(component.programmingExercise?.buildConfig?.solutionCheckoutPath).toBeUndefined(); + expect(component.programmingExercise?.buildConfig?.testCheckoutPath).toBeUndefined(); + expect(component.programmingExercise?.buildConfig?.assignmentCheckoutPath).toBeUndefined(); + }); + + it('should not call service without language', () => { + jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage'); + + component.programmingLanguage = undefined; + component.checkoutSolutionRepository = false; + component.ngOnChanges({ + checkoutSolutionRepository: { + previousValue: true, + currentValue: false, + }, + } as unknown as SimpleChanges); + + // assertion to check if ngOnChanges was executed properly and updated the checkout directories + expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).not.toHaveBeenCalledWith(ProgrammingLanguage.OCAML, false); + }); + + it('should not call service when inEdit and build config is available', () => { + jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage'); + + component.isCreateOrEdit = true; + component.programmingExercise!.buildConfig = new ProgrammingExerciseBuildConfig(); + component.programmingExercise!.buildConfig.solutionCheckoutPath = 'solution'; + component.programmingExercise!.buildConfig.testCheckoutPath = 'tests'; + component.programmingExercise!.buildConfig.assignmentCheckoutPath = 'assignment'; + + component.ngOnChanges({ + checkoutSolutionRepository: { + previousValue: true, + currentValue: true, + }, + } as unknown as SimpleChanges); + + // assertion to check if ngOnChanges was executed properly and updated the checkout directories + expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).not.toHaveBeenCalledWith(ProgrammingLanguage.OCAML, false); + expect(component.checkoutDirectories).toEqual({ + submissionBuildPlanCheckoutDirectories: { + exerciseCheckoutDirectory: '/assignment', + testCheckoutDirectory: '/tests', + solutionCheckoutDirectory: '/solution', + }, + solutionBuildPlanCheckoutDirectories: { + solutionCheckoutDirectory: '/assignment', + testCheckoutDirectory: '/tests', + }, + }); }); it('should update auxiliary repository directories on changes', () => { @@ -214,4 +272,27 @@ describe('ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent', () => { expect(submissionPreviewElement).toBeTruthy(); expect(submissionPreviewElement.textContent).toContain('/assignment/src'); }); + + it('should update component when buildconfig was changed', () => { + component.isCreateOrEdit = true; + component.programmingExercise!.buildConfig = new ProgrammingExerciseBuildConfig(); + component.programmingExercise!.buildConfig.solutionCheckoutPath = 'solution'; + + component.ngOnChanges({ + programmingExercise: { + previousValue: { buildConfig: new ProgrammingExerciseBuildConfig() }, + currentValue: { buildConfig: component.programmingExercise!.buildConfig }, + }, + } as unknown as SimpleChanges); + + expect(component.checkoutDirectories).toEqual({ + solutionBuildPlanCheckoutDirectories: { + testCheckoutDirectory: '/', + }, + submissionBuildPlanCheckoutDirectories: { + solutionCheckoutDirectory: '/solution', + testCheckoutDirectory: '/', + }, + }); + }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts index adc5d04cd682..561469eb9381 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts @@ -940,6 +940,49 @@ describe('ProgrammingExerciseUpdateComponent', () => { }); } }); + + it('should find invalid timeout', () => { + comp.programmingExercise.buildConfig!.timeoutSeconds = -1; + expect(comp.getInvalidReasons()).toContainEqual({ + translateKey: 'artemisApp.programmingExercise.timeout.alert', + translateValues: {}, + }); + + comp.programmingExercise.buildConfig!.timeoutSeconds = 100; + expect(comp.getInvalidReasons()).not.toContainEqual({ + translateKey: 'artemisApp.programmingExercise.timeout.alert', + translateValues: {}, + }); + }); + + it('should find invalid checkoutpaths', () => { + comp.programmingExercise.buildConfig!.assignmentCheckoutPath = 'assignment'; + comp.programmingExercise.buildConfig!.testCheckoutPath = 'assignment'; + comp.programmingExercise.buildConfig!.solutionCheckoutPath = 'solution'; + + expect(comp.getInvalidReasons()).toContainEqual({ + translateKey: 'artemisApp.programmingExercise.checkoutPath.invalid', + translateValues: {}, + }); + + comp.programmingExercise.buildConfig!.testCheckoutPath = 'test'; + expect(comp.getInvalidReasons()).not.toContainEqual({ + translateKey: 'artemisApp.programmingExercise.checkoutPath.invalid', + translateValues: {}, + }); + + comp.programmingExercise.buildConfig!.assignmentCheckoutPath = 'assignment/'; + expect(comp.getInvalidReasons()).toContainEqual({ + translateKey: 'artemisApp.programmingExercise.checkoutPath.invalid', + translateValues: {}, + }); + + comp.programmingExercise.buildConfig!.assignmentCheckoutPath = 'assignment'; + expect(comp.getInvalidReasons()).not.toContainEqual({ + translateKey: 'artemisApp.programmingExercise.checkoutPath.invalid', + translateValues: {}, + }); + }); }); describe('disable features based on selected language and project type', () => { diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts index 963ef4c6ff37..5079df73e575 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts @@ -7,6 +7,7 @@ import { ProgrammingExerciseInformationComponent } from 'app/exercises/programmi import { DefaultValueAccessor, NgModel } from '@angular/forms'; import { RemoveKeysPipe } from 'app/shared/pipes/remove-keys.pipe'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; +import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; import { HelpIconComponent } from 'app/shared/components/help-icon.component'; import { CategorySelectorComponent } from 'app/shared/category-selector/category-selector.component'; import { AddAuxiliaryRepositoryButtonComponent } from 'app/exercises/programming/manage/update/add-auxiliary-repository-button.component'; @@ -15,6 +16,7 @@ import { ExerciseTitleChannelNameComponent } from 'app/exercises/shared/exercise import { TableEditableFieldComponent } from 'app/shared/table/table-editable-field.component'; import { QueryList } from '@angular/core'; import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; +import { ProgrammingExerciseEditCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component'; describe('ProgrammingExerciseInformationComponent', () => { let fixture: ComponentFixture; @@ -30,6 +32,7 @@ describe('ProgrammingExerciseInformationComponent', () => { MockComponent(HelpIconComponent), MockComponent(ExerciseTitleChannelNameComponent), MockComponent(ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent), + MockComponent(ProgrammingExerciseEditCheckoutDirectoriesComponent), MockComponent(CategorySelectorComponent), MockComponent(AddAuxiliaryRepositoryButtonComponent), MockPipe(ArtemisTranslatePipe), @@ -51,6 +54,7 @@ describe('ProgrammingExerciseInformationComponent', () => { comp.programmingExerciseCreationConfig = programmingExerciseCreationConfigMock; comp.programmingExercise = new ProgrammingExercise(undefined, undefined); + comp.programmingExercise.buildConfig = new ProgrammingExerciseBuildConfig(); }); }); @@ -72,6 +76,7 @@ describe('ProgrammingExerciseInformationComponent', () => { comp.recreateBuildPlansField = { valueChanges: new Subject(), valid: true } as any as NgModel; comp.updateTemplateFilesField = { valueChanges: new Subject(), valid: true } as any as NgModel; comp.tableEditableFields = { changes: new Subject() } as any as QueryList; + comp.programmingExerciseEditCheckoutDirectories = { formValidChanges: new Subject() } as ProgrammingExerciseEditCheckoutDirectoriesComponent; comp.ngAfterViewInit(); (comp.tableEditableFields.changes as Subject).next({ toArray: () => [editableField] } as any as QueryList); comp.exerciseTitleChannelComponent.titleChannelNameComponent.formValidChanges.next(false); @@ -80,6 +85,16 @@ describe('ProgrammingExerciseInformationComponent', () => { (comp.recreateBuildPlansField.valueChanges as Subject).next(false); (comp.updateTemplateFilesField.valueChanges as Subject).next(false); (editableField.editingInput.valueChanges as Subject).next(false); - expect(calculateFormValidSpy).toHaveBeenCalledTimes(6); + comp.programmingExerciseEditCheckoutDirectories.formValidChanges.next(false); + expect(calculateFormValidSpy).toHaveBeenCalledTimes(7); + }); + + it('should update checkout directories', () => { + comp.onTestRepositoryCheckoutPathChange('test'); + expect(comp.programmingExercise.buildConfig?.testCheckoutPath).toBe('test'); + comp.onSolutionRepositoryCheckoutPathChange('solution'); + expect(comp.programmingExercise.buildConfig?.solutionCheckoutPath).toBe('solution'); + comp.onAssigmentRepositoryCheckoutPathChange('assignment'); + expect(comp.programmingExercise.buildConfig?.assignmentCheckoutPath).toBe('assignment'); }); });