From b259b8960d191ef4a29a943381f3b8ad9445725c Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:41:28 +0200 Subject: [PATCH] Development: Add build config entity for programming exercises (#8897) --- .../artemis/domain/ProgrammingExercise.java | 186 +++--------- .../ProgrammingExerciseBuildConfig.java | 277 ++++++++++++++++++ .../repository/BuildPlanRepository.java | 13 + .../repository/ParticipationRepository.java | 9 + ...grammingExerciseBuildConfigRepository.java | 38 +++ .../ProgrammingExerciseRepository.java | 186 ++++++++++-- .../BuildScriptProviderService.java | 4 +- .../GenericBuildScriptGenerationService.java | 4 +- .../AeolusBuildScriptGenerationService.java | 2 +- .../aeolus/AeolusTemplateService.java | 4 +- .../ci/AbstractBuildPlanCreator.java | 13 +- ...actContinuousIntegrationResultService.java | 9 +- .../connectors/gitlab/GitLabService.java | 7 +- .../gitlabci/GitLabCIBuildPlanService.java | 8 +- .../gitlabci/GitLabCIResultService.java | 6 +- .../connectors/gitlabci/GitLabCIService.java | 16 +- .../gitlabci/GitLabCITriggerService.java | 11 +- .../jenkins/JenkinsResultService.java | 6 +- .../connectors/jenkins/JenkinsService.java | 18 +- .../build_plan/JenkinsBuildPlanService.java | 24 +- .../JenkinsPipelineScriptCreator.java | 10 +- .../LocalCIBuildConfigurationService.java | 12 +- .../LocalCIResultProcessingService.java | 4 +- .../localci/LocalCIResultService.java | 6 +- .../connectors/localci/LocalCIService.java | 20 +- .../localci/LocalCITriggerService.java | 34 ++- .../connectors/localvc/LocalVCService.java | 7 +- .../vcs/AbstractVersionControlService.java | 17 +- .../service/exam/ExamImportService.java | 2 +- .../behavioral/BehavioralTestCaseService.java | 2 +- .../JavaTemplateUpgradeService.java | 2 +- ...ProgrammingExerciseImportBasicService.java | 37 ++- .../ProgrammingExerciseRepositoryService.java | 6 +- .../ProgrammingExerciseService.java | 51 ++-- .../ProgrammingTriggerService.java | 2 +- ...grammingExerciseSolutionEntryResource.java | 2 +- .../rest/open/PublicBuildPlanResource.java | 4 +- .../web/rest/open/PublicResultResource.java | 2 +- ...ogrammingExerciseExportImportResource.java | 4 +- .../ProgrammingExerciseResource.java | 18 +- .../changelog/20240626200000_changelog.xml | 101 +++++++ .../resources/config/liquibase/master.xml | 1 + .../detail-overview-list.component.html | 2 +- .../entities/programming-exercise.model.ts | 46 ++- ...code-hint-generation-overview.component.ts | 2 +- ...ution-entry-generation-step.component.html | 2 +- ...programming-exercise-detail.component.html | 2 +- .../programming-exercise-detail.component.ts | 29 +- .../programming-exercise-update.component.ts | 28 +- ...se-custom-aeolus-build-plan.component.html | 9 +- ...cise-custom-aeolus-build-plan.component.ts | 40 +-- ...-exercise-custom-build-plan.component.html | 7 +- ...ng-exercise-custom-build-plan.component.ts | 32 +- ...amming-exercise-information.component.html | 2 +- ...ogramming-exercise-language.component.html | 14 +- .../exercise-import-from-file.component.ts | 6 +- ...ctSpringIntegrationLocalCILocalVCTest.java | 4 + .../artemis/BuildPlanIntegrationTest.java | 21 +- .../ManagementResourceIntegrationTest.java | 2 + .../CourseCompetencyIntegrationTest.java | 2 +- ...eolusBuildScriptGenerationServiceTest.java | 14 +- .../artemis/connectors/AeolusServiceTest.java | 13 +- .../artemis/course/CourseUtilService.java | 9 + .../in/www1/artemis/exam/ExamStartTest.java | 1 + .../in/www1/artemis/exam/ExamUtilService.java | 6 + ...ciseGroupIntegrationJenkinsGitlabTest.java | 5 + .../exam/ProgrammingExamIntegrationTest.java | 9 + .../exam/StudentExamIntegrationTest.java | 3 +- .../programming/GitlabServiceTest.java | 7 +- .../ProgrammingAssessmentIntegrationTest.java | 5 + .../ProgrammingExerciseBuildPlanTest.java | 6 +- .../ProgrammingExerciseFactory.java | 27 +- ...ProgrammingExerciseGradingServiceTest.java | 5 + ...rammingExerciseIntegrationTestService.java | 11 +- ...ExerciseLocalVCLocalCIIntegrationTest.java | 4 +- .../ProgrammingExerciseResultTestService.java | 10 +- ...rogrammingExerciseScheduleServiceTest.java | 18 +- ...rammingExerciseServiceIntegrationTest.java | 7 +- ...ammingExerciseTemplateIntegrationTest.java | 2 +- .../programming/ProgrammingExerciseTest.java | 20 +- ...rogrammingExerciseTestCaseServiceTest.java | 3 +- .../ProgrammingExerciseTestService.java | 58 +++- .../ProgrammingExerciseUtilService.java | 31 +- ...AndResultGitlabJenkinsIntegrationTest.java | 5 + .../StaticCodeAnalysisIntegrationTest.java | 2 + ...TestRepositoryResourceIntegrationTest.java | 5 + .../ProgrammingExerciseTaskServiceTest.java | 12 +- .../BehavioralTestCaseServiceTest.java | 2 +- .../iris/IrisChatMessageIntegrationTest.java | 3 +- ...AbstractLocalCILocalVCIntegrationTest.java | 5 +- .../localvcci/LocalCIIntegrationTest.java | 4 +- .../artemis/localvcci/LocalCIServiceTest.java | 16 +- ...VCLocalCIParticipationIntegrationTest.java | 5 +- .../ParticipationIntegrationTest.java | 10 +- .../ProgrammingExerciseTestRepository.java | 1 + .../artemis/service/GitlabCIServiceTest.java | 16 +- .../artemis/service/JenkinsServiceTest.java | 14 +- .../service/ParticipationServiceTest.java | 2 +- .../JenkinsPipelineScriptCreatorTest.java | 12 +- ...ngExerciseFeedbackCreationServiceTest.java | 6 + .../artemis/util/HestiaUtilTestService.java | 9 +- ...hint-generation-overview.component.spec.ts | 2 +- ...custom-aeolus-build-plan.component.spec.ts | 56 ++-- ...ercise-custom-build-plan.component.spec.ts | 54 ++-- ...gramming-exercise-detail.component.spec.ts | 2 +- ...gramming-exercise-update.component.spec.ts | 10 +- .../ProgrammingExerciseParticipation.spec.ts | 1 + .../exercise/programming/c/template.json | 6 +- .../exercise/programming/java/template.json | 6 +- .../exercise/programming/python/template.json | 6 +- .../support/requests/ExerciseAPIRequests.ts | 2 +- 111 files changed, 1406 insertions(+), 577 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExerciseBuildConfig.java create mode 100644 src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseBuildConfigRepository.java create mode 100644 src/main/resources/config/liquibase/changelog/20240626200000_changelog.xml diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java index 4631ad59c6f9..b44ce90a2e67 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java @@ -11,7 +11,6 @@ import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -28,7 +27,6 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.OrderColumn; import jakarta.persistence.SecondaryTable; -import jakarta.validation.constraints.Size; import org.hibernate.Hibernate; import org.slf4j.Logger; @@ -37,8 +35,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.BuildPlanType; @@ -56,8 +52,6 @@ import de.tum.in.www1.artemis.domain.participation.TemplateProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.submissionpolicy.SubmissionPolicy; import de.tum.in.www1.artemis.service.ExerciseDateService; -import de.tum.in.www1.artemis.service.connectors.aeolus.Windfile; -import de.tum.in.www1.artemis.service.connectors.vcs.AbstractVersionControlService; import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -94,6 +88,9 @@ public String getType() { @Column(name = "allow_offline_ide", table = "programming_exercise_details") private Boolean allowOfflineIde; + @Column(name = "allow_online_ide", table = "programming_exercise_details", nullable = false) + private boolean allowOnlineIde = false; + @Column(name = "static_code_analysis_enabled", table = "programming_exercise_details") private Boolean staticCodeAnalysisEnabled; @@ -107,9 +104,6 @@ public String getType() { @Column(name = "package_name") private String packageName; - @Column(name = "sequential_test_runs") - private Boolean sequentialTestRuns; - @Column(name = "show_test_names_to_students", table = "programming_exercise_details") private boolean showTestNamesToStudents; @@ -124,11 +118,6 @@ public String getType() { @Column(name = "project_key", table = "programming_exercise_details", nullable = false) private String projectKey; - @Size(max = 36) - @Nullable - @Column(name = "build_plan_access_secret", table = "programming_exercise_details", length = 36) - private String buildPlanAccessSecret; - @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(unique = true, name = "template_participation_id") @JsonIgnoreProperties("programmingExercise") @@ -164,27 +153,13 @@ public String getType() { @OneToMany(mappedBy = "exercise", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) private Set exerciseHints = new HashSet<>(); - @Column(name = "testwise_coverage_enabled", table = "programming_exercise_details") - private boolean testwiseCoverageEnabled; - - @Column(name = "branch", table = "programming_exercise_details") - private String branch; - - @Column(name = "release_tests_with_example_solution", table = "programming_exercise_details") - private boolean releaseTestsWithExampleSolution; + @Column(name = "release_tests_with_example_solution", table = "programming_exercise_details", nullable = false) + private boolean releaseTestsWithExampleSolution = false; - @Column(name = "build_plan_configuration", table = "programming_exercise_details", columnDefinition = "longtext") - private String buildPlanConfiguration; - - @Column(name = "build_script", table = "programming_exercise_details", columnDefinition = "longtext") - private String buildScript; - - /** - * This boolean flag determines whether the solution repository should be checked out during the build (additional to the student's submission). - * This is currently only supported for HASKELL and OCAML, thus the default value is false. - */ - @Column(name = "checkout_solution_repository", table = "programming_exercise_details", columnDefinition = "boolean default false") - private boolean checkoutSolutionRepository; + @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + @JoinColumn(unique = true, name = "programming_exercise_build_config_id", table = "programming_exercise_details") + @JsonIgnoreProperties("programmingExercise") + private ProgrammingExerciseBuildConfig buildConfig; /** * Convenience getter. The actual URL is stored in the {@link TemplateProgrammingExerciseParticipation} @@ -298,6 +273,18 @@ public void setAllowOfflineIde(Boolean allowOfflineIde) { this.allowOfflineIde = allowOfflineIde; } + public boolean isAllowOnlineIde() { + return allowOnlineIde; + } + + public void setAllowOnlineIde(boolean allowOnlineIde) { + this.allowOnlineIde = allowOnlineIde; + } + + public String getProjectKey() { + return this.projectKey; + } + public Boolean isStaticCodeAnalysisEnabled() { return this.staticCodeAnalysisEnabled; } @@ -314,25 +301,6 @@ public void setMaxStaticCodeAnalysisPenalty(Integer maxStaticCodeAnalysisPenalty this.maxStaticCodeAnalysisPenalty = maxStaticCodeAnalysisPenalty; } - public String getProjectKey() { - return this.projectKey; - } - - public void setBranch(String branch) { - this.branch = branch; - } - - /** - * Getter for the stored default branch of the exercise. - * Use {@link AbstractVersionControlService#getOrRetrieveBranchOfExercise(ProgrammingExercise)} if you are not sure that the value was already set in the Artemis database - * - * @return the name of the default branch or null if not yet stored in Artemis - */ - @JsonIgnore - public String getBranch() { - return branch; - } - public void setReleaseTestsWithExampleSolution(boolean releaseTestsWithExampleSolution) { this.releaseTestsWithExampleSolution = releaseTestsWithExampleSolution; } @@ -482,6 +450,14 @@ public void setSubmissionPolicy(SubmissionPolicy submissionPolicy) { this.submissionPolicy = submissionPolicy; } + public ProgrammingExerciseBuildConfig getBuildConfig() { + return buildConfig; + } + + public void setBuildConfig(ProgrammingExerciseBuildConfig buildConfig) { + this.buildConfig = buildConfig; + } + // jhipster-needle-entity-add-getters-setters - Jhipster will add getters and setters here, do not remove /** @@ -606,15 +582,6 @@ public void addStaticCodeAnalysisCategory(final StaticCodeAnalysisCategory categ staticCodeAnalysisCategories.add(category); } - @JsonProperty("sequentialTestRuns") - public boolean hasSequentialTestRuns() { - return Objects.requireNonNullElse(sequentialTestRuns, false); - } - - public void setSequentialTestRuns(Boolean sequentialTestRuns) { - this.sequentialTestRuns = sequentialTestRuns; - } - public Boolean getShowTestNamesToStudents() { return showTestNamesToStudents; } @@ -657,14 +624,6 @@ public void setProjectType(@Nullable ProjectType projectType) { this.projectType = projectType; } - public Boolean isTestwiseCoverageEnabled() { - return testwiseCoverageEnabled; - } - - public void setTestwiseCoverageEnabled(Boolean testwiseCoverageEnabled) { - this.testwiseCoverageEnabled = testwiseCoverageEnabled; - } - /** * set all sensitive information to null, so no info with respect to the solution gets leaked to students through json */ @@ -675,8 +634,9 @@ public void filterSensitiveInformation() { setTestRepositoryUri(null); setTemplateBuildPlanId(null); setSolutionBuildPlanId(null); - setBuildPlanConfiguration(null); - setBuildScript(null); + if (buildConfig != null && Hibernate.isInitialized(buildConfig)) { + buildConfig.filterSensitiveInformation(); + } super.filterSensitiveInformation(); } @@ -761,16 +721,8 @@ private boolean checkForAssessedResult(Result result) { public String toString() { return "ProgrammingExercise{" + "id=" + getId() + ", templateRepositoryUri='" + getTemplateRepositoryUri() + "'" + ", solutionRepositoryUri='" + getSolutionRepositoryUri() + "'" + ", templateBuildPlanId='" + getTemplateBuildPlanId() + "'" + ", solutionBuildPlanId='" + getSolutionBuildPlanId() + "'" + ", allowOnlineEditor='" - + isAllowOnlineEditor() + "'" + ", programmingLanguage='" + getProgrammingLanguage() + "'" + ", packageName='" + getPackageName() + "'" + ", testCasesChanged='" - + testCasesChanged + "'" + "}"; - } - - public boolean getCheckoutSolutionRepository() { - return this.checkoutSolutionRepository; - } - - public void setCheckoutSolutionRepository(boolean checkoutSolutionRepository) { - this.checkoutSolutionRepository = checkoutSolutionRepository; + + isAllowOnlineEditor() + "'" + ", allowOnlineIde='" + isAllowOnlineIde() + "'" + ", programmingLanguage='" + getProgrammingLanguage() + "'" + ", packageName='" + + getPackageName() + "'" + "'" + ", testCasesChanged='" + testCasesChanged + "'" + "}"; } /** @@ -812,7 +764,7 @@ public void validateStaticCodeAnalysisSettings(ProgrammingLanguageFeature progra } // Check that programming exercise doesn't have sequential test runs and static code analysis enabled - if (Boolean.TRUE.equals(isStaticCodeAnalysisEnabled()) && hasSequentialTestRuns()) { + if (Boolean.TRUE.equals(isStaticCodeAnalysisEnabled()) && getBuildConfig().hasSequentialTestRuns()) { throw new BadRequestAlertException("The static code analysis with sequential test runs is not supported at the moment", "Exercise", "staticCodeAnalysisAndSequential"); } @@ -867,19 +819,6 @@ public void setExerciseHints(Set exerciseHints) { this.exerciseHints = exerciseHints; } - public boolean hasBuildPlanAccessSecretSet() { - return buildPlanAccessSecret != null && !buildPlanAccessSecret.isEmpty(); - } - - @Nullable - public String getBuildPlanAccessSecret() { - return buildPlanAccessSecret; - } - - public void generateAndSetBuildPlanAccessSecret() { - buildPlanAccessSecret = UUID.randomUUID().toString(); - } - /** * {@inheritDoc} */ @@ -890,61 +829,6 @@ public void disconnectRelatedEntities() { super.disconnectRelatedEntities(); } - /** - * Returns the JSON encoded custom build plan configuration - * - * @return the JSON encoded custom build plan configuration or null if the default one should be used - */ - public String getBuildPlanConfiguration() { - return buildPlanConfiguration; - } - - /** - * Sets the JSON encoded custom build plan configuration - * - * @param buildPlanConfiguration the JSON encoded custom build plan configuration - */ - public void setBuildPlanConfiguration(String buildPlanConfiguration) { - this.buildPlanConfiguration = buildPlanConfiguration; - } - - /** - * We store the build plan configuration as a JSON string in the database, as it is easier to handle than a complex object structure. - * This method parses the JSON string and returns a {@link Windfile} object. - * - * @return the {@link Windfile} object or null if the JSON string could not be parsed - */ - public Windfile getWindfile() { - if (buildPlanConfiguration == null) { - return null; - } - try { - return Windfile.deserialize(buildPlanConfiguration); - } - catch (JsonProcessingException e) { - log.error("Could not parse build plan configuration for programming exercise {}", this.getId(), e); - } - return null; - } - - /** - * We store the bash script in the database - * - * @return the build script or null if the build script does not exist - */ - public String getBuildScript() { - return buildScript; - } - - /** - * Update the build script - * - * @param buildScript the new build script for the programming exercise - */ - public void setBuildScript(String buildScript) { - this.buildScript = buildScript; - } - /** * In course exercises students shall receive immediate feedback. {@link Visibility#ALWAYS} * In Exams misconfiguration and leaking test results to students during an exam shall be prevented by the default setting. {@link Visibility#AFTER_DUE_DATE} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExerciseBuildConfig.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExerciseBuildConfig.java new file mode 100644 index 000000000000..5d4bfe7724d0 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExerciseBuildConfig.java @@ -0,0 +1,277 @@ +package de.tum.in.www1.artemis.domain; + +import java.util.Objects; +import java.util.UUID; + +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Size; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; + +import de.tum.in.www1.artemis.service.connectors.aeolus.Windfile; +import de.tum.in.www1.artemis.service.connectors.vcs.AbstractVersionControlService; + +@Entity +@Table(name = "programming_exercise_build_config") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ProgrammingExerciseBuildConfig extends DomainObject { + + private static final Logger log = LoggerFactory.getLogger(ProgrammingExerciseBuildConfig.class); + + @Column(name = "sequential_test_runs") + private Boolean sequentialTestRuns; + + @Column(name = "branch") + private String branch; + + @Column(name = "build_plan_configuration", columnDefinition = "longtext") + private String buildPlanConfiguration; + + @Column(name = "build_script", columnDefinition = "longtext") + private String buildScript; + + /** + * This boolean flag determines whether the solution repository should be checked out during the build (additional to the student's submission). + * This is currently only supported for HASKELL and OCAML, thus the default value is false. + */ + @Column(name = "checkout_solution_repository", columnDefinition = "boolean default false") + private boolean checkoutSolutionRepository = false; + + @Column(name = "checkout_path") + private String checkoutPath; + + @Column(name = "timeout_seconds") + private int timeoutSeconds; + + @Column(name = "docker_flags") + private String dockerFlags; + + @OneToOne(mappedBy = "buildConfig") + @JsonIgnoreProperties("buildConfig") + private ProgrammingExercise programmingExercise; + + @Column(name = "testwise_coverage_enabled") + private boolean testwiseCoverageEnabled; + + @Nullable + @Column(name = "theia_image") + private String theiaImage; + + @Column(name = "allow_branching", columnDefinition = "boolean default false", nullable = false) + private boolean allowBranching = false; // default value + + @Column(name = "branch_regex") + private String branchRegex; + + @Size(max = 36) + @Nullable + @Column(name = "build_plan_access_secret", length = 36) + private String buildPlanAccessSecret; + + public ProgrammingExerciseBuildConfig() { + } + + public ProgrammingExerciseBuildConfig(ProgrammingExerciseBuildConfig originalBuildConfig) { + this.setBranch(originalBuildConfig.getBranch()); + this.setBuildPlanConfiguration(originalBuildConfig.getBuildPlanConfiguration()); + this.setCheckoutPath(originalBuildConfig.getCheckoutPath()); + this.setCheckoutSolutionRepository(originalBuildConfig.getCheckoutSolutionRepository()); + this.setDockerFlags(originalBuildConfig.getDockerFlags()); + this.setSequentialTestRuns(originalBuildConfig.hasSequentialTestRuns()); + this.setBuildScript(originalBuildConfig.getBuildScript()); + this.setTestwiseCoverageEnabled(originalBuildConfig.isTestwiseCoverageEnabled()); + this.setTimeoutSeconds(originalBuildConfig.getTimeoutSeconds()); + this.setTheiaImage(originalBuildConfig.getTheiaImage()); + this.setAllowBranching(originalBuildConfig.isAllowBranching()); + this.setBranchRegex(originalBuildConfig.getBranchRegex()); + this.setProgrammingExercise(null); + this.buildPlanAccessSecret = null; + } + + @JsonProperty("sequentialTestRuns") + public boolean hasSequentialTestRuns() { + return Objects.requireNonNullElse(sequentialTestRuns, false); + } + + public void setSequentialTestRuns(Boolean sequentialTestRuns) { + this.sequentialTestRuns = sequentialTestRuns; + } + + /** + * Getter for the stored default branch of the exercise. + * Use {@link AbstractVersionControlService#getOrRetrieveBranchOfExercise(ProgrammingExercise)} if you are not sure that the value was already set in the Artemis database + * + * @return the name of the default branch or null if not yet stored in Artemis + */ + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + /** + * Returns the JSON encoded custom build plan configuration + * + * @return the JSON encoded custom build plan configuration or null if the default one should be used + */ + public String getBuildPlanConfiguration() { + return buildPlanConfiguration; + } + + /** + * Sets the JSON encoded custom build plan configuration + * + * @param buildPlanConfiguration the JSON encoded custom build plan configuration + */ + public void setBuildPlanConfiguration(String buildPlanConfiguration) { + this.buildPlanConfiguration = buildPlanConfiguration; + } + + /** + * We store the bash script in the database + * + * @return the build script or null if the build script does not exist + */ + public String getBuildScript() { + return buildScript; + } + + /** + * Update the build script + * + * @param buildScript the new build script for the programming exercise + */ + public void setBuildScript(String buildScript) { + this.buildScript = buildScript; + } + + public boolean getCheckoutSolutionRepository() { + return checkoutSolutionRepository; + } + + public void setCheckoutSolutionRepository(boolean checkoutSolutionRepository) { + this.checkoutSolutionRepository = checkoutSolutionRepository; + } + + public String getCheckoutPath() { + return checkoutPath; + } + + public void setCheckoutPath(String checkoutPath) { + this.checkoutPath = checkoutPath; + } + + public int getTimeoutSeconds() { + return timeoutSeconds; + } + + public void setTimeoutSeconds(int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + } + + public String getDockerFlags() { + return dockerFlags; + } + + public void setDockerFlags(String dockerFlags) { + this.dockerFlags = dockerFlags; + } + + @Nullable + public String getTheiaImage() { + return theiaImage; + } + + public void setTheiaImage(@Nullable String theiaImage) { + this.theiaImage = theiaImage; + } + + public String getBranchRegex() { + return branchRegex; + } + + public void setBranchRegex(String branchRegex) { + this.branchRegex = branchRegex; + } + + public boolean isAllowBranching() { + return allowBranching; + } + + public void setAllowBranching(boolean allowBranching) { + this.allowBranching = allowBranching; + } + + /** + * We store the build plan configuration as a JSON string in the database, as it is easier to handle than a complex object structure. + * This method parses the JSON string and returns a {@link Windfile} object. + * + * @return the {@link Windfile} object or null if the JSON string could not be parsed + */ + public Windfile getWindfile() { + if (buildPlanConfiguration == null) { + return null; + } + try { + return Windfile.deserialize(buildPlanConfiguration); + } + catch (JsonProcessingException e) { + log.error("Could not parse build plan configuration for programming exercise {}", this.getId(), e); + } + return null; + } + + public void filterSensitiveInformation() { + setBuildPlanConfiguration(null); + setBuildScript(null); + } + + public boolean isTestwiseCoverageEnabled() { + return testwiseCoverageEnabled; + } + + public void setTestwiseCoverageEnabled(boolean testwiseCoverageEnabled) { + this.testwiseCoverageEnabled = testwiseCoverageEnabled; + } + + public ProgrammingExercise getProgrammingExercise() { + return programmingExercise; + } + + public void setProgrammingExercise(ProgrammingExercise programmingExercise) { + this.programmingExercise = programmingExercise; + } + + public boolean hasBuildPlanAccessSecretSet() { + return buildPlanAccessSecret != null && !buildPlanAccessSecret.isEmpty(); + } + + @Nullable + public String getBuildPlanAccessSecret() { + return buildPlanAccessSecret; + } + + public void generateAndSetBuildPlanAccessSecret() { + buildPlanAccessSecret = UUID.randomUUID().toString(); + } + + @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 + + ", theiaImage='" + theiaImage + '\'' + ", allowBranching=" + allowBranching + ", branchRegex='" + branchRegex + '\'' + '}'; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java index 52638a05b402..820618ebaa9b 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java @@ -22,6 +22,15 @@ public interface BuildPlanRepository extends ArtemisJpaRepository findByProgrammingExercises_IdWithProgrammingExercises(@Param("exerciseId") long exerciseId); + @Query(""" + SELECT buildPlan + FROM BuildPlan buildPlan + INNER JOIN FETCH buildPlan.programmingExercises programmingExercises + LEFT JOIN FETCH programmingExercises.buildConfig buildConfig + WHERE programmingExercises.id = :exerciseId + """) + Optional findByProgrammingExercises_IdWithProgrammingExercisesWithBuildConfig(@Param("exerciseId") long exerciseId); + @Query(""" SELECT buildPlan FROM BuildPlan buildPlan @@ -34,6 +43,10 @@ default BuildPlan findByProgrammingExercises_IdWithProgrammingExercisesElseThrow return getValueElseThrow(findByProgrammingExercises_IdWithProgrammingExercises(exerciseId)); } + default BuildPlan findByProgrammingExercises_IdWithProgrammingExercisesWithBuildConfigElseThrow(final long exerciseId) { + return getValueElseThrow(findByProgrammingExercises_IdWithProgrammingExercisesWithBuildConfig(exerciseId)); + } + @EntityGraph(type = LOAD, attributePaths = { "programmingExercises" }) Optional findByBuildPlan(String buildPlan); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ParticipationRepository.java index 4ee82b45ed4a..b1fbd8fd04bf 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ParticipationRepository.java @@ -110,6 +110,15 @@ SELECT MIN(p.individualDueDate) """) Set findWithIndividualDueDateByExerciseId(@Param("exerciseId") long exerciseId); + @Query(""" + SELECT p + FROM Participation p + LEFT JOIN FETCH p.exercise e + LEFT JOIN FETCH e.buildConfig + WHERE p.id = :participationId + """) + Optional findWithProgrammingExerciseWithBuildConfigById(@Param("participationId") long participationId); + Set findByExerciseId(long exerciseId); /** diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseBuildConfigRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseBuildConfigRepository.java new file mode 100644 index 000000000000..06f2293e6d88 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseBuildConfigRepository.java @@ -0,0 +1,38 @@ +package de.tum.in.www1.artemis.repository; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.util.Optional; + +import org.hibernate.Hibernate; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; +import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; + +@Profile(PROFILE_CORE) +@Repository +public interface ProgrammingExerciseBuildConfigRepository extends ArtemisJpaRepository { + + Optional findByProgrammingExerciseId(Long programmingExerciseId); + + default ProgrammingExerciseBuildConfig getProgrammingExerciseBuildConfigElseThrow(ProgrammingExercise programmingExercise) { + if (programmingExercise.getBuildConfig() == null || !Hibernate.isInitialized(programmingExercise.getBuildConfig())) { + return getValueElseThrow(findByProgrammingExerciseId(programmingExercise.getId())); + } + return programmingExercise.getBuildConfig(); + } + + default void generateBuildPlanAccessSecretIfNotExists(ProgrammingExerciseBuildConfig buildConfig) { + if (!buildConfig.hasBuildPlanAccessSecretSet()) { + buildConfig.generateAndSetBuildPlanAccessSecret(); + save(buildConfig); + } + } + + default void loadAndSetBuildConfig(ProgrammingExercise programmingExercise) { + programmingExercise.setBuildConfig(getProgrammingExerciseBuildConfigElseThrow(programmingExercise)); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java index 5b970dd19fe9..821d7a6932f0 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java @@ -68,22 +68,37 @@ public interface ProgrammingExerciseRepository extends DynamicSpecificationRepos "submissionPolicy" }) Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "teamAssignmentConfig", "categories", "auxiliaryRepositories", + "submissionPolicy", "buildConfig" }) + Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "teamAssignmentConfig", "categories", "competencies", "auxiliaryRepositories", "submissionPolicy" }) Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesById(long exerciseId); @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "teamAssignmentConfig", "categories", "competencies", "auxiliaryRepositories", - "submissionPolicy", "plagiarismDetectionConfig" }) - Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigById(long exerciseId); + "submissionPolicy", "buildConfig" }) + Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesCompetenciesAndBuildConfigById(long exerciseId); + + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "teamAssignmentConfig", "categories", "competencies", "auxiliaryRepositories", + "submissionPolicy", "plagiarismDetectionConfig", "buildConfig" }) + Optional findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigAndBuildConfigById( + long exerciseId); @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "auxiliaryRepositories" }) Optional findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "auxiliaryRepositories", "buildConfig" }) + Optional findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesAndBuildConfigById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation" }) Optional findWithTemplateAndSolutionParticipationById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "buildConfig" }) + Optional findWithTemplateAndSolutionParticipationAndBuildConfigById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "categories", "teamAssignmentConfig", "templateParticipation.submissions.results", "solutionParticipation.submissions.results", - "auxiliaryRepositories", "plagiarismDetectionConfig", "buildPlanConfiguration", "buildScript", "templateParticipation", "solutionParticipation" }) + "auxiliaryRepositories", "plagiarismDetectionConfig", "templateParticipation", "solutionParticipation", "buildConfig" }) Optional findForCreationById(long exerciseId); @EntityGraph(type = LOAD, attributePaths = "testCases") @@ -92,8 +107,8 @@ public interface ProgrammingExerciseRepository extends DynamicSpecificationRepos @EntityGraph(type = LOAD, attributePaths = "auxiliaryRepositories") Optional findWithAuxiliaryRepositoriesById(long exerciseId); - @EntityGraph(type = LOAD, attributePaths = { "auxiliaryRepositories", "competencies" }) - Optional findWithAuxiliaryRepositoriesAndCompetenciesById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "auxiliaryRepositories", "competencies", "buildConfig" }) + Optional findWithAuxiliaryRepositoriesCompetenciesAndBuildConfigById(long exerciseId); @EntityGraph(type = LOAD, attributePaths = "submissionPolicy") Optional findWithSubmissionPolicyById(long exerciseId); @@ -255,8 +270,8 @@ default ProgrammingExercise findOneByProjectKeyOrThrow(String projectKey, boolea """) Optional findWithEagerStudentParticipationsStudentAndLegalSubmissionsById(@Param("exerciseId") long exerciseId); - @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "studentParticipations.team.students" }) - Optional findWithAllParticipationsById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "templateParticipation", "solutionParticipation", "studentParticipations.team.students", "buildConfig" }) + Optional findWithAllParticipationsAndBuildConfigById(long exerciseId); @Query(""" SELECT pe @@ -273,6 +288,31 @@ default ProgrammingExercise findOneByProjectKeyOrThrow(String projectKey, boolea """) Optional findByTemplateParticipationId(@Param("participationId") long participationId); + @Query(""" + SELECT pe + FROM ProgrammingExercise pe + LEFT JOIN FETCH pe.buildConfig + WHERE pe.solutionParticipation.id = :participationId + """) + Optional findBySolutionParticipationIdWithBuildConfig(@Param("participationId") long participationId); + + @Query(""" + SELECT pe + FROM ProgrammingExercise pe + LEFT JOIN pe.studentParticipations spep + LEFT JOIN FETCH pe.buildConfig + WHERE spep.id = :participationId + """) + Optional findByStudentParticipationIdWithBuildConfig(@Param("participationId") long participationId); + + @Query(""" + SELECT pe + FROM ProgrammingExercise pe + LEFT JOIN FETCH pe.buildConfig + WHERE pe.templateParticipation.id = :participationId + """) + Optional findByTemplateParticipationIdWithBuildConfig(@Param("participationId") long participationId); + @Query(""" SELECT pe FROM ProgrammingExercise pe @@ -299,9 +339,27 @@ default ProgrammingExercise findOneByProjectKeyOrThrow(String projectKey, boolea LEFT JOIN FETCH p.solutionParticipation LEFT JOIN FETCH p.auxiliaryRepositories LEFT JOIN FETCH tc.solutionEntries + LEFT JOIN FETCH p.buildConfig WHERE p.id = :exerciseId """) - Optional findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos(@Param("exerciseId") long exerciseId); + Optional findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig( + @Param("exerciseId") long exerciseId); + + @Query(""" + SELECT p + FROM ProgrammingExercise p + LEFT JOIN FETCH p.testCases tc + LEFT JOIN FETCH p.staticCodeAnalysisCategories + LEFT JOIN FETCH p.exerciseHints + LEFT JOIN FETCH p.templateParticipation + LEFT JOIN FETCH p.solutionParticipation + LEFT JOIN FETCH p.auxiliaryRepositories + LEFT JOIN FETCH tc.solutionEntries + LEFT JOIN FETCH p.buildConfig + WHERE p.id = :exerciseId + """) + Optional findByIdWithEagerBuildConfigTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos( + @Param("exerciseId") long exerciseId); /** * Returns all programming exercises that have a due date after {@code now} and have tests marked with @@ -471,8 +529,11 @@ SELECT COUNT (DISTINCT p) """) List findAllProgrammingExercisesInCourseOrInExamsOfCourse(@Param("course") Course course); - @EntityGraph(type = LOAD, attributePaths = { "plagiarismDetectionConfig", "teamAssignmentConfig" }) - Optional findWithPlagiarismDetectionConfigAndTeamConfigById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "plagiarismDetectionConfig", "teamAssignmentConfig", "buildConfig" }) + Optional findWithPlagiarismDetectionConfigTeamConfigAndBuildConfigById(long exerciseId); + + @EntityGraph(type = LOAD, attributePaths = { "buildConfig" }) + Optional findWithBuildConfigById(long exerciseId); long countByShortNameAndCourse(String shortName, Course course); @@ -508,8 +569,13 @@ default ProgrammingExercise findByIdWithGradingCriteriaElseThrow(long exerciseId * @return The programming exercise related to the given id */ @NotNull - default ProgrammingExercise findByIdWithPlagiarismDetectionConfigAndTeamConfigElseThrow(long programmingExerciseId) throws EntityNotFoundException { - return getValueElseThrow(findWithPlagiarismDetectionConfigAndTeamConfigById(programmingExerciseId), programmingExerciseId); + default ProgrammingExercise findByIdWithPlagiarismDetectionConfigTeamConfigAndBuildConfigElseThrow(long programmingExerciseId) throws EntityNotFoundException { + return getValueElseThrow(findWithPlagiarismDetectionConfigTeamConfigAndBuildConfigById(programmingExerciseId), programmingExerciseId); + } + + @NotNull + default ProgrammingExercise findByIdWithBuildConfigElseThrow(long programmingExerciseId) throws EntityNotFoundException { + return getValueElseThrow(findWithBuildConfigById(programmingExerciseId), programmingExerciseId); } /** @@ -524,14 +590,14 @@ default ProgrammingExercise findByIdWithAuxiliaryRepositoriesElseThrow(long prog } /** - * Find a programming exercise with auxiliary repositories and competencies by its id and throw an {@link EntityNotFoundException} if it cannot be found + * Find a programming exercise with auxiliary repositories competencies, and buildConfig by its id and throw an {@link EntityNotFoundException} if it cannot be found * * @param programmingExerciseId of the programming exercise. * @return The programming exercise related to the given id */ @NotNull - default ProgrammingExercise findByIdWithAuxiliaryRepositoriesAndCompetenciesElseThrow(long programmingExerciseId) throws EntityNotFoundException { - return getValueElseThrow(findWithAuxiliaryRepositoriesAndCompetenciesById(programmingExerciseId), programmingExerciseId); + default ProgrammingExercise findByIdWithAuxiliaryRepositoriesCompetenciesAndBuildConfigElseThrow(long programmingExerciseId) throws EntityNotFoundException { + return getValueElseThrow(findWithAuxiliaryRepositoriesCompetenciesAndBuildConfigById(programmingExerciseId), programmingExerciseId); } /** @@ -573,6 +639,20 @@ default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationAndAuxil return getValueElseThrow(findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesById(programmingExerciseId), programmingExerciseId); } + /** + * Find a programming exercise by its id, including auxiliary repositories, template and solution participation, + * their latest results and build config. + * + * @param programmingExerciseId of the programming exercise. + * @return The programming exercise related to the given id + * @throws EntityNotFoundException the programming exercise could not be found. + */ + @NotNull + default ProgrammingExercise findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesAndBuildConfigElseThrow(long programmingExerciseId) throws EntityNotFoundException { + Optional programmingExercise = findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesAndBuildConfigById(programmingExerciseId); + return getValueElseThrow(programmingExercise, programmingExerciseId); + } + /** * Find a programming exercise by its id, with eagerly loaded template and solution participation and auxiliary repositories * @@ -615,6 +695,20 @@ default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssi return getValueElseThrow(findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesById(programmingExerciseId), programmingExerciseId); } + /** + * Find a programming exercise by its id, with eagerly loaded template and solution participation, team assignment config, categories and build config + * + * @param programmingExerciseId of the programming exercise. + * @return The programming exercise related to the given id + * @throws EntityNotFoundException the programming exercise could not be found. + */ + @NotNull + default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigElseThrow(long programmingExerciseId) + throws EntityNotFoundException { + Optional programmingExercise = findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigById(programmingExerciseId); + return getValueElseThrow(programmingExercise, programmingExerciseId); + } + @NotNull default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesElseThrow(long programmingExerciseId) throws EntityNotFoundException { @@ -622,9 +716,18 @@ default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssi } @NotNull - default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigElseThrow( + default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesCompetenciesAndBuildConfigElseThrow(long programmingExerciseId) + throws EntityNotFoundException { + Optional programmingExercise = findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesCompetenciesAndBuildConfigById( + programmingExerciseId); + return getValueElseThrow(programmingExercise, programmingExerciseId); + } + + @NotNull + default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigAndBuildConfigElseThrow( long programmingExerciseId) throws EntityNotFoundException { - return getValueElseThrow(findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigById(programmingExerciseId), + return getValueElseThrow( + findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigAndBuildConfigById(programmingExerciseId), programmingExerciseId); } @@ -714,6 +817,35 @@ default ProgrammingExercise getProgrammingExerciseFromParticipation(ProgrammingE return participation.getProgrammingExercise(); } + /** + * Retrieves the associated ProgrammingExercise with the build config for a given ProgrammingExerciseParticipation. + * If the ProgrammingExercise is not already loaded, it is fetched from the database and linked + * to the specified participation. This method handles different types of participation + * (template, solution, student) to optimize database queries and avoid performance bottlenecks. + * + * @param participation the programming exercise participation object; must not be null + * @return the linked ProgrammingExercise, or null if not found or the participation is not initialized + */ + default ProgrammingExercise getProgrammingExerciseWithBuildConfigFromParticipation(ProgrammingExerciseParticipation participation) { + // Note: if this participation was retrieved as Participation (abstract super class) from the database, the programming exercise might not be correctly initialized + if (participation.getProgrammingExercise() == null || !Hibernate.isInitialized(participation.getProgrammingExercise())) { + // Find the programming exercise for the given participation + // NOTE: we use different methods to find the programming exercise based on the participation type on purpose to avoid slow database queries + long participationId = participation.getId(); + Optional optionalProgrammingExercise = switch (participation) { + case TemplateProgrammingExerciseParticipation ignored -> findByTemplateParticipationIdWithBuildConfig(participationId); + case SolutionProgrammingExerciseParticipation ignored -> findBySolutionParticipationIdWithBuildConfig(participationId); + case ProgrammingExerciseStudentParticipation ignored -> findByStudentParticipationIdWithBuildConfig(participationId); + default -> Optional.empty(); + }; + if (optionalProgrammingExercise.isEmpty()) { + return null; + } + participation.setProgrammingExercise(optionalProgrammingExercise.get()); + } + return participation.getProgrammingExercise(); + } + /** * Retrieve the programming exercise from a programming exercise participation. * @@ -729,6 +861,19 @@ default ProgrammingExercise getProgrammingExerciseFromParticipationElseThrow(Pro return programmingExercise; } + /** + * Fetch the programming exercise with the build config, or throw an EntityNotFoundException if it cannot be found. + * + * @param programmingExercise The programming exercise to fetch the build config for. + * @return The programming exercise with the build config. + */ + default ProgrammingExercise getProgrammingExerciseWithBuildConfigElseThrow(ProgrammingExercise programmingExercise) { + if (programmingExercise.getBuildConfig() == null || !Hibernate.isInitialized(programmingExercise.getBuildConfig())) { + return getValueElseThrow(findWithBuildConfigById(programmingExercise.getId()), programmingExercise.getId()); + } + return programmingExercise; + } + /** * Validate the programming exercise title. * 1. Check presence and length of exercise title @@ -807,13 +952,6 @@ default void validateCourseSettings(ProgrammingExercise programmingExercise, Cou validateCourseAndExerciseShortName(programmingExercise, course); } - default void generateBuildPlanAccessSecretIfNotExists(ProgrammingExercise exercise) { - if (!exercise.hasBuildPlanAccessSecretSet()) { - exercise.generateAndSetBuildPlanAccessSecret(); - save(exercise); - } - } - /** * Fetch options for the {@link ProgrammingExercise} entity. * Each option specifies an entity or a collection of entities to fetch eagerly when using a dynamic fetching query. diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java index 620182f07764..2975cf69b2a4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.service.ResourceLoaderService; @@ -124,8 +125,9 @@ public String getScriptFor(ProgrammingLanguage programmingLanguage, Optional extractProjectType(String filename) { */ public Windfile getDefaultWindfileFor(ProgrammingExercise exercise) { try { + ProgrammingExerciseBuildConfig buildConfig = exercise.getBuildConfig(); return getWindfileFor(exercise.getProgrammingLanguage(), Optional.ofNullable(exercise.getProjectType()), exercise.isStaticCodeAnalysisEnabled(), - exercise.hasSequentialTestRuns(), exercise.isTestwiseCoverageEnabled()); + buildConfig.hasSequentialTestRuns(), buildConfig.isTestwiseCoverageEnabled()); } catch (IOException e) { log.info("No windfile for the settings of exercise {}", exercise.getId(), e); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractBuildPlanCreator.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractBuildPlanCreator.java index 25abf9704c67..9441711e1dad 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractBuildPlanCreator.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractBuildPlanCreator.java @@ -7,20 +7,20 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.repository.BuildPlanRepository; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; public abstract class AbstractBuildPlanCreator { private final BuildPlanRepository buildPlanRepository; - private final ProgrammingExerciseRepository programmingExerciseRepository; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; @Value("${server.url}") private URL artemisServerUrl; - protected AbstractBuildPlanCreator(final BuildPlanRepository buildPlanRepository, final ProgrammingExerciseRepository programmingExerciseRepository) { + protected AbstractBuildPlanCreator(final BuildPlanRepository buildPlanRepository, final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.buildPlanRepository = buildPlanRepository; - this.programmingExerciseRepository = programmingExerciseRepository; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } /** @@ -38,8 +38,9 @@ protected AbstractBuildPlanCreator(final BuildPlanRepository buildPlanRepository * @return The build plan URL. */ public String generateBuildPlanURL(final ProgrammingExercise exercise) { - programmingExerciseRepository.generateBuildPlanAccessSecretIfNotExists(exercise); - return String.format("%s/api/public/programming-exercises/%d/build-plan?secret=%s", artemisServerUrl, exercise.getId(), exercise.getBuildPlanAccessSecret()); + programmingExerciseBuildConfigRepository.generateBuildPlanAccessSecretIfNotExists(exercise.getBuildConfig()); + return String.format("%s/api/public/programming-exercises/%d/build-plan?secret=%s", artemisServerUrl, exercise.getId(), + exercise.getBuildConfig().getBuildPlanAccessSecret()); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractContinuousIntegrationResultService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractContinuousIntegrationResultService.java index 18925c335d31..10e916753a0b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractContinuousIntegrationResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/AbstractContinuousIntegrationResultService.java @@ -12,6 +12,7 @@ import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.repository.BuildLogStatisticsEntryRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.service.dto.AbstractBuildResultNotificationDTO; import de.tum.in.www1.artemis.service.dto.BuildJobDTOInterface; @@ -28,13 +29,16 @@ public abstract class AbstractContinuousIntegrationResultService implements Cont protected final ProgrammingExerciseFeedbackCreationService feedbackCreationService; + protected final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + protected AbstractContinuousIntegrationResultService(ProgrammingExerciseTestCaseRepository testCaseRepository, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, TestwiseCoverageService testwiseCoverageService, - ProgrammingExerciseFeedbackCreationService feedbackCreationService) { + ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.testCaseRepository = testCaseRepository; this.buildLogStatisticsEntryRepository = buildLogStatisticsEntryRepository; this.testwiseCoverageService = testwiseCoverageService; this.feedbackCreationService = feedbackCreationService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } @Override @@ -102,7 +106,8 @@ private void addStaticCodeAnalysisFeedbackToResult(Result result, AbstractBuildR } private void addTestwiseCoverageReportToResult(Result result, AbstractBuildResultNotificationDTO buildResult, ProgrammingExercise programmingExercise) { - if (Boolean.TRUE.equals(programmingExercise.isTestwiseCoverageEnabled())) { + programmingExercise.setBuildConfig(programmingExerciseBuildConfigRepository.getProgrammingExerciseBuildConfigElseThrow(programmingExercise)); + if (Boolean.TRUE.equals(programmingExercise.getBuildConfig().isTestwiseCoverageEnabled())) { var report = buildResult.getTestwiseCoverageReports(); if (report != null) { // since the test cases are not saved to the database yet, the test case is null for the entries diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java index 487b87bd16d4..103a58652025 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java @@ -53,6 +53,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.VersionControlException; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; @@ -89,8 +90,10 @@ public class GitLabService extends AbstractVersionControlService { public GitLabService(UserRepository userRepository, @Qualifier("shortTimeoutGitlabRestTemplate") RestTemplate shortTimeoutRestTemplate, GitLabApi gitlab, UriService uriService, GitLabUserManagementService gitLabUserManagementService, GitService gitService, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, - ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { - super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); + ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository, + programmingExerciseBuildConfigRepository); this.userRepository = userRepository; this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; this.gitlab = gitlab; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIBuildPlanService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIBuildPlanService.java index 3b5391c701a0..80e9dab30be0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIBuildPlanService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIBuildPlanService.java @@ -15,7 +15,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.repository.BuildPlanRepository; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.service.ResourceLoaderService; import de.tum.in.www1.artemis.service.connectors.ci.AbstractBuildPlanCreator; @@ -29,9 +29,9 @@ public class GitLabCIBuildPlanService extends AbstractBuildPlanCreator { private final ResourceLoaderService resourceLoaderService; - public GitLabCIBuildPlanService(BuildPlanRepository buildPlanRepository, ProgrammingExerciseRepository programmingExerciseRepository, + public GitLabCIBuildPlanService(BuildPlanRepository buildPlanRepository, ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, ResourceLoaderService resourceLoaderService) { - super(buildPlanRepository, programmingExerciseRepository); + super(buildPlanRepository, programmingExerciseBuildConfigRepository); this.resourceLoaderService = resourceLoaderService; } @@ -45,7 +45,7 @@ public GitLabCIBuildPlanService(BuildPlanRepository buildPlanRepository, Program @Override public String generateDefaultBuildPlan(ProgrammingExercise programmingExercise) { final Optional projectTypeName = getProjectTypeName(programmingExercise); - final Path resourcePath = buildResourcePath(programmingExercise.getProgrammingLanguage(), projectTypeName, programmingExercise.hasSequentialTestRuns()); + final Path resourcePath = buildResourcePath(programmingExercise.getProgrammingLanguage(), projectTypeName, programmingExercise.getBuildConfig().hasSequentialTestRuns()); final Resource resource = resourceLoaderService.getResource(resourcePath); try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIResultService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIResultService.java index 906167dc0375..620e22e09a55 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIResultService.java @@ -15,6 +15,7 @@ import de.tum.in.www1.artemis.domain.statistics.BuildLogStatisticsEntry; import de.tum.in.www1.artemis.repository.BuildLogStatisticsEntryRepository; import de.tum.in.www1.artemis.repository.FeedbackRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository; import de.tum.in.www1.artemis.service.BuildLogEntryService; @@ -32,8 +33,9 @@ public class GitLabCIResultService extends AbstractContinuousIntegrationResultSe public GitLabCIResultService(ProgrammingSubmissionRepository programmingSubmissionRepository, FeedbackRepository feedbackRepository, BuildLogEntryService buildLogService, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, TestwiseCoverageService testwiseCoverageService, - ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseTestCaseRepository testCaseRepository) { - super(testCaseRepository, buildLogStatisticsEntryRepository, testwiseCoverageService, feedbackCreationService); + ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseTestCaseRepository testCaseRepository, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + super(testCaseRepository, buildLogStatisticsEntryRepository, testwiseCoverageService, feedbackCreationService, programmingExerciseBuildConfigRepository); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java index f338a26e910f..2c78eb608c4e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java @@ -26,6 +26,7 @@ import de.tum.in.www1.artemis.config.ProgrammingLanguageConfiguration; import de.tum.in.www1.artemis.domain.BuildPlan; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; @@ -33,6 +34,7 @@ import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.GitLabCIException; import de.tum.in.www1.artemis.repository.BuildPlanRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.ci.AbstractContinuousIntegrationService; @@ -84,6 +86,8 @@ public class GitLabCIService extends AbstractContinuousIntegrationService { private final ProgrammingLanguageConfiguration programmingLanguageConfiguration; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + @Value("${artemis.version-control.url}") private URL gitlabServerUrl; @@ -103,12 +107,13 @@ public class GitLabCIService extends AbstractContinuousIntegrationService { private String gitlabToken; public GitLabCIService(GitLabApi gitlab, UriService uriService, BuildPlanRepository buildPlanRepository, GitLabCIBuildPlanService buildPlanService, - ProgrammingLanguageConfiguration programmingLanguageConfiguration) { + ProgrammingLanguageConfiguration programmingLanguageConfiguration, ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.gitlab = gitlab; this.uriService = uriService; this.buildPlanRepository = buildPlanRepository; this.buildPlanService = buildPlanService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } @Override @@ -122,6 +127,9 @@ public void createBuildPlanForExercise(ProgrammingExercise exercise, String plan private void setupGitLabCIConfiguration(VcsRepositoryUri repositoryUri, ProgrammingExercise exercise, String buildPlanId) { final String repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); ProjectApi projectApi = gitlab.getProjectApi(); + + programmingExerciseBuildConfigRepository.loadAndSetBuildConfig(exercise); + try { Project project = projectApi.getProject(repositoryPath); @@ -140,7 +148,7 @@ private void setupGitLabCIConfiguration(VcsRepositoryUri repositoryUri, Programm try { // TODO: Reduce the number of API calls - + ProgrammingExerciseBuildConfig buildConfig = exercise.getBuildConfig(); updateVariable(repositoryPath, VARIABLE_BUILD_DOCKER_IMAGE_NAME, programmingLanguageConfiguration.getImage(exercise.getProgrammingLanguage(), Optional.ofNullable(exercise.getProjectType()))); updateVariable(repositoryPath, VARIABLE_BUILD_LOGS_FILE_NAME, "build.log"); @@ -150,8 +158,8 @@ private void setupGitLabCIConfiguration(VcsRepositoryUri repositoryUri, Programm updateVariable(repositoryPath, VARIABLE_NOTIFICATION_PLUGIN_DOCKER_IMAGE_NAME, notificationPluginDockerImage); updateVariable(repositoryPath, VARIABLE_NOTIFICATION_SECRET_NAME, artemisAuthenticationTokenValue); updateVariable(repositoryPath, VARIABLE_NOTIFICATION_URL_NAME, artemisServerUrl.toExternalForm() + NEW_RESULT_RESOURCE_API_PATH); - updateVariable(repositoryPath, VARIABLE_SUBMISSION_GIT_BRANCH_NAME, exercise.getBranch()); - updateVariable(repositoryPath, VARIABLE_TEST_GIT_BRANCH_NAME, exercise.getBranch()); + updateVariable(repositoryPath, VARIABLE_SUBMISSION_GIT_BRANCH_NAME, buildConfig.getBranch()); + updateVariable(repositoryPath, VARIABLE_TEST_GIT_BRANCH_NAME, buildConfig.getBranch()); updateVariable(repositoryPath, VARIABLE_TEST_GIT_REPOSITORY_SLUG_NAME, uriService.getRepositorySlugFromRepositoryUriString(exercise.getTestRepositoryUri())); // TODO: Use a token that is only valid for the test repository for each programming exercise updateVariable(repositoryPath, VARIABLE_TEST_GIT_TOKEN, gitlabToken); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java index bded7d52b9ca..72c4fcb6150b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java @@ -6,10 +6,12 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.GitLabCIException; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationTriggerService; @@ -21,14 +23,19 @@ public class GitLabCITriggerService implements ContinuousIntegrationTriggerServi private final UriService uriService; - public GitLabCITriggerService(GitLabApi gitlab, UriService uriService) { + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + + public GitLabCITriggerService(GitLabApi gitlab, UriService uriService, ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.gitlab = gitlab; this.uriService = uriService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } @Override public void triggerBuild(ProgrammingExerciseParticipation participation, boolean triggerAll) throws ContinuousIntegrationException { - triggerBuild(participation.getVcsRepositoryUri(), participation.getProgrammingExercise().getBranch()); + ProgrammingExercise programmingExercise = participation.getProgrammingExercise(); + programmingExerciseBuildConfigRepository.loadAndSetBuildConfig(programmingExercise); + triggerBuild(participation.getVcsRepositoryUri(), programmingExercise.getBuildConfig().getBranch()); } private void triggerBuild(VcsRepositoryUri vcsRepositoryUri, String branch) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsResultService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsResultService.java index 43723d289f09..dd9de9664f73 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsResultService.java @@ -16,6 +16,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.repository.BuildLogStatisticsEntryRepository; import de.tum.in.www1.artemis.repository.FeedbackRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository; import de.tum.in.www1.artemis.service.BuildLogEntryService; @@ -33,8 +34,9 @@ public class JenkinsResultService extends AbstractContinuousIntegrationResultSer public JenkinsResultService(ProgrammingSubmissionRepository programmingSubmissionRepository, FeedbackRepository feedbackRepository, BuildLogEntryService buildLogService, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, TestwiseCoverageService testwiseCoverageService, - ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseTestCaseRepository testCaseRepository) { - super(testCaseRepository, buildLogStatisticsEntryRepository, testwiseCoverageService, feedbackCreationService); + ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseTestCaseRepository testCaseRepository, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + super(testCaseRepository, buildLogStatisticsEntryRepository, testwiseCoverageService, feedbackCreationService, programmingExerciseBuildConfigRepository); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java index 5f251df9de94..6d3fa87d1c48 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.JenkinsException; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.service.ProfileService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusTemplateService; @@ -67,11 +67,12 @@ public class JenkinsService extends AbstractContinuousIntegrationService { private final ProfileService profileService; - private final ProgrammingExerciseRepository programmingExerciseRepository; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; public JenkinsService(JenkinsServer jenkinsServer, @Qualifier("shortTimeoutJenkinsRestTemplate") RestTemplate shortTimeoutRestTemplate, JenkinsBuildPlanService jenkinsBuildPlanService, JenkinsJobService jenkinsJobService, JenkinsInternalUrlService jenkinsInternalUrlService, - Optional aeolusTemplateService, ProfileService profileService, ProgrammingExerciseRepository programmingExerciseRepository) { + Optional aeolusTemplateService, ProfileService profileService, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.jenkinsServer = jenkinsServer; this.jenkinsBuildPlanService = jenkinsBuildPlanService; this.jenkinsJobService = jenkinsJobService; @@ -79,7 +80,7 @@ public JenkinsService(JenkinsServer jenkinsServer, @Qualifier("shortTimeoutJenki this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; this.aeolusTemplateService = aeolusTemplateService; this.profileService = profileService; - this.programmingExerciseRepository = programmingExerciseRepository; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } @Override @@ -99,7 +100,7 @@ public void recreateBuildPlansForExercise(ProgrammingExercise exercise) throws J deleteBuildPlan(projectKey, exercise.getTemplateBuildPlanId()); deleteBuildPlan(projectKey, exercise.getSolutionBuildPlanId()); - if (exercise.getBuildPlanConfiguration() != null) { + if (exercise.getBuildConfig().getBuildPlanConfiguration() != null) { resetCustomBuildPlanToTemplate(exercise); } @@ -118,16 +119,19 @@ private void resetCustomBuildPlanToTemplate(ProgrammingExercise exercise) throws } Windfile windfile = aeolusTemplateService.get().getDefaultWindfileFor(exercise); if (windfile != null) { - exercise.setBuildPlanConfiguration(mapper.writeValueAsString(windfile)); + exercise.getBuildConfig().setBuildPlanConfiguration(mapper.writeValueAsString(windfile)); } if (profileService.isAeolusActive()) { - programmingExerciseRepository.save(exercise); + programmingExerciseBuildConfigRepository.save(exercise.getBuildConfig()); } } @Override public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName, boolean targetProjectExists) { + // Make sure the build config is loaded + programmingExerciseBuildConfigRepository.loadAndSetBuildConfig(sourceExercise); + programmingExerciseBuildConfigRepository.loadAndSetBuildConfig(targetExercise); return jenkinsBuildPlanService.copyBuildPlan(sourceExercise, sourcePlanName, targetExercise, targetPlanName); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java index e68ae6c904e2..a97f293e1b53 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java @@ -37,6 +37,7 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.AeolusTarget; @@ -47,6 +48,7 @@ import de.tum.in.www1.artemis.exception.ContinuousIntegrationBuildPlanException; import de.tum.in.www1.artemis.exception.JenkinsException; import de.tum.in.www1.artemis.repository.BuildPlanRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusBuildPlanService; @@ -91,6 +93,8 @@ public class JenkinsBuildPlanService { private final ProgrammingExerciseRepository programmingExerciseRepository; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + private final BuildPlanRepository buildPlanRepository; private final Optional aeolusBuildPlanService; @@ -107,7 +111,8 @@ public class JenkinsBuildPlanService { public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate restTemplate, JenkinsServer jenkinsServer, JenkinsBuildPlanCreator jenkinsBuildPlanCreator, JenkinsJobService jenkinsJobService, JenkinsJobPermissionsService jenkinsJobPermissionsService, JenkinsInternalUrlService jenkinsInternalUrlService, UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator, - BuildPlanRepository buildPlanRepository, Optional aeolusBuildPlanService) { + BuildPlanRepository buildPlanRepository, Optional aeolusBuildPlanService, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.restTemplate = restTemplate; this.jenkinsServer = jenkinsServer; this.jenkinsBuildPlanCreator = jenkinsBuildPlanCreator; @@ -119,6 +124,7 @@ public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate re this.jenkinsPipelineScriptCreator = jenkinsPipelineScriptCreator; this.buildPlanRepository = buildPlanRepository; this.aeolusBuildPlanService = aeolusBuildPlanService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } /** @@ -130,11 +136,12 @@ public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate re */ public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUri repositoryUri) { final JenkinsXmlConfigBuilder.InternalVcsRepositoryURLs internalRepositoryUris = getInternalRepositoryUris(exercise, repositoryUri); + programmingExerciseBuildConfigRepository.loadAndSetBuildConfig(exercise); final ProgrammingLanguage programmingLanguage = exercise.getProgrammingLanguage(); final var configBuilder = builderFor(programmingLanguage, exercise.getProjectType()); final String buildPlanUrl = jenkinsPipelineScriptCreator.generateBuildPlanURL(exercise); - final boolean checkoutSolution = exercise.getCheckoutSolutionRepository(); + final boolean checkoutSolution = exercise.getBuildConfig().getCheckoutSolutionRepository(); final Document jobConfig = configBuilder.buildBasicConfig(programmingLanguage, Optional.ofNullable(exercise.getProjectType()), internalRepositoryUris, checkoutSolution, buildPlanUrl); @@ -142,7 +149,7 @@ public void createBuildPlanForExercise(ProgrammingExercise exercise, String plan String job = jobFolder + "-" + planKey; boolean couldCreateBuildPlan = false; - if (aeolusBuildPlanService.isPresent() && exercise.getBuildPlanConfiguration() != null) { + if (aeolusBuildPlanService.isPresent() && exercise.getBuildConfig().getBuildPlanConfiguration() != null) { var createdJob = createCustomAeolusBuildPlanForExercise(exercise, jobFolder + "/" + job, internalRepositoryUris.assignmentRepositoryUri(), internalRepositoryUris.testRepositoryUri(), internalRepositoryUris.solutionRepositoryUri()); couldCreateBuildPlan = createdJob != null; @@ -239,9 +246,9 @@ public void updateBuildPlanRepositories(String buildProjectKey, String buildPlan */ private void updateBuildPlanURLs(ProgrammingExercise templateExercise, ProgrammingExercise newExercise, Document jobConfig) { final Long previousExerciseId = templateExercise.getId(); - final String previousBuildPlanAccessSecret = templateExercise.getBuildPlanAccessSecret(); + final String previousBuildPlanAccessSecret = templateExercise.getBuildConfig().getBuildPlanAccessSecret(); final Long newExerciseId = newExercise.getId(); - final String newBuildPlanAccessSecret = newExercise.getBuildPlanAccessSecret(); + final String newBuildPlanAccessSecret = newExercise.getBuildConfig().getBuildPlanAccessSecret(); String toBeReplaced = String.format("/%d/build-plan?secret=%s", previousExerciseId, previousBuildPlanAccessSecret); String replacement = String.format("/%d/build-plan?secret=%s", newExerciseId, newBuildPlanAccessSecret); @@ -490,13 +497,14 @@ public void enablePlan(String projectKey, String planKey) { */ private String createCustomAeolusBuildPlanForExercise(ProgrammingExercise programmingExercise, String buildPlanId, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri) throws ContinuousIntegrationBuildPlanException { - if (aeolusBuildPlanService.isEmpty() || programmingExercise.getBuildPlanConfiguration() == null) { + if (aeolusBuildPlanService.isEmpty() || programmingExercise.getBuildConfig().getBuildPlanConfiguration() == null) { return null; } try { - Windfile windfile = programmingExercise.getWindfile(); + ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); + Windfile windfile = buildConfig.getWindfile(); Map repositories = aeolusBuildPlanService.get().createRepositoryMapForWindfile(programmingExercise.getProgrammingLanguage(), - programmingExercise.getBranch(), programmingExercise.getCheckoutSolutionRepository(), repositoryUri, testRepositoryUri, solutionRepositoryUri, List.of()); + buildConfig.getBranch(), buildConfig.getCheckoutSolutionRepository(), repositoryUri, testRepositoryUri, solutionRepositoryUri, List.of()); String resultHookUrl = artemisServerUrl + NEW_RESULT_RESOURCE_API_PATH; windfile.setPreProcessingMetadata(buildPlanId, programmingExercise.getProjectName(), this.vcsCredentials, resultHookUrl, "planDescription", repositories, diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java index 8dcc0183a33e..6e6f3efdfc94 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.exception.JenkinsException; import de.tum.in.www1.artemis.repository.BuildPlanRepository; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.service.ResourceLoaderService; import de.tum.in.www1.artemis.service.connectors.ci.AbstractBuildPlanCreator; @@ -42,9 +42,9 @@ public class JenkinsPipelineScriptCreator extends AbstractBuildPlanCreator { private final ProgrammingLanguageConfiguration programmingLanguageConfiguration; - public JenkinsPipelineScriptCreator(final BuildPlanRepository buildPlanRepository, final ProgrammingExerciseRepository programmingExerciseRepository, + public JenkinsPipelineScriptCreator(final BuildPlanRepository buildPlanRepository, final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, final ResourceLoaderService resourceLoaderService, final ProgrammingLanguageConfiguration programmingLanguageConfiguration) { - super(buildPlanRepository, programmingExerciseRepository); + super(buildPlanRepository, programmingExerciseBuildConfigRepository); this.resourceLoaderService = resourceLoaderService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; @@ -58,7 +58,7 @@ protected String generateDefaultBuildPlan(final ProgrammingExercise exercise) { final String pipelineScript = loadPipelineScript(exercise, projectType); final boolean isStaticCodeAnalysisEnabled = exercise.isStaticCodeAnalysisEnabled(); - final boolean isTestwiseCoverageAnalysisEnabled = exercise.isTestwiseCoverageEnabled(); + final boolean isTestwiseCoverageAnalysisEnabled = exercise.getBuildConfig().isTestwiseCoverageEnabled(); final var replacements = getReplacements(programmingLanguage, projectType, isStaticCodeAnalysisEnabled, isTestwiseCoverageAnalysisEnabled); return replaceVariablesInBuildPlanTemplate(replacements, pipelineScript); @@ -73,7 +73,7 @@ protected String generateDefaultBuildPlan(final ProgrammingExercise exercise) { */ private String loadPipelineScript(final ProgrammingExercise exercise, final Optional projectType) { final ProgrammingLanguage programmingLanguage = exercise.getProgrammingLanguage(); - final boolean isSequentialTestRuns = exercise.hasSequentialTestRuns(); + final boolean isSequentialTestRuns = exercise.getBuildConfig().hasSequentialTestRuns(); final Path pipelinePath = buildResourcePath(programmingLanguage, projectType, isSequentialTestRuns); final Resource resource = resourceLoaderService.getResource(pipelinePath); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildConfigurationService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildConfigurationService.java index fcea969b3b89..8a0f499466d8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildConfigurationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildConfigurationService.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.exception.LocalCIException; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusTemplateService; import de.tum.in.www1.artemis.service.connectors.aeolus.ScriptAction; @@ -29,17 +29,17 @@ public LocalCIBuildConfigurationService(AeolusTemplateService aeolusTemplateServ * Creates a build script for a given programming exercise. * The build script is used to build the programming exercise in a Docker container. * - * @param participation the participation for which to create the build script + * @param programmingExercise the programming exercise for which the build script should be created * @return the build script */ - public String createBuildScript(ProgrammingExerciseParticipation participation) { - ProgrammingExercise programmingExercise = participation.getProgrammingExercise(); + 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"); - String customScript = programmingExercise.getBuildScript(); + 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); @@ -47,7 +47,7 @@ public String createBuildScript(ProgrammingExerciseParticipation participation) else { List actions; - Windfile windfile = programmingExercise.getWindfile(); + Windfile windfile = buildConfig.getWindfile(); if (windfile == null) { windfile = aeolusTemplateService.getDefaultWindfileFor(programmingExercise); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java index 82af39694374..fd7e0cc0dc89 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java @@ -129,7 +129,7 @@ public void processResult() { BuildJob savedBuildJob; SecurityUtils.setAuthorizationObject(); - Optional participationOptional = participationRepository.findById(buildJob.participationId()); + Optional participationOptional = participationRepository.findWithProgrammingExerciseWithBuildConfigById(buildJob.participationId()); if (buildResult != null) { Result result = null; @@ -139,7 +139,7 @@ public void processResult() { // In case the participation does not contain the exercise, we have to load it from the database if (participation.getProgrammingExercise() == null) { - participation.setProgrammingExercise(programmingExerciseRepository.getProgrammingExerciseFromParticipationElseThrow(participation)); + participation.setProgrammingExercise(programmingExerciseRepository.getProgrammingExerciseWithBuildConfigFromParticipation(participation)); } result = programmingExerciseGradingService.processNewProgrammingExerciseResult(participation, buildResult); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultService.java index a4a7e9b32955..46ec59008473 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultService.java @@ -13,6 +13,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.exception.LocalCIException; import de.tum.in.www1.artemis.repository.BuildLogStatisticsEntryRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.service.connectors.ci.AbstractContinuousIntegrationResultService; import de.tum.in.www1.artemis.service.connectors.localci.dto.BuildResult; @@ -28,8 +29,9 @@ public class LocalCIResultService extends AbstractContinuousIntegrationResultService { public LocalCIResultService(TestwiseCoverageService testwiseCoverageService, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, - ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseTestCaseRepository testCaseRepository) { - super(testCaseRepository, buildLogStatisticsEntryRepository, testwiseCoverageService, feedbackCreationService); + ProgrammingExerciseFeedbackCreationService feedbackCreationService, ProgrammingExerciseTestCaseRepository testCaseRepository, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + super(testCaseRepository, buildLogStatisticsEntryRepository, testwiseCoverageService, feedbackCreationService, programmingExerciseBuildConfigRepository); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java index c06b74aa03c9..8f3b9c279d83 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java @@ -17,11 +17,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.LocalCIException; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.service.connectors.BuildScriptProviderService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusTemplateService; @@ -47,16 +48,16 @@ public class LocalCIService extends AbstractContinuousIntegrationService { private final AeolusTemplateService aeolusTemplateService; - private final ProgrammingExerciseRepository programmingExerciseRepository; - private final SharedQueueManagementService sharedQueueManagementService; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + public LocalCIService(BuildScriptProviderService buildScriptProviderService, AeolusTemplateService aeolusTemplateService, - ProgrammingExerciseRepository programmingExerciseRepository, SharedQueueManagementService sharedQueueManagementService) { + SharedQueueManagementService sharedQueueManagementService, ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.buildScriptProviderService = buildScriptProviderService; this.aeolusTemplateService = aeolusTemplateService; - this.programmingExerciseRepository = programmingExerciseRepository; this.sharedQueueManagementService = sharedQueueManagementService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } @Override @@ -78,10 +79,11 @@ public void recreateBuildPlansForExercise(ProgrammingExercise exercise) throws J } String script = buildScriptProviderService.getScriptFor(exercise); Windfile windfile = aeolusTemplateService.getDefaultWindfileFor(exercise); - exercise.setBuildScript(script); - exercise.setBuildPlanConfiguration(new ObjectMapper().writeValueAsString(windfile)); - // recreating the build plans for the exercise means we need to store the updated exercise in the database - programmingExerciseRepository.save(exercise); + ProgrammingExerciseBuildConfig buildConfig = exercise.getBuildConfig(); + buildConfig.setBuildScript(script); + buildConfig.setBuildPlanConfiguration(new ObjectMapper().writeValueAsString(windfile)); + // recreating the build plans for the exercise means we need to store the updated build config in the database + programmingExerciseBuildConfigRepository.save(buildConfig); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java index b9de0cb90718..8bca6ec61ec4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java @@ -25,6 +25,7 @@ import de.tum.in.www1.artemis.config.ProgrammingLanguageConfiguration; import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.enumeration.ProjectType; @@ -34,6 +35,7 @@ import de.tum.in.www1.artemis.exception.LocalCIException; import de.tum.in.www1.artemis.exception.localvc.LocalVCInternalException; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository; import de.tum.in.www1.artemis.service.ExerciseDateService; import de.tum.in.www1.artemis.service.connectors.GitService; @@ -87,6 +89,8 @@ public class LocalCITriggerService implements ContinuousIntegrationTriggerServic private final GitService gitService; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + private IQueue queue; private IMap dockerImageCleanupInfo; @@ -97,7 +101,8 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h ProgrammingLanguageConfiguration programmingLanguageConfiguration, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, LocalCIProgrammingLanguageFeatureService programmingLanguageFeatureService, Optional versionControlService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, - LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService) { + LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.hazelcastInstance = hazelcastInstance; this.aeolusTemplateService = aeolusTemplateService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; @@ -107,6 +112,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h this.solutionProgrammingExerciseParticipationRepository = solutionProgrammingExerciseParticipationRepository; this.localCIBuildConfigurationService = localCIBuildConfigurationService; this.gitService = gitService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.exerciseDateService = exerciseDateService; } @@ -178,9 +184,11 @@ else if (triggeredByPushTo.equals(RepositoryType.TESTS)) { JobTimingInfo jobTimingInfo = new JobTimingInfo(submissionDate, null, null); - RepositoryInfo repositoryInfo = getRepositoryInfo(participation, triggeredByPushTo); + var programmingExerciseBuildConfig = loadBuildConfig(programmingExercise); + + RepositoryInfo repositoryInfo = getRepositoryInfo(participation, triggeredByPushTo, programmingExerciseBuildConfig); - BuildConfig buildConfig = getBuildConfig(participation, commitHashToBuild, assignmentCommitHash, testCommitHash); + BuildConfig buildConfig = getBuildConfig(participation, commitHashToBuild, assignmentCommitHash, testCommitHash, programmingExerciseBuildConfig); BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), null, participation.getId(), courseId, programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null); @@ -208,7 +216,7 @@ private List getTestResultPaths(Windfile windfile) throws IllegalArgumen * @param triggeredByPushTo type of the repository that was pushed to and triggered the build job * @return the repository information for the given participation */ - private RepositoryInfo getRepositoryInfo(ProgrammingExerciseParticipation participation, RepositoryType triggeredByPushTo) { + private RepositoryInfo getRepositoryInfo(ProgrammingExerciseParticipation participation, RepositoryType triggeredByPushTo, ProgrammingExerciseBuildConfig buildConfig) { ProgrammingExercise programmingExercise = participation.getProgrammingExercise(); @@ -228,7 +236,7 @@ private RepositoryInfo getRepositoryInfo(ProgrammingExerciseParticipation partic String[] auxiliaryRepositoryUris = auxiliaryRepositories.stream().map(AuxiliaryRepository::getRepositoryUri).toArray(String[]::new); String[] auxiliaryRepositoryCheckoutDirectories1 = auxiliaryRepositories.stream().map(AuxiliaryRepository::getCheckoutDirectory).toArray(String[]::new); - if (programmingExercise.getCheckoutSolutionRepository()) { + if (buildConfig.getCheckoutSolutionRepository()) { ProgrammingLanguageFeature programmingLanguageFeature = programmingLanguageFeatureService.getProgrammingLanguageFeatures(programmingExercise.getProgrammingLanguage()); if (programmingLanguageFeature.checkoutSolutionRepositoryAllowed()) { var solutionParticipation = solutionProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(participation.getProgrammingExercise().getId()); @@ -264,7 +272,8 @@ else if (repositoryTypeOrUserName.equals("solution")) { } - private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participation, String commitHashToBuild, String assignmentCommitHash, String testCommitHash) { + private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participation, String commitHashToBuild, String assignmentCommitHash, String testCommitHash, + ProgrammingExerciseBuildConfig buildConfig) { String branch; try { branch = versionControlService.orElseThrow().getOrRetrieveBranchOfParticipation(participation); @@ -277,13 +286,13 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio ProgrammingLanguage programmingLanguage = programmingExercise.getProgrammingLanguage(); ProjectType projectType = programmingExercise.getProjectType(); boolean staticCodeAnalysisEnabled = programmingExercise.isStaticCodeAnalysisEnabled(); - boolean sequentialTestRunsEnabled = programmingExercise.hasSequentialTestRuns(); - boolean testwiseCoverageEnabled = programmingExercise.isTestwiseCoverageEnabled(); + boolean sequentialTestRunsEnabled = buildConfig.hasSequentialTestRuns(); + boolean testwiseCoverageEnabled = buildConfig.isTestwiseCoverageEnabled(); Windfile windfile; String dockerImage; try { - windfile = programmingExercise.getWindfile(); + windfile = buildConfig.getWindfile(); dockerImage = windfile.getMetadata().docker().getFullImageName(); } catch (NullPointerException e) { @@ -295,12 +304,17 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio List resultPaths = getTestResultPaths(windfile); // 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. - String buildScript = localCIBuildConfigurationService.createBuildScript(participation); + programmingExercise.setBuildConfig(buildConfig); + String buildScript = localCIBuildConfigurationService.createBuildScript(programmingExercise); return new BuildConfig(buildScript, dockerImage, commitHashToBuild, assignmentCommitHash, testCommitHash, branch, programmingLanguage, projectType, staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths); } + private ProgrammingExerciseBuildConfig loadBuildConfig(ProgrammingExercise programmingExercise) { + return programmingExerciseBuildConfigRepository.getProgrammingExerciseBuildConfigElseThrow(programmingExercise); + } + /** * Determines the priority of the build job. * Lower values indicate higher priority. diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java index b33ab1d53e9b..8233efc27944 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java @@ -37,6 +37,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.localvc.LocalVCInternalException; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; @@ -66,8 +67,10 @@ public void setLocalVCBasePath(String localVCBasePath) { } public LocalVCService(UriService uriService, GitService gitService, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, - ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { - super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); + ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { + super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository, + programmingExerciseBuildConfigRepository); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java index fcb98a759d79..9d04a9650407 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java @@ -19,6 +19,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.VersionControlException; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; @@ -43,15 +44,19 @@ public abstract class AbstractVersionControlService implements VersionControlSer protected final ProgrammingExerciseRepository programmingExerciseRepository; + protected final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + protected final TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository; public AbstractVersionControlService(GitService gitService, UriService uriService, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, - ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { + ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.gitService = gitService; this.uriService = uriService; this.studentParticipationRepository = studentParticipationRepository; this.programmingExerciseRepository = programmingExerciseRepository; this.templateProgrammingExerciseParticipationRepository = templateProgrammingExerciseParticipationRepository; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } /** @@ -178,16 +183,18 @@ public String getOrRetrieveBranchOfStudentParticipation(ProgrammingExerciseStude @Override public String getOrRetrieveBranchOfExercise(ProgrammingExercise programmingExercise) { - if (programmingExercise.getBranch() == null) { + programmingExerciseBuildConfigRepository.loadAndSetBuildConfig(programmingExercise); + + if (programmingExercise.getBuildConfig().getBranch() == null) { if (!Hibernate.isInitialized(programmingExercise.getTemplateParticipation())) { programmingExercise.setTemplateParticipation(templateProgrammingExerciseParticipationRepository.findByProgrammingExerciseIdElseThrow(programmingExercise.getId())); } String branch = getDefaultBranchOfRepository(programmingExercise.getVcsTemplateRepositoryUri()); - programmingExercise.setBranch(branch); - programmingExerciseRepository.save(programmingExercise); + programmingExercise.getBuildConfig().setBranch(branch); + programmingExerciseBuildConfigRepository.save(programmingExercise.getBuildConfig()); } - return programmingExercise.getBranch(); + return programmingExercise.getBuildConfig().getBranch(); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java index 2ef4451c4045..4ba3b08130fc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java @@ -309,7 +309,7 @@ private void addExercisesToExerciseGroup(ExerciseGroup exerciseGroupToCopy, Exer case PROGRAMMING -> { final Optional optionalOriginalProgrammingExercise = programmingExerciseRepository - .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos(exerciseToCopy.getId()); + .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig(exerciseToCopy.getId()); if (optionalOriginalProgrammingExercise.isEmpty()) { yield Optional.empty(); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java index 082b11632ea9..785855686a2b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java @@ -82,7 +82,7 @@ public BehavioralTestCaseService(GitService gitService, RepositoryService reposi * @throws BehavioralSolutionEntryGenerationException If there was an error while generating the solution entries */ public List generateBehavioralSolutionEntries(ProgrammingExercise programmingExercise) throws BehavioralSolutionEntryGenerationException { - if (!programmingExercise.isTestwiseCoverageEnabled()) { + if (!programmingExercise.getBuildConfig().isTestwiseCoverageEnabled()) { throw new BehavioralSolutionEntryGenerationException("This feature is only supported for Java Exercises with active Testwise Coverage"); } var testCases = testCaseRepository.findByExerciseIdWithSolutionEntriesAndActive(programmingExercise.getId(), true); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java index 223bf6a42a7a..a4ea308c262a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java @@ -79,7 +79,7 @@ public JavaTemplateUpgradeService(ProgrammingExerciseRepositoryService programmi @Override public void upgradeTemplate(ProgrammingExercise exercise) { // TODO: Support sequential test runs - if (exercise.hasSequentialTestRuns()) { + if (exercise.getBuildConfig().hasSequentialTestRuns()) { return; } // Template and solution repository can also contain a project object model for some project types diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java index 85d0dc0b2f61..34ea5b9fc8c4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java @@ -17,6 +17,7 @@ import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.StaticCodeAnalysisCategory; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; @@ -27,6 +28,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.submissionpolicy.SubmissionPolicy; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.StaticCodeAnalysisCategoryRepository; @@ -58,6 +60,8 @@ public class ProgrammingExerciseImportBasicService { private final ProgrammingExerciseRepository programmingExerciseRepository; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + private final ProgrammingExerciseService programmingExerciseService; private final StaticCodeAnalysisService staticCodeAnalysisService; @@ -80,7 +84,8 @@ public ProgrammingExerciseImportBasicService(ExerciseHintService exerciseHintSer ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseService programmingExerciseService, StaticCodeAnalysisService staticCodeAnalysisService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, SubmissionPolicyRepository submissionPolicyRepository, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ProgrammingExerciseTaskService programmingExerciseTaskService, - ProgrammingExerciseSolutionEntryRepository solutionEntryRepository, ChannelService channelService) { + ProgrammingExerciseSolutionEntryRepository solutionEntryRepository, ChannelService channelService, + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository) { this.exerciseHintService = exerciseHintService; this.exerciseHintRepository = exerciseHintRepository; this.versionControlService = versionControlService; @@ -96,6 +101,7 @@ public ProgrammingExerciseImportBasicService(ExerciseHintService exerciseHintSer this.programmingExerciseTaskService = programmingExerciseTaskService; this.solutionEntryRepository = solutionEntryRepository; this.channelService = channelService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; } /** @@ -127,14 +133,16 @@ public ProgrammingExercise importProgrammingExerciseBasis(final ProgrammingExerc setupTestRepository(newProgrammingExercise); programmingExerciseService.initParticipations(newProgrammingExercise); - if (newProgrammingExercise.getBuildPlanConfiguration() == null) { + newProgrammingExercise.getBuildConfig().setBranch(versionControlService.orElseThrow().getDefaultBranchOfArtemis()); + if (newProgrammingExercise.getBuildConfig().getBuildPlanConfiguration() == null) { // this means the user did not override the build plan config when importing the exercise and want to reuse it from the existing exercise - newProgrammingExercise.setBuildPlanConfiguration(originalProgrammingExercise.getBuildPlanConfiguration()); + newProgrammingExercise.getBuildConfig().setBuildPlanConfiguration(originalProgrammingExercise.getBuildConfig().getBuildPlanConfiguration()); } // Hints, tasks, test cases and static code analysis categories final Map newHintIdByOldId = exerciseHintService.copyExerciseHints(originalProgrammingExercise, newProgrammingExercise); + newProgrammingExercise.setBuildConfig(programmingExerciseBuildConfigRepository.save(newProgrammingExercise.getBuildConfig())); final ProgrammingExercise importedExercise = programmingExerciseRepository.save(newProgrammingExercise); final Map newTestCaseIdByOldId = importTestCases(originalProgrammingExercise, importedExercise); @@ -192,12 +200,11 @@ else if (Boolean.TRUE.equals(importedExercise.isStaticCodeAnalysisEnabled()) && private void prepareBasicExerciseInformation(final ProgrammingExercise originalProgrammingExercise, final ProgrammingExercise newProgrammingExercise) { // Set values we don't want to copy to null setupExerciseForImport(newProgrammingExercise); + setupBuildConfig(newProgrammingExercise, originalProgrammingExercise); - if (originalProgrammingExercise.hasBuildPlanAccessSecretSet()) { - newProgrammingExercise.generateAndSetBuildPlanAccessSecret(); + if (originalProgrammingExercise.getBuildConfig().hasBuildPlanAccessSecretSet()) { + newProgrammingExercise.getBuildConfig().generateAndSetBuildPlanAccessSecret(); } - - newProgrammingExercise.setBranch(versionControlService.orElseThrow().getDefaultBranchOfArtemis()); } /** @@ -211,6 +218,22 @@ private void setupTestRepository(ProgrammingExercise newExercise) { newExercise.setTestRepositoryUri(versionControlService.orElseThrow().getCloneRepositoryUri(newExercise.getProjectKey(), testRepoName).toString()); } + private void setupBuildConfig(ProgrammingExercise newExercise, ProgrammingExercise originalExercise) { + if (newExercise.getBuildConfig() != null) { + var buildConfig = newExercise.getBuildConfig(); + buildConfig.setId(null); + buildConfig.setProgrammingExercise(null); + newExercise.setBuildConfig(buildConfig); + } + else if (originalExercise.getBuildConfig() != null) { + var buildConfig = new ProgrammingExerciseBuildConfig(originalExercise.getBuildConfig()); + newExercise.setBuildConfig(buildConfig); + } + else { + newExercise.setBuildConfig(new ProgrammingExerciseBuildConfig()); + } + } + /** * Persists the submission policy of the new exercise. We ensure that the submission policy does not * have any id or programming exercise set. diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java index 2f219a695cc6..c4a771825205 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java @@ -387,9 +387,9 @@ private void setupJVMTestTemplateAndPush(final RepositoryResources resources, fi // Keep or delete static code analysis configuration in the build configuration file sectionsMap.put("static-code-analysis", Boolean.TRUE.equals(programmingExercise.isStaticCodeAnalysisEnabled())); // Keep or delete testwise coverage configuration in the build file - sectionsMap.put("record-testwise-coverage", Boolean.TRUE.equals(programmingExercise.isTestwiseCoverageEnabled())); + sectionsMap.put("record-testwise-coverage", Boolean.TRUE.equals(programmingExercise.getBuildConfig().isTestwiseCoverageEnabled())); - if (programmingExercise.hasSequentialTestRuns()) { + if (programmingExercise.getBuildConfig().hasSequentialTestRuns()) { setupTestTemplateSequentialTestRuns(resources, templatePath, projectTemplatePath, projectType, sectionsMap); } else { @@ -664,7 +664,7 @@ 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.hasSequentialTestRuns() ? "pom" : "jar"); + replacements.put("${packaging}", programmingExercise.getBuildConfig().hasSequentialTestRuns() ? "pom" : "jar"); fileService.replaceVariablesInFileRecursive(repository.getLocalPath().toAbsolutePath(), replacements, List.of("gradle-wrapper.jar")); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java index 74c80e5056cf..973f049b2e4d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java @@ -45,6 +45,7 @@ import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseBuildConfig; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.Repository; import de.tum.in.www1.artemis.domain.User; @@ -59,6 +60,7 @@ import de.tum.in.www1.artemis.domain.participation.TemplateProgrammingExerciseParticipation; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.repository.ParticipationRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.ResultRepository; @@ -120,6 +122,8 @@ public class ProgrammingExerciseService { private final ProgrammingExerciseRepository programmingExerciseRepository; + private final ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + private final GitService gitService; private final Optional versionControlService; @@ -196,7 +200,7 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc ProgrammingSubmissionService programmingSubmissionService, Optional irisSettingsService, Optional aeolusTemplateService, Optional buildScriptGenerationService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProfileService profileService, ExerciseService exerciseService, - CompetencyProgressService competencyProgressService) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, CompetencyProgressService competencyProgressService) { this.programmingExerciseRepository = programmingExerciseRepository; this.gitService = gitService; this.versionControlService = versionControlService; @@ -228,6 +232,7 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; this.profileService = profileService; this.exerciseService = exerciseService; + this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.competencyProgressService = competencyProgressService; } @@ -264,13 +269,17 @@ public ProgrammingExercise createProgrammingExercise(ProgrammingExercise program // See https://github.com/ls1intum/Artemis/pull/7451/files#r1459228917 programmingExercise.setSolutionParticipation(null); programmingExercise.setTemplateParticipation(null); + programmingExercise.getBuildConfig().setId(null); // We save once in order to generate an id for the programming exercise + var savedBuildConfig = programmingExerciseBuildConfigRepository.saveAndFlush(programmingExercise.getBuildConfig()); + programmingExercise.setBuildConfig(savedBuildConfig); var savedProgrammingExercise = programmingExerciseRepository.saveForCreation(programmingExercise); - + savedProgrammingExercise.getBuildConfig().setProgrammingExercise(savedProgrammingExercise); + programmingExerciseBuildConfigRepository.save(savedProgrammingExercise.getBuildConfig()); // Step 1: Setting constant facts for a programming exercise savedProgrammingExercise.generateAndSetProjectKey(); - savedProgrammingExercise.setBranch(versionControl.getDefaultBranchOfArtemis()); + savedProgrammingExercise.getBuildConfig().setBranch(versionControl.getDefaultBranchOfArtemis()); // Step 2: Creating repositories for new exercise programmingExerciseRepositoryService.createRepositoriesForNewExercise(savedProgrammingExercise); @@ -283,6 +292,7 @@ public ProgrammingExercise createProgrammingExercise(ProgrammingExercise program // Step 4b: Connecting base participations with the exercise connectBaseParticipationsToExerciseAndSave(savedProgrammingExercise); + programmingExerciseBuildConfigRepository.saveAndFlush(savedProgrammingExercise.getBuildConfig()); savedProgrammingExercise = programmingExerciseRepository.saveForCreation(savedProgrammingExercise); // Step 4c: Connect auxiliary repositories @@ -299,11 +309,11 @@ public ProgrammingExercise createProgrammingExercise(ProgrammingExercise program // Step 8: For LocalCI and Aeolus, we store the build plan definition in the database as a windfile, we don't do that for Jenkins as // we want to use the default approach of Jenkinsfiles and Build Plans if no customizations are made - if (aeolusTemplateService.isPresent() && savedProgrammingExercise.getBuildPlanConfiguration() == null && !profileService.isJenkinsActive()) { + if (aeolusTemplateService.isPresent() && savedProgrammingExercise.getBuildConfig().getBuildPlanConfiguration() == null && !profileService.isJenkinsActive()) { Windfile windfile = aeolusTemplateService.get().getDefaultWindfileFor(savedProgrammingExercise); if (windfile != null) { - savedProgrammingExercise.setBuildPlanConfiguration(new ObjectMapper().writeValueAsString(windfile)); - savedProgrammingExercise = programmingExerciseRepository.saveForCreation(savedProgrammingExercise); + savedProgrammingExercise.getBuildConfig().setBuildPlanConfiguration(new ObjectMapper().writeValueAsString(windfile)); + programmingExerciseBuildConfigRepository.saveAndFlush(savedProgrammingExercise.getBuildConfig()); } else { log.warn("No windfile for the settings of exercise {}", savedProgrammingExercise.getId()); @@ -364,13 +374,15 @@ public void validateNewProgrammingExerciseSettings(ProgrammingExercise programmi validatePackageName(programmingExercise, programmingLanguageFeature); validateProjectType(programmingExercise, programmingLanguageFeature); + ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); + // Check if checkout solution repository is enabled - if (programmingExercise.getCheckoutSolutionRepository() && !programmingLanguageFeature.checkoutSolutionRepositoryAllowed()) { + if (buildConfig.getCheckoutSolutionRepository() && !programmingLanguageFeature.checkoutSolutionRepositoryAllowed()) { throw new BadRequestAlertException("Checkout solution repository is not supported for this programming language", "Exercise", "checkoutSolutionRepositoryNotSupported"); } // Check if testwise coverage analysis is enabled - if (Boolean.TRUE.equals(programmingExercise.isTestwiseCoverageEnabled()) && !programmingLanguageFeature.testwiseCoverageAnalysisSupported()) { + if (Boolean.TRUE.equals(buildConfig.isTestwiseCoverageEnabled()) && !programmingLanguageFeature.testwiseCoverageAnalysisSupported()) { throw new BadRequestAlertException("Testwise coverage analysis is not supported for this language", "Exercise", "testwiseCoverageAnalysisNotSupported"); } @@ -460,12 +472,12 @@ public void setupBuildPlansForNewExercise(ProgrammingExercise programmingExercis giveCIProjectPermissions(programmingExercise); - Windfile windfile = programmingExercise.getWindfile(); - if (windfile != null && buildScriptGenerationService.isPresent() && programmingExercise.getBuildScript() == null) { + Windfile windfile = programmingExercise.getBuildConfig().getWindfile(); + if (windfile != null && buildScriptGenerationService.isPresent() && programmingExercise.getBuildConfig().getBuildScript() == null) { String script = buildScriptGenerationService.get().getScript(programmingExercise); - programmingExercise.setBuildPlanConfiguration(new ObjectMapper().writeValueAsString(windfile)); - programmingExercise.setBuildScript(script); - programmingExercise = programmingExerciseRepository.saveForCreation(programmingExercise); + programmingExercise.getBuildConfig().setBuildPlanConfiguration(new ObjectMapper().writeValueAsString(windfile)); + programmingExercise.getBuildConfig().setBuildScript(script); + programmingExerciseBuildConfigRepository.saveAndFlush(programmingExercise.getBuildConfig()); } // if the exercise is imported from a file, the changes fixing the project name will trigger a first build anyway, so @@ -555,6 +567,7 @@ public ProgrammingExercise updateProgrammingExercise(ProgrammingExercise program String problemStatementWithTestNames = updatedProgrammingExercise.getProblemStatement(); programmingExerciseTaskService.replaceTestNamesWithIds(updatedProgrammingExercise); + programmingExerciseBuildConfigRepository.save(updatedProgrammingExercise.getBuildConfig()); ProgrammingExercise savedProgrammingExercise = programmingExerciseRepository.save(updatedProgrammingExercise); // The returned value should use test case names since it gets send back to the client savedProgrammingExercise.setProblemStatement(problemStatementWithTestNames); @@ -581,13 +594,13 @@ public ProgrammingExercise updateProgrammingExercise(ProgrammingExercise program * @param updatedProgrammingExercise the changed programming exercise with its new values */ private void updateBuildPlanForExercise(ProgrammingExercise programmingExerciseBeforeUpdate, ProgrammingExercise updatedProgrammingExercise) throws JsonProcessingException { - if (continuousIntegrationService.isEmpty() - || Objects.equals(programmingExerciseBeforeUpdate.getBuildPlanConfiguration(), updatedProgrammingExercise.getBuildPlanConfiguration())) { + if (continuousIntegrationService.isEmpty() || Objects.equals(programmingExerciseBeforeUpdate.getBuildConfig().getBuildPlanConfiguration(), + updatedProgrammingExercise.getBuildConfig().getBuildPlanConfiguration())) { return; } // we only update the build plan configuration if it has changed and is not null, otherwise we // do not have a valid exercise anymore - if (updatedProgrammingExercise.getBuildPlanConfiguration() != null) { + if (updatedProgrammingExercise.getBuildConfig().getBuildPlanConfiguration() != null) { if (!profileService.isLocalCiActive()) { continuousIntegrationService.get().deleteProject(updatedProgrammingExercise.getProjectKey()); continuousIntegrationService.get().createProjectForExercise(updatedProgrammingExercise); @@ -598,13 +611,13 @@ private void updateBuildPlanForExercise(ProgrammingExercise programmingExerciseB // We skip this for pure LocalCI to prevent the build script from being overwritten by the default one. if (profileService.isAeolusActive() && buildScriptGenerationService.isPresent()) { String script = buildScriptGenerationService.get().getScript(updatedProgrammingExercise); - updatedProgrammingExercise.setBuildScript(script); - programmingExerciseRepository.save(updatedProgrammingExercise); + updatedProgrammingExercise.getBuildConfig().setBuildScript(script); + programmingExerciseBuildConfigRepository.save(updatedProgrammingExercise.getBuildConfig()); } } else { // if the user does not change the build plan configuration, we have to set the old one again - updatedProgrammingExercise.setBuildPlanConfiguration(programmingExerciseBeforeUpdate.getBuildPlanConfiguration()); + updatedProgrammingExercise.getBuildConfig().setBuildPlanConfiguration(programmingExerciseBeforeUpdate.getBuildConfig().getBuildPlanConfiguration()); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingTriggerService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingTriggerService.java index 20827c3db61c..901f25d03311 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingTriggerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingTriggerService.java @@ -108,7 +108,7 @@ public ProgrammingTriggerService(ProgrammingSubmissionRepository programmingSubm */ public void setTestCasesChangedAndTriggerTestCaseUpdate(long programmingExerciseId) throws EntityNotFoundException { setTestCasesChanged(programmingExerciseId, true); - var programmingExercise = programmingExerciseRepository.findWithTemplateAndSolutionParticipationById(programmingExerciseId).orElseThrow(); + var programmingExercise = programmingExerciseRepository.findWithTemplateAndSolutionParticipationAndBuildConfigById(programmingExerciseId).orElseThrow(); try { ContinuousIntegrationTriggerService ciTriggerService = continuousIntegrationTriggerService.orElseThrow(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/ProgrammingExerciseSolutionEntryResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/ProgrammingExerciseSolutionEntryResource.java index 2156f21fce93..2464ceb593a5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/ProgrammingExerciseSolutionEntryResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/ProgrammingExerciseSolutionEntryResource.java @@ -310,7 +310,7 @@ public ResponseEntity> createStructuralSo @EnforceAtLeastEditor public ResponseEntity> createBehavioralSolutionEntries(@PathVariable Long exerciseId) { log.debug("REST request to create behavioral solution entries"); - ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); + ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, exercise, null); try { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicBuildPlanResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicBuildPlanResource.java index 316b179cf005..134d51dc8a68 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicBuildPlanResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicBuildPlanResource.java @@ -43,12 +43,12 @@ public PublicBuildPlanResource(BuildPlanRepository buildPlanRepository) { public ResponseEntity getBuildPlan(@PathVariable Long exerciseId, @RequestParam("secret") String secret) { log.debug("REST request to get build plan for programming exercise with id {}", exerciseId); - final BuildPlan buildPlan = buildPlanRepository.findByProgrammingExercises_IdWithProgrammingExercisesElseThrow(exerciseId); + final BuildPlan buildPlan = buildPlanRepository.findByProgrammingExercises_IdWithProgrammingExercisesWithBuildConfigElseThrow(exerciseId); // orElseThrow is safe here since the query above ensures that we find a build plan that is attached to that exercise final ProgrammingExercise programmingExercise = buildPlan.getProgrammingExerciseById(exerciseId) .orElseThrow(() -> new EntityNotFoundException("Could not find connected exercise for build plan.")); - if (!programmingExercise.hasBuildPlanAccessSecretSet() || !secret.equals(programmingExercise.getBuildPlanAccessSecret())) { + if (!programmingExercise.getBuildConfig().hasBuildPlanAccessSecretSet() || !secret.equals(programmingExercise.getBuildConfig().getBuildPlanAccessSecret())) { throw new AccessForbiddenException(); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicResultResource.java index 1286edf0e8e5..bca3e3378b29 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicResultResource.java @@ -127,7 +127,7 @@ public ResponseEntity processNewProgrammingExerciseResult(@RequestHeader(" triggerTemplateBuildIfTestCasesChanged(participation.getProgrammingExercise().getId(), programmingSubmission); // the test cases and the submission have been saved to the database previously, therefore we can add the reference to the coverage reports - if (Boolean.TRUE.equals(participation.getProgrammingExercise().isTestwiseCoverageEnabled()) && Boolean.TRUE.equals(result.isSuccessful())) { + if (Boolean.TRUE.equals(participation.getProgrammingExercise().getBuildConfig().isTestwiseCoverageEnabled()) && Boolean.TRUE.equals(result.isSuccessful())) { testwiseCoverageService.createTestwiseCoverageReport(result.getCoverageFileReportsByTestCaseName(), participation.getProgrammingExercise(), programmingSubmission); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java index 0d31894553a3..692ad07f381c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java @@ -209,7 +209,7 @@ public ResponseEntity importProgrammingExercise(@PathVariab programmingExerciseRepository.validateCourseSettings(newExercise, course); final var originalProgrammingExercise = programmingExerciseRepository - .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos(sourceExerciseId) + .findByIdWithEagerBuildConfigTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxRepos(sourceExerciseId) .orElseThrow(() -> new EntityNotFoundException("ProgrammingExercise", sourceExerciseId)); var consistencyErrors = consistencyCheckService.checkConsistencyOfProgrammingExercise(originalProgrammingExercise); @@ -319,7 +319,7 @@ public ResponseEntity importProgrammingExerciseFromFile(@Pa @EnforceAtLeastInstructor @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) public ResponseEntity exportInstructorExercise(@PathVariable long exerciseId) throws IOException { - var programmingExercise = programmingExerciseRepository.findByIdWithPlagiarismDetectionConfigAndTeamConfigElseThrow(exerciseId); + var programmingExercise = programmingExerciseRepository.findByIdWithPlagiarismDetectionConfigTeamConfigAndBuildConfigElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, programmingExercise, null); long start = System.nanoTime(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java index c7779a3156ab..180d2ca8e077 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java @@ -291,14 +291,16 @@ public ResponseEntity updateProgrammingExercise(@RequestBod checkProgrammingExerciseForError(updatedProgrammingExercise); - var programmingExerciseBeforeUpdate = programmingExerciseRepository.findByIdWithAuxiliaryRepositoriesAndCompetenciesElseThrow(updatedProgrammingExercise.getId()); + var programmingExerciseBeforeUpdate = programmingExerciseRepository + .findByIdWithAuxiliaryRepositoriesCompetenciesAndBuildConfigElseThrow(updatedProgrammingExercise.getId()); if (!Objects.equals(programmingExerciseBeforeUpdate.getShortName(), updatedProgrammingExercise.getShortName())) { throw new BadRequestAlertException("The programming exercise short name cannot be changed", ENTITY_NAME, "shortNameCannotChange"); } if (!Objects.equals(programmingExerciseBeforeUpdate.isStaticCodeAnalysisEnabled(), updatedProgrammingExercise.isStaticCodeAnalysisEnabled())) { throw new BadRequestAlertException("Static code analysis enabled flag must not be changed", ENTITY_NAME, "staticCodeAnalysisCannotChange"); } - if (!Objects.equals(programmingExerciseBeforeUpdate.isTestwiseCoverageEnabled(), updatedProgrammingExercise.isTestwiseCoverageEnabled())) { + if (!Objects.equals(programmingExerciseBeforeUpdate.getBuildConfig().isTestwiseCoverageEnabled(), + updatedProgrammingExercise.getBuildConfig().isTestwiseCoverageEnabled())) { throw new BadRequestAlertException("Testwise coverage enabled flag must not be changed", ENTITY_NAME, "testwiseCoverageCannotChange"); } if (!Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOnlineEditor()) && !Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOfflineIde())) { @@ -320,7 +322,7 @@ public ResponseEntity updateProgrammingExercise(@RequestBod athenaModuleService.ifPresent(ams -> ams.checkValidAthenaModuleChange(programmingExerciseBeforeUpdate, updatedProgrammingExercise, ENTITY_NAME)); // Ignore changes to the default branch - updatedProgrammingExercise.setBranch(programmingExerciseBeforeUpdate.getBranch()); + updatedProgrammingExercise.getBuildConfig().setBranch(programmingExerciseBeforeUpdate.getBuildConfig().getBranch()); if (updatedProgrammingExercise.getAuxiliaryRepositories() == null) { // make sure the default value is set properly @@ -423,11 +425,11 @@ public ResponseEntity> getProgrammingExercisesForCours private ProgrammingExercise findProgrammingExercise(Long exerciseId, boolean includePlagiarismDetectionConfig) { if (includePlagiarismDetectionConfig) { var programmingExercise = programmingExerciseRepository - .findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigElseThrow(exerciseId); + .findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesAndPlagiarismDetectionConfigAndBuildConfigElseThrow(exerciseId); PlagiarismDetectionConfigHelper.createAndSaveDefaultIfNullAndCourseExercise(programmingExercise, programmingExerciseRepository); return programmingExercise; } - return programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesElseThrow(exerciseId); + return programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesCompetenciesAndBuildConfigElseThrow(exerciseId); } /** @@ -566,7 +568,7 @@ public ResponseEntity combineTemplateRepositoryCommits(@PathVariable long @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity generateStructureOracleForExercise(@PathVariable long exerciseId) { log.debug("REST request to generate the structure oracle for ProgrammingExercise with id: {}", exerciseId); - var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesElseThrow(exerciseId); + var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigElseThrow(exerciseId); User user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, user); if (programmingExercise.getPackageName() == null || programmingExercise.getPackageName().length() < 3) { @@ -581,7 +583,7 @@ public ResponseEntity generateStructureOracleForExercise(@PathVariable l try { String testsPath = Path.of("test", programmingExercise.getPackageFolderName()).toString(); // Atm we only have one folder that can have structural tests, but this could change. - testsPath = programmingExercise.hasSequentialTestRuns() ? Path.of("structural", testsPath).toString() : testsPath; + testsPath = programmingExercise.getBuildConfig().hasSequentialTestRuns() ? Path.of("structural", testsPath).toString() : testsPath; boolean didGenerateOracle = programmingExerciseService.generateStructureOracleFile(solutionRepoUri, exerciseRepoUri, testRepoUri, testsPath, user); if (didGenerateOracle) { @@ -692,7 +694,7 @@ public ResponseEntity> getAuxiliaryRepositories(@PathV public ResponseEntity reset(@PathVariable Long exerciseId, @RequestBody ProgrammingExerciseResetOptionsDTO programmingExerciseResetOptionsDTO) throws JsonProcessingException { log.debug("REST request to reset programming exercise {} with options {}", exerciseId, programmingExerciseResetOptionsDTO); - var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesElseThrow(exerciseId); + var programmingExercise = programmingExerciseRepository.findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesAndBuildConfigElseThrow(exerciseId); final var user = userRepository.getUserWithGroupsAndAuthorities(); if (programmingExerciseResetOptionsDTO.recreateBuildPlans()) { diff --git a/src/main/resources/config/liquibase/changelog/20240626200000_changelog.xml b/src/main/resources/config/liquibase/changelog/20240626200000_changelog.xml new file mode 100644 index 000000000000..c231cb67da8d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240626200000_changelog.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO programming_exercise_build_config (id, sequential_test_runs, branch, build_plan_configuration, build_script, checkout_solution_repository, testwise_coverage_enabled, build_plan_access_secret) + SELECT exercise.id, exercise.sequential_test_runs, programming_exercise_details.branch, programming_exercise_details.build_plan_configuration, programming_exercise_details.build_script, programming_exercise_details.checkout_solution_repository, programming_exercise_details.testwise_coverage_enabled, programming_exercise_details.build_plan_access_secret + FROM exercise JOIN programming_exercise_details ON exercise.id = programming_exercise_details.id + WHERE exercise.discriminator = 'P'; + + UPDATE programming_exercise_details SET programming_exercise_build_config_id = ( + SELECT programming_exercise_build_config.id + FROM programming_exercise_build_config + WHERE programming_exercise_build_config.id = programming_exercise_details.id + ); + + + + + + + + + + + + + + + + + + + + + + DO $$ + DECLARE max_id BIGINT; + BEGIN + max_id := (SELECT (MAX(id)) as id FROM programming_exercise_build_config); + IF (max_id IS NOT NULL) THEN + EXECUTE 'ALTER SEQUENCE programming_exercise_build_config_id_seq RESTART WITH ' || (max_id + 1); + END IF; + END $$; + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index ab3037c89fad..841c3b42900e 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -18,6 +18,7 @@ + diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html index f7b76e97ede4..29ec5888ceae 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html @@ -238,7 +238,7 @@

{{ section [programmingExercise]="detail.data.exercise" [programmingLanguage]="detail.data.programmingLanguage" [isLocal]="detail.data.isLocal" - [checkoutSolutionRepository]="detail.data.exercise.checkoutSolutionRepository" + [checkoutSolutionRepository]="detail.data.exercise.buildConfig?.checkoutSolutionRepository" /> } diff --git a/src/main/webapp/app/entities/programming-exercise.model.ts b/src/main/webapp/app/entities/programming-exercise.model.ts index 5e36139c11cd..7b72d222cfc4 100644 --- a/src/main/webapp/app/entities/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming-exercise.model.ts @@ -58,6 +58,23 @@ export class WindFile { actions: BuildAction[]; } +export class ProgrammingExerciseBuildConfig { + public sequentialTestRuns?: boolean; + public buildPlanConfiguration?: string; + public buildScript?: string; + public checkoutSolutionRepository?: boolean; + public checkoutPath?: string; + public timeoutSeconds?: number; + public dockerFlags?: string; + public windFile?: WindFile; + public testwiseCoverageEnabled?: boolean; + + constructor() { + this.checkoutSolutionRepository = false; // default value + this.testwiseCoverageEnabled = false; // default value + } +} + export enum ProgrammingLanguage { JAVA = 'JAVA', PYTHON = 'PYTHON', @@ -98,25 +115,19 @@ export class ProgrammingExercise extends Exercise { public allowOfflineIde?: boolean; public programmingLanguage?: ProgrammingLanguage; public packageName?: string; - public sequentialTestRuns?: boolean; public showTestNamesToStudents?: boolean; - public checkoutSolutionRepository?: boolean; public auxiliaryRepositories?: AuxiliaryRepository[]; public submissionPolicy?: SubmissionPolicy; public exerciseHints?: ExerciseHint[]; public gitDiffReport?: ProgrammingExerciseGitDiffReport; public buildLogStatistics?: BuildLogStatisticsDTO; + public buildConfig?: ProgrammingExerciseBuildConfig; public releaseTestsWithExampleSolution?: boolean; public buildAndTestStudentSubmissionsAfterDueDate?: dayjs.Dayjs; public testCasesChanged?: boolean; public projectType?: ProjectType; - public windFile?: WindFile; - public buildScript?: string; - public buildPlanConfiguration?: string; - - public testwiseCoverageEnabled?: boolean; // helper attributes @@ -140,10 +151,9 @@ export class ProgrammingExercise extends Exercise { this.allowOfflineIde = true; // default value this.programmingLanguage = ProgrammingLanguage.JAVA; // default value this.noVersionControlAndContinuousIntegrationAvailable = false; // default value - this.checkoutSolutionRepository = false; // default value this.projectType = ProjectType.PLAIN_GRADLE; // default value this.showTestNamesToStudents = false; // default value - this.testwiseCoverageEnabled = false; // default value + this.buildConfig = new ProgrammingExerciseBuildConfig(); } } @@ -155,3 +165,21 @@ export function resetProgrammingForImport(exercise: ProgrammingExercise) { exercise.buildAndTestStudentSubmissionsAfterDueDate = undefined; exercise.assessmentType = AssessmentType.AUTOMATIC; } + +/** + * Copy the build configuration from the given exerciseJson to this build configuration. This is to ensure compatibility with old exported programming exercises. + */ +export function copyBuildConfigFromExerciseJson(exerciseJson: ProgrammingExerciseBuildConfig): ProgrammingExerciseBuildConfig { + const buildConfig = new ProgrammingExerciseBuildConfig(); + buildConfig.sequentialTestRuns = exerciseJson.sequentialTestRuns ?? false; + buildConfig.checkoutPath = exerciseJson.checkoutPath ?? ''; + buildConfig.buildPlanConfiguration = exerciseJson.buildPlanConfiguration ?? ''; + buildConfig.checkoutSolutionRepository = exerciseJson.checkoutSolutionRepository ?? false; + buildConfig.timeoutSeconds = exerciseJson.timeoutSeconds ?? 0; + buildConfig.windFile = exerciseJson.windFile ?? undefined; + buildConfig.buildScript = exerciseJson.buildScript ?? ''; + buildConfig.testwiseCoverageEnabled = exerciseJson.testwiseCoverageEnabled ?? false; + buildConfig.dockerFlags = exerciseJson.dockerFlags ?? ''; + + return buildConfig; +} diff --git a/src/main/webapp/app/exercises/programming/hestia/generation-overview/code-hint-generation-overview/code-hint-generation-overview.component.ts b/src/main/webapp/app/exercises/programming/hestia/generation-overview/code-hint-generation-overview/code-hint-generation-overview.component.ts index 9ff7fdce7e9e..56d9dffe846e 100644 --- a/src/main/webapp/app/exercises/programming/hestia/generation-overview/code-hint-generation-overview/code-hint-generation-overview.component.ts +++ b/src/main/webapp/app/exercises/programming/hestia/generation-overview/code-hint-generation-overview/code-hint-generation-overview.component.ts @@ -34,7 +34,7 @@ export class CodeHintGenerationOverviewComponent implements OnInit { this.isPerformedByStep = new Map(); this.isPerformedByStep.set(CodeHintGenerationStep.SOLUTION_ENTRIES, false); this.isPerformedByStep.set(CodeHintGenerationStep.CODE_HINTS, false); - if (exercise.testwiseCoverageEnabled) { + if (exercise.buildConfig?.testwiseCoverageEnabled) { this.currentStep = CodeHintGenerationStep.GIT_DIFF; this.allowBehavioralEntryGeneration = true; this.isPerformedByStep.set(CodeHintGenerationStep.GIT_DIFF, false); diff --git a/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.html b/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.html index b1610c41d457..433689205c2b 100644 --- a/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.html +++ b/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.html @@ -18,7 +18,7 @@ [ngbTooltip]="'artemisApp.codeHint.management.step3.structuralEntriesButton.tooltip' | artemisTranslate" (click)="onGenerateStructuralSolutionEntries()" > - @if (!!exercise?.testwiseCoverageEnabled) { + @if (!!exercise?.buildConfig?.testwiseCoverageEnabled) {